@mundogamernetwork/shared-ui 1.0.0

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.
Files changed (87) hide show
  1. package/README.md +283 -0
  2. package/components/PressKit/AssetGallery.vue +349 -0
  3. package/components/PressKit/Awards.vue +100 -0
  4. package/components/PressKit/Credits.vue +78 -0
  5. package/components/PressKit/FactSheet.vue +204 -0
  6. package/components/PressKit/Hero.vue +143 -0
  7. package/components/PressKit/Quotes.vue +80 -0
  8. package/components/PressKit/VideoPlayer.vue +134 -0
  9. package/components/checkout/MgCartItemList.vue +214 -0
  10. package/components/checkout/MgCartSummary.vue +204 -0
  11. package/components/checkout/MgCheckoutSidebar.vue +230 -0
  12. package/components/checkout/MgGuestEmailForm.vue +97 -0
  13. package/components/checkout/MgPaymentMethodSelector.vue +162 -0
  14. package/components/checkout/MgPixQRCode.vue +222 -0
  15. package/components/indie-wall/IndieWallLeaderboard.vue +208 -0
  16. package/components/indie-wall/MuralCanvas.vue +481 -0
  17. package/components/indie-wall/StepBlock.vue +314 -0
  18. package/components/indie-wall/StepCustomize.vue +530 -0
  19. package/components/indie-wall/StepGoal.vue +169 -0
  20. package/components/indie-wall/StepPackage.vue +145 -0
  21. package/components/indie-wall/StepPay.vue +209 -0
  22. package/components/indie-wall/SupportStepper.vue +372 -0
  23. package/components/invoices/MgInvoiceDownload.vue +50 -0
  24. package/components/pricing/MgBillingToggle.vue +74 -0
  25. package/components/pricing/MgPricingCard.vue +245 -0
  26. package/components/ui/Header/MgMessageCard.vue +147 -0
  27. package/components/ui/Header/MgMessageModal.vue +414 -0
  28. package/components/ui/Header/MgNotificationCard.vue +200 -0
  29. package/components/ui/Header/MgNotificationsModal.vue +125 -0
  30. package/components/ui/MgAnnouncementBanner.vue +147 -0
  31. package/components/ui/MgBanners.vue +23 -0
  32. package/components/ui/MgHeaderComponent.vue +283 -0
  33. package/components/ui/MgHeaderUIConfig.vue +225 -0
  34. package/components/ui/MgHeaderUIUser.vue +301 -0
  35. package/components/ui/MgLoginModal.vue +156 -0
  36. package/components/ui/MgPromotionBanner.vue +185 -0
  37. package/composables/useLogout.ts +42 -0
  38. package/composables/useMgCheckout.ts +287 -0
  39. package/composables/useMgUserNotifications.ts +122 -0
  40. package/composables/usePaymentMethods.ts +75 -0
  41. package/composables/useSubscription.ts +163 -0
  42. package/middleware/auth.global.ts +40 -0
  43. package/nuxt.config.ts +31 -0
  44. package/package.json +40 -0
  45. package/pages/[slug]/index.vue +112 -0
  46. package/pages/about.vue +133 -0
  47. package/pages/blog.vue +430 -0
  48. package/pages/careers.vue +329 -0
  49. package/pages/contact.vue +339 -0
  50. package/pages/faq.vue +317 -0
  51. package/pages/health-check.vue +20 -0
  52. package/pages/icons.vue +58 -0
  53. package/pages/magazine/[slug].vue +209 -0
  54. package/pages/magazine/index.vue +267 -0
  55. package/pages/media-kit/[slug].vue +625 -0
  56. package/pages/mural/[slug].vue +1058 -0
  57. package/pages/partners.vue +290 -0
  58. package/pages/press.vue +237 -0
  59. package/pages/presskit/[slug].vue +191 -0
  60. package/pages/roadmap.vue +355 -0
  61. package/pages/status.vue +199 -0
  62. package/pages/team.vue +266 -0
  63. package/pages/wall/[slug].vue +11 -0
  64. package/plugins/auth.client.ts +17 -0
  65. package/plugins/echo.client.ts +132 -0
  66. package/services/authService.ts +95 -0
  67. package/services/chatService.ts +53 -0
  68. package/services/contactService.ts +35 -0
  69. package/services/documentService.ts +16 -0
  70. package/services/httpService.ts +95 -0
  71. package/services/indieWallService.ts +174 -0
  72. package/services/institutionalService.ts +248 -0
  73. package/services/mediaKitService.ts +51 -0
  74. package/services/notificationsService.ts +20 -0
  75. package/services/pressKitService.ts +55 -0
  76. package/stores/announcement.ts +129 -0
  77. package/stores/auth.ts +86 -0
  78. package/stores/chat.ts +150 -0
  79. package/stores/contact.ts +28 -0
  80. package/stores/document.ts +27 -0
  81. package/stores/index.ts +34 -0
  82. package/stores/institutional.ts +231 -0
  83. package/stores/login.ts +27 -0
  84. package/stores/notifications.ts +133 -0
  85. package/stores/promotion.ts +154 -0
  86. package/types/index.ts +135 -0
  87. package/utils/serialize.ts +29 -0
@@ -0,0 +1,372 @@
1
+ <script setup lang="ts">
2
+ import StepGoal from './StepGoal.vue'
3
+ import StepPackage from './StepPackage.vue'
4
+ import StepBlock from './StepBlock.vue'
5
+ import StepCustomize from './StepCustomize.vue'
6
+ import StepPay from './StepPay.vue'
7
+ import { supportIndieWall } from '../../services/indieWallService'
8
+
9
+ const props = defineProps<{
10
+ wall: any
11
+ goals: any[]
12
+ tiers: any[]
13
+ supporters: any[]
14
+ mgcBalance: number
15
+ initialGoalId?: number | null
16
+ initialTierId?: number | null
17
+ initialSelection?: { x: number; y: number; w: number; h: number } | null
18
+ startStep?: 1 | 2 | 3 | 4 | 5
19
+ drawer?: boolean
20
+ }>()
21
+
22
+ const emit = defineEmits<{
23
+ (e: 'close'): void
24
+ (e: 'support-success'): void
25
+ }>()
26
+
27
+ const { t } = useI18n()
28
+ const route = useRoute()
29
+ const router = useRouter()
30
+ const authStore = useAuthStore()
31
+
32
+ // ── Step state ─────────────────────────────────────────────────────────────
33
+ const step = ref<1 | 2 | 3 | 4 | 5>(props.startStep ?? 1)
34
+
35
+ // ── Selections ─────────────────────────────────────────────────────────────
36
+ const selectedGoalId = ref<number | null>(props.initialGoalId ?? null)
37
+ const selectedTierId = ref<number | null>(props.initialTierId ?? null)
38
+ const customQuantity = ref<number>(1)
39
+ const pixelCount = ref<number>(1)
40
+ const selection = ref<{ x: number; y: number; w: number; h: number } | null>(props.initialSelection ?? null)
41
+
42
+ const customForm = ref({
43
+ title: '',
44
+ message: '',
45
+ background_color: '#FDB215',
46
+ image_url: '',
47
+ link: '',
48
+ is_anonymous: false,
49
+ guest_email: '',
50
+ guest_name: '',
51
+ })
52
+
53
+ const gatewayProvider = ref<'stripe' | 'paypal'>('stripe')
54
+ const processing = ref(false)
55
+ const errorMsg = ref('')
56
+
57
+ // ── Derived ────────────────────────────────────────────────────────────────
58
+ const selectedGoal = computed(() => props.goals.find(g => g.id === selectedGoalId.value) || null)
59
+ const selectedTier = computed(() => props.tiers.find(t => t.id === selectedTierId.value) || null)
60
+
61
+ // If pre-set tier, ensure pixelCount matches
62
+ watch(() => props.initialTierId, (id) => {
63
+ if (id) {
64
+ const t = props.tiers.find(t => t.id === id)
65
+ if (t) {
66
+ selectedTierId.value = id
67
+ pixelCount.value = t.pixel_count
68
+ }
69
+ }
70
+ }, { immediate: true })
71
+
72
+ // ── Navigation ─────────────────────────────────────────────────────────────
73
+ function next() {
74
+ let nextStep = step.value + 1
75
+ // Skip goals step if there are no goals
76
+ if (nextStep === 2 && props.goals.length === 0) nextStep = 3
77
+ if (nextStep <= 5) step.value = nextStep as 1 | 2 | 3 | 4 | 5
78
+ }
79
+
80
+ function back() {
81
+ let prevStep = step.value - 1
82
+ // Skip goals step if there are no goals
83
+ if (prevStep === 2 && props.goals.length === 0) prevStep = 1
84
+ if (prevStep >= 1) step.value = prevStep as 1 | 2 | 3 | 4 | 5
85
+ }
86
+
87
+ function close() {
88
+ emit('close')
89
+ }
90
+
91
+ // ── Step 1 handlers ────────────────────────────────────────────────────────
92
+ function selectGoal(goalId: number | null) {
93
+ selectedGoalId.value = goalId
94
+ }
95
+
96
+ // ── Step 2 handlers ────────────────────────────────────────────────────────
97
+ function selectTier(tierId: number | null, pc: number) {
98
+ selectedTierId.value = tierId
99
+ pixelCount.value = pc
100
+ // Reset selection if size changed
101
+ if (selection.value && (selection.value.w * selection.value.h) !== pc) {
102
+ selection.value = null
103
+ }
104
+ }
105
+
106
+ function setCustomQuantity(qty: number) {
107
+ customQuantity.value = qty
108
+ }
109
+
110
+ function updatePixelCount(qty: number) {
111
+ const clamped = Math.max(1, Math.floor(qty || 1))
112
+ if (clamped === pixelCount.value) return
113
+ // Free quantity → drop tier link and reset position so step 3 re-suggests
114
+ selectedTierId.value = null
115
+ customQuantity.value = clamped
116
+ pixelCount.value = clamped
117
+ selection.value = null
118
+ }
119
+
120
+ // ── Step 3 handlers ────────────────────────────────────────────────────────
121
+ function selectArea(rect: { x: number; y: number; w: number; h: number } | null) {
122
+ selection.value = rect
123
+ }
124
+
125
+ // ── Step 4 handlers ────────────────────────────────────────────────────────
126
+ function updateCustomForm(form: typeof customForm.value) {
127
+ customForm.value = { ...form }
128
+ }
129
+
130
+ // ── Step 5 / submit ────────────────────────────────────────────────────────
131
+ function setGateway(p: 'stripe' | 'paypal') {
132
+ gatewayProvider.value = p
133
+ }
134
+
135
+ async function confirm() {
136
+ if (!props.wall || !selection.value) return
137
+
138
+ errorMsg.value = ''
139
+ processing.value = true
140
+ try {
141
+ const isGuest = !authStore.signedIn
142
+ const popupSuffix = route.query.popup === '1' ? '&popup=1' : ''
143
+ // Always return to the mural public page so payment=success handling works
144
+ // regardless of which page (stream, embed, etc.) triggered the checkout.
145
+ const locale = useNuxtApp().$i18n.locale.value
146
+ const slug = props.wall.slug
147
+ const muralBase = `${window.location.origin}/${locale}/mural/${slug}`
148
+ const res = await supportIndieWall(props.wall.id, {
149
+ user_id: authStore.user?.id ?? undefined,
150
+ guest_email: isGuest ? customForm.value.guest_email : undefined,
151
+ guest_name: isGuest ? (customForm.value.guest_name || undefined) : undefined,
152
+ indie_wall_goal_id: selectedGoalId.value,
153
+ indie_wall_tier_id: selectedTierId.value,
154
+ pixel_count: pixelCount.value,
155
+ x: selection.value.x,
156
+ y: selection.value.y,
157
+ width: selection.value.w,
158
+ height: selection.value.h,
159
+ title: customForm.value.title,
160
+ message: customForm.value.message,
161
+ background_color: customForm.value.background_color,
162
+ image_url: customForm.value.image_url || undefined,
163
+ link: customForm.value.link || undefined,
164
+ is_anonymous: customForm.value.is_anonymous,
165
+ payment_method: 'gateway',
166
+ gateway_provider: gatewayProvider.value,
167
+ success_url: `${muralBase}?payment=success${popupSuffix}`,
168
+ cancel_url: `${muralBase}?payment=cancelled${popupSuffix}`,
169
+ })
170
+ const data = res.data?.data || res.data
171
+ if (data?.payer_action) {
172
+ try {
173
+ if (data?.pixel_id) {
174
+ sessionStorage.setItem('mgn-mural-pending-pixel', String(data.pixel_id))
175
+ sessionStorage.setItem('mgn-mural-pending-wall', String(props.wall.slug ?? props.wall.id))
176
+ }
177
+ } catch { /* sessionStorage may be unavailable */ }
178
+ window.location.href = data.payer_action
179
+ return
180
+ }
181
+ if (data?.status === 'pending_payment') {
182
+ errorMsg.value = t('tv.dashboard.indie_wall.error_gateway')
183
+ return
184
+ }
185
+ emit('support-success')
186
+ emit('close')
187
+ } catch (err: any) {
188
+ const apiError = err?.response?.data?.error
189
+ if (apiError === 'selected_area_occupied') {
190
+ errorMsg.value = t('tv.dashboard.indie_wall.error_area_occupied')
191
+ step.value = 3
192
+ selection.value = null
193
+ } else {
194
+ errorMsg.value = apiError
195
+ || err?.response?.data?.errors?.pixel_count?.[0]
196
+ || err?.response?.data?.errors?.indie_wall_tier_id?.[0]
197
+ || err?.message
198
+ || t('tv.dashboard.indie_wall.error_save')
199
+ }
200
+ } finally {
201
+ processing.value = false
202
+ }
203
+ }
204
+ </script>
205
+
206
+ <template>
207
+ <div :class="['ss-overlay', { 'ss-overlay--drawer': drawer }]" @click.self="close">
208
+ <div :class="['ss-modal', { 'ss-modal--drawer': drawer }]">
209
+ <button class="ss-close" type="button" aria-label="Close" @click="close">×</button>
210
+
211
+ <div v-if="wall" class="ss-availability">
212
+ <span class="ss-availability-label">{{ $t('tv.dashboard.indie_wall.pixels_available') }}:</span>
213
+ <strong>
214
+ {{ Math.max(0, (wall.total_pixels || 0) - (wall.used_pixels || 0)) }}
215
+ / {{ wall.total_pixels || 0 }}
216
+ </strong>
217
+ </div>
218
+
219
+ <div class="ss-stepper-bar">
220
+ <div v-for="n in 5" :key="n" :class="['ss-dot', { active: step >= n, current: step === n }]">
221
+ <span>{{ n }}</span>
222
+ </div>
223
+ </div>
224
+
225
+ <div class="ss-body">
226
+ <StepPackage
227
+ v-if="step === 1"
228
+ :tiers="tiers"
229
+ :selected-tier-id="selectedTierId"
230
+ :custom-quantity="customQuantity"
231
+ :pixel-price="Number(wall?.pixel_price || 0)"
232
+ :currency="wall?.currency || 'USD'"
233
+ @select-tier="selectTier"
234
+ @set-custom="setCustomQuantity"
235
+ @next="next"
236
+ />
237
+
238
+ <StepGoal
239
+ v-else-if="step === 2"
240
+ :goals="goals"
241
+ :selected-goal-id="selectedGoalId"
242
+ :currency="wall?.currency || 'USD'"
243
+ @select="selectGoal"
244
+ @next="next"
245
+ />
246
+
247
+ <StepBlock
248
+ v-else-if="step === 3"
249
+ :wall="wall"
250
+ :supporters="supporters"
251
+ :pixel-count="pixelCount"
252
+ :selection="selection"
253
+ @select="selectArea"
254
+ @update:pixel-count="updatePixelCount"
255
+ @next="next"
256
+ />
257
+
258
+ <StepCustomize
259
+ v-else-if="step === 4"
260
+ :form="customForm"
261
+ :is-guest="!authStore.signedIn"
262
+ :pixel-w="selection?.w ?? 1"
263
+ :pixel-h="selection?.h ?? 1"
264
+ @update="updateCustomForm"
265
+ @next="next"
266
+ />
267
+
268
+ <StepPay
269
+ v-else-if="step === 5"
270
+ :wall="wall"
271
+ :selected-goal="selectedGoal"
272
+ :selected-tier="selectedTier"
273
+ :selection="selection"
274
+ :form="customForm"
275
+ :pixel-count="pixelCount"
276
+ :gateway-provider="gatewayProvider"
277
+ :processing="processing"
278
+ :error-msg="errorMsg"
279
+ @set-gateway="setGateway"
280
+ @confirm="confirm"
281
+ @cancel="close"
282
+ />
283
+ </div>
284
+
285
+ <div class="ss-footer">
286
+ <button v-if="step > 1" type="button" class="ss-back" @click="back">
287
+ ← {{ $t('tv.dashboard.indie_wall.step_back') }}
288
+ </button>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </template>
293
+
294
+ <style scoped lang="scss">
295
+ .ss-overlay {
296
+ position: fixed; inset: 0;
297
+ background: rgba(0,0,0,0.72);
298
+ display: flex; align-items: center; justify-content: center;
299
+ z-index: 300; padding: 16px;
300
+ }
301
+
302
+ .ss-modal {
303
+ background: #191B20; border: 1px solid #272930;
304
+ padding: 24px; width: 100%; max-width: 600px;
305
+ max-height: 92vh; overflow-y: auto; position: relative;
306
+ display: flex; flex-direction: column; gap: 16px;
307
+ }
308
+
309
+ .ss-close {
310
+ position: absolute; top: 10px; right: 10px;
311
+ background: #272930; border: none; color: var(--title-fg, #fff);
312
+ width: 28px; height: 28px;
313
+ display: flex; align-items: center; justify-content: center;
314
+ cursor: pointer; font-size: 1.1rem; line-height: 1;
315
+ }
316
+
317
+ .ss-availability {
318
+ display: flex; align-items: center; gap: 6px;
319
+ font-size: 0.78rem; color: var(--secondary-info-fg, #aaa);
320
+ background: #13161C; padding: 6px 10px; border: 1px solid #1e2028;
321
+ strong { color: var(--chip-text, #D297FF); font-weight: 600; }
322
+ }
323
+
324
+ .ss-stepper-bar { display: flex; align-items: center; gap: 4px; margin-top: 8px; }
325
+ .ss-dot {
326
+ flex: 1; height: 4px; background: #272930; position: relative;
327
+ span {
328
+ position: absolute; top: -16px; left: 50%; transform: translateX(-50%);
329
+ font-size: 0.65rem; color: var(--secondary-info-fg, #555);
330
+ }
331
+ &.active { background: var(--chip-text, #D297FF); }
332
+ &.current span { color: var(--chip-text, #D297FF); font-weight: 700; }
333
+ }
334
+
335
+ .ss-body { display: flex; flex-direction: column; }
336
+ .ss-footer { display: flex; justify-content: flex-start; }
337
+
338
+ .ss-back {
339
+ background: none; border: none;
340
+ color: var(--secondary-info-fg, #888);
341
+ cursor: pointer; font-size: 0.82rem; padding: 6px 0;
342
+ &:hover { color: var(--title-fg, #fff); }
343
+ }
344
+
345
+ /* ── Drawer variant ─────────────────────────────────────────── */
346
+ .ss-overlay--drawer {
347
+ align-items: flex-end;
348
+ background: rgba(0,0,0,0.5);
349
+
350
+ @media (min-width: 768px) {
351
+ align-items: stretch;
352
+ justify-content: flex-end;
353
+ }
354
+ }
355
+
356
+ .ss-modal--drawer {
357
+ max-width: 100%;
358
+ max-height: 88vh;
359
+ border-top: 2px solid rgba(107,21,172,0.5);
360
+ border-left: none;
361
+ border-right: none;
362
+ border-bottom: none;
363
+
364
+ @media (min-width: 768px) {
365
+ max-width: 440px;
366
+ max-height: 100vh;
367
+ height: 100%;
368
+ border-top: none;
369
+ border-left: 2px solid rgba(107,21,172,0.5);
370
+ }
371
+ }
372
+ </style>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ invoiceId: string;
4
+ isPaid: boolean;
5
+ apiBaseUrl?: string;
6
+ }>();
7
+
8
+ function downloadPdf() {
9
+ if (!props.isPaid) return;
10
+ const base = props.apiBaseUrl ?? "";
11
+ window.open(`${base}/api/v1/invoices/${props.invoiceId}/pdf`, "_blank");
12
+ }
13
+ </script>
14
+
15
+ <template>
16
+ <button
17
+ v-if="isPaid"
18
+ class="mg-invoice-download"
19
+ @click="downloadPdf"
20
+ >
21
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
22
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
23
+ <polyline points="7 10 12 15 17 10" />
24
+ <line x1="12" y1="15" x2="12" y2="3" />
25
+ </svg>
26
+ {{ $t?.("invoice.download") ?? "Download Invoice" }}
27
+ </button>
28
+ </template>
29
+
30
+ <style lang="scss" scoped>
31
+ .mg-invoice-download {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: 6px;
35
+ padding: 6px 14px;
36
+ border: 1px solid var(--inactive, #d1d5db);
37
+ border-radius: 6px;
38
+ background: transparent;
39
+ color: var(--active, #374151);
40
+ font-size: 13px;
41
+ font-weight: 500;
42
+ cursor: pointer;
43
+ transition: all 0.2s;
44
+
45
+ &:hover {
46
+ border-color: #4f46e5;
47
+ color: #4f46e5;
48
+ }
49
+ }
50
+ </style>
@@ -0,0 +1,74 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ modelValue: "monthly" | "annual";
4
+ yearlyDiscount?: number;
5
+ }>();
6
+
7
+ const emit = defineEmits<{
8
+ "update:modelValue": [value: "monthly" | "annual"];
9
+ }>();
10
+ </script>
11
+
12
+ <template>
13
+ <div class="mg-billing-toggle">
14
+ <button
15
+ class="toggle-option"
16
+ :class="{ active: modelValue === 'monthly' }"
17
+ @click="emit('update:modelValue', 'monthly')"
18
+ >
19
+ {{ $t?.("pricing.monthly") ?? "Monthly" }}
20
+ </button>
21
+ <button
22
+ class="toggle-option"
23
+ :class="{ active: modelValue === 'annual' }"
24
+ @click="emit('update:modelValue', 'annual')"
25
+ >
26
+ {{ $t?.("pricing.annual") ?? "Annual" }}
27
+ <span v-if="yearlyDiscount" class="discount-badge">
28
+ -{{ yearlyDiscount }}%
29
+ </span>
30
+ </button>
31
+ </div>
32
+ </template>
33
+
34
+ <style lang="scss" scoped>
35
+ .mg-billing-toggle {
36
+ display: inline-flex;
37
+ border: 1px solid var(--inactive, #e5e7eb);
38
+ border-radius: 8px;
39
+ overflow: hidden;
40
+ }
41
+
42
+ .toggle-option {
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 6px;
46
+ padding: 10px 20px;
47
+ border: none;
48
+ background: transparent;
49
+ color: var(--inactive, #6b7280);
50
+ font-size: 14px;
51
+ font-weight: 500;
52
+ cursor: pointer;
53
+ transition: all 0.2s;
54
+
55
+ &.active {
56
+ background: #4f46e5;
57
+ color: #fff;
58
+ }
59
+
60
+ &:hover:not(.active) {
61
+ background: var(--body-bg-card, #f9fafb);
62
+ }
63
+ }
64
+
65
+ .discount-badge {
66
+ display: inline-flex;
67
+ padding: 2px 6px;
68
+ border-radius: 4px;
69
+ background: #22c55e;
70
+ color: #fff;
71
+ font-size: 11px;
72
+ font-weight: 600;
73
+ }
74
+ </style>