@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
package/README.md CHANGED
@@ -1,63 +1,131 @@
1
- # RewardPlay Frontend Package
1
+ # @kennofizet/rewardplay-frontend
2
2
 
3
- Vue.js 3 component library for RewardPlay game interface.
3
+ Vue 3 frontend package for **RewardPlay**: game UI (bag, shop, daily reward, ranking, settings) that talks to the RewardPlay backend API.
4
4
 
5
- ## Installation
5
+ ## What This Package Is
6
6
 
7
- **Important:** This package is part of an npm workspace. Dependencies are installed at the **root level**, not in this directory.
7
+ - **Vue 3 component library** for the RewardPlay game interface (login, loading, bag/gear, shop, daily rewards, ranking, manage settings).
8
+ - **Token-based**: you pass a backend URL and token; the package uses them for all API calls.
9
+ - **Embeddable**: mount it in a host app inside a div (e.g. after login or when entering the game section).
10
+ - Backend API, auth, and data format are defined in **@kennofizet/rewardplay-backend**. Use that package (or its docs) for token, endpoints, and data structures.
8
11
 
9
- ### From Root (Recommended)
10
- ```bash
11
- cd ../.. # Go to monorepo root
12
- npm install
13
- ```
12
+ ## Installation
14
13
 
15
- ### From Package Directory
16
14
  ```bash
17
- npm install
15
+ npm i @kennofizet/rewardplay-frontend
18
16
  ```
19
- This will install dependencies, but they'll be hoisted to the root `node_modules/` (this is normal for workspaces).
20
17
 
21
- ## What `npm install` Does
18
+ **Peer dependency:** Vue 3 (`vue@^3.2.0`). The host project must provide Vue.
22
19
 
23
- When you run `npm install` in this package:
20
+ ## Setup: Mount Point and Initialization
24
21
 
25
- 1. Installs dependencies listed in `package.json`
26
- 2. ✅ Creates/updates `node_modules/` at the **root level** (not here)
27
- 3. ❌ Does NOT create local `node_modules/` (workspace hoisting)
28
- 4. ❌ Does NOT build or create dist files
29
- 5. ❌ Does NOT modify source files
22
+ ### 1. Mount point in template
30
23
 
31
- **This is normal!** npm workspaces hoist dependencies to the root to avoid duplication.
24
+ Render a stable DOM node only when you are ready to mount RewardPlay (e.g. after you have a token):
32
25
 
33
- ## Development
26
+ ```html
27
+ <div v-show="initialized" id="rewardplay-mount-point"></div>
28
+ ```
34
29
 
35
- For local development (like in the test environment), the package uses source files directly from `src/`. No build step is required.
30
+ - Use `v-show="initialized"` (or `v-if`) so the element exists only when `initialized` is true, then mount in a `nextTick` after setting `initialized = true`.
31
+ - The ID (`rewardplay-mount-point`) is what you pass to `document.getElementById` before mounting.
32
+
33
+ ### 2. Initialize and mount in your app
34
+
35
+ Example (Vue 3 Composition API with `createApp`):
36
+
37
+ ```js
38
+ import { createApp, ref, nextTick } from 'vue'
39
+ import RewardPlay, { RewardPlayPage } from '@kennofizet/rewardplay-frontend'
40
+
41
+ const initialized = ref(false)
42
+ let rewardPlayApp = null
43
+
44
+ async function initialize() {
45
+ const backendUrl = 'https://your-backend.com' // from your config
46
+ if (!backendUrl?.trim()) {
47
+ // handle error
48
+ return
49
+ }
50
+
51
+ try {
52
+ const token = await fetchToken() // get token from your backend (see rewardplay-backend)
53
+ initialized.value = true
54
+ await nextTick()
55
+
56
+ const mountPoint = document.getElementById('rewardplay-mount-point')
57
+ if (!mountPoint) {
58
+ initialized.value = false
59
+ return
60
+ }
61
+
62
+ rewardPlayApp = createApp(RewardPlayPage, {
63
+ imageUrls: [],
64
+ scriptUrls: [],
65
+ stylesheetUrls: [],
66
+ fontUrls: [],
67
+ backgroundImage: null,
68
+ language: 'vi', // 'en' | 'vi'
69
+ // enableUnzip: true, // if your RewardPlayPage supports it
70
+ })
71
+
72
+ rewardPlayApp.use(RewardPlay, {
73
+ backendUrl: backendUrl.trim(),
74
+ token,
75
+ })
76
+
77
+ rewardPlayApp.mount(mountPoint)
78
+ } catch (err) {
79
+ initialized.value = false
80
+ // handle err
81
+ }
82
+ }
83
+
84
+ // e.g. in onMounted or after login
85
+ onMounted(() => {
86
+ initialize()
87
+ })
88
+ ```
36
89
 
37
- The `package.json` is configured to point to `src/index.js` for local development, so Vite can resolve the source files directly.
90
+ ### 3. Plugin options (required)
38
91
 
39
- ## Building for Production
92
+ | Option | Type | Description |
93
+ |------------|--------|--------------------------------|
94
+ | `backendUrl` | string | Base URL of RewardPlay API |
95
+ | `token` | string | RewardPlay auth token |
40
96
 
41
- If you need to build the package for distribution:
97
+ Both are required when calling `app.use(RewardPlay, { backendUrl, token })`.
42
98
 
43
- ```bash
44
- npm run production
45
- ```
99
+ ### 4. RewardPlayPage root props (optional)
46
100
 
47
- This will create built files in the `dist/` directory using Laravel Mix.
101
+ | Prop | Type | Default | Description |
102
+ |------------------|--------|--------|--------------------------------|
103
+ | `imageUrls` | Array | `[]` | URLs for image manifest/assets |
104
+ | `scriptUrls` | Array | `[]` | Script URLs to load |
105
+ | `stylesheetUrls` | Array | `[]` | CSS URLs |
106
+ | `fontUrls` | Array | `[]` | Font URLs |
107
+ | `backgroundImage`| string | null | Background image URL |
108
+ | `language` | string | `'en'` | UI language: `'en'` or `'vi'` |
109
+ | `rotate` | boolean| true | Auto-rotate in portrait |
110
+ | `customStyles` | Object | `{}` | Extra inline/CSS variables |
48
111
 
49
- ## Local Development Setup
112
+ ## Backend and data
50
113
 
51
- When using this package locally (via `file:` in package.json), Vite will resolve the source files directly. No build needed!
114
+ - **Token**: Your host app must obtain the RewardPlay token (e.g. from your own backend that talks to RewardPlay). How to issue/validate the token and which endpoints to call is defined in **@kennofizet/rewardplay-backend**.
115
+ - **API**: The frontend calls endpoints under `backendUrl` (e.g. `/api/rewardplay/auth/user-data`, `/api/rewardplay/player/shop`, etc.). Base URL, routes, and response shapes are documented in the backend package.
116
+ - For API contracts, env vars, and server setup, read **@kennofizet/rewardplay-backend** (or the backend README in the same repo).
52
117
 
53
- ## Where Are My Dependencies?
118
+ ## Exports
54
119
 
55
- Dependencies are installed at:
56
- ```
57
- rewardplay-packages/node_modules/ (root level)
58
- ```
120
+ - **default**: plugin object `{ install }` — use with `app.use(RewardPlay, { backendUrl, token })`.
121
+ - **RewardPlayPage**: root Vue component to mount.
122
+ - **installGameModule**, **createGameApi**, **RewardPlayPage**, **ComingSoonPage**, **LoadingSource**, **LoginScreen**.
123
+ - Utilities: **ResourceLoader**, **useResourceLoader**, constants helpers, **createTranslator**, **translations**.
59
124
 
60
- Not at:
61
- ```
62
- packages/frontend/node_modules/ (doesn't exist - this is normal!)
63
- ```
125
+ ## Development
126
+
127
+ If you work inside the repo (e.g. `rewardplay-packages`):
128
+
129
+ - Dependencies are usually installed at the **root** of the repo: `npm install` at root.
130
+ - The package entry is `src/index.js`; the host can resolve source directly (no build step required for development).
131
+ - For a production build of the frontend package, run `npm run production` in this package (output in `dist/`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kennofizet/rewardplay-frontend",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "RewardPlay frontend package - Token-based API client for RewardPlay",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
package/src/api/index.js CHANGED
@@ -24,12 +24,150 @@ export function createGameApi(backendUrl, token) {
24
24
  },
25
25
  })
26
26
 
27
+ // Add interceptor to automatically include zone_id from localStorage as header
28
+ api.interceptors.request.use((config) => {
29
+ try {
30
+ const selectedZone = localStorage.getItem('selected_zone')
31
+ if (selectedZone) {
32
+ const zone = JSON.parse(selectedZone)
33
+ if (zone && zone.id) {
34
+ // Add zone_id to headers (similar to X-RewardPlay-Token)
35
+ config.headers = config.headers || {}
36
+ if (!config.headers['X-RewardPlay-Zone-Id']) {
37
+ config.headers['X-RewardPlay-Zone-Id'] = zone.id.toString()
38
+ }
39
+ }
40
+ }
41
+ } catch (e) {
42
+ // Silently fail if localStorage is not available or zone is invalid
43
+ console.warn('Failed to get zone_id from localStorage:', e)
44
+ }
45
+ // When sending FormData, remove Content-Type so the browser sets multipart/form-data with boundary
46
+ if (config.data instanceof FormData) {
47
+ delete config.headers['Content-Type']
48
+ }
49
+ return config
50
+ })
51
+
27
52
  return {
28
53
  // Auth
29
- checkUser: () => api.post('/api/rewardplay/auth/check'),
30
-
31
- // Demo
32
- getDemo: () => api.get('/api/rewardplay/demo'),
54
+ checkUser: () => api.get('/api/rewardplay/auth/check'),
55
+ getUserData: () => api.get('/api/rewardplay/auth/user-data'),
56
+ // Zones the current user belongs to
57
+ getZones: () => api.get('/api/rewardplay/player/zones'),
58
+ getCustomImages: (params) => api.get('/api/rewardplay/player/custom-images', { params }),
59
+ // Manifest
60
+ getManifest: () => api.get('/api/rewardplay/manifest'),
61
+ // Ranking (period: day | week | month | year)
62
+ getRanking: (period = 'day') => api.get('/api/rewardplay/ranking', { params: { period } }),
63
+ // Zones the current user can manage (for settings)
64
+ getManagedZones: () => api.get('/api/rewardplay/player/managed-zones'),
65
+ // Zone management (settings)
66
+ getAllZones: (params) => api.get('/api/rewardplay/zones', { params }),
67
+ createZone: (data) => api.post('/api/rewardplay/zones', data),
68
+ updateZone: (id, data) => api.put(`/api/rewardplay/zones/${id}`, data),
69
+ deleteZone: (id) => api.delete(`/api/rewardplay/zones/${id}`),
70
+ // Zone users (server members + assigned)
71
+ getZoneUsers: (zoneId, params) => api.get(`/api/rewardplay/zones/${zoneId}/users`, { params }),
72
+ assignZoneUser: (zoneId, userId) => api.post(`/api/rewardplay/zones/${zoneId}/users`, { user_id: userId }),
73
+ removeZoneUser: (zoneId, userId) => api.delete(`/api/rewardplay/zones/${zoneId}/users/${userId}`),
74
+
75
+ // Setting Items CRUD
76
+ getSettingItems: (params) => api.get('/api/rewardplay/setting-items', { params }),
77
+ suggestSettingItems: () => api.post('/api/rewardplay/setting-items/suggest'),
78
+ createSettingItem: (data) => api.post('/api/rewardplay/setting-items', data),
79
+ updateSettingItem: (id, data) => {
80
+ if (data instanceof FormData) {
81
+ if (!data.has('_method')) data.append('_method', 'PUT')
82
+ return api.post(`/api/rewardplay/setting-items/${id}`, data)
83
+ }
84
+ return api.put(`/api/rewardplay/setting-items/${id}`, data)
85
+ },
86
+ deleteSettingItem: (id) => api.delete(`/api/rewardplay/setting-items/${id}`),
87
+
88
+ // Setting Options CRUD
89
+ getSettingOptions: (params) => api.get('/api/rewardplay/setting-options', { params }),
90
+ getSettingOption: (id) => api.get(`/api/rewardplay/setting-options/${id}`),
91
+ createSettingOption: (data) => api.post('/api/rewardplay/setting-options', data),
92
+ updateSettingOption: (id, data) => api.put(`/api/rewardplay/setting-options/${id}`, data),
93
+ deleteSettingOption: (id) => api.delete(`/api/rewardplay/setting-options/${id}`),
94
+
95
+ // Setting Stats Transforms CRUD
96
+ getSettingStatsTransforms: (params) => api.get('/api/rewardplay/setting-stats-transforms', { params }),
97
+ createSettingStatsTransform: (data) => api.post('/api/rewardplay/setting-stats-transforms', data),
98
+ updateSettingStatsTransform: (id, data) => api.put(`/api/rewardplay/setting-stats-transforms/${id}`, data),
99
+ deleteSettingStatsTransform: (id) => api.delete(`/api/rewardplay/setting-stats-transforms/${id}`),
100
+ suggestSettingStatsTransforms: () => api.post('/api/rewardplay/setting-stats-transforms/suggest'),
101
+ getSettingStatsTransformsAllowedKeys: () => api.get('/api/rewardplay/setting-stats-transforms/allowed-keys'),
102
+
103
+ // Global data (accessible to both player and manage)
104
+ getAllStats: () => api.get('/api/rewardplay/stats/all'),
105
+ getRewardTypes: (mode) => api.get('/api/rewardplay/stats/reward-types', { params: { mode } }),
106
+
107
+ // Setting Item Sets CRUD
108
+ getSettingItemSets: (params) => api.get('/api/rewardplay/setting-item-sets', { params }),
109
+ getItemsForZone: (params) => api.get('/api/rewardplay/setting-items/items-for-zone', { params }),
110
+ createSettingItemSet: (data) => api.post('/api/rewardplay/setting-item-sets', data),
111
+ updateSettingItemSet: (id, data) => api.put(`/api/rewardplay/setting-item-sets/${id}`, data),
112
+ deleteSettingItemSet: (id) => api.delete(`/api/rewardplay/setting-item-sets/${id}`),
113
+
114
+ // Setting Stack Bonuses CRUD
115
+ getStackBonuses: (params) => api.get('/api/rewardplay/setting-stack-bonuses', { params }),
116
+ getStackBonus: (id) => api.get(`/api/rewardplay/setting-stack-bonuses/${id}`),
117
+ createStackBonus: (data) => api.post('/api/rewardplay/setting-stack-bonuses', data),
118
+ updateStackBonus: (id, data) => api.put(`/api/rewardplay/setting-stack-bonuses/${id}`, data),
119
+ deleteStackBonus: (id) => api.delete(`/api/rewardplay/setting-stack-bonuses/${id}`),
120
+ suggestStackBonuses: () => api.post('/api/rewardplay/setting-stack-bonuses/suggest'),
121
+
122
+ // Setting Daily Rewards (Manage)
123
+ getDailyRewardConfigs: (params) => api.get('/api/rewardplay/setting-daily-rewards', { params }), // Expects month/year
124
+ saveDailyRewardConfig: (data) => api.post('/api/rewardplay/setting-daily-rewards', data), // Update or Create based on date
125
+ suggestDailyRewards: (data) => api.post('/api/rewardplay/setting-daily-rewards/suggest', data),
126
+
127
+ // Setting Level Exps (Manage)
128
+ getLevelExps: (params) => api.get('/api/rewardplay/setting-level-exps', { params }),
129
+ getLevelExp: (id) => api.get(`/api/rewardplay/setting-level-exps/${id}`),
130
+ createLevelExp: (data) => api.post('/api/rewardplay/setting-level-exps', data),
131
+ updateLevelExp: (id, data) => api.put(`/api/rewardplay/setting-level-exps/${id}`, data),
132
+ deleteLevelExp: (id) => api.delete(`/api/rewardplay/setting-level-exps/${id}`),
133
+ suggestLevelExps: () => api.post('/api/rewardplay/setting-level-exps/suggest'),
134
+
135
+ // Setting Events (Manage)
136
+ getSettingEvents: (params) => api.get('/api/rewardplay/setting-events', { params }),
137
+ getSettingEvent: (id) => api.get(`/api/rewardplay/setting-events/${id}`),
138
+ suggestSettingEvents: () => api.post('/api/rewardplay/setting-events/suggest'),
139
+ createSettingEvent: (data) => api.post('/api/rewardplay/setting-events', data),
140
+ updateSettingEvent: (id, data) => {
141
+ if (data instanceof FormData) {
142
+ return api.post(`/api/rewardplay/setting-events/${id}`, data)
143
+ }
144
+ return api.put(`/api/rewardplay/setting-events/${id}`, data)
145
+ },
146
+ deleteSettingEvent: (id) => api.delete(`/api/rewardplay/setting-events/${id}`),
147
+
148
+ // Setting Shop Items (Manage)
149
+ getSettingShopItems: (params) => api.get('/api/rewardplay/setting-shop-items', { params }),
150
+ getSettingShopItem: (id) => api.get(`/api/rewardplay/setting-shop-items/${id}`),
151
+ suggestSettingShopItems: () => api.post('/api/rewardplay/setting-shop-items/suggest'),
152
+ createSettingShopItem: (data) => api.post('/api/rewardplay/setting-shop-items', data),
153
+ updateSettingShopItem: (id, data) => api.put(`/api/rewardplay/setting-shop-items/${id}`, data),
154
+ deleteSettingShopItem: (id) => api.delete(`/api/rewardplay/setting-shop-items/${id}`),
155
+
156
+ // Player Daily Rewards & Bag
157
+ getPlayerDailyRewardState: () => api.get('/api/rewardplay/player/daily-rewards'), // Includes stack info
158
+ collectDailyReward: () => api.post('/api/rewardplay/player/daily-rewards/collect'),
159
+ getPlayerBag: () => api.get('/api/rewardplay/player/bag'),
160
+ saveGears: (gears) => api.post('/api/rewardplay/player/bag/gears', { gears }),
161
+ openBox: (userBagItemId, quantity = 1) =>
162
+ api.post('/api/rewardplay/player/bag/open-box', { user_bag_item_id: userBagItemId, quantity: Math.max(1, Math.min(parseInt(quantity, 10) || 1, 99)) }),
163
+
164
+ // Player Events (active events for popup)
165
+ getPlayerEvents: () => api.get('/api/rewardplay/player/events'),
166
+
167
+ // Player Shop (active shop items + purchase)
168
+ getPlayerShop: () => api.get('/api/rewardplay/player/shop'),
169
+ purchaseShopItem: (shopItemId, quantity = 1) =>
170
+ api.post('/api/rewardplay/player/shop/purchase', { shop_item_id: shopItemId, quantity }),
33
171
  }
34
172
  }
35
173
 
@@ -0,0 +1,256 @@
1
+ <template>
2
+ <div class="bonus-section">
3
+ <label>{{ label }}</label>
4
+ <div class="bonus-list">
5
+ <div
6
+ v-for="(bonus, index) in bonuses"
7
+ :key="index"
8
+ class="bonus-item"
9
+ >
10
+ <template v-if="bonus.type === 'custom_option' && bonus.name">
11
+ <!-- Show display component for custom options (snapshot) -->
12
+ <CustomOptionDisplay
13
+ :name="bonus.name"
14
+ :properties="bonus.properties || {}"
15
+ :stats-label="t('page.manageSetting.settingItems.stats')"
16
+ class="bonus-custom-display"
17
+ />
18
+ </template>
19
+ <template v-else>
20
+ <!-- Show unified selector for stats or when nothing selected yet -->
21
+ <CustomSelect
22
+ v-model="bonus.selectedValue"
23
+ :options="unifiedOptions"
24
+ :placeholder="selectKeyPlaceholder"
25
+ @change="handleKeyChange(index)"
26
+ trigger-class="bonus-key-select"
27
+ />
28
+ <input
29
+ v-if="bonus.type === 'stat' && bonus.selectedValue"
30
+ v-model.number="bonus.value"
31
+ type="number"
32
+ step="0.01"
33
+ :placeholder="valuePlaceholder"
34
+ class="bonus-value-input"
35
+ />
36
+ </template>
37
+ <button
38
+ type="button"
39
+ class="btn-remove-bonus"
40
+ @click="handleRemove(index)"
41
+ :title="removeLabel"
42
+ >
43
+ ×
44
+ </button>
45
+ </div>
46
+ <button
47
+ type="button"
48
+ class="btn-add-bonus"
49
+ @click="handleAdd"
50
+ >
51
+ <span class="btn-add-icon">+</span>
52
+ <span>{{ addOptionLabel }}</span>
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </template>
57
+
58
+ <script setup>
59
+ import { inject, computed } from 'vue'
60
+ import CustomSelect from './CustomSelect.vue'
61
+ import CustomOptionDisplay from './CustomOptionDisplay.vue'
62
+
63
+ const translator = inject('translator', null)
64
+ const t = translator || ((key) => key)
65
+
66
+ const props = defineProps({
67
+ label: {
68
+ type: String,
69
+ required: true
70
+ },
71
+ bonuses: {
72
+ type: Array,
73
+ required: true
74
+ },
75
+ bonusKeyOptions: {
76
+ type: Array,
77
+ required: true
78
+ },
79
+ customOptionOptions: {
80
+ type: Array,
81
+ default: () => []
82
+ },
83
+ selectKeyPlaceholder: {
84
+ type: String,
85
+ default: 'Select Key'
86
+ },
87
+ valuePlaceholder: {
88
+ type: String,
89
+ default: 'Value'
90
+ },
91
+ addOptionLabel: {
92
+ type: String,
93
+ default: 'Add Option'
94
+ },
95
+ removeLabel: {
96
+ type: String,
97
+ default: 'Remove'
98
+ }
99
+ })
100
+
101
+ const emit = defineEmits(['add', 'remove', 'key-change'])
102
+
103
+ // Unified options combining stats and custom options
104
+ const unifiedOptions = computed(() => {
105
+ const options = []
106
+
107
+ // Add regular stats
108
+ props.bonusKeyOptions.forEach(stat => {
109
+ options.push({
110
+ value: stat.value,
111
+ label: stat.label,
112
+ type: 'stat',
113
+ isCustom: false
114
+ })
115
+ })
116
+
117
+ // Add custom options
118
+ props.customOptionOptions.forEach(customOption => {
119
+ options.push({
120
+ value: customOption.value,
121
+ label: customOption.label,
122
+ type: 'custom_option',
123
+ isCustom: true,
124
+ properties: customOption.properties
125
+ })
126
+ })
127
+
128
+ return options
129
+ })
130
+
131
+ const handleAdd = () => {
132
+ emit('add')
133
+ }
134
+
135
+ const handleRemove = (index) => {
136
+ emit('remove', index)
137
+ }
138
+
139
+ const handleKeyChange = (index) => {
140
+ emit('key-change', index)
141
+ }
142
+ </script>
143
+
144
+ <style scoped>
145
+ .bonus-section {
146
+ display: flex;
147
+ flex-direction: column;
148
+ }
149
+
150
+ .bonus-section label {
151
+ margin-bottom: 5px;
152
+ font-size: 13px;
153
+ color: #d0d4d6;
154
+ font-weight: 500;
155
+ }
156
+
157
+ .bonus-list {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 10px;
161
+ }
162
+
163
+ .bonus-item {
164
+ display: flex;
165
+ gap: 8px;
166
+ align-items: center;
167
+ }
168
+
169
+ .bonus-key-select {
170
+ flex: 1;
171
+ min-width: 0;
172
+ }
173
+
174
+ .bonus-value-input {
175
+ flex: 1;
176
+ min-width: 0;
177
+ padding: 8px 10px;
178
+ background: #253344;
179
+ border: 1px solid #1a2332;
180
+ color: #d0d4d6;
181
+ font-size: 14px;
182
+ }
183
+
184
+ .bonus-custom-value {
185
+ flex: 1;
186
+ display: flex;
187
+ flex-direction: column;
188
+ gap: 4px;
189
+ padding: 8px 10px;
190
+ background: #1a2332;
191
+ border: 1px solid #253344;
192
+ border-radius: 4px;
193
+ cursor: help;
194
+ }
195
+
196
+ .custom-value-label {
197
+ font-size: 12px;
198
+ color: #f6a901;
199
+ font-weight: 500;
200
+ }
201
+
202
+ .custom-value-count {
203
+ font-size: 11px;
204
+ color: #999;
205
+ }
206
+
207
+ .btn-remove-bonus {
208
+ padding: 8px 12px;
209
+ background: #ff6b6b;
210
+ color: white;
211
+ border: none;
212
+ border-radius: 4px;
213
+ cursor: pointer;
214
+ font-size: 18px;
215
+ line-height: 1;
216
+ transition: background 0.2s;
217
+ flex-shrink: 0;
218
+ }
219
+
220
+ .btn-remove-bonus:hover {
221
+ background: #ee5a5a;
222
+ }
223
+
224
+ .btn-add-bonus {
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: center;
228
+ gap: 6px;
229
+ padding: 10px 16px;
230
+ background: #253344;
231
+ border: 2px dashed #1a2332;
232
+ color: #d0d4d6;
233
+ cursor: pointer;
234
+ font-size: 14px;
235
+ border-radius: 4px;
236
+ transition: all 0.2s;
237
+ width: 100%;
238
+ margin-top: 5px;
239
+ }
240
+
241
+ .btn-add-bonus:hover {
242
+ background: #1a2332;
243
+ border-color: #f6a901;
244
+ color: #f6a901;
245
+ }
246
+
247
+ .btn-add-icon {
248
+ font-size: 18px;
249
+ font-weight: bold;
250
+ line-height: 1;
251
+ }
252
+
253
+ .bonus-custom-display {
254
+ flex: 1;
255
+ }
256
+ </style>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div
3
+ class="custom-option-display"
4
+ :title="tooltipText"
5
+ >
6
+ <span class="custom-option-name">{{ name }}</span>
7
+ <span class="custom-option-info">{{ statsCount }} {{ statsLabel }}</span>
8
+ </div>
9
+ </template>
10
+
11
+ <script setup>
12
+ import { computed, inject } from 'vue'
13
+
14
+ const props = defineProps({
15
+ name: {
16
+ type: String,
17
+ required: true
18
+ },
19
+ properties: {
20
+ type: Object,
21
+ default: () => ({})
22
+ },
23
+ statsLabel: {
24
+ type: String,
25
+ default: 'stats'
26
+ }
27
+ })
28
+
29
+ const statsCount = computed(() => {
30
+ return Object.keys(props.properties || {}).length
31
+ })
32
+
33
+ const tooltipText = computed(() => {
34
+ if (!props.properties || Object.keys(props.properties).length === 0) {
35
+ return props.name
36
+ }
37
+
38
+ // Format properties for tooltip display
39
+ const statsList = Object.entries(props.properties)
40
+ .map(([key, value]) => `${key}: ${value}`)
41
+ .join('\n')
42
+
43
+ return `${props.name}\n\n${statsList}`
44
+ })
45
+ </script>
46
+
47
+ <style scoped>
48
+ .custom-option-display {
49
+ flex: 1;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ padding: 8px 10px;
54
+ background: #1a2332;
55
+ border: 1px solid #253344;
56
+ border-radius: 4px;
57
+ cursor: help;
58
+ white-space: nowrap;
59
+ }
60
+
61
+ .custom-option-display.property-display {
62
+ min-width: 0;
63
+ }
64
+
65
+ .custom-option-name {
66
+ font-weight: 600;
67
+ color: #f6a901;
68
+ font-size: 14px;
69
+ margin-right: 8px;
70
+ }
71
+
72
+ .custom-option-info {
73
+ font-size: 11px;
74
+ color: #8a9196;
75
+ flex-shrink: 0;
76
+ }
77
+ </style>