diff --git a/public/lang/de/auth.json b/public/lang/de/auth.json index 4423b91..cc575a5 100644 --- a/public/lang/de/auth.json +++ b/public/lang/de/auth.json @@ -23,6 +23,8 @@ "description": "Melde dich mit deinem Fotospiel-Zugang an und steuere deine Events zentral in einem Dashboard.", "brand": "Die Fotospiel App", "logo_alt": "Logo Die Fotospiel App", + "identifier": "E-Mail oder Username", + "identifier_placeholder": "z. B. name@beispiel.de oder hochzeit_julia", "username_or_email": "Username oder E-Mail", "email": "E-Mail-Adresse", "email_placeholder": "ihre@email.de", @@ -49,7 +51,16 @@ "address": "Adresse", "phone": "Telefonnummer", "privacy_consent": "Ich stimme der Datenschutzerklärung zu und akzeptiere die Verarbeitung meiner persönlichen Daten.", - "submit": "Registrieren" + "privacy_policy_link": "Datenschutzerklärung", + "submit": "Registrieren", + "first_name_placeholder": "Vorname", + "last_name_placeholder": "Nachname", + "email_placeholder": "beispiel@email.de", + "address_placeholder": "Straße Hausnummer, PLZ Ort", + "phone_placeholder": "+49 170 1234567", + "username_placeholder": "z. B. hochzeit_julia", + "password_placeholder": "Mindestens 8 Zeichen", + "password_confirmation_placeholder": "Passwort erneut eingeben" }, "verification": { "notice": "Bitte bestätigen Sie Ihre E-Mail-Adresse.", diff --git a/public/lang/de/marketing.json b/public/lang/de/marketing.json index e945577..5cb3119 100644 --- a/public/lang/de/marketing.json +++ b/public/lang/de/marketing.json @@ -98,10 +98,15 @@ "reseller_benefits": "Vorteile für Reseller", "unlimited_events": "Unbegrenzte Events", "custom_branding": "Benutzerdefiniertes Branding", + "available": "Verfügbar", + "not_available": "Nicht verfügbar", + "standard_support": "Standard-Support", "priority_support": "Priorisierter Support", "cancel_link": "Abo kündigen: :link", "hero_title": "Entdecken Sie unsere flexiblen Packages", "hero_description": "Von kostenlosem Einstieg bis Premium-Features: Passen Sie Ihr Event-Paket an Ihre Bedürfnisse an. Einfach, sicher und skalierbar.", + "hero_secondary": "Teste den kompletten Gäste-Flow in unserer Live-Demo – kein Login, kein App-Store.", + "cta_demo": "Demo ansehen", "cta_explore": "Pakete entdecken", "cta_explore_highlight": "Lieblingspaket sichern", "tab_endcustomer": "Endkunden", @@ -121,6 +126,7 @@ "register_buy": "Registrieren und kaufen", "register_subscribe": "Registrieren und abonnieren", "faq_title": "Häufige Fragen zu Packages", + "faq_lead": "Antworten auf die wichtigsten Fragen – mehr Details findest du im Guide „So funktioniert’s“.", "faq_q1": "Was ist ein Package?", "faq_a1": "Ein Package definiert Limits und Features für Ihr Event, z.B. Anzahl Fotos und Galerie-Dauer.", "faq_q2": "Kann ich upgraden?", @@ -171,6 +177,10 @@ "for_resellers": "Für Reseller", "details_show": "Details anzeigen", "comparison_title": "Packages vergleichen", + "comparison_subtitle": "Alle Limits und Features auf einen Blick.", + "comparison_hint": "Wähle deine Zielgruppe und scrolle durch die Karten. Die Tabelle zeigt genaue Unterschiede.", + "comparison_limits": "Limits", + "comparison_features": "Features", "max_photos_label": "Max. Fotos", "max_guests_label": "Max. Gäste", "gallery_days_label": "Galerie-Tage", @@ -388,6 +398,33 @@ "currency": { "euro": "€" }, + "coupon": { + "label": "Gutscheincode", + "placeholder": "Gutscheincode eingeben", + "apply": "Gutschein anwenden", + "remove": "Gutschein entfernen", + "applied": "Gutschein {{code}} aktiviert. Du sparst {{amount}}.", + "summary_title": "Aktualisierte Bestellsumme", + "fields": { + "subtotal": "Zwischensumme", + "discount": "Rabatt", + "tax": "MwSt.", + "total": "Gesamtsumme nach Rabatt" + }, + "errors": { + "required": "Bitte gib einen Gutscheincode ein.", + "not_found": "Dieser Gutschein konnte nicht gefunden werden.", + "inactive": "Dieser Gutschein ist nicht aktiv.", + "disabled": "Dieser Gutschein kann derzeit nicht eingelöst werden.", + "not_applicable": "Dieser Gutschein gilt nicht für das ausgewählte Package.", + "limit_reached": "Dieser Gutschein wurde bereits maximal genutzt.", + "currency_mismatch": "Dieser Gutschein passt nicht zur gewählten Währung.", + "not_synced": "Dieser Gutschein ist noch nicht bereit. Bitte versuche es später erneut.", + "package_not_configured": "Dieses Package unterstützt aktuell keine Gutscheine.", + "login_required": "Bitte melde dich an, um diesen Gutschein zu nutzen.", + "generic": "Der Gutschein konnte nicht angewendet werden. Bitte versuche einen anderen." + } + }, "meta": { "title": "Fotospiel - Sammle Gastfotos für Events mit QR-Codes", "description": "Sammle Gastfotos für Events mit QR-Codes. Unsere sichere PWA-Plattform für Gäste und Organisatoren – einfach, mobil und datenschutzkonform." @@ -436,9 +473,7 @@ "description": "Wähle das passende Paket für deine Bedürfnisse", "no_package_selected": "Kein Paket ausgewählt. Bitte wähle ein Paket aus der Paketübersicht.", "alternatives_title": "Alternative Pakete", - "no_alternatives": "Keine weiteren Pakete in dieser Kategorie verfügbar.", - "next_to_account": "Weiter zum Konto", - "loading": "Wird geladen..." + "no_alternatives": "Keine weiteren Pakete in dieser Kategorie verfügbar." }, "auth_step": { "title": "Konto", @@ -455,7 +490,9 @@ "google_error_title": "Google-Anmeldung fehlgeschlagen", "google_missing_package": "Bitte wähle zuerst ein Paket aus, bevor du Google Login nutzt.", "google_missing_email": "Wir konnten deine Google-E-Mail-Adresse nicht abrufen.", - "google_error_fallback": "Die Google-Anmeldung konnte nicht abgeschlossen werden. Bitte versuche es erneut." + "google_error_fallback": "Die Google-Anmeldung konnte nicht abgeschlossen werden. Bitte versuche es erneut.", + "google_helper": "Schneller Login über Google – deine Daten werden ausschließlich zur Kontoeinrichtung verwendet.", + "google_helper_badge": "Warum Google?" }, "payment_step": { "title": "Zahlung", @@ -467,6 +504,14 @@ "loading_payment": "Zahlungsdaten werden geladen...", "secure_payment_desc": "Sichere Zahlung über Paddle.", "paddle_intro": "Starte den Paddle-Checkout direkt hier im Wizard – ganz ohne Seitenwechsel.", + "guided_title": "Sichere Zahlung mit Paddle – unserem geprüften Partner", + "guided_body": "Wir führen dich Schritt für Schritt durch den Bezahlprozess. Paddle wickelt den Kauf als Merchant of Record ab und sorgt dafür, dass Steuern und Rechnungen automatisch korrekt erstellt werden.", + "paddle_partner": "Powered by Paddle", + "trust_secure": "Verschlüsselte Zahlung", + "trust_tax": "Automatische Steuerberechnung", + "trust_support": "Support in Minuten", + "guided_cta_hint": "Paddle wickelt deine Zahlung als Merchant of Record ab", + "toast_success": "Zahlung erfolgreich – wir bereiten alles vor.", "paddle_preparing": "Paddle-Checkout wird vorbereitet…", "paddle_overlay_ready": "Der Paddle-Checkout läuft jetzt in einem Overlay. Schließe die Zahlung dort ab und kehre anschließend hierher zurück.", "paddle_ready": "Paddle-Checkout wurde in einem neuen Tab geöffnet. Schließe die Zahlung dort ab und kehre dann hierher zurück.", @@ -511,6 +556,30 @@ "welcome": "Danke, dass du die Fotospiel App gewählt hast!", "package_summary": "Dein Paket {name} ist jetzt freigeschaltet. Du kannst sofort mit der Einrichtung loslegen.", "email_followup": "Wir haben dir gerade alle Details per E-Mail geschickt – inklusive Rechnung und den nächsten Schritten.", + "hero_badge": "Checkout abgeschlossen", + "hero_title": "Weiter geht's im Marketing-Dashboard", + "hero_body": "Wir haben deinen Zugang aktiviert und Paddle synchronisiert. Mit diesen Aufgaben startest du direkt durch.", + "hero_next": "Nutze den Button unten, um in deinen Kundenbereich zu wechseln – diese Übersicht kannst du jederzeit erneut öffnen.", + "onboarding_title": "Vorschau auf deine Onboarding-Schritte", + "onboarding_subtitle": "Diese Aufgaben erwarten dich direkt nach dem Login.", + "onboarding_badge": "Nächste Schritte", + "onboarding_items": { + "event": { + "title": "Erstes Event anlegen", + "body": "Titel, Datum und Highlights festlegen – alles bleibt anpassbar." + }, + "invites": { + "title": "QR-Einladungen aktivieren", + "body": "Teile deinen Event-QR-Code oder den Shortcut-Link mit Gästen." + }, + "tasks": { + "title": "Fotoaufgaben planen", + "body": "Nutze Vorlagen oder füge eigene kreative Aufgaben hinzu." + } + }, + "control_center_title": "Event Control Center (PWA)", + "control_center_body": "Alle Live-Aufgaben erledigst du später im Control Center – optimiert für Mobilgeräte und offlinefähig.", + "control_center_hint": "Installiere die PWA direkt aus dem Dashboard.", "package_activated": "Ihr Paket '{name}' ist aktiviert.", "email_sent": "Wir haben Ihnen eine Bestätigungs-E-Mail gesendet.", "open_profile": "Profil öffnen", diff --git a/public/lang/en/auth.json b/public/lang/en/auth.json index 4f6e271..288d0d0 100644 --- a/public/lang/en/auth.json +++ b/public/lang/en/auth.json @@ -23,6 +23,8 @@ "description": "Sign in with your Fotospiel account to manage every event in one place.", "brand": "Die Fotospiel App", "logo_alt": "Fotospiel App logo", + "identifier": "Email or Username", + "identifier_placeholder": "you@example.com or username", "username_or_email": "Username or Email", "email": "Email Address", "email_placeholder": "your@email.com", @@ -49,7 +51,16 @@ "address": "Address", "phone": "Phone Number", "privacy_consent": "I agree to the privacy policy and accept the processing of my personal data.", - "submit": "Register" + "privacy_policy_link": "Privacy Policy", + "submit": "Register", + "first_name_placeholder": "First name", + "last_name_placeholder": "Last name", + "email_placeholder": "you@example.com", + "address_placeholder": "Street, ZIP, City", + "phone_placeholder": "+1 555 123 4567", + "username_placeholder": "e.g. wedding_julia", + "password_placeholder": "At least 8 characters", + "password_confirmation_placeholder": "Repeat your password" }, "verification": { "notice": "Please verify your email address.", diff --git a/public/lang/en/marketing.json b/public/lang/en/marketing.json index 299943c..92e0964 100644 --- a/public/lang/en/marketing.json +++ b/public/lang/en/marketing.json @@ -92,6 +92,8 @@ "cancel_link": "Cancel Subscription: :link", "hero_title": "Discover our flexible Packages", "hero_description": "From free entry to premium features: Tailor your event package to your needs. Simple, secure, and scalable.", + "hero_secondary": "Experience the full guest flow in our live demo – no login, no install.", + "cta_demo": "View demo", "cta_explore": "Discover Packages", "cta_explore_highlight": "Explore top packages", "tab_endcustomer": "End Customers", @@ -111,6 +113,7 @@ "register_buy": "Register and Buy", "register_subscribe": "Register and Subscribe", "faq_title": "Frequently Asked Questions about Packages", + "faq_lead": "Quick answers to the essentials – check “How it works” for the full deep dive.", "faq_q1": "What is a Package?", "faq_a1": "A Package defines limits and features for your event, e.g. number of photos and gallery duration.", "faq_q2": "Can I upgrade?", @@ -161,6 +164,10 @@ "for_resellers": "For Resellers", "details_show": "Show Details", "comparison_title": "Compare Packages", + "comparison_subtitle": "Every limit and feature side by side.", + "comparison_hint": "Pick your audience and slide through the cards. The table surfaces the fine print.", + "comparison_limits": "Limits", + "comparison_features": "Features", "price": "Price", "max_photos_label": "Max. Photos", "max_guests_label": "Max. Guests", @@ -168,6 +175,9 @@ "watermark_label": "Watermark", "no_watermark": "No Watermark", "custom_branding": "Custom Branding", + "available": "Available", + "not_available": "Not available", + "standard_support": "Standard support", "max_tenants": "Max. Tenants", "max_events": "Max. Events/Year", "faq_free": "What is the Free Package?", @@ -382,6 +392,33 @@ "currency": { "euro": "€" }, + "coupon": { + "label": "Coupon code", + "placeholder": "Enter your coupon code", + "apply": "Apply coupon", + "remove": "Remove coupon", + "applied": "Coupon {{code}} applied. You save {{amount}}.", + "summary_title": "Updated order summary", + "fields": { + "subtotal": "Subtotal", + "discount": "Discount", + "tax": "Tax", + "total": "Total after discount" + }, + "errors": { + "required": "Please enter a coupon code.", + "not_found": "We could not find this coupon.", + "inactive": "This coupon is not active anymore.", + "disabled": "This coupon cannot be used at checkout.", + "not_applicable": "This coupon is not valid for the selected package.", + "limit_reached": "This coupon has already been used the maximum number of times.", + "currency_mismatch": "This coupon cannot be used with the selected currency.", + "not_synced": "This coupon is not ready yet. Please try again later.", + "package_not_configured": "This package is not configured for coupon redemption.", + "login_required": "Please log in to use this coupon.", + "generic": "We could not apply this coupon. Please try another one." + } + }, "meta": { "title": "Fotospiel - Collect Guest Photos for Events with QR Codes", "description": "Collect guest photos for events with QR codes. Our secure PWA platform for guests and organizers – simple, mobile, and privacy-compliant." @@ -430,9 +467,7 @@ "description": "Choose the right package for your needs", "no_package_selected": "No package selected. Please choose a package from the overview.", "alternatives_title": "Alternative Packages", - "no_alternatives": "No further packages in this category available.", - "next_to_account": "Next to Account", - "loading": "Loading..." + "no_alternatives": "No further packages in this category available." }, "auth_step": { "title": "Account", @@ -449,7 +484,9 @@ "google_error_title": "Google login failed", "google_missing_package": "Please choose a package before using Google login.", "google_missing_email": "We could not retrieve your Google email address.", - "google_error_fallback": "We couldn't complete the Google login. Please try again." + "google_error_fallback": "We couldn't complete the Google login. Please try again.", + "google_helper": "Sign in faster with Google – we only use your details to create your Fotospiel account.", + "google_helper_badge": "Why Google?" }, "payment_step": { "title": "Payment", @@ -461,6 +498,14 @@ "loading_payment": "Payment data is loading...", "secure_payment_desc": "Secure payment with Paddle.", "paddle_intro": "Launch the Paddle checkout right here in the wizard—no page changes required.", + "guided_title": "Secure checkout, powered by Paddle", + "guided_body": "We walk you through every step. Paddle acts as merchant of record, handles taxes automatically, and delivers compliant invoices instantly.", + "paddle_partner": "Powered by Paddle", + "trust_secure": "Encrypted payment", + "trust_tax": "Automatic tax handling", + "trust_support": "Live support within minutes", + "guided_cta_hint": "Securely processed by Paddle as Merchant of Record", + "toast_success": "Payment received – setting everything up for you.", "paddle_preparing": "Preparing Paddle checkout…", "paddle_overlay_ready": "Paddle checkout is running in a secure overlay. Complete the payment there and then continue here.", "paddle_ready": "Paddle checkout opened in a new tab. Complete the payment and then continue here.", @@ -505,6 +550,30 @@ "welcome": "Thank you for choosing the Fotospiel App!", "package_summary": "Your {name} package is now active. You're ready to get everything set up.", "email_followup": "We've just sent a confirmation email with your receipt and the next steps.", + "hero_badge": "Checkout complete", + "hero_title": "You're ready for the Marketing Dashboard", + "hero_body": "We activated your access and synced Paddle. Follow the checklist below to launch your first event.", + "hero_next": "Use the button below whenever you're ready to jump into your customer area—this summary is always available.", + "onboarding_title": "Preview your onboarding steps", + "onboarding_subtitle": "These are the first tasks you'll see after logging in.", + "onboarding_badge": "Next steps", + "onboarding_items": { + "event": { + "title": "Create your first event", + "body": "Set title, date, and highlights. You can adjust everything later." + }, + "invites": { + "title": "Activate QR invites", + "body": "Share your event QR code or shortcut link with guests." + }, + "tasks": { + "title": "Plan photo tasks", + "body": "Pick from the library or add your own creative prompts." + } + }, + "control_center_title": "Event Control Center (PWA)", + "control_center_body": "You handle live moderation and uploads in the Control Center — mobile-first and offline-ready.", + "control_center_hint": "Install the PWA directly from the dashboard.", "package_activated": "Your package '{name}' is activated.", "email_sent": "We have sent you a confirmation email.", "open_profile": "Open Profile", diff --git a/public/paddle.logo.svg b/public/paddle.logo.svg new file mode 100644 index 0000000..528fbee --- /dev/null +++ b/public/paddle.logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/js/admin/i18n/locales/de/dashboard.json b/resources/js/admin/i18n/locales/de/dashboard.json index 2083221..63e0927 100644 --- a/resources/js/admin/i18n/locales/de/dashboard.json +++ b/resources/js/admin/i18n/locales/de/dashboard.json @@ -97,6 +97,42 @@ "description": "Aktives Paket und Historie einsehen." } }, + "onboarding": { + "hero": { + "cta": "Setup fortsetzen" + }, + "card": { + "title": "Dein Start in fünf Schritten", + "description": "Bearbeite die Schritte in der Admin-App – das Dashboard zeigt dir den Status.", + "completed": "Alle Schritte abgeschlossen – großartig! Du kannst jederzeit zur Admin-App wechseln.", + "cta_fallback": "Jetzt starten" + }, + "admin_app": { + "title": "Admin-App öffnen", + "description": "Verwalte Events, Uploads und Gäste direkt in der Admin-App. Die mobile Oberfläche ist für Live-Einsätze optimiert.", + "cta": "Admin-App starten" + }, + "event_setup": { + "title": "Erstes Event vorbereiten", + "description": "Lege in der Admin-App Name, Datum und Aufgaben fest. So wissen Gäste, welche Fotos ihr euch wünscht.", + "cta": "Event anlegen" + }, + "invite_guests": { + "title": "Gäste einladen", + "description": "Teile QR-Codes oder Links, damit Gäste direkt mit dem Hochladen beginnen können.", + "cta": "QR-Links öffnen" + }, + "collect_photos": { + "title": "Erste Fotos einsammeln", + "description": "Sobald die ersten Uploads eintrudeln, erscheint alles in eurer Galerie. Moderation und Freigaben laufen in der Admin-App.", + "cta": "Uploads prüfen" + }, + "branding": { + "title": "Branding & Aufgaben verfeinern", + "description": "Passt Farbwelt und Aufgabenpakete an euren Anlass an – so fühlt sich alles wie aus einem Guss an.", + "cta": "Branding öffnen" + } + }, "limitsCard": { "title": "Kontingente & Laufzeiten", "description": "Fokus-Event: {{name}}", @@ -251,6 +287,42 @@ "planning": "In Planung", "noDate": "Kein Datum" } + }, + "onboarding": { + "hero": { + "cta": "Setup fortsetzen" + }, + "card": { + "title": "Dein Start in fünf Schritten", + "description": "Bearbeite die Schritte in der Admin-App – das Dashboard zeigt dir den Status.", + "completed": "Alle Schritte abgeschlossen – großartig! Du kannst jederzeit zur Admin-App wechseln.", + "cta_fallback": "Jetzt starten" + }, + "admin_app": { + "title": "Admin-App öffnen", + "description": "Verwalte Events, Uploads und Gäste direkt in der Admin-App. Die mobile Oberfläche ist für Live-Einsätze optimiert.", + "cta": "Admin-App starten" + }, + "event_setup": { + "title": "Erstes Event vorbereiten", + "description": "Lege in der Admin-App Name, Datum und Aufgaben fest. So wissen Gäste, welche Fotos ihr euch wünscht.", + "cta": "Event anlegen" + }, + "invite_guests": { + "title": "Gäste einladen", + "description": "Teile QR-Codes oder Links, damit Gäste direkt mit dem Hochladen beginnen können.", + "cta": "QR-Links öffnen" + }, + "collect_photos": { + "title": "Erste Fotos einsammeln", + "description": "Sobald die ersten Uploads eintrudeln, erscheint alles in eurer Galerie. Moderation und Freigaben laufen in der Admin-App.", + "cta": "Uploads prüfen" + }, + "branding": { + "title": "Branding & Aufgaben verfeinern", + "description": "Passt Farbwelt und Aufgabenpakete an euren Anlass an – so fühlt sich alles wie aus einem Guss an.", + "cta": "Branding öffnen" + } } } } diff --git a/resources/js/admin/i18n/locales/en/dashboard.json b/resources/js/admin/i18n/locales/en/dashboard.json index 546f937..38ebed8 100644 --- a/resources/js/admin/i18n/locales/en/dashboard.json +++ b/resources/js/admin/i18n/locales/en/dashboard.json @@ -97,6 +97,42 @@ "description": "View your active package and history." } }, + "onboarding": { + "hero": { + "cta": "Continue setup" + }, + "card": { + "title": "Your start in five steps", + "description": "Complete the steps inside the Admin App — the dashboard keeps track of your status.", + "completed": "All steps finished — fantastic! You can switch to the Admin App at any time.", + "cta_fallback": "Start now" + }, + "admin_app": { + "title": "Open the Admin App", + "description": "Manage events, uploads, and guests inside the Admin App. The mobile interface is optimised for live operations.", + "cta": "Launch Admin App" + }, + "event_setup": { + "title": "Prepare first event", + "description": "Define name, date, and tasks inside the Admin App so guests know which photos you expect.", + "cta": "Create event" + }, + "invite_guests": { + "title": "Invite guests", + "description": "Share QR codes or links so guests can start uploading instantly.", + "cta": "Open QR links" + }, + "collect_photos": { + "title": "Collect first photos", + "description": "As soon as uploads arrive, they show up in your gallery. Moderation happens inside the Admin App.", + "cta": "Review uploads" + }, + "branding": { + "title": "Fine-tune branding & tasks", + "description": "Adjust colours and task bundles to match your occasion — everything feels tailor-made.", + "cta": "Open branding" + } + }, "limitsCard": { "title": "Limits & gallery status", "description": "Focus event: {{name}}", @@ -251,6 +287,42 @@ "planning": "In planning", "noDate": "No date" } + }, + "onboarding": { + "hero": { + "cta": "Continue setup" + }, + "card": { + "title": "Your start in five steps", + "description": "Complete the steps inside the Admin App — the dashboard keeps track of your status.", + "completed": "All steps finished — fantastic! You can switch to the Admin App at any time.", + "cta_fallback": "Start now" + }, + "admin_app": { + "title": "Open the Admin App", + "description": "Manage events, uploads, and guests inside the Admin App. The mobile interface is optimised for live operations.", + "cta": "Launch Admin App" + }, + "event_setup": { + "title": "Prepare first event", + "description": "Define name, date, and tasks inside the Admin App so guests know which photos you expect.", + "cta": "Create event" + }, + "invite_guests": { + "title": "Invite guests", + "description": "Share QR codes or links so guests can start uploading instantly.", + "cta": "Open QR links" + }, + "collect_photos": { + "title": "Collect first photos", + "description": "As soon as uploads arrive, they show up in your gallery. Moderation happens inside the Admin App.", + "cta": "Review uploads" + }, + "branding": { + "title": "Fine-tune branding & tasks", + "description": "Adjust colours and task bundles to match your occasion — everything feels tailor-made.", + "cta": "Open branding" + } } } } diff --git a/resources/js/admin/lib/navigation.ts b/resources/js/admin/lib/navigation.ts new file mode 100644 index 0000000..bbf2682 --- /dev/null +++ b/resources/js/admin/lib/navigation.ts @@ -0,0 +1,3 @@ +export function navigateToHref(target: string): void { + window.location.assign(target); +} diff --git a/resources/js/admin/pages/WelcomeTeaserPage.tsx b/resources/js/admin/pages/WelcomeTeaserPage.tsx index 2cb1dd2..db6c4ad 100644 --- a/resources/js/admin/pages/WelcomeTeaserPage.tsx +++ b/resources/js/admin/pages/WelcomeTeaserPage.tsx @@ -1,31 +1,148 @@ import React from 'react'; -import { Sparkles, Users, Camera, Heart, ArrowRight } from 'lucide-react'; +import { + ArrowRight, + Camera, + Clock3, + Heart, + MessageCircle, + Moon, + QrCode, + ShieldCheck, + Sparkles, + SunMedium, + Users, + Wand2, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import { LanguageSwitcher } from '../components/LanguageSwitcher'; +import { FrostedSurface } from '../components/tenant'; import { ADMIN_DEFAULT_AFTER_LOGIN_PATH, ADMIN_LOGIN_PATH } from '../constants'; import { encodeReturnTo, resolveReturnTarget } from '../lib/returnTo'; +import { navigateToHref } from '../lib/navigation'; -const highlights = [ +const heroStats = [ + { label: 'Events begleitet', value: '2.100+' }, + { label: 'Fotos kuratiert', value: '680k' }, + { label: 'Mission Cards live', value: '120+' }, +]; + +const featureCards = [ { icon: Sparkles, - title: 'Momente lenken, nicht das Handy', + badge: 'Welcome Flow', + title: 'Geführte Einrichtung', description: - 'Fotospiel liefert euch spielerische Aufgaben, damit eure Gäste das Fest genießen und gleichzeitig emotionale Motive festhalten.', + 'Ein roter Faden von eurer ersten Mission bis zum Event-Branding. Alles läuft dort, wo ihr später auch Events steuert.', }, { - icon: Users, - title: 'Alle Gäste auf einer Reise', + icon: QrCode, + badge: 'Gäste einladen', + title: 'Links & QR-Cards teilen', description: - 'Einladungslinks und QR-Codes führen direkt in eure Event-Galerie. Kein Technik-Know-how nötig – nur teilen und loslegen.', + 'Mit einem Fingertipp entstehen Einladungslinks, QR-Codes und TWA-Links für Android & iOS – ganz ohne App Store Hürden.', }, { icon: Camera, - title: 'Live-Galerie und Moderation', + badge: 'Live Galerie', + title: 'Moderieren wie im Dashboard', description: - 'Sammelt Bilder in Echtzeit, markiert Highlights und entscheidet gemeinsam, welche Erinnerungen groß rauskommen.', + 'Markiert Highlights, ordnet Emotions oder sperrt Fotos – exakt die Tools aus dem Event Admin, nur mit Willkommens-Glow.', + }, +]; + +const timelineSteps = [ + { + title: 'Mission Cards wählen', + description: 'Kuratiert Aufgaben, die eure Gäste ins Erzählen bringen. Jeder Schritt speichert automatisch.', + }, + { + title: 'Event Branding festlegen', + description: 'Farben, Story, Cover – alles in einer Ansicht. Vorschau zeigt sofort, wie der Gastzugang wirkt.', + }, + { + title: 'Link teilen & feiern', + description: 'QR-Code oder Link verschicken, fertig. Gäste landen direkt in eurer Galerie und können ohne Login starten.', + }, +]; + +const supportHighlights = [ + { + icon: ShieldCheck, + title: 'Datenschutz-ready', + description: 'Keine versteckten Tracker, DSGVO-konformes Hosting und Kontrolle, wer Fotos sieht.', + }, + { + icon: Clock3, + title: 'Offline nutzbar', + description: 'Uploads puffern automatisch, falls das WLAN in der Location aussetzt.', + }, + { + icon: MessageCircle, + title: 'Crew an eurer Seite', + description: 'Direkter Chat zum Fotospiel-Team – aus der App heraus oder via hallo@fotospiel.de.', }, ]; export default function WelcomeTeaserPage() { const [isRedirecting, setIsRedirecting] = React.useState(false); + const [mode, setMode] = React.useState<'dark' | 'light'>('dark'); + const isLightMode = mode === 'light'; + const flowSectionRef = React.useRef(null); + + React.useEffect(() => { + document.body.classList.add('tenant-admin-theme', 'tenant-admin-welcome-theme'); + + return () => { + document.body.classList.remove('tenant-admin-theme', 'tenant-admin-welcome-theme'); + }; + }, []); + + React.useEffect(() => { + document.body.classList.toggle('tenant-admin-welcome-light', isLightMode); + document.body.classList.toggle('tenant-admin-welcome-dark', !isLightMode); + }, [isLightMode]); + + const theme = React.useMemo( + () => ({ + rootBackground: isLightMode + ? 'bg-gradient-to-br from-rose-50 via-white to-sky-50 text-slate-900' + : 'bg-slate-950 text-white', + aurora: isLightMode + ? 'bg-[radial-gradient(ellipse_at_top,_rgba(255,179,205,0.55),_transparent_55%),radial-gradient(ellipse_at_bottom,_rgba(148,187,233,0.45),_transparent_60%)]' + : 'bg-[radial-gradient(ellipse_at_top,_rgba(255,137,170,0.25),_transparent_60%),radial-gradient(ellipse_at_bottom,_rgba(99,102,241,0.25),_transparent_62%)]', + overlay: isLightMode + ? 'bg-gradient-to-br from-white/85 via-rose-50/70 to-sky-50/70' + : 'bg-gradient-to-br from-slate-950/95 via-slate-950/80 to-[#150b1f]/90', + headerBadge: isLightMode ? 'border-rose-100 bg-white text-rose-500' : 'border-white/30 bg-white/5 text-white', + headerSubtitle: isLightMode ? 'text-slate-600' : 'text-white/70', + heroEyebrow: isLightMode ? 'text-rose-500' : 'text-rose-200', + heroTitle: isLightMode ? 'text-slate-900' : 'text-white', + heroBody: isLightMode ? 'text-slate-600' : 'text-white/75', + ghostButton: isLightMode ? 'text-slate-700 hover:bg-slate-100' : 'text-white/80 hover:bg-white/10', + statCard: isLightMode + ? 'border-rose-100/70 bg-white/90 text-slate-900 shadow-rose-100/50' + : 'border-white/15 bg-white/10 text-white shadow-rose-500/30', + featureCard: isLightMode + ? 'border-rose-100 bg-white text-slate-900 shadow-rose-100/40' + : 'border-white/15 bg-white/10 text-white shadow-slate-900/40', + timelineCard: isLightMode ? 'border-slate-200 bg-white text-slate-900' : 'border-white/15 bg-white/5 text-white', + timelineDescription: isLightMode ? 'text-slate-600' : 'text-white/70', + supportCard: isLightMode ? 'border-slate-200 bg-white text-slate-900' : 'border-white/15 bg-white/10 text-white', + supportDescription: isLightMode ? 'text-slate-600' : 'text-white/70', + ctaSurface: isLightMode ? 'border-slate-200 bg-white text-slate-900' : 'border-white/15 bg-white/10 text-white', + ctaDetail: isLightMode ? 'text-slate-600' : 'text-white/70', + footer: isLightMode + ? 'border-t border-slate-200/80 bg-white/70 text-slate-500' + : 'border-t border-white/10 bg-black/20 text-white/60', + }), + [isLightMode] + ); + + const toggleMode = React.useCallback(() => { + setMode((prev) => (prev === 'dark' ? 'light' : 'dark')); + }, []); const handleLoginRedirect = React.useCallback(() => { if (isRedirecting) { @@ -39,90 +156,339 @@ export default function WelcomeTeaserPage() { const { finalTarget, encodedFinal } = resolveReturnTarget(rawReturnTo, ADMIN_DEFAULT_AFTER_LOGIN_PATH); const target = new URL(ADMIN_LOGIN_PATH, window.location.origin); target.searchParams.set('return_to', encodedFinal ?? encodeReturnTo(finalTarget)); - window.location.href = `${target.pathname}${target.search}`; + const nextHref = `${target.pathname}${target.search}`; + navigateToHref(nextHref); }, [isRedirecting]); + const handleScrollToFlow = React.useCallback(() => { + flowSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, []); + return ( -
-
-
- Willkommen bei Fotospiel -
-

- Eure Gäste als Geschichtenerzähler – ohne Technikstress -

-

- Dieses Kontrollzentrum zeigt euch, wie ihr Fotospiel für Hochzeit, Jubiläum oder Team-Event einsetzt. - Wir führen euch Schritt für Schritt durch Aufgaben, Event-Setup und Einladungen. -

-
- - -
-
+
+
+
-
-
- {highlights.map((item) => ( -
-
- -
-

{item.title}

-

{item.description}

-
- ))} -
- -
-
-
- - So startet ihr -
-

In drei Schritten zu eurer Story

-
    -
  1. - 1. Aufgaben entdecken  - Stellt euer erstes Aufgabenpaket zusammen, das zu eurer Feier passt. -
  2. -
  3. - 2. Event anlegen  - Benennt euer Event, legt Farben fest und erstellt den QR-Einladungslink. -
  4. -
  5. - 3. Link teilen  - Gäste scannen, laden Fotos hoch und ihr entscheidet, was in euer Album kommt. -
  6. -
+
+
+
+ + Event Admin · Fotospiel + +

+ Euer mobiles Kontrollzentrum für Events, Workshops und Feiern +

- -
-
+
+ + + +
+ -
- Fotospiel • Eure Gäste gestalten eure Lieblingsmomente -
+
+
+
+
+

+ Willkommen im Event-Kontrollzentrum +

+

+ Alles für eure Gästegeschichte in einer App +

+

+ Der neue Startscreen begrüßt euch wie eine mobile App, führt euch in den Welcome Flow und lässt euch + jederzeit zurück ins Dashboard springen, sobald ihr bereit seid. +

+
+ +
+ + +
+ +
+ {heroStats.map((stat) => ( + +

{stat.value}

+

+ {stat.label} +

+
+ ))} +
+
+ +
+
+
+
+ Fotospiel + Event Admin +
+
+
+ Heute + Live +
+
+ {['Mission Pack auswählen', 'Event Branding finalisieren', 'Einladungslink teilen'].map((item) => ( +
+
+

{item}

+

Welcome Flow

+
+ +
+ ))} +
+
+
+
+ Live Moments + +28 +
+
+ {[ + { title: 'Anna lädt 6 Fotos hoch', subtitle: 'Event · Gartenpalast' }, + { title: 'Max reagiert auf Mission „Emotionen“', subtitle: 'Tasks · Emotion Board' }, + { title: 'QR-Link 124x gescannt', subtitle: 'Einladungen · Photobooth' }, + ].map((entry) => ( +
+

{entry.title}

+

{entry.subtitle}

+
+ ))} +
+
+
+
+
+ +
+ {featureCards.map((feature) => ( + +
+ + {feature.badge} +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+ +
+ +

+ Guided Journey +

+

+ So leitet euch der Welcome Flow +

+
+ {timelineSteps.map((step, index) => ( +
+
+ {index + 1} +
+
+

+ {step.title} +

+

{step.description}

+
+
+ ))} +
+
+ +
+ +
+ Mobile Greeting + Neu +
+

Fühlt sich an wie eine native App

+

+ Mit großen Touch-Flächen, Animationen und frosted Cards wirkt der Startscreen wie die mobile Version + des Event Admins – perfekt für TWA und Capacitor Builds. +

+
+
+

CTA

+

Event Admin öffnen

+

Leitet direkt zum OAuth Login

+
+
+

Scroll Action

+

Journey ansehen

+

Smooth Scroll bis zur Timeline

+
+
+
+ +
+ {supportHighlights.map((support) => ( + + +

+ {support.title} +

+

{support.description}

+
+ ))} +
+
+
+ + +
+

Bereit?

+

+ Wechselt ins Dashboard, sobald euer Flow steht. +

+

+ Alle Schritte lassen sich später direkt aus dem Event Admin wieder öffnen. +

+
+ +
+
+ +
+ Fotospiel · Eure Gäste gestalten eure Lieblingsmomente +
+
); } diff --git a/resources/js/admin/pages/__tests__/WelcomeTeaserPage.test.tsx b/resources/js/admin/pages/__tests__/WelcomeTeaserPage.test.tsx new file mode 100644 index 0000000..5a2da08 --- /dev/null +++ b/resources/js/admin/pages/__tests__/WelcomeTeaserPage.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { describe, expect, it, afterEach, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import WelcomeTeaserPage from '../WelcomeTeaserPage'; + +const navigateMock = vi.fn(); + +vi.mock('../../components/LanguageSwitcher', () => ({ + LanguageSwitcher: () =>
, +})); + +vi.mock('../../lib/navigation', () => ({ + navigateToHref: (href: string) => navigateMock(href), +})); + +describe('WelcomeTeaserPage', () => { + afterEach(() => { + document.body.classList.remove('tenant-admin-theme', 'tenant-admin-welcome-theme'); + vi.clearAllMocks(); + navigateMock.mockReset(); + }); + + it('applies the tenant admin theme classes while mounted', () => { + const { unmount } = render(); + + expect(document.body.classList.contains('tenant-admin-theme')).toBe(true); + expect(document.body.classList.contains('tenant-admin-welcome-theme')).toBe(true); + + unmount(); + + expect(document.body.classList.contains('tenant-admin-theme')).toBe(false); + expect(document.body.classList.contains('tenant-admin-welcome-theme')).toBe(false); + }); + + it('shows the hero stats and triggers the login redirect CTA', async () => { + render(); + + expect(screen.getByText('Events begleitet')).toBeInTheDocument(); + + const user = userEvent.setup(); + await user.click(screen.getByRole('button', { name: /event admin öffnen/i })); + + expect(navigateMock).toHaveBeenCalledWith(expect.stringContaining('/event-admin/login')); + }); + + it('allows switching between light and dark presentation modes', async () => { + render(); + + const user = userEvent.setup(); + const toggle = screen.getByRole('button', { name: /light mode/i }); + + await user.click(toggle); + + expect(toggle).toHaveTextContent(/dark mode/i); + }); +}); diff --git a/resources/js/pages/auth/LoginForm.tsx b/resources/js/pages/auth/LoginForm.tsx index 49c6ea1..04d0387 100644 --- a/resources/js/pages/auth/LoginForm.tsx +++ b/resources/js/pages/auth/LoginForm.tsx @@ -50,6 +50,7 @@ export default function LoginForm({ onSuccess, canResetPassword = true, locale } const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [hasTriedSubmit, setHasTriedSubmit] = useState(false); + const [shouldFocusError, setShouldFocusError] = useState(false); useEffect(() => { if (!hasTriedSubmit) { @@ -63,7 +64,7 @@ export default function LoginForm({ onSuccess, canResetPassword = true, locale } }, [errors, hasTriedSubmit]); useEffect(() => { - if (!hasTriedSubmit) { + if (!hasTriedSubmit || !shouldFocusError) { return; } @@ -75,11 +76,13 @@ export default function LoginForm({ onSuccess, canResetPassword = true, locale } const field = document.querySelector(`[name="${firstKey}"]`); field?.scrollIntoView({ behavior: "smooth", block: "center" }); field?.focus(); - }, [errors, hasTriedSubmit]); + setShouldFocusError(false); + }, [errors, hasTriedSubmit, shouldFocusError]); const updateValue = (key: keyof typeof values, value: string | boolean) => { setValues((current) => ({ ...current, [key]: value })); setErrors((current) => ({ ...current, [key as string]: "" })); + setShouldFocusError(false); }; const submit = async (event: React.FormEvent) => { @@ -129,11 +132,13 @@ export default function LoginForm({ onSuccess, canResetPassword = true, locale } }); setErrors(fieldErrors); + setShouldFocusError(true); toast.error(t("login.failed_generic", "Ungueltige Anmeldedaten")); return; } toast.error(t("login.unexpected_error", "Beim Login ist ein Fehler aufgetreten.")); + setShouldFocusError(false); } catch (error) { console.error("Login request failed", error); toast.error(t("login.unexpected_error", "Beim Login ist ein Fehler aufgetreten.")); @@ -205,4 +210,3 @@ export default function LoginForm({ onSuccess, canResetPassword = true, locale } ); } - diff --git a/resources/js/pages/auth/RegisterForm.tsx b/resources/js/pages/auth/RegisterForm.tsx index 7ce30ba..5417a93 100644 --- a/resources/js/pages/auth/RegisterForm.tsx +++ b/resources/js/pages/auth/RegisterForm.tsx @@ -390,7 +390,7 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale
@@ -410,7 +410,7 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale } }} 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={t('register.confirm_password_placeholder')} + placeholder={t('register.password_confirmation_placeholder')} />
{errors.password_confirmation &&

{errors.password_confirmation}

} @@ -442,7 +442,7 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale onClick={() => setPrivacyOpen(true)} className="text-[#FFB6C1] hover:underline inline bg-transparent border-none cursor-pointer p-0 font-medium" > - {t('register.privacy_policy')} + {t('register.privacy_policy_link')} . {errors.privacy_consent &&

{errors.privacy_consent}

} @@ -485,5 +485,3 @@ export default function RegisterForm({ packageId, onSuccess, privacyHtml, locale
); } - - diff --git a/resources/js/pages/auth/register.tsx b/resources/js/pages/auth/register.tsx index ac641f0..4427d92 100644 --- a/resources/js/pages/auth/register.tsx +++ b/resources/js/pages/auth/register.tsx @@ -265,7 +265,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -285,7 +285,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} 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={t('register.confirm_password_placeholder')} + placeholder={t('register.password_confirmation_placeholder')} />
{errors.password_confirmation &&

{errors.password_confirmation}

} @@ -313,7 +313,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis onClick={() => setPrivacyOpen(true)} className="text-[#FFB6C1] hover:underline inline bg-transparent border-none cursor-pointer p-0 font-medium" > - {t('register.privacy_policy')} + {t('register.privacy_policy_link')} . {errors.privacy_consent &&

{errors.privacy_consent}

} diff --git a/resources/js/pages/marketing/CheckoutWizardPage.tsx b/resources/js/pages/marketing/CheckoutWizardPage.tsx index 07d5f18..27f4266 100644 --- a/resources/js/pages/marketing/CheckoutWizardPage.tsx +++ b/resources/js/pages/marketing/CheckoutWizardPage.tsx @@ -3,8 +3,6 @@ import { Head, usePage } from "@inertiajs/react"; import MarketingLayout from "@/layouts/mainWebsite"; import type { CheckoutPackage, GoogleProfilePrefill } from "./checkout/types"; import { CheckoutWizard } from "./checkout/CheckoutWizard"; -import { Button } from "@/components/ui/button"; -import { X } from "lucide-react"; interface CheckoutWizardPageProps { package: CheckoutPackage; @@ -50,19 +48,6 @@ const CheckoutWizardPage: React.FC = ({
- {/* Abbruch-Button oben rechts */} -
- -
- + pkg.price === 0 ? t('packages.free') : `${pkg.price.toLocaleString()} ${t('packages.currency.euro')}`; + + const limits = + variant === 'endcustomer' + ? [ + { + key: 'price', + label: t('packages.price'), + value: (pkg: Package) => `${formatPrice(pkg)} / ${t('packages.billing_per_event')}`, + }, + { + key: 'max_photos', + label: t('packages.max_photos_label'), + value: (pkg: Package) => pkg.limits?.max_photos?.toLocaleString() ?? tCommon('unlimited'), + }, + { + key: 'max_guests', + label: t('packages.max_guests_label'), + value: (pkg: Package) => pkg.limits?.max_guests?.toLocaleString() ?? tCommon('unlimited'), + }, + { + key: 'gallery_days', + label: t('packages.gallery_days_label'), + value: (pkg: Package) => + pkg.gallery_duration_label ?? + pkg.limits?.gallery_days?.toLocaleString() ?? + tCommon('unlimited'), + }, + ] + : [ + { + key: 'price', + label: t('packages.price'), + value: (pkg: Package) => `${formatPrice(pkg)} / ${t('packages.billing_per_year')}`, + }, + { + key: 'max_tenants', + label: t('packages.max_tenants'), + value: (pkg: Package) => pkg.limits?.max_tenants?.toLocaleString() ?? tCommon('unlimited'), + }, + { + key: 'max_events_per_year', + label: t('packages.max_events_year'), + value: (pkg: Package) => + pkg.limits?.max_events_per_year?.toLocaleString() ?? tCommon('unlimited'), + }, + ]; + + const features = [ + { + key: 'watermark', + label: t('packages.watermark_label'), + value: (pkg: Package) => + pkg.watermark_allowed === false ? t('packages.no_watermark') : t('packages.feature_watermark'), + }, + { + key: 'branding', + label: t('packages.feature_custom_branding'), + value: (pkg: Package) => (pkg.branding_allowed ? t('packages.available') : t('packages.not_available')), + }, + { + key: 'support', + label: t('packages.feature_support'), + value: (pkg: Package) => + pkg.features.includes('priority_support') ? t('packages.priority_support') : t('packages.standard_support'), + }, + ]; + + return ( +
+
+

{t('packages.comparison_title')}

+

{t('packages.comparison_subtitle')}

+
+ + + {t('packages.comparison_limits')} + {t('packages.comparison_features')} + + + + + + + + {t('packages.feature')} + {packages.map((pkg) => ( + + {pkg.name} + + ))} + + + + {limits.map((row) => ( + + {row.label} + {packages.map((pkg) => ( + + {row.value(pkg)} + + ))} + + ))} + +
+
+
+
+ + + + + + + {t('packages.feature')} + {packages.map((pkg) => ( + + {pkg.name} + + ))} + + + + {features.map((row) => ( + + {row.label} + {packages.map((pkg) => ( + + {row.value(pkg)} + + ))} + + ))} + +
+
+
+
+
+
+ ); +} + type DescriptionEntry = { title?: string | null; value: string; @@ -54,11 +209,7 @@ interface PackagesProps { const Packages: React.FC = ({ endcustomerPackages, resellerPackages }) => { const [open, setOpen] = useState(false); const [selectedPackage, setSelectedPackage] = useState(null); - const [currentStep, setCurrentStep] = useState<'overview' | 'deep' | 'testimonials'>('overview'); - const [couponCode, setCouponCode] = useState(''); - const [couponPreview, setCouponPreview] = useState(null); - const [couponError, setCouponError] = useState(null); - const [couponLoading, setCouponLoading] = useState(false); + const [currentStep, setCurrentStep] = useState<'overview' | 'testimonials'>('overview'); const { props } = usePage(); const { auth } = props as any; const { t } = useTranslation('marketing'); @@ -82,18 +233,6 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag } }, [endcustomerPackages, resellerPackages]); - useEffect(() => { - const couponParam = new URLSearchParams(window.location.search).get('coupon'); - if (couponParam) { - setCouponCode(couponParam.toUpperCase()); - } - }, []); - - useEffect(() => { - setCouponPreview(null); - setCouponError(null); - setCouponLoading(false); - }, [selectedPackage?.id]); const testimonials = [ { name: tCommon('testimonials.anna.name'), text: t('packages.testimonials.anna'), rating: 5 }, @@ -145,10 +284,7 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag ? isHighlightedPackage(selectedPackage, selectedVariant) : false; - const appliedCouponCode = couponPreview?.coupon.code ?? null; - const purchaseUrl = selectedPackage - ? `/purchase-wizard/${selectedPackage.id}${appliedCouponCode ? `?coupon=${appliedCouponCode}` : ''}` - : '#'; + const purchaseUrl = selectedPackage ? `/purchase-wizard/${selectedPackage.id}` : '#'; const { trackEvent } = useAnalytics(); @@ -173,40 +309,6 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag }); }; - const handleCouponSubmit = async (event: FormEvent) => { - event.preventDefault(); - - if (!selectedPackage) { - return; - } - - const trimmed = couponCode.trim(); - if (!trimmed) { - setCouponPreview(null); - setCouponError(t('coupon.errors.required')); - return; - } - - setCouponLoading(true); - setCouponError(null); - - try { - const preview = await requestCouponPreview(selectedPackage.id, trimmed); - setCouponPreview(preview); - } catch (error) { - setCouponPreview(null); - setCouponError(error instanceof Error ? error.message : t('coupon.errors.generic')); - } finally { - setCouponLoading(false); - } - }; - - const handleRemoveCoupon = () => { - setCouponPreview(null); - setCouponError(null); - setCouponCode(''); - }; - // nextStep entfernt, da Tabs nun parallel sind const getFeatureIcon = (feature: string) => { @@ -223,31 +325,24 @@ const getFeatureIcon = (feature: string) => { } }; -const getAccentTheme = (variant: 'endcustomer' | 'reseller') => ( +const getAccentTheme = (variant: 'endcustomer' | 'reseller') => variant === 'reseller' ? { - gradient: 'from-amber-100/80 via-white to-white', - ring: 'ring-amber-200 dark:ring-amber-500/40', - badge: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-100', - price: 'text-amber-500 dark:text-amber-300', - buttonHighlight: 'bg-gradient-to-r from-amber-500 via-rose-400 to-pink-500 text-white hover:from-amber-500/95 hover:via-rose-400/95 hover:to-pink-500/95', - buttonDefault: 'bg-gradient-to-r from-amber-50 via-white to-rose-50 text-amber-600 border border-amber-100/80 shadow-sm hover:from-amber-100 hover:via-rose-50 hover:to-white hover:text-amber-600 dark:from-amber-500/20 dark:via-amber-500/10 dark:to-rose-500/20 dark:text-amber-200 dark:border-amber-500/30', - highlightShadow: 'shadow-[0_28px_65px_-20px_rgba(245,158,11,0.55)]', - topBar: 'from-amber-400 via-rose-300 to-pink-400', - ctaShadow: 'shadow-lg shadow-amber-500/25', + badge: 'bg-amber-50 text-amber-700', + price: 'text-amber-600', + buttonHighlight: 'bg-gray-900 text-white hover:bg-gray-800', + buttonDefault: 'border border-amber-200 text-amber-700 hover:bg-amber-50', + cardBorder: 'border border-amber-100', + highlightShadow: 'shadow-lg shadow-amber-100/60', } : { - gradient: 'from-rose-100/80 via-white to-white', - ring: 'ring-rose-200 dark:ring-rose-500/40', - badge: 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-100', - price: 'text-rose-500 dark:text-rose-300', - buttonHighlight: 'bg-gradient-to-r from-rose-500 via-pink-500 to-amber-400 text-white hover:from-rose-500/95 hover:via-pink-500/95 hover:to-amber-400/95', - buttonDefault: 'bg-gradient-to-r from-rose-50 via-white to-pink-50 text-rose-600 border border-rose-100/80 shadow-sm hover:from-rose-100 hover:via-white hover:to-pink-100 hover:text-rose-600 dark:from-rose-500/15 dark:via-rose-500/10 dark:to-rose-500/20 dark:text-rose-200 dark:border-rose-500/30', - highlightShadow: 'shadow-[0_28px_70px_-25px_rgba(244,63,94,0.55)]', - topBar: 'from-rose-500 via-pink-400 to-amber-300', - ctaShadow: 'shadow-lg shadow-rose-500/30', - } -); + badge: 'bg-rose-50 text-rose-700', + price: 'text-rose-600', + buttonHighlight: 'bg-gray-900 text-white hover:bg-gray-800', + buttonDefault: 'border border-rose-100 text-rose-700 hover:bg-rose-50', + cardBorder: 'border border-rose-100', + highlightShadow: 'shadow-lg shadow-rose-100/60', + }; type PackageMetric = { key: string; @@ -337,10 +432,11 @@ function PackageCard({ const accent = getAccentTheme(variant); + const numericPrice = Number(pkg.price); const priceLabel = - pkg.price === 0 + numericPrice === 0 ? t('packages.free') - : `${pkg.price.toLocaleString()} ${t('packages.currency.euro')}`; + : `${numericPrice.toLocaleString()} ${t('packages.currency.euro')}`; const cadenceLabel = variant === 'reseller' ? t('packages.billing_per_year') @@ -356,149 +452,88 @@ function PackageCard({ ? t('packages.badge_starter') : null; - const featureBadges = pkg.features.slice(0, 4); - const extraFeatureCount = Math.max(pkg.features.length - featureBadges.length, 0); - + const keyFeatures = pkg.features.slice(0, 3); const metrics = resolvePackageMetrics(pkg, variant, t, tCommon); return ( -
- -
- - {typeLabel} - + +
+ {typeLabel} {badgeLabel && ( - - {highlight && } {badgeLabel} - + )}
- - {pkg.name} - - - {pkg.description} - -
-
+ {pkg.name} + {pkg.description} +
+ + +
- {priceLabel} + {priceLabel} {pkg.price !== 0 && ( - - / {cadenceLabel} - + / {cadenceLabel} )}
{variant === 'endcustomer' && ( -

+

{pkg.events} × {t('packages.one_time')}

)}
- - - -
- {featureBadges.map((feature) => ( - - {getFeatureIcon(feature)} - {t(`packages.feature_${feature}`)} - - ))} - {pkg.watermark_allowed === false && ( - - - {t('packages.no_watermark')} - - )} - {pkg.branding_allowed && ( - - - {t('packages.custom_branding')} - - )} - {extraFeatureCount > 0 && ( - - {t('packages.more_features', { count: extraFeatureCount })} - - )} -
- -
+
{metrics.map((metric) => ( -
-

- {metric.value} -

-

- {metric.label} -

+
+

{metric.value}

+

{metric.label}

))}
+
    + {keyFeatures.map((feature) => ( +
  • + + {t(`packages.feature_${feature}`)} +
  • + ))} + {pkg.watermark_allowed === false && ( +
  • + + {t('packages.no_watermark')} +
  • + )} + {pkg.branding_allowed && ( +
  • + + {t('packages.custom_branding')} +
  • + )} +
{showCTA && onSelect && ( - + - )} -
- setCouponCode(event.target.value.toUpperCase())} - placeholder={t('coupon.placeholder')} - className="flex-1" - /> -
- - {couponPreview && ( - - )} -
-
- {couponError && ( -
- - {couponError} -
- )} - {couponPreview && ( -
-
- - {t('coupon.applied', { code: couponPreview.coupon.code, amount: couponPreview.pricing.formatted.discount })} -
-
-
- {t('coupon.fields.subtotal')} - {couponPreview.pricing.formatted.subtotal} -
-
- {t('coupon.fields.discount')} - {couponPreview.pricing.formatted.discount} -
-
- {t('coupon.fields.total')} - {couponPreview.pricing.formatted.total} -
-
-
- )} - -

- {t('packages.order_hint')} -

-
-
-
-
-

- {t('packages.quick_facts')} -

-

- {t('packages.quick_facts_hint')} -

-
    - {quickFacts.map((metric) => ( -
  • -

    {metric.value}

    -

    - {metric.label} -

    -
  • - ))} -
- {showDeepLink && ( - - )} -
-
- ); - })()} - - - - {(() => { - const accent = getAccentTheme(selectedVariant); - const metrics = resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon); - const descriptionEntries = selectedPackage.description_breakdown ?? []; - const entriesWithTitle = descriptionEntries.filter((entry) => entry.title); - const entriesWithoutTitle = descriptionEntries.filter((entry) => !entry.title); - - return ( -
-
-
- - {t('packages.features_label')} - - {selectedHighlight && ( - - {t('packages.badge_deep_dive')} - - )} -
-
- {selectedPackage.features.map((feature) => ( - - {getFeatureIcon(feature)} - {t(`packages.feature_${feature}`)} - - ))} - {selectedPackage.watermark_allowed === false && ( - - - {t('packages.no_watermark')} - - )} - {selectedPackage.branding_allowed && ( - - - {t('packages.custom_branding')} - - )} -
-
- - {metrics.length > 0 && ( -
-

- {t('packages.limits_label')} -

-

- {t('packages.limits_label_hint')} -

-
- {metrics.map((metric) => ( -
-

{metric.value}

-

- {metric.label} -

-
- ))} -
-
- )} - - {descriptionEntries.length > 0 && ( -
-

- {t('packages.breakdown_label')} -

-

- {t('packages.breakdown_label_hint')} -

- {entriesWithTitle.length > 0 && ( - - {entriesWithTitle.map((entry, index) => ( - - - {entry.title} - - - {entry.value} - - - ))} - - )} - {entriesWithoutTitle.length > 0 && ( -
- {entriesWithoutTitle.map((entry, index) => ( -
- {entry.value} -
- ))} -
- )} -
- )} -
- ); - })()} -
- -
-

- {t('packages.what_customers_say')} -

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

“{testimonial.text}”

-
- {testimonial.name} -
- {[...Array(testimonial.rating)].map((_, i) => ( - - ))} -
-
+
+ {resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon).map((metric) => ( +
+

{metric.value}

+

{metric.label}

))}
-
- + {t('packages.to_order')} + + + +

{t('packages.order_hint')}

+
+
+

{t('packages.feature_highlights')}

+
    + {selectedPackage.features.slice(0, 5).map((feature) => ( +
  • + + {t(`packages.feature_${feature}`)} +
  • + ))} + {selectedPackage.watermark_allowed === false && ( +
  • + + {t('packages.no_watermark')} +
  • + )} + {selectedPackage.branding_allowed && ( +
  • + + {t('packages.custom_branding')} +
  • + )} +
+
+
+
+ + + + {t('packages.details')} + + + {t('packages.customer_opinions')} + + + +
+

{t('packages.quick_facts')}

+

{t('packages.quick_facts_hint')}

+
+ {resolvePackageMetrics(selectedPackage, selectedVariant, t, tCommon).map((metric) => ( +
+

{metric.value}

+

{metric.label}

+
+ ))}
+
+

{t('packages.feature_highlights')}

+
    + {selectedPackage.features.slice(0, 4).map((feature) => ( +
  • + + {t(`packages.feature_${feature}`)} +
  • + ))} + {selectedPackage.watermark_allowed === false && ( +
  • + + {t('packages.no_watermark')} +
  • + )} + {selectedPackage.branding_allowed && ( +
  • + + {t('packages.custom_branding')} +
  • + )} +
+
-
+ + {selectedPackage.description_breakdown?.length ? ( + + {selectedPackage.description_breakdown.map((entry, index) => ( + + + {entry.title ?? t('packages.limits_label')} + + + {entry.value} + + + ))} + + ) : ( +

{t('packages.breakdown_label_hint')}

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

{testimonial.name}

+

{selectedPackage.name}

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

“{testimonial.text}”

+
+ ))} + +
+
+ +
+
)} - {/* Testimonials Section entfernt, da nun im Dialog */} ); diff --git a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx index c375cff..e27cd7c 100644 --- a/resources/js/pages/marketing/checkout/CheckoutWizard.tsx +++ b/resources/js/pages/marketing/checkout/CheckoutWizard.tsx @@ -9,6 +9,7 @@ import { PackageStep } from "./steps/PackageStep"; import { AuthStep } from "./steps/AuthStep"; import { ConfirmationStep } from "./steps/ConfirmationStep"; import { useAnalytics } from '@/hooks/useAnalytics'; +import { cn } from "@/lib/utils"; const PaymentStep = lazy(() => import('./steps/PaymentStep').then((module) => ({ default: module.PaymentStep }))); @@ -69,6 +70,7 @@ const WizardBody: React.FC<{ googleProfile?: GoogleProfilePrefill | null; onClearGoogleProfile?: () => void; }> = ({ privacyHtml, googleProfile, onClearGoogleProfile }) => { + const primaryCtaClassName = "min-w-[160px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground"; const { t } = useTranslation('marketing'); const { currentStep, @@ -160,13 +162,8 @@ const WizardBody: React.FC<{ return true; }, [atLastStep, authUser, currentStep, isAuthenticated, isFreeSelected, paymentCompleted, selectedPackage]); - const shouldShowNextButton = useMemo(() => { - if (currentStep !== 'payment') { - return true; - } - - return isFreeSelected || paymentCompleted; - }, [currentStep, isFreeSelected, paymentCompleted]); + const shouldShowNextButton = useMemo(() => currentStep !== 'confirmation', [currentStep]); + const highlightNextCta = currentStep === 'payment' && paymentCompleted; const handleNext = useCallback(() => { if (!canProceedToNextStep) { @@ -200,8 +197,42 @@ const WizardBody: React.FC<{ window.location.href = '/event-admin'; }, []); + const primaryCta = useMemo(() => { + if (currentStep === 'confirmation') { + return { + label: t('checkout.confirmation_step.to_admin'), + onClick: handleGoToAdmin, + disabled: false, + }; + } + + if (!shouldShowNextButton) { + return null; + } + + return { + label: t('checkout.next'), + onClick: handleNext, + disabled: !canProceedToNextStep, + }; + }, [currentStep, handleGoToAdmin, handleNext, shouldShowNextButton, t, canProceedToNextStep]); + const ctaClassName = cn(primaryCtaClassName, highlightNextCta && 'animate-pulse ring-2 ring-primary/50 ring-offset-2 ring-offset-background'); + return (
+ {primaryCta && ( +
+ +
+ )} +
= 0 ? currentIndex : 0} /> @@ -226,13 +257,18 @@ const WizardBody: React.FC<{ )}
-
+
- {shouldShowNextButton ? ( - ) : (