diff --git a/app/Console/Commands/AddDummyTenantUser.php b/app/Console/Commands/AddDummyTenantUser.php index b4af270..305affe 100644 --- a/app/Console/Commands/AddDummyTenantUser.php +++ b/app/Console/Commands/AddDummyTenantUser.php @@ -19,6 +19,10 @@ class AddDummyTenantUser extends Command {--password=secret123!} {--tenant="Demo Tenant"} {--name="Demo Admin"} + {--first_name="Demo"} + {--last_name="Admin"} + {--address="Demo Str. 1, 12345 Demo City"} + {--phone="+49 123 4567890"} {--update-password : Overwrite password if user already exists} '; protected $description = 'Create a demo tenant and a tenant user with given credentials.'; @@ -29,6 +33,12 @@ class AddDummyTenantUser extends Command $password = (string) $this->option('password'); $tenantName = (string) $this->option('tenant'); $userName = (string) $this->option('name'); + $firstName = (string) $this->option('first_name'); + $lastName = (string) $this->option('last_name'); + $address = (string) $this->option('address'); + $phone = (string) $this->option('phone'); + + $this->info('Starting dummy tenant creation with email: ' . $email); // Pre-flight checks for common failures if (! Schema::hasTable('users')) { @@ -53,12 +63,17 @@ class AddDummyTenantUser extends Command $tenant->domain = null; $tenant->contact_name = $userName; $tenant->contact_email = $email; - $tenant->contact_phone = null; + $tenant->contact_phone = $phone ?: null; $tenant->event_credits_balance = 1; $tenant->max_photos_per_event = 500; $tenant->max_storage_mb = 1024; $tenant->features = ['custom_branding' => false]; + $tenant->is_active = true; + $tenant->is_suspended = false; $tenant->save(); + $this->info('Created new tenant: ' . $tenant->name); + } else { + $this->info('Using existing tenant: ' . $tenant->name); } // Create or fetch user @@ -70,9 +85,15 @@ class AddDummyTenantUser extends Command if (Schema::hasColumn($user->getTable(), 'name')) $user->name = $userName; $user->email = $email; $user->password = Hash::make($password); + $this->info('Creating new user: ' . $email); } else if ($updatePassword) { $user->password = Hash::make($password); + $this->info('Updating password for existing user: ' . $email); } + if (Schema::hasColumn($user->getTable(), 'first_name')) $user->first_name = $firstName; + if (Schema::hasColumn($user->getTable(), 'last_name')) $user->last_name = $lastName; + if (Schema::hasColumn($user->getTable(), 'address')) $user->address = $address; + if (Schema::hasColumn($user->getTable(), 'phone')) $user->phone = $phone; if (Schema::hasColumn($user->getTable(), 'tenant_id')) { $user->tenant_id = $tenant->id; } @@ -80,11 +101,13 @@ class AddDummyTenantUser extends Command $user->role = 'tenant_admin'; } $user->save(); + $this->info('User saved successfully.'); DB::commit(); } catch (\Throwable $e) { DB::rollBack(); $this->error('Failed: '.$e->getMessage()); + $this->error('Stack trace: ' . $e->getTraceAsString()); return self::FAILURE; } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..b2ea796 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,56 @@ + + */ + protected $dontFlash = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * Register the exception handling callbacks for the application. + */ + public function register(): void + { + $this->reportable(function (Throwable $e) { + // + }); + } + + public function render($request, Throwable $e) + { + \Illuminate\Support\Facades\Log::info('Handler render called', ['inertia' => $request->inertia(), 'exception' => get_class($e)]); + if ($request->inertia()) { + if ($e instanceof ValidationException) { + \Illuminate\Support\Facades\Log::info('ValidationException in Inertia', ['errors' => $e->errors()]); + return response()->json([ + 'message' => 'The given data was invalid.', + 'errors' => $e->errors(), + ], 422)->header('X-Inertia-Error', 'true'); + } + + if ($e instanceof \Exception) { + \Illuminate\Support\Facades\Log::info('Exception in Inertia', ['message' => $e->getMessage()]); + return response()->json([ + 'message' => 'Registrierung fehlgeschlagen.', + 'errors' => ['general' => $e->getMessage()], + ], 500)->header('X-Inertia-Error', 'true'); + } + } + + return parent::render($request, $e); + } +} \ No newline at end of file diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 9e93112..536bb07 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -40,10 +40,12 @@ class TenantResource extends Resource { return $form->schema([ - TextInput::make('name') + TextInput::make('user.full_name') ->label(__('admin.tenants.fields.name')) ->required() - ->maxLength(255), + ->readOnly() + ->dehydrated(false) + ->getStateUsing(fn (Tenant $record) => $record->user->full_name), TextInput::make('slug') ->label(__('admin.tenants.fields.slug')) ->required() @@ -90,7 +92,11 @@ class TenantResource extends Resource return $table ->columns([ Tables\Columns\TextColumn::make('id')->sortable(), - Tables\Columns\TextColumn::make('name')->searchable()->sortable(), + Tables\Columns\TextColumn::make('user.full_name') + ->label(__('admin.tenants.fields.name')) + ->searchable() + ->sortable() + ->getStateUsing(fn (Tenant $record) => $record->user->full_name), Tables\Columns\TextColumn::make('slug')->searchable(), Tables\Columns\TextColumn::make('contact_email'), Tables\Columns\TextColumn::make('active_reseller_package_id') diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index d81d7c4..924605a 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -41,11 +41,11 @@ class AuthenticatedSessionController extends Controller $request->session()->regenerate(); $user = Auth::user(); - if ($user && !$user->hasVerifiedEmail()) { - return redirect()->route('verification.notice'); + if ($user && $user->email_verified_at === null) { + return Inertia::location(route('verification.notice')); } - return redirect()->intended(route('dashboard', absolute: false)); + return Inertia::location(route('dashboard', absolute: false)); } /** diff --git a/app/Http/Controllers/Auth/MarketingRegisterController.php b/app/Http/Controllers/Auth/MarketingRegisterController.php index bc3a947..ebdf42d 100644 --- a/app/Http/Controllers/Auth/MarketingRegisterController.php +++ b/app/Http/Controllers/Auth/MarketingRegisterController.php @@ -15,18 +15,24 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rules; use Illuminate\Support\Str; +use Illuminate\Support\Facades\App; +use Inertia\Inertia; +use Inertia\Response; class MarketingRegisterController extends Controller { /** * Show the registration page. */ - public function create(Request $request, $package_id = null): \Illuminate\View\View + public function create(Request $request, $package_id = null): Response { $package = $package_id ? Package::find($package_id) : null; - return view('marketing.register', [ + App::setLocale('de'); + + return Inertia::render('Auth/Register', [ 'package' => $package, + 'privacyHtml' => view('legal.datenschutz')->render(), ]); } @@ -38,7 +44,6 @@ class MarketingRegisterController extends Controller public function store(Request $request): RedirectResponse { $request->validate([ - 'name' => ['required', 'string', 'max:255'], 'username' => ['required', 'string', 'max:255', 'unique:'.User::class], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'password' => ['required', 'confirmed', Rules\Password::defaults()], @@ -51,7 +56,6 @@ class MarketingRegisterController extends Controller ]); $user = User::create([ - 'name' => $request->name, 'username' => $request->username, 'email' => $request->email, 'first_name' => $request->first_name, @@ -109,7 +113,7 @@ class MarketingRegisterController extends Controller 'active' => true, 'price' => 0, ]); - + PackagePurchase::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, @@ -118,16 +122,16 @@ class MarketingRegisterController extends Controller 'purchased_at' => now(), 'provider_id' => 'free', ]); - + $tenant->update(['subscription_status' => 'active']); } else { // Redirect to buy for paid package - return redirect()->route('buy.packages', $package->id); + return Inertia::location(route('buy.packages', $package->id)); } } return $user->hasVerifiedEmail() - ? redirect()->intended(route('dashboard')) - : redirect()->route('verification.notice'); + ? Inertia::location(route('dashboard')) + : Inertia::location(route('verification.notice')); } } \ No newline at end of file diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 9b867c6..1c811e7 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -14,6 +14,7 @@ use Inertia\Inertia; use Inertia\Response; use App\Models\Tenant; use Illuminate\Support\Str; +use Illuminate\Support\Facades\Validator; class RegisteredUserController extends Controller { @@ -36,8 +37,9 @@ class RegisteredUserController extends Controller */ public function store(Request $request): RedirectResponse { - $request->validate([ - 'name' => ['required', 'string', 'max:255'], + $fullName = trim($request->first_name . ' ' . $request->last_name); + + $validated = $request->validate([ 'username' => ['required', 'string', 'max:255', 'unique:'.User::class], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class], 'password' => ['required', 'confirmed', Rules\Password::defaults()], @@ -50,23 +52,20 @@ class RegisteredUserController extends Controller ]); $user = User::create([ - 'name' => $request->name, - 'username' => $request->username, - 'email' => $request->email, - 'first_name' => $request->first_name, - 'last_name' => $request->last_name, - 'address' => $request->address, - 'phone' => $request->phone, - 'password' => Hash::make($request->password), + 'username' => $validated['username'], + 'email' => $validated['email'], + 'first_name' => $validated['first_name'], + 'last_name' => $validated['last_name'], + 'address' => $validated['address'], + 'phone' => $validated['phone'], + 'password' => Hash::make($validated['password']), 'privacy_consent_at' => now(), // Neues Feld für Consent (füge Migration hinzu, falls nötig) ]); - \Illuminate\Support\Facades\Log::info('Creating tenant for user ID: ' . $user->id); - $tenant = Tenant::create([ 'user_id' => $user->id, - 'name' => $request->name, - 'slug' => Str::slug($request->name . '-' . now()->timestamp), + 'name' => $fullName, + 'slug' => Str::slug($fullName . '-' . now()->timestamp), 'email' => $request->email, 'is_active' => true, 'is_suspended' => false, @@ -92,8 +91,6 @@ class RegisteredUserController extends Controller ]), ]); - \Illuminate\Support\Facades\Log::info('Tenant created with ID: ' . $tenant->id); - event(new Registered($user)); // Send Welcome Email @@ -126,9 +123,7 @@ class RegisteredUserController extends Controller } } - \Illuminate\Support\Facades\Log::info('Logging in user ID: ' . $user->id); Auth::login($user); - \Illuminate\Support\Facades\Log::info('User logged in: ' . (Auth::check() ? 'Yes' : 'No')); return $user->hasVerifiedEmail() ? redirect()->intended(route('dashboard')) diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index 7fca2a3..fd62e15 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -22,6 +22,7 @@ use App\Models\Package; use App\Models\TenantPackage; use App\Models\PackagePurchase; use Illuminate\Support\Facades\Auth; +use Inertia\Inertia; class MarketingController extends Controller { @@ -38,7 +39,7 @@ class MarketingController extends Controller ['id' => 'premium', 'name' => 'Premium', 'events' => 50, 'price' => 199, 'description' => '50 Events, Support & Custom, Alle Features'], ]; - return view('marketing', compact('packages')); + return Inertia::render('marketing/Home', compact('packages')); } public function contact(Request $request) @@ -57,6 +58,11 @@ class MarketingController extends Controller return redirect()->back()->with('success', 'Nachricht gesendet!'); } + public function contactView() + { + return Inertia::render('marketing/Kontakt'); + } + /** * Handle package purchase flow. */ @@ -341,7 +347,7 @@ class MarketingController extends Controller return redirect('/admin')->with('success', __('marketing.success.welcome')); } - return view('marketing.success', compact('packageId')); + return Inertia::render('marketing/Success', compact('packageId')); } public function blogIndex(Request $request) @@ -377,7 +383,7 @@ class MarketingController extends Controller Log::info('Blog Index Debug - Final Posts', ['count' => $posts->count(), 'total' => $posts->total()]); - return view('marketing.blog', compact('posts')); + return Inertia::render('marketing/Blog', compact('posts')); } public function blogShow($slug) @@ -394,26 +400,28 @@ class MarketingController extends Controller ->whereJsonContains("translations->locale->title->{$locale}", true) ->firstOrFail(); - return view('marketing.blog-show', compact('post')); + return Inertia::render('marketing/BlogShow', compact('post')); } public function packagesIndex() { - - $endcustomerPackages = Package::where('type', 'endcustomer')->orderBy('price')->get(); - $resellerPackages = Package::where('type', 'reseller')->orderBy('price')->get(); + $endcustomerPackages = Package::where('type', 'endcustomer')->orderBy('price')->get()->map(function ($p) { + return $p->append(['features', 'limits']); + }); + $resellerPackages = Package::where('type', 'reseller')->orderBy('price')->get()->map(function ($p) { + return $p->append(['features', 'limits']); + }); - return view('marketing.packages', compact('endcustomerPackages', 'resellerPackages')); + return Inertia::render('marketing/Packages', compact('endcustomerPackages', 'resellerPackages')); } public function occasionsType($locale, $type) { - $validTypes = ['weddings', 'birthdays', 'corporate-events', 'family-celebrations']; if (!in_array($type, $validTypes)) { abort(404, 'Invalid occasion type'); } - return view('marketing.occasions', ['type' => $type]); + return Inertia::render('marketing/Occasions', ['type' => $type]); } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index ceddc9a..cb11314 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -30,7 +30,6 @@ class ProfileController extends Controller // Authorized via auth middleware $request->validate([ - 'name' => 'required|string|max:255', 'username' => ['required', 'string', 'max:255', 'alpha_dash', 'unique:users,username,' . $user->id], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id], 'first_name' => ['required', 'string', 'max:255'], @@ -40,7 +39,7 @@ class ProfileController extends Controller ]); $user->update($request->only([ - 'name', 'username', 'email', 'first_name', 'last_name', 'address', 'phone' + 'username', 'email', 'first_name', 'last_name', 'address', 'phone' ])); return back()->with('status', 'profile-updated'); diff --git a/app/Models/User.php b/app/Models/User.php index ceb795b..e7b0c29 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -22,7 +22,6 @@ class User extends Authenticatable implements MustVerifyEmail * @var list */ protected $fillable = [ - 'name', 'email', 'password', 'username', diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index b2ce4bf..7ea80a7 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -24,7 +24,8 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), + 'first_name' => fake()->firstName(), + 'last_name' => fake()->lastName(), 'username' => fake()->unique()->userName(), 'email' => fake()->unique()->safeEmail(), 'first_name' => fake()->firstName(), diff --git a/database/migrations/2025_10_01_123600_remove_name_from_users_table.php b/database/migrations/2025_10_01_123600_remove_name_from_users_table.php new file mode 100644 index 0000000..f75d73a --- /dev/null +++ b/database/migrations/2025_10_01_123600_remove_name_from_users_table.php @@ -0,0 +1,28 @@ +dropColumn('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('name')->after('id'); + }); + } +}; \ No newline at end of file diff --git a/database/seeders/PackageSeeder.php b/database/seeders/PackageSeeder.php index 485d67e..dfb089f 100644 --- a/database/seeders/PackageSeeder.php +++ b/database/seeders/PackageSeeder.php @@ -17,7 +17,7 @@ class PackageSeeder extends Seeder // Endcustomer Packages Package::create([ 'name' => 'Free / Test', - 'slug' => Str::slug('Free / Test'), + 'slug' => 'free-package', 'type' => PackageType::ENDCUSTOMER, 'price' => 0.00, 'max_photos' => 30, diff --git a/database/seeders/SuperAdminSeeder.php b/database/seeders/SuperAdminSeeder.php index 24f2ac7..b07414a 100644 --- a/database/seeders/SuperAdminSeeder.php +++ b/database/seeders/SuperAdminSeeder.php @@ -13,7 +13,8 @@ class SuperAdminSeeder extends Seeder $email = env('ADMIN_EMAIL', 'admin@example.com'); $password = env('ADMIN_PASSWORD', 'ChangeMe123!'); User::updateOrCreate(['email'=>$email], [ - 'name' => 'Super Admin', + 'first_name' => 'Super', + 'last_name' => 'Admin', 'password' => Hash::make($password), 'role' => 'super_admin', ]); diff --git a/docs/prp/marketing-frontend-unification.md b/docs/prp/marketing-frontend-unification.md new file mode 100644 index 0000000..8cbe86b --- /dev/null +++ b/docs/prp/marketing-frontend-unification.md @@ -0,0 +1,69 @@ +# Vereinheitlichung des Marketing-Frontends: Von Hybrid (Blade + React) zu konsistentem Inertia/React-Layout + +## Problemstellung +Das aktuelle Marketing-Frontend kombiniert Blade-Templates (z.B. für statische Seiten wie Blog, Legal) mit Vite/React-Komponenten (z.B. Packages, Register). Bei rein React-gerenderten Seiten fehlt das Layout (Header, Footer) und das Styling (z.B. Aurora-Gradient, Fonts), was zu inkonsistentem UX führt: +- Blade-Seiten: Vollständiges Layout via @extends('layouts.marketing'). +- React-Seiten: Nur Komponente, kein Wrapper → Kein Header/Footer, anderes Styling. + +Ziel: Vollständige Migration zu Inertia.js für SPA-ähnliche Konsistenz, mit einem zentralen React-Layout für alle Marketing-Seiten. Vorteile: Einheitliches Design, bessere Navigation, einfachere Wartung. + +## Architektur-Vorschlag +### 1. Kernkomponenten +- **MarketingLayout.tsx** (resources/js/layouts/MarketingLayout.tsx): Wrapper für alle Marketing-Seiten. + - Header: Logo, Navigation (Home, Packages, Blog, Occasions, Register/Login). + - Hauptinhalt: `{children}` (die spezifische Page-Komponente). + - Footer: Impressum, Datenschutz, Social-Links, Copyright. + - Styling: Tailwind-Klassen für Aurora-Gradient (bg-aurora-enhanced), Fonts (Playfair Display für Überschriften, Montserrat für Text). +- **Globale Styles** (resources/css/app.css): + - @font-face für Montserrat und Playfair Display (via Google Fonts oder lokal). + - .bg-aurora-enhanced: radial-gradient(circle at 20% 80%, #a8edea 0%, #fed6e3 50%, #d299c2 100%) + linear-gradient + animation (shadcn-Style). + - Theme: Primärfarbe #FFB6C1, responsive (mobile-first). + +### 2. Routing & Controller +- **web.php** (routes/web.php): Alle /marketing/*-Routes zu Inertia::render umstellen. + - Beispiel: Route::inertia('/marketing/packages', 'Marketing/Packages'); +- **MarketingController.php** (app/Http/Controllers/MarketingController.php): + - Methoden (z.B. packages(), blog(), register()) liefern Props (z.B. packages: Package::all()->map(fn($p) => ['id' => $p->id, 'features' => $p->features, 'limits' => $p->limits])). + - Für dynamische Inhalte: DB-Queries (z.B. BlogPosts für /blog). + +### 3. Page-Komponenten +- Alle Marketing-Seiten als React/TSX (resources/js/pages/marketing/*.tsx): + - z.B. Packages.tsx: Rendert Paket-Karten in Grid/Carousel (shadcn), mit Modal für Details/Upsell. + - Wrapper: In App.tsx oder router.tsx: if (route.startsWith('/marketing')) return ; +- Migration-Reihenfolge: + 1. Statische Seiten (Home, Blog-Index): Von Blade zu Inertia. + 2. Dynamische (Packages, Register): Props integrieren. + 3. Legal-Seiten: Als einfache Inertia-Pages (statischer Text). + +### 4. Technische Umsetzung +- **Inertia-Setup**: Stelle sicher, config/inertia.php hat middleware für SSR (optional) und shared props (z.B. auth, flash). +- **Auth-Integration**: usePage().props.auth für CTAs (z.B. Login-Button im Header). +- **Responsive Design**: Tailwind: block md:hidden für Mobile-Carousel in Packages. +- **Fonts & Assets**: Vite-Plugin für Fonts/SVGs; preload in Layout. +- **Tests**: E2E mit Playwright (z.B. navigate to /packages, check header/footer presence). + +### 5. Diagramm: Layout-Struktur +``` +MarketingLayout.tsx +├── Header (Navigation, Logo) +├── {children} (z.B. Packages.tsx) +│ ├── Hero (Aurora-Gradient) +│ ├── Content (Grid/Carousel) +│ └── CTA-Section +└── Footer (Legal-Links) +``` + +### 6. Migrations-Schritte (für Code-Modus) +1. Erstelle MarketingLayout.tsx und integriere in Router. +2. Migriere eine Test-Seite (z.B. /packages): Controller + Page-Komponente. +3. Passe app.css an (Fonts, Gradients). +4. Test: npm run dev, Browser-Check auf Layout-Konsistenz. +5. Vollständige Migration: Alle Blade-Seiten umstellen. +6. Edge-Cases: SEO (Inertia Head), Performance (Lazy-Loading). + +### 7. Risiken & Mitigation +- Layout-Brüche während Migration: Fallback zu Blade via Feature-Flag. +- Styling-Konflikte: CSS-Isolation mit Tailwind-Prefix. +- Performance: Code-Splitting für große Pages. + +Dieser Plan basiert auf bestehender Struktur (docs/prp/ als Referenz). Nach Umsetzung: Update PRP (docs/prp/01-architecture.md). \ No newline at end of file diff --git a/docs/prp/packages-ui-improvements.md b/docs/prp/packages-ui-improvements.md new file mode 100644 index 0000000..c0d5653 --- /dev/null +++ b/docs/prp/packages-ui-improvements.md @@ -0,0 +1,41 @@ +# Packages-Seite UI-Verbesserungen: Analyse und Plan + +## Recherche-Zusammenfassung +### Features und Abgrenzungen (aus docs/prp/15-packages-design.md, Model/Seeder) +- **Endkunden-Pakete (Einmalkauf pro Event)**: + - Free/Test (0€): max_photos=30, max_guests=50, gallery_days=7, max_tasks=5, watermark_allowed=true, features=['basic_uploads', 'limited_sharing']. + - Starter (29€): max_photos=200, max_guests=100, gallery_days=30, max_tasks=10, watermark_allowed=true, features=['basic_uploads', 'unlimited_sharing', 'no_watermark', 'custom_tasks']. + - Pro (79€): max_photos=1000, max_guests=500, gallery_days=90, max_tasks=20, watermark_allowed=false, features=['basic_uploads', 'unlimited_sharing', 'no_watermark', 'custom_tasks', 'advanced_analytics', 'priority_support', 'live_slideshow']. +- **Reseller-Pakete (Subscription jährlich)**: + - S (199€): max_events_per_year=5, max_photos=500 (per Event), features=['reseller_dashboard', 'custom_branding', 'priority_support']. + - M (399€): max_events_per_year=15, max_photos=1000, features=['reseller_dashboard', 'custom_branding', 'priority_support', 'advanced_reporting']. + - Enterprise (999€+): Unlimited, White-Label, Custom Domain. +- **Limits**: max_photos, max_guests, gallery_days, max_tasks, max_events_per_year (vererbt an Events). +- **Abgrenzungen**: Watermark/Branding (bool), Support (priority), Analytics (advanced), Galerie-Dauer, Task-Anzahl. Filament: TenantResource trackt active_package, remaining_events; RelationManagers für Purchases/Packages (manual add). + +### Carousel-Umsetzung +- Ja, vollständig: shadcn Carousel für mobile (block md:hidden, Swipe mit Previous/Next Buttons, basis-full Items); Desktop: hidden md:block Grid (md:grid-cols-3/2). Dynamisch aus Props (endcustomerPackages, resellerPackages). + +### Umgesetzte UI-Verbesserungen +- Responsive Design: Mobile Carousel vs Desktop Grid. +- Dynamische Darstellung: Features map() aus JSON, partial Limits (max_photos, max_tenants). +- Hero-Section: Aurora-Gradient (bg-aurora-enhanced), Playfair Display Überschrift, Montserrat Text. +- Konsistentes Layout: MarketingLayout (Header mit Nav, Footer mit Legal-Links). +- CTA: Links zu /buy-packages/{id}, Hover-Transitions. +- Fonts: font-display (Playfair), font-sans-marketing (Montserrat). + +### Offene/Mögliche Verbesserungen +1. **Multi-Step-Modal auf Card-Click**: Dialog (shadcn) mit Tabs (Step 1: Details + Social Proof/Testimonials (3 Cards mit Stars); Step 2: Upsell-Tabelle (shadcn Table, Spalten: Features/Limits, Zeilen: alle Packages, Highlight selected mit bg-[#FFB6C1]); Step 3: CTA (usePage().props.auth ? Link /buy-packages : /register?package_id, localStorage pre-fill für Name/Email)). +2. **Erweiterte Limits-Darstellung**: Vollständig in Cards (gallery_days, max_guests, max_tasks als
  • , watermark/branding als Badge/Check/X-Icons). +3. **UI-Enhancements**: Progress Bar (33/66/100% für Steps), Micro-Interactions (Card-Hover: scale-105/shadow-lg), FAQ-Section (Accordion mit 4 Fragen: Free-Paket, Upgrade, Reseller, Zahlung), Testimonials-Section (3 Cards mit Quotes/Ratings). +4. **Desktop Pricing Table**: Toggle-Button neben Grid (View: Table-Modus, Vergleichs-View mit Checkmarks für Features). +5. **Weitere**: A/B-Testing (CTAs), Accessibility (ARIA-Labels für Carousel/Modal, Keyboard-Nav), SEO (Head meta description pro Package), Performance (Lazy Testimonials), Integration (Track Clicks mit Analytics). + +## Implementierungs-Plan (Code-Modus) +1. **Modal hinzufügen**: useState für open/selected/step; Dialog mit Tabs; Step 1: Details + Testimonials; Step 2: Table (alle Packages); Step 3: CTA (auth-check, pre-fill). +2. **Limits erweitern**: In Cards
  • für gallery_days/max_guests/max_tasks; Badges für watermark/branding. +3. **UI-Verbesserungen**: Progress in Modal, Hover auf Cards, FAQ-Accordion, Testimonials-Section. +4. **Pricing Table**: useState für viewMode (Grid/Table); Table mit Check/X für Features. +5. **Test**: npm run build/dev; Browser: Card-Click → Modal-Steps, Tabelle-Vergleich, CTA-Redirect, Responsiveness. + +Nach Umsetzung: Update PRP (docs/prp/15-packages-design.md mit UI-Details). \ No newline at end of file diff --git a/free-step1-home.png b/free-step1-home.png new file mode 100644 index 0000000..4085a4e Binary files /dev/null and b/free-step1-home.png differ diff --git a/free-step1-packages.png b/free-step1-packages.png new file mode 100644 index 0000000..a35f266 Binary files /dev/null and b/free-step1-packages.png differ diff --git a/free-step2-packages.png b/free-step2-packages.png new file mode 100644 index 0000000..908c76b Binary files /dev/null and b/free-step2-packages.png differ diff --git a/package-lock.json b/package-lock.json index 10a42ce..b2c34a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,19 @@ "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.1.0", "@playwright/mcp": "^0.0.37", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", @@ -29,6 +32,9 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", + "embla-carousel": "^8.6.0", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", "globals": "^15.14.0", "html5-qrcode": "^2.3.8", "laravel-vite-plugin": "^2.0", @@ -1860,6 +1866,37 @@ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -2023,6 +2060,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -2372,6 +2410,30 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -2470,6 +2532,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, @@ -2483,6 +2546,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", @@ -4821,6 +4914,43 @@ "integrity": "sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==", "license": "ISC" }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-autoplay": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz", + "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7198,6 +7328,7 @@ "version": "0.475.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } diff --git a/package.json b/package.json index 0327faf..5c0930d 100644 --- a/package.json +++ b/package.json @@ -29,16 +29,19 @@ "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.1.0", "@playwright/mcp": "^0.0.37", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", @@ -50,6 +53,9 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", + "embla-carousel": "^8.6.0", + "embla-carousel-autoplay": "^8.6.0", + "embla-carousel-react": "^8.6.0", "globals": "^15.14.0", "html5-qrcode": "^2.3.8", "laravel-vite-plugin": "^2.0", diff --git a/paid-end-step1-packages.png b/paid-end-step1-packages.png new file mode 100644 index 0000000..3df69a6 Binary files /dev/null and b/paid-end-step1-packages.png differ diff --git a/paid-res-step1-packages.png b/paid-res-step1-packages.png new file mode 100644 index 0000000..f209050 Binary files /dev/null and b/paid-res-step1-packages.png differ diff --git a/playwright-report/data/290c9bab29612768a345a71b0f76c3297c4d4235.png b/playwright-report/data/290c9bab29612768a345a71b0f76c3297c4d4235.png new file mode 100644 index 0000000..71f3d20 Binary files /dev/null and b/playwright-report/data/290c9bab29612768a345a71b0f76c3297c4d4235.png differ diff --git a/playwright-report/data/346c13d5b5efe55fbc31bac706e3819995351c5a.webm b/playwright-report/data/346c13d5b5efe55fbc31bac706e3819995351c5a.webm deleted file mode 100644 index 4fe4093..0000000 Binary files a/playwright-report/data/346c13d5b5efe55fbc31bac706e3819995351c5a.webm and /dev/null differ diff --git a/playwright-report/data/3f9362498fef5095ac418b481173c877d2003abf.md b/playwright-report/data/3f9362498fef5095ac418b481173c877d2003abf.md deleted file mode 100644 index 48fb67e..0000000 --- a/playwright-report/data/3f9362498fef5095ac418b481173c877d2003abf.md +++ /dev/null @@ -1,36 +0,0 @@ -# Page snapshot - -```yaml -- generic [ref=e4]: - - generic [ref=e5]: - - link [ref=e6] [cursor=pointer]: - - /url: https://laravel.com - - img [ref=e7] [cursor=pointer] - - img [ref=e9] - - link [ref=e11] [cursor=pointer]: - - /url: https://vitejs.dev - - img [ref=e12] [cursor=pointer] - - generic [ref=e15]: - - generic [ref=e16]: - - paragraph [ref=e17]: This is the Vite development server that provides Hot Module Replacement for your Laravel application. - - paragraph [ref=e18]: To access your Laravel application, you will need to run a local development server. - - heading "Artisan Serve" [level=2] [ref=e19]: - - link "Artisan Serve" [ref=e20] [cursor=pointer]: - - /url: https://laravel.com/docs/installation#your-first-laravel-project - - paragraph [ref=e21]: Laravel's local development server powered by PHP's built-in web server. - - heading "Laravel Sail" [level=2] [ref=e22]: - - link "Laravel Sail" [ref=e23] [cursor=pointer]: - - /url: https://laravel.com/docs/sail - - paragraph [ref=e24]: A light-weight command-line interface for interacting with Laravel's default Docker development environment. - - generic [ref=e25]: - - paragraph [ref=e26]: - - text: Your Laravel application's configured - - code [ref=e27]: APP_URL - - text: "is:" - - link "http://localhost:8000" [ref=e28] [cursor=pointer]: - - /url: http://localhost:8000 - - paragraph [ref=e29]: Want more information on Laravel's Vite integration? - - paragraph [ref=e30]: - - link "Read the docs →" [ref=e31] [cursor=pointer]: - - /url: https://laravel.com/docs/vite -``` \ No newline at end of file diff --git a/playwright-report/data/506cdad3bc3afbd43b412d67b67fe89fe9e3717e.webm b/playwright-report/data/506cdad3bc3afbd43b412d67b67fe89fe9e3717e.webm new file mode 100644 index 0000000..e5f6677 Binary files /dev/null and b/playwright-report/data/506cdad3bc3afbd43b412d67b67fe89fe9e3717e.webm differ diff --git a/playwright-report/data/5fabb6bee539a448c2e28d185949bad5209a3e06.png b/playwright-report/data/5fabb6bee539a448c2e28d185949bad5209a3e06.png new file mode 100644 index 0000000..06f1a61 Binary files /dev/null and b/playwright-report/data/5fabb6bee539a448c2e28d185949bad5209a3e06.png differ diff --git a/playwright-report/data/768a83adbd7d74bdff644209f594e4f1a6604a5b.md b/playwright-report/data/768a83adbd7d74bdff644209f594e4f1a6604a5b.md new file mode 100644 index 0000000..e68e36a --- /dev/null +++ b/playwright-report/data/768a83adbd7d74bdff644209f594e4f1a6604a5b.md @@ -0,0 +1,274 @@ +# Page snapshot + +```yaml +- generic: + - generic: + - generic: + - banner: + - generic: + - generic: + - link: + - /url: / + - text: Die Fotospiel.App + - img + - navigation: + - link: + - /url: /#how-it-works + - text: So funktioniert es + - link: + - /url: /#features + - text: Features + - generic: + - button: Anlässe + - link: + - /url: /blog + - text: Blog + - link: + - /url: /packages + - text: Packages + - link: + - /url: /kontakt + - text: Kontakt + - link: + - /url: /packages + - text: Packages entdecken + - main: + - generic: + - generic: + - heading [level=1]: Unsere Packages + - paragraph: Wählen Sie das passende Paket für Ihr Event – von kostenlos bis premium. + - link: + - /url: "#endcustomer" + - text: Jetzt entdecken + - generic: + - generic: + - heading [level=2]: Für Endkunden + - generic: + - generic: + - generic: + - generic: + - img + - heading [level=3]: Free / Test + - paragraph: 0.00 € + - list: + - listitem: • Events + - listitem: • Max. 30 Fotos + - listitem: • Galerie 7 Tage + - listitem: • Max. 50 Gäste + - button: Details anzeigen + - generic: + - generic: + - img + - heading [level=3]: Starter + - paragraph: 29.00 € + - list: + - listitem: • Events + - listitem: • Max. 200 Fotos + - listitem: • Galerie 30 Tage + - listitem: • Max. 100 Gäste + - button: Details anzeigen + - generic: + - generic: + - img + - heading [level=3]: Pro + - paragraph: 79.00 € + - list: + - listitem: • Events + - listitem: • Max. 1000 Fotos + - listitem: • Galerie 90 Tage + - listitem: • Max. 500 Gäste + - listitem: + - generic: Kein Watermark + - button: Details anzeigen + - generic: + - heading [level=3]: Endkunden-Pakete vergleichen + - generic: + - generic: + - table: + - rowgroup: + - row: + - cell: Feature + - cell: Free / Test + - cell: Starter + - cell: Pro + - rowgroup: + - row: + - cell: Preis + - cell: 0.00 € + - cell: 29.00 € + - cell: 79.00 € + - row: + - cell: + - text: Max. Fotos + - img + - cell: "30" + - cell: "200" + - cell: "1000" + - row: + - cell: + - text: Max. Gäste + - img + - cell: "50" + - cell: "100" + - cell: "500" + - row: + - cell: + - text: Galerie Tage + - img + - cell: "7" + - cell: "30" + - cell: "90" + - row: + - cell: + - text: Watermark + - img + - cell: + - img + - cell: + - img + - cell: + - img + - generic: + - generic: + - heading [level=2]: Für Reseller + - generic: + - generic: + - generic: + - generic: + - img + - heading [level=3]: S (Small Reseller) + - paragraph: 199.00 € / Jahr + - list: + - listitem: + - generic: Custom Branding + - button: Details anzeigen + - generic: + - generic: + - img + - heading [level=3]: M (Medium Reseller) + - paragraph: 399.00 € / Jahr + - list: + - listitem: + - generic: Custom Branding + - button: Details anzeigen + - generic: + - generic: + - heading [level=2]: Häufige Fragen + - generic: + - generic: + - heading [level=3]: Was ist das Free-Paket? + - paragraph: "Ideal für Tests: 30 Fotos, 7 Tage Galerie, mit Watermark." + - generic: + - heading [level=3]: Kann ich upgraden? + - paragraph: Ja, jederzeit im Dashboard – Limits werden sofort erweitert. + - generic: + - heading [level=3]: Was für Reseller? + - paragraph: Jährliche Subscriptions mit Dashboard, Branding und Support. + - generic: + - heading [level=3]: Zahlungssicher? + - paragraph: Sichere Zahlung via Stripe/PayPal, 14 Tage Rückgaberecht. + - generic: + - generic: + - heading [level=2]: Was unsere Kunden sagen + - generic: + - generic: + - paragraph: "\"Das Starter-Paket war perfekt für unsere Hochzeit – einfach und günstig!\"" + - generic: + - generic: + - img + - img + - img + - img + - img + - paragraph: Anna M. + - generic: + - paragraph: "\"Pro-Paket mit Analytics hat uns geholfen, die besten Momente zu finden.\"" + - generic: + - generic: + - img + - img + - img + - img + - img + - paragraph: Max B. + - generic: + - paragraph: "\"Als Reseller spare ich Zeit mit dem M-Paket – super Support!\"" + - generic: + - generic: + - img + - img + - img + - img + - img + - paragraph: Lisa K. + - contentinfo: + - generic: + - paragraph: © 2025 Fotospiel GmbH. Alle Rechte vorbehalten. + - generic: + - link: + - /url: /impressum + - text: Impressum + - link: + - /url: /datenschutz + - text: Datenschutz + - link: + - /url: /kontakt + - text: Kontakt + - dialog "Starter - Details" [ref=e2]: + - heading "Starter - Details" [level=2] [ref=e4] + - generic [ref=e5]: + - tablist [ref=e6]: + - tab "Details" [active] [selected] [ref=e7] + - tab "Kaufen" [ref=e8] + - progressbar [ref=e9] + - tabpanel "Details" [ref=e11]: + - generic [ref=e12]: + - generic [ref=e13]: + - heading "Starter" [level=2] [ref=e14] + - paragraph [ref=e15]: 29.00 € + - paragraph + - generic [ref=e16]: + - generic [ref=e17]: + - img + - text: Max. 200 Fotos + - generic [ref=e18]: + - img + - text: Max. 100 Gäste + - generic [ref=e19]: + - img + - text: 30 Tage Galerie + - generic [ref=e20]: + - heading "Was Kunden sagen" [level=3] [ref=e21] + - generic [ref=e22]: + - generic [ref=e23]: + - paragraph [ref=e24]: "\"Das Starter-Paket war perfekt für unsere Hochzeit – einfach und günstig!\"" + - paragraph [ref=e25]: Anna M. + - generic [ref=e26]: + - img [ref=e27] + - img [ref=e29] + - img [ref=e31] + - img [ref=e33] + - img [ref=e35] + - generic [ref=e37]: + - paragraph [ref=e38]: "\"Pro-Paket mit Analytics hat uns geholfen, die besten Momente zu finden.\"" + - paragraph [ref=e39]: Max B. + - generic [ref=e40]: + - img [ref=e41] + - img [ref=e43] + - img [ref=e45] + - img [ref=e47] + - img [ref=e49] + - generic [ref=e51]: + - paragraph [ref=e52]: "\"Als Reseller spare ich Zeit mit dem M-Paket – super Support!\"" + - paragraph [ref=e53]: Lisa K. + - generic [ref=e54]: + - img [ref=e55] + - img [ref=e57] + - img [ref=e59] + - img [ref=e61] + - img [ref=e63] + - button "Zum Kauf" [ref=e65] + - button "Close" [ref=e66]: + - img + - generic [ref=e67]: Close +``` \ No newline at end of file diff --git a/playwright-report/data/85e55eb23cc05de7d445e0361a6021c61dae8afe.webm b/playwright-report/data/85e55eb23cc05de7d445e0361a6021c61dae8afe.webm deleted file mode 100644 index ce133f9..0000000 Binary files a/playwright-report/data/85e55eb23cc05de7d445e0361a6021c61dae8afe.webm and /dev/null differ diff --git a/playwright-report/data/a7a68315c466417d2dc68cd27f7f4045569bdd81.md b/playwright-report/data/a7a68315c466417d2dc68cd27f7f4045569bdd81.md new file mode 100644 index 0000000..7a7ec23 --- /dev/null +++ b/playwright-report/data/a7a68315c466417d2dc68cd27f7f4045569bdd81.md @@ -0,0 +1,274 @@ +# Page snapshot + +```yaml +- generic: + - generic: + - generic: + - banner: + - generic: + - generic: + - link: + - /url: / + - text: Die Fotospiel.App + - img + - navigation: + - link: + - /url: /#how-it-works + - text: So funktioniert es + - link: + - /url: /#features + - text: Features + - generic: + - button: Anlässe + - link: + - /url: /blog + - text: Blog + - link: + - /url: /packages + - text: Packages + - link: + - /url: /kontakt + - text: Kontakt + - link: + - /url: /packages + - text: Packages entdecken + - main: + - generic: + - generic: + - heading [level=1]: Unsere Packages + - paragraph: Wählen Sie das passende Paket für Ihr Event – von kostenlos bis premium. + - link: + - /url: "#endcustomer" + - text: Jetzt entdecken + - generic: + - generic: + - heading [level=2]: Für Endkunden + - generic: + - generic: + - generic: + - generic: + - img + - heading [level=3]: Free / Test + - paragraph: 0.00 € + - list: + - listitem: • Events + - listitem: • Max. 30 Fotos + - listitem: • Galerie 7 Tage + - listitem: • Max. 50 Gäste + - button: Details anzeigen + - generic: + - generic: + - img + - heading [level=3]: Starter + - paragraph: 29.00 € + - list: + - listitem: • Events + - listitem: • Max. 200 Fotos + - listitem: • Galerie 30 Tage + - listitem: • Max. 100 Gäste + - button: Details anzeigen + - generic: + - generic: + - img + - heading [level=3]: Pro + - paragraph: 79.00 € + - list: + - listitem: • Events + - listitem: • Max. 1000 Fotos + - listitem: • Galerie 90 Tage + - listitem: • Max. 500 Gäste + - listitem: + - generic: Kein Watermark + - button: Details anzeigen + - generic: + - heading [level=3]: Endkunden-Pakete vergleichen + - generic: + - generic: + - table: + - rowgroup: + - row: + - cell: Feature + - cell: Free / Test + - cell: Starter + - cell: Pro + - rowgroup: + - row: + - cell: Preis + - cell: 0.00 € + - cell: 29.00 € + - cell: 79.00 € + - row: + - cell: + - text: Max. Fotos + - img + - cell: "30" + - cell: "200" + - cell: "1000" + - row: + - cell: + - text: Max. Gäste + - img + - cell: "50" + - cell: "100" + - cell: "500" + - row: + - cell: + - text: Galerie Tage + - img + - cell: "7" + - cell: "30" + - cell: "90" + - row: + - cell: + - text: Watermark + - img + - cell: + - img + - cell: + - img + - cell: + - img + - generic: + - generic: + - heading [level=2]: Für Reseller + - generic: + - generic: + - generic: + - generic: + - img + - heading [level=3]: S (Small Reseller) + - paragraph: 199.00 € / Jahr + - list: + - listitem: + - generic: Custom Branding + - button: Details anzeigen + - generic: + - generic: + - img + - heading [level=3]: M (Medium Reseller) + - paragraph: 399.00 € / Jahr + - list: + - listitem: + - generic: Custom Branding + - button: Details anzeigen + - generic: + - generic: + - heading [level=2]: Häufige Fragen + - generic: + - generic: + - heading [level=3]: Was ist das Free-Paket? + - paragraph: "Ideal für Tests: 30 Fotos, 7 Tage Galerie, mit Watermark." + - generic: + - heading [level=3]: Kann ich upgraden? + - paragraph: Ja, jederzeit im Dashboard – Limits werden sofort erweitert. + - generic: + - heading [level=3]: Was für Reseller? + - paragraph: Jährliche Subscriptions mit Dashboard, Branding und Support. + - generic: + - heading [level=3]: Zahlungssicher? + - paragraph: Sichere Zahlung via Stripe/PayPal, 14 Tage Rückgaberecht. + - generic: + - generic: + - heading [level=2]: Was unsere Kunden sagen + - generic: + - generic: + - paragraph: "\"Das Starter-Paket war perfekt für unsere Hochzeit – einfach und günstig!\"" + - generic: + - generic: + - img + - img + - img + - img + - img + - paragraph: Anna M. + - generic: + - paragraph: "\"Pro-Paket mit Analytics hat uns geholfen, die besten Momente zu finden.\"" + - generic: + - generic: + - img + - img + - img + - img + - img + - paragraph: Max B. + - generic: + - paragraph: "\"Als Reseller spare ich Zeit mit dem M-Paket – super Support!\"" + - generic: + - generic: + - img + - img + - img + - img + - img + - paragraph: Lisa K. + - contentinfo: + - generic: + - paragraph: © 2025 Fotospiel GmbH. Alle Rechte vorbehalten. + - generic: + - link: + - /url: /impressum + - text: Impressum + - link: + - /url: /datenschutz + - text: Datenschutz + - link: + - /url: /kontakt + - text: Kontakt + - dialog "Free / Test - Details" [ref=e2]: + - heading "Free / Test - Details" [level=2] [ref=e4] + - generic [ref=e5]: + - tablist [ref=e6]: + - tab "Details" [active] [selected] [ref=e7] + - tab "Kaufen" [ref=e8] + - progressbar [ref=e9] + - tabpanel "Details" [ref=e11]: + - generic [ref=e12]: + - generic [ref=e13]: + - heading "Free / Test" [level=2] [ref=e14] + - paragraph [ref=e15]: 0.00 € + - paragraph + - generic [ref=e16]: + - generic [ref=e17]: + - img + - text: Max. 30 Fotos + - generic [ref=e18]: + - img + - text: Max. 50 Gäste + - generic [ref=e19]: + - img + - text: 7 Tage Galerie + - generic [ref=e20]: + - heading "Was Kunden sagen" [level=3] [ref=e21] + - generic [ref=e22]: + - generic [ref=e23]: + - paragraph [ref=e24]: "\"Das Starter-Paket war perfekt für unsere Hochzeit – einfach und günstig!\"" + - paragraph [ref=e25]: Anna M. + - generic [ref=e26]: + - img [ref=e27] + - img [ref=e29] + - img [ref=e31] + - img [ref=e33] + - img [ref=e35] + - generic [ref=e37]: + - paragraph [ref=e38]: "\"Pro-Paket mit Analytics hat uns geholfen, die besten Momente zu finden.\"" + - paragraph [ref=e39]: Max B. + - generic [ref=e40]: + - img [ref=e41] + - img [ref=e43] + - img [ref=e45] + - img [ref=e47] + - img [ref=e49] + - generic [ref=e51]: + - paragraph [ref=e52]: "\"Als Reseller spare ich Zeit mit dem M-Paket – super Support!\"" + - paragraph [ref=e53]: Lisa K. + - generic [ref=e54]: + - img [ref=e55] + - img [ref=e57] + - img [ref=e59] + - img [ref=e61] + - img [ref=e63] + - button "Zum Kauf" [ref=e65] + - button "Close" [ref=e66]: + - img + - generic [ref=e67]: Close +``` \ No newline at end of file diff --git a/playwright-report/data/ab8955fe4c74a2c8835d79a54de138dea7be6493.png b/playwright-report/data/ab8955fe4c74a2c8835d79a54de138dea7be6493.png deleted file mode 100644 index a665ba0..0000000 Binary files a/playwright-report/data/ab8955fe4c74a2c8835d79a54de138dea7be6493.png and /dev/null differ diff --git a/playwright-report/data/bd9be2eae434aef8a78a9b1876e71a4a2a825617.webm b/playwright-report/data/bd9be2eae434aef8a78a9b1876e71a4a2a825617.webm new file mode 100644 index 0000000..5b8621c Binary files /dev/null and b/playwright-report/data/bd9be2eae434aef8a78a9b1876e71a4a2a825617.webm differ diff --git a/playwright-report/index.html b/playwright-report/index.html index 463556d..8129984 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -73,4 +73,4 @@ Error generating stack: `+u.message+`
    - \ No newline at end of file + \ No newline at end of file diff --git a/public/fonts/GreatVibes-Regular.ttf b/public/fonts/GreatVibes-Regular.ttf new file mode 100644 index 0000000..acc5d37 Binary files /dev/null and b/public/fonts/GreatVibes-Regular.ttf differ diff --git a/public/fonts/Lora-Bold.ttf b/public/fonts/Lora-Bold.ttf new file mode 100644 index 0000000..edae21e Binary files /dev/null and b/public/fonts/Lora-Bold.ttf differ diff --git a/public/fonts/Lora-BoldItalic.ttf b/public/fonts/Lora-BoldItalic.ttf new file mode 100644 index 0000000..12dea8c Binary files /dev/null and b/public/fonts/Lora-BoldItalic.ttf differ diff --git a/public/fonts/Lora-Italic.ttf b/public/fonts/Lora-Italic.ttf new file mode 100644 index 0000000..e24b69b Binary files /dev/null and b/public/fonts/Lora-Italic.ttf differ diff --git a/public/fonts/Lora-Medium.ttf b/public/fonts/Lora-Medium.ttf new file mode 100644 index 0000000..46a0c1c Binary files /dev/null and b/public/fonts/Lora-Medium.ttf differ diff --git a/public/fonts/Lora-MediumItalic.ttf b/public/fonts/Lora-MediumItalic.ttf new file mode 100644 index 0000000..286c311 Binary files /dev/null and b/public/fonts/Lora-MediumItalic.ttf differ diff --git a/public/fonts/Lora-Regular.ttf b/public/fonts/Lora-Regular.ttf new file mode 100644 index 0000000..dc751db Binary files /dev/null and b/public/fonts/Lora-Regular.ttf differ diff --git a/public/fonts/Lora-SemiBold.ttf b/public/fonts/Lora-SemiBold.ttf new file mode 100644 index 0000000..f5d5fd3 Binary files /dev/null and b/public/fonts/Lora-SemiBold.ttf differ diff --git a/public/fonts/Lora-SemiBoldItalic.ttf b/public/fonts/Lora-SemiBoldItalic.ttf new file mode 100644 index 0000000..57f9e48 Binary files /dev/null and b/public/fonts/Lora-SemiBoldItalic.ttf differ diff --git a/public/fonts/Montserrat-Black.ttf b/public/fonts/Montserrat-Black.ttf new file mode 100644 index 0000000..2fab7ab Binary files /dev/null and b/public/fonts/Montserrat-Black.ttf differ diff --git a/public/fonts/Montserrat-BlackItalic.ttf b/public/fonts/Montserrat-BlackItalic.ttf new file mode 100644 index 0000000..04d3b47 Binary files /dev/null and b/public/fonts/Montserrat-BlackItalic.ttf differ diff --git a/public/fonts/Montserrat-Bold.ttf b/public/fonts/Montserrat-Bold.ttf new file mode 100644 index 0000000..4033587 Binary files /dev/null and b/public/fonts/Montserrat-Bold.ttf differ diff --git a/public/fonts/Montserrat-BoldItalic.ttf b/public/fonts/Montserrat-BoldItalic.ttf new file mode 100644 index 0000000..0cc5c2c Binary files /dev/null and b/public/fonts/Montserrat-BoldItalic.ttf differ diff --git a/public/fonts/Montserrat-ExtraBold.ttf b/public/fonts/Montserrat-ExtraBold.ttf new file mode 100644 index 0000000..476ec30 Binary files /dev/null and b/public/fonts/Montserrat-ExtraBold.ttf differ diff --git a/public/fonts/Montserrat-ExtraBoldItalic.ttf b/public/fonts/Montserrat-ExtraBoldItalic.ttf new file mode 100644 index 0000000..a1ac9a9 Binary files /dev/null and b/public/fonts/Montserrat-ExtraBoldItalic.ttf differ diff --git a/public/fonts/Montserrat-ExtraLight.ttf b/public/fonts/Montserrat-ExtraLight.ttf new file mode 100644 index 0000000..efaeab0 Binary files /dev/null and b/public/fonts/Montserrat-ExtraLight.ttf differ diff --git a/public/fonts/Montserrat-ExtraLightItalic.ttf b/public/fonts/Montserrat-ExtraLightItalic.ttf new file mode 100644 index 0000000..a8d18de Binary files /dev/null and b/public/fonts/Montserrat-ExtraLightItalic.ttf differ diff --git a/public/fonts/Montserrat-Italic.ttf b/public/fonts/Montserrat-Italic.ttf new file mode 100644 index 0000000..5f08df0 Binary files /dev/null and b/public/fonts/Montserrat-Italic.ttf differ diff --git a/public/fonts/Montserrat-Light.ttf b/public/fonts/Montserrat-Light.ttf new file mode 100644 index 0000000..881f12d Binary files /dev/null and b/public/fonts/Montserrat-Light.ttf differ diff --git a/public/fonts/Montserrat-LightItalic.ttf b/public/fonts/Montserrat-LightItalic.ttf new file mode 100644 index 0000000..b2991d0 Binary files /dev/null and b/public/fonts/Montserrat-LightItalic.ttf differ diff --git a/public/fonts/Montserrat-Medium.ttf b/public/fonts/Montserrat-Medium.ttf new file mode 100644 index 0000000..c9a39ea Binary files /dev/null and b/public/fonts/Montserrat-Medium.ttf differ diff --git a/public/fonts/Montserrat-MediumItalic.ttf b/public/fonts/Montserrat-MediumItalic.ttf new file mode 100644 index 0000000..086dd6e Binary files /dev/null and b/public/fonts/Montserrat-MediumItalic.ttf differ diff --git a/public/fonts/Montserrat-Regular.ttf b/public/fonts/Montserrat-Regular.ttf new file mode 100644 index 0000000..895e220 Binary files /dev/null and b/public/fonts/Montserrat-Regular.ttf differ diff --git a/public/fonts/Montserrat-SemiBold.ttf b/public/fonts/Montserrat-SemiBold.ttf new file mode 100644 index 0000000..161477a Binary files /dev/null and b/public/fonts/Montserrat-SemiBold.ttf differ diff --git a/public/fonts/Montserrat-SemiBoldItalic.ttf b/public/fonts/Montserrat-SemiBoldItalic.ttf new file mode 100644 index 0000000..73dc6c6 Binary files /dev/null and b/public/fonts/Montserrat-SemiBoldItalic.ttf differ diff --git a/public/fonts/Montserrat-Thin.ttf b/public/fonts/Montserrat-Thin.ttf new file mode 100644 index 0000000..c9cf195 Binary files /dev/null and b/public/fonts/Montserrat-Thin.ttf differ diff --git a/public/fonts/Montserrat-ThinItalic.ttf b/public/fonts/Montserrat-ThinItalic.ttf new file mode 100644 index 0000000..e6dfc05 Binary files /dev/null and b/public/fonts/Montserrat-ThinItalic.ttf differ diff --git a/public/fonts/PlayfairDisplay-Black.ttf b/public/fonts/PlayfairDisplay-Black.ttf new file mode 100644 index 0000000..2b53a15 Binary files /dev/null and b/public/fonts/PlayfairDisplay-Black.ttf differ diff --git a/public/fonts/PlayfairDisplay-BlackItalic.ttf b/public/fonts/PlayfairDisplay-BlackItalic.ttf new file mode 100644 index 0000000..7963352 Binary files /dev/null and b/public/fonts/PlayfairDisplay-BlackItalic.ttf differ diff --git a/public/fonts/PlayfairDisplay-Bold.ttf b/public/fonts/PlayfairDisplay-Bold.ttf new file mode 100644 index 0000000..86bfcfa Binary files /dev/null and b/public/fonts/PlayfairDisplay-Bold.ttf differ diff --git a/public/fonts/PlayfairDisplay-BoldItalic.ttf b/public/fonts/PlayfairDisplay-BoldItalic.ttf new file mode 100644 index 0000000..82e9ca0 Binary files /dev/null and b/public/fonts/PlayfairDisplay-BoldItalic.ttf differ diff --git a/public/fonts/PlayfairDisplay-ExtraBold.ttf b/public/fonts/PlayfairDisplay-ExtraBold.ttf new file mode 100644 index 0000000..a16e13c Binary files /dev/null and b/public/fonts/PlayfairDisplay-ExtraBold.ttf differ diff --git a/public/fonts/PlayfairDisplay-ExtraBoldItalic.ttf b/public/fonts/PlayfairDisplay-ExtraBoldItalic.ttf new file mode 100644 index 0000000..3b95568 Binary files /dev/null and b/public/fonts/PlayfairDisplay-ExtraBoldItalic.ttf differ diff --git a/public/fonts/PlayfairDisplay-Italic.ttf b/public/fonts/PlayfairDisplay-Italic.ttf new file mode 100644 index 0000000..520b9db Binary files /dev/null and b/public/fonts/PlayfairDisplay-Italic.ttf differ diff --git a/public/fonts/PlayfairDisplay-Medium.ttf b/public/fonts/PlayfairDisplay-Medium.ttf new file mode 100644 index 0000000..42f7c02 Binary files /dev/null and b/public/fonts/PlayfairDisplay-Medium.ttf differ diff --git a/public/fonts/PlayfairDisplay-MediumItalic.ttf b/public/fonts/PlayfairDisplay-MediumItalic.ttf new file mode 100644 index 0000000..055af62 Binary files /dev/null and b/public/fonts/PlayfairDisplay-MediumItalic.ttf differ diff --git a/public/fonts/PlayfairDisplay-Regular.ttf b/public/fonts/PlayfairDisplay-Regular.ttf new file mode 100644 index 0000000..2cd12a3 Binary files /dev/null and b/public/fonts/PlayfairDisplay-Regular.ttf differ diff --git a/public/fonts/PlayfairDisplay-SemiBold.ttf b/public/fonts/PlayfairDisplay-SemiBold.ttf new file mode 100644 index 0000000..a1a0401 Binary files /dev/null and b/public/fonts/PlayfairDisplay-SemiBold.ttf differ diff --git a/public/fonts/PlayfairDisplay-SemiBoldItalic.ttf b/public/fonts/PlayfairDisplay-SemiBoldItalic.ttf new file mode 100644 index 0000000..9fd9153 Binary files /dev/null and b/public/fonts/PlayfairDisplay-SemiBoldItalic.ttf differ diff --git a/resources/css/app.css b/resources/css/app.css index 2e23049..756a69f 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -10,6 +10,10 @@ @theme { --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --font-display: 'Playfair Display', serif; + --font-serif: 'Lora', serif; + --font-sans-marketing: 'Montserrat', sans-serif; + --font-script: 'Great Vibes', cursive; --radius-lg: var(--radius); --radius-md: calc(var(--radius) - 2px); @@ -59,6 +63,77 @@ --color-sidebar-ring: var(--sidebar-ring); } +@font-face { + font-family: 'Montserrat'; + src: url('/fonts/Montserrat-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Montserrat'; + src: url('/fonts/Montserrat-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Lora'; + src: url('/fonts/Lora-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Lora'; + src: url('/fonts/Lora-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Playfair Display'; + src: url('/fonts/PlayfairDisplay-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Playfair Display'; + src: url('/fonts/PlayfairDisplay-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Great Vibes'; + src: url('/fonts/GreatVibes-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@layer utilities { + .font-display { + font-family: var(--font-display); + } + .font-serif-custom { + font-family: var(--font-serif); + } + .font-sans-marketing { + font-family: var(--font-sans-marketing); + } + .font-script { + font-family: var(--font-script); + } +} + :root { --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); @@ -174,3 +249,14 @@ @apply bg-background text-foreground; } } + +@keyframes aurora { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +.bg-aurora-enhanced { + background: radial-gradient(circle at 20% 80%, #a8edea 0%, #fed6e3 50%, #d299c2 100%), linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); + background-size: 400% 400%, 400% 400%; + animation: aurora 20s ease infinite; +} diff --git a/resources/js/components/marketing/MarketingFooter.tsx b/resources/js/components/marketing/MarketingFooter.tsx new file mode 100644 index 0000000..0366a99 --- /dev/null +++ b/resources/js/components/marketing/MarketingFooter.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Link } from '@inertiajs/react'; + +const MarketingFooter: React.FC = () => { + return ( +
    +
    +

    © 2025 Fotospiel GmbH. Alle Rechte vorbehalten.

    +
    + + Impressum + + + Datenschutz + + + Kontakt + +
    +
    +
    + ); +}; + +export default MarketingFooter; \ No newline at end of file diff --git a/resources/js/components/marketing/MarketingHeader.tsx b/resources/js/components/marketing/MarketingHeader.tsx new file mode 100644 index 0000000..1700819 --- /dev/null +++ b/resources/js/components/marketing/MarketingHeader.tsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import { Link } from '@inertiajs/react'; +import { usePage } from '@inertiajs/react'; + +const MarketingHeader: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const { url } = usePage(); + + const occasions = [ + { href: '/de/occasions/weddings', label: 'Hochzeiten' }, + { href: '/de/occasions/birthdays', label: 'Geburtstage' }, + { href: '/de/occasions/corporate-events', label: 'Firmenevents' }, + { href: '/de/occasions/family-celebrations', label: 'Familienfeiern' }, + ]; + + return ( +
    +
    +
    + + Die Fotospiel.App + + + + + +
    + + +
    +
    + ); +}; + +export default MarketingHeader; \ No newline at end of file diff --git a/resources/js/components/ui/accordion.tsx b/resources/js/components/ui/accordion.tsx new file mode 100644 index 0000000..e1797c9 --- /dev/null +++ b/resources/js/components/ui/accordion.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
    {children}
    +
    +)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/resources/js/components/ui/carousel.tsx b/resources/js/components/ui/carousel.tsx new file mode 100644 index 0000000..eae73f7 --- /dev/null +++ b/resources/js/components/ui/carousel.tsx @@ -0,0 +1,144 @@ +"use client" + +import * as React from "react" +import { ArrowLeft, ArrowRight } from "lucide-react" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +import Autoplay from "embla-carousel-autoplay" +import useEmblaCarousel from "embla-carousel-react" + +interface CarouselApi { + slideNodes(): HTMLElement[] + on(event: string, listener: (...args: any[]) => void): void + scrollPrev(): void + scrollNext(): void + reInit(): void +} + +const CarouselContext = React.createContext(null) + +interface CarouselProps { + opts?: { + align?: "start" | "center" | "end" + loop?: boolean + } + plugins?: any[] + setApi?: (api: CarouselApi) => void + [key: string]: any +} + +const Carousel = React.forwardRef( + ({ opts, plugins = [Autoplay()], setApi, className, children, ...props }, ref) => { + const [api, setApiInternal] = React.useState(null) + const [current, setCurrent] = React.useState(0) + const [count, setCount] = React.useState(0) + + const [emblaRef] = useEmblaCarousel(opts, plugins) + + React.useEffect(() => { + if (!api) { + return + } + + setCount(api.slideNodes().length) + api.on("reInit", setCount) + api.on("slideChanged", ({ slide }: { slide: number }) => setCurrent(slide)) + setApi?.(api) + }, [api, setApi]) + + return ( + +
    +
    +
    {children}
    +
    +
    +
    + ) + } +) +Carousel.displayName = "Carousel" + +interface CarouselContentProps { + children: React.ReactNode + className?: string +} + +const CarouselContent = React.forwardRef( + ({ children, className }, ref) => ( +
    + {children} +
    + ) +) +CarouselContent.displayName = "CarouselContent" + +interface CarouselItemProps { + children: React.ReactNode + className?: string +} + +const CarouselItem = React.forwardRef( + ({ children, className }, ref) => ( +
    + {children} +
    + ) +) +CarouselItem.displayName = "CarouselItem" + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CarouselPrevious.displayName = "CarouselPrevious" + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +CarouselNext.displayName = "CarouselNext" + +export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext } \ No newline at end of file diff --git a/resources/js/components/ui/progress.tsx b/resources/js/components/ui/progress.tsx new file mode 100644 index 0000000..3fd47ad --- /dev/null +++ b/resources/js/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/resources/js/components/ui/table.tsx b/resources/js/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/resources/js/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    + + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
    [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/resources/js/components/ui/tabs.tsx b/resources/js/components/ui/tabs.tsx new file mode 100644 index 0000000..85d83be --- /dev/null +++ b/resources/js/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/resources/js/layouts/MarketingLayout.tsx b/resources/js/layouts/MarketingLayout.tsx new file mode 100644 index 0000000..10c8321 --- /dev/null +++ b/resources/js/layouts/MarketingLayout.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { Link } from '@inertiajs/react'; +import { usePage } from '@inertiajs/react'; + +interface MarketingLayoutProps { + children: React.ReactNode; + title?: string; +} + +const MarketingLayout: React.FC = ({ children, title }) => { + const { props } = usePage(); + const { auth } = props as any; + + return ( +
    + {/* Header */} +
    +
    +
    + {/* Logo */} + + FotoSpiel + + + {/* Navigation */} + + + {/* Mobile Menu Button – TODO: Implementiere Dropdown */} +
    + +
    +
    +
    +
    + + {/* Main Content */} +
    + {title && ( + {title} + )} + {children} +
    + + {/* Footer */} +
    +
    +
    +
    + + FotoSpiel + +

    + Deine Plattform für Event-Fotos. +

    +
    + +
    +

    Rechtliches

    +
      +
    • Impressum
    • +
    • Datenschutz
    • +
    • AGB
    • +
    • Kontakt
    • +
    +
    + +
    +

    Social

    + +
    +
    + +
    + © 2025 FotoSpiel. Alle Rechte vorbehalten. +
    +
    +
    +
    + ); +}; + +export default MarketingLayout; \ No newline at end of file diff --git a/resources/js/layouts/marketing/MarketingLayout.tsx b/resources/js/layouts/marketing/MarketingLayout.tsx new file mode 100644 index 0000000..139b9e5 --- /dev/null +++ b/resources/js/layouts/marketing/MarketingLayout.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode } from 'react'; +import { Head } from '@inertiajs/react'; +import MarketingHeader from '@/components/marketing/MarketingHeader'; +import MarketingFooter from '@/components/marketing/MarketingFooter'; + +interface MarketingLayoutProps { + children: ReactNode; + title?: string; + description?: string; +} + +const MarketingLayout: React.FC = ({ + children, + title = 'Fotospiel - Event-Fotos einfach und sicher mit QR-Codes', + description = 'Sammle Gastfotos für Events mit QR-Codes und unserer PWA-Plattform. Für Hochzeiten, Firmenevents und mehr. Kostenloser Einstieg.' +}) => { + return ( + <> + + {title} + + + +
    + +
    + {children} +
    + +
    + + ); +}; + +export default MarketingLayout; \ No newline at end of file diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index c86c8cb..b18e7ee 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -25,14 +25,28 @@ export default function Login({ status, canResetPassword }: LoginProps) { const submit = (e: React.FormEvent) => { e.preventDefault(); - post('/login'); + post('/login', { + preserveState: true, + onSuccess: () => { + console.log('Login successful'); + }, + onError: (errors: Record) => { + console.log('Login errors:', errors); + }, + }); }; + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [errors]); + return ( -
    +
    @@ -48,7 +62,7 @@ export default function Login({ status, canResetPassword }: LoginProps) { value={data.email} onChange={(e) => setData('email', e.target.value)} /> - +
    @@ -71,7 +85,7 @@ export default function Login({ status, canResetPassword }: LoginProps) { value={data.password} onChange={(e) => setData('password', e.target.value)} /> - +
    @@ -100,6 +114,14 @@ export default function Login({ status, canResetPassword }: LoginProps) { {status &&
    {status}
    } + + {Object.keys(errors).length > 0 && ( +
    +

    + {Object.values(errors).join(' ')} +

    +
    + )} ); } diff --git a/resources/js/pages/auth/register.tsx b/resources/js/pages/auth/register.tsx index eb0284e..d62d574 100644 --- a/resources/js/pages/auth/register.tsx +++ b/resources/js/pages/auth/register.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useForm, router } from '@inertiajs/react'; import { Head } from '@inertiajs/react'; -import { LoaderCircle } from 'lucide-react'; +import { LoaderCircle, User, Mail, Phone, Lock, Home, MapPin } from 'lucide-react'; +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; interface RegisterProps { package?: { @@ -10,11 +11,15 @@ interface RegisterProps { description: string; price: number; } | null; + privacyHtml: string; } -export default function Register({ package: initialPackage }: RegisterProps) { - const { data, setData, post, processing, errors } = useForm({ - name: '', +import MarketingLayout from '@/layouts/marketing/MarketingLayout'; + +export default function Register({ package: initialPackage, privacyHtml }: RegisterProps) { + const [privacyOpen, setPrivacyOpen] = useState(false); + + const { data, setData, post, processing, errors, setError } = useForm({ username: '', email: '', password: '', @@ -27,225 +32,295 @@ export default function Register({ package: initialPackage }: RegisterProps) { package_id: initialPackage?.id || null, }); + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + console.log('Validation errors received:', errors); + } + if (!processing) { + console.log('Registration processing completed'); + } + }, [errors, processing, data]); + + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [errors]); + + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + // Force re-render or scroll to errors + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [errors]); + const submit = (e: React.FormEvent) => { e.preventDefault(); - router.post('/register'); + console.log('Submitting registration form with data:', data); + router.post('/register', data, { + preserveState: true, + forceFormData: true, + onSuccess: () => { + console.log('Registration successful'); + }, + onError: (errors) => { + console.log('Registration errors:', errors); + setError(errors); + }, + }); + console.log('POST to /register initiated'); }; return ( -
    - -
    -
    -

    - Registrieren -

    - {initialPackage && ( -
    -

    {initialPackage.name}

    -

    {initialPackage.description}

    -

    - {initialPackage.price === 0 ? 'Kostenlos' : `${initialPackage.price} €`} + +

    +
    +
    +
    +

    + Willkommen bei Fotospiel – Erstellen Sie Ihren Account +

    +

    + Registrierung ermöglicht Zugriff auf Events, Galerien und personalisierte Features. +

    + {initialPackage && ( +
    +

    {initialPackage.name}

    +

    {initialPackage.description}

    +

    + {initialPackage.price === 0 ? 'Kostenlos' : `${initialPackage.price} €`} +

    +
    + )} +
    +
    +
    +
    + +
    + + setData('first_name', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.first_name ? 'border-red-500' : 'border-gray-300'}`} + placeholder="Vorname" + /> +
    + {errors.first_name &&

    {errors.first_name}

    } +
    + +
    + +
    + + setData('last_name', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.last_name ? 'border-red-500' : 'border-gray-300'}`} + placeholder="Nachname" + /> +
    + {errors.last_name &&

    {errors.last_name}

    } +
    + +
    + +
    + + setData('email', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.email ? 'border-red-500' : 'border-gray-300'}`} + placeholder="email@example.com" + /> +
    + {errors.email &&

    {errors.email}

    } +
    + +
    + +
    + + setData('address', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.address ? 'border-red-500' : 'border-gray-300'}`} + placeholder="Adresse" + /> +
    + {errors.address &&

    {errors.address}

    } +
    + +
    + +
    + + setData('phone', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.phone ? 'border-red-500' : 'border-gray-300'}`} + placeholder="Telefonnummer" + /> +
    + {errors.phone &&

    {errors.phone}

    } +
    + +
    + +
    + + setData('username', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.username ? 'border-red-500' : 'border-gray-300'}`} + placeholder="Benutzername" + /> +
    + {errors.username &&

    {errors.username}

    } +
    + +
    + +
    + + setData('password', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.password ? 'border-red-500' : 'border-gray-300'}`} + placeholder="Passwort" + /> +
    + {errors.password &&

    {errors.password}

    } +
    + +
    + +
    + + setData('password_confirmation', e.target.value)} + className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.password_confirmation ? 'border-red-500' : 'border-gray-300'}`} + placeholder="Passwort bestätigen" + /> +
    + {errors.password_confirmation &&

    {errors.password_confirmation}

    } +
    + +
    + setData('privacy_consent', e.target.checked)} + className="h-4 w-4 text-[#FFB6C1] focus:ring-[#FFB6C1] border-gray-300 rounded" + /> + + {errors.privacy_consent &&

    {errors.privacy_consent}

    } +
    +
    + + {Object.keys(errors).length > 0 && ( +
    +

    + {Object.entries(errors).map(([key, value]) => ( + + {value} + + ))} +

    +
    + )} + + + +
    +

    + Bereits registriert?{' '} + + Anmelden +

    - )} +
    -
    -
    -
    - - setData('name', e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="Vollständiger Name" - /> - {errors.name &&

    {errors.name}

    } -
    - -
    - - setData('username', e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="Benutzername" - /> - {errors.username &&

    {errors.username}

    } -
    - -
    - - setData('email', e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="email@example.com" - /> - {errors.email &&

    {errors.email}

    } -
    - -
    - - setData('first_name', e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="Vorname" - /> - {errors.first_name &&

    {errors.first_name}

    } -
    - -
    - - setData('last_name', e.target.value)} - className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="Nachname" - /> - {errors.last_name &&

    {errors.last_name}

    } -
    - -
    - - + {errors.message &&

    {errors.message}

    } +
    + + + {Object.keys(errors).length === 0 && data.message && !processing && ( +

    Nachricht gesendet!

    + )} + + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [errors]); +
    + + + {/* Testimonials Section */} +
    +
    +

    Was unsere Kunden sagen

    +
    +
    +

    "Perfekt für unsere Hochzeit! QR-Sharing war super einfach."

    +

    - Anna & Max

    +
    +
    +

    "Großes Firmenevent – alle Fotos zentral via QR."

    +

    - Team XYZ GmbH

    +
    +
    +
    +
    + + {/* FAQ Section */} +
    +
    +

    Häufige Fragen

    +
    +
    +

    Ist es kostenlos?

    +

    Ja, der Basic-Tarif ist kostenlos für 1 Event mit QR. Upgrades ab 99€.

    +
    +
    +

    Datenschutz?

    +

    100% GDPR-konform. Keine personenbezogenen Daten gespeichert. QR-Zugriffe anonym. Siehe Datenschutzerklärung.

    +
    +
    +

    Wie funktioniert QR-Sharing?

    +

    Generiere QR im Dashboard, teile es – Gäste scannen, laden Fotos hoch in der PWA.

    +
    +
    +
    +
    + + {/* Packages Section (aus aktuellem TSX, angepasst) */} +
    +
    +

    Unsere Pakete

    +
    + {packages.map((pkg) => ( +
    +

    {pkg.name}

    +

    {pkg.description}

    +

    €{pkg.price}

    + + Kaufen + +
    + ))} +
    +
    +
    + + ); +}; + +export default Home; \ No newline at end of file diff --git a/resources/js/pages/marketing/Kontakt.tsx b/resources/js/pages/marketing/Kontakt.tsx new file mode 100644 index 0000000..19457e1 --- /dev/null +++ b/resources/js/pages/marketing/Kontakt.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { Head, Link, useForm, usePage } from '@inertiajs/react'; +import MarketingLayout from '@/layouts/marketing/MarketingLayout'; + +const Kontakt: React.FC = () => { + const { data, setData, post, processing, errors, reset } = useForm({ + name: '', + email: '', + message: '', + }); + + const { flash } = usePage().props as any; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post('/kontakt', { + onSuccess: () => reset(), + }); + }; + + return ( + + +
    +
    +

    Kontakt

    +

    Haben Sie Fragen? Schreiben Sie uns!

    +
    +
    + + setData('name', e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1]" + /> + {errors.name &&

    {errors.name}

    } +
    +
    + + setData('email', e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1]" + /> + {errors.email &&

    {errors.email}

    } +
    +
    + + + {errors.message &&

    {errors.message}

    } +
    + +
    + {flash?.success &&

    {flash.success}

    } + {Object.keys(errors).length > 0 && ( +
    +
      + {Object.values(errors).map((error, index) => ( +
    • {error}
    • + ))} +
    +
    + )} + + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [errors]); +
    + Zurück zur Startseite +
    +
    +
    +
    + ); +}; + +export default Kontakt; \ No newline at end of file diff --git a/resources/js/pages/marketing/Occasions.tsx b/resources/js/pages/marketing/Occasions.tsx new file mode 100644 index 0000000..c2e8c33 --- /dev/null +++ b/resources/js/pages/marketing/Occasions.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Head, Link } from '@inertiajs/react'; +import MarketingLayout from '@/layouts/marketing/MarketingLayout'; + +interface Props { + type: string; +} + +const Occasions: React.FC = ({ type }) => { + const occasions = { + weddings: { + title: 'Hochzeiten', + description: 'Erfangen Sie die magischen Momente Ihrer Hochzeit mit professionellen Fotos.', + features: ['Unbegrenzte Fotos', 'Sofort-Download', 'Privat-Event-Code', 'Emotionen tracken'], + image: '/images/wedding-lights-background.svg' // Platzhalter + }, + birthdays: { + title: 'Geburtstage', + description: 'Feiern Sie Geburtstage unvergesslich mit unseren Event-Foto-Lösungen.', + features: ['Schnelle Einrichtung', 'Gäste teilen Fotos', 'Themen-Filter', 'Druck-Optionen'], + image: '/images/birthday-placeholder.jpg' + }, + 'corporate-events': { + title: 'Firmenevents', + description: 'Professionelle Fotos für Teamevents, Konferenzen und Unternehmensfeiern.', + features: ['Branding-Integration', 'Sichere Cloud-Speicher', 'Analytics & Reports', 'Schnelle Bearbeitung'], + image: '/images/corporate-placeholder.jpg' + }, + 'family-celebrations': { + title: 'Familienfeiern', + description: 'Erinnerungen an Taufen, Jubiläen und Familienzusammenkünfte festhalten.', + features: ['Persönliche Alben', 'Gemeinsame Zugriffe', 'Einfache Bedienung', 'Hohe Qualität'], + image: '/images/family-placeholder.jpg' + } + }; + + const occasion = occasions[type as keyof typeof occasions] || occasions.weddings; + + return ( + + + {/* Hero Section */} +
    +
    +

    {occasion.title}

    +

    {occasion.description}

    + {occasion.image && ( + {occasion.title} + )} +
    +
    + + {/* Features Section */} +
    +
    +

    Warum Fotospiel für {occasion.title}?

    +
    + {occasion.features.map((feature, index) => ( +
    +
    + +
    +

    {feature}

    +
    + ))} +
    +
    + + Passendes Paket wählen + +
    +
    +
    +
    + ); +}; + +export default Occasions; \ No newline at end of file diff --git a/resources/js/pages/marketing/Packages.tsx b/resources/js/pages/marketing/Packages.tsx new file mode 100644 index 0000000..c9aee8d --- /dev/null +++ b/resources/js/pages/marketing/Packages.tsx @@ -0,0 +1,553 @@ +import React, { useState } from 'react'; +import { Head, Link, usePage } from '@inertiajs/react'; +import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { Button } from "@/components/ui/button" +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" +import MarketingLayout from '@/layouts/marketing/MarketingLayout'; +import { ArrowRight, ShoppingCart, Check, X, Users, Image, Calendar, Shield, Star } from 'lucide-react'; + +interface Package { + id: number; + name: string; + description: string; + price: number; + events: number; + features: string[]; + limits?: { + max_photos?: number; + max_guests?: number; + max_tenants?: number; + max_events?: number; + gallery_days?: number; + }; + watermark_allowed?: boolean; + branding_allowed?: boolean; +} + +interface PackagesProps { + endcustomerPackages: Package[]; + resellerPackages: Package[]; +} + +const Packages: React.FC = ({ endcustomerPackages, resellerPackages }) => { + const [open, setOpen] = useState(false); + const [selectedPackage, setSelectedPackage] = useState(null); + const [currentStep, setCurrentStep] = useState('step1'); + const { props } = usePage(); + const { auth } = props as any; + + const testimonials = [ + { name: 'Anna M.', text: 'Das Starter-Paket war perfekt für unsere Hochzeit – einfach und günstig!', rating: 5 }, + { name: 'Max B.', text: 'Pro-Paket mit Analytics hat uns geholfen, die besten Momente zu finden.', rating: 5 }, + { name: 'Lisa K.', text: 'Als Reseller spare ich Zeit mit dem M-Paket – super Support!', rating: 5 }, + ]; + + const allPackages = [...endcustomerPackages, ...resellerPackages]; + + const handleCardClick = (pkg: Package) => { + setSelectedPackage(pkg); + setCurrentStep('step1'); + setOpen(true); + }; + + const nextStep = () => { + if (currentStep === 'step1') setCurrentStep('step3'); + }; + + const getFeatureIcon = (feature: string) => { + switch (feature) { + case 'basic_uploads': return ; + case 'unlimited_sharing': return ; + case 'no_watermark': return ; + case 'custom_tasks': return ; + case 'advanced_analytics': return ; + case 'priority_support': return ; + case 'reseller_dashboard': return ; + case 'custom_branding': return ; + default: return ; + } + }; + + return ( + + {/* Hero Section */} +
    +
    +

    Unsere Packages

    +

    Wählen Sie das passende Paket für Ihr Event – von kostenlos bis premium.

    + + Jetzt entdecken + +
    +
    + +
    +
    +

    Für Endkunden

    + + {/* Mobile Carousel for Endcustomer Packages */} +
    + + + {endcustomerPackages.map((pkg) => ( + +
    +
    + +
    +

    {pkg.name}

    +

    {pkg.description}

    +

    + {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`} +

    +
      +
    • • {pkg.events} Events
    • + {pkg.features.map((feature, index) => ( +
    • + {getFeatureIcon(feature)} {feature} +
    • + ))} + {pkg.limits?.max_photos &&
    • • Max. {pkg.limits.max_photos} Fotos
    • } + {pkg.limits?.gallery_days &&
    • • Galerie {pkg.limits.gallery_days} Tage
    • } + {pkg.limits?.max_guests &&
    • • Max. {pkg.limits.max_guests} Gäste
    • } + {pkg.watermark_allowed === false &&
    • Kein Watermark
    • } + {pkg.branding_allowed &&
    • Custom Branding
    • } +
    + +
    +
    + ))} +
    + + +
    +
    + + {/* Desktop Grid for Endcustomer Packages */} +
    +
    + {endcustomerPackages.map((pkg) => ( +
    +
    + +
    +

    {pkg.name}

    +

    {pkg.description}

    +

    + {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`} +

    +
      +
    • • {pkg.events} Events
    • + {pkg.features.map((feature, index) => ( +
    • + {getFeatureIcon(feature)} {feature} +
    • + ))} + {pkg.limits?.max_photos &&
    • • Max. {pkg.limits.max_photos} Fotos
    • } + {pkg.limits?.gallery_days &&
    • • Galerie {pkg.limits.gallery_days} Tage
    • } + {pkg.limits?.max_guests &&
    • • Max. {pkg.limits.max_guests} Gäste
    • } + {pkg.watermark_allowed === false &&
    • Kein Watermark
    • } + {pkg.branding_allowed &&
    • Custom Branding
    • } +
    + +
    + ))} +
    +
    +
    + + {/* Comparison Section for Endcustomer */} +
    +

    Endkunden-Pakete vergleichen

    +
    + + + Preis + +
    + {endcustomerPackages.map((pkg) => ( +
    +

    {pkg.name}

    +

    {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`}

    +
    + ))} +
    +
    +
    + + Max. Fotos {getFeatureIcon('max_photos')} + +
    + {endcustomerPackages.map((pkg) => ( +
    +

    {pkg.name}

    +

    {pkg.limits?.max_photos || 'Unbegrenzt'}

    +
    + ))} +
    +
    +
    + + Max. Gäste {getFeatureIcon('max_guests')} + +
    + {endcustomerPackages.map((pkg) => ( +
    +

    {pkg.name}

    +

    {pkg.limits?.max_guests || 'Unbegrenzt'}

    +
    + ))} +
    +
    +
    + + Galerie Tage {getFeatureIcon('gallery_days')} + +
    + {endcustomerPackages.map((pkg) => ( +
    +

    {pkg.name}

    +

    {pkg.limits?.gallery_days || 'Unbegrenzt'}

    +
    + ))} +
    +
    +
    + + Watermark {getFeatureIcon('no_watermark')} + +
    + {endcustomerPackages.map((pkg) => ( +
    +

    {pkg.name}

    + {pkg.watermark_allowed === false ? : } +
    + ))} +
    +
    +
    +
    +
    +
    + + + + Feature + {endcustomerPackages.map((pkg) => ( + + {pkg.name} + + ))} + + + + + Preis + {endcustomerPackages.map((pkg) => ( + + {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`} + + ))} + + + Max. Fotos {getFeatureIcon('max_photos')} + {endcustomerPackages.map((pkg) => ( + + {pkg.limits?.max_photos || 'Unbegrenzt'} + + ))} + + + Max. Gäste {getFeatureIcon('max_guests')} + {endcustomerPackages.map((pkg) => ( + + {pkg.limits?.max_guests || 'Unbegrenzt'} + + ))} + + + Galerie Tage {getFeatureIcon('gallery_days')} + {endcustomerPackages.map((pkg) => ( + + {pkg.limits?.gallery_days || 'Unbegrenzt'} + + ))} + + + Watermark {getFeatureIcon('no_watermark')} + {endcustomerPackages.map((pkg) => ( + + {pkg.watermark_allowed === false ? : } + + ))} + + +
    +
    +
    +
    + +
    +
    +

    Für Reseller

    + + {/* Mobile Carousel for Reseller Packages */} +
    + + + {resellerPackages.map((pkg) => ( + +
    +
    + +
    +

    {pkg.name}

    +

    {pkg.description}

    +

    + {pkg.price} € / Jahr +

    +
      + {pkg.features.map((feature, index) => ( +
    • + {getFeatureIcon(feature)} {feature} +
    • + ))} + {pkg.limits?.max_tenants &&
    • • Max. {pkg.limits.max_tenants} Tenants
    • } + {pkg.limits?.max_events &&
    • • Max. {pkg.limits.max_events} Events/Jahr
    • } + {pkg.branding_allowed &&
    • Custom Branding
    • } +
    + +
    +
    + ))} +
    + + +
    +
    + + {/* Desktop Grid for Reseller Packages */} +
    +
    + {resellerPackages.map((pkg) => ( +
    +
    + +
    +

    {pkg.name}

    +

    {pkg.description}

    +

    + {pkg.price} € / Jahr +

    +
      + {pkg.features.map((feature, index) => ( +
    • + {getFeatureIcon(feature)} {feature} +
    • + ))} + {pkg.limits?.max_tenants &&
    • • Max. {pkg.limits.max_tenants} Tenants
    • } + {pkg.limits?.max_events &&
    • • Max. {pkg.limits.max_events} Events/Jahr
    • } + {pkg.branding_allowed &&
    • Custom Branding
    • } +
    + +
    + ))} +
    +
    +
    +
    + + {/* FAQ Section */} +
    +
    +

    Häufige Fragen

    +
    +
    +

    Was ist das Free-Paket?

    +

    Ideal für Tests: 30 Fotos, 7 Tage Galerie, mit Watermark.

    +
    +
    +

    Kann ich upgraden?

    +

    Ja, jederzeit im Dashboard – Limits werden sofort erweitert.

    +
    +
    +

    Was für Reseller?

    +

    Jährliche Subscriptions mit Dashboard, Branding und Support.

    +
    +
    +

    Zahlungssicher?

    +

    Sichere Zahlung via Stripe/PayPal, 14 Tage Rückgaberecht.

    +
    +
    +
    +
    + + {/* Modal */} + {selectedPackage && ( + + + + {selectedPackage.name} - Details + + + + Details + Kaufen + + + +
    +
    +

    {selectedPackage.name}

    +

    + {selectedPackage.price === 0 ? 'Kostenlos' : `${selectedPackage.price} €`} +

    +
    +

    {selectedPackage.description}

    +
    + {selectedPackage.features.map((feature, index) => ( + + {getFeatureIcon(feature)} {feature} + + ))} + {selectedPackage.limits?.max_photos && ( + + Max. {selectedPackage.limits.max_photos} Fotos + + )} + {selectedPackage.limits?.max_guests && ( + + Max. {selectedPackage.limits.max_guests} Gäste + + )} + {selectedPackage.limits?.gallery_days && ( + + {selectedPackage.limits.gallery_days} Tage Galerie + + )} + {selectedPackage.watermark_allowed === false && ( + + Kein Watermark + + )} + {selectedPackage.branding_allowed && ( + + Custom Branding + + )} +
    + {/* Social Proof - unten verschoben */} +
    +

    Was Kunden sagen

    +
    + {testimonials.map((testimonial, index) => ( +
    +

    "{testimonial.text}"

    +

    {testimonial.name}

    +
    + {[...Array(testimonial.rating)].map((_, i) => )} +
    +
    + ))} +
    +
    + +
    +
    + +

    Bereit zum Kaufen?

    +
    +

    Sie haben {selectedPackage.name} ausgewählt.

    + {auth.user ? ( + + Jetzt kaufen + + ) : ( + { + localStorage.setItem('preferred_package', JSON.stringify(selectedPackage)); + }} + > + Registrieren & Kaufen + + )} +
    + +
    +
    +
    +
    + )} + + {/* Testimonials Section */} +
    +
    +

    Was unsere Kunden sagen

    +
    + {testimonials.map((testimonial, index) => ( +
    +

    "{testimonial.text}"

    +
    +
    + {[...Array(testimonial.rating)].map((_, i) => )} +
    +

    {testimonial.name}

    +
    +
    + ))} +
    +
    +
    +
    + ); +}; + +export default Packages; \ No newline at end of file diff --git a/resources/js/pages/marketing/Success.tsx b/resources/js/pages/marketing/Success.tsx new file mode 100644 index 0000000..4d402dd --- /dev/null +++ b/resources/js/pages/marketing/Success.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { usePage, router } from '@inertiajs/react'; +import { Head } from '@inertiajs/react'; +import MarketingLayout from '@/layouts/marketing/MarketingLayout'; +import { Loader } from 'lucide-react'; + +const Success: React.FC = () => { + const { auth, flash } = usePage().props as any; + + if (auth.user && auth.user.email_verified_at) { + // Redirect to admin + router.visit('/admin', { preserveState: false }); + return ( +
    +
    + +

    Wird weitergeleitet...

    +
    +
    + ); + } + + if (auth.user && !auth.user.email_verified_at) { + return ( + +
    +
    +
    +

    + E-Mail verifizieren +

    +

    + Bitte überprüfen Sie Ihre E-Mail und klicken Sie auf den Verifizierungslink. +

    +
    + +
    +

    + Bereits registriert? Anmelden +

    +
    +
    +
    +
    + ); + } + + return ( + +
    +
    +
    +

    + Kauf abschließen +

    +

    + Melden Sie sich an, um fortzufahren. +

    + + Anmelden + +

    + Kein Konto? Registrieren +

    +
    +
    +
    +
    + ); +}; + +export default Success; \ No newline at end of file diff --git a/resources/js/pages/settings/profile.tsx b/resources/js/pages/settings/profile.tsx index a51f844..efa42ee 100644 --- a/resources/js/pages/settings/profile.tsx +++ b/resources/js/pages/settings/profile.tsx @@ -41,22 +41,6 @@ export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: > {({ processing, recentlySuccessful, errors }) => ( <> -
    - - - - - -
    -
    diff --git a/resources/views/layouts/marketing.blade.php b/resources/views/layouts/marketing.blade.php index af8d0d6..94cd84e 100644 --- a/resources/views/layouts/marketing.blade.php +++ b/resources/views/layouts/marketing.blade.php @@ -6,6 +6,7 @@ @yield('title', 'Fotospiel - Event-Fotos einfach und sicher mit QR-Codes') + @vite(['resources/css/app.css', 'resources/js/app.tsx'])