@vicket/create-support 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-vicket-support.js +429 -389
- package/package.json +1 -1
- package/templates/next/src/app/api/vicket/[...path]/route.ts +2 -55
- package/templates/next/src/app/components/vicket/ReplyForm.tsx +154 -0
- package/templates/next/src/app/components/vicket/SupportContent.tsx +298 -0
- package/templates/next/src/app/components/vicket/TicketDialog.tsx +3 -3
- package/templates/next/src/app/support/page.tsx +27 -353
- package/templates/next/src/app/ticket/page.tsx +110 -325
- package/templates/next/src/app/vicket.css +1325 -1325
- package/templates/nuxt/app/assets/css/vicket.css +1325 -1325
- package/templates/nuxt/app/components/VicketReplyForm.vue +154 -0
- package/templates/nuxt/app/components/VicketSupportContent.vue +255 -0
- package/templates/nuxt/app/components/VicketTicketDialog.vue +2 -2
- package/templates/nuxt/app/pages/support.vue +7 -293
- package/templates/nuxt/app/pages/ticket.vue +36 -178
- package/templates/nuxt/server/api/vicket/[...path].ts +2 -85
- package/templates/sveltekit/src/lib/vicket/ReplyForm.svelte +134 -0
- package/templates/sveltekit/src/lib/vicket/SupportContent.svelte +263 -0
- package/templates/sveltekit/src/lib/vicket/TicketDialog.svelte +457 -459
- package/templates/sveltekit/src/lib/vicket.css +1325 -1325
- package/templates/sveltekit/src/routes/api/vicket/[...path]/+server.ts +2 -76
- package/templates/sveltekit/src/routes/support/+page.server.ts +13 -0
- package/templates/sveltekit/src/routes/support/+page.svelte +3 -312
- package/templates/sveltekit/src/routes/ticket/+page.server.ts +19 -0
- package/templates/sveltekit/src/routes/ticket/+page.svelte +13 -188
- package/templates-tailwind/next/src/app/api/vicket/[...path]/route.ts +6 -0
- package/templates-tailwind/next/src/app/support/page.tsx +33 -3
- package/templates-tailwind/next/src/app/ticket/page.tsx +249 -6
- package/templates-tailwind/next/src/components/vicket/reply-form.tsx +113 -0
- package/templates-tailwind/next/src/components/vicket/support-content.tsx +265 -0
- package/templates-tailwind/next/src/components/vicket/ticket-dialog.tsx +2 -2
- package/templates-tailwind/nuxt/app/components/VicketReplyForm.vue +169 -0
- package/templates-tailwind/nuxt/app/components/{VicketSupportPage.vue → VicketSupportContent.vue} +275 -317
- package/templates-tailwind/nuxt/app/components/VicketTicketDialog.vue +3 -0
- package/templates-tailwind/nuxt/app/pages/support.vue +10 -1
- package/templates-tailwind/nuxt/app/pages/ticket.vue +298 -1
- package/templates-tailwind/nuxt/server/api/vicket/[...path].ts +2 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/ReplyForm.svelte +127 -0
- package/templates-tailwind/sveltekit/src/lib/vicket/{SupportPage.svelte → SupportContent.svelte} +9 -71
- package/templates-tailwind/sveltekit/src/lib/vicket/TicketDialog.svelte +405 -406
- package/templates-tailwind/sveltekit/src/routes/api/vicket/[...path]/+server.ts +3 -0
- package/templates-tailwind/sveltekit/src/routes/support/+page.server.ts +13 -0
- package/templates-tailwind/sveltekit/src/routes/support/+page.svelte +4 -2
- package/templates-tailwind/sveltekit/src/routes/ticket/+page.server.ts +19 -0
- package/templates-tailwind/sveltekit/src/routes/ticket/+page.svelte +292 -2
- package/templates/next/src/app/utils/vicket/api.ts +0 -149
- package/templates/next/src/app/utils/vicket/types.ts +0 -85
- package/templates/next/src/app/utils/vicket/utils.ts +0 -49
- package/templates/nuxt/app/composables/useVicket.ts +0 -274
- package/templates/sveltekit/src/lib/vicket/api.ts +0 -162
- package/templates/sveltekit/src/lib/vicket/types.ts +0 -87
- package/templates/sveltekit/src/lib/vicket/utils.ts +0 -55
- package/templates-tailwind/next/src/app/api/vicket/init/route.ts +0 -24
- package/templates-tailwind/next/src/app/api/vicket/messages/route.ts +0 -36
- package/templates-tailwind/next/src/app/api/vicket/thread/route.ts +0 -27
- package/templates-tailwind/next/src/app/api/vicket/tickets/route.ts +0 -37
- package/templates-tailwind/next/src/components/vicket/support-page.tsx +0 -359
- package/templates-tailwind/next/src/components/vicket/ticket-page.tsx +0 -425
- package/templates-tailwind/next/src/lib/vicket.ts +0 -257
- package/templates-tailwind/nuxt/app/components/VicketTicketPage.vue +0 -449
- package/templates-tailwind/nuxt/app/composables/use-vicket.ts +0 -249
- package/templates-tailwind/nuxt/server/api/vicket/init.get.ts +0 -22
- package/templates-tailwind/nuxt/server/api/vicket/messages.post.ts +0 -56
- package/templates-tailwind/nuxt/server/api/vicket/thread.get.ts +0 -26
- package/templates-tailwind/nuxt/server/api/vicket/tickets.post.ts +0 -53
- package/templates-tailwind/sveltekit/src/lib/vicket/TicketPage.svelte +0 -465
- package/templates-tailwind/sveltekit/src/lib/vicket/index.ts +0 -257
- package/templates-tailwind/sveltekit/src/routes/api/vicket/init/+server.ts +0 -22
- package/templates-tailwind/sveltekit/src/routes/api/vicket/messages/+server.ts +0 -40
- package/templates-tailwind/sveltekit/src/routes/api/vicket/thread/+server.ts +0 -25
- package/templates-tailwind/sveltekit/src/routes/api/vicket/tickets/+server.ts +0 -37
|
@@ -1,465 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { page } from "$app/state";
|
|
3
|
-
import type { TicketThread, Message } from "$lib/vicket";
|
|
4
|
-
import { fetchThread, sendReply, sanitizeHtml, stripHtml, formatDate, cn, isFileAnswer, formatAnswerText, AUTHOR_LABELS } from "$lib/vicket";
|
|
5
|
-
|
|
6
|
-
/* ---------------------------------------------- */
|
|
7
|
-
/* State */
|
|
8
|
-
/* ---------------------------------------------- */
|
|
9
|
-
let thread = $state<TicketThread | null>(null);
|
|
10
|
-
let content = $state("");
|
|
11
|
-
let files = $state<File[]>([]);
|
|
12
|
-
let isLoading = $state(true);
|
|
13
|
-
let isSending = $state(false);
|
|
14
|
-
let error = $state("");
|
|
15
|
-
let success = $state("");
|
|
16
|
-
|
|
17
|
-
/* ---------------------------------------------- */
|
|
18
|
-
/* Derived */
|
|
19
|
-
/* ---------------------------------------------- */
|
|
20
|
-
let token = $derived(page.url.searchParams.get("token") || "");
|
|
21
|
-
let hasToken = $derived(token.trim().length > 0);
|
|
22
|
-
|
|
23
|
-
let firstReporterMessage = $derived.by(() => {
|
|
24
|
-
if (!thread?.messages || thread.messages.length === 0) return null;
|
|
25
|
-
const sorted = [...thread.messages].sort(
|
|
26
|
-
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
|
|
27
|
-
);
|
|
28
|
-
// Only treat as description if the very first message is from the reporter
|
|
29
|
-
return sorted[0].author_type === "reporter" ? sorted[0] : null;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
let sortedMessages = $derived.by(() => {
|
|
33
|
-
if (!thread?.messages) return [];
|
|
34
|
-
return [...thread.messages]
|
|
35
|
-
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
|
36
|
-
.filter((m) => !firstReporterMessage || m.id !== firstReporterMessage.id);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
let summaryAnswers = $derived.by(() => {
|
|
40
|
-
if (!thread?.answers) return [];
|
|
41
|
-
return thread.answers.filter((answer) => {
|
|
42
|
-
if (answer.attachments && answer.attachments.length > 0) return true;
|
|
43
|
-
if (answer.answer && answer.answer.trim().length > 0) return true;
|
|
44
|
-
return false;
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/* ---------------------------------------------- */
|
|
49
|
-
/* Functions */
|
|
50
|
-
/* ---------------------------------------------- */
|
|
51
|
-
function removeFile(index: number) {
|
|
52
|
-
files = files.filter((_, i) => i !== index);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function onFileChange(event: Event) {
|
|
56
|
-
const input = event.target as HTMLInputElement;
|
|
57
|
-
const newFiles = Array.from(input.files || []);
|
|
58
|
-
files = [...files, ...newFiles];
|
|
59
|
-
input.value = "";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function loadThread() {
|
|
63
|
-
if (!hasToken) {
|
|
64
|
-
isLoading = false;
|
|
65
|
-
error = "Missing ticket token in URL.";
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
isLoading = true;
|
|
70
|
-
error = "";
|
|
71
|
-
try {
|
|
72
|
-
const data = await fetchThread(token);
|
|
73
|
-
thread = data;
|
|
74
|
-
} catch (loadError) {
|
|
75
|
-
error = loadError instanceof Error ? loadError.message : "Unexpected error.";
|
|
76
|
-
} finally {
|
|
77
|
-
isLoading = false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function onSubmitReply(event: SubmitEvent) {
|
|
82
|
-
event.preventDefault();
|
|
83
|
-
error = "";
|
|
84
|
-
success = "";
|
|
85
|
-
|
|
86
|
-
if (!content.trim() && files.length === 0) {
|
|
87
|
-
error = "Reply content is required.";
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!hasToken) {
|
|
92
|
-
error = "Missing ticket token.";
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
isSending = true;
|
|
97
|
-
try {
|
|
98
|
-
await sendReply(token, content.trim(), files);
|
|
99
|
-
content = "";
|
|
100
|
-
files = [];
|
|
101
|
-
success = "Reply sent.";
|
|
102
|
-
await loadThread();
|
|
103
|
-
} catch (replyError) {
|
|
104
|
-
error = replyError instanceof Error ? replyError.message : "Unexpected error.";
|
|
105
|
-
} finally {
|
|
106
|
-
isSending = false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/* ---------------------------------------------- */
|
|
111
|
-
/* Load on mount */
|
|
112
|
-
/* ---------------------------------------------- */
|
|
113
|
-
$effect(() => {
|
|
114
|
-
void loadThread();
|
|
115
|
-
});
|
|
116
|
-
</script>
|
|
117
|
-
|
|
118
|
-
<div class="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
|
|
119
|
-
<div class="mx-auto max-w-4xl px-6 py-16">
|
|
120
|
-
<!-- Back link -->
|
|
121
|
-
<div class="mb-6">
|
|
122
|
-
<a href="/support" class="inline-flex items-center gap-1.5 text-sm font-medium text-slate-500 no-underline transition-colors hover:text-slate-900">
|
|
123
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg>
|
|
124
|
-
Back to support
|
|
125
|
-
</a>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<!-- Alerts -->
|
|
129
|
-
{#if error}
|
|
130
|
-
<div class="mb-6 flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900" role="alert">
|
|
131
|
-
<span class="mt-0.5 shrink-0">
|
|
132
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
|
|
133
|
-
</span>
|
|
134
|
-
<span class="flex-1">{error}</span>
|
|
135
|
-
<button type="button" onclick={() => (error = "")} class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100" aria-label="Dismiss">
|
|
136
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
137
|
-
</button>
|
|
138
|
-
</div>
|
|
139
|
-
{/if}
|
|
140
|
-
{#if success}
|
|
141
|
-
<div class="mb-6 flex items-start gap-3 rounded-xl border border-green-200 bg-green-50 p-4 text-sm text-green-900" role="alert">
|
|
142
|
-
<span class="mt-0.5 shrink-0">
|
|
143
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
|
|
144
|
-
</span>
|
|
145
|
-
<span class="flex-1">{success}</span>
|
|
146
|
-
<button type="button" onclick={() => (success = "")} class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100" aria-label="Dismiss">
|
|
147
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
148
|
-
</button>
|
|
149
|
-
</div>
|
|
150
|
-
{/if}
|
|
151
|
-
|
|
152
|
-
<!-- Loading skeleton -->
|
|
153
|
-
{#if isLoading}
|
|
154
|
-
<div class="space-y-6">
|
|
155
|
-
<!-- Header skeleton -->
|
|
156
|
-
<div>
|
|
157
|
-
<div class="h-8 w-80 animate-pulse rounded-lg bg-slate-200" aria-hidden="true"></div>
|
|
158
|
-
<div class="mt-3 flex gap-2">
|
|
159
|
-
<div class="h-6 w-16 animate-pulse rounded-full bg-slate-200" aria-hidden="true"></div>
|
|
160
|
-
<div class="h-6 w-20 animate-pulse rounded-full bg-slate-200" aria-hidden="true"></div>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
<!-- Messages skeleton -->
|
|
164
|
-
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
165
|
-
<div class="border-b border-slate-100 px-5 py-3">
|
|
166
|
-
<div class="h-5 w-24 animate-pulse rounded bg-slate-200" aria-hidden="true"></div>
|
|
167
|
-
</div>
|
|
168
|
-
{#each [1, 2, 3] as i (i)}
|
|
169
|
-
<div class="flex gap-3 border-b border-slate-50 px-5 py-4 last:border-b-0">
|
|
170
|
-
<div class="h-8 w-8 shrink-0 animate-pulse rounded-full bg-slate-200" aria-hidden="true"></div>
|
|
171
|
-
<div class="flex-1 space-y-2">
|
|
172
|
-
<div class="h-4 w-32 animate-pulse rounded bg-slate-200" aria-hidden="true"></div>
|
|
173
|
-
<div class="h-4 w-full animate-pulse rounded bg-slate-200" aria-hidden="true"></div>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
{/each}
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
{/if}
|
|
180
|
-
|
|
181
|
-
<!-- Thread -->
|
|
182
|
-
{#if !isLoading && thread}
|
|
183
|
-
<div class="space-y-6">
|
|
184
|
-
<!-- Header -->
|
|
185
|
-
<div>
|
|
186
|
-
<div class="flex items-start gap-3">
|
|
187
|
-
<h1 class="m-0 flex-1 text-xl font-bold text-slate-900 md:text-2xl">{thread.title}</h1>
|
|
188
|
-
{#if thread.id}
|
|
189
|
-
<span class="shrink-0 rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-500">
|
|
190
|
-
#{thread.id.slice(0, 8)}
|
|
191
|
-
</span>
|
|
192
|
-
{/if}
|
|
193
|
-
</div>
|
|
194
|
-
{#if (thread.status?.label && thread.status.label.toLowerCase() !== "open") || (thread.priority?.label && thread.priority.label.toLowerCase() !== "low")}
|
|
195
|
-
<div class="mt-3 flex flex-wrap gap-2">
|
|
196
|
-
{#if thread.status?.label && thread.status.label.toLowerCase() !== "open"}
|
|
197
|
-
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-700">
|
|
198
|
-
{thread.status.label}
|
|
199
|
-
</span>
|
|
200
|
-
{/if}
|
|
201
|
-
{#if thread.priority?.label && thread.priority.label.toLowerCase() !== "low"}
|
|
202
|
-
<span class="rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700">
|
|
203
|
-
{thread.priority.label}
|
|
204
|
-
</span>
|
|
205
|
-
{/if}
|
|
206
|
-
</div>
|
|
207
|
-
{/if}
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
<!-- Summary (ticket form answers) -->
|
|
211
|
-
{#if summaryAnswers.length > 0}
|
|
212
|
-
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
213
|
-
<div class="flex items-center gap-2 border-b border-slate-100 px-5 py-3">
|
|
214
|
-
<span class="text-slate-400">
|
|
215
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /><path d="M16 13H8" /><path d="M16 17H8" /><path d="M10 9H8" /></svg>
|
|
216
|
-
</span>
|
|
217
|
-
<h2 class="m-0 text-sm font-semibold text-slate-900">Summary</h2>
|
|
218
|
-
</div>
|
|
219
|
-
<div class="divide-y divide-slate-50 px-5">
|
|
220
|
-
{#each summaryAnswers as answer (answer.id)}
|
|
221
|
-
<div class="py-3">
|
|
222
|
-
<p class="m-0 text-xs font-semibold uppercase tracking-wider text-slate-400">{answer.question_label || "Question"}</p>
|
|
223
|
-
{#if answer.attachments && answer.attachments.length > 0}
|
|
224
|
-
<div class="mt-1.5 flex flex-wrap gap-1.5">
|
|
225
|
-
{#each answer.attachments as attachment (attachment.id)}
|
|
226
|
-
<a
|
|
227
|
-
class="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs text-slate-600 no-underline transition-colors hover:bg-slate-100"
|
|
228
|
-
href={attachment.url}
|
|
229
|
-
rel="noopener noreferrer"
|
|
230
|
-
target="_blank"
|
|
231
|
-
>
|
|
232
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" /></svg>
|
|
233
|
-
{attachment.original_filename}
|
|
234
|
-
</a>
|
|
235
|
-
{/each}
|
|
236
|
-
</div>
|
|
237
|
-
{:else if isFileAnswer(answer.answer)}
|
|
238
|
-
<p class="m-0 mt-1 text-sm italic text-slate-400">File uploaded</p>
|
|
239
|
-
{:else}
|
|
240
|
-
<p class="m-0 mt-1 text-sm text-slate-700">{formatAnswerText(answer.answer) || "-"}</p>
|
|
241
|
-
{/if}
|
|
242
|
-
</div>
|
|
243
|
-
{/each}
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
246
|
-
{/if}
|
|
247
|
-
|
|
248
|
-
<!-- Description (first reporter message) -->
|
|
249
|
-
{#if firstReporterMessage}
|
|
250
|
-
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
251
|
-
<div class="flex items-center gap-2 border-b border-slate-100 px-5 py-3">
|
|
252
|
-
<span class="text-slate-400">
|
|
253
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" /><path d="M14 2v6h6" /><path d="M16 13H8" /><path d="M16 17H8" /><path d="M10 9H8" /></svg>
|
|
254
|
-
</span>
|
|
255
|
-
<h2 class="m-0 text-sm font-semibold text-slate-900">Description</h2>
|
|
256
|
-
</div>
|
|
257
|
-
<div class="px-5 py-4">
|
|
258
|
-
<div class="vk-message-content text-sm leading-relaxed text-slate-700">
|
|
259
|
-
{@html sanitizeHtml(firstReporterMessage.content)}
|
|
260
|
-
</div>
|
|
261
|
-
{#if firstReporterMessage.attachments && firstReporterMessage.attachments.length > 0}
|
|
262
|
-
<div class="mt-3 flex flex-wrap gap-1.5">
|
|
263
|
-
{#each firstReporterMessage.attachments as att (att.id)}
|
|
264
|
-
<a
|
|
265
|
-
href={att.url}
|
|
266
|
-
target="_blank"
|
|
267
|
-
rel="noopener noreferrer"
|
|
268
|
-
class="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs text-slate-600 no-underline transition-colors hover:bg-slate-100"
|
|
269
|
-
>
|
|
270
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" /></svg>
|
|
271
|
-
{att.original_filename}
|
|
272
|
-
</a>
|
|
273
|
-
{/each}
|
|
274
|
-
</div>
|
|
275
|
-
{/if}
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
{/if}
|
|
279
|
-
|
|
280
|
-
<!-- Comments card -->
|
|
281
|
-
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
282
|
-
<div class="flex items-center gap-2 border-b border-slate-100 px-5 py-3">
|
|
283
|
-
<span class="text-slate-400">
|
|
284
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
|
|
285
|
-
</span>
|
|
286
|
-
<h2 class="m-0 text-sm font-semibold text-slate-900">Comments</h2>
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
<!-- Compose area -->
|
|
290
|
-
<div class="border-b border-slate-100 px-5 py-4">
|
|
291
|
-
<form class="space-y-3" onsubmit={onSubmitReply}>
|
|
292
|
-
<textarea
|
|
293
|
-
class="min-h-[80px] w-full resize-y rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 transition-all duration-150 placeholder:text-slate-500/60 focus:border-blue-600 focus:outline-none focus:ring-3 focus:ring-blue-600/10"
|
|
294
|
-
bind:value={content}
|
|
295
|
-
placeholder="Write your reply..."
|
|
296
|
-
></textarea>
|
|
297
|
-
|
|
298
|
-
<div class="flex items-center justify-between gap-3">
|
|
299
|
-
<!-- File input -->
|
|
300
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
301
|
-
<label class="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:bg-slate-50">
|
|
302
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" /></svg>
|
|
303
|
-
Browse files
|
|
304
|
-
<input type="file" multiple class="hidden" onchange={onFileChange} />
|
|
305
|
-
</label>
|
|
306
|
-
{#each files as file, i}
|
|
307
|
-
<span class="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs text-slate-500">
|
|
308
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" /></svg>
|
|
309
|
-
<span class="max-w-[120px] truncate">{file.name}</span>
|
|
310
|
-
<button
|
|
311
|
-
type="button"
|
|
312
|
-
onclick={() => removeFile(i)}
|
|
313
|
-
class="flex cursor-pointer items-center border-none bg-transparent p-0 text-slate-400 transition-colors hover:text-red-600"
|
|
314
|
-
aria-label="Remove {file.name}"
|
|
315
|
-
>
|
|
316
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
317
|
-
</button>
|
|
318
|
-
</span>
|
|
319
|
-
{/each}
|
|
320
|
-
</div>
|
|
321
|
-
|
|
322
|
-
<!-- Send -->
|
|
323
|
-
<button
|
|
324
|
-
class="inline-flex shrink-0 items-center gap-2 rounded-lg border-none bg-blue-600 px-5 py-2 text-sm font-semibold text-white cursor-pointer transition-all hover:bg-blue-700 hover:-translate-y-px hover:shadow-lg active:translate-y-0 disabled:opacity-60 disabled:cursor-not-allowed"
|
|
325
|
-
disabled={isSending}
|
|
326
|
-
type="submit"
|
|
327
|
-
>
|
|
328
|
-
{#if isSending}
|
|
329
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
|
|
330
|
-
Sending...
|
|
331
|
-
{:else}
|
|
332
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" /><path d="m21.854 2.147-10.94 10.939" /></svg>
|
|
333
|
-
Send
|
|
334
|
-
{/if}
|
|
335
|
-
</button>
|
|
336
|
-
</div>
|
|
337
|
-
</form>
|
|
338
|
-
</div>
|
|
339
|
-
|
|
340
|
-
<!-- Message list -->
|
|
341
|
-
{#if sortedMessages.length === 0}
|
|
342
|
-
<div class="px-5 py-10 text-center">
|
|
343
|
-
<div class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-slate-100 text-slate-400">
|
|
344
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
|
|
345
|
-
</div>
|
|
346
|
-
<p class="m-0 text-sm text-slate-500">No messages yet. Be the first to reply!</p>
|
|
347
|
-
</div>
|
|
348
|
-
{:else}
|
|
349
|
-
<div class="divide-y divide-slate-50">
|
|
350
|
-
{#each sortedMessages as message (message.id)}
|
|
351
|
-
{#if message.author_type === "system"}
|
|
352
|
-
<!-- System message -->
|
|
353
|
-
<div class="flex gap-3 px-5 py-4">
|
|
354
|
-
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-slate-100 text-xs font-bold text-slate-500" aria-hidden="true">
|
|
355
|
-
S
|
|
356
|
-
</div>
|
|
357
|
-
<div class="min-w-0 flex-1">
|
|
358
|
-
<div class="flex items-center gap-2">
|
|
359
|
-
<span class="text-xs font-semibold italic text-slate-400">System</span>
|
|
360
|
-
<span class="text-xs text-slate-400">{formatDate(message.created_at)}</span>
|
|
361
|
-
</div>
|
|
362
|
-
<p class="m-0 mt-1 text-sm italic text-slate-500">
|
|
363
|
-
{stripHtml(message.content)}
|
|
364
|
-
</p>
|
|
365
|
-
</div>
|
|
366
|
-
</div>
|
|
367
|
-
{:else}
|
|
368
|
-
<!-- Regular message -->
|
|
369
|
-
<div class="flex gap-3 px-5 py-4">
|
|
370
|
-
<div class={cn(
|
|
371
|
-
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold",
|
|
372
|
-
message.author_type === "reporter" ? "bg-blue-100 text-blue-700" : "bg-green-100 text-green-700"
|
|
373
|
-
)} aria-hidden="true">
|
|
374
|
-
{(AUTHOR_LABELS[message.author_type] || "?")[0]}
|
|
375
|
-
</div>
|
|
376
|
-
<div class="min-w-0 flex-1">
|
|
377
|
-
<div class="flex items-center gap-2">
|
|
378
|
-
<span class="text-xs font-semibold text-slate-900">{AUTHOR_LABELS[message.author_type] || message.author_type}</span>
|
|
379
|
-
<span class="text-xs text-slate-400">{formatDate(message.created_at)}</span>
|
|
380
|
-
</div>
|
|
381
|
-
<div class={cn(
|
|
382
|
-
"mt-1.5 rounded-xl px-4 py-3 text-sm leading-relaxed",
|
|
383
|
-
message.author_type === "user" ? "bg-green-50 text-slate-800" : "bg-slate-50 text-slate-800"
|
|
384
|
-
)}>
|
|
385
|
-
<div class="vk-message-content">
|
|
386
|
-
{@html sanitizeHtml(message.content)}
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
{#if message.attachments && message.attachments.length > 0}
|
|
390
|
-
<div class="mt-2 flex flex-wrap gap-1.5">
|
|
391
|
-
{#each message.attachments as attachment (attachment.id)}
|
|
392
|
-
<a
|
|
393
|
-
class="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs text-slate-600 no-underline transition-colors hover:bg-slate-100"
|
|
394
|
-
href={attachment.url}
|
|
395
|
-
rel="noopener noreferrer"
|
|
396
|
-
target="_blank"
|
|
397
|
-
>
|
|
398
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" /></svg>
|
|
399
|
-
{attachment.original_filename}
|
|
400
|
-
</a>
|
|
401
|
-
{/each}
|
|
402
|
-
</div>
|
|
403
|
-
{/if}
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
406
|
-
{/if}
|
|
407
|
-
{/each}
|
|
408
|
-
</div>
|
|
409
|
-
{/if}
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
{/if}
|
|
413
|
-
</div>
|
|
414
|
-
</div>
|
|
415
|
-
|
|
416
|
-
<style>
|
|
417
|
-
:global(.vk-message-content h1,
|
|
418
|
-
.vk-message-content h2,
|
|
419
|
-
.vk-message-content h3,
|
|
420
|
-
.vk-message-content h4,
|
|
421
|
-
.vk-message-content h5,
|
|
422
|
-
.vk-message-content h6) {
|
|
423
|
-
margin: 1em 0 0.5em;
|
|
424
|
-
line-height: 1.4;
|
|
425
|
-
}
|
|
426
|
-
:global(.vk-message-content p) {
|
|
427
|
-
margin: 0.5em 0;
|
|
428
|
-
}
|
|
429
|
-
:global(.vk-message-content ul,
|
|
430
|
-
.vk-message-content ol) {
|
|
431
|
-
margin: 0.5em 0;
|
|
432
|
-
padding-left: 1.5em;
|
|
433
|
-
}
|
|
434
|
-
:global(.vk-message-content a) {
|
|
435
|
-
color: #2563eb;
|
|
436
|
-
text-decoration: underline;
|
|
437
|
-
}
|
|
438
|
-
:global(.vk-message-content img) {
|
|
439
|
-
max-width: 100%;
|
|
440
|
-
border-radius: 0.5rem;
|
|
441
|
-
}
|
|
442
|
-
:global(.vk-message-content pre) {
|
|
443
|
-
background: #f1f5f9;
|
|
444
|
-
padding: 0.75em 1em;
|
|
445
|
-
border-radius: 0.5rem;
|
|
446
|
-
overflow-x: auto;
|
|
447
|
-
font-size: 0.85em;
|
|
448
|
-
}
|
|
449
|
-
:global(.vk-message-content code) {
|
|
450
|
-
background: #f1f5f9;
|
|
451
|
-
padding: 0.15em 0.35em;
|
|
452
|
-
border-radius: 0.25rem;
|
|
453
|
-
font-size: 0.9em;
|
|
454
|
-
}
|
|
455
|
-
:global(.vk-message-content pre code) {
|
|
456
|
-
background: none;
|
|
457
|
-
padding: 0;
|
|
458
|
-
}
|
|
459
|
-
:global(.vk-message-content blockquote) {
|
|
460
|
-
border-left: 3px solid #cbd5e1;
|
|
461
|
-
margin: 0.5em 0;
|
|
462
|
-
padding-left: 1em;
|
|
463
|
-
color: #64748b;
|
|
464
|
-
}
|
|
465
|
-
</style>
|