Tirage des carte
This commit is contained in:
parent
a81ec57958
commit
5e4a4955f3
60
app/Http/Controllers/CardController.php
Normal file
60
app/Http/Controllers/CardController.php
Normal 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
25
app/Models/Card.php
Normal 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',
|
||||
];
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
84
app/Repositories/CardRepository.php
Normal file
84
app/Repositories/CardRepository.php
Normal 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 (Fisher–Yates 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();
|
||||
}
|
||||
}
|
||||
21
app/Repositories/CardRepositoryInterface.php
Normal file
21
app/Repositories/CardRepositoryInterface.php
Normal 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
0
bootstrap/cache/.gitignore
vendored
Normal file → Executable file
66
database/seeders/CardSeeder.php
Normal file
66
database/seeders/CardSeeder.php
Normal 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 l’intuition, 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
145
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
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
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
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
BIN
public/cards/4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
@ -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);
|
||||
},
|
||||
|
||||
@ -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>
|
||||
0
resources/js/components/organism/StripePayment.vue
Normal file
0
resources/js/components/organism/StripePayment.vue
Normal file
17
resources/js/components/template/CardShuffleTemplate.vue
Normal file
17
resources/js/components/template/CardShuffleTemplate.vue
Normal 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
50
resources/js/lib/http.ts
Normal 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
|
||||
|
||||
19
resources/js/pages/cards/index.vue
Normal file
19
resources/js/pages/cards/index.vue
Normal 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>
|
||||
107
resources/js/pages/cards/shuffle.vue
Normal file
107
resources/js/pages/cards/shuffle.vue
Normal 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>
|
||||
39
resources/js/stores/tarot.ts
Normal file
39
resources/js/stores/tarot.ts
Normal 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
12
resources/js/types/cart.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@ -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
4
storage 1/app/.gitignore
vendored
Executable file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
2
storage 1/app/private/.gitignore
vendored
Executable file
2
storage 1/app/private/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage 1/app/public/.gitignore
vendored
Executable file
2
storage 1/app/public/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
9
storage 1/framework/.gitignore
vendored
Executable file
9
storage 1/framework/.gitignore
vendored
Executable 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
3
storage 1/framework/cache/.gitignore
vendored
Executable file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
2
storage 1/framework/cache/data/.gitignore
vendored
Executable file
2
storage 1/framework/cache/data/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage 1/framework/sessions/.gitignore
vendored
Executable file
2
storage 1/framework/sessions/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage 1/framework/testing/.gitignore
vendored
Executable file
2
storage 1/framework/testing/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage 1/framework/views/.gitignore
vendored
Executable file
2
storage 1/framework/views/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage 1/logs/.gitignore
vendored
Executable file
2
storage 1/logs/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage 1/pail/.gitignore
vendored
Executable file
2
storage 1/pail/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
0
storage/app/.gitignore
vendored
Normal file → Executable file
0
storage/app/.gitignore
vendored
Normal file → Executable file
0
storage/app/private/.gitignore
vendored
Normal file → Executable file
0
storage/app/private/.gitignore
vendored
Normal file → Executable file
0
storage/app/public/.gitignore
vendored
Normal file → Executable file
0
storage/app/public/.gitignore
vendored
Normal file → Executable file
0
storage/framework/.gitignore
vendored
Normal file → Executable file
0
storage/framework/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/data/.gitignore
vendored
Normal file → Executable file
0
storage/framework/cache/data/.gitignore
vendored
Normal file → Executable file
0
storage/framework/sessions/.gitignore
vendored
Normal file → Executable file
0
storage/framework/sessions/.gitignore
vendored
Normal file → Executable file
0
storage/framework/testing/.gitignore
vendored
Normal file → Executable file
0
storage/framework/testing/.gitignore
vendored
Normal file → Executable file
0
storage/framework/views/.gitignore
vendored
Normal file → Executable file
0
storage/framework/views/.gitignore
vendored
Normal file → Executable file
0
storage/logs/.gitignore
vendored
Normal file → Executable file
0
storage/logs/.gitignore
vendored
Normal file → Executable file
Loading…
x
Reference in New Issue
Block a user