@mundogamernetwork/shared-ui 1.1.79 → 1.1.81

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.
@@ -250,7 +250,7 @@ const detailRoute = computed(() =>
250
250
  top: 6px;
251
251
  right: 6px;
252
252
  background: var(--primary);
253
- color: #fff;
253
+ color: var(--key-accent-ink, #13161C);
254
254
  font-size: 11px;
255
255
  font-weight: 700;
256
256
  padding: 2px 7px;
@@ -280,7 +280,7 @@ const detailRoute = computed(() =>
280
280
  bottom: 6px;
281
281
  left: 6px;
282
282
  background: var(--primary);
283
- color: #fff;
283
+ color: var(--key-accent-ink, #13161C);
284
284
  font-size: 10px;
285
285
  font-weight: 700;
286
286
  padding: 3px 7px;
package/locales/de.json CHANGED
@@ -169,7 +169,22 @@
169
169
  },
170
170
  "status_label": "Status",
171
171
  "error_no_keys": "Derzeit sind keine Schlüssel für diese Kampagne verfügbar.",
172
- "error_not_approved": "Ihre Anfrage wurde noch nicht genehmigt."
172
+ "error_not_approved": "Ihre Anfrage wurde noch nicht genehmigt.",
173
+ "quantity": "Anzahl der Schlüssel",
174
+ "error_duplicate": "Sie haben bereits eine Anfrage für diese Kampagne. Überprüfen Sie 'Meine Bestellungen' für den Status.",
175
+ "error_user_blocked": "Ihr Zugang zu Key-Anfragen wurde vorübergehend gesperrt, da Material-Einreichungen überfällig sind. Bitte reichen Sie Ihre ausstehenden Materialien ein.",
176
+ "error_not_eligible": "Diese Kampagne ist für Ihren aktuellen Plan nicht verfügbar.",
177
+ "error_pending_limit": "Sie haben das gleichzeitige Anfragenlimit Ihres Plans erreicht. Bitte erfüllen Sie Ihre ausstehenden Verpflichtungen, bevor Sie einen neuen Key anfordern.",
178
+ "select_option": "Option auswählen",
179
+ "required_data": "Erforderliche Angaben",
180
+ "already_requested": "Sie haben bereits eine Anfrage für diese Kampagne gestellt.",
181
+ "back_to_campaign": "Zur Kampagne zurückkehren",
182
+ "status_pending": "Anfrage ausstehend — bitte warten Sie auf die Genehmigung",
183
+ "status_approved": "Anfrage genehmigt — Sie können jetzt Ihren Schlüssel abrufen",
184
+ "status_completed": "Anfrage abgeschlossen",
185
+ "status_refused": "Anfrage abgelehnt",
186
+ "status_cancelled": "Anfrage storniert",
187
+ "refused_reason": "Grund der Ablehnung:"
173
188
  },
174
189
  "materials": {
175
190
  "title": "Submit material",
package/locales/en.json CHANGED
@@ -169,7 +169,22 @@
169
169
  },
170
170
  "status_label": "Status",
171
171
  "error_no_keys": "No keys available for this campaign at the moment.",
172
- "error_not_approved": "Your request has not been approved yet."
172
+ "error_not_approved": "Your request has not been approved yet.",
173
+ "quantity": "Number of keys",
174
+ "error_duplicate": "You already have a request for this campaign. Check 'My Orders' to see the status.",
175
+ "error_user_blocked": "Your key request access has been temporarily blocked due to overdue material submissions. Please submit your pending materials to restore access.",
176
+ "error_not_eligible": "This campaign is not available for your current plan.",
177
+ "error_pending_limit": "You have reached the simultaneous request limit for your plan. Please fulfill your pending obligations before requesting a new key.",
178
+ "select_option": "Select an option",
179
+ "required_data": "Required data",
180
+ "already_requested": "You have already submitted a request for this campaign.",
181
+ "back_to_campaign": "Back to campaign",
182
+ "status_pending": "Request pending — waiting for approval",
183
+ "status_approved": "Request approved — you can now retrieve your key",
184
+ "status_completed": "Request completed",
185
+ "status_refused": "Request refused",
186
+ "status_cancelled": "Request cancelled",
187
+ "refused_reason": "Reason for refusal:"
173
188
  },
174
189
  "materials": {
175
190
  "title": "Submit material",
@@ -169,7 +169,22 @@
169
169
  },
170
170
  "status_label": "Status",
171
171
  "error_no_keys": "Não há chaves disponíveis para esta campanha no momento.",
172
- "error_not_approved": "Sua solicitação ainda não foi aprovada."
172
+ "error_not_approved": "Sua solicitação ainda não foi aprovada.",
173
+ "quantity": "Quantidade de chaves",
174
+ "error_duplicate": "Você já tem um pedido para esta campanha. Acesse 'Meus Pedidos' para ver o status.",
175
+ "error_user_blocked": "Seu acesso a pedidos de chave está temporariamente bloqueado por materiais não entregues. Envie seus materiais pendentes para restaurar o acesso.",
176
+ "error_not_eligible": "Esta campanha não está disponível para o seu plano atual.",
177
+ "error_pending_limit": "Você atingiu o limite de pedidos simultâneos do seu plano. Entregue o material pendente antes de solicitar uma nova chave.",
178
+ "select_option": "Selecione uma opção",
179
+ "required_data": "Dados obrigatórios",
180
+ "already_requested": "Você já enviou uma solicitação para esta campanha.",
181
+ "back_to_campaign": "Voltar para a campanha",
182
+ "status_pending": "Solicitação pendente — aguardando aprovação",
183
+ "status_approved": "Solicitação aprovada — você pode resgatar sua chave",
184
+ "status_completed": "Solicitação concluída",
185
+ "status_refused": "Solicitação recusada",
186
+ "status_cancelled": "Solicitação cancelada",
187
+ "refused_reason": "Motivo da recusa:"
173
188
  },
174
189
  "materials": {
175
190
  "title": "Submit material",
package/locales/ro.json CHANGED
@@ -169,7 +169,22 @@
169
169
  },
170
170
  "status_label": "Status",
171
171
  "error_no_keys": "Nu există chei disponibile pentru această campanie în acest moment.",
172
- "error_not_approved": "Cererea ta nu a fost aprobată încă."
172
+ "error_not_approved": "Cererea ta nu a fost aprobată încă.",
173
+ "quantity": "Numărul de chei",
174
+ "error_duplicate": "Aveți deja o cerere pentru această campanie. Verificați 'Comenzile mele' pentru status.",
175
+ "error_user_blocked": "Accesul dvs. la solicitările de chei a fost blocat temporar din cauza transmisiilor de materiale restante. Trimiteți materialele dvs. pentru a restabili accesul.",
176
+ "error_not_eligible": "Această campanie nu este disponibilă pentru planul dvs. actual.",
177
+ "error_pending_limit": "Ați atins limita de cereri simultane a planului dvs. Îndepliniți-vă obligațiile restante înainte de a solicita o nouă cheie.",
178
+ "select_option": "Selectează o opțiune",
179
+ "required_data": "Date obligatorii",
180
+ "already_requested": "Ați trimis deja o cerere pentru această campanie.",
181
+ "back_to_campaign": "Înapoi la campanie",
182
+ "status_pending": "Cerere în așteptare — așteptați aprobarea",
183
+ "status_approved": "Cerere aprobată — puteți prelua cheia",
184
+ "status_completed": "Cerere finalizată",
185
+ "status_refused": "Cerere refuzată",
186
+ "status_cancelled": "Cerere anulată",
187
+ "refused_reason": "Motivul refuzului:"
173
188
  },
174
189
  "materials": {
175
190
  "title": "Submit material",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.1.79",
3
+ "version": "1.1.81",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -302,7 +302,7 @@ function goBack() {
302
302
  font-size: 14px;
303
303
  font-weight: 700;
304
304
  background: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
305
- color: #fff;
305
+ color: var(--key-accent-ink, #13161C);
306
306
  border: none;
307
307
  cursor: pointer;
308
308
  transition: opacity 0.15s;
@@ -35,6 +35,15 @@ const requestError = ref("")
35
35
  const selectedPlatformId = ref<number | null>(null)
36
36
  const selectedRegionId = ref<number | null>(null)
37
37
  const description = ref("")
38
+ const quantity = ref<number | null>(1)
39
+
40
+ const maxKeysForUser = computed(() => campaign.value?.max_keys_for_user ?? 1)
41
+
42
+ function clampQuantity() {
43
+ if (quantity.value === null) return
44
+ if (quantity.value < 1) quantity.value = 1
45
+ if (quantity.value > maxKeysForUser.value) quantity.value = maxKeysForUser.value
46
+ }
38
47
 
39
48
  onMounted(async () => {
40
49
  if (!currentUser) {
@@ -63,9 +72,36 @@ onMounted(async () => {
63
72
  })
64
73
 
65
74
  const canSubmit = computed(() =>
66
- !!selectedPlatformId.value && !!selectedRegionId.value && !submitting.value,
75
+ !!selectedPlatformId.value && !!selectedRegionId.value && !!quantity.value && !submitting.value,
67
76
  )
68
77
 
78
+ // Collapsible "Required data" box — matches tv's original toggle behavior
79
+ const isBoxOpen = ref(true)
80
+ function toggleBox() {
81
+ isBoxOpen.value = !isBoxOpen.value
82
+ }
83
+
84
+ // request_status: null/undefined/1 = no request yet, anything else = existing request
85
+ const hasExistingRequest = computed(() => {
86
+ const s = campaign.value?.request_status
87
+ return s !== null && s !== undefined && s !== 1
88
+ })
89
+
90
+ const existingRequestStatus = computed(() => {
91
+ switch (campaign.value?.request_status) {
92
+ case 2: return $i18n.t("keys.campaigns.status_pending")
93
+ case 3: return $i18n.t("keys.campaigns.status_approved")
94
+ case 4: return $i18n.t("keys.campaigns.status_approved")
95
+ case 5: return $i18n.t("keys.campaigns.status_completed")
96
+ case 6: return $i18n.t("keys.campaigns.status_refused")
97
+ case 7: return $i18n.t("keys.campaigns.status_cancelled")
98
+ default: return ""
99
+ }
100
+ })
101
+
102
+ const isRefused = computed(() => campaign.value?.request_status === 6)
103
+ const refusedReason = computed(() => campaign.value?.request_admin_notes || "")
104
+
69
105
  async function submit() {
70
106
  if (!canSubmit.value) return
71
107
  submitting.value = true
@@ -76,15 +112,27 @@ async function submit() {
76
112
  platform_ids: [selectedPlatformId.value],
77
113
  region_ids: [selectedRegionId.value],
78
114
  description: description.value || "-",
79
- quantity_keys: 1,
115
+ quantity_keys: Number(quantity.value) || 1,
80
116
  })
81
117
  success.value = true
82
118
  setTimeout(() => {
83
119
  navigateTo(`/${locale.value}/key-campaigns`)
84
120
  }, 2000)
85
121
  } catch (e: any) {
86
- const msg = e?.response?.data?.message || e?.message || ""
87
- requestError.value = msg || $i18n.t("keys.campaigns.error_request")
122
+ const status = e?.response?.status
123
+ const errorCode = e?.response?.data?.error_code
124
+ if (status === 409) {
125
+ requestError.value = $i18n.t("keys.campaigns.error_duplicate")
126
+ } else if (status === 403 && errorCode === "user_key_blocked") {
127
+ requestError.value = $i18n.t("keys.campaigns.error_user_blocked")
128
+ } else if (status === 403) {
129
+ requestError.value = $i18n.t("keys.campaigns.error_not_eligible")
130
+ } else if (status === 422 && errorCode === "pending_limit_reached") {
131
+ requestError.value = $i18n.t("keys.campaigns.error_pending_limit")
132
+ } else {
133
+ const msg = e?.response?.data?.message || e?.message || ""
134
+ requestError.value = msg || $i18n.t("keys.campaigns.error_request")
135
+ }
88
136
  } finally {
89
137
  submitting.value = false
90
138
  }
@@ -126,50 +174,74 @@ function goBack() {
126
174
  <p v-if="campaign?.name" class="campaign-name">{{ campaign.name }}</p>
127
175
  </div>
128
176
 
129
- <form class="form" @submit.prevent="submit">
130
- <!-- Platform -->
131
- <div class="field">
132
- <label class="label">{{ $t("keys.campaigns.platform") }}</label>
133
- <div class="options">
134
- <button
135
- v-for="p in platforms"
136
- :key="p.id"
137
- type="button"
138
- class="option"
139
- :class="{ selected: selectedPlatformId === p.id }"
140
- @click="selectedPlatformId = p.id"
141
- >
142
- {{ p.name }}
143
- </button>
177
+ <!-- Already has a request -->
178
+ <div v-if="hasExistingRequest" class="existing-request-notice">
179
+ <MGIcon size="1x" icon="version" class="notice-icon" />
180
+ <div class="notice-texts">
181
+ <div class="notice-title">{{ $t("keys.campaigns.already_requested") }}</div>
182
+ <div class="notice-status">{{ existingRequestStatus }}</div>
183
+ <div v-if="isRefused && refusedReason" class="notice-reason">
184
+ <span class="notice-reason-label">{{ $t("keys.campaigns.refused_reason") }}</span>
185
+ <span class="notice-reason-text">{{ refusedReason }}</span>
144
186
  </div>
145
187
  </div>
188
+ <button class="btn notice-back" @click="goBack">
189
+ {{ $t("keys.campaigns.back_to_campaign") }}
190
+ </button>
191
+ </div>
146
192
 
147
- <!-- Region -->
148
- <div class="field">
149
- <label class="label">{{ $t("keys.campaigns.region") }}</label>
150
- <div class="options">
151
- <button
152
- v-for="r in regions"
153
- :key="r.id"
154
- type="button"
155
- class="option"
156
- :class="{ selected: selectedRegionId === r.id }"
157
- @click="selectedRegionId = r.id"
158
- >
159
- {{ r.name }}
193
+ <form v-else class="form" @submit.prevent="submit">
194
+ <div class="required-data">
195
+ <div class="required-data-header" @click="toggleBox">
196
+ <div class="required-data-header-left">
197
+ <MGIcon size="1x" icon="check" :style="{ color: isBoxOpen ? 'var(--key-accent, var(--primary, #D297FF))' : 'var(--inactive)' }" />
198
+ <span>{{ $t("keys.campaigns.required_data") }}</span>
199
+ </div>
200
+ <button type="button" class="box-toggler" :aria-expanded="isBoxOpen">
201
+ <MGIcon icon="chevron-down" :class="{ rotate: isBoxOpen }" />
160
202
  </button>
161
203
  </div>
162
- </div>
163
204
 
164
- <!-- Description (optional) -->
165
- <div class="field">
166
- <label class="label">{{ $t("keys.campaigns.description_optional") }}</label>
167
- <textarea
168
- v-model="description"
169
- class="textarea"
170
- :placeholder="$t('keys.campaigns.description_placeholder')"
171
- rows="3"
172
- />
205
+ <div v-show="isBoxOpen" class="required-data-body">
206
+ <div class="form-row">
207
+ <label class="label">{{ $t("keys.campaigns.platform") }}</label>
208
+ <select v-model.number="selectedPlatformId" class="select">
209
+ <option :value="null" disabled>{{ $t("keys.campaigns.select_option") }}</option>
210
+ <option v-for="p in platforms" :key="p.id" :value="p.id">{{ p.name }}</option>
211
+ </select>
212
+ </div>
213
+
214
+ <div class="form-row">
215
+ <label class="label">{{ $t("keys.campaigns.region") }}</label>
216
+ <select v-model.number="selectedRegionId" class="select">
217
+ <option :value="null" disabled>{{ $t("keys.campaigns.select_option") }}</option>
218
+ <option v-for="r in regions" :key="r.id" :value="r.id">{{ r.name }}</option>
219
+ </select>
220
+ </div>
221
+
222
+ <div class="form-row" v-if="maxKeysForUser > 1">
223
+ <label class="label">{{ $t("keys.campaigns.quantity") }}</label>
224
+ <input
225
+ v-model.number="quantity"
226
+ type="number"
227
+ min="1"
228
+ :max="maxKeysForUser"
229
+ class="input quantity-input"
230
+ :placeholder="$t('keys.campaigns.quantity')"
231
+ @change="clampQuantity"
232
+ />
233
+ </div>
234
+
235
+ <div class="form-row">
236
+ <label class="label">{{ $t("keys.campaigns.description_optional") }}</label>
237
+ <textarea
238
+ v-model="description"
239
+ class="textarea"
240
+ :placeholder="$t('keys.campaigns.description_placeholder')"
241
+ rows="3"
242
+ />
243
+ </div>
244
+ </div>
173
245
  </div>
174
246
 
175
247
  <div v-if="requestError" class="req-error">{{ requestError }}</div>
@@ -242,55 +314,120 @@ function goBack() {
242
314
 
243
315
  .form { display: flex; flex-direction: column; gap: 24px; }
244
316
 
245
- .field {
317
+ .required-data {
246
318
  display: flex;
247
319
  flex-direction: column;
248
- gap: 10px;
320
+ background: var(--bg-app-badge);
321
+ border: 1px solid var(--button-secondary-default-bg);
249
322
  }
250
323
 
251
- .label {
252
- font-size: 13px;
324
+ .required-data-header {
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: space-between;
328
+ gap: 10px;
329
+ padding: 16px 20px;
330
+ font-size: 14px;
253
331
  font-weight: 600;
254
332
  color: var(--card-article-title);
333
+ border-bottom: 1px solid var(--button-secondary-default-bg);
334
+ cursor: pointer;
255
335
  }
256
336
 
257
- .options {
337
+ .required-data-header-left {
258
338
  display: flex;
259
- flex-wrap: wrap;
260
- gap: 8px;
339
+ align-items: center;
340
+ gap: 10px;
261
341
  }
262
342
 
263
- .option {
264
- padding: 8px 16px;
265
- font-size: 13px;
266
- font-weight: 500;
267
- border: 1px solid var(--button-secondary-default-bg);
343
+ .box-toggler {
344
+ display: flex;
345
+ align-items: center;
268
346
  background: transparent;
347
+ border: none;
269
348
  color: var(--secondary-info-fg);
270
349
  cursor: pointer;
271
- transition: border-color 0.15s, color 0.15s;
272
350
 
273
- &:hover { border-color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF)))); color: var(--card-article-title); }
274
- &.selected {
275
- border-color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
276
- color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
277
- background: var(--key-accent-subtle-bg, rgba(210, 151, 255, 0.08));
278
- }
351
+ svg { transition: transform 0.15s; }
352
+ svg.rotate { transform: rotate(180deg); }
279
353
  }
280
354
 
281
- .textarea {
282
- width: 100%;
355
+ .existing-request-notice {
356
+ display: flex;
357
+ align-items: flex-start;
358
+ gap: 14px;
359
+ padding: 20px;
283
360
  background: var(--bg-app-badge);
284
361
  border: 1px solid var(--button-secondary-default-bg);
362
+ }
363
+
364
+ .notice-icon { color: var(--key-accent, var(--primary, #D297FF)); margin-top: 2px; }
365
+
366
+ .notice-texts { flex: 1; display: flex; flex-direction: column; gap: 4px; }
367
+
368
+ .notice-title { font-size: 14px; font-weight: 600; color: var(--card-article-title); }
369
+ .notice-status { font-size: 13px; color: var(--secondary-info-fg); }
370
+
371
+ .notice-reason {
372
+ display: flex;
373
+ flex-direction: column;
374
+ gap: 2px;
375
+ margin-top: 6px;
376
+ font-size: 13px;
377
+
378
+ .notice-reason-label { font-weight: 600; color: var(--card-article-title); }
379
+ .notice-reason-text { color: var(--secondary-info-fg); }
380
+ }
381
+
382
+ .notice-back {
383
+ background: transparent;
384
+ border: 1px solid var(--button-secondary-default-bg);
385
+ color: var(--card-article-title);
386
+ padding: 8px 16px;
387
+ font-size: 13px;
388
+ font-weight: 600;
389
+ cursor: pointer;
390
+ white-space: nowrap;
391
+ &:hover { border-color: var(--key-accent, var(--primary, #D297FF)); }
392
+ }
393
+
394
+ .form-row {
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 10px;
398
+ padding: 20px;
399
+ & + .form-row { border-top: 1px solid var(--button-secondary-default-bg); }
400
+ }
401
+
402
+ .label {
403
+ font-size: 13px;
404
+ font-weight: 600;
405
+ color: var(--card-article-title);
406
+ }
407
+
408
+ .select, .input, .textarea {
409
+ width: 100%;
410
+ background: var(--body-bg-card, var(--bg-app-badge));
411
+ border: 1px solid var(--button-secondary-default-bg);
285
412
  color: var(--card-article-title);
286
413
  font-size: 13px;
287
414
  padding: 10px 12px;
288
- resize: vertical;
289
415
  outline: none;
290
416
  font-family: inherit;
291
417
  &:focus { border-color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF)))); }
292
418
  }
293
419
 
420
+ .select {
421
+ appearance: auto;
422
+ cursor: pointer;
423
+ }
424
+
425
+ .textarea { resize: vertical; }
426
+
427
+ .quantity-input {
428
+ width: 120px;
429
+ }
430
+
294
431
  .req-error {
295
432
  font-size: 13px;
296
433
  color: var(--danger, #e53935);
@@ -303,7 +440,7 @@ function goBack() {
303
440
  font-size: 14px;
304
441
  font-weight: 700;
305
442
  background: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
306
- color: #fff;
443
+ color: var(--key-accent-ink, #13161C);
307
444
  border: none;
308
445
  cursor: pointer;
309
446
  transition: opacity 0.15s;