diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..64f0d4f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,94 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Development Commands
+
+### Frontend Development
+- `npm run dev` - Start Vite development server
+- `npm run build` - Build for production
+- `npm run build:ssr` - Build with SSR support
+- `npm run lint` - Run ESLint with auto-fix
+- `npm run types` - Run TypeScript type checking
+- `npm run format` - Format code with Prettier
+- `npm run format:check` - Check code formatting
+
+### Backend Development
+- `composer dev` - Start full development environment (Laravel server, queue worker, logs, Vite)
+- `composer dev:ssr` - Start development with SSR
+- `composer test` - Run PHP tests with Pest
+- `php artisan serve` - Start Laravel development server
+- `php artisan queue:listen --tries=1` - Start queue worker
+- `php artisan pail --timeout=0` - Start log viewer
+
+## Architecture Overview
+
+### Stack
+- **Backend**: Laravel 12 with PHP 8.2+
+- **Frontend**: React 19 + TypeScript with Inertia.js
+- **Database**: PostgreSQL with pgvector extension for embeddings
+- **Queue System**: Laravel Horizon for job processing
+- **Payment**: Laravel Cashier with Stripe integration
+- **Media Processing**: FFmpeg integration via pbmedia/laravel-ffmpeg
+- **Authentication**: Laravel Sanctum + Google OAuth via Socialite
+
+### Core Application Structure
+
+**MemeAI Generator Platform**: This is a meme generation platform that uses AI to create memes from user inputs.
+
+#### Key Models & Relationships
+- `User` - Uses Billable trait for Stripe, has UUID-based public IDs
+- `Meme` - Generated memes with keyword categorization (action/emotion/misc)
+- `MemeMedia` - Template media files for meme generation
+- `BackgroundMedia` - Background images/videos for memes
+- `Category` - Hierarchical categories using kalnoy/nestedset
+- `UserUsage` & `UserPlan` - Usage tracking and subscription management
+- `KeywordEmbedding` & `MemeMediaEmbedding` - Vector embeddings for content matching
+
+#### AI Integration Points
+- `CloudflareAI`, `OpenAI`, `RunwareAI` - Multiple AI service integrations
+- `MemeGenerator` - Core meme generation logic with keyword matching via vector similarity
+- Uses pgvector for semantic search and content matching
+
+#### Frontend Architecture
+- **Inertia.js** full-stack integration between Laravel and React
+- **Zustand** for state management (VideoEditorStore, MediaStore, UserStore, PricingStore)
+- **Radix UI** + **Tailwind CSS** for component library
+- **Konva.js** + **React-Konva** for canvas-based video/meme editing
+- **FFmpeg.wasm** for client-side media processing
+
+#### Media Processing Pipeline
+- Video templates stored as JSON configurations
+- Canvas-based editor with timeline support
+- Server-side FFmpeg processing for final video generation
+- S3/R2 storage integration via Laravel Filesystem
+
+#### Key Helper Classes
+- `MediaEngine` - Media processing utilities
+- `PurchaseHelper`, `SubscriptionHelper` - Payment processing
+- `WatermarkUsageHelper` - Usage tracking and limits
+- `StripeHelper` - Stripe integration utilities
+
+### Development Notes
+
+#### Testing
+- Uses Pest PHP for backend testing
+- Test files in `tests/Feature/` and `tests/Unit/`
+- Run tests with `composer test`
+
+#### Code Organization
+- Custom helpers in `app/Helpers/FirstParty/`
+- Global helper functions in `app/Helpers/Global/helpers.php`
+- Frontend components organized by feature in `resources/js/modules/`
+- Reusable UI components in `resources/js/components/ui/`
+
+#### Special Configurations
+- CORS headers configured for FFmpeg.wasm in Vite config
+- Vector search capabilities via PostgreSQL pgvector extension
+- Hashids for public ID generation (users, etc.)
+- Queue-based background processing for media generation
+
+#### Environment-Specific Features
+- Test routes can be enabled via `ENABLE_TEST_ROUTES=true`
+- Uses Laravel Horizon for queue management in production
+- Supports both regular and SSR deployments
\ No newline at end of file
diff --git a/MEMEAIGEN/STRIPE WEBHOOKS.bru b/MEMEAIGEN/STRIPE WEBHOOKS.bru
index 94474e1..ee323d2 100644
--- a/MEMEAIGEN/STRIPE WEBHOOKS.bru
+++ b/MEMEAIGEN/STRIPE WEBHOOKS.bru
@@ -12,135 +12,136 @@ post {
body:json {
{
- "id": "evt_1Rg71IEEXQJo9EEO4GHqZdRZ",
+ "id": "evt_1Rg71IEEXQJo9EEOmxJtfwVm",
"object": "event",
"api_version": "2025-05-28.basil",
"created": 1751387215,
"data": {
"object": {
- "id": "sub_1Rg71FEEXQJo9EEO18sXSEho",
- "object": "subscription",
+ "id": "in_1Rg71FEEXQJo9EEOmxbGjtdH",
+ "object": "invoice",
+ "account_country": "MY",
+ "account_name": "MEMEAIGEN sandbox",
+ "account_tax_ids": null,
+ "amount_due": 400,
+ "amount_overpaid": 0,
+ "amount_paid": 400,
+ "amount_remaining": 0,
+ "amount_shipping": 0,
"application": null,
- "application_fee_percent": null,
+ "attempt_count": 1,
+ "attempted": true,
+ "auto_advance": false,
"automatic_tax": {
"disabled_reason": null,
"enabled": false,
- "liability": null
- },
- "billing_cycle_anchor": 1751387213,
- "billing_cycle_anchor_config": null,
- "billing_mode": {
- "type": "classic"
- },
- "billing_thresholds": null,
- "cancel_at": null,
- "cancel_at_period_end": false,
- "canceled_at": null,
- "cancellation_details": {
- "comment": null,
- "feedback": null,
- "reason": null
+ "liability": null,
+ "provider": null,
+ "status": null
},
+ "automatically_finalizes_at": null,
+ "billing_reason": "subscription_create",
"collection_method": "charge_automatically",
"created": 1751387213,
"currency": "usd",
+ "custom_fields": null,
"customer": "cus_SbGYl34MpG4nv5",
- "days_until_due": null,
- "default_payment_method": "pm_1Rg71EEEXQJo9EEOJWUAU6EQ",
+ "customer_address": null,
+ "customer_email": "memeaigen.com@gmail.com",
+ "customer_name": null,
+ "customer_phone": null,
+ "customer_shipping": null,
+ "customer_tax_exempt": "none",
+ "customer_tax_ids": [],
+ "default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"description": null,
"discounts": [],
- "ended_at": null,
- "invoice_settings": {
- "account_tax_ids": null,
- "issuer": {
- "type": "self"
- }
+ "due_date": null,
+ "effective_at": 1751387213,
+ "ending_balance": 0,
+ "footer": null,
+ "from_invoice": null,
+ "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1RfMzuEEXQJo9EEO/test_YWNjdF8xUmZNenVFRVhRSm85RUVPLF9TYkplU0VDWWdKSG53bDVZT2VuNjNGZFFaaFJsbFFkLDE0MTkyODAxNg0200EQHRg6hn?s=ap",
+ "invoice_pdf": "https://pay.stripe.com/invoice/acct_1RfMzuEEXQJo9EEO/test_YWNjdF8xUmZNenVFRVhRSm85RUVPLF9TYkplU0VDWWdKSG53bDVZT2VuNjNGZFFaaFJsbFFkLDE0MTkyODAxNg0200EQHRg6hn/pdf?s=ap",
+ "issuer": {
+ "type": "self"
},
- "items": {
+ "last_finalization_error": null,
+ "latest_revision": null,
+ "lines": {
"object": "list",
"data": [
{
- "id": "si_SbJesuW5WgGoZ7",
- "object": "subscription_item",
- "billing_thresholds": null,
- "created": 1751387213,
- "current_period_end": 1754065613,
- "current_period_start": 1751387213,
+ "id": "il_1Rg71FEEXQJo9EEOhVjRevMb",
+ "object": "line_item",
+ "amount": 400,
+ "currency": "usd",
+ "description": "1 × Personal Creator (at $4.00 / month)",
+ "discount_amounts": [],
+ "discountable": true,
"discounts": [],
- "metadata": {},
- "plan": {
- "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt",
- "object": "plan",
- "active": true,
- "amount": 400,
- "amount_decimal": "400",
- "billing_scheme": "per_unit",
- "created": 1751210467,
- "currency": "usd",
- "interval": "month",
- "interval_count": 1,
- "livemode": false,
- "metadata": {},
- "meter": null,
- "nickname": null,
- "product": "prod_SaY8TGjiPi5hWu",
- "tiers_mode": null,
- "transform_usage": null,
- "trial_period_days": null,
- "usage_type": "licensed"
+ "invoice": "in_1Rg71FEEXQJo9EEOmxbGjtdH",
+ "livemode": false,
+ "metadata": {
+ "is_on_session_checkout": "true"
},
- "price": {
- "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt",
- "object": "price",
- "active": true,
- "billing_scheme": "per_unit",
- "created": 1751210467,
- "currency": "usd",
- "custom_unit_amount": null,
- "livemode": false,
- "lookup_key": null,
- "metadata": {},
- "nickname": null,
- "product": "prod_SaY8TGjiPi5hWu",
- "recurring": {
- "interval": "month",
- "interval_count": 1,
- "meter": null,
- "trial_period_days": null,
- "usage_type": "licensed"
+ "parent": {
+ "invoice_item_details": null,
+ "subscription_item_details": {
+ "invoice_item": null,
+ "proration": false,
+ "proration_details": {
+ "credited_items": null
+ },
+ "subscription": "sub_1Rg71FEEXQJo9EEO18sXSEho",
+ "subscription_item": "si_SbJesuW5WgGoZ7"
},
- "tax_behavior": "unspecified",
- "tiers_mode": null,
- "transform_quantity": null,
- "type": "recurring",
- "unit_amount": 400,
+ "type": "subscription_item_details"
+ },
+ "period": {
+ "end": 1754065613,
+ "start": 1751387213
+ },
+ "pretax_credit_amounts": [],
+ "pricing": {
+ "price_details": {
+ "price": "price_1RfN2VEEXQJo9EEOzjPI2HGt",
+ "product": "prod_SaY8TGjiPi5hWu"
+ },
+ "type": "price_details",
"unit_amount_decimal": "400"
},
"quantity": 1,
- "subscription": "sub_1Rg71FEEXQJo9EEO18sXSEho",
- "tax_rates": []
+ "taxes": []
}
],
"has_more": false,
"total_count": 1,
- "url": "/v1/subscription_items?subscription=sub_1Rg71FEEXQJo9EEO18sXSEho"
+ "url": "/v1/invoices/in_1Rg71FEEXQJo9EEOmxbGjtdH/lines"
},
- "latest_invoice": "in_1Rg71FEEXQJo9EEOmxbGjtdH",
"livemode": false,
- "metadata": {
- "is_on_session_checkout": "true"
- },
- "next_pending_invoice_item_invoice": null,
+ "metadata": {},
+ "next_payment_attempt": null,
+ "number": "IN7OJLTH-0001",
"on_behalf_of": null,
- "pause_collection": null,
+ "parent": {
+ "quote_details": null,
+ "subscription_details": {
+ "metadata": {
+ "is_on_session_checkout": "true"
+ },
+ "subscription": "sub_1Rg71FEEXQJo9EEO18sXSEho"
+ },
+ "type": "subscription_details"
+ },
"payment_settings": {
+ "default_mandate": null,
"payment_method_options": {
"acss_debit": null,
"bancontact": null,
"card": {
- "network": null,
"request_three_d_secure": "automatic"
},
"customer_balance": null,
@@ -148,50 +149,34 @@ body:json {
"sepa_debit": null,
"us_bank_account": null
},
- "payment_method_types": null,
- "save_default_payment_method": "off"
+ "payment_method_types": null
},
- "pending_invoice_item_interval": null,
- "pending_setup_intent": null,
- "pending_update": null,
- "plan": {
- "id": "price_1RfN2VEEXQJo9EEOzjPI2HGt",
- "object": "plan",
- "active": true,
- "amount": 400,
- "amount_decimal": "400",
- "billing_scheme": "per_unit",
- "created": 1751210467,
- "currency": "usd",
- "interval": "month",
- "interval_count": 1,
- "livemode": false,
- "metadata": {},
- "meter": null,
- "nickname": null,
- "product": "prod_SaY8TGjiPi5hWu",
- "tiers_mode": null,
- "transform_usage": null,
- "trial_period_days": null,
- "usage_type": "licensed"
+ "period_end": 1751387213,
+ "period_start": 1751387213,
+ "post_payment_credit_notes_amount": 0,
+ "pre_payment_credit_notes_amount": 0,
+ "receipt_number": null,
+ "rendering": null,
+ "shipping_cost": null,
+ "shipping_details": null,
+ "starting_balance": 0,
+ "statement_descriptor": null,
+ "status": "paid",
+ "status_transitions": {
+ "finalized_at": 1751387213,
+ "marked_uncollectible_at": null,
+ "paid_at": 1751387215,
+ "voided_at": null
},
- "quantity": 1,
- "schedule": null,
- "start_date": 1751387213,
- "status": "active",
+ "subtotal": 400,
+ "subtotal_excluding_tax": 400,
"test_clock": null,
- "transfer_data": null,
- "trial_end": null,
- "trial_settings": {
- "end_behavior": {
- "missing_payment_method": "create_invoice"
- }
- },
- "trial_start": null
- },
- "previous_attributes": {
- "default_payment_method": null,
- "status": "incomplete"
+ "total": 400,
+ "total_discount_amounts": [],
+ "total_excluding_tax": 400,
+ "total_pretax_credit_amounts": [],
+ "total_taxes": [],
+ "webhooks_delivered_at": null
}
},
"livemode": false,
@@ -200,6 +185,6 @@ body:json {
"id": null,
"idempotency_key": "e50baede-20b6-4a06-a2c4-15f43ca47dd4"
},
- "type": "customer.subscription.updated"
+ "type": "invoice.paid"
}
}
diff --git a/app/Helpers/FirstParty/Purchase/PurchaseHelper.php b/app/Helpers/FirstParty/Purchase/PurchaseHelper.php
index 6fed130..74043c1 100644
--- a/app/Helpers/FirstParty/Purchase/PurchaseHelper.php
+++ b/app/Helpers/FirstParty/Purchase/PurchaseHelper.php
@@ -43,7 +43,6 @@ public static function getSubscriptionPlanByStripePriceID($stripe_price_id)
foreach ($plans as $plan) {
-
if ($plan['id'] == 'free') {
continue;
}
@@ -76,10 +75,10 @@ public static function getPlanSystemProperty($plan, $property)
// Inject environment into the path
// stripe.product_id.month becomes system.stripe.product_id.{env}.month
array_splice($propertyParts, 2, 0, $environment);
- $fullPath = 'system.' . implode('.', $propertyParts);
+ $fullPath = 'system.'.implode('.', $propertyParts);
} else {
// For non-stripe properties, just prepend 'system.'
- $fullPath = 'system.' . $property;
+ $fullPath = 'system.'.$property;
}
return data_get($plan, $fullPath);
diff --git a/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php b/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php
index 379b2a8..1a04588 100644
--- a/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php
+++ b/app/Helpers/FirstParty/Purchase/SubscriptionHelper.php
@@ -5,7 +5,6 @@
use App\Helpers\FirstParty\Stripe\StripeHelper;
use App\Models\Plan;
use App\Models\UserPlan;
-use Illuminate\Support\Facades\Log;
use Laravel\Cashier\Events\WebhookReceived;
class SubscriptionHelper
@@ -28,7 +27,7 @@ private static function handleSubscriptionUpsert(WebhookReceived $event)
{
$object = $event->payload['data']['object'];
- //dump($object);
+ // dump($object);
$ignore_statuses = ['incomplete_expired', 'incomplete'];
@@ -41,7 +40,7 @@ private static function handleSubscriptionUpsert(WebhookReceived $event)
if ($user) {
foreach ($object['items']['data'] as $line_item) {
- //dump($line_item);
+ // dump($line_item);
$stripe_price_id = $line_item['plan']['id'];
$current_period_end = $line_item['current_period_end'];
diff --git a/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php b/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php
index 617cbb5..ae1d5ec 100644
--- a/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php
+++ b/app/Helpers/FirstParty/Purchase/WatermarkUsageHelper.php
@@ -4,7 +4,6 @@
use App\Helpers\FirstParty\Stripe\StripeHelper;
use App\Models\UserUsage;
-use Illuminate\Support\Facades\App;
use Laravel\Cashier\Events\WebhookReceived;
class WatermarkUsageHelper
@@ -22,11 +21,11 @@ private static function handleInvoicePaid(WebhookReceived $event)
{
$object = $event->payload['data']['object'];
- //dump($object);
+ // dump($object);
$accept_statuses = ['paid', 'partially_paid'];
- if (!in_array($object['status'], $accept_statuses)) {
+ if (! in_array($object['status'], $accept_statuses)) {
return;
}
@@ -36,7 +35,7 @@ private static function handleInvoicePaid(WebhookReceived $event)
foreach ($object['lines']['data'] as $line_item) {
$stripe_price_id = $line_item['pricing']['price_details']['price'];
- //dd($stripe_price_id);
+ // dd($stripe_price_id);
$subscription_config = PurchaseHelper::getSubscriptionPlanByStripePriceID($stripe_price_id);
@@ -57,7 +56,7 @@ private static function handleInvoicePaid(WebhookReceived $event)
}
}
- //dd($subscription_config);
+ // dd($subscription_config);
}
}
}
diff --git a/app/Helpers/FirstParty/Stripe/StripeHelper.php b/app/Helpers/FirstParty/Stripe/StripeHelper.php
index 6e63b69..b8d1ccd 100644
--- a/app/Helpers/FirstParty/Stripe/StripeHelper.php
+++ b/app/Helpers/FirstParty/Stripe/StripeHelper.php
@@ -3,7 +3,6 @@
namespace App\Helpers\FirstParty\Stripe;
use App\Models\User;
-use Laravel\Cashier\Events\WebhookReceived;
class StripeHelper
{
diff --git a/app/Http/Controllers/SocialAuthController.php b/app/Http/Controllers/SocialAuthController.php
index aca487b..1953636 100644
--- a/app/Http/Controllers/SocialAuthController.php
+++ b/app/Http/Controllers/SocialAuthController.php
@@ -65,7 +65,7 @@ public function handleGoogleCallback()
return redirect()->intended(route('home'))->with('success', "You're now logged in!");
} catch (\Exception $e) {
- //throw $e;
+ // throw $e;
$error_message = 'Google login failed. Please try again.';
if (config('app.debug')) {
$error_message = $e->getMessage();
diff --git a/app/Http/Controllers/UserAccountController.php b/app/Http/Controllers/UserAccountController.php
new file mode 100644
index 0000000..ef0aad8
--- /dev/null
+++ b/app/Http/Controllers/UserAccountController.php
@@ -0,0 +1,28 @@
+load('user_usage');
+ $user->load('plan');
+
+ return response()->json([
+ 'success' => [
+ 'data' => [
+ 'user' => $user,
+ 'billing' => [
+ 'provider' => 'stripe',
+ 'portal' => Auth::user()->billingPortalUrl(route('home'))
+ ]
+ ],
+ ],
+ ]);
+ }
+}
diff --git a/app/Models/Plan.php b/app/Models/Plan.php
index 02b1f15..beaa3c5 100644
--- a/app/Models/Plan.php
+++ b/app/Models/Plan.php
@@ -26,4 +26,11 @@ class Plan extends Model
'name',
'tier',
];
+
+ protected $hidden = [
+ 'id',
+ 'created_at',
+ 'updated_at',
+ 'laravel_through_key',
+ ];
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 4e43720..2c794dd 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -3,6 +3,8 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
+
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -14,7 +16,7 @@
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
- use HasApiTokens, HasFactory, Notifiable, SoftDeletes, Billable;
+ use Billable, HasApiTokens, HasFactory, Notifiable, SoftDeletes;
/**
* The attributes that are mass assignable.
@@ -37,8 +39,28 @@ class User extends Authenticatable
'password',
'remember_token',
'id',
+ 'created_at',
+ 'updated_at',
+ 'deleted_at',
+ 'uuid',
+ 'stripe_id',
+ 'google_id',
+ 'email_verified_at',
+ 'pm_last_four',
+ 'pm_type',
+ 'trial_ends_at',
+ 'email',
];
+ protected $appends = ['ids'];
+
+ protected function ids(): Attribute
+ {
+ return Attribute::make(
+ get: fn($value, $attributes) => hashids_encode($attributes['id']),
+ );
+ }
+
/**
* Get the attributes that should be cast.
*
@@ -61,4 +83,19 @@ protected static function booted(): void
$model->uuid = $model->uuid ?? (string) Str::uuid();
});
}
+
+ public function user_usage()
+ {
+ return $this->hasOne(UserUsage::class, 'user_id');
+ }
+
+ public function user_plan()
+ {
+ return $this->hasOne(UserPlan::class, 'user_id');
+ }
+
+ public function plan()
+ {
+ return $this->hasOneThrough(Plan::class, UserPlan::class, 'user_id', 'id', 'id', 'plan_id');
+ }
}
diff --git a/app/Models/UserPlan.php b/app/Models/UserPlan.php
index 7b3e150..cdf8cc6 100644
--- a/app/Models/UserPlan.php
+++ b/app/Models/UserPlan.php
@@ -40,4 +40,14 @@ class UserPlan extends Model
'cancel_at',
'canceled_at',
];
+
+ public function user()
+ {
+ return $this->belongsTo(User::class, 'user_id');
+ }
+
+ public function plan()
+ {
+ return $this->belongsTo(Plan::class, 'plan_id');
+ }
}
diff --git a/app/Models/UserUsage.php b/app/Models/UserUsage.php
index 171dabe..6ea6d7d 100644
--- a/app/Models/UserUsage.php
+++ b/app/Models/UserUsage.php
@@ -7,30 +7,45 @@
namespace App\Models;
use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
/**
* Class UserUsage
- *
+ *
* @property int $id
* @property int $user_id
* @property int $non_watermark_videos_left
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
- *
- * @package App\Models
*/
class UserUsage extends Model
{
- protected $table = 'user_usages';
+ protected $table = 'user_usages';
- protected $casts = [
- 'user_id' => 'int',
- 'non_watermark_videos_left' => 'int'
- ];
+ protected $casts = [
+ 'user_id' => 'int',
+ 'non_watermark_videos_left' => 'int',
+ ];
- protected $fillable = [
- 'user_id',
- 'non_watermark_videos_left'
- ];
+ protected $fillable = [
+ 'user_id',
+ 'non_watermark_videos_left',
+ ];
+
+ protected $hidden = [
+ 'created_at',
+ 'updated_at',
+ 'id',
+ 'user_id',
+ ];
+
+ protected $appends = ['ids'];
+
+ protected function ids(): Attribute
+ {
+ return Attribute::make(
+ get: fn($value, $attributes) => hashids_encode($attributes['id']),
+ );
+ }
}
diff --git a/resources/js/app.tsx b/resources/js/app.tsx
index 17de309..5c06599 100644
--- a/resources/js/app.tsx
+++ b/resources/js/app.tsx
@@ -38,6 +38,7 @@ createInertiaApp({