ComposeView.svelte raw
1 <script>
2 export let composeEventJson = "";
3 export let userPubkey = "";
4 export let userRole = "";
5 export let policyEnabled = false;
6 export let publishError = "";
7 export let localOnly = true;
8
9 import { createEventDispatcher } from "svelte";
10 import EventTemplateSelector from "./EventTemplateSelector.svelte";
11
12 const dispatch = createEventDispatcher();
13
14 let isTemplateSelectorOpen = false;
15
16 function reformatJson() {
17 dispatch("reformatJson");
18 }
19
20 function signEvent() {
21 dispatch("signEvent");
22 }
23
24 function publishEvent() {
25 dispatch("publishEvent");
26 }
27
28 function openTemplateSelector() {
29 isTemplateSelectorOpen = true;
30 }
31
32 function handleTemplateSelect(event) {
33 const { kind, template } = event.detail;
34 composeEventJson = JSON.stringify(template, null, 2);
35 dispatch("templateSelected", { kind, template });
36 }
37
38 function handleTemplateSelectorClose() {
39 isTemplateSelectorOpen = false;
40 }
41
42 function clearError() {
43 publishError = "";
44 dispatch("clearError");
45 }
46 </script>
47
48 <div class="compose-view">
49 <div class="compose-header">
50 <button class="compose-btn template-btn" on:click={openTemplateSelector}
51 >Generate Template</button
52 >
53 <button class="compose-btn reformat-btn" on:click={reformatJson}
54 >Reformat</button
55 >
56 <button class="compose-btn sign-btn" on:click={signEvent}>Sign</button>
57 <label class="local-only-label">
58 <input type="checkbox" bind:checked={localOnly} />
59 This relay only
60 </label>
61 <button class="compose-btn publish-btn" on:click={publishEvent}
62 >Publish</button
63 >
64 </div>
65
66 {#if publishError}
67 <div class="error-banner">
68 <div class="error-content">
69 <span class="error-icon">⚠</span>
70 <span class="error-message">{publishError}</span>
71 </div>
72 <button class="error-dismiss" on:click={clearError}>×</button>
73 </div>
74 {/if}
75
76 <div class="compose-editor">
77 <textarea
78 bind:value={composeEventJson}
79 class="compose-textarea"
80 placeholder="Enter your Nostr event JSON here, or click 'Generate Template' to start with a template..."
81 spellcheck="false"
82 ></textarea>
83 </div>
84 </div>
85
86 <EventTemplateSelector
87 bind:isOpen={isTemplateSelectorOpen}
88 {userPubkey}
89 on:select={handleTemplateSelect}
90 on:close={handleTemplateSelectorClose}
91 />
92
93 <style>
94 .compose-view {
95 position: fixed;
96 top: 3em;
97 left: 200px;
98 right: 0;
99 bottom: 0;
100 display: flex;
101 flex-direction: column;
102 background: transparent;
103 }
104
105 .compose-header {
106 display: flex;
107 gap: 0.5em;
108 padding: 0.5em;
109 background: transparent;
110 }
111
112 .compose-btn {
113 padding: 0.5em 1em;
114 border: 1px solid var(--border-color);
115 border-radius: 0.25rem;
116 background: var(--button-bg);
117 color: var(--button-text);
118 cursor: pointer;
119 font-size: 0.9rem;
120 transition: background-color 0.2s;
121 }
122
123 .compose-btn:hover {
124 background: var(--button-hover-bg);
125 }
126
127 .template-btn {
128 background: var(--primary);
129 color: var(--text-color);
130 }
131
132 .template-btn:hover {
133 background: var(--primary);
134 filter: brightness(0.9);
135 }
136
137 .reformat-btn {
138 background: var(--info);
139 color: var(--text-color);
140 }
141
142 .reformat-btn:hover {
143 background: var(--info);
144 filter: brightness(0.9);
145 }
146
147 .sign-btn {
148 background: var(--warning);
149 color: var(--text-color);
150 }
151
152 .sign-btn:hover {
153 background: var(--warning);
154 filter: brightness(0.9);
155 }
156
157 .local-only-label {
158 display: flex;
159 align-items: center;
160 gap: 0.35em;
161 font-size: 0.85rem;
162 color: var(--text-color);
163 cursor: pointer;
164 user-select: none;
165 margin-left: auto;
166 padding: 0 0.5em;
167 white-space: nowrap;
168 }
169
170 .local-only-label input[type="checkbox"] {
171 cursor: pointer;
172 accent-color: var(--accent-color);
173 }
174
175 .publish-btn {
176 background: var(--success);
177 color: var(--text-color);
178 }
179
180 .publish-btn:hover {
181 background: var(--success);
182 filter: brightness(0.9);
183 }
184
185 .error-banner {
186 display: flex;
187 align-items: center;
188 justify-content: space-between;
189 padding: 0.75em 1em;
190 margin: 0 0.5em;
191 background: #f8d7da;
192 border: 1px solid #f5c6cb;
193 border-radius: 0.25rem;
194 color: #721c24;
195 }
196
197 :global(.dark-theme) .error-banner {
198 background: #4a1c24;
199 border-color: #6a2c34;
200 color: #f8d7da;
201 }
202
203 .error-content {
204 display: flex;
205 align-items: center;
206 gap: 0.5em;
207 }
208
209 .error-icon {
210 font-size: 1.2em;
211 }
212
213 .error-message {
214 font-size: 0.9rem;
215 line-height: 1.4;
216 }
217
218 .error-dismiss {
219 background: none;
220 border: none;
221 font-size: 1.25rem;
222 cursor: pointer;
223 color: inherit;
224 padding: 0 0.25em;
225 opacity: 0.7;
226 }
227
228 .error-dismiss:hover {
229 opacity: 1;
230 }
231
232 .compose-editor {
233 flex: 1;
234 display: flex;
235 flex-direction: column;
236 padding: 0;
237 }
238
239 .compose-textarea {
240 flex: 1;
241 width: 100%;
242 padding: 1em;
243 border-radius: 0.5em;
244 background: var(--input-bg);
245 color: var(--input-text-color);
246 font-family: monospace;
247 font-size: 0.9em;
248 line-height: 1.4;
249 resize: vertical;
250 outline: none;
251 }
252
253 .compose-textarea:focus {
254 border-color: var(--accent-color);
255 box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
256 }
257
258 @media (max-width: 1280px) {
259 .compose-view {
260 left: 60px;
261 }
262 }
263
264 @media (max-width: 640px) {
265 .compose-view {
266 left: 0;
267 }
268
269 .compose-header {
270 flex-wrap: wrap;
271 }
272
273 .compose-btn {
274 flex: 1;
275 min-width: calc(50% - 0.5em);
276 }
277 }
278 </style>
279