Fix : Changer Id en UUID

This commit is contained in:
NyAvoKevin 2026-05-28 13:28:13 +03:00
parent 79c52d0236
commit 390e84a55e
16 changed files with 1124 additions and 74 deletions

View File

@ -64,7 +64,7 @@ class CardController extends Controller
]); ]);
} }
public function freeCartResult($id) public function freeCartResult(string $id)
{ {
$card = $this->cardRepository->find($id); $card = $this->cardRepository->find($id);
return response()->json([ return response()->json([

View File

@ -4,14 +4,18 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class Card extends Model class Card extends Model
{ {
use HasFactory; use HasFactory, HasUuids;
protected $table = 'cards'; protected $table = 'cards';
protected $fillable = [ protected $fillable = [
'asset_id',
'name', 'name',
'description_upright', 'description_upright',
'description_reversed', 'description_reversed',
@ -23,4 +27,30 @@ class Card extends Model
protected $casts = [ protected $casts = [
'symbolism' => 'array', 'symbolism' => 'array',
]; ];
public function getIncrementing(): bool
{
return ! $this->usesUuidPrimaryKey();
}
public function getKeyType(): string
{
return $this->usesUuidPrimaryKey() ? 'string' : 'int';
}
public function uniqueIds(): array
{
return $this->usesUuidPrimaryKey() ? [$this->getKeyName()] : [];
}
protected function usesUuidPrimaryKey(): bool
{
if (! Schema::hasTable($this->getTable())) {
return false;
}
$column = collect(DB::select(sprintf('SHOW COLUMNS FROM `%s` LIKE "id"', $this->getTable())))->first();
return $column && str_starts_with(strtolower((string) $column->Type), 'char(36)');
}
} }

View File

@ -13,7 +13,7 @@ class CardRepository implements CardRepositoryInterface
return Card::all(); return Card::all();
} }
public function find(int $id): ?Card public function find(string $id): ?Card
{ {
return Card::find($id); return Card::find($id);
} }
@ -23,7 +23,7 @@ class CardRepository implements CardRepositoryInterface
return Card::create($data); return Card::create($data);
} }
public function update(int $id, array $data): ?Card public function update(string $id, array $data): ?Card
{ {
$card = Card::find($id); $card = Card::find($id);
@ -36,7 +36,7 @@ class CardRepository implements CardRepositoryInterface
return $card; return $card;
} }
public function delete(int $id): bool public function delete(string $id): bool
{ {
$card = Card::find($id); $card = Card::find($id);
@ -70,6 +70,7 @@ class CardRepository implements CardRepositoryInterface
return [ return [
'id' => $card->id, 'id' => $card->id,
'asset_id' => $card->asset_id,
'name' => $card->name, 'name' => $card->name,
'image_url' => $card->image_url, 'image_url' => $card->image_url,
'orientation' => $isReversed ? 'reversed' : 'upright', 'orientation' => $isReversed ? 'reversed' : 'upright',

View File

@ -9,13 +9,13 @@ interface CardRepositoryInterface
{ {
public function all(): Collection; public function all(): Collection;
public function find(int $id): ?Card; public function find(string $id): ?Card;
public function create(array $data): Card; public function create(array $data): Card;
public function update(int $id, array $data): ?Card; public function update(string $id, array $data): ?Card;
public function delete(int $id): bool; public function delete(string $id): bool;
public function draw(): array; public function draw(): array;
} }

BIN
build.tar.gz Normal file

Binary file not shown.

View File

@ -59,12 +59,15 @@
], ],
"dev": [ "dev": [
"Composer\\Config::disableProcessTimeout", "Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" "npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"npm run dev\" --names=server,queue,vite --kill-others"
], ],
"dev:ssr": [ "dev:ssr": [
"npm run build:ssr", "npm run build:ssr",
"Composer\\Config::disableProcessTimeout", "Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr --kill-others" "npx concurrently -c \"#93c5fd,#c4b5fd,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan inertia:start-ssr\" --names=server,queue,ssr --kill-others"
],
"logs": [
"powershell -NoProfile -ExecutionPolicy Bypass -Command \"if (!(Test-Path 'storage\\\\logs\\\\laravel.log')) { New-Item -ItemType File -Path 'storage\\\\logs\\\\laravel.log' | Out-Null }; Get-Content 'storage\\\\logs\\\\laravel.log' -Wait -Tail 50\""
], ],
"test": [ "test": [
"@php artisan config:clear --ansi", "@php artisan config:clear --ansi",

View File

@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasTable('cards') || Schema::hasColumn('cards', 'uuid')) {
return;
}
Schema::table('cards', function (Blueprint $table) {
$table->uuid('uuid')->nullable()->after('id');
});
DB::statement('ALTER TABLE cards ADD UNIQUE KEY cards_uuid_unique (uuid)');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (! Schema::hasTable('cards') || ! Schema::hasColumn('cards', 'uuid')) {
return;
}
$idColumn = collect(DB::select('SHOW COLUMNS FROM cards LIKE "id"'))->first();
if ($idColumn && str_starts_with(strtolower((string) $idColumn->Type), 'char(36)')) {
return;
}
Schema::table('cards', function (Blueprint $table) {
$table->dropUnique('cards_uuid_unique');
$table->dropColumn('uuid');
});
}
};

View File

@ -0,0 +1,91 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! Schema::hasTable('cards')) {
return;
}
if (! Schema::hasColumn('cards', 'asset_id')) {
Schema::table('cards', function (Blueprint $table) {
$table->unsignedInteger('asset_id')->nullable()->after('id');
});
}
$cards = DB::table('cards')
->select(['id', 'image_url', 'created_at', 'updated_at', 'name'])
->orderBy('created_at')
->orderBy('updated_at')
->orderBy('name')
->orderBy('id')
->get();
$usedAssetIds = [];
foreach ($cards as $card) {
if (! $card->image_url || ! preg_match('/(\d+)\.(png|jpe?g|webp|gif)$/i', $card->image_url, $matches)) {
continue;
}
$assetId = (int) $matches[1];
if ($assetId < 1 || isset($usedAssetIds[$assetId])) {
continue;
}
DB::table('cards')
->where('id', $card->id)
->update(['asset_id' => $assetId]);
$usedAssetIds[$assetId] = true;
}
$nextAssetId = 1;
$cardsWithoutAssetId = DB::table('cards')
->select(['id'])
->whereNull('asset_id')
->orderBy('created_at')
->orderBy('updated_at')
->orderBy('name')
->orderBy('id')
->get();
foreach ($cardsWithoutAssetId as $card) {
while (isset($usedAssetIds[$nextAssetId])) {
$nextAssetId++;
}
DB::table('cards')
->where('id', $card->id)
->update(['asset_id' => $nextAssetId]);
$usedAssetIds[$nextAssetId] = true;
$nextAssetId++;
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (! Schema::hasTable('cards') || ! Schema::hasColumn('cards', 'asset_id')) {
return;
}
Schema::table('cards', function (Blueprint $table) {
$table->dropColumn('asset_id');
});
}
};

733
oracle_dump.sql Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import CardShuffleTemplate from '@/components/template/CardShuffleTemplate.vue'; import CardShuffleTemplate from '@/components/template/CardShuffleTemplate.vue';
import { Card } from '@/types/cart'; import { Card } from '@/types/cart';
import { resolveCardImage } from '@/utils/resolveCardImage';
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import { computed, onMounted, ref, watchEffect } from 'vue'; import { computed, onMounted, ref, watchEffect } from 'vue';
@ -197,7 +198,18 @@ const goToResult = () => {
if (props.clientSessionId) { if (props.clientSessionId) {
router.visit(`/resultat?client_session_id=${props.clientSessionId}`); router.visit(`/resultat?client_session_id=${props.clientSessionId}`);
} else { } else {
router.visit(`/resultat-gratuit?id=${props.drawnCards ? props.drawnCards[0].id : ''}`); const selectedCard = props.drawnCards?.[0];
if (selectedCard) {
sessionStorage.setItem('free_draw_card', JSON.stringify(selectedCard));
} else {
sessionStorage.removeItem('free_draw_card');
}
const numericId = Number(selectedCard?.id);
const resultId = Number.isInteger(numericId) && numericId > 0 ? String(numericId) : String(selectedCard?.id ?? '');
router.visit(`/resultat-gratuit?id=${encodeURIComponent(resultId)}`);
} }
}; };
@ -281,7 +293,7 @@ defineExpose({ setDrawnCards });
</div> </div>
</div> </div>
<div class="card-face card-known-back"> <div class="card-face card-known-back">
<img :src="`/cards/${card.id + 1}.png`" :alt="card.name" class="card-image" /> <img :src="resolveCardImage(card, index)" :alt="card.name" class="card-image" />
<div class="card-description-overlay"> <div class="card-description-overlay">
<h3>{{ card.name }}</h3> <h3>{{ card.name }}</h3>
</div> </div>

View File

@ -1,50 +1,62 @@
<template> <template>
<div <div
class="border-linen flex flex-col items-center gap-6 md:gap-8 rounded-lg border bg-white p-6 md:p-8 shadow-sm transition-all duration-300 hover:shadow-md md:flex-row" class="border-linen flex flex-col items-center gap-6 rounded-lg border bg-white p-6 shadow-sm transition-all duration-300 hover:shadow-md md:flex-row md:gap-8 md:p-8"
> >
<!-- Card Image -->
<div class="relative"> <div class="relative">
<div <div
class="h-56 w-36 sm:h-64 sm:w-40 md:h-72 md:w-48 flex-shrink-0 rounded-md bg-cover bg-center bg-no-repeat transition-transform duration-300 hover:scale-105" class="h-56 w-36 flex-shrink-0 rounded-md bg-cover bg-center bg-no-repeat transition-transform duration-300 hover:scale-105 sm:h-64 sm:w-40 md:h-72 md:w-48"
:style="{ :style="{
'background-image': `url(/cards/${cardNumber + 1}.png)`, 'background-image': `url(${cardImage})`,
'box-shadow': '0 0 15px 5px rgba(215, 186, 141, 0.3)', 'box-shadow': '0 0 15px 5px rgba(215, 186, 141, 0.3)',
transform: orientation === 'reversed' ? 'rotate(180deg)' : 'none' transform: orientation === 'reversed' ? 'rotate(180deg)' : 'none'
}" }"
></div> ></div>
<div <div
v-if="orientation === 'reversed'" v-if="orientation === 'reversed'"
class="absolute top-2 right-2 bg-red-100 text-red-800 text-xs px-2 py-1 rounded-full" class="absolute top-2 right-2 rounded-full bg-red-100 px-2 py-1 text-xs text-red-800"
> >
Inversée Inversée
</div> </div>
</div> </div>
<!-- Card Content -->
<div class="flex flex-col gap-3 text-center md:text-left"> <div class="flex flex-col gap-3 text-center md:text-left">
<p class="text-spiritual-earth text-xs sm:text-sm tracking-widest uppercase font-semibold"> <p class="text-spiritual-earth text-xs font-semibold tracking-widest uppercase sm:text-sm">
Carte {{ cardNumber }} Carte
</p> </p>
<h3 class="text-midnight-blue text-xl sm:text-2xl font-bold leading-tight"> <h3 class="text-midnight-blue text-xl leading-tight font-bold sm:text-2xl">
{{ name }} {{ name }}
</h3> </h3>
<div class="text-midnight-blue/80 text-sm sm:text-base leading-relaxed" v-html="description"> <div class="text-midnight-blue/80 text-sm leading-relaxed sm:text-base" v-html="description"></div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { resolveCardImage } from '@/utils/resolveCardImage';
import { computed } from 'vue';
interface Props { interface Props {
cardNumber: number; cardId: number | string;
assetId?: number | null;
fallbackIndex?: number;
name: string; name: string;
imageUrl: string; imageUrl: string | null;
orientation?: string; orientation?: string;
description: string; description: string;
} }
defineProps<Props>(); const props = defineProps<Props>();
const cardImage = computed(() =>
resolveCardImage(
{
id: props.cardId,
asset_id: props.assetId ?? null,
image_url: props.imageUrl,
},
props.fallbackIndex,
),
);
</script> </script>
<style scoped> <style scoped>

View File

@ -2,32 +2,26 @@
<LandingLayout> <LandingLayout>
<main class="flex flex-1 justify-center px-4 py-8 sm:px-6 sm:py-12 md:py-16 lg:px-8"> <main class="flex flex-1 justify-center px-4 py-8 sm:px-6 sm:py-12 md:py-16 lg:px-8">
<div class="layout-content-container flex w-full max-w-4xl flex-col"> <div class="layout-content-container flex w-full max-w-4xl flex-col">
<!-- Header Section -->
<div class="mb-8 text-center md:mb-12"> <div class="mb-8 text-center md:mb-12">
<h1 class="text-midnight-blue mb-2 text-3xl font-bold sm:text-4xl md:text-5xl">Votre Lecture</h1> <h1 class="text-midnight-blue mb-2 text-3xl font-bold sm:text-4xl md:text-5xl">Votre Lecture</h1>
<p class="text-spiritual-earth mx-auto max-w-2xl text-base sm:text-lg">Voici une analyse détaillée de votre lecture choisie.</p> <p class="text-spiritual-earth mx-auto max-w-2xl text-base sm:text-lg">Voici une analyse détaillée de votre lecture choisie.</p>
</div> </div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-16"> <div v-if="loading" class="flex items-center justify-center py-16">
<div class="h-12 w-12 animate-spin rounded-full border-t-2 border-b-2 border-[var(--subtle-gold)]"></div> <div class="h-12 w-12 animate-spin rounded-full border-t-2 border-b-2 border-[var(--subtle-gold)]"></div>
</div> </div>
<!-- Error State -->
<div v-else-if="error" class="mb-8 rounded-lg bg-red-50 p-6 text-center"> <div v-else-if="error" class="mb-8 rounded-lg bg-red-50 p-6 text-center">
<div class="mb-2 font-medium text-red-700">Erreur</div> <div class="mb-2 font-medium text-red-700">Erreur</div>
<p class="text-red-600">{{ error }}</p> <p class="text-red-600">{{ error }}</p>
</div> </div>
<!-- Empty State -->
<div v-else-if="!hasCard" class="rounded-lg bg-gray-50 p-8 text-center"> <div v-else-if="!hasCard" class="rounded-lg bg-gray-50 p-8 text-center">
<p class="text-gray-600">Aucune carte n'a été trouvée pour votre session.</p> <p class="text-gray-600">Aucune carte n'a été trouvée pour votre session.</p>
</div> </div>
<!-- Single Card Display -->
<div v-else class="mb-8 md:mb-12"> <div v-else class="mb-8 md:mb-12">
<div class="overflow-hidden rounded-lg border border-gray-100 bg-white shadow-md"> <div class="overflow-hidden rounded-lg border border-gray-100 bg-white shadow-md">
<!-- Card Header -->
<div class="bg-[var(--midnight-blue)] p-6 text-center text-white"> <div class="bg-[var(--midnight-blue)] p-6 text-center text-white">
<h2 class="text-2xl font-bold">{{ card.name }}</h2> <h2 class="text-2xl font-bold">{{ card.name }}</h2>
<div v-if="card.orientation" class="mt-2 text-sm opacity-80"> <div v-if="card.orientation" class="mt-2 text-sm opacity-80">
@ -35,25 +29,16 @@
</div> </div>
</div> </div>
<!-- Card Content -->
<div class="p-6"> <div class="p-6">
<!-- Card Image -->
<div class="mb-6 flex justify-center"> <div class="mb-6 flex justify-center">
<img <img :src="cardImage" :alt="card.name" class="w-full max-w-xs rounded-lg shadow-md" />
:src="card.image_url || `/cards/${card.id + 1}.png`"
:alt="card.name"
class="w-full max-w-xs rounded-lg shadow-md"
/>
</div> </div>
<!-- General description (if available) -->
<div v-if="card.description" class="mb-6"> <div v-if="card.description" class="mb-6">
<h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]">Description</h3> <h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]">Description</h3>
<div class="leading-relaxed text-gray-700" v-html="card.description"></div> <div class="leading-relaxed text-gray-700" v-html="card.description"></div>
</div> </div>
<!-- Description based on orientation -->
<div class="mb-6"> <div class="mb-6">
<h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]"> <h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]">
{{ card.orientation === 'reversed' ? 'Signification Inversée' : 'Signification Droite' }} {{ card.orientation === 'reversed' ? 'Signification Inversée' : 'Signification Droite' }}
@ -64,7 +49,6 @@
></div> ></div>
</div> </div>
<!-- Alternative meaning -->
<div class="mb-6"> <div class="mb-6">
<h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]"> <h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]">
{{ card.orientation === 'reversed' ? 'Signification Droite' : 'Signification Inversée' }} {{ card.orientation === 'reversed' ? 'Signification Droite' : 'Signification Inversée' }}
@ -75,7 +59,6 @@
></div> ></div>
</div> </div>
<!-- Symbolism -->
<div v-if="card.symbolism && Object.keys(card.symbolism).length > 0" class="mb-6"> <div v-if="card.symbolism && Object.keys(card.symbolism).length > 0" class="mb-6">
<h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]">Symbolisme</h3> <h3 class="mb-3 text-lg font-semibold text-[var(--midnight-blue)]">Symbolisme</h3>
<ul class="space-y-2 text-gray-700"> <ul class="space-y-2 text-gray-700">
@ -88,21 +71,17 @@
</div> </div>
</div> </div>
<!-- Consultation CTA -->
<div <div
v-if="!loading && !error" v-if="!loading && !error"
class="border-linen rounded-lg border bg-white p-6 text-center shadow-sm transition-all duration-300 hover:shadow-md md:p-8 lg:p-12" class="border-linen rounded-lg border bg-white p-6 text-center shadow-sm transition-all duration-300 hover:shadow-md md:p-8 lg:p-12"
> >
<div class="bg-subtle-gold mx-auto mb-6 h-1 w-16 rounded-full"></div> <div class="bg-subtle-gold mx-auto mb-6 h-1 w-16 rounded-full"></div>
<!-- Nouveau contenu -->
<div class="space-y-6 text-left"> <div class="space-y-6 text-left">
<!-- En-tête -->
<div class="text-center"> <div class="text-center">
<h3 class="text-midnight-blue mb-4 font-heading text-xl font-bold md:text-2xl">Cher.e Explorateur des Symboles,</h3> <h3 class="text-midnight-blue mb-4 font-heading text-xl font-bold md:text-2xl">Cher.e Explorateur des Symboles,</h3>
</div> </div>
<!-- Grilles de lecture -->
<div class="space-y-3"> <div class="space-y-3">
<p class="text-midnight-blue/80 font-medium">Votre tirage en ligne vous a offert 3 grilles de lecture :</p> <p class="text-midnight-blue/80 font-medium">Votre tirage en ligne vous a offert 3 grilles de lecture :</p>
<div class="space-y-2 pl-4"> <div class="space-y-2 pl-4">
@ -121,7 +100,6 @@
</div> </div>
</div> </div>
<!-- Explication consultation -->
<div class="space-y-3"> <div class="space-y-3">
<p class="text-midnight-blue/80"> <p class="text-midnight-blue/80">
Ces révélations ne sont qu'un prélude à ce que nous pourrions accomplir en consultation directe. Parce que les arcanes Ces révélations ne sont qu'un prélude à ce que nous pourrions accomplir en consultation directe. Parce que les arcanes
@ -133,18 +111,17 @@
<p class="text-midnight-blue/80"> Se transmuent en plan d'action sur-mesure</p> <p class="text-midnight-blue/80"> Se transmuent en plan d'action sur-mesure</p>
</div> </div>
</div> </div>
<!-- Offre Éclaireur -->
</div> </div>
<!-- Bouton CTA et informations -->
</div> </div>
</div> </div>
</main> </main>
</LandingLayout> </LandingLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import LandingLayout from '@/layouts/app/LandingLayout.vue'; import LandingLayout from '@/layouts/app/LandingLayout.vue';
import type { Card } from '@/types/cart';
import { resolveCardImage } from '@/utils/resolveCardImage';
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import axios from 'axios'; import axios from 'axios';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
@ -153,22 +130,52 @@ const goToBooking = () => {
router.visit('/rendez-vous'); router.visit('/rendez-vous');
}; };
const card = ref<any>(null); const card = ref<Card | null>(null);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const loading = ref(true); const loading = ref(true);
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const cardId = params.get('id'); const cardId = params.get('id');
const storedCardKey = 'free_draw_card';
const hasCard = computed(() => card.value !== null); const hasCard = computed(() => card.value !== null);
const cardImage = computed(() => (card.value ? resolveCardImage(card.value) : '/cards/1.png'));
onMounted(async () => { onMounted(async () => {
const storedCard = sessionStorage.getItem(storedCardKey);
if (storedCard) {
try { try {
const response = await axios.get(`/api/get-card/${cardId}`); card.value = JSON.parse(storedCard) as Card;
} catch (parseError) {
console.error('Stored card parse error:', parseError);
sessionStorage.removeItem(storedCardKey);
}
}
if (!cardId) {
if (card.value) {
loading.value = false;
return;
}
error.value = 'No card ID provided.';
loading.value = false;
return;
}
try {
const response = await axios.get(`/api/get-card/${encodeURIComponent(cardId)}`);
if (response.data.cards) {
card.value = response.data.cards; card.value = response.data.cards;
sessionStorage.setItem(storedCardKey, JSON.stringify(response.data.cards));
}
} catch (err: any) { } catch (err: any) {
console.error('Card fetch error:', err); console.error('Card fetch error:', err);
if (!card.value) {
error.value = err.response?.data?.message || 'Failed to get cards from the server. Please contact support.'; error.value = err.response?.data?.message || 'Failed to get cards from the server. Please contact support.';
}
} finally { } finally {
loading.value = false; loading.value = false;
} }

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import CardResult from '@/components/ui/card/CardResult.vue'; import CardResult from '@/components/ui/card/CardResult.vue';
import LandingLayout from '@/layouts/app/LandingLayout.vue'; import LandingLayout from '@/layouts/app/LandingLayout.vue';
import type { Card } from '@/types/cart';
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import axios from 'axios'; import axios from 'axios';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
@ -9,7 +10,7 @@ const goToBooking = () => {
router.visit('/rendez-vous'); router.visit('/rendez-vous');
}; };
const cards = ref<Array<any>>([]); const cards = ref<Card[]>([]);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const loading = ref(true); const loading = ref(true);
@ -46,7 +47,6 @@ onMounted(async () => {
<LandingLayout> <LandingLayout>
<main class="flex flex-1 justify-center px-4 py-8 sm:px-6 sm:py-12 md:py-16 lg:px-8"> <main class="flex flex-1 justify-center px-4 py-8 sm:px-6 sm:py-12 md:py-16 lg:px-8">
<div class="layout-content-container flex w-full max-w-4xl flex-col"> <div class="layout-content-container flex w-full max-w-4xl flex-col">
<!-- Header Section -->
<div class="mb-8 text-center md:mb-12"> <div class="mb-8 text-center md:mb-12">
<h1 class="text-midnight-blue mb-2 text-3xl font-bold sm:text-4xl md:text-5xl">Votre Lecture</h1> <h1 class="text-midnight-blue mb-2 text-3xl font-bold sm:text-4xl md:text-5xl">Votre Lecture</h1>
<p class="text-spiritual-earth mx-auto max-w-2xl text-base sm:text-lg"> <p class="text-spiritual-earth mx-auto max-w-2xl text-base sm:text-lg">
@ -54,12 +54,10 @@ onMounted(async () => {
</p> </p>
</div> </div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-16"> <div v-if="loading" class="flex items-center justify-center py-16">
<div class="h-12 w-12 animate-spin rounded-full border-t-2 border-b-2 border-[var(--subtle-gold)]"></div> <div class="h-12 w-12 animate-spin rounded-full border-t-2 border-b-2 border-[var(--subtle-gold)]"></div>
</div> </div>
<!-- Error State -->
<div v-else-if="error" class="mb-8 rounded-lg bg-red-50 p-6 text-center"> <div v-else-if="error" class="mb-8 rounded-lg bg-red-50 p-6 text-center">
<div class="mb-2 font-medium text-red-700">Erreur</div> <div class="mb-2 font-medium text-red-700">Erreur</div>
<p class="text-red-600">{{ error }}</p> <p class="text-red-600">{{ error }}</p>
@ -68,18 +66,18 @@ onMounted(async () => {
</button> </button>
</div> </div>
<!-- Empty State -->
<div v-else-if="!hasCards" class="rounded-lg bg-gray-50 p-8 text-center"> <div v-else-if="!hasCards" class="rounded-lg bg-gray-50 p-8 text-center">
<p class="text-gray-600">Aucune carte n'a été trouvée pour votre session.</p> <p class="text-gray-600">Aucune carte n'a été trouvée pour votre session.</p>
</div> </div>
<!-- Cards Results -->
<div v-else class="mb-8 md:mb-12"> <div v-else class="mb-8 md:mb-12">
<div class="space-y-6 md:space-y-8"> <div class="space-y-6 md:space-y-8">
<card-result <card-result
v-for="(card, index) in cards" v-for="(card, index) in cards"
:key="card.id || index" :key="card.id || index"
:card-number="card.id" :card-id="card.id"
:asset-id="card.asset_id"
:fallback-index="index"
:name="card.name" :name="card.name"
:image-url="card.image_url" :image-url="card.image_url"
:orientation="card.orientation" :orientation="card.orientation"
@ -88,21 +86,17 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- Consultation CTA -->
<div <div
v-if="!loading && !error" v-if="!loading && !error"
class="border-linen rounded-lg border bg-white p-6 text-center shadow-sm transition-all duration-300 hover:shadow-md md:p-8 lg:p-12" class="border-linen rounded-lg border bg-white p-6 text-center shadow-sm transition-all duration-300 hover:shadow-md md:p-8 lg:p-12"
> >
<div class="bg-subtle-gold mx-auto mb-6 h-1 w-16 rounded-full"></div> <div class="bg-subtle-gold mx-auto mb-6 h-1 w-16 rounded-full"></div>
<!-- Nouveau contenu -->
<div class="space-y-6 text-left"> <div class="space-y-6 text-left">
<!-- En-tête -->
<div class="text-center"> <div class="text-center">
<h3 class="text-midnight-blue mb-4 font-heading text-xl font-bold md:text-2xl">Cher.e Explorateur des Symboles,</h3> <h3 class="text-midnight-blue mb-4 font-heading text-xl font-bold md:text-2xl">Cher.e Explorateur des Symboles,</h3>
</div> </div>
<!-- Grilles de lecture -->
<div class="space-y-3"> <div class="space-y-3">
<p class="text-midnight-blue/80 font-medium">Votre tirage en ligne vous a offert 3 grilles de lecture :</p> <p class="text-midnight-blue/80 font-medium">Votre tirage en ligne vous a offert 3 grilles de lecture :</p>
<div class="space-y-2 pl-4"> <div class="space-y-2 pl-4">
@ -121,7 +115,6 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- Explication consultation -->
<div class="space-y-3"> <div class="space-y-3">
<p class="text-midnight-blue/80"> <p class="text-midnight-blue/80">
Ces révélations ne sont qu'un prélude à ce que nous pourrions accomplir en consultation directe. Parce que les arcanes Ces révélations ne sont qu'un prélude à ce que nous pourrions accomplir en consultation directe. Parce que les arcanes
@ -134,7 +127,6 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- Offre Éclaireur -->
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-spiritual-earth text-lg"></span> <span class="text-spiritual-earth text-lg"></span>
@ -160,7 +152,6 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- Bouton CTA et informations -->
<div class="mt-8 space-y-4"> <div class="mt-8 space-y-4">
<button <button
@click="goToBooking" @click="goToBooking"
@ -169,7 +160,6 @@ onMounted(async () => {
Réserver ma session amplifiée ici Réserver ma session amplifiée ici
</button> </button>
<!-- Informations tarif et durée -->
<div class="space-y-2"> <div class="space-y-2">
<p class="text-spiritual-earth text-sm font-semibold">Durée limitée à 15 jours</p> <p class="text-spiritual-earth text-sm font-semibold">Durée limitée à 15 jours</p>
<p class="text-midnight-blue text-lg font-bold">Tarif préférentiel pour les détenteurs de tirage numérique : 390 </p> <p class="text-midnight-blue text-lg font-bold">Tarif préférentiel pour les détenteurs de tirage numérique : 390 </p>

View File

@ -1,5 +1,6 @@
export interface Card { export interface Card {
id: number; id: number | string;
asset_id?: number | null;
name: string; name: string;
description_upright: string; description_upright: string;
description_reversed: string; description_reversed: string;

View File

@ -0,0 +1,23 @@
import type { Card } from '@/types/cart';
export function resolveCardImage(card: Pick<Card, 'id' | 'asset_id' | 'image_url'>, fallbackIndex?: number): string {
if (card.image_url) {
return card.image_url;
}
if (typeof card.asset_id === 'number' && Number.isInteger(card.asset_id) && card.asset_id > 0) {
return `/cards/${card.asset_id + 1}.png`;
}
const numericId = Number(card.id);
if (Number.isInteger(numericId) && numericId > 0) {
return `/cards/${numericId + 1}.png`;
}
if (typeof fallbackIndex === 'number' && fallbackIndex >= 0) {
return `/cards/${fallbackIndex + 2}.png`;
}
return '/cards/2.png';
}

View File

@ -1,7 +1,10 @@
<?php <?php
use Illuminate\Foundation\Inspiring; use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use App\Support\CardCsvImporter; use App\Support\CardCsvImporter;
Artisan::command('inspire', function () { Artisan::command('inspire', function () {
@ -24,3 +27,101 @@ Artisan::command('cards:import {file : Absolute path to the CSV file} {--delimit
$this->error($e->getMessage()); $this->error($e->getMessage());
} }
})->purpose('Import cards from a CSV file (upsert by name).'); })->purpose('Import cards from a CSV file (upsert by name).');
Artisan::command('cards:convert-id-to-uuid {--prepare-only : Only backfill the temporary uuid column} {--force : Skip the confirmation prompt}', function () {
if (! Schema::hasTable('cards')) {
$this->error('The cards table does not exist.');
return self::FAILURE;
}
$idColumn = collect(DB::select('SHOW COLUMNS FROM cards LIKE "id"'))->first();
if (! $idColumn) {
$this->error('The cards.id column does not exist.');
return self::FAILURE;
}
if (str_starts_with(strtolower((string) $idColumn->Type), 'char(36)')) {
$this->info('cards.id is already using UUIDs.');
return self::SUCCESS;
}
if (! Schema::hasColumn('cards', 'uuid')) {
$this->error('Missing cards.uuid column. Run php artisan migrate first.');
return self::FAILURE;
}
if (! Schema::hasColumn('cards', 'asset_id')) {
$this->error('Missing cards.asset_id column. Run php artisan migrate first.');
return self::FAILURE;
}
$cards = DB::table('cards')->select(['id', 'uuid', 'asset_id'])->orderBy('id')->get();
foreach ($cards as $card) {
$updates = [];
if (! $card->uuid) {
$updates['uuid'] = (string) Str::uuid();
}
if (! $card->asset_id) {
$updates['asset_id'] = (int) $card->id;
}
if ($updates === []) {
continue;
}
DB::table('cards')
->where('id', $card->id)
->update($updates);
}
$missingUuidCount = DB::table('cards')->whereNull('uuid')->count();
if ($missingUuidCount > 0) {
$this->error("Backfill incomplete. {$missingUuidCount} cards are still missing UUIDs.");
return self::FAILURE;
}
$duplicateUuidRows = collect(DB::select('SELECT uuid, COUNT(*) AS total FROM cards GROUP BY uuid HAVING COUNT(*) > 1'));
if ($duplicateUuidRows->isNotEmpty()) {
$this->error('Duplicate UUIDs detected in cards.uuid. Aborting swap.');
return self::FAILURE;
}
$this->info('Temporary UUIDs are ready for all existing cards.');
if ($this->option('prepare-only')) {
$this->comment('Preparation only mode finished. Run the command again without --prepare-only to swap the primary key.');
return self::SUCCESS;
}
if (! $this->option('force') && ! $this->confirm('This will permanently replace cards.id with UUID values. Continue?')) {
$this->comment('Operation cancelled.');
return self::SUCCESS;
}
// MySQL requires AUTO_INCREMENT columns to remain indexed, so remove the
// attribute before dropping the integer primary key.
DB::statement('ALTER TABLE cards MODIFY id BIGINT UNSIGNED NOT NULL');
DB::statement('ALTER TABLE cards DROP PRIMARY KEY');
DB::statement('ALTER TABLE cards DROP COLUMN id');
DB::statement('ALTER TABLE cards CHANGE uuid id CHAR(36) NOT NULL');
DB::statement('ALTER TABLE cards ADD PRIMARY KEY (id)');
$this->info('cards.id has been converted to UUID successfully.');
return self::SUCCESS;
})->purpose('Backfill UUIDs for cards and swap cards.id from bigint to UUID.');