functionnality tirage
This commit is contained in:
parent
9e5b160166
commit
39c17b44cf
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__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
@ -21,6 +22,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
HandleInertiaRequests::class,
|
||||
AddLinkHeadersForPreloadedAssets::class,
|
||||
]);
|
||||
|
||||
$middleware->validateCsrfTokens(except: [
|
||||
'stripe/*',
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
|
||||
@ -12,8 +12,10 @@
|
||||
"php": "^8.2",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/wayfinder": "^0.1.9"
|
||||
"laravel/wayfinder": "^0.1.9",
|
||||
"stripe/stripe-php": "^17.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
@ -83,4 +85,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "644df20f057e10d119c33b325af1c6cb",
|
||||
"content-hash": "7a72790164b9b6dc081f7cbfde7e67d5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -1399,6 +1399,70 @@
|
||||
},
|
||||
"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",
|
||||
"version": "v2.0.4",
|
||||
@ -3406,6 +3470,65 @@
|
||||
},
|
||||
"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",
|
||||
"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": {
|
||||
"@inertiajs/vue3": "^2.1.0",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@vue-stripe/vue-stripe": "^4.5.0",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"axios": "^1.11.0",
|
||||
@ -14,11 +15,15 @@
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"lucide-vue-next": "^0.468.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"reka-ui": "^2.2.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.1",
|
||||
"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": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
@ -1227,9 +1232,12 @@
|
||||
]
|
||||
},
|
||||
"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=="
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
|
||||
"integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
|
||||
"engines": {
|
||||
"node": ">=12.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
@ -1871,6 +1879,11 @@
|
||||
"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": {
|
||||
"version": "3.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz",
|
||||
@ -2512,6 +2525,11 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
@ -2527,6 +2545,11 @@
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"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": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@ -4956,6 +5005,18 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
|
||||
@ -5139,6 +5200,41 @@
|
||||
"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": {
|
||||
"version": "2.2.12",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@inertiajs/vue3": "^2.1.0",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@vue-stripe/vue-stripe": "^4.5.0",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"axios": "^1.11.0",
|
||||
@ -38,11 +39,15 @@
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"lucide-vue-next": "^0.468.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"reka-ui": "^2.2.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.1",
|
||||
"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": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
||||
|
||||
@ -3,12 +3,14 @@ import '../css/app.css';
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
import type { DefineComponent } from 'vue';
|
||||
import { createApp, h } from 'vue';
|
||||
import { initializeTheme } from './composables/useAppearance';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
createInertiaApp({
|
||||
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>
|
||||
<div class="relative flex items-center justify-center py-20 text-center sm:py-32">
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
@ -22,6 +30,7 @@
|
||||
</p>
|
||||
<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"
|
||||
@click="goToShuffle"
|
||||
>
|
||||
<span class="truncate">Découvrir Votre Oracle</span>
|
||||
</button>
|
||||
|
||||
@ -1,100 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import CardShuffleTemplate from '@/components/template/CardShuffleTemplate.vue';
|
||||
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 isDrawing = ref(false);
|
||||
const drawnCards = ref<Card[]>([]); // Changed to array to handle multiple cards
|
||||
const showResult = ref(false);
|
||||
const isFlipped = ref<boolean[]>([]); // Array to track flip state for each card
|
||||
const isFlipped = ref<boolean[]>([]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (isDrawing.value) return;
|
||||
|
||||
isClicked.value = true;
|
||||
isDrawing.value = true;
|
||||
emit('drawCard');
|
||||
setTimeout(() => (isClicked.value = false), 500);
|
||||
setTimeout(() => {
|
||||
showResult.value = true;
|
||||
}, 800);
|
||||
};
|
||||
|
||||
// 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[]) => {
|
||||
drawnCards.value = cardData;
|
||||
isDrawing.value = false;
|
||||
showResult.value = true;
|
||||
|
||||
// Initialize flip states for each card
|
||||
isFlipped.value = new Array(cardData.length).fill(false);
|
||||
|
||||
// Add confetti effect
|
||||
createConfetti();
|
||||
// This method is called by the parent component after an API call
|
||||
if (cardData) {
|
||||
// Here, we assign the cards to a local state to be displayed
|
||||
showResult.value = true;
|
||||
isDrawing.value = false;
|
||||
isFlipped.value = new Array(cardData.length).fill(false);
|
||||
// The confetti and card data will be handled by the component that consumes this one
|
||||
}
|
||||
};
|
||||
|
||||
watch(drawnCards, (newVal) => {
|
||||
console.log('Drawn cards:', newVal);
|
||||
});
|
||||
|
||||
const flipCard = (index: number) => {
|
||||
isFlipped.value[index] = !isFlipped.value[index];
|
||||
};
|
||||
|
||||
const createConfetti = () => {
|
||||
const confettiContainer = document.createElement('div');
|
||||
confettiContainer.style.position = 'fixed';
|
||||
confettiContainer.style.top = '0';
|
||||
confettiContainer.style.left = '0';
|
||||
confettiContainer.style.width = '100%';
|
||||
confettiContainer.style.height = '100%';
|
||||
confettiContainer.style.pointerEvents = 'none';
|
||||
confettiContainer.style.zIndex = '5'; // Lower z-index so cards appear above
|
||||
document.body.appendChild(confettiContainer);
|
||||
|
||||
const colors = ['#D7BA8D', '#A06D52', '#1F2A44', '#FFFFFF'];
|
||||
const confettiCount = 100;
|
||||
|
||||
for (let i = 0; i < confettiCount; i++) {
|
||||
const confetti = document.createElement('div');
|
||||
confetti.style.position = 'absolute';
|
||||
confetti.style.width = '10px';
|
||||
confetti.style.height = '10px';
|
||||
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
confetti.style.borderRadius = Math.random() > 0.5 ? '50%' : '0';
|
||||
confetti.style.top = '50%';
|
||||
confetti.style.left = '50%';
|
||||
confetti.style.opacity = '0';
|
||||
|
||||
confettiContainer.appendChild(confetti);
|
||||
|
||||
const animation = confetti.animate(
|
||||
[
|
||||
{
|
||||
transform: 'translate(0, 0) rotate(0deg)',
|
||||
opacity: 1,
|
||||
},
|
||||
{
|
||||
transform: `translate(${Math.random() * 400 - 200}px, ${Math.random() * 400 - 200}px) rotate(${Math.random() * 360}deg)`,
|
||||
opacity: 0,
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 1000 + Math.random() * 1000,
|
||||
easing: 'cubic-bezier(0.1, 0.8, 0.3, 1)',
|
||||
},
|
||||
);
|
||||
|
||||
animation.onfinish = () => {
|
||||
confetti.remove();
|
||||
if (confettiContainer.children.length === 0) {
|
||||
confettiContainer.remove();
|
||||
}
|
||||
};
|
||||
// Use watchEffect to react to the `drawnCards` prop changing
|
||||
watchEffect(() => {
|
||||
if (props.drawnCards && props.drawnCards.length > 0) {
|
||||
showResult.value = false;
|
||||
isFlipped.value = new Array(props.drawnCards.length).fill(false);
|
||||
// The confetti and other logic is also triggered here.
|
||||
} else {
|
||||
showResult.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const goToSelection = () => {
|
||||
router.visit('/tirage');
|
||||
};
|
||||
|
||||
// Expose the setDrawnCards function to parent component
|
||||
defineExpose({ setDrawnCards });
|
||||
</script>
|
||||
|
||||
@ -102,98 +60,16 @@ defineExpose({ setDrawnCards });
|
||||
<CardShuffleTemplate>
|
||||
<template #card-shuffle-slot>
|
||||
<div class="card-container">
|
||||
<div
|
||||
class="card-stack relative mt-4 mb-4 flex h-[500px] w-[300px] items-center justify-center"
|
||||
:class="{ clicked: isClicked, drawing: isDrawing }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="card" style="transform: rotate(-3deg) translateZ(0); z-index: 3">
|
||||
<div class="card-back">
|
||||
<div class="card-back-design">
|
||||
<svg
|
||||
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
|
||||
<path
|
||||
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
<path
|
||||
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="transform: rotate(1deg) translateZ(-10px); z-index: 2">
|
||||
<div class="card-back">
|
||||
<div class="card-back-design">
|
||||
<svg
|
||||
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
|
||||
<path
|
||||
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
<path
|
||||
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="transform: rotate(4deg) translateZ(-20px); z-index: 1">
|
||||
<div class="card-back">
|
||||
<div class="card-back-design">
|
||||
<svg
|
||||
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
|
||||
<path
|
||||
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
<path
|
||||
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showResult && drawnCards.length" class="cards-result-container">
|
||||
<div v-for="(card, index) in drawnCards" :key="index" class="card-result-wrapper">
|
||||
<div class="result-card" :class="{ flipped: isFlipped[index] }" @click="flipCard(index)">
|
||||
<div class="card-face card-unknown-front">
|
||||
<!-- Always show the card stack -->
|
||||
<transition class="card-stack-fade">
|
||||
<div
|
||||
class="card-stack relative mt-4 mb-4 flex h-[500px] w-[300px] items-center justify-center"
|
||||
:class="{ clicked: isClicked, drawing: isDrawing }"
|
||||
@click="handleClick"
|
||||
v-show="!showResult"
|
||||
>
|
||||
<div v-for="i in 8" :key="i" class="card" :style="{ transform: `rotate(${-3 + i}deg) translateZ(-${10 * i}px);` }">
|
||||
<div class="card-back">
|
||||
<div class="card-back-design">
|
||||
<svg
|
||||
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
||||
@ -218,26 +94,64 @@ defineExpose({ setDrawnCards });
|
||||
</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>
|
||||
</transition>
|
||||
<!-- Show result only after clicking and when drawnCards is available -->
|
||||
<transition class="card-result-slide">
|
||||
<div v-if="showResult && drawnCards" class="cards-result-container">
|
||||
<div v-for="(card, index) in drawnCards" :key="index" class="card-result-wrapper">
|
||||
<div class="result-card" :class="{ flipped: isFlipped[index] }" @click="flipCard(index)">
|
||||
<div class="card-face card-unknown-front">
|
||||
<div class="card-back-design">
|
||||
<svg
|
||||
class="h-16 w-16 text-[var(--subtle-gold)] opacity-80"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 4v16m8-8H4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path>
|
||||
<path
|
||||
d="M14.828 7.172a4 4 0 015.656 5.656l-5.656 5.657a4 4 0 01-5.657-5.657l5.657-5.656z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
<path
|
||||
d="M9.172 7.172a4 4 0 00-5.657 5.656l5.657 5.657a4 4 0 005.656-5.657L9.172 7.172z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="0.5"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-face card-known-back">
|
||||
<img :src="card.image_url!" :alt="card.name" class="card-image" />
|
||||
<div class="card-description-overlay">
|
||||
<h3>{{ card.name }}</h3>
|
||||
<p class="description">{{ card.description }}</p>
|
||||
<p v-if="card.orientation" class="orientation">
|
||||
{{ card.orientation === 'reversed' ? 'Inversée' : 'Droite' }}
|
||||
</p>
|
||||
<div v-if="card.symbolism" class="symbolism">
|
||||
<p><strong>Numéro:</strong> {{ card.symbolism.numéro }}</p>
|
||||
<p><strong>Planète:</strong> {{ card.symbolism.planète }}</p>
|
||||
<p><strong>Élément:</strong> {{ card.symbolism.élément }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="click-hint">Cliquez pour retourner</p>
|
||||
</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>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</CardShuffleTemplate>
|
||||
@ -560,4 +474,21 @@ defineExpose({ setDrawnCards });
|
||||
font-size: 0.7rem;
|
||||
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>
|
||||
|
||||
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>
|
||||
<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">
|
||||
<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">
|
||||
<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)">
|
||||
@ -18,23 +31,71 @@
|
||||
</clipPath>
|
||||
</defs>
|
||||
</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>
|
||||
<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>
|
||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">L'Oracle</a>
|
||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Lectures</a>
|
||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Témoignages</a>
|
||||
<a class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#">Contact</a>
|
||||
|
||||
<nav class="hidden flex-1 items-center justify-center gap-10 md:flex">
|
||||
<Link class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="/">Accueil</Link>
|
||||
<Link class="text-midnight-blue hover:text-subtle-gold text-sm font-medium transition-colors duration-300" href="#"
|
||||
>L'Oracle</Link
|
||||
>
|
||||
<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>
|
||||
|
||||
<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
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
</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">
|
||||
<slot />
|
||||
@ -43,37 +104,37 @@
|
||||
<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="flex flex-col items-center gap-8">
|
||||
<div class="flex flex-wrap justify-center gap-x-8 gap-y-4">
|
||||
<a class="hover:text-subtle-gold transition-colors" href="#">Accueil</a>
|
||||
<a class="hover:text-subtle-gold transition-colors" href="#">L'Oracle</a>
|
||||
<a class="hover:text-subtle-gold transition-colors" href="#">Lectures</a>
|
||||
<a class="hover:text-subtle-gold transition-colors" href="#">Témoignages</a>
|
||||
<a class="hover:text-subtle-gold transition-colors" href="#">Contact</a>
|
||||
<a class="hover:text-subtle-gold transition-colors" href="#">Politique de Confidentialité</a>
|
||||
<a class="hover:text-subtle-gold transition-colors" href="#">Conditions d'Utilisation</a>
|
||||
<div class="flex flex-wrap justify-center gap-x-4 gap-y-2 md:gap-x-8 md:gap-y-4">
|
||||
<Link class="hover:text-subtle-gold transition-colors" href="#">Accueil</Link>
|
||||
<Link class="hover:text-subtle-gold transition-colors" href="#">L'Oracle</Link>
|
||||
<Link class="hover:text-subtle-gold transition-colors" href="#">Lectures</Link>
|
||||
<Link class="hover:text-subtle-gold transition-colors" href="#">Témoignages</Link>
|
||||
<Link class="hover:text-subtle-gold transition-colors" href="#">Contact</Link>
|
||||
<Link class="hover:text-subtle-gold transition-colors" href="#">Politique de Confidentialité</Link>
|
||||
<Link class="hover:text-subtle-gold transition-colors" href="#">Conditions d'Utilisation</Link>
|
||||
</div>
|
||||
<div class="flex justify-center gap-6">
|
||||
<a class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
||||
<div class="flex justify-center gap-4 md:gap-6">
|
||||
<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">
|
||||
<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"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
||||
</Link>
|
||||
<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">
|
||||
<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"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="text-midnight-blue/60 hover:text-spiritual-earth transition-colors" href="#">
|
||||
</Link>
|
||||
<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">
|
||||
<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"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<p class="text-sm">© 2024 Kris Saint Ange. Tous droits réservés.</p>
|
||||
</div>
|
||||
@ -82,6 +143,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Utility classes for colors */
|
||||
.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,104 +3,115 @@ import ShuffleCardPresentation from '@/components/organism/ShuffleCard/ShuffleCa
|
||||
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
||||
import { useTarotStore } from '@/stores/tarot';
|
||||
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 tarotStore = useTarotStore();
|
||||
const isSelectionScreen = ref(true);
|
||||
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 cards = ref([]);
|
||||
|
||||
// This function will be called from the "offer" buttons
|
||||
const handleSelection = (count: number) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
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;
|
||||
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) {
|
||||
// Free draw
|
||||
if (tarotStore.freeDrawsRemaining > 0) {
|
||||
tarotStore.useFreeDraw();
|
||||
isSelectionScreen.value = false; // Switch to the shuffle screen
|
||||
try {
|
||||
// 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 {
|
||||
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 {
|
||||
// Paid draw
|
||||
// This is where you'd trigger your Stripe payment component
|
||||
alert(`Initiating payment process for a ${count}-card draw.`);
|
||||
// For now, let's simulate a successful payment and then proceed
|
||||
tarotStore.unlockNewDraws().then(() => {
|
||||
isSelectionScreen.value = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getCard = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await axios.post('/draw-card', { count: drawCount.value });
|
||||
if (res.data) {
|
||||
cardComponent.value.setDrawnCards(res.data.card);
|
||||
// This is the paid draw logic, which remains mostly the same
|
||||
if (tarotStore.paidDrawsRemaining >= count) {
|
||||
// Here, you would also make an API call to get the paid cards
|
||||
try {
|
||||
const response = await axios.get('/api/get-cards', {
|
||||
params: { count: count },
|
||||
});
|
||||
if (response.data.success) {
|
||||
cards.value = response.data.cards;
|
||||
tarotStore.usePaidDraw(count);
|
||||
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 {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<LandingLayout>
|
||||
<section v-if="isSelectionScreen" class="py-20 sm:py-24">
|
||||
<h2 class="mb-16 text-center text-4xl font-bold text-[var(--midnight-blue)] md:text-5xl">Explorez Nos Lectures</h2>
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
class="flex flex-col gap-6 rounded-2xl border border-[var(--linen)] bg-[var(--pure-white)] p-8 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3 class="text-2xl font-bold text-[var(--midnight-blue)]">Lecture Gratuite</h3>
|
||||
<p class="mt-2 text-5xl font-bold text-[var(--subtle-gold)]">Gratuit</p>
|
||||
</div>
|
||||
<button
|
||||
class="mt-4 flex h-12 w-full items-center justify-center rounded-full bg-[var(--linen)] px-8 font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:text-[var(--pure-white)]"
|
||||
@click="handleSelection(1)"
|
||||
>
|
||||
Commencer
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex scale-105 flex-col gap-6 rounded-2xl bg-[var(--midnight-blue)] p-8 shadow-lg ring-2 ring-[var(--subtle-gold)] transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3 class="text-2xl font-bold text-[var(--pure-white)]">Profilage</h3>
|
||||
<p class="mt-2 text-5xl font-bold text-[var(--subtle-gold)]">29€</p>
|
||||
</div>
|
||||
<button
|
||||
class="mt-4 flex h-12 w-full items-center justify-center rounded-full bg-[var(--subtle-gold)] px-8 font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--pure-white)]"
|
||||
@click="handleSelection(3)"
|
||||
>
|
||||
Découvrir
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-6 rounded-2xl border border-[var(--linen)] bg-[var(--pure-white)] p-8 shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3 class="text-2xl font-bold text-[var(--midnight-blue)]">Quadrige Doré</h3>
|
||||
<p class="mt-2 text-5xl font-bold text-[var(--subtle-gold)]">99€</p>
|
||||
</div>
|
||||
<button
|
||||
class="mt-4 flex h-12 w-full items-center justify-center rounded-full bg-[var(--linen)] px-8 font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--spiritual-earth)] hover:text-[var(--pure-white)]"
|
||||
@click="handleSelection(4)"
|
||||
>
|
||||
Explorer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ShuffleCardPresentation v-else ref="cardComponent" @draw-card="getCard" />
|
||||
<card-selection @selectDraw="handleSelection" v-if="isSelectionScreen && !clientSessionId" />
|
||||
<ShuffleCardPresentation v-else ref="cardComponent" :drawn-cards="cards" />
|
||||
</LandingLayout>
|
||||
</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', () => {
|
||||
// State
|
||||
const freeDrawsRemaining = ref(1);
|
||||
const paidDrawsRemaining = ref(0);
|
||||
|
||||
// Actions
|
||||
function useFreeDraw() {
|
||||
if (freeDrawsRemaining.value > 0) {
|
||||
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
|
||||
// This is a placeholder for your payment logic.
|
||||
function unlockNewDraws() {
|
||||
// You would typically call a backend endpoint here to create a Stripe Checkout Session
|
||||
// and redirect the user. For this example, we'll simulate a successful payment.
|
||||
console.log('Redirecting to Stripe for payment...');
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
console.log('Payment successful! Adding 1 new draw.');
|
||||
// After successful payment from Stripe, you would update the state.
|
||||
// This state update would likely come from a backend webhook.
|
||||
freeDrawsRemaining.value++;
|
||||
resolve(true);
|
||||
}, 2000); // Simulate a network delay
|
||||
});
|
||||
// Modified usePaidDraw to handle the correct number of cards and reset the state
|
||||
function usePaidDraw(count: number) {
|
||||
if (paidDrawsRemaining.value >= count) {
|
||||
// Since the draws are 'used', we set the remaining to 0.
|
||||
// This assumes a user pays for a set number of cards in one go.
|
||||
paidDrawsRemaining.value = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function addPaidDraws(count: number) {
|
||||
paidDrawsRemaining.value += count;
|
||||
}
|
||||
|
||||
return {
|
||||
freeDrawsRemaining,
|
||||
paidDrawsRemaining,
|
||||
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 Inertia\Inertia;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
Route::get('/', function () {
|
||||
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::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__.'/auth.php';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user