Fix : Changer Id en UUID
This commit is contained in:
parent
79c52d0236
commit
390e84a55e
@ -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([
|
||||||
|
|||||||
@ -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)');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
BIN
build.tar.gz
Normal file
Binary file not shown.
@ -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",
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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
733
oracle_dump.sql
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
3
resources/js/types/cart.d.ts
vendored
3
resources/js/types/cart.d.ts
vendored
@ -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;
|
||||||
|
|||||||
23
resources/js/utils/resolveCardImage.ts
Normal file
23
resources/js/utils/resolveCardImage.ts
Normal 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';
|
||||||
|
}
|
||||||
@ -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.');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user