@mundogamernetwork/shared-ui 1.1.61 → 1.1.63

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.
@@ -0,0 +1,653 @@
1
+ <template>
2
+ <div id="shared-key-materials">
3
+ <div class="container">
4
+ <div class="header">
5
+ <NuxtLink @click.prevent="goBack" class="back">
6
+ <MGIcon size="2x" icon="chevron-left" />
7
+ <span>{{ $t("keys.materials.back") }}</span>
8
+ </NuxtLink>
9
+ </div>
10
+
11
+ <div class="form-content">
12
+ <div class="title-bar">
13
+ <div class="text-container">
14
+ <h3 class="custom-text">{{ $t("keys.materials.title") }}</h3>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="boxes">
19
+ <!-- Submission form -->
20
+ <div class="box-container">
21
+ <div class="title-box">
22
+ <div>
23
+ <svg
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ width="24"
26
+ height="25"
27
+ viewBox="0 0 24 25"
28
+ :style="{ fill: isContentOpened[1] ? '#D297FF' : 'var(--inactive)' }"
29
+ >
30
+ <path d="M9.08471 16.3961L5.29164 12.603L4 13.8856L9.08471 18.9703L20 8.05501L18.7175 6.77246L9.08471 16.3961Z" />
31
+ </svg>
32
+ {{ $t("keys.materials.subtitle") }}
33
+ </div>
34
+ <button class="content__info__toggler" @click="toggleBox(1)">
35
+ <MGIcon icon="chevron-down" :class="{ rotate: isContentOpened[1] }" />
36
+ </button>
37
+ </div>
38
+
39
+ <div v-show="isContentOpened[1]">
40
+ <div v-for="(form, index) in forms" :key="index" v-show="activeForm === index">
41
+ <form class="card-form">
42
+ <div class="form-row">
43
+ <div class="col1">
44
+ <h5>{{ $t("keys.materials.type") }}</h5>
45
+ </div>
46
+ <div class="col2">
47
+ <div class="types">
48
+ <div
49
+ class="type"
50
+ :class="{ selected: form.content_types.includes('article') }"
51
+ @click="toggleType(index, 'article')"
52
+ >
53
+ <MGIcon icon="editorial-journalism" />
54
+ {{ $t("keys.materials.type_article") }}
55
+ </div>
56
+ <div
57
+ class="type"
58
+ :class="{ selected: form.content_types.includes('web') }"
59
+ @click="toggleType(index, 'web')"
60
+ >
61
+ <MGIcon icon="audio-sound" />
62
+ {{ $t("keys.materials.type_web") }}
63
+ </div>
64
+ <div
65
+ class="type"
66
+ :class="{ selected: form.content_types.includes('print') }"
67
+ @click="toggleType(index, 'print')"
68
+ >
69
+ <MGIcon icon="camera" />
70
+ {{ $t("keys.materials.type_print") }}
71
+ </div>
72
+ <div
73
+ class="type"
74
+ :class="{ selected: form.content_types.includes('stream') }"
75
+ @click="toggleType(index, 'stream')"
76
+ >
77
+ <MGIcon icon="twtch" />
78
+ {{ $t("keys.materials.type_stream") }}
79
+ </div>
80
+ <div
81
+ class="type"
82
+ :class="{ selected: form.content_types.includes('video') }"
83
+ @click="toggleType(index, 'video')"
84
+ >
85
+ <MGIcon icon="youtube" />
86
+ {{ $t("keys.materials.type_video") }}
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <div class="form-row">
93
+ <div class="col1">
94
+ <h5>{{ $t("keys.materials.link") }}</h5>
95
+ </div>
96
+ <div class="col2">
97
+ <MGIcon size="2x" icon="others" />
98
+ <input
99
+ class="form-control"
100
+ v-model="form.material_url"
101
+ :class="{ 'error-border': urlError }"
102
+ placeholder="https://example.com"
103
+ />
104
+ <div v-if="urlError" class="error-message">{{ urlError }}</div>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="form-row">
109
+ <div class="col1">
110
+ <h5>{{ $t("keys.materials.description") }}</h5>
111
+ </div>
112
+ <div class="col2">
113
+ <textarea class="form-control textarea" v-model="form.description"></textarea>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="form-row">
118
+ <div class="col1">
119
+ <h5>{{ $t("keys.materials.date") }}</h5>
120
+ </div>
121
+ <div class="col2">
122
+ <input type="datetime-local" class="form-control" v-model="form.submitted_at" />
123
+ </div>
124
+ </div>
125
+ </form>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="bottom">
130
+ <div class="buttons">
131
+ <div v-if="formError" class="form-error">{{ formError }}</div>
132
+ <div
133
+ class="btn button-submit"
134
+ :class="{ disabled: submitting }"
135
+ @click="submitForm"
136
+ >
137
+ <span v-if="submitting">{{ $t("keys.materials.sending") }}</span>
138
+ <span v-else>{{ $t("keys.materials.finish") }}</span>
139
+ </div>
140
+
141
+ <!-- Success modal -->
142
+ <div v-if="showSuccessModal" class="success-modal-overlay" @click.self="handleSuccessClose">
143
+ <div class="success-modal">
144
+ <MGIcon size="2x" icon="check" style="color: #42FF00" />
145
+ <p>{{ $t("keys.materials.success_message") }}</p>
146
+ <button class="btn button-submit" @click="handleSuccessClose">
147
+ {{ $t("keys.materials.success_close") }}
148
+ </button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Previously submitted materials -->
156
+ <div class="box-container" v-if="submittedMaterials.length > 0">
157
+ <div class="title-box">
158
+ <div>
159
+ <MGIcon size="1x" icon="version" style="color: #D297FF" />
160
+ {{ $t("keys.materials.submitted_title") }}
161
+ </div>
162
+ </div>
163
+ <div class="submitted-list">
164
+ <div class="submitted-item" v-for="(mat, i) in submittedMaterials" :key="mat.id">
165
+ <div class="submitted-index">#{{ i + 1 }}</div>
166
+ <div class="submitted-info">
167
+ <a :href="mat.material_url" target="_blank" rel="noopener" class="submitted-url">
168
+ {{ mat.material_url }}
169
+ </a>
170
+ <p v-if="mat.description" class="submitted-desc">{{ mat.description }}</p>
171
+ <div class="submitted-types" v-if="mat.content_types?.length">
172
+ <span v-for="ct in mat.content_types" :key="ct.id" class="submitted-tag">
173
+ {{ ct.name }}
174
+ </span>
175
+ </div>
176
+ <p v-if="mat.submitted_at" class="submitted-date">
177
+ {{ formatSubmittedAt(mat.submitted_at) }}
178
+ </p>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </template>
188
+
189
+ <script setup lang="ts">
190
+ import { ref, inject, onMounted } from "vue"
191
+ import { useRoute, useRouter } from "vue-router"
192
+ import { useI18n } from "#imports"
193
+ import { submitMaterial, fetchMaterialsByRequest } from "../../services/campaignService"
194
+
195
+ definePageMeta({
196
+ middleware: [
197
+ function (to) {
198
+ const requestId = Number(to.query.requestId)
199
+ if (!requestId || isNaN(requestId)) {
200
+ const locale = useCookie("i18n_redirected").value || "en"
201
+ return navigateTo(`/${locale}/key-campaigns`)
202
+ }
203
+ },
204
+ ],
205
+ })
206
+
207
+ // Consuming front injects current user id (or null if not logged in)
208
+ const currentUserId = inject<number | null>("currentUserId", null)
209
+
210
+ const props = defineProps<{ goBackPage?: string | null }>()
211
+
212
+ const route = useRoute()
213
+ const { t, locale } = useI18n()
214
+ const requestId = ref<number>(Number(route.query.requestId))
215
+
216
+ type ContentType = "article" | "web" | "print" | "stream" | "video"
217
+
218
+ interface FormData {
219
+ content_types: ContentType[]
220
+ material_url: string
221
+ description: string
222
+ submitted_at: string
223
+ }
224
+
225
+ const contentTypesMap: Record<ContentType, number> = {
226
+ article: 1,
227
+ video: 2,
228
+ stream: 3,
229
+ web: 4,
230
+ print: 5,
231
+ }
232
+
233
+ const forms = ref<FormData[]>([
234
+ { content_types: [], material_url: "", description: "", submitted_at: "" },
235
+ ])
236
+ const activeForm = ref(0)
237
+ const isContentOpened = ref<Record<number, boolean>>({ 1: true })
238
+ const submitting = ref(false)
239
+ const submittedMaterials = ref<any[]>([])
240
+ const urlError = ref<string | null>(null)
241
+ const formError = ref<string | null>(null)
242
+ const showSuccessModal = ref(false)
243
+
244
+ function toggleType(formIndex: number, type: ContentType) {
245
+ const currentTypes = forms.value[formIndex].content_types
246
+ const idx = currentTypes.indexOf(type)
247
+ if (idx === -1) { currentTypes.push(type) } else { currentTypes.splice(idx, 1) }
248
+ }
249
+
250
+ function toggleBox(boxNumber: number) {
251
+ Object.keys(isContentOpened.value).forEach((key) => {
252
+ if (Number(key) !== boxNumber) isContentOpened.value[Number(key)] = false
253
+ })
254
+ isContentOpened.value[boxNumber] = !isContentOpened.value[boxNumber]
255
+ }
256
+
257
+ function goBack() {
258
+ if (props.goBackPage) {
259
+ window.location.href = props.goBackPage
260
+ } else {
261
+ window.history.back()
262
+ }
263
+ }
264
+
265
+ function handleSuccessClose() {
266
+ showSuccessModal.value = false
267
+ goBack()
268
+ }
269
+
270
+ function formatSubmittedAt(dateStr: string): string {
271
+ const d = new Date(dateStr)
272
+ if (isNaN(d.getTime())) return dateStr
273
+ return d.toLocaleString(locale.value, { dateStyle: "short", timeStyle: "short" })
274
+ }
275
+
276
+ function isValidUrl(urlString: string): boolean {
277
+ try {
278
+ const url = new URL(urlString)
279
+ return ["http:", "https:"].includes(url.protocol)
280
+ } catch {
281
+ return false
282
+ }
283
+ }
284
+
285
+ const submitForm = async () => {
286
+ if (submitting.value) return
287
+ submitting.value = true
288
+ urlError.value = null
289
+ formError.value = null
290
+
291
+ try {
292
+ forms.value.forEach((form) => {
293
+ form.material_url = form.material_url.trim()
294
+ form.description = form.description.trim()
295
+ })
296
+
297
+ const hasEmptyFields = forms.value.some(
298
+ (form) => form.content_types.length === 0 || !form.material_url || !form.description || !form.submitted_at,
299
+ )
300
+
301
+ if (hasEmptyFields) {
302
+ formError.value = t("keys.materials.required_fields")
303
+ return
304
+ }
305
+
306
+ const invalidUrls = forms.value.filter((form) => !isValidUrl(form.material_url))
307
+ if (invalidUrls.length > 0) {
308
+ urlError.value = t("keys.materials.url_error")
309
+ return
310
+ }
311
+
312
+ for (const form of forms.value) {
313
+ await submitMaterial({
314
+ key_request_id: requestId.value,
315
+ user_id: currentUserId,
316
+ status: true,
317
+ content_types: form.content_types.map((type) => contentTypesMap[type]),
318
+ material_url: form.material_url,
319
+ description: form.description,
320
+ submitted_at: new Date(form.submitted_at).toISOString(),
321
+ })
322
+ }
323
+
324
+ // Reload submitted list
325
+ try {
326
+ const res = await fetchMaterialsByRequest(requestId.value)
327
+ submittedMaterials.value = res?.data?.data ?? []
328
+ } catch { /* silent */ }
329
+
330
+ forms.value = [{ content_types: [], material_url: "", description: "", submitted_at: "" }]
331
+ activeForm.value = 0
332
+ showSuccessModal.value = true
333
+ } catch (error) {
334
+ console.error("[key-materials] Submit error:", error)
335
+ formError.value = t("keys.materials.submit_error")
336
+ } finally {
337
+ submitting.value = false
338
+ }
339
+ }
340
+
341
+ onMounted(async () => {
342
+ if (requestId.value) {
343
+ try {
344
+ const res = await fetchMaterialsByRequest(requestId.value)
345
+ submittedMaterials.value = res?.data?.data ?? []
346
+ } catch { /* silent */ }
347
+ }
348
+ })
349
+ </script>
350
+
351
+ <style scoped lang="scss">
352
+ .form-error {
353
+ color: #ff4800;
354
+ font-size: 12px;
355
+ padding: 8px 0;
356
+ width: 100%;
357
+ text-align: right;
358
+ }
359
+
360
+ .error-border { border: 1px solid rgb(248, 60, 60) !important; }
361
+ .error-message { color: rgb(248, 60, 60); font-size: 0.8rem; margin-top: 5px; }
362
+
363
+ .success-modal-overlay {
364
+ position: fixed;
365
+ inset: 0;
366
+ background: rgba(0, 0, 0, 0.7);
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ z-index: 1000;
371
+
372
+ .success-modal {
373
+ background: var(--body-bg-dash, #1c1c1c);
374
+ padding: 32px;
375
+ display: flex;
376
+ flex-direction: column;
377
+ align-items: center;
378
+ gap: 16px;
379
+ min-width: 300px;
380
+
381
+ p {
382
+ color: var(--card-cover-title);
383
+ font-size: 16px;
384
+ font-weight: 600;
385
+ text-align: center;
386
+ }
387
+ }
388
+ }
389
+
390
+ #shared-key-materials {
391
+ align-items: center;
392
+ width: 100%;
393
+ justify-content: center;
394
+ overflow: auto;
395
+ height: 100%;
396
+
397
+ .container { max-width: 1140px !important; }
398
+
399
+ .header {
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 43px;
403
+ margin-top: 12px;
404
+
405
+ .back {
406
+ display: flex;
407
+ align-items: center;
408
+ gap: 4px;
409
+ color: var(--card-article-title);
410
+ font-size: 12px;
411
+ cursor: pointer;
412
+ text-decoration: none;
413
+ }
414
+ }
415
+
416
+ .form-content {
417
+ width: 100%;
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 24px;
421
+
422
+ .title-bar {
423
+ display: flex;
424
+ margin-top: 32px;
425
+ .text-container {
426
+ width: 100%;
427
+ padding-left: 0.8em;
428
+ border-left: 2px solid #d297ff;
429
+ .custom-text {
430
+ margin: 0;
431
+ font-size: 18px;
432
+ font-weight: 600;
433
+ color: var(--card-cover-title);
434
+ }
435
+ }
436
+ }
437
+
438
+ .disabled {
439
+ cursor: not-allowed;
440
+ pointer-events: none;
441
+ opacity: 0.6;
442
+ background: var(--tab-control-disabled-bg) !important;
443
+ }
444
+
445
+ .rotate { transform: rotate(180deg); transition: transform 1s ease; }
446
+
447
+ .submitted-list {
448
+ padding: 12px 16px;
449
+ display: flex;
450
+ flex-direction: column;
451
+ gap: 12px;
452
+
453
+ .submitted-item {
454
+ display: flex;
455
+ gap: 12px;
456
+ padding: 12px;
457
+ background: var(--button-secondary-default-bg);
458
+
459
+ .submitted-index { font-size: 12px; font-weight: 700; color: #D297FF; min-width: 24px; }
460
+
461
+ .submitted-info {
462
+ display: flex;
463
+ flex-direction: column;
464
+ gap: 4px;
465
+
466
+ .submitted-url { font-size: 13px; color: #D297FF; word-break: break-all; text-decoration: underline; }
467
+ .submitted-desc { font-size: 12px; color: var(--secondary-info-fg); margin: 0; }
468
+
469
+ .submitted-types {
470
+ display: flex;
471
+ gap: 6px;
472
+ flex-wrap: wrap;
473
+ .submitted-tag {
474
+ font-size: 11px;
475
+ padding: 2px 8px;
476
+ background: rgba(210, 151, 255, 0.15);
477
+ color: #D297FF;
478
+ }
479
+ }
480
+
481
+ .submitted-date { font-size: 11px; color: rgba(255, 255, 255, 0.4); margin: 4px 0 0; }
482
+ }
483
+ }
484
+ }
485
+
486
+ .boxes {
487
+ display: flex;
488
+ flex-direction: column;
489
+ gap: 24px;
490
+ width: 100%;
491
+
492
+ .box-container {
493
+ width: 100%;
494
+
495
+ .content__info__toggler {
496
+ color: var(--card-cover-title);
497
+ display: flex;
498
+ height: 32px;
499
+ align-items: center;
500
+ background: transparent;
501
+ border: none;
502
+ padding: 0;
503
+ font-size: 14px;
504
+ font-weight: 600;
505
+ cursor: pointer;
506
+
507
+ .rotate { transform: rotate(180deg); transition: 0.5s; }
508
+ &:hover { color: var(--faq-toggler-active-fg); }
509
+ }
510
+
511
+ .title-box {
512
+ display: flex;
513
+ background-color: var(--bg-app-badge);
514
+ padding: 8px 16px;
515
+ align-items: center;
516
+ gap: 16px;
517
+ justify-content: space-between;
518
+ color: var(--card-cover-title);
519
+ font-size: 16px;
520
+ font-weight: 600;
521
+ }
522
+
523
+ .card-form {
524
+ width: 100%;
525
+ display: flex;
526
+ flex-direction: column;
527
+
528
+ .form-row {
529
+ width: 100%;
530
+ display: flex;
531
+ gap: 16px;
532
+ padding: 40px 24px;
533
+ background-color: var(--body-bg-dash);
534
+ border-bottom: 1px solid var(--bg-app-badge);
535
+
536
+ .col1 {
537
+ width: 100%;
538
+ max-width: 280px;
539
+ display: flex;
540
+ flex-direction: column;
541
+ justify-content: center;
542
+ margin-right: 16px;
543
+
544
+ h5 { font-size: 16px; font-weight: 600; color: var(--card-cover-title); margin-bottom: 4px; }
545
+ }
546
+
547
+ .col2 {
548
+ width: 100%;
549
+ display: flex;
550
+ align-items: center;
551
+ gap: 16px;
552
+ flex: 1 0 0;
553
+
554
+ input::placeholder { color: var(--inactive); }
555
+ i { color: #D297FF; }
556
+
557
+ .form-control {
558
+ display: flex;
559
+ padding: 12px;
560
+ border-radius: 0;
561
+ background-color: transparent;
562
+ height: 48px;
563
+ border: 1px solid var(--bg-app-badge);
564
+ width: 100%;
565
+ font-size: 16px;
566
+ color: var(--secondary-info-fg);
567
+ &:focus { border: 1px solid var(--section-title-marker-fg); box-shadow: none; outline: 0; }
568
+ }
569
+
570
+ .textarea { height: 134px; &::placeholder { color: var(--inactive); } }
571
+
572
+ .types {
573
+ display: flex;
574
+ align-items: center;
575
+ gap: 16px;
576
+ flex: 1 0 0;
577
+ flex-wrap: wrap;
578
+
579
+ .type {
580
+ display: flex;
581
+ height: 80px;
582
+ padding: 24px;
583
+ flex-direction: column;
584
+ justify-content: center;
585
+ align-items: center;
586
+ gap: 12px;
587
+ flex: 1 0 0;
588
+ background-color: var(--bg-app-badge);
589
+ color: var(--inactive);
590
+ text-align: center;
591
+ font-size: 10px;
592
+ cursor: pointer;
593
+ i { font-size: 32px; color: #D297FF; }
594
+ &:hover { border: 2px solid #D297FF; }
595
+ &.selected { border: 2px solid #D297FF; }
596
+ }
597
+ }
598
+ }
599
+ }
600
+ }
601
+
602
+ .bottom {
603
+ display: flex;
604
+ padding: 24px 32px;
605
+ width: 100%;
606
+ justify-content: flex-end;
607
+
608
+ .buttons {
609
+ display: flex;
610
+ align-items: center;
611
+ gap: 16px;
612
+ width: 100%;
613
+ justify-content: flex-end;
614
+
615
+ .button-submit {
616
+ border: none;
617
+ color: #0E0E0E;
618
+ text-align: center;
619
+ font-size: 16px;
620
+ font-weight: 400;
621
+ display: flex;
622
+ width: fit-content;
623
+ height: 44px;
624
+ padding: 0 12px;
625
+ justify-content: center;
626
+ align-items: center;
627
+ background: #d297ff;
628
+ min-width: 200px;
629
+ cursor: pointer;
630
+ }
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ @media screen and (max-width: 767px) {
637
+ padding: 10px;
638
+
639
+ .boxes .box-container {
640
+ .card-form .form-row {
641
+ display: block;
642
+ .col1 { width: 100%; max-width: 100%; margin-bottom: 16px; }
643
+ .col2 { width: 100%; margin-bottom: 16px; }
644
+ }
645
+ .bottom {
646
+ padding: 24px 0;
647
+ .buttons .button-submit { min-width: 100%; margin: 24px 0; }
648
+ }
649
+ }
650
+ }
651
+ }
652
+ }
653
+ </style>