refactor(shop): découpe MesPersos en sous-sections + nettoyage ProductCard
All checks were successful
Deploy XIP / deploy (push) Successful in 35s

- MesPersos.vue (347L) éclaté en 5 sous-composants autonomes sous
  shop/persos/ (BgPrefsSection, SendButtonPrefsSection, SendSkinPrefsSection,
  IpColorPrefsSection, PetsPrefsSection). MesPersos n'est plus qu'un wrapper.
- CSS partagé des sections déplacé en classes globales .pf-* dans style.css
  (plus de duplication entre les sections).
- ProductCard : metaJson typé via parseMeta<ProductMeta>(), suppression des
  casts `any` (find/designs) — comportement identique.
- vue-tsc --noEmit : 0 erreur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 20:03:23 +02:00
parent aca608e520
commit 9dd72b9b2d
8 changed files with 323 additions and 342 deletions

View File

@@ -108,6 +108,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import type { Product, PurchaseOptions } from '@/composables/useShop';
import { parseMeta, type ProductMeta } from '@/composables/useMeta';
const props = defineProps<{
product: Product;
@@ -123,10 +124,7 @@ const emit = defineEmits<{
goPerso: [];
}>();
const meta = computed<any>(() => {
try { return props.product.metaJson ? JSON.parse(props.product.metaJson) : {}; }
catch { return {}; }
});
const meta = computed(() => parseMeta<ProductMeta>(props.product.metaJson));
// Subscription
const plans = computed(() => meta.value.plans ?? []);
@@ -143,11 +141,11 @@ const url = ref('');
const designs = computed(() => meta.value.designs ?? []);
const petDesign = ref<string>('');
const availableDesigns = computed(() =>
designs.value.filter((d: any) => !props.ownedPetChars.includes(d.char))
designs.value.filter((d) => !props.ownedPetChars.includes(d.char))
);
watch(availableDesigns, (ds) => {
if (ds.length > 0 && !ds.find((d: any) => d.id === petDesign.value)) {
petDesign.value = (ds[0] as any).id;
if (ds.length > 0 && !ds.find((d) => d.id === petDesign.value)) {
petDesign.value = ds[0].id;
}
}, { immediate: true });
@@ -169,12 +167,12 @@ const icon = computed(() => {
const effectivePrice = computed(() => {
let price = props.product.promoPrice ?? props.product.basePrice;
if (props.product.kind === 'subscription') {
const p = plans.value.find((x: any) => x.id === plan.value);
const p = plans.value.find((x) => x.id === plan.value);
if (p) price = p.price;
}
if (props.product.kind === 'ad-frame') {
const d = durations.value.find((x: any) => x.days === durationDays.value);
const f = formats.value.find((x: any) => x.id === format.value);
const d = durations.value.find((x) => x.days === durationDays.value);
const f = formats.value.find((x) => x.id === format.value);
price += (d?.extra ?? 0) + (f?.extra ?? 0);
}
return price;
@@ -220,8 +218,8 @@ function onBuy(): void {
options.url = url.value || undefined;
}
if (props.product.kind === 'pet' || props.product.id === 'bundle-cosmetic') {
const d = availableDesigns.value.find((x: any) => x.id === petDesign.value) ?? availableDesigns.value[0];
if (d) { options.petDesign = (d as any).id; options.petChar = (d as any).char; }
const d = availableDesigns.value.find((x) => x.id === petDesign.value) ?? availableDesigns.value[0];
if (d) { options.petDesign = d.id; options.petChar = d.char; }
}
emit('buy', props.product.id, options);
}