Tirage des carte

This commit is contained in:
Nyavokevin 2025-09-05 17:28:33 +03:00
parent a81ec57958
commit 5e4a4955f3
44 changed files with 1253 additions and 2 deletions

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Log;
use App\Repositories\CardRepositoryInterface;
class CardController extends Controller
{
protected $cardRepository;
public function __construct(CardRepositoryInterface $cardRepository)
{
$this->cardRepository = $cardRepository;
}
public function index()
{
try {
$cards = app('App\Repositories\CardRepositoryInterface')->all();
return Inertia::render('cards/shuffle', [
'cards' => $cards,
]);
} catch (\Exception $e) {
// Log the error for debugging
Log::error('Error fetching cards: '.$e->getMessage(), [
'trace' => $e->getTraceAsString()
]);
// Optionally, you can return an Inertia error page or empty array
return Inertia::render('Cards/Index', [
'cards' => [],
'error' => 'Impossible de récupérer les cartes pour le moment.'
]);
}
}
public function drawCard(Request $request)
{
// Validate the request if needed
$request->validate([
'count' => 'sometimes|integer'
]);
$cardDraw = $this->cardRepository->draw($request->count);
// Return the response (Inertia will automatically handle this)
return response()->json([
'success' => true,
'card' => $cardDraw,
'message' => 'Card drawn successfully'
]);
}
}

25
app/Models/Card.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Card extends Model
{
use HasFactory;
protected $table = 'cards';
protected $fillable = [
'name',
'description_upright',
'description_reversed',
'symbolism',
'image_url',
];
protected $casts = [
'symbolism' => 'array',
];
}

View File

@ -4,6 +4,9 @@ namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Repositories\CardRepository;
use App\Repositories\CardRepositoryInterface;
class AppServiceProvider extends ServiceProvider
{
/**
@ -11,7 +14,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
$this->app->bind(CardRepositoryInterface::class, CardRepository::class);
}
/**

View File

@ -0,0 +1,84 @@
<?php
namespace App\Repositories;
use App\Models\Card;
use Illuminate\Database\Eloquent\Collection;
use App\Repositories\CardRepositoryInterface;
class CardRepository implements CardRepositoryInterface
{
public function all(): Collection
{
return Card::all();
}
public function find(int $id): ?Card
{
return Card::find($id);
}
public function create(array $data): Card
{
return Card::create($data);
}
public function update(int $id, array $data): ?Card
{
$card = Card::find($id);
if (! $card) {
return null;
}
$card->update($data);
return $card;
}
public function delete(int $id): bool
{
$card = Card::find($id);
if (! $card) {
return false;
}
return (bool) $card->delete();
}
/**
* Draw oracle cards
*
* @param int $count Number of cards to draw (1, 6, 18, 21, etc.)
* @return array
*/
public function draw(int $count = 1): array
{
// Récupère toutes les cartes (80 dans la DB)
$cards = Card::all();
// Mélange avec shuffle (FisherYates est fait par Laravel via ->shuffle())
$shuffled = $cards->shuffle();
// Prend les $count premières cartes
$selected = $shuffled->take($count);
// Pour chaque carte, ajoute orientation + description
$results = $selected->map(function ($card) {
$isReversed = (bool) random_int(0, 1); // 50% upright / 50% reversed
return [
'id' => $card->id,
'name' => $card->name,
'image_url' => $card->image_url,
'orientation' => $isReversed ? 'reversed' : 'upright',
'description' => $isReversed ? $card->description_reversed : $card->description_upright,
'symbolism' => $card->symbolism,
'created_at' => now(),
];
});
return $results->toArray();
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Repositories;
use App\Models\Card;
use Illuminate\Database\Eloquent\Collection;
interface CardRepositoryInterface
{
public function all(): Collection;
public function find(int $id): ?Card;
public function create(array $data): Card;
public function update(int $id, array $data): ?Card;
public function delete(int $id): bool;
public function draw(): array;
}

0
bootstrap/cache/.gitignore vendored Normal file → Executable file
View File

View File

@ -0,0 +1,66 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Card;
class CardSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$cards = [
[
'name' => 'Le Fou',
'description_upright' => 'Nouveaux départs, spontanéité, innocence, esprit libre.',
'description_reversed' => 'Imprudence, prise de risques inconsidérée, blocages.',
'symbolism' => [
'numéro' => 0,
'élément' => 'Air',
'planète' => 'Uranus'
],
'image_url' => 'storage/cards/1.png',
],
[
'name' => 'Le Magicien',
'description_upright' => 'Manifestation, ingéniosité, pouvoir, action inspirée.',
'description_reversed' => 'Manipulation, talents inexploités, illusions.',
'symbolism' => [
'numéro' => 1,
'élément' => 'Air',
'planète' => 'Mercure'
],
'image_url' => 'storage/cards/2.png',
],
[
'name' => 'La Grande Prêtresse',
'description_upright' => 'Intuition, savoir sacré, féminin divin, mystère.',
'description_reversed' => 'Secrets, déconnexion de lintuition, retrait.',
'symbolism' => [
'numéro' => 2,
'élément' => 'Eau',
'planète' => 'Lune'
],
'image_url' => 'storage/cards/3.png',
],
[
'name' => "L'Impératrice",
'description_upright' => 'Féminité, abondance, fertilité, créativité, nature.',
'description_reversed' => 'Dépendance, blocages créatifs, excès ou manque.',
'symbolism' => [
'numéro' => 3,
'élément' => 'Terre',
'planète' => 'Vénus'
],
'image_url' => 'storage/cards/4.png',
],
];
foreach ($cards as $card) {
Card::create($card);
}
}
}

145
package-lock.json generated
View File

@ -6,11 +6,14 @@
"": {
"dependencies": {
"@inertiajs/vue3": "^2.1.0",
"@vue-stripe/vue-stripe": "^4.5.0",
"@vueuse/core": "^12.8.2",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"laravel-vite-plugin": "^2.0.0",
"lucide-vue-next": "^0.468.0",
"pinia": "^3.0.3",
"reka-ui": "^2.2.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",
@ -1223,6 +1226,11 @@
"win32"
]
},
"node_modules/@stripe/stripe-js": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz",
"integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg=="
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
@ -1854,6 +1862,15 @@
"vscode-uri": "^3.0.8"
}
},
"node_modules/@vue-stripe/vue-stripe": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@vue-stripe/vue-stripe/-/vue-stripe-4.5.0.tgz",
"integrity": "sha512-BU449XT5zegjNQirl+SSztbzGIvPjhxlHv8ybomSZcI1jB6qEpLgpk2eHMFDKnOGZZRhqtg4C5FiErwSJ/yuRw==",
"dependencies": {
"@stripe/stripe-js": "^1.13.2",
"vue-coerce-props": "^1.0.0"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.18",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
@ -1915,6 +1932,36 @@
"he": "^1.2.0"
}
},
"node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"dependencies": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/eslint-config-typescript": {
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.6.0.tgz",
@ -2154,7 +2201,6 @@
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@ -2168,6 +2214,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/birpc": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz",
"integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@ -2378,6 +2432,20 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -3303,6 +3371,11 @@
"he": "bin/he"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -3383,6 +3456,17 @@
"node": ">=0.12.0"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -3847,6 +3931,11 @@
"node": ">= 18"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
@ -4023,6 +4112,11 @@
"node": ">=8"
}
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -4041,6 +4135,26 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@ -4317,6 +4431,11 @@
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
},
"node_modules/rollup": {
"version": "4.46.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.3.tgz",
@ -4533,6 +4652,14 @@
"node": ">=0.10.0"
}
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -4574,6 +4701,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@ -4959,6 +5097,11 @@
}
}
},
"node_modules/vue-coerce-props": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vue-coerce-props/-/vue-coerce-props-1.0.0.tgz",
"integrity": "sha512-4fdRMXO6FHzmE7H4soAph6QmPg3sL/RiGdd+axuxuU07f02LNMns0jMM88fmt1bvSbN+2Wyd8raho6p6nXUzag=="
},
"node_modules/vue-eslint-parser": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz",

View File

@ -30,11 +30,14 @@
},
"dependencies": {
"@inertiajs/vue3": "^2.1.0",
"@vue-stripe/vue-stripe": "^4.5.0",
"@vueuse/core": "^12.8.2",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"laravel-vite-plugin": "^2.0.0",
"lucide-vue-next": "^0.468.0",
"pinia": "^3.0.3",
"reka-ui": "^2.2.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.1",

BIN
public/cards/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
public/cards/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
public/cards/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
public/cards/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -2,17 +2,20 @@ import '../css/app.css';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createPinia } from 'pinia';
import type { DefineComponent } from 'vue';
import { createApp, h } from 'vue';
import { initializeTheme } from './composables/useAppearance';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
const pinia = createPinia();
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
resolve: (name) => resolvePageComponent(`./pages/${name}.vue`, import.meta.glob<DefineComponent>('./pages/**/*.vue')),
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(pinia)
.use(plugin)
.mount(el);
},

View File

@ -0,0 +1,563 @@
<script setup lang="ts">
import CardShuffleTemplate from '@/components/template/CardShuffleTemplate.vue';
import { Card } from '@/types/cart';
import { ref, watch } from 'vue';
const emit = defineEmits(['drawCard']);
const isClicked = ref(false);
const isDrawing = ref(false);
const drawnCards = ref<Card[]>([]); // Changed to array to handle multiple cards
const showResult = ref(false);
const isFlipped = ref<boolean[]>([]); // Array to track flip state for each card
const handleClick = () => {
if (isDrawing.value) return;
isClicked.value = true;
isDrawing.value = true;
emit('drawCard');
setTimeout(() => (isClicked.value = false), 500);
};
// This function would be called from the parent component when the card data is received
const setDrawnCards = (cardData: Card[]) => {
drawnCards.value = cardData;
isDrawing.value = false;
showResult.value = true;
// Initialize flip states for each card
isFlipped.value = new Array(cardData.length).fill(false);
// Add confetti effect
createConfetti();
};
watch(drawnCards, (newVal) => {
console.log('Drawn cards:', newVal);
});
const flipCard = (index: number) => {
isFlipped.value[index] = !isFlipped.value[index];
};
const createConfetti = () => {
const confettiContainer = document.createElement('div');
confettiContainer.style.position = 'fixed';
confettiContainer.style.top = '0';
confettiContainer.style.left = '0';
confettiContainer.style.width = '100%';
confettiContainer.style.height = '100%';
confettiContainer.style.pointerEvents = 'none';
confettiContainer.style.zIndex = '5'; // Lower z-index so cards appear above
document.body.appendChild(confettiContainer);
const colors = ['#D7BA8D', '#A06D52', '#1F2A44', '#FFFFFF'];
const confettiCount = 100;
for (let i = 0; i < confettiCount; i++) {
const confetti = document.createElement('div');
confetti.style.position = 'absolute';
confetti.style.width = '10px';
confetti.style.height = '10px';
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0';
confetti.style.top = '50%';
confetti.style.left = '50%';
confetti.style.opacity = '0';
confettiContainer.appendChild(confetti);
const animation = confetti.animate(
[
{
transform: 'translate(0, 0) rotate(0deg)',
opacity: 1,
},
{
transform: `translate(${Math.random() * 400 - 200}px, ${Math.random() * 400 - 200}px) rotate(${Math.random() * 360}deg)`,
opacity: 0,
},
],
{
duration: 1000 + Math.random() * 1000,
easing: 'cubic-bezier(0.1, 0.8, 0.3, 1)',
},
);
animation.onfinish = () => {
confetti.remove();
if (confettiContainer.children.length === 0) {
confettiContainer.remove();
}
};
}
};
// Expose the setDrawnCards function to parent component
defineExpose({ setDrawnCards });
</script>
<template>
<CardShuffleTemplate>
<template #card-shuffle-slot>
<div class="card-container">
<div
class="card-stack relative mt-4 mb-4 flex h-[500px] w-[300px] items-center justify-center"
:class="{ clicked: isClicked, drawing: isDrawing }"
@click="handleClick"
>
<div class="card" style="transform: rotate(-3deg) translateZ(0); z-index: 3">
<div class="card-back">
<div class="card-back-design">
<svg
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
<path
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
<path
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
</svg>
</div>
</div>
</div>
<div class="card" style="transform: rotate(1deg) translateZ(-10px); z-index: 2">
<div class="card-back">
<div class="card-back-design">
<svg
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
<path
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
<path
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
</svg>
</div>
</div>
</div>
<div class="card" style="transform: rotate(4deg) translateZ(-20px); z-index: 1">
<div class="card-back">
<div class="card-back-design">
<svg
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
<path
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
<path
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
</svg>
</div>
</div>
</div>
</div>
<div v-if="showResult && drawnCards.length" class="cards-result-container">
<div v-for="(card, index) in drawnCards" :key="index" class="card-result-wrapper">
<div class="result-card" :class="{ flipped: isFlipped[index] }" @click="flipCard(index)">
<div class="card-face card-unknown-front">
<div class="card-back-design">
<svg
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
<path
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
<path
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="0.5"
></path>
</svg>
</div>
</div>
<div class="card-face card-known-back">
<img :src="card.image_url!" :alt="card.name" class="card-image" />
<div class="card-description-overlay">
<h3>{{ card.name }}</h3>
<p class="description">{{ card.description }}</p>
<p v-if="card.orientation" class="orientation">
{{ card.orientation === 'reversed' ? 'Inversée' : 'Droite' }}
</p>
<div v-if="card.symbolism" class="symbolism">
<p><strong>Numéro:</strong> {{ card.symbolism.numéro }}</p>
<p><strong>Planète:</strong> {{ card.symbolism.planète }}</p>
<p><strong>Élément:</strong> {{ card.symbolism.élément }}</p>
</div>
<p class="click-hint">Cliquez pour retourner</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</CardShuffleTemplate>
</template>
<style scoped>
.card-container {
display: flex;
flex-direction: column;
align-items: center;
}
.card {
width: 250px;
height: 400px;
background: linear-gradient(145deg, var(--pure-white), var(--linen));
border-radius: 16px;
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.1),
0 6px 6px rgba(0, 0, 0, 0.1);
position: absolute;
transition:
transform 0.5s ease-in-out,
box-shadow 0.5s ease-in-out;
cursor: pointer;
transform-style: preserve-3d;
backface-visibility: hidden;
}
/* Animation stack globale */
.card-stack {
transition: transform 0.6s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
perspective: 1000px; /* ajoute profondeur */
}
/* Hover sur la pile */
.card-stack:hover {
transform: translateY(-10px) rotateX(2deg) rotateY(-2deg);
}
.card-stack:hover .card:nth-child(1) {
transform: rotateY(-5deg) rotateX(5deg) translateZ(30px) translateX(-20px);
}
.card-stack:hover .card:nth-child(2) {
transform: rotateY(0deg) rotateX(2deg) translateZ(20px);
}
.card-stack:hover .card:nth-child(3) {
transform: rotateY(5deg) rotateX(5deg) translateZ(10px) translateX(20px);
}
/* Glow doré subtil au hover */
.card-stack:hover .card-back {
box-shadow: 0 0 20px rgba(215, 186, 141, 0.6);
transition: box-shadow 0.6s ease-in-out;
}
/* Animation click */
@keyframes card-click-tilt {
0% {
transform: translateY(-10px) rotateX(2deg) rotateY(-2deg);
}
30% {
transform: translateY(-5px) rotateX(-4deg) rotateY(4deg);
}
60% {
transform: translateY(-12px) rotateX(3deg) rotateY(-3deg);
}
100% {
transform: translateY(-10px) rotateX(2deg) rotateY(-2deg);
}
}
/* Drawing animation */
@keyframes card-drawing {
0% {
transform: translateY(-10px) rotateX(2deg) rotateY(-2deg);
}
25% {
transform: translateY(-30px) rotateX(10deg) rotateY(-10deg);
}
50% {
transform: translateY(-40px) rotateX(-5deg) rotateY(5deg);
}
75% {
transform: translateY(-30px) rotateX(5deg) rotateY(-5deg);
}
100% {
transform: translateY(-10px) rotateX(2deg) rotateY(-2deg);
}
}
/* Active sur clic */
.card-stack.clicked {
animation: card-click-tilt 0.4s ease-in-out;
}
.card-stack.drawing {
animation: card-drawing 1.5s ease-in-out;
}
/* Back des cartes */
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--subtle-gold);
background: radial-gradient(circle, var(--midnight-blue) 0%, #121a2c 100%);
}
.card-result-wrapper {
width: 250px;
height: 400px;
perspective: 1000px;
}
.card-back-design-wrapper {
background: radial-gradient(circle, var(--midnight-blue) 0%, #121a2c 100%);
display: flex;
align-items: center;
justify-content: center;
}
.card-back-design {
width: 80%;
height: 80%;
border: 2px solid var(--subtle-gold);
border-radius: 8px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.card-back-design::before,
.card-back-design::after {
content: '';
position: absolute;
width: 50%;
height: 50%;
border-color: var(--subtle-gold);
opacity: 0.5;
}
.card-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden; /* This is crucial for the flip effect */
border-radius: 16px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
overflow: hidden; /* To keep content within borders */
}
.card-unknown-front {
background: radial-gradient(circle, var(--midnight-blue) 0%, #121a2c 100%);
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--subtle-gold);
}
.card-known-back {
transform: rotateY(180deg); /* This face starts rotated, so it's hidden */
position: relative;
}
.card-back-design::before {
top: -2px;
left: -2px;
border-top-width: 1px;
border-left-width: 1px;
border-top-style: solid;
border-left-style: solid;
border-top-left-radius: 8px;
}
.card-back-design::after {
bottom: -2px;
right: -2px;
border-bottom-width: 1px;
border-right-width: 1px;
border-bottom-style: solid;
border-right-style: solid;
border-bottom-right-radius: 8px;
}
/* Result card styles */
.cards-result-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
margin-top: 2rem;
position: relative;
z-index: 10;
}
.card-result-container {
perspective: 1000px;
}
.result-card {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.8s;
cursor: pointer;
}
.result-card.flipped {
transform: rotateY(180deg);
}
.card-front,
.card-back-info {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
text-align: center;
overflow-y: auto;
}
.card-back-info p {
color: var(--midnight-blue); /* Adjust color if needed */
}
.card-front {
background: linear-gradient(145deg, var(--pure-white), var(--linen));
color: var(--midnight-blue);
}
.card-back-info {
background: linear-gradient(145deg, var(--midnight-blue), #121a2c);
color: var(--pure-white);
transform: rotateY(180deg);
text-align: center;
overflow-y: auto;
}
.card-content-wrapper {
background: linear-gradient(145deg, var(--pure-white), var(--linen));
color: var(--midnight-blue);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
transform: rotateY(180deg); /* This is the key part to make it the 'back' of the card */
}
.card-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 16px;
position: absolute;
top: 0;
left: 0;
}
.card-description-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6); /* Semi-transparent overlay for readability */
color: white;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: flex-end; /* Align content to the bottom */
align-items: center;
text-align: center;
box-sizing: border-box;
}
.card-description-overlay h3,
.card-description-overlay p {
margin: 0.2rem 0;
}
.orientation {
font-style: italic;
margin-top: 0.5rem;
color: var(--spiritual-earth);
}
.description {
margin: 1rem 0;
font-size: 0.9rem;
}
.symbolism {
margin-top: 1rem;
font-size: 0.8rem;
}
.symbolism p {
margin: 0.3rem 0;
}
.click-hint {
margin-top: 1rem;
font-size: 0.7rem;
opacity: 0.7;
}
</style>

View File

@ -0,0 +1,17 @@
<template>
<main class="flex flex-1 flex-col items-center justify-center px-4 pt-12 pb-24 sm:px-6 lg:px-8">
<div class="relative z-10 flex w-full max-w-4xl flex-col items-center justify-center text-center">
<h1 class="text-5xl font-bold text-[var(--midnight-blue)] md:text-6xl">
L'Oracle de votre<span class="citadel-script ml-4 text-6xl text-[var(--spiritual-earth)] md:text-7xl">Destinée</span>
</h1>
<p class="mt-4 max-w-2xl text-lg text-[var(--midnight-blue)]/80">
Puisez dans la sagesse ancestrale pour éclairer votre chemin. Tirez une carte et recevez le message qui vous est destiné aujourd'hui.
</p>
<!-- Card shuffle slot -->
<slot name="card-shuffle-slot" />
<!-- Button tirage -->
</div>
</main>
</template>

50
resources/js/lib/http.ts Normal file
View File

@ -0,0 +1,50 @@
import axios, { type AxiosError, type AxiosInstance } from 'axios'
// SSR-safe guard for browser-only features
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
function getCsrfTokenFromMeta(): string | null {
if (!isBrowser) return null
const el = document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null
return el?.content ?? null
}
// Create a preconfigured Axios instance for the app
const http: AxiosInstance = axios.create({
baseURL: '/',
withCredentials: true, // include cookies for same-origin requests
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
},
// If you use Laravel Sanctum's CSRF cookie, these defaults help automatically send it
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
timeout: 30000,
})
// Attach CSRF token from Blade <meta name="csrf-token" ...> when present
http.interceptors.request.use((config) => {
const token = getCsrfTokenFromMeta()
if (token) {
// Laravel will accept either X-CSRF-TOKEN (meta) or X-XSRF-TOKEN (cookie)
config.headers = config.headers ?? {}
;(config.headers as Record<string, string>)['X-CSRF-TOKEN'] = token
}
return config
})
// Basic error passthrough; customize as needed
http.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
// Example handling: if (error.response?.status === 401) { /* redirect to login */ }
// Example handling: if (error.response?.status === 419) { /* CSRF token mismatch */ }
return Promise.reject(error)
}
)
export type { AxiosError, AxiosInstance }
export { http }
export default http

View File

@ -0,0 +1,19 @@
<template>
<div>
<h1>Liste des cards</h1>
<ul>
<li v-for="card in cards" :key="card.id">{{ card.name }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
import type { Card } from '@/types/cart';
import { defineProps } from 'vue';
const props = defineProps({
cards: Array<Card>,
});
console.log(props.cards);
</script>

View File

@ -0,0 +1,107 @@
<script setup lang="ts">
import ShuffleCardPresentation from '@/components/organism/ShuffleCard/ShuffleCardPresentation.vue';
import LandingLayout from '@/layouts/app/LandingLayout.vue';
import { useTarotStore } from '@/stores/tarot';
import axios from 'axios';
import { ref } from 'vue';
const cardComponent = ref();
const tarotStore = useTarotStore();
const isSelectionScreen = ref(true);
const loading = ref(false);
// This variable will hold the number of cards to draw
const drawCount = ref(0);
// This function will be called from the "offer" buttons
const handleSelection = (count: number) => {
drawCount.value = count;
// Check if the draw is free or requires payment
if (count === 1) {
// Free draw
if (tarotStore.freeDrawsRemaining > 0) {
tarotStore.useFreeDraw();
isSelectionScreen.value = false; // Switch to the shuffle screen
} else {
alert('You have used your free draw. Please choose a paid option to unlock more.');
}
} else {
// Paid draw
// This is where you'd trigger your Stripe payment component
alert(`Initiating payment process for a ${count}-card draw.`);
// For now, let's simulate a successful payment and then proceed
tarotStore.unlockNewDraws().then(() => {
isSelectionScreen.value = false;
});
}
};
const getCard = async () => {
loading.value = true;
try {
const res = await axios.post('/draw-card', { count: drawCount.value });
if (res.data) {
cardComponent.value.setDrawnCards(res.data.card);
}
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
};
</script>
<template>
<LandingLayout>
<section v-if="isSelectionScreen" class="py-20 sm:py-24">
<h2 class="mb-16 text-center text-4xl font-bold text-[var(--midnight-blue)] md:text-5xl">Explorez Nos Lectures</h2>
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
<div
class="flex flex-col gap-6 rounded-2xl border border-[var(--linen)] bg-[var(--pure-white)] p-8 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
>
<div class="text-center">
<h3 class="text-2xl font-bold text-[var(--midnight-blue)]">Lecture Gratuite</h3>
<p class="mt-2 text-5xl font-bold text-[var(--subtle-gold)]">Gratuit</p>
</div>
<button
class="mt-4 flex h-12 w-full items-center justify-center rounded-full bg-[var(--linen)] px-8 font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:text-[var(--pure-white)]"
@click="handleSelection(1)"
>
Commencer
</button>
</div>
<div
class="flex scale-105 flex-col gap-6 rounded-2xl bg-[var(--midnight-blue)] p-8 shadow-lg ring-2 ring-[var(--subtle-gold)] transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
>
<div class="text-center">
<h3 class="text-2xl font-bold text-[var(--pure-white)]">Profilage</h3>
<p class="mt-2 text-5xl font-bold text-[var(--subtle-gold)]">29</p>
</div>
<button
class="mt-4 flex h-12 w-full items-center justify-center rounded-full bg-[var(--subtle-gold)] px-8 font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--pure-white)]"
@click="handleSelection(3)"
>
Découvrir
</button>
</div>
<div
class="flex flex-col gap-6 rounded-2xl border border-[var(--linen)] bg-[var(--pure-white)] p-8 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
>
<div class="text-center">
<h3 class="text-2xl font-bold text-[var(--midnight-blue)]">Quadrige Doré</h3>
<p class="mt-2 text-5xl font-bold text-[var(--subtle-gold)]">99</p>
</div>
<button
class="mt-4 flex h-12 w-full items-center justify-center rounded-full bg-[var(--linen)] px-8 font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:text-[var(--pure-white)]"
@click="handleSelection(4)"
>
Explorer
</button>
</div>
</div>
</section>
<ShuffleCardPresentation v-else ref="cardComponent" @draw-card="getCard" />
</LandingLayout>
</template>

View File

@ -0,0 +1,39 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useTarotStore = defineStore('tarot', () => {
// State
const freeDrawsRemaining = ref(1);
// Actions
function useFreeDraw() {
if (freeDrawsRemaining.value > 0) {
freeDrawsRemaining.value--;
return true; // Indicates a free draw was used
}
return false; // No more free draws
}
// You would integrate Stripe here in a more advanced application
// This is a placeholder for your payment logic.
function unlockNewDraws() {
// You would typically call a backend endpoint here to create a Stripe Checkout Session
// and redirect the user. For this example, we'll simulate a successful payment.
console.log('Redirecting to Stripe for payment...');
return new Promise((resolve) => {
setTimeout(() => {
console.log('Payment successful! Adding 1 new draw.');
// After successful payment from Stripe, you would update the state.
// This state update would likely come from a backend webhook.
freeDrawsRemaining.value++;
resolve(true);
}, 2000); // Simulate a network delay
});
}
return {
freeDrawsRemaining,
useFreeDraw,
unlockNewDraws,
};
});

12
resources/js/types/cart.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
export interface Card {
id: number;
name: string;
description_upright: string;
description_reversed: string;
symbolism: Record<string, string> | null; // objet JSON ou null
orientation?: string;
image_url: string | null;
created_at: string;
updated_at: string;
description: string;
}

View File

@ -11,5 +11,9 @@ Route::get('dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
// Route::get('/cards', [App\Http\Controllers\CardController::class, 'index'])->name('cards.index');
Route::get('/tirage',[App\Http\Controllers\CardController::class, 'index'])->name('cards.shuffle');
Route::post('/draw-card', [App\Http\Controllers\CardController::class, 'drawCard']);
require __DIR__.'/settings.php';
require __DIR__.'/auth.php';

4
storage 1/app/.gitignore vendored Executable file
View File

@ -0,0 +1,4 @@
*
!private/
!public/
!.gitignore

2
storage 1/app/private/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage 1/app/public/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

9
storage 1/framework/.gitignore vendored Executable file
View File

@ -0,0 +1,9 @@
compiled.php
config.php
down
events.scanned.php
maintenance.php
routes.php
routes.scanned.php
schedule-*
services.json

3
storage 1/framework/cache/.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
*
!data/
!.gitignore

2
storage 1/framework/cache/data/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage 1/framework/sessions/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage 1/framework/testing/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage 1/framework/views/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage 1/logs/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

2
storage 1/pail/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

0
storage/app/.gitignore vendored Normal file → Executable file
View File

0
storage/app/private/.gitignore vendored Normal file → Executable file
View File

0
storage/app/public/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/cache/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/cache/data/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/sessions/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/testing/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/views/.gitignore vendored Normal file → Executable file
View File

0
storage/logs/.gitignore vendored Normal file → Executable file
View File