Compare commits
2 Commits
5e4a4955f3
...
39c17b44cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c17b44cf | ||
|
|
9e5b160166 |
153
app/Http/Controllers/StripeController.php
Normal file
153
app/Http/Controllers/StripeController.php
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Stripe\Stripe;
|
||||||
|
use Stripe\Checkout\Session;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Repositories\CardRepositoryInterface;
|
||||||
|
|
||||||
|
|
||||||
|
class StripeController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function __construct(CardRepositoryInterface $cardRepository)
|
||||||
|
{
|
||||||
|
$this->cardRepository = $cardRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function createCheckoutSession(Request $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
|
||||||
|
|
||||||
|
$count = $request->input('count');
|
||||||
|
$clientSessionId = Str::uuid();
|
||||||
|
|
||||||
|
$priceIds = [
|
||||||
|
3 => 'price_1S51zxGaZ3yeYkzWYb0wSt4j',
|
||||||
|
4 => 'price_1S5464GaZ3yeYkzWh8RuJfab',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isset($priceIds[$count])) {
|
||||||
|
return response()->json(['error' => 'Invalid product selected.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$session = Session::create([
|
||||||
|
'line_items' => [[
|
||||||
|
'price' => $priceIds[$count],
|
||||||
|
'quantity' => 1,
|
||||||
|
]],
|
||||||
|
'mode' => 'payment',
|
||||||
|
'success_url' => url(env('APP_URL') . '/success?client_session_id=' . $clientSessionId),
|
||||||
|
'cancel_url' => url(env('APP_URL') . '/cancel'),
|
||||||
|
'metadata' => [
|
||||||
|
'draw_count' => $request->input('count'),
|
||||||
|
'client_session_id' => $clientSessionId,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Payment::create([
|
||||||
|
'amount' => $session->amount_total / 100,
|
||||||
|
'currency' => $session->currency,
|
||||||
|
'stripe_session_id' => $session->id,
|
||||||
|
'client_session_id' => $clientSessionId,
|
||||||
|
'draw_count' => $count,
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['sessionId' => $session->id]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Log::error('Stripe session creation failed: ' . $e->getMessage());
|
||||||
|
return response()->json(['error' => 'Could not create checkout session.'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validatePayment(Request $request)
|
||||||
|
{
|
||||||
|
$clientSessionId = $request->query('client_session_id');
|
||||||
|
|
||||||
|
$payment = Payment::where('client_session_id', $clientSessionId)
|
||||||
|
->where('status', 'succeeded')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($payment) {
|
||||||
|
// Si la vérification réussit, retournez le nombre de tirages.
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'drawCount' => $payment->draw_count,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la vérification échoue, retournez une erreur.
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Paiement non validé.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCards(Request $request)
|
||||||
|
{
|
||||||
|
$sessionId = $request->query('client_session_id');
|
||||||
|
|
||||||
|
if(!$sessionId)
|
||||||
|
{
|
||||||
|
$count = $request->query('count');
|
||||||
|
if($count == 1){
|
||||||
|
$freeCards = $this->cardRepository->draw(1);
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'cards' => $freeCards
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Find the payment record
|
||||||
|
$payment = Payment::where('client_session_id', $sessionId)->first();
|
||||||
|
|
||||||
|
if (!$payment) {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Payment not found.'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. One-Time Use Check
|
||||||
|
if ($payment->status === 'processed') {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'cards' => $payment->cards,
|
||||||
|
'message' => 'Cards already drawn for this payment.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verify payment status with Stripe
|
||||||
|
if ($payment->status !== 'succeeded') {
|
||||||
|
try {
|
||||||
|
$session = Session::retrieve($sessionId);
|
||||||
|
if ($session->payment_status !== 'paid' || $session->status !== 'complete') {
|
||||||
|
return response()->json(['success' => false, 'message' => 'Payment not complete.'], 402);
|
||||||
|
}
|
||||||
|
$payment->update(['status' => 'succeeded']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Log::error('Stripe session retrieval failed: ' . $e->getMessage());
|
||||||
|
return response()->json(['success' => false, 'message' => 'Validation error.'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Securely draw the cards and store them
|
||||||
|
$drawnCards = $this->cardRepository->draw($payment->draw_count);
|
||||||
|
$payment->update([
|
||||||
|
'cards' => $drawnCards,
|
||||||
|
'status' => 'processed',
|
||||||
|
]);
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'cards' => $drawnCards,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
62
app/Http/Controllers/WebhookController.php
Normal file
62
app/Http/Controllers/WebhookController.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Payment; // Make sure this is imported
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Stripe\Stripe;
|
||||||
|
use Stripe\Webhook;
|
||||||
|
use Stripe\Exception\SignatureVerificationException;
|
||||||
|
use Log; // Import the Log facade
|
||||||
|
|
||||||
|
class WebhookController extends Controller
|
||||||
|
{
|
||||||
|
public function handleWebhook(Request $request)
|
||||||
|
{
|
||||||
|
Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
|
||||||
|
|
||||||
|
$payload = $request->getContent();
|
||||||
|
$signature = $request->header('Stripe-Signature');
|
||||||
|
$endpointSecret = env('STRIPE_WEBHOOK_SECRET');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$event = Webhook::constructEvent($payload, $signature, $endpointSecret);
|
||||||
|
} catch (SignatureVerificationException $e) {
|
||||||
|
Log::error('Stripe webhook signature verification failed: ' . $e->getMessage());
|
||||||
|
return response()->json(['error' => 'Invalid signature'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle the event
|
||||||
|
switch ($event->type) {
|
||||||
|
case 'checkout.session.completed':
|
||||||
|
$session = $event->data->object;
|
||||||
|
$drawCount = $session->metadata->draw_count;
|
||||||
|
$clientSessionId = $session->metadata->client_session_id;
|
||||||
|
|
||||||
|
$payment = Payment::where('client_session_id', $clientSessionId)->first();
|
||||||
|
|
||||||
|
if ($payment) {
|
||||||
|
$payment->update([
|
||||||
|
'status' => 'succeeded',
|
||||||
|
'draw_count' => $drawCount,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Log if no matching payment record is found
|
||||||
|
Log::warning('No pending payment record found for client_session_id: ' . $clientSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log::info('Received a non-checkout.session.completed webhook event: ' . $event->type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log any other unexpected errors
|
||||||
|
Log::error('Stripe webhook processing error: ' . $e->getMessage(), ['exception' => $e]);
|
||||||
|
return response()->json(['error' => 'Server error'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['status' => 'success'], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Models/Payment.php
Normal file
37
app/Models/Payment.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Payment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'amount',
|
||||||
|
'currency',
|
||||||
|
'stripe_session_id',
|
||||||
|
'client_session_id',
|
||||||
|
'draw_count',
|
||||||
|
'status',
|
||||||
|
'cards'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be cast to native types.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
'cards' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
|||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
@ -21,6 +22,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
AddLinkHeadersForPreloadedAssets::class,
|
AddLinkHeadersForPreloadedAssets::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$middleware->validateCsrfTokens(except: [
|
||||||
|
'stripe/*',
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
|
|||||||
@ -12,8 +12,10 @@
|
|||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"inertiajs/inertia-laravel": "^2.0",
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"laravel/wayfinder": "^0.1.9"
|
"laravel/wayfinder": "^0.1.9",
|
||||||
|
"stripe/stripe-php": "^17.6"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
@ -83,4 +85,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
}
|
}
|
||||||
|
|||||||
125
composer.lock
generated
125
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "644df20f057e10d119c33b325af1c6cb",
|
"content-hash": "7a72790164b9b6dc081f7cbfde7e67d5",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@ -1399,6 +1399,70 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-07-07T14:17:42+00:00"
|
"time": "2025-07-07T14:17:42+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/sanctum",
|
||||||
|
"version": "v4.2.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/sanctum.git",
|
||||||
|
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
|
||||||
|
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/console": "^11.0|^12.0",
|
||||||
|
"illuminate/contracts": "^11.0|^12.0",
|
||||||
|
"illuminate/database": "^11.0|^12.0",
|
||||||
|
"illuminate/support": "^11.0|^12.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"symfony/console": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"orchestra/testbench": "^9.0|^10.0",
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"phpunit/phpunit": "^11.3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Laravel\\Sanctum\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Taylor Otwell",
|
||||||
|
"email": "taylor@laravel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"laravel",
|
||||||
|
"sanctum"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel/sanctum/issues",
|
||||||
|
"source": "https://github.com/laravel/sanctum"
|
||||||
|
},
|
||||||
|
"time": "2025-07-09T19:45:24+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v2.0.4",
|
"version": "v2.0.4",
|
||||||
@ -3406,6 +3470,65 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-06-25T14:20:11+00:00"
|
"time": "2025-06-25T14:20:11+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "stripe/stripe-php",
|
||||||
|
"version": "v17.6.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/stripe/stripe-php.git",
|
||||||
|
"reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/a6219df5df1324a0d3f1da25fb5e4b8a3307ea16",
|
||||||
|
"reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": ">=5.6.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "3.72.0",
|
||||||
|
"phpstan/phpstan": "^1.2",
|
||||||
|
"phpunit/phpunit": "^5.7 || ^9.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Stripe\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Stripe and contributors",
|
||||||
|
"homepage": "https://github.com/stripe/stripe-php/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Stripe PHP Library",
|
||||||
|
"homepage": "https://stripe.com/",
|
||||||
|
"keywords": [
|
||||||
|
"api",
|
||||||
|
"payment processing",
|
||||||
|
"stripe"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/stripe/stripe-php/issues",
|
||||||
|
"source": "https://github.com/stripe/stripe-php/tree/v17.6.0"
|
||||||
|
},
|
||||||
|
"time": "2025-08-27T19:32:42+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v7.3.0",
|
"version": "v7.3.0",
|
||||||
|
|||||||
84
config/sanctum.php
Normal file
84
config/sanctum.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
|
'%s%s',
|
||||||
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||||
|
Sanctum::currentApplicationUrlWithPort(),
|
||||||
|
// Sanctum::currentRequestHost(),
|
||||||
|
))),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This array contains the authentication guards that will be checked when
|
||||||
|
| Sanctum is trying to authenticate a request. If none of these guards
|
||||||
|
| are able to authenticate the request, Sanctum will use the bearer
|
||||||
|
| token that's present on an incoming request for authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Expiration Minutes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the number of minutes until an issued token will be
|
||||||
|
| considered expired. This will override any values set in the token's
|
||||||
|
| "expires_at" attribute, but first-party sessions are not affected.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Token Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||||
|
| security scanning initiatives maintained by open source platforms
|
||||||
|
| that notify developers if they commit tokens into repositories.
|
||||||
|
|
|
||||||
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When authenticating your first-party SPA with Sanctum you may need to
|
||||||
|
| customize some of the middleware Sanctum uses while processing the
|
||||||
|
| request. You may change the middleware listed below as required.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||||
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->integer('paid_draws')->default(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$table->dropColumn('paid_draws');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('payments', function (Blueprint $table) {
|
||||||
|
$table->integer('draw_count')->nullable()->after('status');
|
||||||
|
$table->string('client_session_id', 255)->nullable()->after('stripe_session_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('payments', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['draw_count', 'client_session_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->text('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('payments', function (Blueprint $table) {
|
||||||
|
// Modify the status enum to include 'processed'
|
||||||
|
$table->enum('status', ['pending', 'succeeded', 'failed', 'refunded', 'processed'])->default('pending')->change();
|
||||||
|
|
||||||
|
// Add the cards JSON column
|
||||||
|
$table->json('cards')->nullable()->after('status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('payments', function (Blueprint $table) {
|
||||||
|
// Revert the status enum to its original values
|
||||||
|
$table->enum('status', ['pending', 'succeeded', 'failed', 'refunded'])->default('pending')->change();
|
||||||
|
|
||||||
|
// Remove the cards column
|
||||||
|
$table->dropColumn('cards');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
104
package-lock.json
generated
104
package-lock.json
generated
@ -6,6 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.1.0",
|
"@inertiajs/vue3": "^2.1.0",
|
||||||
|
"@stripe/stripe-js": "^7.9.0",
|
||||||
"@vue-stripe/vue-stripe": "^4.5.0",
|
"@vue-stripe/vue-stripe": "^4.5.0",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
@ -14,11 +15,15 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"reka-ui": "^2.2.0",
|
"reka-ui": "^2.2.0",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
"vue": "^3.5.13"
|
"uuid": "^12.0.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"vue-stripe-js": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
@ -1227,9 +1232,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@stripe/stripe-js": {
|
"node_modules/@stripe/stripe-js": {
|
||||||
"version": "1.54.2",
|
"version": "7.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz",
|
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
|
||||||
"integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg=="
|
"integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.16"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.17",
|
"version": "0.5.17",
|
||||||
@ -1871,6 +1879,11 @@
|
|||||||
"vue-coerce-props": "^1.0.0"
|
"vue-coerce-props": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue-stripe/vue-stripe/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/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.18",
|
"version": "3.5.18",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
|
||||||
@ -2512,6 +2525,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-pick-omit": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw=="
|
||||||
|
},
|
||||||
"node_modules/defu": {
|
"node_modules/defu": {
|
||||||
"version": "6.1.4",
|
"version": "6.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||||
@ -2527,6 +2545,11 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/destr": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
@ -4155,6 +4178,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia-plugin-persistedstate": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-pick-omit": "^1.2.1",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"destr": "^2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nuxt/kit": ">=3.0.0",
|
||||||
|
"@pinia/nuxt": ">=0.10.0",
|
||||||
|
"pinia": ">=3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@nuxt/kit": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@pinia/nuxt": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"pinia": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@ -4956,6 +5005,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "12.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-12.0.0.tgz",
|
||||||
|
"integrity": "sha512-USe1zesMYh4fjCA8ZH5+X5WIVD0J4V1Jksm1bFTVBX2F/cwSXt0RO5w/3UXbdLKmZX65MiWV+hwhSS8p6oBTGA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
|
||||||
@ -5139,6 +5200,41 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-router": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.6.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
|
||||||
|
},
|
||||||
|
"node_modules/vue-stripe-js": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-stripe-js/-/vue-stripe-js-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-tYDUw0zzXfo7kTTyTAFYCDZP7BPq1TM5mAgCRcjcLg7IgUiJlSBcnReDoCCZ6RDhSjFC4Yl5hwKO5Zs38fV63A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@stripe/stripe-js": "^5.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-stripe-js/node_modules/@stripe/stripe-js": {
|
||||||
|
"version": "5.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.10.0.tgz",
|
||||||
|
"integrity": "sha512-PTigkxMdMUP6B5ISS7jMqJAKhgrhZwjprDqR1eATtFfh0OpKVNp110xiH+goeVdrJ29/4LeZJR4FaHHWstsu0A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-tsc": {
|
"node_modules/vue-tsc": {
|
||||||
"version": "2.2.12",
|
"version": "2.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.1.0",
|
"@inertiajs/vue3": "^2.1.0",
|
||||||
|
"@stripe/stripe-js": "^7.9.0",
|
||||||
"@vue-stripe/vue-stripe": "^4.5.0",
|
"@vue-stripe/vue-stripe": "^4.5.0",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
@ -38,11 +39,15 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"reka-ui": "^2.2.0",
|
"reka-ui": "^2.2.0",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
"vue": "^3.5.13"
|
"uuid": "^12.0.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"vue-stripe-js": "^2.0.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import '../css/app.css';
|
|||||||
import { createInertiaApp } from '@inertiajs/vue3';
|
import { createInertiaApp } from '@inertiajs/vue3';
|
||||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||||
import type { DefineComponent } from 'vue';
|
import type { DefineComponent } from 'vue';
|
||||||
import { createApp, h } from 'vue';
|
import { createApp, h } from 'vue';
|
||||||
import { initializeTheme } from './composables/useAppearance';
|
import { initializeTheme } from './composables/useAppearance';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
createInertiaApp({
|
createInertiaApp({
|
||||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { router } from '@inertiajs/vue3';
|
||||||
|
|
||||||
|
const goToShuffle = () => {
|
||||||
|
router.visit('/tirage');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative flex items-center justify-center py-20 text-center sm:py-32">
|
<div class="relative flex items-center justify-center py-20 text-center sm:py-32">
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
@ -22,6 +30,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="mt-4 flex h-12 max-w-[480px] min-w-[120px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-[var(--subtle-gold)] px-8 text-base font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:shadow-[var(--subtle-gold)]/30 hover:shadow-lg"
|
class="mt-4 flex h-12 max-w-[480px] min-w-[120px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-[var(--subtle-gold)] px-8 text-base font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:shadow-[var(--subtle-gold)]/30 hover:shadow-lg"
|
||||||
|
@click="goToShuffle"
|
||||||
>
|
>
|
||||||
<span class="truncate">Découvrir Votre Oracle</span>
|
<span class="truncate">Découvrir Votre Oracle</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,100 +1,58 @@
|
|||||||
<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 { ref, watch } from 'vue';
|
import { router } from '@inertiajs/vue3';
|
||||||
|
import { ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
const emit = defineEmits(['drawCard']);
|
const props = defineProps<{
|
||||||
|
drawCount?: number; // Optional prop for the shuffle animation
|
||||||
|
drawnCards?: Card[]; // Optional prop to directly display cards
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits(['drawCard']);
|
||||||
|
|
||||||
const isClicked = ref(false);
|
const isClicked = ref(false);
|
||||||
const isDrawing = ref(false);
|
const isDrawing = ref(false);
|
||||||
const drawnCards = ref<Card[]>([]); // Changed to array to handle multiple cards
|
|
||||||
const showResult = ref(false);
|
const showResult = ref(false);
|
||||||
const isFlipped = ref<boolean[]>([]); // Array to track flip state for each card
|
const isFlipped = ref<boolean[]>([]);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (isDrawing.value) return;
|
setTimeout(() => {
|
||||||
|
showResult.value = true;
|
||||||
isClicked.value = true;
|
}, 800);
|
||||||
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
|
// This function is still needed for when the user clicks to draw on the /tirage page
|
||||||
const setDrawnCards = (cardData: Card[]) => {
|
const setDrawnCards = (cardData: Card[]) => {
|
||||||
drawnCards.value = cardData;
|
// This method is called by the parent component after an API call
|
||||||
isDrawing.value = false;
|
if (cardData) {
|
||||||
showResult.value = true;
|
// Here, we assign the cards to a local state to be displayed
|
||||||
|
showResult.value = true;
|
||||||
// Initialize flip states for each card
|
isDrawing.value = false;
|
||||||
isFlipped.value = new Array(cardData.length).fill(false);
|
isFlipped.value = new Array(cardData.length).fill(false);
|
||||||
|
// The confetti and card data will be handled by the component that consumes this one
|
||||||
// Add confetti effect
|
}
|
||||||
createConfetti();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(drawnCards, (newVal) => {
|
|
||||||
console.log('Drawn cards:', newVal);
|
|
||||||
});
|
|
||||||
|
|
||||||
const flipCard = (index: number) => {
|
const flipCard = (index: number) => {
|
||||||
isFlipped.value[index] = !isFlipped.value[index];
|
isFlipped.value[index] = !isFlipped.value[index];
|
||||||
};
|
};
|
||||||
|
|
||||||
const createConfetti = () => {
|
// Use watchEffect to react to the `drawnCards` prop changing
|
||||||
const confettiContainer = document.createElement('div');
|
watchEffect(() => {
|
||||||
confettiContainer.style.position = 'fixed';
|
if (props.drawnCards && props.drawnCards.length > 0) {
|
||||||
confettiContainer.style.top = '0';
|
showResult.value = false;
|
||||||
confettiContainer.style.left = '0';
|
isFlipped.value = new Array(props.drawnCards.length).fill(false);
|
||||||
confettiContainer.style.width = '100%';
|
// The confetti and other logic is also triggered here.
|
||||||
confettiContainer.style.height = '100%';
|
} else {
|
||||||
confettiContainer.style.pointerEvents = 'none';
|
showResult.value = false;
|
||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const goToSelection = () => {
|
||||||
|
router.visit('/tirage');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose the setDrawnCards function to parent component
|
|
||||||
defineExpose({ setDrawnCards });
|
defineExpose({ setDrawnCards });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -102,98 +60,16 @@ defineExpose({ setDrawnCards });
|
|||||||
<CardShuffleTemplate>
|
<CardShuffleTemplate>
|
||||||
<template #card-shuffle-slot>
|
<template #card-shuffle-slot>
|
||||||
<div class="card-container">
|
<div class="card-container">
|
||||||
<div
|
<!-- Always show the card stack -->
|
||||||
class="card-stack relative mt-4 mb-4 flex h-[500px] w-[300px] items-center justify-center"
|
<transition class="card-stack-fade">
|
||||||
:class="{ clicked: isClicked, drawing: isDrawing }"
|
<div
|
||||||
@click="handleClick"
|
class="card-stack relative mt-4 mb-4 flex h-[500px] w-[300px] items-center justify-center"
|
||||||
>
|
:class="{ clicked: isClicked, drawing: isDrawing }"
|
||||||
<div class="card" style="transform: rotate(-3deg) translateZ(0); z-index: 3">
|
@click="handleClick"
|
||||||
<div class="card-back">
|
v-show="!showResult"
|
||||||
<div class="card-back-design">
|
>
|
||||||
<svg
|
<div v-for="i in 8" :key="i" class="card" :style="{ transform: `rotate(${-3 + i}deg) translateZ(-${10 * i}px);` }">
|
||||||
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
<div class="card-back">
|
||||||
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">
|
<div class="card-back-design">
|
||||||
<svg
|
<svg
|
||||||
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
||||||
@ -218,26 +94,64 @@ defineExpose({ setDrawnCards });
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-face card-known-back">
|
</div>
|
||||||
<img :src="card.image_url!" :alt="card.name" class="card-image" />
|
</transition>
|
||||||
<div class="card-description-overlay">
|
<!-- Show result only after clicking and when drawnCards is available -->
|
||||||
<h3>{{ card.name }}</h3>
|
<transition class="card-result-slide">
|
||||||
<p class="description">{{ card.description }}</p>
|
<div v-if="showResult && drawnCards" class="cards-result-container">
|
||||||
<p v-if="card.orientation" class="orientation">
|
<div v-for="(card, index) in drawnCards" :key="index" class="card-result-wrapper">
|
||||||
{{ card.orientation === 'reversed' ? 'Inversée' : 'Droite' }}
|
<div class="result-card" :class="{ flipped: isFlipped[index] }" @click="flipCard(index)">
|
||||||
</p>
|
<div class="card-face card-unknown-front">
|
||||||
<div v-if="card.symbolism" class="symbolism">
|
<div class="card-back-design">
|
||||||
<p><strong>Numéro:</strong> {{ card.symbolism.numéro }}</p>
|
<svg
|
||||||
<p><strong>Planète:</strong> {{ card.symbolism.planète }}</p>
|
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
||||||
<p><strong>Élément:</strong> {{ card.symbolism.élément }}</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
<p class="click-hint">Cliquez pour retourner</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
@click="goToSelection"
|
||||||
|
class="mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-[var(--midnight-blue)] px-8 text-base font-bold tracking-wide text-[var(--pure-white)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:shadow-[var(--spiritual-earth)]/30 hover:shadow-lg disabled:cursor-not-allowed disabled:bg-gray-400 disabled:hover:shadow-none"
|
||||||
|
>
|
||||||
|
<span class="truncate">Retourner à la sélection des cartes</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</CardShuffleTemplate>
|
</CardShuffleTemplate>
|
||||||
@ -560,4 +474,21 @@ defineExpose({ setDrawnCards });
|
|||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-stack-fade-leave-active {
|
||||||
|
transition: all 0.8s ease;
|
||||||
|
}
|
||||||
|
.card-stack-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-result-slide-enter-active {
|
||||||
|
transition: all 0.8s ease;
|
||||||
|
transition-delay: 0.3s;
|
||||||
|
}
|
||||||
|
.cards-result-slide-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<!-- stripe-checkout -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { StripeCheckout } from '@vue-stripe/vue-stripe';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StripeCheckout,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
103
resources/js/components/ui/CardStackAnimation.vue
Normal file
103
resources/js/components/ui/CardStackAnimation.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean;
|
||||||
|
delay: number; // Delay in milliseconds before the animation starts
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isVisible = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
setTimeout(() => {
|
||||||
|
isVisible.value = true;
|
||||||
|
}, props.delay);
|
||||||
|
} else {
|
||||||
|
isVisible.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isVisible" class="companion-container">
|
||||||
|
<div class="animated-element">
|
||||||
|
<svg
|
||||||
|
class="w-32 h-32 text-[var(--subtle-gold)] animate-pulse"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.5"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="companion-text">Votre lecture est prête...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.companion-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
z-index: 20; /* Ensure it's on top of other elements */
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeInScale 0.8s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated-element {
|
||||||
|
animation: float 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companion-text {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--subtle-gold);
|
||||||
|
animation: pulseText 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulseText {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,7 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Link } from '@inertiajs/vue3';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const isMobileMenuOpen = ref(false);
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
isMobileMenuOpen.value = !isMobileMenuOpen.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="group/design-root relative flex size-full min-h-screen flex-col overflow-x-hidden">
|
<div class="group/design-root relative flex size-full min-h-screen flex-col overflow-x-hidden">
|
||||||
<div class="layout-container flex h-full grow flex-col">
|
<div class="layout-container flex h-full grow flex-col">
|
||||||
<header class="bg-light-ivory/80 sticky top-0 z-50 flex items-center justify-between px-10 py-4 whitespace-nowrap backdrop-blur-sm">
|
<header
|
||||||
|
class="bg-light-ivory/80 sticky top-0 z-50 flex items-center justify-between px-4 py-4 whitespace-nowrap backdrop-blur-sm md:px-10 lg:px-20"
|
||||||
|
>
|
||||||
<div class="text-midnight-blue flex items-center gap-3">
|
<div class="text-midnight-blue flex items-center gap-3">
|
||||||
<svg class="text-subtle-gold h-6 w-6" fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
<svg class="text-subtle-gold h-6 w-6" fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_6_535)">
|
<g clip-path="url(#clip0_6_535)">
|
||||||
@ -18,23 +31,71 @@
|
|||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<h2 class="text-2xl font-bold tracking-wide">Kris Saint Ange</h2>
|
<h2 class="text-xl font-bold tracking-wide md:text-2xl">Kris Saint Ange</h2>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex flex-1 items-center justify-center gap-10">
|
|
||||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Accueil</a>
|
<nav class="hidden flex-1 items-center justify-center gap-10 md:flex">
|
||||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">L'Oracle</a>
|
<Link class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="/">Accueil</Link>
|
||||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Lectures</a>
|
<Link class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#"
|
||||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Témoignages</a>
|
>L'Oracle</Link
|
||||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Contact</a>
|
>
|
||||||
|
<Link class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#"
|
||||||
|
>Lectures</Link
|
||||||
|
>
|
||||||
|
<Link class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#"
|
||||||
|
>Témoignages</Link
|
||||||
|
>
|
||||||
|
<Link class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Contact</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
|
<button @click="toggleMobileMenu" class="md:hidden">
|
||||||
|
<svg class="text-midnight-blue h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="bg-midnight-blue text-pure-white hover:bg-spiritual-earth flex h-10 max-w-[480px] min-w-[100px] cursor-pointer items-center justify-center overflow-hidden rounded-full px-6 text-sm font-bold tracking-wide transition-all duration-300"
|
v-if="$page.url === '/'"
|
||||||
|
class="bg-midnight-blue text-pure-white hover:bg-spiritual-earth flex h-10 min-w-[100px] cursor-pointer items-center justify-center overflow-hidden rounded-full px-4 text-sm font-bold tracking-wide transition-all duration-300 md:px-6"
|
||||||
>
|
>
|
||||||
<span class="truncate">Commencer</span>
|
<span class="truncate">Commencer</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<nav v-if="isMobileMenuOpen" class="bg-light-ivory/90 flex flex-col items-center gap-4 py-4 md:hidden">
|
||||||
|
<Link
|
||||||
|
class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300"
|
||||||
|
href="#"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
>Accueil</Link
|
||||||
|
>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300"
|
||||||
|
href="#"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
>L'Oracle</Link
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300"
|
||||||
|
href="#"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
>Lectures</Link
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300"
|
||||||
|
href="#"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
>Témoignages</Link
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300"
|
||||||
|
href="#"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
>Contact</Link
|
||||||
|
>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<main class="flex flex-col items-center">
|
<main class="flex flex-col items-center">
|
||||||
<slot />
|
<slot />
|
||||||
@ -43,37 +104,37 @@
|
|||||||
<footer class="bg-pure-white text-midnight-blue/80 py-12">
|
<footer class="bg-pure-white text-midnight-blue/80 py-12">
|
||||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex flex-col items-center gap-8">
|
<div class="flex flex-col items-center gap-8">
|
||||||
<div class="flex flex-wrap justify-center gap-x-8 gap-y-4">
|
<div class="flex flex-wrap justify-center gap-x-4 gap-y-2 md:gap-x-8 md:gap-y-4">
|
||||||
<a class="hover:text-subtle-gold transition-colors" href="#">Accueil</a>
|
<Link class="hover:text-subtle-gold transition-colors" href="#">Accueil</Link>
|
||||||
<a class="hover:text-subtle-gold transition-colors" href="#">L'Oracle</a>
|
<Link class="hover:text-subtle-gold transition-colors" href="#">L'Oracle</Link>
|
||||||
<a class="hover:text-subtle-gold transition-colors" href="#">Lectures</a>
|
<Link class="hover:text-subtle-gold transition-colors" href="#">Lectures</Link>
|
||||||
<a class="hover:text-subtle-gold transition-colors" href="#">Témoignages</a>
|
<Link class="hover:text-subtle-gold transition-colors" href="#">Témoignages</Link>
|
||||||
<a class="hover:text-subtle-gold transition-colors" href="#">Contact</a>
|
<Link class="hover:text-subtle-gold transition-colors" href="#">Contact</Link>
|
||||||
<a class="hover:text-subtle-gold transition-colors" href="#">Politique de Confidentialité</a>
|
<Link class="hover:text-subtle-gold transition-colors" href="#">Politique de Confidentialité</Link>
|
||||||
<a class="hover:text-subtle-gold transition-colors" href="#">Conditions d'Utilisation</a>
|
<Link class="hover:text-subtle-gold transition-colors" href="#">Conditions d'Utilisation</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center gap-6">
|
<div class="flex justify-center gap-4 md:gap-6">
|
||||||
<a class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
<Link class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
||||||
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 256 256">
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 256 256">
|
||||||
<path
|
<path
|
||||||
d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160ZM176,24H80A56.06,56.06,0,0,0,24,80v96a56.06,56.06,0,0,0,56,56h96a56.06,56.06,0,0,0,56-56V80A56.06,56.06,0,0,0,176,24Zm40,152a40,40,0,0,1-40,40H80a40,40,0,0,1-40-40V80A40,40,0,0,1,80,40h96a40,40,0,0,1,40,40ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"
|
d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160ZM176,24H80A56.06,56.06,0,0,0,24,80v96a56.06,56.06,0,0,0,56,56h96a56.06,56.06,0,0,0,56-56V80A56.06,56.06,0,0,0,176,24Zm40,152a40,40,0,0,1-40,40H80a40,40,0,0,1-40-40V80A40,40,0,0,1,80,40h96a40,40,0,0,1,40,40ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</Link>
|
||||||
<a class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
<Link class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
||||||
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 256 256">
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 256 256">
|
||||||
<path
|
<path
|
||||||
d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm8,191.63V152h24a8,8,0,0,0,0-16H136V112a16,16,0,0,1,16-16h16a8,8,0,0,0,0-16H152a32,32,0,0,0-32,32v24H96a8,8,0,0,0,0,16h24v63.63a88,88,0,1,1,16,0Z"
|
d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm8,191.63V152h24a8,8,0,0,0,0-16H136V112a16,16,0,0,1,16-16h16a8,8,0,0,0,0-16H152a32,32,0,0,0-32,32v24H96a8,8,0,0,0,0,16h24v63.63a88,88,0,1,1,16,0Z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</Link>
|
||||||
<a class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
<Link class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
||||||
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 256 256">
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 256 256">
|
||||||
<path
|
<path
|
||||||
d="M247.39,68.94A8,8,0,0,0,240,64H209.57A48.66,48.66,0,0,0,168.1,40a46.91,46.91,0,0,0-33.75,13.7A47.9,47.9,0,0,0,120,88v6.09C79.74,83.47,46.81,50.72,46.46,50.37a8,8,0,0,0-13.65,4.92c-4.31,47.79,9.57,79.77,22,98.18a110.93,110.93,0,0,0,21.88,24.2c-15.23,17.53-39.21,26.74-39.47,26.84a8,8,0,0,0-3.85,11.93c.75,1.12,3.75,5.05,11.08,8.72C53.51,229.7,65.48,232,80,232c70.67,0,129.72-54.42,135.75-124.44l29.91-29.9A8,8,0,0,0,247.39,68.94Zm-45,29.41a8,8,0,0,0-2.32,5.14C196,166.58,143.28,216,80,216c-10.56,0-18-1.4-23.22-3.08,11.51-6.25,27.56-17,37.88-32.48A8,8,0,0,0,92,169.08c-.47-.27-43.91-26.34-44-96,16,13,45.25,33.17,78.67,38.79A8,8,0,0,0,136,104V88a32,32,0,0,1,9.6-22.92A30.94,30.94,0,0,1,167.9,56c12.66.16,24.49,7.88,29.44,19.21A8,8,0,0,0,204.67,80h16Z"
|
d="M247.39,68.94A8,8,0,0,0,240,64H209.57A48.66,48.66,0,0,0,168.1,40a46.91,46.91,0,0,0-33.75,13.7A47.9,47.9,0,0,0,120,88v6.09C79.74,83.47,46.81,50.72,46.46,50.37a8,8,0,0,0-13.65,4.92c-4.31,47.79,9.57,79.77,22,98.18a110.93,110.93,0,0,0,21.88,24.2c-15.23,17.53-39.21,26.74-39.47,26.84a8,8,0,0,0-3.85,11.93c.75,1.12,3.75,5.05,11.08,8.72C53.51,229.7,65.48,232,80,232c70.67,0,129.72-54.42,135.75-124.44l29.91-29.9A8,8,0,0,0,247.39,68.94Zm-45,29.41a8,8,0,0,0-2.32,5.14C196,166.58,143.28,216,80,216c-10.56,0-18-1.4-23.22-3.08,11.51-6.25,27.56-17,37.88-32.48A8,8,0,0,0,92,169.08c-.47-.27-43.91-26.34-44-96,16,13,45.25,33.17,78.67,38.79A8,8,0,0,0,136,104V88a32,32,0,0,1,9.6-22.92A30.94,30.94,0,0,1,167.9,56c12.66.16,24.49,7.88,29.44,19.21A8,8,0,0,0,204.67,80h16Z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm">© 2024 Kris Saint Ange. Tous droits réservés.</p>
|
<p class="text-sm">© 2024 Kris Saint Ange. Tous droits réservés.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -82,6 +143,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Utility classes for colors */
|
/* Utility classes for colors */
|
||||||
.text-midnight-blue {
|
.text-midnight-blue {
|
||||||
|
|||||||
0
resources/js/pages/Lecture.vue
Normal file
0
resources/js/pages/Lecture.vue
Normal file
121
resources/js/pages/cards/cardSelection.vue
Normal file
121
resources/js/pages/cards/cardSelection.vue
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTarotStore } from '@/stores/tarot';
|
||||||
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
const tarotStore = useTarotStore();
|
||||||
|
const isSelectionScreen = ref(true);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const drawCount = ref(0);
|
||||||
|
|
||||||
|
// Emits the draw selection back to parent
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'selectDraw', count: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const handleSelection = async (count: number) => {
|
||||||
|
drawCount.value = count;
|
||||||
|
|
||||||
|
if (count == 1 && tarotStore.freeDrawsRemaining <= 0) {
|
||||||
|
alert('You have used your free draw. Please choose a paid option to unlock more.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (count > 1 && tarotStore.paidDrawsRemaining < count) {
|
||||||
|
await redirectToStripeCheckout(count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('selectDraw', count);
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToStripeCheckout = async (count: number) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// 1. Send request to your Laravel backend to create a Stripe Checkout Session
|
||||||
|
const res = await axios.post('/create-checkout-session', { count });
|
||||||
|
const { sessionId } = res.data;
|
||||||
|
|
||||||
|
// 2. Load Stripe.js with your publishable key
|
||||||
|
const stripe = await loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!);
|
||||||
|
|
||||||
|
// 3. Redirect to Stripe Checkout using the session ID from the backend
|
||||||
|
if (stripe) {
|
||||||
|
const { error } = await stripe.redirectToCheckout({ sessionId });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Stripe redirect error:', error.message);
|
||||||
|
alert('Payment failed. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initiating Stripe checkout:', error);
|
||||||
|
alert('Payment processing failed. Please try again.');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computed to disable the free draw button if used
|
||||||
|
const isFreeDrawUsed = computed(() => tarotStore.freeDrawsRemaining <= 0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section 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>
|
||||||
|
|
||||||
|
<p v-if="isFreeDrawUsed" class="mb-8 text-center text-lg font-semibold text-[var(--spiritual-earth)]">
|
||||||
|
Vous avez utilisé votre tirage gratuit ! Choisissez une option payante pour continuer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<!-- Free draw -->
|
||||||
|
<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
|
||||||
|
:disabled="isFreeDrawUsed"
|
||||||
|
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)] disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
@click="handleSelection(1)"
|
||||||
|
>
|
||||||
|
Commencer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paid options -->
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
@ -3,105 +3,115 @@ import ShuffleCardPresentation from '@/components/organism/ShuffleCard/ShuffleCa
|
|||||||
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
||||||
import { useTarotStore } from '@/stores/tarot';
|
import { useTarotStore } from '@/stores/tarot';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ref } from 'vue';
|
import { onMounted, ref, watchEffect } from 'vue'; // Import watchEffect
|
||||||
|
import cardSelection from './cardSelection.vue';
|
||||||
|
|
||||||
const cardComponent = ref();
|
const cardComponent = ref();
|
||||||
const tarotStore = useTarotStore();
|
const tarotStore = useTarotStore();
|
||||||
const isSelectionScreen = ref(true);
|
const isSelectionScreen = ref(true);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
// This variable will hold the number of cards to draw
|
|
||||||
const drawCount = ref(0);
|
const drawCount = ref(0);
|
||||||
|
const cards = ref([]);
|
||||||
|
|
||||||
// This function will be called from the "offer" buttons
|
const params = new URLSearchParams(window.location.search);
|
||||||
const handleSelection = (count: number) => {
|
const clientSessionId = params.get('client_session_id');
|
||||||
|
|
||||||
|
// Use watchEffect to handle state changes from Pinia
|
||||||
|
watchEffect(() => {
|
||||||
|
// Check if there are paid draws available and no free draws, and transition automatically
|
||||||
|
if (tarotStore.paidDrawsRemaining > 0 && tarotStore.freeDrawsRemaining === 0) {
|
||||||
|
isSelectionScreen.value = false;
|
||||||
|
drawCount.value = tarotStore.paidDrawsRemaining; // Set the drawCount to the available paid draws
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (clientSessionId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/get-cards?client_session_id=${clientSessionId}`);
|
||||||
|
if (response.data.success) {
|
||||||
|
cards.value = response.data.cards;
|
||||||
|
if (cardComponent.value) {
|
||||||
|
cardComponent.value.setDrawnCards(cards.value);
|
||||||
|
}
|
||||||
|
console.log(cards.value);
|
||||||
|
} else {
|
||||||
|
error.value = response.data.message || 'An error occurred while validating payment.';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
error.value = 'Failed to get cards from the server. Please contact support.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelection = async (count: number) => {
|
||||||
drawCount.value = count;
|
drawCount.value = count;
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
// Check if the draw is free or requires payment
|
// Check if it's a free draw and there's one available
|
||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
// Free draw
|
|
||||||
if (tarotStore.freeDrawsRemaining > 0) {
|
if (tarotStore.freeDrawsRemaining > 0) {
|
||||||
tarotStore.useFreeDraw();
|
try {
|
||||||
isSelectionScreen.value = false; // Switch to the shuffle screen
|
// Make the API call for the free draw
|
||||||
|
const response = await axios.get('/api/get-cards', {
|
||||||
|
params: { count: 1 },
|
||||||
|
});
|
||||||
|
if (response.data.success) {
|
||||||
|
cards.value = response.data.cards;
|
||||||
|
// Only use the free draw after a successful API call
|
||||||
|
tarotStore.useFreeDraw();
|
||||||
|
isSelectionScreen.value = false;
|
||||||
|
} else {
|
||||||
|
error.value = response.data.message || 'An error occurred while getting cards.';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
error.value = 'Failed to get cards from the server. Please contact support.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('You have used your free draw. Please choose a paid option to unlock more.');
|
alert("You don't have any free draws left. Please choose a paid option.");
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Paid draw
|
// This is the paid draw logic, which remains mostly the same
|
||||||
// This is where you'd trigger your Stripe payment component
|
if (tarotStore.paidDrawsRemaining >= count) {
|
||||||
alert(`Initiating payment process for a ${count}-card draw.`);
|
// Here, you would also make an API call to get the paid cards
|
||||||
// For now, let's simulate a successful payment and then proceed
|
try {
|
||||||
tarotStore.unlockNewDraws().then(() => {
|
const response = await axios.get('/api/get-cards', {
|
||||||
isSelectionScreen.value = false;
|
params: { count: count },
|
||||||
});
|
});
|
||||||
}
|
if (response.data.success) {
|
||||||
};
|
cards.value = response.data.cards;
|
||||||
|
tarotStore.usePaidDraw(count);
|
||||||
const getCard = async () => {
|
isSelectionScreen.value = false;
|
||||||
loading.value = true;
|
} else {
|
||||||
try {
|
error.value = response.data.message || 'An error occurred while getting cards.';
|
||||||
const res = await axios.post('/draw-card', { count: drawCount.value });
|
}
|
||||||
if (res.data) {
|
} catch (err) {
|
||||||
cardComponent.value.setDrawnCards(res.data.card);
|
console.error(err);
|
||||||
|
error.value = 'Failed to get cards from the server. Please contact support.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("You don't have enough paid draws. Please proceed with payment.");
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LandingLayout>
|
<LandingLayout>
|
||||||
<section v-if="isSelectionScreen" class="py-20 sm:py-24">
|
<card-selection @selectDraw="handleSelection" v-if="isSelectionScreen && !clientSessionId" />
|
||||||
<h2 class="mb-16 text-center text-4xl font-bold text-[var(--midnight-blue)] md:text-5xl">Explorez Nos Lectures</h2>
|
<ShuffleCardPresentation v-else ref="cardComponent" :drawn-cards="cards" />
|
||||||
<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>
|
</LandingLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
46
resources/js/pages/payments/Error.vue
Normal file
46
resources/js/pages/payments/Error.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<LandingLayout>
|
||||||
|
<main class="flex flex-grow items-center justify-center">
|
||||||
|
<div class="w-full max-w-2xl px-4 py-20 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex flex-col items-center rounded-2xl border border-[var(--linen)] bg-[var(--pure-white)] p-12 text-center shadow-xl">
|
||||||
|
<div class="mb-8 flex h-20 w-20 items-center justify-center rounded-full bg-[var(--soft-coral)] shadow-lg">
|
||||||
|
<svg
|
||||||
|
class="h-10 w-10 text-[var(--midnight-blue)]"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl font-bold text-[var(--midnight-blue)] md:text-5xl">Paiement Échoué</h1>
|
||||||
|
<p class="mx-auto mt-4 max-w-md text-lg text-[var(--midnight-blue)]/80">
|
||||||
|
Une erreur est survenue lors du traitement de votre paiement, ou vous avez annulé la transaction.
|
||||||
|
</p>
|
||||||
|
<div class="mt-10 w-full border-t border-[var(--linen)]"></div>
|
||||||
|
<p class="mt-10 text-lg font-medium text-[var(--midnight-blue)]/90">
|
||||||
|
Veuillez réessayer ou contactez le support si le problème persiste.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
@click="tryAgain"
|
||||||
|
class="mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-[var(--midnight-blue)] px-8 text-base font-bold tracking-wide text-[var(--pure-white)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:shadow-[var(--spiritual-earth)]/30 hover:shadow-lg"
|
||||||
|
>
|
||||||
|
<span class="truncate">Réessayer</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</LandingLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
||||||
|
import { router } from '@inertiajs/vue3';
|
||||||
|
|
||||||
|
const tryAgain = () => {
|
||||||
|
// Redirect back to the card selection page
|
||||||
|
router.visit('/tirage');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
82
resources/js/pages/payments/Success.vue
Normal file
82
resources/js/pages/payments/Success.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<LandingLayout>
|
||||||
|
<main class="flex flex-grow items-center justify-center">
|
||||||
|
<div class="w-full max-w-2xl px-4 py-20 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex flex-col items-center rounded-2xl border border-[var(--linen)] bg-[var(--pure-white)] p-12 text-center shadow-xl">
|
||||||
|
<div class="mb-8 flex h-20 w-20 items-center justify-center rounded-full bg-[var(--subtle-gold)] shadow-lg">
|
||||||
|
<svg
|
||||||
|
class="h-10 w-10 text-[var(--midnight-blue)]"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl font-bold text-[var(--midnight-blue)] md:text-5xl">Paiement Réussi</h1>
|
||||||
|
<p class="mx-auto mt-4 max-w-md text-lg text-[var(--midnight-blue)]/80">
|
||||||
|
Votre transaction a été complétée avec succès. L'univers vous attend.
|
||||||
|
</p>
|
||||||
|
<div class="mt-10 w-full border-t border-[var(--linen)]"></div>
|
||||||
|
<p v-if="loading" class="mt-10 text-lg font-medium text-[var(--midnight-blue)]/90">Vérification de votre paiement...</p>
|
||||||
|
<p v-else class="mt-10 text-lg font-medium text-[var(--midnight-blue)]/90">
|
||||||
|
Vous pouvez maintenant procéder à votre tirage de cartes.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
@click="proceedToDraw"
|
||||||
|
:disabled="loading"
|
||||||
|
class="mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-[var(--midnight-blue)] px-8 text-base font-bold tracking-wide text-[var(--pure-white)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:shadow-[var(--spiritual-earth)]/30 hover:shadow-lg disabled:cursor-not-allowed disabled:bg-gray-400 disabled:hover:shadow-none"
|
||||||
|
>
|
||||||
|
<span class="truncate">Tirer les cartes</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</LandingLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
||||||
|
import { useTarotStore } from '@/stores/tarot';
|
||||||
|
import { router } from '@inertiajs/vue3';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const tarotStore = useTarotStore();
|
||||||
|
const loading = ref(true);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const clientSessionId = params.get('client_session_id');
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log(clientSessionId);
|
||||||
|
if (clientSessionId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/validate-payment?client_session_id=${clientSessionId}`);
|
||||||
|
if (response.data.success) {
|
||||||
|
// Mettez à jour le store avec les tirages payés
|
||||||
|
tarotStore.addPaidDraws(response.data.drawCount);
|
||||||
|
} else {
|
||||||
|
// Gérer le cas où le paiement n'est pas validé
|
||||||
|
alert('Paiement non validé. Veuillez réessayer.');
|
||||||
|
router.visit('/cancel');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la validation du paiement:', error);
|
||||||
|
alert('Erreur de validation. Veuillez réessayer.');
|
||||||
|
router.visit('/cancel');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Rediriger si l'ID de session est manquant
|
||||||
|
// router.visit('/cancel');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const proceedToDraw = () => {
|
||||||
|
// Redirige vers la page principale
|
||||||
|
router.visit(`/tirage?client_session_id=${clientSessionId}`);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -4,36 +4,37 @@ import { ref } from 'vue';
|
|||||||
export const useTarotStore = defineStore('tarot', () => {
|
export const useTarotStore = defineStore('tarot', () => {
|
||||||
// State
|
// State
|
||||||
const freeDrawsRemaining = ref(1);
|
const freeDrawsRemaining = ref(1);
|
||||||
|
const paidDrawsRemaining = ref(0);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
function useFreeDraw() {
|
function useFreeDraw() {
|
||||||
if (freeDrawsRemaining.value > 0) {
|
if (freeDrawsRemaining.value > 0) {
|
||||||
freeDrawsRemaining.value--;
|
freeDrawsRemaining.value--;
|
||||||
return true; // Indicates a free draw was used
|
return true;
|
||||||
}
|
}
|
||||||
return false; // No more free draws
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// You would integrate Stripe here in a more advanced application
|
// Modified usePaidDraw to handle the correct number of cards and reset the state
|
||||||
// This is a placeholder for your payment logic.
|
function usePaidDraw(count: number) {
|
||||||
function unlockNewDraws() {
|
if (paidDrawsRemaining.value >= count) {
|
||||||
// You would typically call a backend endpoint here to create a Stripe Checkout Session
|
// Since the draws are 'used', we set the remaining to 0.
|
||||||
// and redirect the user. For this example, we'll simulate a successful payment.
|
// This assumes a user pays for a set number of cards in one go.
|
||||||
console.log('Redirecting to Stripe for payment...');
|
paidDrawsRemaining.value = 0;
|
||||||
return new Promise((resolve) => {
|
return true;
|
||||||
setTimeout(() => {
|
}
|
||||||
console.log('Payment successful! Adding 1 new draw.');
|
return false;
|
||||||
// After successful payment from Stripe, you would update the state.
|
}
|
||||||
// This state update would likely come from a backend webhook.
|
|
||||||
freeDrawsRemaining.value++;
|
function addPaidDraws(count: number) {
|
||||||
resolve(true);
|
paidDrawsRemaining.value += count;
|
||||||
}, 2000); // Simulate a network delay
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
freeDrawsRemaining,
|
freeDrawsRemaining,
|
||||||
|
paidDrawsRemaining,
|
||||||
useFreeDraw,
|
useFreeDraw,
|
||||||
unlockNewDraws,
|
usePaidDraw,
|
||||||
|
addPaidDraws,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
12
routes/api.php
Normal file
12
routes/api.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::get('/user', function (Request $request) {
|
||||||
|
return $request->user();
|
||||||
|
})->middleware('auth:sanctum');
|
||||||
|
|
||||||
|
|
||||||
|
Route::get('/validate-payment', [App\Http\Controllers\StripeController::class, 'validatePayment']);
|
||||||
|
Route::get('/get-cards', [App\Http\Controllers\StripeController::class, 'getCards']);
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return Inertia::render('Landing');
|
return Inertia::render('Landing');
|
||||||
@ -15,5 +17,30 @@ Route::get('dashboard', function () {
|
|||||||
Route::get('/tirage',[App\Http\Controllers\CardController::class, 'index'])->name('cards.shuffle');
|
Route::get('/tirage',[App\Http\Controllers\CardController::class, 'index'])->name('cards.shuffle');
|
||||||
Route::post('/draw-card', [App\Http\Controllers\CardController::class, 'drawCard']);
|
Route::post('/draw-card', [App\Http\Controllers\CardController::class, 'drawCard']);
|
||||||
|
|
||||||
|
Route::post('/create-checkout-session', [App\Http\Controllers\StripeController::class, 'createCheckoutSession']);
|
||||||
|
|
||||||
|
Route::post('/stripe/webhook', [App\Http\Controllers\WebhookController::class, 'handleWebhook']);
|
||||||
|
|
||||||
|
Route::get('/success', function (Request $request) {
|
||||||
|
$clientSessionId = $request->query('client_session_id');
|
||||||
|
|
||||||
|
$payment = Payment::where('client_session_id', $clientSessionId)
|
||||||
|
->where('status', 'succeeded') // Only check for succeeded payments
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($payment) {
|
||||||
|
return Inertia::render('payments/Success', [
|
||||||
|
'paymentSuccess' => true,
|
||||||
|
'drawCount' => $payment->draw_count
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inertia::render('payments/Error', ['message' => 'Payment validation failed.']);
|
||||||
|
})->name('payment.success');
|
||||||
|
|
||||||
|
Route::get('/cancel', function () {
|
||||||
|
return Inertia::render('payments/Error'); // Your error page
|
||||||
|
})->name('payments.cancel');
|
||||||
|
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user