@kennofizet/rewardplay-frontend 1.0.2 → 1.0.4

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 (72) hide show
  1. package/README.md +108 -40
  2. package/package.json +1 -1
  3. package/src/api/index.js +142 -4
  4. package/src/components/BonusSection.vue +256 -0
  5. package/src/components/CustomOptionDisplay.vue +77 -0
  6. package/src/components/CustomSelect.vue +220 -0
  7. package/src/components/LoadingSource.vue +72 -9
  8. package/src/components/LoginScreen.vue +119 -13
  9. package/src/components/MainGame.vue +40 -11
  10. package/src/components/StatMapPreview.vue +294 -0
  11. package/src/components/game/AlertMessage.vue +153 -0
  12. package/src/components/game/BackgroundLuckyWheel.vue +8089 -0
  13. package/src/components/game/EventsPopup.vue +408 -0
  14. package/src/components/game/ExpBar.vue +140 -0
  15. package/src/components/game/GameLayout.vue +33 -9
  16. package/src/components/game/ItemBox.vue +43 -19
  17. package/src/components/game/LevelBadge.vue +112 -0
  18. package/src/components/game/MenuItem.vue +10 -9
  19. package/src/components/game/RankingItem.vue +58 -17
  20. package/src/components/game/RewardCard.vue +84 -20
  21. package/src/components/game/RewardItem.vue +43 -3
  22. package/src/components/game/RewardItemsTooltip.vue +217 -0
  23. package/src/components/game/Sector.vue +15 -2
  24. package/src/components/game/TopCoinCard.vue +178 -0
  25. package/src/components/game/TopCoinCardSkeleton.vue +133 -0
  26. package/src/components/game/TopMeCard.vue +121 -0
  27. package/src/components/game/TopMeCardSkeleton.vue +92 -0
  28. package/src/components/game/TopMenu.vue +23 -11
  29. package/src/components/game/TopWeekCard.vue +333 -0
  30. package/src/components/game/TopWeekCardSkeleton.vue +143 -0
  31. package/src/components/game/WeekDay.vue +1 -1
  32. package/src/components/game/ZoneSelectModal.vue +76 -0
  33. package/src/components/game/ZoneSelectPage.vue +64 -0
  34. package/src/components/ui/ErrorState.vue +69 -0
  35. package/src/composables/useResourceLoader.js +1 -69
  36. package/src/composables/useTimezone.js +100 -0
  37. package/src/data/eventsDemo.js +63 -0
  38. package/src/data/shopDemo.js +160 -0
  39. package/src/i18n/index.js +40 -0
  40. package/src/i18n/translations/en.js +789 -0
  41. package/src/i18n/translations/vi.js +792 -0
  42. package/src/i18n/utils.js +40 -0
  43. package/src/index.js +25 -0
  44. package/src/pages/ComingSoonPage.vue +371 -0
  45. package/src/pages/RewardPlayPage.vue +425 -49
  46. package/src/pages/game/BagGearPage.vue +1805 -117
  47. package/src/pages/game/DailyRewardPage.vue +251 -116
  48. package/src/pages/game/LuckyWheelPage.vue +4 -546
  49. package/src/pages/game/ManageSettingPage.vue +195 -0
  50. package/src/pages/game/RankingPage.vue +214 -0
  51. package/src/pages/game/RulesPage.vue +26 -13
  52. package/src/pages/game/ShopPage.vue +812 -11
  53. package/src/pages/game/manage-setting/ManageZonesPage.vue +579 -0
  54. package/src/pages/game/manage-setting/SettingBoxTicketBuffPage.vue +502 -0
  55. package/src/pages/game/manage-setting/SettingDailyRewardConfigPage.vue +288 -0
  56. package/src/pages/game/manage-setting/SettingEventsListPage.vue +836 -0
  57. package/src/pages/game/manage-setting/SettingItemSetsListPage.vue +1132 -0
  58. package/src/pages/game/manage-setting/SettingItemsListPage.vue +1134 -0
  59. package/src/pages/game/manage-setting/SettingLevelExpListPage.vue +242 -0
  60. package/src/pages/game/manage-setting/SettingOptionsListPage.vue +775 -0
  61. package/src/pages/game/manage-setting/SettingShopListPage.vue +851 -0
  62. package/src/pages/game/manage-setting/SettingStackBonusListPage.vue +280 -0
  63. package/src/pages/game/manage-setting/SettingStatsTransformListPage.vue +853 -0
  64. package/src/utils/constants.js +141 -0
  65. package/src/utils/globalData.js +174 -0
  66. package/src/utils/imageResolverRuntime.js +92 -0
  67. package/src/utils/numberFormat.js +17 -0
  68. package/src/utils/resourceLoader.js +228 -52
  69. package/src/utils/settingApiResponse.js +35 -0
  70. package/src/utils/statHelpers.js +144 -0
  71. package/src/utils/timezone.js +123 -0
  72. package/src/pages/game/RankingCoinPage.vue +0 -184
@@ -0,0 +1,220 @@
1
+ <template>
2
+ <div class="custom-select" :class="{ 'is-open': isOpen, 'is-disabled': disabled }" ref="dropdownRef">
3
+ <div
4
+ class="select-trigger"
5
+ @click="toggleDropdown"
6
+ :class="triggerClass"
7
+ >
8
+ <span class="select-value">{{ displayValue }}</span>
9
+ <span class="select-arrow" :class="{ 'rotated': isOpen }">▼</span>
10
+ </div>
11
+ <Transition name="dropdown">
12
+ <div v-if="isOpen" class="select-dropdown">
13
+ <div
14
+ v-for="option in options"
15
+ :key="option.value"
16
+ class="select-option"
17
+ :class="{ 'is-selected': modelValue === option.value, 'is-disabled': option.disabled }"
18
+ @click="selectOption(option)"
19
+ >
20
+ {{ option.label }}
21
+ </div>
22
+ </div>
23
+ </Transition>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
29
+
30
+ const props = defineProps({
31
+ modelValue: {
32
+ type: [String, Number],
33
+ default: ''
34
+ },
35
+ options: {
36
+ type: Array,
37
+ required: true,
38
+ validator: (options) => {
39
+ return options.every(opt => opt.hasOwnProperty('value') && opt.hasOwnProperty('label'))
40
+ }
41
+ },
42
+ placeholder: {
43
+ type: String,
44
+ default: 'Select...'
45
+ },
46
+ disabled: {
47
+ type: Boolean,
48
+ default: false
49
+ },
50
+ triggerClass: {
51
+ type: String,
52
+ default: ''
53
+ }
54
+ })
55
+
56
+ const emit = defineEmits(['update:modelValue', 'change'])
57
+
58
+ const isOpen = ref(false)
59
+ const dropdownRef = ref(null)
60
+
61
+ const displayValue = computed(() => {
62
+ if (!props.modelValue) {
63
+ return props.placeholder
64
+ }
65
+ const selectedOption = props.options.find(opt => opt.value === props.modelValue)
66
+ return selectedOption ? selectedOption.label : props.placeholder
67
+ })
68
+
69
+ const toggleDropdown = () => {
70
+ if (props.disabled) return
71
+ isOpen.value = !isOpen.value
72
+ }
73
+
74
+ const selectOption = (option) => {
75
+ if (option.disabled) return
76
+ emit('update:modelValue', option.value)
77
+ emit('change', option.value)
78
+ isOpen.value = false
79
+ }
80
+
81
+ const handleClickOutside = (event) => {
82
+ if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
83
+ isOpen.value = false
84
+ }
85
+ }
86
+
87
+ onMounted(() => {
88
+ document.addEventListener('click', handleClickOutside)
89
+ })
90
+
91
+ onUnmounted(() => {
92
+ document.removeEventListener('click', handleClickOutside)
93
+ })
94
+ </script>
95
+
96
+ <style scoped>
97
+ .custom-select {
98
+ position: relative;
99
+ display: inline-block;
100
+ max-width: 100%;
101
+ }
102
+
103
+ .select-trigger {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: space-between;
107
+ padding: 8px 12px;
108
+ background: rgba(255, 255, 255, 0.1);
109
+ border: 1px solid rgba(255, 255, 255, 0.2);
110
+ border-radius: 8px;
111
+ color: #fff;
112
+ font-size: 0.9rem;
113
+ cursor: pointer;
114
+ transition: all 0.2s ease;
115
+ user-select: none;
116
+ }
117
+
118
+ .custom-select.is-disabled .select-trigger {
119
+ opacity: 0.6;
120
+ cursor: not-allowed;
121
+ }
122
+
123
+ .select-trigger:hover:not(.is-disabled) {
124
+ background: rgba(255, 255, 255, 0.15);
125
+ border-color: rgba(255, 255, 255, 0.3);
126
+ }
127
+
128
+ .custom-select.is-open .select-trigger {
129
+ border-color: rgba(255, 255, 255, 0.4);
130
+ }
131
+
132
+ .select-value {
133
+ flex: 1;
134
+ text-align: left;
135
+ overflow: hidden;
136
+ text-overflow: ellipsis;
137
+ white-space: nowrap;
138
+ }
139
+
140
+ .select-arrow {
141
+ margin-left: 8px;
142
+ font-size: 0.7rem;
143
+ color: rgba(255, 255, 255, 0.8);
144
+ transition: transform 0.2s ease;
145
+ flex-shrink: 0;
146
+ }
147
+
148
+ .select-arrow.rotated {
149
+ transform: rotate(180deg);
150
+ }
151
+
152
+ .select-dropdown {
153
+ position: absolute;
154
+ top: calc(100% + 4px);
155
+ left: 0;
156
+ right: 0;
157
+ background: #253344;
158
+ border: 1px solid #1a2332;
159
+ border-radius: 8px;
160
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
161
+ z-index: 1000;
162
+ max-height: 200px;
163
+ overflow-y: auto;
164
+ margin-top: 4px;
165
+ }
166
+
167
+ .select-option {
168
+ padding: 10px 12px;
169
+ color: #d0d4d6;
170
+ font-size: 0.9rem;
171
+ cursor: pointer;
172
+ transition: background-color 0.2s ease;
173
+ }
174
+
175
+ .select-option:hover:not(.is-disabled) {
176
+ background: rgba(255, 255, 255, 0.1);
177
+ }
178
+
179
+ .select-option.is-selected {
180
+ background: rgba(246, 169, 1, 0.2);
181
+ color: #f6a901;
182
+ font-weight: 500;
183
+ }
184
+
185
+ .select-option.is-disabled {
186
+ opacity: 0.5;
187
+ cursor: not-allowed;
188
+ }
189
+
190
+ /* Dropdown transition */
191
+ .dropdown-enter-active,
192
+ .dropdown-leave-active {
193
+ transition: opacity 0.2s ease, transform 0.2s ease;
194
+ }
195
+
196
+ .dropdown-enter-from,
197
+ .dropdown-leave-to {
198
+ opacity: 0;
199
+ transform: translateY(-10px);
200
+ }
201
+
202
+ /* Scrollbar styling */
203
+ .select-dropdown::-webkit-scrollbar {
204
+ width: 6px;
205
+ }
206
+
207
+ .select-dropdown::-webkit-scrollbar-track {
208
+ background: #1a2332;
209
+ border-radius: 3px;
210
+ }
211
+
212
+ .select-dropdown::-webkit-scrollbar-thumb {
213
+ background: #253344;
214
+ border-radius: 3px;
215
+ }
216
+
217
+ .select-dropdown::-webkit-scrollbar-thumb:hover {
218
+ background: #2d3a4b;
219
+ }
220
+ </style>
@@ -37,9 +37,37 @@ const props = defineProps({
37
37
  type: Number,
38
38
  default: 0
39
39
  },
40
+ userDataProgress: {
41
+ type: Number,
42
+ default: 0
43
+ },
40
44
  backgroundImage: {
41
45
  type: String,
42
46
  default: null
47
+ },
48
+ loadingTitle: {
49
+ type: String,
50
+ default: 'LOADING'
51
+ },
52
+ loadingSubtitle: {
53
+ type: String,
54
+ default: 'Preparing your gaming experience...'
55
+ },
56
+ loadingLabels: {
57
+ type: Object,
58
+ default: () => ({
59
+ assets: 'Loading Assets',
60
+ unzipping: 'Unzipping Files',
61
+ userData: 'Load Data User'
62
+ })
63
+ },
64
+ loadingSubTexts: {
65
+ type: Object,
66
+ default: () => ({
67
+ assets: '',
68
+ unzipping: '',
69
+ userData: ''
70
+ })
43
71
  }
44
72
  })
45
73
 
@@ -155,7 +183,8 @@ const updateCanvas = () => {
155
183
  ctx.font = 'bold 48px Arial, sans-serif'
156
184
  ctx.textAlign = 'center'
157
185
  ctx.textBaseline = 'middle'
158
- ctx.fillText('LOADING', width / 2, titleY)
186
+ const loadingTitle = props.loadingTitle
187
+ ctx.fillText(loadingTitle, width / 2, titleY)
159
188
 
160
189
  // Reset shadow
161
190
  ctx.shadowBlur = 0
@@ -163,11 +192,12 @@ const updateCanvas = () => {
163
192
  // Draw subtitle
164
193
  ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
165
194
  ctx.font = '18px Arial, sans-serif'
166
- ctx.fillText('Preparing your gaming experience...', width / 2, titleY + 50)
195
+ const loadingSubtitle = props.loadingSubtitle
196
+ ctx.fillText(loadingSubtitle, width / 2, titleY + 50)
167
197
 
168
198
  // Draw main loading bar with chunky game-like style
169
- // Calculate total progress as average of Loading Assets and Unzipping Files
170
- const totalProgress = (props.loadingProgress + props.unzipProgress) / 2
199
+ // Calculate total progress as average of Loading Assets, Unzipping Files, and Load Data User
200
+ const totalProgress = (props.loadingProgress + props.unzipProgress + props.userDataProgress) / 3
171
201
  const barWidth = width * 0.65
172
202
  const barHeight = 35
173
203
  const barX = (width - barWidth) / 2
@@ -262,12 +292,25 @@ const updateCanvas = () => {
262
292
 
263
293
  // Draw progress items with better styling
264
294
  const progressItems = [
265
- { label: 'Loading Assets', progress: Math.min(props.loadingProgress, 100) },
266
- { label: 'Unzipping Files', progress: Math.min(props.unzipProgress, 100) }
295
+ {
296
+ label: props.loadingLabels.assets,
297
+ progress: Math.min(props.loadingProgress, 100),
298
+ subText: props.loadingSubTexts.assets || ''
299
+ },
300
+ {
301
+ label: props.loadingLabels.unzipping,
302
+ progress: Math.min(props.unzipProgress, 100),
303
+ subText: props.loadingSubTexts.unzipping || ''
304
+ },
305
+ {
306
+ label: props.loadingLabels.userData,
307
+ progress: Math.min(props.userDataProgress, 100),
308
+ subText: props.loadingSubTexts.userData || ''
309
+ }
267
310
  ]
268
311
 
269
312
  const startY = height * 0.68
270
- const itemHeight = 30
313
+ const itemHeight = 40 // Increased to accommodate sub-text
271
314
  const spacing = 12
272
315
 
273
316
  progressItems.forEach((item, index) => {
@@ -300,10 +343,30 @@ const updateCanvas = () => {
300
343
  ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'
301
344
  ctx.textAlign = 'right'
302
345
  ctx.fillText(`${Math.round(item.progress)}%`, itemBarX + itemBarWidth, y - 5)
346
+
347
+ // Sub-text under progress bar
348
+ if (item.subText) {
349
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
350
+ ctx.font = '12px Arial, sans-serif'
351
+ ctx.textAlign = 'left'
352
+ ctx.textBaseline = 'top'
353
+ // Truncate long text
354
+ const maxWidth = itemBarWidth - 10
355
+ let displayText = item.subText
356
+ const metrics = ctx.measureText(displayText)
357
+ if (metrics.width > maxWidth) {
358
+ // Truncate with ellipsis
359
+ while (ctx.measureText(displayText + '...').width > maxWidth && displayText.length > 0) {
360
+ displayText = displayText.slice(0, -1)
361
+ }
362
+ displayText = displayText + '...'
363
+ }
364
+ ctx.fillText(displayText, itemBarX, y + 12)
365
+ }
303
366
  })
304
367
 
305
368
  // Check if loading is complete
306
- if (props.loadingProgress >= 100 && props.unzipProgress >= 100) {
369
+ if (props.loadingProgress >= 100 && props.unzipProgress >= 100 && props.userDataProgress >= 100) {
307
370
  emit('loading-complete')
308
371
  }
309
372
  }
@@ -325,7 +388,7 @@ const handleResize = () => {
325
388
  })
326
389
  }
327
390
 
328
- watch([() => props.isLoading, () => props.loadingProgress, () => props.unzipProgress], () => {
391
+ watch([() => props.isLoading, () => props.loadingProgress, () => props.unzipProgress, () => props.userDataProgress], () => {
329
392
  if (props.isLoading) {
330
393
  nextTick(() => {
331
394
  if (animationFrameId) {
@@ -30,7 +30,8 @@ const emit = defineEmits(['login-success', 'login-failed'])
30
30
  const gameApi = inject('gameApi')
31
31
  const canvasRef = ref(null)
32
32
  const error = ref(null)
33
- const errorType = ref(null) // 'no-api', 'invalid-token', 'connection-failed', 'server-error'
33
+ const errorType = ref(null) // 'no-api', 'invalid-token', 'connection-failed', 'server-error', 'no-zone'
34
+ const errorMessage = ref(null) // Custom error message to display
34
35
  const status = ref('checking') // 'checking', 'success', 'error'
35
36
  let animationFrameId = null
36
37
  let pulseAnimation = 0
@@ -67,6 +68,10 @@ const drawContent = (ctx, width, height) => {
67
68
  drawCheckingAnimation(ctx, centerX, centerY, width, height)
68
69
  } else if (status.value === 'error') {
69
70
  drawErrorIcon(ctx, centerX, centerY, width, height)
71
+ // Draw error message if available
72
+ if (errorMessage.value) {
73
+ drawErrorMessage(ctx, centerX, centerY + Math.min(width, height) * 0.15, width, height)
74
+ }
70
75
  } else if (status.value === 'success') {
71
76
  drawSuccessIcon(ctx, centerX, centerY, width, height)
72
77
  }
@@ -172,6 +177,8 @@ const drawErrorIcon = (ctx, centerX, centerY, width, height) => {
172
177
  drawConnectionError(ctx, centerX, centerY + size * 1.2, width, height)
173
178
  } else if (errorType.value === 'server-error') {
174
179
  drawServerError(ctx, centerX, centerY + size * 1.2, width, height)
180
+ } else if (errorType.value === 'no-zone') {
181
+ drawNoZoneError(ctx, centerX, centerY + size * 1.2, width, height)
175
182
  } else {
176
183
  drawGenericError(ctx, centerX, centerY + size * 1.2, width, height)
177
184
  }
@@ -426,6 +433,91 @@ const drawGenericError = (ctx, centerX, centerY, width, height) => {
426
433
  }
427
434
  }
428
435
 
436
+ const drawNoZoneError = (ctx, centerX, centerY, width, height) => {
437
+ const iconSize = Math.min(width, height) * 0.08
438
+
439
+ // Draw zone/server icon with X mark
440
+ ctx.strokeStyle = '#ff6666'
441
+ ctx.fillStyle = '#ff6666'
442
+ ctx.lineWidth = iconSize * 0.12
443
+ ctx.lineCap = 'round'
444
+
445
+ // Draw server/zone box
446
+ const boxX = centerX - iconSize * 0.4
447
+ const boxY = centerY - iconSize * 0.3
448
+ const boxWidth = iconSize * 0.8
449
+ const boxHeight = iconSize * 0.6
450
+
451
+ ctx.strokeRect(boxX, boxY, boxWidth, boxHeight)
452
+
453
+ // Draw X mark over the box
454
+ ctx.strokeStyle = '#ff0000'
455
+ ctx.lineWidth = iconSize * 0.15
456
+ const xSize = iconSize * 0.5
457
+
458
+ ctx.beginPath()
459
+ ctx.moveTo(boxX + boxWidth * 0.2, boxY + boxHeight * 0.2)
460
+ ctx.lineTo(boxX + boxWidth * 0.8, boxY + boxHeight * 0.8)
461
+ ctx.stroke()
462
+
463
+ ctx.beginPath()
464
+ ctx.moveTo(boxX + boxWidth * 0.8, boxY + boxHeight * 0.2)
465
+ ctx.lineTo(boxX + boxWidth * 0.2, boxY + boxHeight * 0.8)
466
+ ctx.stroke()
467
+ }
468
+
469
+ const drawErrorMessage = (ctx, centerX, centerY, width, height) => {
470
+ if (!errorMessage.value) return
471
+
472
+ ctx.save()
473
+ ctx.fillStyle = '#ffffff'
474
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'
475
+ ctx.lineWidth = 2
476
+ ctx.textAlign = 'center'
477
+ ctx.textBaseline = 'middle'
478
+
479
+ // Calculate font size based on screen size
480
+ const fontSize = Math.min(width, height) * 0.025
481
+ ctx.font = `bold ${fontSize}px Arial, sans-serif`
482
+
483
+ // Draw text with outline for better visibility
484
+ const text = errorMessage.value
485
+ const maxWidth = width * 0.8
486
+ const words = text.split(' ')
487
+ const lines = []
488
+ let currentLine = ''
489
+
490
+ // Word wrap
491
+ words.forEach(word => {
492
+ const testLine = currentLine + (currentLine ? ' ' : '') + word
493
+ const metrics = ctx.measureText(testLine)
494
+ if (metrics.width > maxWidth && currentLine) {
495
+ lines.push(currentLine)
496
+ currentLine = word
497
+ } else {
498
+ currentLine = testLine
499
+ }
500
+ })
501
+ if (currentLine) {
502
+ lines.push(currentLine)
503
+ }
504
+
505
+ // Draw each line
506
+ const lineHeight = fontSize * 1.4
507
+ const startY = centerY - ((lines.length - 1) * lineHeight) / 2
508
+
509
+ lines.forEach((line, index) => {
510
+ const y = startY + index * lineHeight
511
+
512
+ // Draw text outline (shadow)
513
+ ctx.strokeText(line, centerX, y)
514
+ // Draw text
515
+ ctx.fillText(line, centerX, y)
516
+ })
517
+
518
+ ctx.restore()
519
+ }
520
+
429
521
  const drawSuccessIcon = (ctx, centerX, centerY, width, height) => {
430
522
  const size = Math.min(width, height) * 0.15
431
523
  const x = centerX
@@ -498,11 +590,14 @@ const animate = () => {
498
590
  }
499
591
  }
500
592
 
593
+ const translator = inject('translator', null)
594
+ const t = translator || ((key) => key)
595
+
501
596
  const checkUser = async () => {
502
597
  if (!gameApi) {
503
598
  status.value = 'error'
504
599
  errorType.value = 'no-api'
505
- emit('login-failed', 'API not available')
600
+ emit('login-failed', t('component.login.apiNotAvailable'))
506
601
  return
507
602
  }
508
603
 
@@ -514,16 +609,15 @@ const checkUser = async () => {
514
609
 
515
610
  if (response.data.success) {
516
611
  status.value = 'success'
612
+ // Determine is_manager flag from response (if present)
613
+ const isManager = !!(response.data?.datas?.is_manager)
517
614
  // Wait a moment to show success icon
518
615
  setTimeout(() => {
519
- emit('login-success', {
520
- ...response.data.user,
521
- imagesUrl: response.data.images_url || ''
522
- })
616
+ emit('login-success', { is_manager: isManager })
523
617
  }, 500)
524
618
  } else {
525
619
  status.value = 'error'
526
- const errorMsg = response.data.error || 'Authentication failed'
620
+ const errorMsg = response.data.message || t('component.login.authenticationFailed')
527
621
 
528
622
  // Determine error type
529
623
  if (errorMsg.toLowerCase().includes('token') || errorMsg.toLowerCase().includes('invalid')) {
@@ -538,20 +632,32 @@ const checkUser = async () => {
538
632
  }
539
633
  } catch (err) {
540
634
  status.value = 'error'
541
- const errorMessage = err.response?.data?.error || err.message || 'Connection failed'
635
+ const errorMsg = err.response?.data?.message || err.message || t('component.login.connectionFailed')
636
+
637
+ // Handle 403 error specifically
638
+ if (err.response?.status === 403) {
639
+ errorType.value = 'no-zone'
640
+ errorMessage.value = 'User not in any zone or not managing any server'
641
+ emit('login-failed', errorMessage.value)
642
+ return
643
+ }
542
644
 
543
- // Determine error type
544
- if (err.code === 'ECONNREFUSED' || err.message.includes('Network') || errorMessage.includes('Connection')) {
645
+ // Determine error type for other errors
646
+ if (err.code === 'ECONNREFUSED' || err.message.includes('Network') || errorMsg.includes('Connection')) {
545
647
  errorType.value = 'connection-failed'
648
+ errorMessage.value = null
546
649
  } else if (err.response?.status >= 500) {
547
650
  errorType.value = 'server-error'
548
- } else if (err.response?.status === 401 || errorMessage.includes('token') || errorMessage.includes('Invalid')) {
651
+ errorMessage.value = null
652
+ } else if (err.response?.status === 401 || errorMsg.includes('token') || errorMsg.includes('Invalid')) {
549
653
  errorType.value = 'invalid-token'
654
+ errorMessage.value = null
550
655
  } else {
551
656
  errorType.value = 'generic'
657
+ errorMessage.value = null
552
658
  }
553
659
 
554
- emit('login-failed', errorMessage)
660
+ emit('login-failed', errorMsg)
555
661
  }
556
662
  }
557
663
 
@@ -564,6 +670,7 @@ watch(() => props.showLogin, (newVal) => {
564
670
  status.value = 'checking'
565
671
  error.value = null
566
672
  errorType.value = null
673
+ errorMessage.value = null
567
674
  pulseAnimation = 0
568
675
  errorAnimation = 0
569
676
  animate()
@@ -583,7 +690,6 @@ onMounted(() => {
583
690
  if (props.showLogin) {
584
691
  nextTick(() => {
585
692
  animate()
586
- checkUser()
587
693
  })
588
694
  }
589
695
  })
@@ -1,30 +1,42 @@
1
1
  <template>
2
2
  <GameLayout
3
3
  :rotate="rotate"
4
+ :is-manager="isManager"
4
5
  @page-change="handlePageChange"
5
6
  @icon-click="handleIconClick"
6
7
  >
7
8
  <component :is="currentPage" />
9
+ <template #overlay>
10
+ <EventsPopup
11
+ v-if="showEventsPopup && activeEvents.length > 0"
12
+ :events="activeEvents"
13
+ @close="showEventsPopup = false"
14
+ />
15
+ </template>
8
16
  </GameLayout>
9
17
  </template>
10
18
 
11
19
  <script setup>
12
- import { ref, shallowRef } from 'vue'
20
+ import { ref, shallowRef, onMounted, inject } from 'vue'
13
21
  import GameLayout from './game/GameLayout.vue'
22
+ import EventsPopup from './game/EventsPopup.vue'
14
23
  import DailyRewardPage from '../pages/game/DailyRewardPage.vue'
15
24
  import BagGearPage from '../pages/game/BagGearPage.vue'
16
25
  import LuckyWheelPage from '../pages/game/LuckyWheelPage.vue'
17
- import RankingCoinPage from '../pages/game/RankingCoinPage.vue'
26
+ import RankingPage from '../pages/game/RankingPage.vue'
18
27
  import RulesPage from '../pages/game/RulesPage.vue'
28
+ import ShopPage from '../pages/game/ShopPage.vue'
29
+ import ComingSoonPage from '../pages/ComingSoonPage.vue'
30
+ import ManageSettingPage from '../pages/game/ManageSettingPage.vue'
19
31
 
20
32
  const props = defineProps({
21
- imagesUrl: {
22
- type: String,
23
- default: ''
24
- },
25
33
  rotate: {
26
34
  type: Boolean,
27
35
  default: true
36
+ },
37
+ isManager: {
38
+ type: Boolean,
39
+ default: false
28
40
  }
29
41
  })
30
42
 
@@ -32,19 +44,37 @@ const pageMap = {
32
44
  'reward': DailyRewardPage,
33
45
  'bag': BagGearPage,
34
46
  'lucky-wheel': LuckyWheelPage,
35
- 'ranking': RankingCoinPage,
36
- 'rules': RulesPage,
37
- 'shop': () => import('../pages/game/ShopPage.vue').catch(() => ({ default: () => null }))
47
+ 'ranking': RankingPage,
48
+ 'rules': ComingSoonPage,
49
+ 'shop': ShopPage,
50
+ 'manage-setting': ManageSettingPage
38
51
  }
39
52
 
40
53
  const currentPage = shallowRef(DailyRewardPage) // Default to DailyReward
54
+ const showEventsPopup = ref(false)
55
+ const activeEvents = ref([])
56
+ const gameApi = inject('gameApi', null)
57
+
58
+ onMounted(async () => {
59
+ if (gameApi) {
60
+ try {
61
+ const res = await gameApi.getPlayerEvents()
62
+ const list = res?.data?.datas?.events ?? []
63
+ activeEvents.value = Array.isArray(list) ? list : []
64
+ if (activeEvents.value.length > 0) {
65
+ showEventsPopup.value = true
66
+ }
67
+ } catch (e) {
68
+ activeEvents.value = []
69
+ }
70
+ }
71
+ })
41
72
 
42
73
  const handlePageChange = (page) => {
43
74
  if (page === 'back') {
44
75
  // Handle back navigation
45
76
  return
46
77
  }
47
-
48
78
  if (pageMap[page]) {
49
79
  const pageComponent = pageMap[page]
50
80
  if (typeof pageComponent === 'function') {
@@ -60,7 +90,6 @@ const handlePageChange = (page) => {
60
90
 
61
91
  const handleIconClick = (icon) => {
62
92
  // Handle icon clicks (notifications, settings, etc.)
63
- console.log('Icon clicked:', icon)
64
93
  }
65
94
  </script>
66
95