@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,449 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/* ── Reactive state ─────────────────────────────── */
|
|
3
|
-
const route = useRoute();
|
|
4
|
-
const token = computed(() => (route.query.token as string) || "");
|
|
5
|
-
const hasToken = computed(() => token.value.trim().length > 0);
|
|
6
|
-
|
|
7
|
-
const thread = ref<TicketThread | null>(null);
|
|
8
|
-
const content = ref("");
|
|
9
|
-
const files = ref<File[]>([]);
|
|
10
|
-
const isLoading = ref(true);
|
|
11
|
-
const isSending = ref(false);
|
|
12
|
-
const error = ref("");
|
|
13
|
-
const success = ref("");
|
|
14
|
-
|
|
15
|
-
/* ── Computed ───────────────────────────────────── */
|
|
16
|
-
const firstReporterMessage = computed(() => {
|
|
17
|
-
if (!thread.value?.messages || thread.value.messages.length === 0) return null;
|
|
18
|
-
const sorted = [...thread.value.messages].sort(
|
|
19
|
-
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
|
|
20
|
-
);
|
|
21
|
-
// Only treat as description if the very first message is from the reporter
|
|
22
|
-
return sorted[0].author_type === "reporter" ? sorted[0] : null;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const sortedMessages = computed(() => {
|
|
26
|
-
if (!thread.value?.messages) return [];
|
|
27
|
-
return [...thread.value.messages]
|
|
28
|
-
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
|
29
|
-
.filter((m) => !firstReporterMessage.value || m.id !== firstReporterMessage.value.id);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const summaryAnswers = computed(() => {
|
|
33
|
-
if (!thread.value?.answers) return [];
|
|
34
|
-
return thread.value.answers.filter((answer) => {
|
|
35
|
-
if (answer.attachments && answer.attachments.length > 0) return true;
|
|
36
|
-
if (answer.answer && answer.answer.trim().length > 0) return true;
|
|
37
|
-
return false;
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
/* ── Helpers ────────────────────────────────────── */
|
|
42
|
-
const removeFile = (index: number) => {
|
|
43
|
-
files.value = files.value.filter((_, i) => i !== index);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const onFileChange = (event: Event) => {
|
|
47
|
-
const input = event.target as HTMLInputElement;
|
|
48
|
-
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
49
|
-
const newFiles = Array.from(input.files || []).filter((f) => ALLOWED_TYPES.includes(f.type));
|
|
50
|
-
files.value = [...files.value, ...newFiles];
|
|
51
|
-
input.value = "";
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const avatarColors = (authorType: string) =>
|
|
55
|
-
authorType === "reporter"
|
|
56
|
-
? "bg-blue-600/15 text-blue-600"
|
|
57
|
-
: authorType === "user"
|
|
58
|
-
? "bg-emerald-500/15 text-emerald-500"
|
|
59
|
-
: "bg-slate-400/20 text-slate-400";
|
|
60
|
-
|
|
61
|
-
/* ── Data loading ───────────────────────────────── */
|
|
62
|
-
const loadThread = async () => {
|
|
63
|
-
if (!hasToken.value) {
|
|
64
|
-
isLoading.value = false;
|
|
65
|
-
error.value = "Missing ticket token in URL.";
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
isLoading.value = true;
|
|
69
|
-
error.value = "";
|
|
70
|
-
try {
|
|
71
|
-
const data = await fetchThread(token.value);
|
|
72
|
-
thread.value = data;
|
|
73
|
-
} catch (loadError) {
|
|
74
|
-
error.value = loadError instanceof Error ? loadError.message : "Unexpected error.";
|
|
75
|
-
} finally {
|
|
76
|
-
isLoading.value = false;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const submitReply = async () => {
|
|
81
|
-
error.value = "";
|
|
82
|
-
success.value = "";
|
|
83
|
-
if (!content.value.trim() && files.value.length === 0) {
|
|
84
|
-
error.value = "Reply content is required.";
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (!hasToken.value) {
|
|
88
|
-
error.value = "Missing ticket token.";
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
isSending.value = true;
|
|
92
|
-
try {
|
|
93
|
-
await sendReply(token.value, content.value.trim(), files.value);
|
|
94
|
-
content.value = "";
|
|
95
|
-
files.value = [];
|
|
96
|
-
success.value = "Reply sent.";
|
|
97
|
-
await loadThread();
|
|
98
|
-
} catch (replyError) {
|
|
99
|
-
error.value = replyError instanceof Error ? replyError.message : "Unexpected error.";
|
|
100
|
-
} finally {
|
|
101
|
-
isSending.value = false;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
watch(
|
|
106
|
-
() => token.value,
|
|
107
|
-
() => { void loadThread(); },
|
|
108
|
-
{ immediate: true },
|
|
109
|
-
);
|
|
110
|
-
</script>
|
|
111
|
-
|
|
112
|
-
<template>
|
|
113
|
-
<div class="min-h-screen bg-slate-50 font-[system-ui,-apple-system,sans-serif] antialiased">
|
|
114
|
-
<div class="mx-auto w-full max-w-3xl px-4 py-8">
|
|
115
|
-
<!-- Back link -->
|
|
116
|
-
<div class="mb-6">
|
|
117
|
-
<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">
|
|
118
|
-
<!-- IconArrowLeft -->
|
|
119
|
-
<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>
|
|
120
|
-
Back to support
|
|
121
|
-
</a>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<!-- Error alert -->
|
|
125
|
-
<div v-if="error" class="mb-4">
|
|
126
|
-
<div
|
|
127
|
-
class="flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-900"
|
|
128
|
-
role="alert"
|
|
129
|
-
>
|
|
130
|
-
<span class="mt-0.5 shrink-0">
|
|
131
|
-
<!-- IconAlert -->
|
|
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
|
|
136
|
-
type="button"
|
|
137
|
-
class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100"
|
|
138
|
-
aria-label="Dismiss"
|
|
139
|
-
@click="error = ''"
|
|
140
|
-
>
|
|
141
|
-
<!-- IconX -->
|
|
142
|
-
<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>
|
|
143
|
-
</button>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
<!-- Success alert -->
|
|
148
|
-
<div v-if="success" class="mb-4">
|
|
149
|
-
<div
|
|
150
|
-
class="flex items-start gap-3 rounded-xl border border-green-200 bg-green-50 p-4 text-sm text-green-900"
|
|
151
|
-
role="alert"
|
|
152
|
-
>
|
|
153
|
-
<span class="mt-0.5 shrink-0">
|
|
154
|
-
<!-- IconCheck -->
|
|
155
|
-
<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>
|
|
156
|
-
</span>
|
|
157
|
-
<span class="flex-1">{{ success }}</span>
|
|
158
|
-
<button
|
|
159
|
-
type="button"
|
|
160
|
-
class="shrink-0 cursor-pointer border-none bg-transparent p-0 opacity-50 transition-opacity hover:opacity-100"
|
|
161
|
-
aria-label="Dismiss"
|
|
162
|
-
@click="success = ''"
|
|
163
|
-
>
|
|
164
|
-
<!-- IconX -->
|
|
165
|
-
<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>
|
|
166
|
-
</button>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
<!-- Loading skeleton -->
|
|
171
|
-
<div v-if="isLoading" class="space-y-6">
|
|
172
|
-
<div>
|
|
173
|
-
<div class="mb-3 h-7 w-64 animate-pulse rounded-lg bg-slate-200" />
|
|
174
|
-
<div class="flex gap-2">
|
|
175
|
-
<div class="h-6 w-20 animate-pulse rounded-full bg-slate-200" />
|
|
176
|
-
<div class="h-6 w-16 animate-pulse rounded-full bg-slate-200" />
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
<div class="rounded-2xl border border-slate-200 bg-white">
|
|
180
|
-
<div class="border-b border-slate-200 px-5 py-4">
|
|
181
|
-
<div class="h-5 w-28 animate-pulse rounded bg-slate-200" />
|
|
182
|
-
</div>
|
|
183
|
-
<div v-for="i in 3" :key="i" class="border-b border-slate-200 px-5 py-4 last:border-b-0">
|
|
184
|
-
<div class="flex gap-3">
|
|
185
|
-
<div class="h-7 w-7 shrink-0 animate-pulse rounded-full bg-slate-200" />
|
|
186
|
-
<div class="flex-1 space-y-2">
|
|
187
|
-
<div class="h-4 w-32 animate-pulse rounded bg-slate-200" />
|
|
188
|
-
<div class="h-14 w-full animate-pulse rounded-lg bg-slate-200" />
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
|
|
195
|
-
<!-- Thread content -->
|
|
196
|
-
<div v-if="!isLoading && thread" class="space-y-6">
|
|
197
|
-
<!-- Header -->
|
|
198
|
-
<div>
|
|
199
|
-
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
200
|
-
<h1 class="m-0 text-xl font-semibold tracking-tight text-slate-900 sm:text-2xl">{{ thread.title }}</h1>
|
|
201
|
-
<span
|
|
202
|
-
v-if="thread.id"
|
|
203
|
-
class="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-2.5 py-0.5 text-xs font-semibold font-mono text-slate-500"
|
|
204
|
-
>
|
|
205
|
-
#{{ thread.id.slice(0, 8) }}
|
|
206
|
-
</span>
|
|
207
|
-
</div>
|
|
208
|
-
<div
|
|
209
|
-
v-if="(thread.status?.label && thread.status.label.toLowerCase() !== 'open') || (thread.priority?.label && thread.priority.label.toLowerCase() !== 'low')"
|
|
210
|
-
class="mt-2.5 flex flex-wrap items-center gap-2"
|
|
211
|
-
>
|
|
212
|
-
<span
|
|
213
|
-
v-if="thread.status?.label && thread.status.label.toLowerCase() !== 'open'"
|
|
214
|
-
class="inline-flex items-center rounded-full border border-blue-600/20 bg-blue-50 px-2.5 py-0.5 text-xs font-semibold text-blue-600"
|
|
215
|
-
>
|
|
216
|
-
{{ thread.status.label }}
|
|
217
|
-
</span>
|
|
218
|
-
<span
|
|
219
|
-
v-if="thread.priority?.label && thread.priority.label.toLowerCase() !== 'low'"
|
|
220
|
-
class="inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-xs font-semibold text-amber-700"
|
|
221
|
-
>
|
|
222
|
-
{{ thread.priority.label }}
|
|
223
|
-
</span>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
<!-- Summary (ticket form answers) -->
|
|
228
|
-
<div v-if="summaryAnswers.length > 0" class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
229
|
-
<div class="flex items-center gap-2 border-b border-slate-200 px-5 py-3.5">
|
|
230
|
-
<!-- IconFile -->
|
|
231
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="text-slate-500"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /></svg>
|
|
232
|
-
<h2 class="m-0 text-sm font-semibold text-slate-900">Summary</h2>
|
|
233
|
-
</div>
|
|
234
|
-
<div class="space-y-3 p-5">
|
|
235
|
-
<div v-for="answer in summaryAnswers" :key="answer.id" class="rounded-xl bg-slate-50/70 p-4">
|
|
236
|
-
<p class="mb-2.5 text-xs font-medium text-slate-500">{{ answer.question_label || "Question" }}</p>
|
|
237
|
-
<!-- File attachments -->
|
|
238
|
-
<div v-if="answer.attachments && answer.attachments.length > 0" class="flex flex-wrap gap-1.5">
|
|
239
|
-
<a
|
|
240
|
-
v-for="att in answer.attachments"
|
|
241
|
-
:key="att.id"
|
|
242
|
-
:href="att.url"
|
|
243
|
-
target="_blank"
|
|
244
|
-
rel="noopener noreferrer"
|
|
245
|
-
class="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-500 no-underline transition-colors hover:border-slate-300 hover:text-slate-900"
|
|
246
|
-
>
|
|
247
|
-
<!-- IconPaperclip -->
|
|
248
|
-
<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>
|
|
249
|
-
{{ att.original_filename }}
|
|
250
|
-
</a>
|
|
251
|
-
</div>
|
|
252
|
-
<!-- File answer placeholder -->
|
|
253
|
-
<p v-else-if="isFileAnswer(answer.answer)" class="text-sm italic text-slate-500">File uploaded</p>
|
|
254
|
-
<!-- Text answer -->
|
|
255
|
-
<p v-else class="whitespace-pre-wrap break-words rounded-md border border-slate-200/60 bg-white px-3 py-2 text-sm leading-relaxed text-slate-900">
|
|
256
|
-
{{ formatAnswerText(answer.answer) || "-" }}
|
|
257
|
-
</p>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
<!-- Description (first reporter message) -->
|
|
263
|
-
<div v-if="firstReporterMessage" class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
264
|
-
<div class="flex items-center gap-2 border-b border-slate-200 px-5 py-3.5">
|
|
265
|
-
<!-- IconFile -->
|
|
266
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="text-slate-500"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /></svg>
|
|
267
|
-
<h2 class="m-0 text-sm font-semibold text-slate-900">Description</h2>
|
|
268
|
-
</div>
|
|
269
|
-
<div class="p-5">
|
|
270
|
-
<div
|
|
271
|
-
class="vk-message-content text-sm leading-relaxed text-slate-500"
|
|
272
|
-
v-html="sanitizeHtml(firstReporterMessage.content)"
|
|
273
|
-
/>
|
|
274
|
-
<div
|
|
275
|
-
v-if="firstReporterMessage.attachments && firstReporterMessage.attachments.length > 0"
|
|
276
|
-
class="mt-3 flex flex-wrap gap-1.5"
|
|
277
|
-
>
|
|
278
|
-
<a
|
|
279
|
-
v-for="att in firstReporterMessage.attachments"
|
|
280
|
-
:key="att.id"
|
|
281
|
-
:href="att.url"
|
|
282
|
-
target="_blank"
|
|
283
|
-
rel="noopener noreferrer"
|
|
284
|
-
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 no-underline transition-colors hover:border-slate-300 hover:text-slate-900"
|
|
285
|
-
>
|
|
286
|
-
<!-- IconPaperclip -->
|
|
287
|
-
<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>
|
|
288
|
-
{{ att.original_filename }}
|
|
289
|
-
</a>
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
|
|
294
|
-
<!-- Comments -->
|
|
295
|
-
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
|
296
|
-
<div class="flex items-center gap-2 border-b border-slate-200 px-5 py-3.5">
|
|
297
|
-
<!-- IconMessageCircle -->
|
|
298
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="text-slate-500"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z" /></svg>
|
|
299
|
-
<h2 class="m-0 text-sm font-semibold text-slate-900">Comments</h2>
|
|
300
|
-
</div>
|
|
301
|
-
|
|
302
|
-
<!-- Reply form -->
|
|
303
|
-
<div class="border-b border-slate-200 p-5">
|
|
304
|
-
<form class="space-y-3" @submit.prevent="submitReply">
|
|
305
|
-
<textarea
|
|
306
|
-
v-model="content"
|
|
307
|
-
class="min-h-[80px] w-full resize-y rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 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"
|
|
308
|
-
placeholder="Write your reply..."
|
|
309
|
-
/>
|
|
310
|
-
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
311
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
312
|
-
<!-- File upload -->
|
|
313
|
-
<label class="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-medium text-slate-500 transition-colors hover:border-slate-300 hover:text-slate-900">
|
|
314
|
-
<!-- IconPaperclip -->
|
|
315
|
-
<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>
|
|
316
|
-
Browse files
|
|
317
|
-
<input
|
|
318
|
-
type="file"
|
|
319
|
-
class="hidden"
|
|
320
|
-
multiple
|
|
321
|
-
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
322
|
-
@change="onFileChange"
|
|
323
|
-
/>
|
|
324
|
-
</label>
|
|
325
|
-
<!-- File chips -->
|
|
326
|
-
<span
|
|
327
|
-
v-for="(file, i) in files"
|
|
328
|
-
:key="`${file.name}-${i}`"
|
|
329
|
-
class="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-xs text-slate-500"
|
|
330
|
-
>
|
|
331
|
-
<!-- IconPaperclip -->
|
|
332
|
-
<svg width="11" height="11" 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>
|
|
333
|
-
<span class="max-w-[120px] truncate">{{ file.name }}</span>
|
|
334
|
-
<button
|
|
335
|
-
type="button"
|
|
336
|
-
class="flex cursor-pointer items-center border-none bg-transparent p-0 text-slate-500 transition-colors hover:text-red-600"
|
|
337
|
-
:aria-label="`Remove ${file.name}`"
|
|
338
|
-
@click="removeFile(i)"
|
|
339
|
-
>
|
|
340
|
-
<!-- IconX -->
|
|
341
|
-
<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="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
|
|
342
|
-
</button>
|
|
343
|
-
</span>
|
|
344
|
-
</div>
|
|
345
|
-
<!-- Send button -->
|
|
346
|
-
<button
|
|
347
|
-
class="inline-flex items-center gap-2 rounded-lg border-none bg-blue-600 px-5 py-2.5 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"
|
|
348
|
-
type="submit"
|
|
349
|
-
:disabled="isSending"
|
|
350
|
-
>
|
|
351
|
-
<template v-if="isSending">
|
|
352
|
-
<!-- IconSpinner -->
|
|
353
|
-
<svg width="16" height="16" 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>
|
|
354
|
-
Sending...
|
|
355
|
-
</template>
|
|
356
|
-
<template v-else>
|
|
357
|
-
<!-- IconSend -->
|
|
358
|
-
<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.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>
|
|
359
|
-
Send
|
|
360
|
-
</template>
|
|
361
|
-
</button>
|
|
362
|
-
</div>
|
|
363
|
-
</form>
|
|
364
|
-
</div>
|
|
365
|
-
|
|
366
|
-
<!-- Empty messages -->
|
|
367
|
-
<div v-if="sortedMessages.length === 0" class="px-5 py-12 text-center">
|
|
368
|
-
<div class="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-slate-50 text-slate-500">
|
|
369
|
-
<!-- IconMessageCircle -->
|
|
370
|
-
<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>
|
|
371
|
-
</div>
|
|
372
|
-
<p class="m-0 text-sm text-slate-500">No messages yet. Be the first to reply!</p>
|
|
373
|
-
</div>
|
|
374
|
-
|
|
375
|
-
<!-- Message list -->
|
|
376
|
-
<div v-else class="divide-y divide-slate-200">
|
|
377
|
-
<div v-for="message in sortedMessages" :key="message.id" class="flex gap-3 px-5 py-3.5">
|
|
378
|
-
<!-- System message -->
|
|
379
|
-
<template v-if="message.author_type === 'system'">
|
|
380
|
-
<div
|
|
381
|
-
:class="cn('mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[10px] font-bold leading-none', avatarColors('system'))"
|
|
382
|
-
aria-hidden="true"
|
|
383
|
-
>
|
|
384
|
-
S
|
|
385
|
-
</div>
|
|
386
|
-
<div class="min-w-0 flex-1">
|
|
387
|
-
<div class="flex flex-wrap items-baseline gap-2">
|
|
388
|
-
<span class="text-xs font-medium text-slate-500">System</span>
|
|
389
|
-
<span class="text-xs text-slate-500/50">{{ formatDate(message.created_at) }}</span>
|
|
390
|
-
</div>
|
|
391
|
-
<p class="mt-1 text-xs italic text-slate-500">{{ message.content.replace(/<[^>]*>/g, "") }}</p>
|
|
392
|
-
</div>
|
|
393
|
-
</template>
|
|
394
|
-
|
|
395
|
-
<!-- Reporter / Support message -->
|
|
396
|
-
<template v-else>
|
|
397
|
-
<div
|
|
398
|
-
:class="cn('mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[10px] font-bold leading-none', avatarColors(message.author_type))"
|
|
399
|
-
aria-hidden="true"
|
|
400
|
-
>
|
|
401
|
-
{{ (AUTHOR_LABELS[message.author_type] || "?")[0] }}
|
|
402
|
-
</div>
|
|
403
|
-
<div class="min-w-0 flex-1">
|
|
404
|
-
<div class="flex flex-wrap items-baseline gap-2">
|
|
405
|
-
<span class="text-sm font-medium text-slate-900">{{ AUTHOR_LABELS[message.author_type] || message.author_type }}</span>
|
|
406
|
-
<span class="text-xs text-slate-500/50">{{ formatDate(message.created_at) }}</span>
|
|
407
|
-
</div>
|
|
408
|
-
<div
|
|
409
|
-
:class="cn(
|
|
410
|
-
'vk-message-content mt-1.5 break-words rounded-xl px-4 py-3 text-sm leading-relaxed',
|
|
411
|
-
message.author_type === 'user' ? 'border border-blue-600/10 bg-blue-50 text-slate-800' : 'border border-slate-200 bg-slate-50 text-slate-800',
|
|
412
|
-
)"
|
|
413
|
-
v-html="sanitizeHtml(message.content)"
|
|
414
|
-
/>
|
|
415
|
-
<div
|
|
416
|
-
v-if="message.attachments && message.attachments.length > 0"
|
|
417
|
-
class="mt-2 flex flex-wrap gap-1.5"
|
|
418
|
-
>
|
|
419
|
-
<a
|
|
420
|
-
v-for="att in message.attachments"
|
|
421
|
-
:key="att.id"
|
|
422
|
-
class="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-500 no-underline transition-all duration-150 hover:border-slate-300 hover:text-slate-900"
|
|
423
|
-
:href="att.url"
|
|
424
|
-
rel="noopener noreferrer"
|
|
425
|
-
target="_blank"
|
|
426
|
-
>
|
|
427
|
-
<!-- IconPaperclip -->
|
|
428
|
-
<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>
|
|
429
|
-
{{ att.original_filename }}
|
|
430
|
-
</a>
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
433
|
-
</template>
|
|
434
|
-
</div>
|
|
435
|
-
</div>
|
|
436
|
-
</div>
|
|
437
|
-
</div>
|
|
438
|
-
</div>
|
|
439
|
-
</div>
|
|
440
|
-
</template>
|
|
441
|
-
|
|
442
|
-
<style>
|
|
443
|
-
.vk-message-content p { margin-bottom: 0.5rem; }
|
|
444
|
-
.vk-message-content p:last-child { margin-bottom: 0; }
|
|
445
|
-
.vk-message-content a { color: #2563eb; text-decoration: underline; }
|
|
446
|
-
.vk-message-content ul, .vk-message-content ol { margin: 0.25rem 0; padding-left: 1.5rem; }
|
|
447
|
-
.vk-message-content pre { overflow-x: auto; border-radius: 6px; background: #f8fafc; padding: 0.75rem; font-size: 0.75rem; }
|
|
448
|
-
.vk-message-content code { font-family: monospace; font-size: 0.85em; }
|
|
449
|
-
</style>
|