@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,314 @@
1
+ <script setup lang="ts">
2
+ import MuralCanvas from './MuralCanvas.vue'
3
+ import { suggestPosition } from '../../services/indieWallService'
4
+
5
+ const props = defineProps<{
6
+ wall: any
7
+ supporters: any[]
8
+ pixelCount: number
9
+ selection: { x: number; y: number; w: number; h: number } | null
10
+ }>()
11
+
12
+ const emit = defineEmits<{
13
+ (e: 'select', rect: { x: number; y: number; w: number; h: number }): void
14
+ (e: 'update:pixel-count', count: number): void
15
+ (e: 'next'): void
16
+ }>()
17
+
18
+ const mode = ref<'auto' | 'manual'>('auto')
19
+ const loading = ref(false)
20
+ const errorMsg = ref('')
21
+
22
+ // All width × height combinations whose product equals pixelCount
23
+ const dimensionOptions = computed(() => {
24
+ const opts: { w: number; h: number; label: string }[] = []
25
+ if (!props.wall) return opts
26
+ const max = Math.max(props.wall.width, props.wall.height)
27
+ for (let w = 1; w <= max; w++) {
28
+ if (props.pixelCount % w !== 0) continue
29
+ const h = props.pixelCount / w
30
+ if (w > props.wall.width || h > props.wall.height) continue
31
+ opts.push({ w, h, label: `${w} × ${h}` })
32
+ }
33
+ return opts
34
+ })
35
+
36
+ const selectedDim = ref<{ w: number; h: number } | null>(null)
37
+
38
+ function nearSquareDim(count: number): { w: number; h: number } {
39
+ if (count <= 1) return { w: 1, h: 1 }
40
+ const root = Math.floor(Math.sqrt(count))
41
+ for (let w = root; w >= 1; w--) {
42
+ if (count % w === 0) {
43
+ return { w, h: count / w }
44
+ }
45
+ }
46
+ return { w: count, h: 1 }
47
+ }
48
+
49
+ function initDefaultDim() {
50
+ if (dimensionOptions.value.length === 0) {
51
+ selectedDim.value = nearSquareDim(props.pixelCount)
52
+ } else {
53
+ // Default to the most "square" option
54
+ const sq = nearSquareDim(props.pixelCount)
55
+ const match = dimensionOptions.value.find(o => o.w === sq.w && o.h === sq.h)
56
+ selectedDim.value = match ?? dimensionOptions.value[0]
57
+ }
58
+ }
59
+
60
+ async function autoSuggest() {
61
+ if (!props.wall || !selectedDim.value) return
62
+ errorMsg.value = ''
63
+ loading.value = true
64
+ try {
65
+ const { w, h } = selectedDim.value
66
+ const res = await suggestPosition(props.wall.id, w, h)
67
+ const data = res.data?.data || res.data
68
+ if (data?.x !== undefined) {
69
+ emit('select', { x: data.x, y: data.y, w: data.width, h: data.height })
70
+ } else {
71
+ fallbackToManual()
72
+ }
73
+ } catch (err: any) {
74
+ const code = err?.response?.status
75
+ if (code === 422) {
76
+ errorMsg.value = err?.response?.data?.error || 'No free area large enough for this size.'
77
+ } else {
78
+ // Network/404 → soft fallback: try local empty-space scan
79
+ tryLocalScan()
80
+ }
81
+ } finally {
82
+ loading.value = false
83
+ }
84
+ }
85
+
86
+ // Local fallback: client-side scan for a free area using current supporters
87
+ function tryLocalScan() {
88
+ if (!props.wall || !selectedDim.value) return
89
+ const { w, h } = selectedDim.value
90
+ const W = props.wall.width
91
+ const H = props.wall.height
92
+ const occ = new Set<string>()
93
+ for (const s of props.supporters || []) {
94
+ const pw = s.width || 1
95
+ const ph = s.height || 1
96
+ for (let r = s.y; r < s.y + ph; r++) {
97
+ for (let c = s.x; c < s.x + pw; c++) {
98
+ occ.add(`${c},${r}`)
99
+ }
100
+ }
101
+ }
102
+ for (let y = 0; y <= H - h; y++) {
103
+ for (let x = 0; x <= W - w; x++) {
104
+ let free = true
105
+ outer: for (let r = y; r < y + h; r++) {
106
+ for (let c = x; c < x + w; c++) {
107
+ if (occ.has(`${c},${r}`)) { free = false; break outer }
108
+ }
109
+ }
110
+ if (free) {
111
+ emit('select', { x, y, w, h })
112
+ errorMsg.value = ''
113
+ return
114
+ }
115
+ }
116
+ }
117
+ fallbackToManual('No free area large enough — try a different shape or pick manually.')
118
+ }
119
+
120
+ function fallbackToManual(msg = '') {
121
+ mode.value = 'manual'
122
+ errorMsg.value = msg
123
+ }
124
+
125
+ function onTapEmpty(coord: { x: number; y: number }) {
126
+ if (!props.wall || !selectedDim.value) return
127
+ const { w, h } = selectedDim.value
128
+ const x = Math.max(0, Math.min(coord.x, props.wall.width - w))
129
+ const y = Math.max(0, Math.min(coord.y, props.wall.height - h))
130
+ emit('select', { x, y, w, h })
131
+ }
132
+
133
+ watch(() => props.pixelCount, () => {
134
+ initDefaultDim()
135
+ if (mode.value === 'auto') autoSuggest()
136
+ }, { immediate: false })
137
+
138
+ watch(selectedDim, (newDim, oldDim) => {
139
+ if (!newDim) return
140
+ if (!oldDim || newDim.w !== oldDim.w || newDim.h !== oldDim.h) {
141
+ if (mode.value === 'auto') autoSuggest()
142
+ // In manual, clear current selection so user has to place again
143
+ else emit('select', null as any)
144
+ }
145
+ })
146
+
147
+ watch(mode, (newMode) => {
148
+ errorMsg.value = ''
149
+ if (newMode === 'auto') autoSuggest()
150
+ })
151
+
152
+ onMounted(() => {
153
+ initDefaultDim()
154
+ if (mode.value === 'auto') autoSuggest()
155
+ })
156
+
157
+ function confirm() {
158
+ if (props.selection) emit('next')
159
+ }
160
+ </script>
161
+
162
+ <template>
163
+ <div class="sw-step">
164
+ <h3 class="sw-step-title">{{ $t('tv.dashboard.indie_wall.step_block_title') }}</h3>
165
+ <p class="sw-step-help">
166
+ {{ $t('tv.dashboard.indie_wall.step_block_help', { count: pixelCount }) }}
167
+ </p>
168
+
169
+ <div class="sw-qty-row">
170
+ <span class="sw-qty-label">{{ $t('tv.dashboard.indie_wall.step_block_quantity') }}:</span>
171
+ <button type="button" class="sw-qty-btn" :disabled="pixelCount <= 1" @click="emit('update:pixel-count', pixelCount - 1)">−</button>
172
+ <input
173
+ type="number"
174
+ min="1"
175
+ :value="pixelCount"
176
+ class="sw-qty-input"
177
+ @change="(e) => emit('update:pixel-count', Math.max(1, parseInt((e.target as HTMLInputElement).value) || 1))"
178
+ />
179
+ <button type="button" class="sw-qty-btn" @click="emit('update:pixel-count', pixelCount + 1)">+</button>
180
+ <span class="sw-qty-total">= {{ $t('tv.dashboard.indie_wall.step_block_total', { total: pixelCount }) }}</span>
181
+ </div>
182
+
183
+ <div v-if="dimensionOptions.length > 1" class="sw-shape-row">
184
+ <span class="sw-shape-label">{{ $t('tv.dashboard.indie_wall.step_block_shape') }}:</span>
185
+ <button
186
+ v-for="opt in dimensionOptions"
187
+ :key="opt.label"
188
+ type="button"
189
+ :class="['sw-shape-btn', { active: selectedDim?.w === opt.w && selectedDim?.h === opt.h }]"
190
+ @click="selectedDim = { w: opt.w, h: opt.h }"
191
+ >
192
+ {{ opt.label }}
193
+ </button>
194
+ </div>
195
+
196
+ <div class="sw-mode-toggle">
197
+ <button
198
+ type="button"
199
+ :class="['sw-mode-btn', { active: mode === 'auto' }]"
200
+ @click="mode = 'auto'"
201
+ >
202
+ {{ $t('tv.dashboard.indie_wall.step_block_auto') }}
203
+ </button>
204
+ <button
205
+ type="button"
206
+ :class="['sw-mode-btn', { active: mode === 'manual' }]"
207
+ @click="mode = 'manual'"
208
+ >
209
+ {{ $t('tv.dashboard.indie_wall.step_block_manual') }}
210
+ </button>
211
+ </div>
212
+
213
+ <div v-if="errorMsg" class="sw-error">{{ errorMsg }}</div>
214
+
215
+ <MuralCanvas
216
+ :wall="wall"
217
+ :supporters="supporters"
218
+ :selection="selection"
219
+ :interactive="mode === 'manual'"
220
+ :height="360"
221
+ @tap-empty="onTapEmpty"
222
+ />
223
+
224
+ <div v-if="selection" class="sw-selected">
225
+ <span>{{ $t('tv.dashboard.indie_wall.mural_position') }}: ({{ selection.x }}, {{ selection.y }})</span>
226
+ <span>{{ selection.w }} × {{ selection.h }} = {{ selection.w * selection.h }} {{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
227
+ </div>
228
+
229
+ <div class="sw-actions">
230
+ <button type="button" class="sw-confirm" :disabled="!selection || loading" @click="confirm">
231
+ {{ loading ? '…' : $t('tv.dashboard.indie_wall.step_block_confirm') }}
232
+ </button>
233
+ </div>
234
+ </div>
235
+ </template>
236
+
237
+ <style scoped lang="scss">
238
+ .sw-step { display: flex; flex-direction: column; gap: 12px; }
239
+ .sw-step-title { font-size: 1.05rem; font-weight: 600; color: var(--title-fg, #fff); margin: 0; }
240
+ .sw-step-help { font-size: 0.82rem; color: var(--secondary-info-fg, #aaa); margin: 0 0 4px; }
241
+
242
+ .sw-qty-row {
243
+ display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
244
+ background: #13161C; padding: 8px 10px; border: 1px solid #1e2028;
245
+ }
246
+ .sw-qty-label { font-size: 0.78rem; color: var(--secondary-info-fg, #aaa); }
247
+ .sw-qty-btn {
248
+ width: 30px; height: 30px; background: #1e2028; color: var(--title-fg, #fff);
249
+ border: 1px solid #272930; cursor: pointer;
250
+ font-size: 1rem; line-height: 1; padding: 0;
251
+ display: flex; align-items: center; justify-content: center;
252
+ &:hover:not(:disabled) { border-color: var(--chip-text, #D297FF); }
253
+ &:disabled { opacity: 0.4; cursor: not-allowed; }
254
+ }
255
+ .sw-qty-input {
256
+ width: 56px; height: 30px;
257
+ background: #0E0F13; border: 1px solid #272930; color: var(--title-fg, #fff);
258
+ text-align: center; font-size: 0.85rem;
259
+ -moz-appearance: textfield;
260
+ &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
261
+ &:focus { border-color: var(--chip-text, #D297FF); outline: none; }
262
+ }
263
+ .sw-qty-total { font-size: 0.78rem; color: var(--chip-text, #D297FF); margin-left: auto; }
264
+
265
+ .sw-shape-row {
266
+ display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
267
+ }
268
+ .sw-shape-label {
269
+ font-size: 0.78rem; color: var(--secondary-info-fg, #aaa); margin-right: 4px;
270
+ }
271
+ .sw-shape-btn {
272
+ background: #1e2028; color: var(--title-fg, #fff);
273
+ border: 1px solid #272930; padding: 5px 10px;
274
+ font-size: 0.78rem; cursor: pointer;
275
+ &:hover { border-color: var(--chip-text, #D297FF); }
276
+ &.active {
277
+ background: var(--chip-text, #D297FF); color: #13161C;
278
+ border-color: var(--chip-text, #D297FF); font-weight: 600;
279
+ }
280
+ }
281
+
282
+ .sw-mode-toggle { display: flex; gap: 4px; }
283
+ .sw-mode-btn {
284
+ flex: 1; background: #1e2028; color: var(--secondary-info-fg, #888);
285
+ border: 1px solid #272930; padding: 8px; cursor: pointer;
286
+ font-size: 0.8rem; transition: all 0.15s;
287
+ &.active {
288
+ background: var(--chip-text, #D297FF);
289
+ color: #13161C; border-color: var(--chip-text, #D297FF); font-weight: 600;
290
+ }
291
+ }
292
+
293
+ .sw-error {
294
+ background: rgba(255,59,48,0.07);
295
+ border: 1px solid rgba(255,59,48,0.25);
296
+ color: #ff6b6b; padding: 7px 10px; font-size: 0.8rem;
297
+ }
298
+
299
+ .sw-selected {
300
+ display: flex; justify-content: space-between;
301
+ color: var(--secondary-info-fg, #888);
302
+ font-size: 0.78rem;
303
+ background: #13161C; padding: 8px 10px; border: 1px solid #1e2028;
304
+ }
305
+
306
+ .sw-actions { display: flex; gap: 8px; justify-content: flex-end; }
307
+ .sw-confirm {
308
+ background: var(--chip-text, #D297FF);
309
+ color: #13161C; border: none; padding: 10px 18px;
310
+ font-weight: 700; font-size: 0.85rem; cursor: pointer;
311
+ &:hover { opacity: 0.88; }
312
+ &:disabled { opacity: 0.4; cursor: not-allowed; }
313
+ }
314
+ </style>