From 20eda6b4f82a881562fac23454e537bb0a89ac3b Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 3 Nov 2025 11:47:19 +0100 Subject: [PATCH] =?UTF-8?q?login-seiten=20neu=20designt,=20homepage=20neu?= =?UTF-8?q?=20designt.=20"so=20funktioniert's"=20erg=C3=A4nzt=20und=20Demo?= =?UTF-8?q?-Seite=20hinzugef=C3=BCgt.=20Paketansicht=20in=20mobile=20verbe?= =?UTF-8?q?ssert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/MarketingController.php | 12 +- docs/todo/localized-seo-hreflang-strategy.md | 29 + public/lang/de/auth.json | 41 +- public/lang/de/marketing.json | 418 ++++++++++- public/lang/en/auth.json | 39 +- public/lang/en/marketing.json | 420 ++++++++++- resources/js/admin/i18n/locales/de/auth.json | 22 +- resources/js/admin/i18n/locales/en/auth.json | 22 +- resources/js/admin/pages/EventInvitesPage.tsx | 28 +- resources/js/admin/pages/LoginPage.tsx | 193 +++-- .../InviteLayoutCustomizerPanel.tsx | 46 +- .../invite-layout/DesignerCanvas.tsx | 2 +- .../components/invite-layout/fileNames.ts | 52 ++ resources/js/layouts/app/Header.tsx | 213 ++++-- .../js/layouts/auth/auth-simple-layout.tsx | 116 ++- resources/js/pages/auth/login.tsx | 104 ++- resources/js/pages/marketing/Demo.tsx | 125 ++++ resources/js/pages/marketing/Home.tsx | 693 +++++++++++++----- resources/js/pages/marketing/HowItWorks.tsx | 423 +++++++++++ resources/js/pages/marketing/Occasions.tsx | 13 +- resources/js/pages/marketing/Packages.tsx | 45 +- .../marketing/checkout/steps/PaymentStep.tsx | 10 +- routes/web.php | 2 + 23 files changed, 2481 insertions(+), 587 deletions(-) create mode 100644 docs/todo/localized-seo-hreflang-strategy.md create mode 100644 resources/js/admin/pages/components/invite-layout/fileNames.ts create mode 100644 resources/js/pages/marketing/Demo.tsx create mode 100644 resources/js/pages/marketing/HowItWorks.tsx diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index 26e2bb0..b9b626e 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -282,6 +282,16 @@ class MarketingController extends Controller return Inertia::render('marketing/BlogShow', compact('post')); } + public function howItWorks() + { + return Inertia::render('marketing/HowItWorks'); + } + + public function demo() + { + return Inertia::render('marketing/Demo'); + } + public function packagesIndex() { $endcustomerPackages = Package::where('type', 'endcustomer') @@ -314,7 +324,7 @@ class MarketingController extends Controller 'isInertia' => request()->header('X-Inertia'), ]); - $validTypes = ['hochzeit', 'geburtstag', 'firmenevent']; + $validTypes = ['hochzeit', 'geburtstag', 'firmenevent', 'konfirmation']; if (! in_array($type, $validTypes)) { Log::warning('Invalid occasion type accessed', ['type' => $type]); abort(404, 'Invalid occasion type'); diff --git a/docs/todo/localized-seo-hreflang-strategy.md b/docs/todo/localized-seo-hreflang-strategy.md new file mode 100644 index 0000000..38cf0af --- /dev/null +++ b/docs/todo/localized-seo-hreflang-strategy.md @@ -0,0 +1,29 @@ +# Localized SEO hreflang Strategy TODO + +## Goal +Establish a consistent canonical and hreflang setup for the marketing site so search engines can index German and English content without duplicate-content penalties. + +## Status (Stand 17.02.2026) +- **Discovery:** Not started. +- **Implementation:** Not started. +- **Validation:** Not started. + +## Discovery +- [ ] Audit current route map and localized content coverage (marketing pages, blog, checkout flow). +- [ ] Decide on URL strategy (session-based locale vs. language-prefixed routes) and document migration implications. +- [ ] Identify required updates to `MarketingLayout`, sitemap generation, and Inertia responses for localized alternates. + +## Implementation +- [ ] Ensure canonical URLs and hreflang tags are generated per locale with reciprocal references. +- [ ] Expose locale-specific URLs in navigation, Open Graph tags, and any structured data. +- [ ] Update translation files and config to support the chosen URL strategy. + +## Validation +- [ ] Add automated checks (feature test or Playwright) verifying hreflang/canonical tags for both locales. +- [ ] Validate via Search Console-style inspection or lighthouse to confirm alternate links render correctly. +- [ ] Update docs (PRP + marketing playbooks) with the final hreflang strategy and operational guidance. + +## Open Questions +- Should blog posts use language-specific slugs or shared identifiers with query parameters? +- How will we handle locale fallbacks for missing translations when hreflang is enforced? +- Do we need localized sitemap indexes per language or a unified sitemap with hreflang annotations? diff --git a/public/lang/de/auth.json b/public/lang/de/auth.json index b297acd..ec45f18 100644 --- a/public/lang/de/auth.json +++ b/public/lang/de/auth.json @@ -1,17 +1,25 @@ { "header": { "home": "Startseite", + "how_it_works": "So geht's", "packages": "Pakete", "blog": "Blog", "occasions": { - "wedding": "Hochzeit", - "birthday": "Geburtstag", - "corporate": "Firmenevent", + "wedding": "Hochzeiten", + "birthday": "Geburtstage", + "corporate": "Firmenfeiern", + "confirmation": "Konfirmation/Jugendweihe", "label": "Anlässe" }, "contact": "Kontakt", "login": "Anmelden", - "register": "Registrieren" + "register": "Registrieren", + "cta": "Jetzt ausprobieren", + "utility": "Darstellung & Sprache", + "appearance": "Darstellung", + "appearance_light": "Hell", + "appearance_dark": "Dunkel", + "language": "Sprache" }, "login_failed": "Ungültige E-Mail oder Passwort.", "login_success": "Sie sind nun eingeloggt.", @@ -21,14 +29,37 @@ "failed_credentials": "Falsche Anmeldedaten.", "login": { "title": "Anmelden", + "description": "Melde dich mit deinem Fotospiel Konto an und begleite dein Event vom ersten Upload bis zum finalen Download.", "identifier": "E-Mail oder Username", "identifier_placeholder": "Geben Sie Ihre E-Mail oder Ihren Username ein", "username_or_email": "Username oder E-Mail", + "email": "E-Mail-Adresse", + "email_placeholder": "Deine E-Mail-Adresse", "password": "Passwort", "password_placeholder": "Geben Sie Ihr Passwort ein", "forgot": "Passwort vergessen?", "remember": "Angemeldet bleiben", - "submit": "Anmelden" + "submit": "Anmelden", + "no_account": "Noch keinen Account?", + "sign_up": "Jetzt registrieren", + "success_toast": "Login erfolgreich", + "unexpected_error": "Beim Login ist ein Fehler aufgetreten.", + "highlights": { + "moments": "Momente in Echtzeit teilen", + "moments_description": "Uploads landen sofort in der Event-Galerie – ohne App-Download.", + "branding": "Branding & Slideshows, die begeistern", + "branding_description": "Konfiguriere Slideshow, Wasserzeichen und Aufgaben für dein Event.", + "privacy": "Sicherer Zugang über Tokens", + "privacy_description": "Eventzugänge bleiben geschützt – DSGVO-konform mit Join Tokens." + }, + "hero_tagline": "Event-Tech mit Herz", + "hero_heading": "Willkommen zurück bei Fotospiel", + "hero_subheading": "Verwalte Events, Galerien und Gästelisten in einem liebevoll gestalteten Dashboard.", + "hero_footer": { + "headline": "Noch kein Account?", + "subline": "Entdecke unsere Packages und erlebe Fotospiel live.", + "cta": "Packages entdecken" + } }, "register": { "title": "Registrieren", diff --git a/public/lang/de/marketing.json b/public/lang/de/marketing.json index 5a93d07..8db640d 100644 --- a/public/lang/de/marketing.json +++ b/public/lang/de/marketing.json @@ -9,43 +9,83 @@ }, "home": { "title": "Startseite - Fotospiel", - "hero_title": "Fotospiel", - "hero_description": "Sammle Gastfotos für Events mit QR-Codes. Unsere sichere PWA-Plattform für Gäste und Organisatoren – einfach, mobil und datenschutzkonform. Besser als Konkurrenz, geliebt von Tausenden.", - "cta_explore": "Pakete entdecken", - "cta_explore_highlight": "Jetzt loslegen", - "hero_image_alt": "Event-Fotos mit QR-Code", - "how_title": "So funktioniert es", - "step1_title": "Paket wählen", - "step1_desc": "Wähle das passende Paket für dein Event.", - "step2_title": "QR-Code teilen", - "step2_desc": "Teile den QR-Code mit deinen Gästen.", - "step3_title": "Fotos sammeln", - "step3_desc": "Gäste laden Fotos hoch – sicher und einfach.", - "features_title": "Warum Fotospiel?", - "feature1_title": "Sicher & Datenschutzkonform", - "feature1_desc": "GDPR-konform, keine PII-Speicherung.", - "feature2_title": "Mobil & PWA", - "feature2_desc": "Funktioniert offline, installierbar wie App.", - "feature3_title": "Einfach zu bedienen", - "feature3_desc": "Intuitive UI für Gäste und Organisatoren.", - "packages_title": "Unsere Pakete", + "hero_tagline": "Eventfotos ohne App-Zwang", + "hero_title": "Dein Event. Eure Fotos. Echtzeit bereit.", + "hero_description": "Fotospiel bündelt QR-Zugänge, Live-Galerien und Moderation in einer einzigen Plattform – für Hochzeiten, Firmenfeiern und jedes Fest, das Erinnerungen verdient.", + "hero_bullets": [ + "Live-Galerie in Sekunden startklar", + "Join Tokens schützen jeden Zugang", + "Slideshows, Branding und Aufgaben on-the-fly" + ], + "cta_demo": "Demo ansehen", + "cta_demo_highlight": "Live-Demo starten", + "cta_how": "So funktioniert's", + "cta_packages": "Pakete ansehen", + "cta_explore": "Pakete ansehen", + "cta_explore_highlight": "Jetzt Fotospiel testen", + "hero_image_alt": "Gäste teilen Fotos per QR-Code auf ihrem Smartphone", + "how_title": "So läuft Fotospiel", + "how_subtitle": "Von der Einladung bis zur fertigen Galerie in drei cleveren Schritten.", + "step1_title": "Event erstellen & Paket wählen", + "step1_desc": "In wenigen Klicks zum Event: Grenzen für Fotos, Gäste und Branding festlegen.", + "step2_title": "Join Token & QR-Code teilen", + "step2_desc": "Gäste scannen, wählen Emotionen oder Aufgaben und laden direkt hoch – ohne App-Store.", + "step3_title": "Live moderieren & Highlights zeigen", + "step3_desc": "Schalte Beiträge frei, triggere Slideshows und exportiere Lieblingsfotos sofort.", + "demo_title": "Erlebe die Fotospiel Demo", + "demo_description": "Unser Demo-Event zeigt dir die Gäste-PWA im echten 9:16-Frame. Öffne es auf dem Handy und teste Uploads, Emotionen und Likes live.", + "demo_hint": "Tipp: Teile den Link mit deinem Team, um gemeinsam auszuprobieren.", + "demo_cta": "Zur Demo-Seite", + "demo_media_alt": "Smartphone Rahmen mit geöffneter Fotospiel Demo", + "features_title": "Alles, was dein Event braucht", + "features_highlight": [ + { + "title": "Branding & Slideshows", + "description": "Passe Farben, Overlays und Aufgaben an deinen Event an – inklusive Live-Slideshow." + }, + { + "title": "Moderation in Echtzeit", + "description": "Sperre Inhalte mit einem Klick, markiere Favoriten und exportiere Best-of-Galerien." + }, + { + "title": "Analytics & Archiv", + "description": "Verfolge Uploads, Reaktionen und Downloads – DSGVO-konform archiviert." + } + ], + "occasions_title": "Anlässe, die Fotospiel liebt", + "occasions_description": "Wähle einen Einstieg und entdecke Best-Practices für dein Eventformat.", + "occasions": { + "wedding": "Hochzeiten – romantische Momente sammeln", + "birthday": "Geburtstage – Erinnerungen von 7 bis 70", + "corporate": "Firmenfeiern – Branding & Sicherheit im Fokus", + "confirmation": "Konfirmation & Jugendweihe – Familienalbum to go" + }, + "blog_teaser_title": "Insights aus dem Fotospiel Blog", + "blog_teaser_description": "Trends, Setups und Playbooks für deine nächste Veranstaltung.", + "blog_teaser_cta": "Zum Blog", + "packages_title": "Packages & Preise", + "packages_subtitle": "Flexibel für Einzelevents oder Agenturen – mit Paddle-Checkout in Minuten aktiviert.", "view_details": "Details ansehen", - "all_packages": "Alle Pakete ansehen", - "contact_title": "Kontakt", + "all_packages": "Alle Pakete vergleichen", + "contact_title": "Lass uns über dein Event sprechen", + "contact_lead": "Wir beraten dich zu Aufgaben, Tokens, Hardware-Setups oder individuellen Workflows.", "name_label": "Name", "email_label": "E-Mail", "message_label": "Nachricht", - "sending": "Wird gesendet...", - "send": "Senden", - "testimonials_title": "Was unsere Kunden sagen", - "testimonial1": "Toll für Hochzeiten! Einfach und sicher.", - "testimonial2": "Beste App für Event-Fotos.", - "testimonial3": "Schnell und benutzerfreundlich.", - "faq_title": "Häufige Fragen", - "faq1_q": "Ist es kostenlos?", - "faq1_a": "Ja, es gibt ein kostenloses Paket für kleine Events.", - "faq2_q": "Wie funktioniert der QR-Code?", - "faq2_a": "Gäste scannen und laden Fotos hoch – einfach!" + "contact_privacy": "Mit dem Absenden bestätigst du unsere Datenschutzhinweise. Wir melden uns innerhalb von 24 Stunden.", + "sending": "Wird gesendet …", + "send": "Nachricht senden", + "testimonials_title": "Stimmen aus der Community", + "testimonials_subtitle": "Über 1.200 Events wurden bereits mit Fotospiel begleitet.", + "testimonial1": "„Unsere Gäste haben das Event förmlich dokumentiert – und wir hatten alles in einem sicheren Archiv.“", + "testimonial2": "„Branding, Moderation und Analytics – alles genau da, wo ich es im Event brauche.“", + "testimonial3": "„Konfirmation ohne WhatsApp-Chaos. QR-Code raus, Emojis rein, Bilder für alle!“", + "faq_title": "Noch Fragen?", + "faq_subtitle": "Hier findest du schnelle Antworten. Mehr Details gibt es in So funktioniert’s.", + "faq1_q": "Kann ich Fotospiel vorab testen?", + "faq1_a": "Ja! Nutze unser Demo-Event oder buche das Free Package und teste alle Grundfunktionen.", + "faq2_q": "Brauchen Gäste ein Konto?", + "faq2_a": "Nein. Ein Join Token reicht. Auf Wunsch lässt sich die Galerie zusätzlich mit PIN schützen." }, "packages": { "title": "Unsere Packages", @@ -234,6 +274,16 @@ "benefit4": "GDPR-sicher: Keine PII gespeichert.", "image_alt": "Firmenevent-Fotos" }, + "confirmation": { + "title": "Konfirmation & Jugendweihe feiern", + "description": "Halte den Übergang ins Erwachsenenleben fest: Gäste teilen Fotos direkt aus Kirche und Feier – ohne App-Installation.", + "benefits_title": "Vorteile für Konfirmation & Jugendweihe", + "benefit1": "Familienfreundlicher QR-Code für alle Generationen.", + "benefit2": "Sammlungen für Kirche, Feier und Gruppenbilder.", + "benefit3": "Moderation: Eltern entscheiden, welche Fotos sichtbar sind.", + "benefit4": "Download-Paket für Dankeskarten und Fotoalbum.", + "image_alt": "Konfirmationsfeier" + }, "family": { "title": "Familienfeiern", "description": "Von Taufen bis Jubiläen: Erinnerungen von allen Verwandten sammeln.", @@ -469,5 +519,307 @@ "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." } + }, + "how_it_works_page": { + "hero": { + "title": "So funktioniert Fotospiel", + "subtitle": "Teile deinen QR-Code, sammle Fotos in Echtzeit und behalte die Moderation. Alles läuft im Browser – ganz ohne App.", + "primaryCta": "Event starten", + "secondaryCta": "Kontakt aufnehmen", + "stats": [ + { + "value": "5 Min", + "label": "Setup vom Account zur Galerie" + }, + { + "value": "0 Apps", + "label": "Gäste nutzen nur ihren Browser" + }, + { + "value": "100 %", + "label": "Hosting & Datenschutz in der EU" + } + ], + "demoNote": "Demo anschauen", + "demoLabel": "Demo ansehen" + }, + "experience": { + "host": { + "label": "Veranstalter:innen", + "intro": "Von der ersten Idee bis zum Export deiner Lieblingsmomente – du steuerst alles aus einem Dashboard.", + "steps": [ + { + "title": "Event anlegen", + "description": "Titel, Datum und Paket wählen, optionale Challenges und Freigaberichtlinien festlegen." + }, + { + "title": "Material teilen", + "description": "QR-Code als PNG/PDF exportieren, Link in Einladungen oder Displays integrieren, Liveshow optional aktivieren." + }, + { + "title": "Moderieren & sichern", + "description": "Uploads freigeben oder kuratieren, Favoriten markieren, ZIP-Download und Dankesnachricht vorbereiten." + } + ], + "callouts": [ + "Co-Hosts für Moderation & Liveshow hinzufügen", + "Offline-Uploads werden automatisch nachgesendet", + "Integrationen über Paddle-Abrechnung und RevenueCat für Apps" + ] + }, + "guest": { + "label": "Gäste", + "intro": "Für deine Gäste zählt nur der Moment: scannen, fotografieren, fertig. Keine Registrierung, keine Hürden.", + "steps": [ + { + "title": "QR-Code scannen", + "description": "Der Browser öffnet sich mit deiner gebrandeten Eventseite – kein Download nötig." + }, + { + "title": "Foto aufnehmen", + "description": "Direkt mit der Kamera oder aus der Galerie, optionale Challenges machen es spielerisch." + }, + { + "title": "Galerie erleben", + "description": "Uploads erscheinen nach Freigabe in der Galerie und in der Live-Show, Downloads bleiben jederzeit möglich." + } + ], + "callouts": [ + "Progressiver Web-App Modus mit Homescreen-Icon", + "Lokale Zwischenspeicherung bei schwacher Verbindung", + "Barrierearme UI, unterstützt aktuelle iOS/Android Browser" + ] + } + }, + "pillars": [ + { + "title": "Launch in Minuten", + "description": "QR-Code, Link und Liveshow sind sofort einsatzbereit. Druckvorlagen und Textbausteine liefern wir mit." + }, + { + "title": "Moderation & Sicherheit", + "description": "Freigabemodi, Löschrichtlinien und Co-Hosts sichern eure Inhalte – vollständig DSGVO-konform." + }, + { + "title": "Erlebnis vor Ort", + "description": "Aufgaben, Reaktionen und Live-Slideshow motivieren zum Mitmachen – ohne zusätzliche Technik." + }, + { + "title": "Nach dem Event", + "description": "ZIP-Exports, Favoritenlisten, Follow-up-Mailings – alles direkt aus dem Dashboard steuerbar." + } + ], + "timeline": [ + { + "title": "Event vorbereiten", + "body": "Account registrieren, Paket wählen und Branding setzen. Abos laufen über Paddle, Mobile-Apps über RevenueCat.", + "tips": [ + "Testevent anlegen, um Upload-Flow vorab zu prüfen", + "Trauzeug:innen oder Kolleg:innen als Co-Hosts einladen" + ] + }, + { + "title": "Challenges & Regeln wählen", + "body": "Aktiviere optionale Aufgaben, Live-Slideshow oder Vorab-Freigabe. Definiere Hausregeln für Uploads.", + "tips": [ + "Kurze Belohnungen motivieren zum Mitmachen", + "Bei Kinderfotos klare Hinweise platzieren" + ] + }, + { + "title": "Material bereitstellen", + "body": "QR-Code als PNG, PDF oder Druckbogen herunterladen. Nutze Templates für Tische, Namensschilder und Bildschirm-Slides.", + "tips": [ + "QR-Code an Eingängen, Buffet und Bühne platzieren", + "Link in Einladung und Reminder-Mail aufnehmen" + ] + }, + { + "title": "Event starten", + "body": "Liveshow starten, Uploads freischalten und optional Reaktionen erlauben. Offline-Uploads werden automatisch synchronisiert.", + "tips": [ + "MC oder Moderator:in weist in der Begrüßung auf Fotospiel hin", + "Zeige die Liveshow auf TV, Beamer oder Tablet" + ] + }, + { + "title": "Moderieren & kuratieren", + "body": "Uploads freigeben, markieren, kommentieren und bei Bedarf ausblenden. Alle Aktionen sind protokolliert.", + "tips": [ + "Favoritenlisten für Best-of-Exports nutzen", + "Verstöße konsequent entfernen – Datenschutz zuerst" + ] + }, + { + "title": "Nachbereitung", + "body": "Galerie Zeitraum festlegen, ZIP-Export herunterladen und Dankesmail mit Galerie-Link versenden.", + "tips": [ + "Erinnerung 24 Stunden nach dem Event schicken", + "Galerie bei Bedarf frühzeitig schließen oder verlängern" + ] + } + ], + "use_cases": { + "title": "Passend für jedes Event", + "description": "Wähle den passenden Fokus – die Galerie bleibt dieselbe.", + "tabs": [ + { + "value": "wedding", + "label": "Hochzeiten", + "goal": "Emotionen und Perspektiven der Gäste festhalten.", + "recommendations": [ + "QR-Code in Kirchenheften und auf Tischen platzieren", + "Trauzeug:innen als Co-Hosts einsetzen", + "Liveshow beim Empfang zeigen" + ], + "ideas": [ + "Selfie mit dem Brautpaar", + "Lieblingsmoment der Party", + "Etwas Blaues entdecken" + ] + }, + { + "value": "birthday", + "label": "Geburtstage", + "goal": "Spontane Schnappschüsse und Gratulationen sammeln.", + "recommendations": [ + "Hinweis in der Geburtstagsrede", + "Kleinen Preis für kreativstes Foto ausloben", + "Kuchen-Cam neben dem Dessertbuffet" + ], + "ideas": [ + "Throwback nachstellen", + "Größtes Gruppenlächeln", + "Geburtstagskuchen close-up" + ] + }, + { + "value": "corporate", + "label": "Firmenfeiern", + "goal": "Authentische Einblicke für Employer Branding und Recap.", + "recommendations": [ + "Gebrandete Startseite und Liveshow", + "HR oder Moderation für Ankündigungen nutzen", + "Fotos für Post-Event-Kommunikation kuratieren" + ], + "ideas": [ + "Bestes Team-Selfie", + "Behind the Scenes", + "Neue Kolleg:innen kennenlernen" + ] + }, + { + "value": "confirmation", + "label": "Konfirmation & Jugendweihe", + "goal": "Familienmomente sicher bündeln und teilen.", + "recommendations": [ + "Separate Galerieabschnitte für Kirche und Feier", + "Moderation aktiv lassen, um sensible Inhalte zu prüfen", + "Download-Link nur an Familie weitergeben" + ], + "ideas": [ + "Gruppenfoto mit Paten", + "Highlight des Tages", + "Selfie mit dem Konfirmanden / der Konfirmandin" + ] + }, + { + "value": "public", + "label": "Public Events", + "goal": "Community-Momente und Social-Media-Content generieren.", + "recommendations": [ + "Große QR-Poster und Hashtags kombinieren", + "Liveshow auf LED-Wänden zeigen", + "Upload-Regeln klar kommunizieren" + ], + "ideas": [ + "Buntestes Outfit", + "Lieblingsact", + "Geheime Festival-Ecke" + ] + } + ] + }, + "checklist": { + "title": "Checklist: In 10 Minuten startklar", + "items": [ + "Event angelegt, Paket & Branding festgelegt", + "Freigabe-Modus und Moderationsteam definiert", + "QR-Code & Link getestet (eigenes Smartphone!)", + "Druck- und Screen-Material vorbereitet", + "Liveshow / Displays geprüft", + "Kurzansage und Follow-up vorbereitet" + ], + "cta": "Event jetzt starten" + }, + "faq": { + "title": "Häufige Fragen", + "items": [ + { + "question": "Brauchen Gäste eine App?", + "answer": "Nein. Alles läuft im mobilen Browser. Auf Wunsch kann die Seite als PWA auf den Homescreen gelegt werden." + }, + { + "question": "Wie funktioniert der Upload ohne Internet?", + "answer": "Fotos werden lokal zwischengespeichert. Sobald wieder WLAN oder LTE verfügbar ist, werden sie automatisch hochgeladen." + }, + { + "question": "Kann ich Uploads zuerst prüfen?", + "answer": "Ja. Aktiviere den Freigabe-Modus in den Event-Einstellungen, um Beiträge vor Veröffentlichung zu moderieren." + }, + { + "question": "Wie lange bleiben Fotos online?", + "answer": "Je nach Paket bleiben Galerien zwischen 14 und 90 Tagen aktiv. Die genaue Dauer steht in der Paketübersicht." + }, + { + "question": "Wie läuft die Bezahlung?", + "answer": "Web-Pakete werden über Paddle abgerechnet (inklusive Rechnung & Steuerhandling). Mobile Abos verwalten wir über RevenueCat." + }, + { + "question": "Welche Dateiformate sind erlaubt?", + "answer": "Aktuell akzeptieren wir Fotos (JPEG, PNG, HEIC). Videos werden aus Datenschutzgründen nicht unterstützt." + } + ] + }, + "support": { + "title": "Noch Fragen?", + "description": "Unser Team hilft dir bei der Einrichtung oder plant mit dir ein Pilot-Event.", + "cta": "Kontakt aufnehmen" + }, + "timeline_title": "Der Ablauf im Detail" + }, + "labels": { + "recommendations": "Empfehlungen", + "challengeIdeas": "Ideen für Challenges", + "prepHint": "Alles, was du vor dem Event abhaken solltest.", + "good_to_know": "Gut zu wissen", + "openDemoFull": "Demo im neuen Tab öffnen", + "readyToLaunch": "Bereit für dein Event?", + "readyToLaunchCopy": "Registriere dich kostenlos und lege noch heute dein erstes Event an." + }, + "actions": { + "tips": "Tipps" + }, + "demo_page": { + "title": "Jetzt ausprobieren", + "subtitle": "Scanne den QR-Code, lade Fotos hoch und erlebe die Fotospiel-Galerie genau so, wie deine Gäste sie sehen – komplett im Browser.", + "primaryCta": "Pakete entdecken", + "secondaryCta": "Zum Leitfaden", + "iframeNote": "Demo-Uploads werden regelmäßig zurückgesetzt – nutze den QR-Code im Frame oder öffne die Demo im neuen Tab.", + "openFull": "Demo im neuen Tab öffnen", + "features": [ + { + "title": "Echter Gast-Flow", + "description": "Direkter Zugang zur Galerie ohne App oder Login – inklusive Offline-Puffer." + }, + { + "title": "Live-Slideshow ready", + "description": "Zeige neue Uploads sofort auf jedem Bildschirm über die Live-Show." + }, + { + "title": "Moderation inklusive", + "description": "Teste Freigabemodus, Reaktionen und Favoriten – alles DSGVO-konform." + } + ] } } diff --git a/public/lang/en/auth.json b/public/lang/en/auth.json index 720fc41..b86cb7f 100644 --- a/public/lang/en/auth.json +++ b/public/lang/en/auth.json @@ -1,17 +1,25 @@ { "header": { "home": "Home", + "how_it_works": "How it works", "packages": "Packages", "blog": "Blog", "occasions": { - "wedding": "Wedding", - "birthday": "Birthday", - "corporate": "Corporate Event", + "wedding": "Weddings", + "birthday": "Birthdays", + "corporate": "Corporate Events", + "confirmation": "Confirmation/Coming of Age", "label": "Occasions" }, "contact": "Contact", "login": "Login", - "register": "Register" + "register": "Register", + "cta": "Try now", + "utility": "Display & language", + "appearance": "Appearance", + "appearance_light": "Light", + "appearance_dark": "Dark", + "language": "Language" }, "login_failed": "Invalid email or password.", "login_success": "You are now logged in.", @@ -21,6 +29,7 @@ "failed_credentials": "Invalid credentials.", "login": { "title": "Login", + "description": "Sign in to your Fotospiel account and keep your event experience flowing from upload to download.", "identifier": "Email or Username", "identifier_placeholder": "Enter your email or username", "email": "Email", @@ -29,7 +38,27 @@ "password_placeholder": "Enter your password", "forgot": "Forgot Password?", "remember": "Remember me", - "submit": "Login" + "submit": "Login", + "no_account": "No account yet?", + "sign_up": "Create one now", + "success_toast": "Login successful", + "unexpected_error": "Unable to log in right now.", + "highlights": { + "moments": "Share moments in real time", + "moments_description": "Uploads appear instantly in the event gallery – no app installation required.", + "branding": "Branding & slideshows that delight", + "branding_description": "Fine-tune slideshows, watermarks, and event tasks with a few clicks.", + "privacy": "Secure access via join tokens", + "privacy_description": "Keep events private and GDPR compliant with protected join tokens." + }, + "hero_tagline": "Event-tech with heart", + "hero_heading": "Welcome back to Fotospiel", + "hero_subheading": "Manage events, galleries, and guest lists in a lovingly crafted dashboard.", + "hero_footer": { + "headline": "Need an account?", + "subline": "Explore our packages and experience Fotospiel live.", + "cta": "Explore packages" + } }, "register": { "title": "Register", diff --git a/public/lang/en/marketing.json b/public/lang/en/marketing.json index 467a63b..27d1e9d 100644 --- a/public/lang/en/marketing.json +++ b/public/lang/en/marketing.json @@ -1,43 +1,83 @@ { "home": { "title": "Home - Fotospiel", - "hero_title": "Fotospiel", - "hero_description": "Collect guest photos for events with QR codes. Our secure PWA platform for guests and organizers – simple, mobile, and privacy-compliant. Better than competitors, loved by thousands.", - "cta_explore": "Discover Packages", - "cta_explore_highlight": "Get started now", - "hero_image_alt": "Event photos with QR code", - "how_title": "How it works", - "step1_title": "Choose Package", - "step1_desc": "Choose the right package for your event.", - "step2_title": "Share QR Code", - "step2_desc": "Share the QR code with your guests.", - "step3_title": "Collect Photos", - "step3_desc": "Guests upload photos – secure and easy.", - "features_title": "Why Fotospiel?", - "feature1_title": "Secure & Privacy Compliant", - "feature1_desc": "GDPR compliant, no PII storage.", - "feature2_title": "Mobile & PWA", - "feature2_desc": "Works offline, installable like an app.", - "feature3_title": "Easy to Use", - "feature3_desc": "Intuitive UI for guests and organizers.", - "packages_title": "Our Packages", - "view_details": "View Details", - "all_packages": "View All Packages", - "contact_title": "Contact", + "hero_tagline": "Event photos without app downloads", + "hero_title": "Your event. Their photos. Ready in real time.", + "hero_description": "Fotospiel combines QR access, live galleries, and moderation in one platform—perfect for weddings, corporate events, and every celebration that deserves a highlight reel.", + "hero_bullets": [ + "Launch a live gallery in seconds", + "Join tokens keep every access private", + "Slideshows, branding, and tasks on the fly" + ], + "cta_demo": "View demo", + "cta_demo_highlight": "Launch live demo", + "cta_how": "How Fotospiel works", + "cta_packages": "See packages", + "cta_explore": "See packages", + "cta_explore_highlight": "Start your Fotospiel trial", + "hero_image_alt": "Guests sharing photos via QR code on their phone", + "how_title": "How Fotospiel flows", + "how_subtitle": "From invitation to finished gallery in three smart steps.", + "step1_title": "Create event & pick a package", + "step1_desc": "Set limits for photos, guests, and branding in just a few clicks.", + "step2_title": "Share join token & QR code", + "step2_desc": "Guests scan, choose emotions or tasks, and upload instantly—no app store required.", + "step3_title": "Moderate live & spotlight favorites", + "step3_desc": "Approve posts, trigger slideshows, and export highlight galleries on demand.", + "demo_title": "Experience the Fotospiel demo", + "demo_description": "Our demo event shows the guest PWA inside a true 9:16 frame. Open it on your phone to try uploads, emotions, and likes live.", + "demo_hint": "Pro tip: share the link with your team so everyone can explore together.", + "demo_cta": "Go to demo", + "demo_media_alt": "Smartphone frame displaying the Fotospiel demo", + "features_title": "Everything your event needs", + "features_highlight": [ + { + "title": "Branding & slideshows", + "description": "Match colors, overlays, and tasks to your event—complete with a live slideshow." + }, + { + "title": "Real-time moderation", + "description": "Block posts with one tap, mark favorites, and export best-of galleries effortlessly." + }, + { + "title": "Analytics & archive", + "description": "Track uploads, reactions, and downloads—archived in a GDPR-compliant workflow." + } + ], + "occasions_title": "Occasions we love", + "occasions_description": "Pick a starting point and explore best practices for your format.", + "occasions": { + "wedding": "Weddings – capture every candid", + "birthday": "Birthdays – memories from 7 to 70", + "corporate": "Corporate events – branding & security built in", + "confirmation": "Confirmation & coming of age – the family album to go" + }, + "blog_teaser_title": "Insights from the Fotospiel blog", + "blog_teaser_description": "Trends, setups, and playbooks for your next celebration.", + "blog_teaser_cta": "Visit the blog", + "packages_title": "Packages & pricing", + "packages_subtitle": "Flexible for single events or agencies—activated within minutes via Paddle checkout.", + "view_details": "View details", + "all_packages": "Compare all packages", + "contact_title": "Let's plan your event", + "contact_lead": "We’ll walk you through tasks, tokens, hardware setups, or custom workflows.", "name_label": "Name", "email_label": "Email", "message_label": "Message", - "sending": "Sending...", - "send": "Send", - "testimonials_title": "What our customers say", - "testimonial1": "Great for weddings! Simple and secure.", - "testimonial2": "Best app for event photos.", - "testimonial3": "Fast and user-friendly.", - "faq_title": "Frequently Asked Questions", - "faq1_q": "Is it free?", - "faq1_a": "Yes, there is a free package for small events.", - "faq2_q": "How does the QR code work?", - "faq2_a": "Guests scan and upload photos – easy!" + "contact_privacy": "By submitting you confirm our privacy notice. We typically reply within 24 hours.", + "sending": "Sending …", + "send": "Send message", + "testimonials_title": "Voices from the community", + "testimonials_subtitle": "Over 1,200 events have already run on Fotospiel.", + "testimonial1": "\"Our guests documented the day for us—and everything landed in one secure archive.\"", + "testimonial2": "\"Branding, moderation, analytics—all right where I need them during an event.\"", + "testimonial3": "\"Confirmation without messaging chaos. QR out, emojis in, photos for everyone!\"", + "faq_title": "Still curious?", + "faq_subtitle": "Find quick answers here. For deep dives visit How it works.", + "faq1_q": "Can I try Fotospiel first?", + "faq1_a": "Absolutely! Use our demo event or pick the Free package to explore all core features.", + "faq2_q": "Do guests need an account?", + "faq2_a": "No. A join token is enough, and you can add an optional PIN for extra gallery protection." }, "packages": { "title": "Our Packages", @@ -220,6 +260,16 @@ "benefit4": "GDPR-secure: No PII stored.", "image_alt": "Corporate event photos" }, + "confirmation": { + "title": "Confirmation & Coming-of-Age Celebrations", + "description": "Capture the rite of passage: Guests share photos from ceremony and party without installing an app.", + "benefits_title": "Benefits for Confirmation & Coming of Age", + "benefit1": "Family-friendly QR code accessible to every generation.", + "benefit2": "Collections for ceremony, celebration, and group shots.", + "benefit3": "Moderation keeps parents in control of what is visible.", + "benefit4": "Download bundle for thank-you cards and keepsake albums.", + "image_alt": "Confirmation celebration" + }, "family": { "title": "Family Celebrations", "description": "From baptisms to anniversaries: Collect memories from all relatives.", @@ -463,5 +513,307 @@ "google_missing_email": "We could not retrieve your Google email address.", "google_error_fallback": "We couldn't complete the Google login. Please try again." } + }, + "how_it_works_page": { + "hero": { + "title": "How Fotospiel Works", + "subtitle": "Share your QR code, collect guest photos in real time, and stay in full control – all inside the browser.", + "primaryCta": "Create an event", + "secondaryCta": "Talk to our team", + "stats": [ + { + "value": "5 min", + "label": "From sign-up to your first upload" + }, + { + "value": "0 apps", + "label": "Guests only need their browser" + }, + { + "value": "100%", + "label": "EU hosting & GDPR compliance" + } + ], + "demoNote": "See the demo", + "demoLabel": "View demo" + }, + "experience": { + "host": { + "label": "Hosts", + "intro": "Plan, moderate, and export your event memories from a single dashboard.", + "steps": [ + { + "title": "Create your event", + "description": "Pick a package, set the basics, define optional challenges and approval rules." + }, + { + "title": "Share materials", + "description": "Export the QR code, embed the link in invitations or displays, and enable the live gallery if you like." + }, + { + "title": "Moderate & secure", + "description": "Approve uploads, highlight favorites, schedule the follow-up email, and download everything as a ZIP." + } + ], + "callouts": [ + "Add co-hosts for moderation and the live show", + "Offline uploads sync automatically once back online", + "Billing handled via Paddle, mobile apps through RevenueCat" + ] + }, + "guest": { + "label": "Guests", + "intro": "Your guests simply scan, shoot, and share. No login, no download, no friction.", + "steps": [ + { + "title": "Scan the QR code", + "description": "The branded event page opens instantly in the browser – no install required." + }, + { + "title": "Snap a photo", + "description": "Use the camera or pick from the gallery, optional challenges keep it fun." + }, + { + "title": "Enjoy the gallery", + "description": "Uploads appear after approval in the gallery and live show, downloads stay accessible whenever needed." + } + ], + "callouts": [ + "Progressive Web App mode with optional homescreen icon", + "Local caching if the connection drops – auto sync later", + "Accessible UI for modern iOS and Android browsers" + ] + } + }, + "pillars": [ + { + "title": "Launch in minutes", + "description": "QR code, link, and live show are ready instantly. We provide print templates and copy blocks." + }, + { + "title": "Moderation & safety", + "description": "Approval modes, takedowns, and co-hosts keep your content secure and compliant." + }, + { + "title": "On-site engagement", + "description": "Challenges, reactions, and the live slideshow spark participation without extra hardware." + }, + { + "title": "Post-event follow-up", + "description": "Export ZIPs, build best-of highlights, and send thank-you emails straight from the dashboard." + } + ], + "timeline": [ + { + "title": "Prepare your event", + "body": "Register, choose a package, and apply your branding. Web payments run through Paddle, mobile apps via RevenueCat.", + "tips": [ + "Create a test event to experience the upload flow", + "Invite co-hosts like MCs or colleagues" + ] + }, + { + "title": "Configure rules & challenges", + "body": "Enable optional challenges, the live slideshow, or pre-approval. Communicate your house rules.", + "tips": [ + "Small rewards boost participation", + "Use extra guidance for photos of kids" + ] + }, + { + "title": "Distribute assets", + "body": "Download the QR code as PNG/PDF, print table cards, and add the link to invitation emails and slides.", + "tips": [ + "Place the QR at entrances and high-traffic spots", + "Add the link to reminders before the event" + ] + }, + { + "title": "Go live", + "body": "Start the live show, approve uploads, and optionally allow reactions. Offline uploads sync when the connection returns.", + "tips": [ + "Have the MC mention Fotospiel during the welcome", + "Stream the live gallery on TV, projector, or tablet" + ] + }, + { + "title": "Moderate & curate", + "body": "Approve, pin, or remove uploads. Every action is logged for compliance.", + "tips": [ + "Use favorites for best-of highlight reels", + "Remove inappropriate content immediately" + ] + }, + { + "title": "Wrap up", + "body": "Define the gallery duration, export a ZIP, and send your thank-you message with the gallery link.", + "tips": [ + "Send a reminder 24 hours after the event", + "Close or extend the gallery with one click" + ] + } + ], + "use_cases": { + "title": "Fits every event", + "description": "Pick the focus that matches your format – the gallery stays the same.", + "tabs": [ + { + "value": "wedding", + "label": "Weddings", + "goal": "Capture genuine guest perspectives beyond staged photos.", + "recommendations": [ + "Place QR codes on programs and tables", + "Assign co-hosts (best man, maid of honor)", + "Run the live show during reception or dinner" + ], + "ideas": [ + "Selfie with the couple", + "Favorite dance move", + "Something blue" + ] + }, + { + "value": "birthday", + "label": "Birthdays", + "goal": "Collect candid greetings and fun surprises.", + "recommendations": [ + "Mention Fotospiel in the birthday speech", + "Offer a small prize for the most creative photo", + "Place a QR sign next to the cake table" + ], + "ideas": [ + "Recreate an old memory", + "Biggest group smile", + "Cake close-up" + ] + }, + { + "value": "corporate", + "label": "Corporate", + "goal": "Generate authentic employer-branding content and recap material.", + "recommendations": [ + "Use branded start screen and live show", + "Let HR or moderation introduce Fotospiel", + "Curate highlights for internal comms" + ], + "ideas": [ + "Best team selfie", + "Behind the scenes", + "Meet someone new" + ] + }, + { + "value": "confirmation", + "label": "Coming of age", + "goal": "Keep family moments secure and easy to share.", + "recommendations": [ + "Separate sections for ceremony and celebration", + "Keep approval mode on for sensitive content", + "Share the download link only with close family" + ], + "ideas": [ + "Portrait with godparents", + "Highlight of the day", + "Selfie with the celebrant" + ] + }, + { + "value": "public", + "label": "Public events", + "goal": "Activate communities and gather social media content.", + "recommendations": [ + "Combine large QR posters with hashtags", + "Show the live feed on LED walls", + "Communicate upload guidelines clearly" + ], + "ideas": [ + "Most colourful outfit", + "Favourite act", + "Hidden festival gem" + ] + } + ] + }, + "checklist": { + "title": "Checklist: ready in 10 minutes", + "items": [ + "Event created, package & branding confirmed", + "Approval mode and moderation team defined", + "QR code & link tested (use your own phone!)", + "Printed and screen materials prepared", + "Live show / displays checked", + "Announcement and follow-up drafted" + ], + "cta": "Create your event" + }, + "faq": { + "title": "FAQ", + "items": [ + { + "question": "Do guests need an app?", + "answer": "No. Everything runs in the mobile browser. Guests can optionally save it as a PWA on their homescreen." + }, + { + "question": "What if the internet is unstable?", + "answer": "Uploads are cached locally and synchronised automatically once the connection is back." + }, + { + "question": "Can I approve uploads first?", + "answer": "Yes. Enable approval mode in the event settings to moderate before publishing." + }, + { + "question": "How long are photos available?", + "answer": "Gallery retention depends on the package – typically 14 to 90 days. See the pricing table for details." + }, + { + "question": "How do payments work?", + "answer": "Web packages are billed through Paddle (with invoices and tax handling). Mobile subscriptions are managed via RevenueCat." + }, + { + "question": "Which file formats are supported?", + "answer": "We currently accept photos (JPEG, PNG, HEIC). Videos are disabled for privacy reasons." + } + ] + }, + "support": { + "title": "Need a hand?", + "description": "Our team is happy to set up a pilot event or walk you through the dashboard.", + "cta": "Contact us" + }, + "timeline_title": "The detailed flow" + }, + "labels": { + "recommendations": "Recommendations", + "challengeIdeas": "Challenge ideas", + "prepHint": "Everything you should tick off before the event.", + "good_to_know": "Good to know", + "openDemoFull": "Open demo in new tab", + "readyToLaunch": "Ready to launch?", + "readyToLaunchCopy": "Sign up for free and create your first event today." + }, + "actions": { + "tips": "Tips" + }, + "demo_page": { + "title": "Try Fotospiel now", + "subtitle": "Scan the QR code, upload photos, and experience the guest gallery exactly as attendees do – all inside the browser.", + "primaryCta": "Explore packages", + "secondaryCta": "Read the guide", + "iframeNote": "Demo uploads reset regularly—use the QR inside the frame or open the demo in a new tab.", + "openFull": "Open demo in new tab", + "features": [ + { + "title": "Authentic guest flow", + "description": "Instant gallery access without apps or logins – offline uploads queue automatically." + }, + { + "title": "Live slideshow ready", + "description": "Show fresh uploads on any screen using the built-in live show." + }, + { + "title": "Moderation included", + "description": "Test approval, reactions, and favourites – fully GDPR compliant." + } + ] } } diff --git a/resources/js/admin/i18n/locales/de/auth.json b/resources/js/admin/i18n/locales/de/auth.json index 1bfe804..5ebbb92 100644 --- a/resources/js/admin/i18n/locales/de/auth.json +++ b/resources/js/admin/i18n/locales/de/auth.json @@ -1,21 +1,23 @@ { "login": { - "title": "Tenant-Admin", - "badge": "Fotospiel Tenant Admin", - "hero_title": "Event-Steuerung, die sich wie Zuhause anfühlt.", - "hero_subtitle": "Wechsle mühelos zwischen Mandanten, behalte Live-Uploads im Blick und teile elegante Einladungen – alles in einer ruhigen Oberfläche.", + "title": "Event-Admin", + "badge": "Fotospiel Event Admin", + "hero_tagline": "Kontrolle behalten, entspannt bleiben", + "hero_title": "Das Cockpit für dein Fotospiel Event", + "hero_subtitle": "Moderation, Uploads und Kommunikation laufen hier zusammen – mobil wie auf dem Desktop.", "features": [ - "Gestalte QR-Einladungen und druckfertige Layouts in wenigen Klicks passend zu eurer Marke.", - "Organisiere Aufgaben, Emotionen und Sammlungen für jeden Eventtyp ohne Excel-Chaos.", - "Bleib am Eventtag souverän mit Dashboards, Live-Statistiken und sofortiger Moderation." + "Überwache Uploads in Echtzeit und archiviere Highlights ohne Aufwand.", + "Erstelle Einladungen mit personalisierten QR-Codes und teile sie sofort.", + "Steuere Aufgaben, Emotionen und Slideshows direkt vom Event aus." ], - "lead": "Die Anmeldung erfolgt über unseren sicheren OAuth-Login und bringt dich direkt wieder zurück.", - "panel_copy": "Melde dich mit deinen Fotospiel-Admin-Zugangsdaten an. Wir schützen dein Konto mit OAuth 2.1 und mandantenbewussten Berechtigungen.", + "lead": "Du meldest dich über unseren sicheren OAuth-Login an und landest direkt im Event-Dashboard.", + "panel_title": "Melde dich an", + "panel_copy": "Logge dich mit deinem Fotospiel-Adminzugang ein. Wir schützen dein Konto mit OAuth 2.1 und klaren Rollenrechten.", "cta": "Mit Fotospiel-Login fortfahren", "loading": "Bitte warten …", "oauth_error_title": "Login aktuell nicht möglich", "oauth_error": "Anmeldung fehlgeschlagen: {{message}}", - "support": "Du brauchst Zugriff? Wende dich an den Tenant-Inhaber oder schreibe an support@fotospiel.de – wir helfen gern weiter.", + "support": "Du brauchst Zugriff? Kontaktiere dein Event-Team oder schreib uns an support@fotospiel.de.", "appearance_label": "Darstellung" } } diff --git a/resources/js/admin/i18n/locales/en/auth.json b/resources/js/admin/i18n/locales/en/auth.json index dc1005c..4b4bc35 100644 --- a/resources/js/admin/i18n/locales/en/auth.json +++ b/resources/js/admin/i18n/locales/en/auth.json @@ -1,21 +1,23 @@ { "login": { - "title": "Tenant Admin", - "badge": "Fotospiel Tenant Admin", - "hero_title": "Event control that feels at home.", - "hero_subtitle": "Switch between tenants, monitor live uploads, and share beautiful invites — all in one calm workspace.", + "title": "Event Admin", + "badge": "Fotospiel Event Admin", + "hero_tagline": "Stay in control, stay relaxed", + "hero_title": "Your cockpit for every Fotospiel event", + "hero_subtitle": "Moderation, uploads, and communication come together in one calm workspace — on desktop and mobile.", "features": [ - "Design QR invites and print-ready layouts that match your brand in minutes.", - "Coordinate tasks, emotions, and achievements for every event flow.", - "Stay confident on event day with dashboards, live stats, and instant moderation." + "Monitor uploads in real time and archive highlights effortlessly.", + "Create invites with personalized QR codes and share them instantly.", + "Run tasks, emotions, and slideshows right from the event dashboard." ], - "lead": "You will be redirected to our secure OAuth login and come right back afterwards.", - "panel_copy": "Sign in with your Fotospiel admin credentials to continue. We secure your account with OAuth 2.1 and tenant-aware permissions.", + "lead": "Use our secure OAuth login and land directly in the event dashboard.", + "panel_title": "Sign in", + "panel_copy": "Sign in with your Fotospiel admin access. OAuth 2.1 and clear role permissions keep your account protected.", "cta": "Continue with Fotospiel login", "loading": "Signing you in …", "oauth_error_title": "Login not possible right now", "oauth_error": "Sign-in failed: {{message}}", - "support": "Need access? Contact your tenant owner or email support@fotospiel.de — we're happy to help.", + "support": "Need access? Contact your event team or email support@fotospiel.de — we're happy to help.", "appearance_label": "Appearance" } } diff --git a/resources/js/admin/pages/EventInvitesPage.tsx b/resources/js/admin/pages/EventInvitesPage.tsx index 3b40802..22ee36c 100644 --- a/resources/js/admin/pages/EventInvitesPage.tsx +++ b/resources/js/admin/pages/EventInvitesPage.tsx @@ -30,6 +30,7 @@ import { } from '../constants'; import { buildLimitWarnings } from '../lib/limitWarnings'; import { InviteLayoutCustomizerPanel, QrLayoutCustomization } from './components/InviteLayoutCustomizerPanel'; +import { buildDownloadFilename, normalizeEventDateSegment } from './components/invite-layout/fileNames'; import { DesignerCanvas } from './components/invite-layout/DesignerCanvas'; import { CANVAS_HEIGHT, @@ -252,6 +253,7 @@ export default function EventInvitesPage(): React.ReactElement { const event = state.event; const eventName = event ? renderEventName(event.name) : t('toolkit.titleFallback', 'Event'); + const eventDate = event?.event_date ?? null; const selectedInvite = React.useMemo( () => state.invites.find((invite) => invite.id === selectedInviteId) ?? null, @@ -587,7 +589,13 @@ export default function EventInvitesPage(): React.ReactElement { const objectUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = objectUrl; - link.download = `${selectedInvite.token || 'invite'}-qr.png`; + const eventDateSegment = normalizeEventDateSegment(eventDate); + const downloadName = buildDownloadFilename( + ['QR Code fuer', eventName, eventDateSegment], + 'png', + 'qr-code', + ); + link.download = downloadName; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -596,7 +604,7 @@ export default function EventInvitesPage(): React.ReactElement { console.error('[Invites] QR download failed', error); setExportError(t('invites.export.qr.error', 'QR-Code konnte nicht gespeichert werden.')); } - }, [selectedInvite, t]); + }, [selectedInvite, eventName, eventDate, t]); const handleExportDownload = React.useCallback( async (format: string) => { @@ -609,6 +617,13 @@ export default function EventInvitesPage(): React.ReactElement { setExportDownloadBusy(busyKey); setExportError(null); + const eventDateSegment = normalizeEventDateSegment(eventDate); + const filename = buildDownloadFilename( + ['Einladungslayout', eventName, exportLayout.name ?? null, eventDateSegment], + normalizedFormat, + 'einladungslayout', + ); + const exportOptions = { elements: exportElements, accentColor: exportPreview.accentColor, @@ -623,19 +638,17 @@ export default function EventInvitesPage(): React.ReactElement { selectedId: null, } as const; - const filenameStem = `${selectedInvite.token || 'invite'}-${exportLayout.id}`; - try { if (normalizedFormat === 'png') { const dataUrl = await generatePngDataUrl(exportOptions); - await triggerDownloadFromDataUrl(dataUrl, `${filenameStem}.png`); + await triggerDownloadFromDataUrl(dataUrl, filename); } else if (normalizedFormat === 'pdf') { const pdfBytes = await generatePdfBytes( exportOptions, 'a4', 'portrait', ); - triggerDownloadFromBlob(new Blob([pdfBytes as any], { type: 'application/pdf' }), `${filenameStem}.pdf`); + triggerDownloadFromBlob(new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' }), filename); } else { setExportError(t('invites.customizer.errors.downloadFailed', 'Download fehlgeschlagen. Bitte versuche es erneut.')); } @@ -646,7 +659,7 @@ export default function EventInvitesPage(): React.ReactElement { setExportDownloadBusy(null); } }, - [selectedInvite, exportLayout, exportPreview, exportElements, exportQr, exportLogo, t] + [selectedInvite, exportLayout, exportPreview, exportElements, exportQr, exportLogo, eventName, eventDate, t] ); const handleExportPrint = React.useCallback( @@ -809,6 +822,7 @@ export default function EventInvitesPage(): React.ReactElement { -
-
-
+
+
+
-
-
+
+
-
-

{t('login.badge')}

+
+

{t('login.badge', 'Fotospiel Event Admin')}

Fotospiel

-
-
-
-
- - - {t('login.badge')} - -

- {t('login.hero_title')} -

-

- {t('login.hero_subtitle')} -

+
+
+ + + {heroTagline} + +

{heroTitle}

+

{heroSubtitle}

+
+ +
+
+
+
- {featureList.length ? ( -
- {featureList.map(({ text, Icon }, index) => ( -
- -

{text}

-
- ))} +
+
+ + {heroTagline}
- ) : null} -

- - {t('login.lead')} +

+

{heroTitle}

+

{heroSubtitle}

+
+ + {featureList.length ? ( +
    + {featureList.map(({ text, Icon }, index) => ( +
  • + + + + +

    {text}

    +
    +
  • + ))} +
+ ) : null} +
+ +

+ + {leadCopy}

-
-
-
-
-
-
-

{t('login.title')}

-

{t('login.panel_copy')}

-
- - {oauthError ? ( - - {t('login.oauth_error_title')} - {t('login.oauth_error', { message: oauthError })} - - ) : null} - - - -

- {t('login.support')} -

+
+
+
+
+ + {t('login.badge', 'Fotospiel Event Admin')} + +
+

{panelTitle}

+

{panelCopy}

+ + {oauthError ? ( + + {t('login.oauth_error_title')} + {t('login.oauth_error', { message: oauthError })} + + ) : null} + + + +
+

{leadCopy}

+

{supportCopy}

+
+ + {featureList.length ? ( +
+ {featureList.map(({ text, Icon }, index) => ( +
+ + + +

{text}

+
+ ))} +
+ ) : null}
diff --git a/resources/js/admin/pages/components/InviteLayoutCustomizerPanel.tsx b/resources/js/admin/pages/components/InviteLayoutCustomizerPanel.tsx index 60108ce..cf0cb22 100644 --- a/resources/js/admin/pages/components/InviteLayoutCustomizerPanel.tsx +++ b/resources/js/admin/pages/components/InviteLayoutCustomizerPanel.tsx @@ -54,6 +54,7 @@ import { triggerDownloadFromBlob, triggerDownloadFromDataUrl, } from './invite-layout/export-utils'; +import { buildDownloadFilename, normalizeEventDateSegment } from './invite-layout/fileNames'; export type { QrLayoutCustomization } from './invite-layout/schema'; @@ -182,6 +183,7 @@ function serializeElements(elements: LayoutElement[], context: LayoutSerializati type InviteLayoutCustomizerPanelProps = { invite: EventQrInvite | null; eventName: string; + eventDate: string | null; saving: boolean; resetting: boolean; onSave: (customization: QrLayoutCustomization) => Promise; @@ -199,6 +201,7 @@ const ZOOM_STEP = 0.05; export function InviteLayoutCustomizerPanel({ invite, eventName, + eventDate, saving, resetting, onSave, @@ -1391,7 +1394,12 @@ export function InviteLayoutCustomizerPanel({ } const normalizedFormat = format.toLowerCase(); - const filenameStem = `${invite.token || 'invite'}-${normalizedFormat}`; + const eventDateSegment = normalizeEventDateSegment(eventDate); + const filename = buildDownloadFilename( + ['Einladungslayout', eventName, activeLayout?.name ?? null, eventDateSegment], + normalizedFormat, + 'einladungslayout', + ); setDownloadBusy(normalizedFormat); setError(null); @@ -1412,14 +1420,14 @@ export function InviteLayoutCustomizerPanel({ if (normalizedFormat === 'png') { const dataUrl = await generatePngDataUrl(exportOptions); - await triggerDownloadFromDataUrl(dataUrl, `${filenameStem}.png`); + await triggerDownloadFromDataUrl(dataUrl, filename); } else if (normalizedFormat === 'pdf') { const pdfBytes = await generatePdfBytes( exportOptions, 'a4', 'portrait', ); - triggerDownloadFromBlob(new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' }), `${filenameStem}.pdf`); + triggerDownloadFromBlob(new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' }), filename); } else { throw new Error(`Unsupported format: ${normalizedFormat}`); } @@ -1509,34 +1517,8 @@ export function InviteLayoutCustomizerPanel({ return (
-
-
-

{t('invites.customizer.heading', 'Layout anpassen')}

-

{t('invites.customizer.copy', 'Bearbeite Texte, Farben und Positionen direkt neben der Live-Vorschau. Änderungen werden sofort sichtbar.')}

-
-
- - -
+
+ {renderActionButtons('inline')}
{error ? ( @@ -1845,7 +1827,7 @@ export function InviteLayoutCustomizerPanel({ -
+
{renderActionButtons('inline')}
diff --git a/resources/js/admin/pages/components/invite-layout/DesignerCanvas.tsx b/resources/js/admin/pages/components/invite-layout/DesignerCanvas.tsx index be18991..9588cd5 100644 --- a/resources/js/admin/pages/components/invite-layout/DesignerCanvas.tsx +++ b/resources/js/admin/pages/components/invite-layout/DesignerCanvas.tsx @@ -239,7 +239,7 @@ export function DesignerCanvas({ const elementId = target.elementId; const bounds = target.getBoundingRect(); - let nextPatch: Partial = { + const nextPatch: Partial = { x: clamp(Math.round(bounds.left ?? 0), 20, CANVAS_WIDTH - 20), y: clamp(Math.round(bounds.top ?? 0), 20, CANVAS_HEIGHT - 20), }; diff --git a/resources/js/admin/pages/components/invite-layout/fileNames.ts b/resources/js/admin/pages/components/invite-layout/fileNames.ts new file mode 100644 index 0000000..d9ebe00 --- /dev/null +++ b/resources/js/admin/pages/components/invite-layout/fileNames.ts @@ -0,0 +1,52 @@ +export function sanitizeFilenameSegment(value: string | null | undefined, fallback = ''): string { + if (typeof value !== 'string') { + return fallback; + } + + const normalized = value + .trim() + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, ''); + + const slug = normalized.replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase(); + + return slug.length ? slug : fallback; +} + +export function normalizeEventDateSegment(dateValue: string | null | undefined): string | null { + if (!dateValue) { + return null; + } + + const trimmed = dateValue.trim(); + if (!trimmed) { + return null; + } + + const isoCandidate = trimmed.slice(0, 10); + if (/^\d{4}-\d{2}-\d{2}$/.test(isoCandidate)) { + return isoCandidate; + } + + const parsed = new Date(trimmed); + if (Number.isNaN(parsed.getTime())) { + return null; + } + + return parsed.toISOString().slice(0, 10); +} + +export function buildDownloadFilename( + parts: Array, + extension: string, + fallback = 'download', +): string { + const sanitizedParts = parts + .map((part) => sanitizeFilenameSegment(part, '')) + .filter((segment) => segment.length > 0); + + const base = sanitizedParts.length ? sanitizedParts.join('-') : fallback; + const cleanExtension = sanitizeFilenameSegment(extension, '').replace(/[^a-z0-9]/gi, '') || 'bin'; + + return `${base}.${cleanExtension.toLowerCase()}`; +} diff --git a/resources/js/layouts/app/Header.tsx b/resources/js/layouts/app/Header.tsx index aa761d1..14ba346 100644 --- a/resources/js/layouts/app/Header.tsx +++ b/resources/js/layouts/app/Header.tsx @@ -8,11 +8,11 @@ import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; import { Separator } from '@/components/ui/separator'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; -import { Sun, Moon, Menu, X, ChevronRight } from 'lucide-react'; +import { Sun, Moon, Menu, X, Languages, UserRound } from 'lucide-react'; import { cn } from '@/lib/utils'; import { NavigationMenu, @@ -31,10 +31,15 @@ const Header: React.FC = () => { const { localizedPath } = useLocalizedRoutes(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const toggleTheme = () => { - const newAppearance = appearance === 'dark' ? 'light' : 'dark'; - updateAppearance(newAppearance); + const setTheme = useCallback((mode: 'light' | 'dark') => { + if (appearance !== mode) { + updateAppearance(mode); + } setMobileMenuOpen(false); + }, [appearance, updateAppearance]); + + const toggleTheme = () => { + setTheme(appearance === 'dark' ? 'light' : 'dark'); }; const handleLanguageChange = useCallback(async (value: string) => { @@ -68,49 +73,61 @@ const Header: React.FC = () => { }); }; - const navItems = useMemo(() => ([ - { - key: 'home', - label: t('header.home', 'Home'), - href: localizedPath('/'), - }, - { - key: 'packages', - label: t('header.packages', 'Pakete'), - href: localizedPath('/packages'), - }, - { - key: 'blog', - label: t('header.blog', 'Blog'), - href: localizedPath('/blog'), - }, - { - key: 'occasions', - label: t('header.occasions.label', 'Anlässe'), - children: [ - { - key: 'wedding', - label: t('header.occasions.wedding', 'Hochzeit'), - href: localizedPath('/anlaesse/hochzeit'), - }, - { - key: 'birthday', - label: t('header.occasions.birthday', 'Geburtstag'), - href: localizedPath('/anlaesse/geburtstag'), - }, - { - key: 'corporate', - label: t('header.occasions.corporate', 'Firmenevent'), - href: localizedPath('/anlaesse/firmenevent'), - }, - ], - }, - { - key: 'contact', - label: t('header.contact', 'Kontakt'), - href: localizedPath('/kontakt'), - }, - ]), [localizedPath, t]); + const ctaHref = localizedPath('/demo'); + + const navItems = useMemo(() => { + const homeHref = localizedPath('/'); + const howItWorksHref = localizedPath('/so-funktionierts'); + + return [ + { + key: 'howItWorks', + label: t('header.how_it_works', "So geht's"), + href: howItWorksHref, + }, + { + key: 'packages', + label: t('header.packages', 'Pakete'), + href: localizedPath('/packages'), + }, + { + key: 'occasions', + label: t('header.occasions.label', 'Anlässe'), + children: [ + { + key: 'wedding', + label: t('header.occasions.wedding', 'Hochzeiten'), + href: localizedPath('/anlaesse/hochzeit'), + }, + { + key: 'birthday', + label: t('header.occasions.birthday', 'Geburtstage'), + href: localizedPath('/anlaesse/geburtstag'), + }, + { + key: 'corporate', + label: t('header.occasions.corporate', 'Firmenfeiern'), + href: localizedPath('/anlaesse/firmenevent'), + }, + { + key: 'confirmation', + label: t('header.occasions.confirmation', 'Konfirmation/Jugendweihe'), + href: localizedPath('/anlaesse/konfirmation'), + }, + ], + }, + { + key: 'blog', + label: t('header.blog', 'Blog'), + href: localizedPath('/blog'), + }, + { + key: 'contact', + label: t('header.contact', 'Kontakt'), + href: localizedPath('/kontakt'), + }, + ]; + }, [localizedPath, t]); const handleNavSelect = useCallback(() => setMobileMenuOpen(false), []); @@ -125,25 +142,27 @@ const Header: React.FC = () => { - + {navItems.map((item) => ( {item.children ? ( <> - + {item.label} - +
    {item.children.map((child) => (
  • - {child.label} - + + › + + {child.label}
  • @@ -157,7 +176,7 @@ const Header: React.FC = () => { href={item.href} className={cn( navigationMenuTriggerStyle(), - "bg-transparent !text-lg font-medium text-gray-700 hover:bg-pink-50 hover:text-pink-600 dark:text-gray-300 dark:hover:bg-pink-950/20 dark:hover:text-pink-400 font-sans-marketing" + "bg-transparent px-3 py-1.5 !text-lg font-medium text-gray-700 hover:bg-pink-50 hover:text-pink-600 dark:text-gray-300 dark:hover:bg-pink-950/20 dark:hover:text-pink-400 font-sans-marketing" )} > {item.label} @@ -168,26 +187,49 @@ const Header: React.FC = () => { ))} -
    - - + + + + + + + {t('header.appearance', 'Darstellung')} + + setTheme('light')} className="font-sans-marketing"> + + {t('header.appearance_light', 'Hell')} + + setTheme('dark')} className="font-sans-marketing"> + + {t('header.appearance_dark', 'Dunkel')} + + + + {t('header.language', 'Sprache')} + + + + Deutsch + + + English + + + + {auth.user ? ( @@ -223,14 +265,16 @@ const Header: React.FC = () => { ) : ( - <> + )}
    @@ -279,11 +323,11 @@ const Header: React.FC = () => { + {child.label} - ))} @@ -305,6 +349,15 @@ const Header: React.FC = () => {
    + + + {t('header.cta', 'Jetzt ausprobieren')} + +
    Darstellung +
    +
    + +
    +
    +
    +
    + + + + + Fotospiel + {title} + + +
    +

    {title}

    +

    {description}

    +
    +
    + + {children} +
    - {children}
diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index 4c4f081..793cb3d 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -35,17 +35,20 @@ export default function Login({ status, canResetPassword }: LoginProps) { }); }; + const errorKeys = Object.keys(errors); + const hasErrors = errorKeys.length > 0; + useEffect(() => { if (!hasTriedSubmit) { return; } - const errorKeys = Object.keys(errors); - if (errorKeys.length === 0) { + const keys = Object.keys(errors); + if (keys.length === 0) { return; } - const field = document.querySelector(`[name="${errorKeys[0]}"]`); + const field = document.querySelector(`[name="${keys[0]}"]`); if (field) { field.scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -57,10 +60,17 @@ export default function Login({ status, canResetPassword }: LoginProps) { -
-
+ +
+ +
- + + -
- + {canResetPassword && ( - + {t('login.forgot')} )} @@ -105,44 +126,69 @@ export default function Login({ status, canResetPassword }: LoginProps) { clearErrors('password'); } }} + className="h-12 rounded-xl border-gray-200/80 bg-white/90 px-4 text-base shadow-inner shadow-gray-200/40 focus-visible:ring-[#ff8bb1]/40 dark:border-gray-800/70 dark:bg-gray-900/60 dark:text-gray-100" + /> + -
-
+
setData('remember', Boolean(checked))} /> - +
- -
-
+ + +
+ {status && ( +
+ {status} +
+ )} + + {hasErrors && ( +
+ {Object.values(errors).join(' ')} +
+ )} +
+ +
{t('login.no_account')}{' '} - + {t('login.sign_up')}
- - {status &&
{status}
} - - {Object.keys(errors).length > 0 && ( -
-

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

-
- )} ); } diff --git a/resources/js/pages/marketing/Demo.tsx b/resources/js/pages/marketing/Demo.tsx new file mode 100644 index 0000000..308b796 --- /dev/null +++ b/resources/js/pages/marketing/Demo.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { Head, Link } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; +import MarketingLayout from '@/layouts/mainWebsite'; +import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Sparkles, CheckCircle2 } from 'lucide-react'; + +type DemoFeature = { title: string; description: string }; + +const DEMO_TOKEN = 'mdhyA5XSVEVEabF8JhZ85B6fMocyyRMTfmThSeUKPzk7LLTu'; + +const DemoPage: React.FC = () => { + const { t } = useTranslation('marketing'); + const { localizedPath } = useLocalizedRoutes(); + + const demo = t('demo_page', { returnObjects: true }) as { + title: string; + subtitle: string; + primaryCta: string; + secondaryCta: string; + iframeNote: string; + openFull: string; + features: DemoFeature[]; + }; + + return ( + + + +
+
+
+
+ + Demo Live + +

+ {demo.title} +

+

+ {demo.subtitle} +

+
+ + +
+
+
+
+
+