diff --git a/.gitignore b/.gitignore index 945e31c..c2c101b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ fotospiel-tenant-app .phpactor.json .phpunit.result.cache Homestead.json +gogs.ini +stripe.exe Homestead.yaml npm-debug.log yarn-error.log diff --git a/AGENTS.md b/AGENTS.md index 458c77e..cc0d349 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ This repository hosts a multi-tenant event photo platform (Laravel 12, PHP 8.3, ## Tools & Permissions - Languages/Frameworks: PHP 8.3 (Laravel 12), JS/TS (React/Vite/Tailwind), Filament 4. - Dev Commands: composer, npm, vite, artisan, PHPUnit, Pint/ESLint, Docker/Compose (for dev). -- Git Hosting: Gogs at http://nas:10880 (token found locally in gogs.ini, never printed or committed). +- Git Hosting: Gogs at http://192.168.78.2:10880 (token found locally in gogs.ini, never printed or committed). - Issue API: Gogs REST /api/v1 for labels/issues/milestones (token auth). - Libraries: simplesoftwareio/simple-qrcode for server-side QR generation. diff --git a/neu 6.txt b/docs/checkout wizard flow.txt similarity index 100% rename from neu 6.txt rename to docs/checkout wizard flow.txt diff --git a/docs/legal/agb-de.md b/docs/legal/agb-de.md new file mode 100644 index 0000000..e1f506c --- /dev/null +++ b/docs/legal/agb-de.md @@ -0,0 +1,118 @@ +# Allgemeine Geschäftsbedingungen (AGB) für „Die Fotospiel App“ + +**Stand:** Oktober 2025 + +**Anbieter:** +Sören Eberhardt-Biermann +Schweriner Str. 15 +19306 Neustadt-Glewe +Mobil 0173 / 9266802 +W-IdNr. DE 428754098 +E-Mail: [Kontakt siehe Impressum](https://fotospiel.app/impressum) + +--- + +## 1. Geltungsbereich +Diese AGB regeln sämtliche Nutzungsverträge über die App und Online-Dienste „Die Fotospiel App“ zwischen dem Anbieter und den Kunden (Veranstalter, Agenturen, Reseller oder – bei Direktbuchung – Verbraucher). +Abweichende Bedingungen finden keine Anwendung, es sei denn sie werden vom Anbieter schriftlich bestätigt. +Für Teilnehmer (Gäste) gelten ergänzend die Nutzungsbedingungen des jeweiligen Events. + +--- + +## 2. Leistungsbeschreibung +1. Die App ermöglicht Veranstaltern die Durchführung von Foto-Events mit Aufgaben, bei denen Gäste Bilder hochladen, ansehen und bewerten können. +2. Der Leistungsumfang richtet sich nach dem gebuchten Paket (Upload-Limit, Anzahl Aufgaben, Teilnehmerzahl, Design-Optionen u. a.). +3. Die Bereitstellung erfolgt als **Software-as-a-Service** (SaaS) über Server der Hetzner Online GmbH in Deutschland. + +--- + +## 3. Vertragsschluss +Der Vertrag kommt zustande, wenn der Kunde ein Paket bucht und der Anbieter die Buchung elektronisch bestätigt (z. B. per E-Mail). +Die Darstellung auf der Website stellt kein bindendes Angebot dar. + +--- + +## 4. Nutzungsrechte an der App +Der Kunde erhält für die gebuchte Laufzeit ein einfaches, nicht übertragbares Nutzungsrecht zur vertragsgemäßen Verwendung der App. +Quellcode, Design und Systemarchitektur bleiben alleiniges Eigentum des Anbieters. +Eine Weitergabe oder kommerzielle Vermietung ist nur bei vorliegendem Resellervertrag zulässig. + +--- + +## 5. Pflichten des Kunden +1. Der Kunde trägt die Verantwortung für sämtliche von ihm oder seinen Teilnehmern eingestellten Inhalte. +2. Er hat sicherzustellen, dass die Teilnehmer über die Zwecke der Verarbeitung informiert sind und Einwilligungen (z. B. bei Abbildungen von Personen) vorliegen. +3. Verboten sind rechtswidrige, jugendgefährdende oder verletzende Inhalte. Der Anbieter kann solche Beiträge löschen oder sperren. +4. Zugangsdaten dürfen nicht an Dritte weitergegeben werden. + +--- + +## 6. Pflichten der Teilnehmer +Teilnehmer dürfen nur Fotos hochladen, an denen sie die erforderlichen Rechte besitzen. +Sie räumen dem Veranstalter eine einfache, zeitlich auf die Event- und Galerie-Dauer begrenzte Nutzungslizenz ein. +Der Anbieter verwendet Inhalte ausschließlich zur technischen Bereitstellung (Speicherung, Anzeige, Sicherungskopie). + +--- + +## 7. Preise und Zahlung +1. Es gelten die auf der Website veröffentlichten Preise zum Zeitpunkt der Buchung. +2. Alle Preise verstehen sich einschließlich gesetzlicher Umsatzsteuer. +3. Die Zahlung erfolgt im Voraus über **PayPal** oder **Stripe Checkout** (Kreditkarte, Apple Pay, Google Pay u. a.). +4. Bei Nutzung dieser Dienste gelten zusätzlich die AGB und Datenschutzhinweise der jeweiligen Anbieter: + – PayPal (Europe) S.à r.l. et Cie, S.C.A., L-2449 Luxembourg + – Stripe Payments Europe Ltd., Dublin, Irland +5. Der Anbieter erhält von diesen Diensten nur Zahlungs- und Statusinformationen zur Abwicklung. +6. Rechnungen werden elektronisch bereitgestellt. + +--- + +## 8. Verfügbarkeit und Wartung +Der Anbieter bemüht sich um eine hohe Verfügbarkeit (Hosting bei Hetzner). +Kurzzeitige Ausfälle durch Wartung, Updates oder höhere Gewalt sind möglich. +Ein konkreter Verfügbarkeitsgrad wird nicht geschuldet. + +--- + +## 9. Haftung +1. Der Anbieter haftet bei Vorsatz und grober Fahrlässigkeit uneingeschränkt. +2. Bei einfacher Fahrlässigkeit nur bei Verletzung wesentlicher Pflichten und begrenzt auf den vorhersehbaren Schaden. +3. Keine Haftung für rechtswidrige Inhalte oder Verluste durch unsachgemäße Nutzung. +4. Haftung für Personenschäden bleibt unberührt. + +--- + +## 10. Datenschutz +1. Personenbezogene Daten werden gemäß der **Datenschutzerklärung** verarbeitet ([https://fotospiel.app/datenschutz](https://fotospiel.app/datenschutz)). +2. Der Betrieb erfolgt auf Servern der **Hetzner Online GmbH**, mit der ein Auftragsverarbeitungsvertrag besteht. +3. Analysen erfolgen mit **Matomo**, nur mit technisch notwendigen Cookies. + +--- + +## 11. Laufzeit und Kündigung +Der Vertrag endet automatisch nach Ablauf der gebuchten Event- bzw. Galeriedauer. +Eine ordentliche Kündigung während der Laufzeit ist ausgeschlossen. +Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt bestehen. + +--- + +## 12. Löschung und Sperrung von Inhalten +Der Anbieter ist berechtigt, Inhalte oder Konten zu löschen oder zu sperren, wenn Rechtsverstöße oder Beschwerden vorliegen oder technische Gründe es erfordern. + +--- + +## 13. Änderungen der AGB +Änderungen werden dem Kunden in Textform mitgeteilt. Widerspricht der Kunde nicht innerhalb eines Monats, gelten die neuen AGB als angenommen. + +--- + +## 14. Streitbeilegung und Gerichtsstand +1. Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts. +2. Gerichtsstand für Kaufleute ist Neustadt-Glewe. +3. Die EU-Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: + Der Anbieter ist nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen. + +--- + +## 15. Schlussbestimmungen +Sollten einzelne Bestimmungen unwirksam sein, bleibt die Wirksamkeit der übrigen unberührt. +Nebenabreden bedürfen der Textform. diff --git a/docs/legal/agb-en.md b/docs/legal/agb-en.md new file mode 100644 index 0000000..378f68d --- /dev/null +++ b/docs/legal/agb-en.md @@ -0,0 +1,119 @@ +# Terms and Conditions (T&C) for "The Fotospiel App" + +**Last updated:** October 2025 + +**Provider:** +Sören Eberhardt-Biermann +Schweriner Str. 15 +19306 Neustadt-Glewe, Germany +Mobile +49 173 9266802 +Business ID: DE 428754098 +Email: [Contact via imprint](https://fotospiel.app/impressum) + +--- + +## 1. Scope +These Terms govern all usage agreements for the app and online services "The Fotospiel App" between the Provider and Customers (event organizers, agencies, resellers, or—if booked directly—consumers). +Deviating conditions shall not apply unless expressly confirmed in writing by the Provider. +For event participants (guests), the respective event’s participant terms apply in addition. + +--- + +## 2. Description of Services +1. The App allows organizers to host photo events with creative tasks where guests can upload, view, and like photos. +2. The scope of services depends on the selected package (upload limits, number of tasks, participants, design options, etc.). +3. The service is provided as **Software-as-a-Service (SaaS)** via servers of Hetzner Online GmbH, Germany. + +--- + +## 3. Conclusion of Contract +A contract is formed when the Customer books a package and the Provider confirms the booking electronically (e.g., by email). +The presentation on the website does not constitute a binding offer. + +--- + +## 4. Rights of Use +The Customer receives a simple, non-transferable right to use the App for the booked period. +All source code, design, and infrastructure remain the sole property of the Provider. +Transfer or subleasing is only permitted under an existing reseller agreement. + +--- + +## 5. Customer Obligations +1. The Customer is responsible for all content uploaded by themselves or their participants. +2. The Customer must ensure that participants are informed about data processing purposes and that required consents (e.g., image rights) are obtained. +3. Unlawful, offensive, or harmful content is prohibited; the Provider may remove or block such content. +4. Access data must be kept confidential. + +--- + +## 6. Participant Obligations +Participants may upload only photos for which they hold the necessary rights. +They grant the organizer a simple, time-limited license for use during the event and gallery period. +The Provider uses such content solely for technical purposes (storage, display, backups). + +--- + +## 7. Prices and Payment +1. Prices valid at the time of booking apply. +2. All prices include VAT, unless otherwise stated. +3. Payment is made in advance via **PayPal** or **Stripe Checkout** (credit card, Apple Pay, Google Pay, etc.). +4. The payment process is governed by the respective provider’s terms: + – PayPal (Europe) S.à r.l. et Cie, S.C.A., L-2449 Luxembourg + – Stripe Payments Europe Ltd., Dublin, Ireland +5. The Provider only receives transaction and payment status data necessary for processing. +6. Invoices are issued electronically. + +--- + +## 8. Availability and Maintenance +The Provider strives for high availability (hosting via Hetzner). +Short interruptions due to maintenance, updates, or force majeure may occur. +No specific uptime is guaranteed. + +--- + +## 9. Liability +1. The Provider is fully liable for intent and gross negligence. +2. For ordinary negligence, liability is limited to essential contractual obligations and foreseeable damages. +3. The Provider is not liable for unlawful content or data loss caused by improper use. +4. Liability for personal injury remains unaffected. + +--- + +## 10. Data Protection +1. Personal data is processed in accordance with the **Privacy Policy** ([https://fotospiel.app/datenschutz](https://fotospiel.app/datenschutz)). +2. Operation takes place on servers of **Hetzner Online GmbH** under a data processing agreement (Art. 28 GDPR). +3. Web analytics are conducted via **Matomo**, using only technically necessary cookies. + +--- + +## 11. Term and Termination +The contract ends automatically after the booked event or gallery period expires. +Ordinary termination during the contract term is excluded. +The right to extraordinary termination for good cause remains unaffected. + +--- + +## 12. Deletion and Suspension +The Provider may delete or suspend content or accounts if legal violations, complaints, or technical issues arise. + +--- + +## 13. Amendments +The Provider will inform Customers of changes in text form. +If the Customer does not object within one month, the new terms are deemed accepted. + +--- + +## 14. Dispute Resolution and Jurisdiction +1. German law applies, excluding the UN Convention on Contracts for the International Sale of Goods. +2. For merchants, the place of jurisdiction is Neustadt-Glewe, Germany. +3. The EU Commission provides an online dispute resolution (ODR) platform: + The Provider is neither obliged nor willing to participate in consumer arbitration proceedings. + +--- + +## 15. Final Provisions +Should any provision be invalid, the remaining provisions remain in effect. +Side agreements require text form. diff --git a/docs/legal/datenschutz-de.md b/docs/legal/datenschutz-de.md new file mode 100644 index 0000000..6c6d9fe --- /dev/null +++ b/docs/legal/datenschutz-de.md @@ -0,0 +1,124 @@ +# Datenschutzerklärung +**Stand:** Oktober 2025 + +## 1. Verantwortlicher +Verantwortlich für die Datenverarbeitung im Sinne der Datenschutz-Grundverordnung (DSGVO): + +**Sören Eberhardt-Biermann** +Schweriner Str. 15 +19306 Neustadt-Glewe +Deutschland + +E-Mail: info@fotospiel.app +Website: [https://fotospiel.app](https://fotospiel.app) + +--- + +## 2. Allgemeines zur Datenverarbeitung +Wir verarbeiten personenbezogene Daten ausschließlich im Rahmen der geltenden Datenschutzgesetze, insbesondere der DSGVO und des BDSG. +Die Nutzung der Fotospiel App ist grundsätzlich nur mit den personenbezogenen Daten erforderlich, die für die Durchführung eines Foto-Events und die Bereitstellung der Funktionen notwendig sind. + +--- + +## 3. Arten der verarbeiteten Daten +- Veranstalterdaten: Name, E-Mail-Adresse, Zahlungsinformationen (über PayPal/Stripe), Eventdaten (Titel, Datum, Aufgaben, Bilder) +- Nutzerdaten (Gäste): hochgeladene Fotos, Anzeigename (frei wählbar), Reaktionen/Likes +- Technische Daten: IP-Adresse, Browsertyp, Zeitstempel, Geräteinformationen +- Kommunikationsdaten: Inhalte von Kontaktanfragen über das Formular oder per E-Mail + +--- + +## 4. Zweck und Rechtsgrundlage der Verarbeitung +| Zweck der Verarbeitung | Rechtsgrundlage | Beschreibung | +|------------------------|----------------|---------------| +| Bereitstellung der App und Durchführung von Veranstaltungen | Art. 6 Abs. 1 lit. b DSGVO | Nutzung der App durch Veranstalter und Gäste | +| Speicherung und Anzeige von Fotos innerhalb des Events | Art. 6 Abs. 1 lit. b DSGVO | Durchführung der Fotospiel-Funktionalität | +| Abrechnung und Zahlungsabwicklung | Art. 6 Abs. 1 lit. b, lit. c DSGVO | Nutzung der Dienste von PayPal und Stripe | +| Webanalyse über Matomo (selbst gehostet) | Art. 6 Abs. 1 lit. f DSGVO | Statistische Auswertung zur Verbesserung der App | +| Sicherheit, Server-Logs | Art. 6 Abs. 1 lit. f DSGVO | Sicherstellung des Betriebs, Fehleranalyse | +| Beantwortung von Kontaktanfragen | Art. 6 Abs. 1 lit. f oder lit. b DSGVO | Kommunikation mit Nutzern und Interessenten | + +--- + +## 5. Hosting und Auftragsverarbeitung +Unsere Server werden bei **Hetzner Online GmbH**, Industriestr. 25, 91710 Gunzenhausen, Deutschland, betrieben. +Mit Hetzner besteht ein Vertrag zur Auftragsverarbeitung (Art. 28 DSGVO). +Die Verarbeitung erfolgt ausschließlich innerhalb der EU. + +--- + +## 6. Zahlungsabwicklung +Die Zahlungsabwicklung erfolgt über **PayPal (Europe) S.à r.l. et Cie, S.C.A.** und **Stripe Payments Europe, Ltd.** +Bei der Zahlung werden personenbezogene Daten an diese Dienstleister übermittelt. +Wir speichern keine Zahlungs- oder Kreditkartendaten. +Rechtsgrundlage: Art. 6 Abs. 1 lit. b und lit. c DSGVO. + +Datenschutzhinweise der Anbieter: +- PayPal: https://www.paypal.com/de/webapps/mpp/ua/privacy-full +- Stripe: https://stripe.com/de/privacy + +--- + +## 7. Webanalyse mit Matomo +Wir verwenden **Matomo** (lokal gehostet) zur Analyse des Nutzerverhaltens. +Es werden keine Daten an Dritte übermittelt. +IP-Adressen werden anonymisiert gespeichert. +Nur technisch notwendige Cookies werden gesetzt. +Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO. + +--- + +## 8. Cookies +Es werden ausschließlich technisch notwendige Cookies verwendet. +Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO. +Eine Einwilligung ist nicht erforderlich. + +--- + +## 9. Löschfristen +| Datenart | Löschfrist | Begründung | +|-----------|-------------|-------------| +| Fotos | Innerhalb von 30 Tagen nach Ablauf der Event-Speicherdauer | Automatische Löschung | +| Benutzerkonten (Gastgeber) | Nach 24 Monaten Inaktivität | Vertragserfüllung abgeschlossen | +| Zahlungsdaten | 10 Jahre | Gesetzliche Aufbewahrungspflichten | +| Server-Logs | 7 Tage | IT-Sicherheit | +| Kontaktanfragen | Max. 6 Monate | Nach Bearbeitung gelöscht | + +--- + +## 10. Weitergabe an Dritte +Eine Weitergabe erfolgt nur an: +- Zahlungsdienstleister (PayPal, Stripe) +- Hoster (Hetzner) +- Gesetzlich erforderliche Stellen (z. B. Finanzbehörden) + +Keine Übermittlung in Drittländer außerhalb der EU. + +--- + +## 11. Rechte der betroffenen Personen +Betroffene Personen haben folgende Rechte: +- Auskunft (Art. 15 DSGVO) +- Berichtigung (Art. 16 DSGVO) +- Löschung (Art. 17 DSGVO) +- Einschränkung (Art. 18 DSGVO) +- Datenübertragbarkeit (Art. 20 DSGVO) +- Widerspruch (Art. 21 DSGVO) + +# Zur Ausübung genügt eine Mitteilung an: info@fotospiel.app + +--- + +## 12. Widerruf von Einwilligungen +Sofern die Verarbeitung auf Einwilligung beruht, kann diese jederzeit mit Wirkung für die Zukunft widerrufen werden. + +--- + +## 13. Sicherheit der Datenverarbeitung +Wir setzen technische und organisatorische Maßnahmen zur Sicherung Ihrer Daten ein (z. B. Verschlüsselung, Zugriffsbeschränkungen, Backups). + +--- + +## 14. Änderungen dieser Datenschutzerklärung +Wir behalten uns vor, diese Datenschutzerklärung anzupassen. +Die jeweils aktuelle Fassung ist unter [https://fotospiel.app/datenschutz](https://fotospiel.app/datenschutz) abrufbar. diff --git a/docs/legal/datenschutz-en.md b/docs/legal/datenschutz-en.md new file mode 100644 index 0000000..031dda3 --- /dev/null +++ b/docs/legal/datenschutz-en.md @@ -0,0 +1,123 @@ +# Privacy Policy +**Last updated:** October 2025 + +## 1. Data Controller +Responsible under the General Data Protection Regulation (GDPR): + +**Sören Eberhardt-Biermann** +Schweriner Str. 15 +19306 Neustadt-Glewe +Germany + +Email: info@fotospiel.app +Website: [https://fotospiel.app](https://fotospiel.app) + +--- + +## 2. General Information +We process personal data in compliance with the GDPR and the German Federal Data Protection Act (BDSG). +Use of the Fotospiel App requires only the personal data necessary to host and participate in photo events. + +--- + +## 3. Types of Data Processed +- Organizer data: name, email address, payment information (via PayPal/Stripe), event details (title, date, photo tasks, photos) +- Guest data: uploaded photos, display name (optional), likes/reactions +- Technical data: IP address, browser type, timestamp, device information +- Communication data: messages sent via contact form or email + +--- + +## 4. Purpose and Legal Basis of Processing +| Purpose | Legal Basis | Description | +|----------|--------------|-------------| +| Providing the app and hosting events | Art. 6(1)(b) GDPR | Contract performance | +| Storing and displaying photos | Art. 6(1)(b) GDPR | Core feature of the app | +| Payment processing and invoicing | Art. 6(1)(b), (c) GDPR | Use of PayPal and Stripe services | +| Web analytics via Matomo | Art. 6(1)(f) GDPR | Statistical analysis to improve the app | +| Server logs and security | Art. 6(1)(f) GDPR | Ensuring system security | +| Responding to inquiries | Art. 6(1)(f) or (b) GDPR | Communication with users | + +--- + +## 5. Hosting and Data Processing +Our servers are operated by **Hetzner Online GmbH**, Industriestr. 25, 91710 Gunzenhausen, Germany. +A data processing agreement pursuant to Art. 28 GDPR is in place. +All processing takes place within the EU. + +--- + +## 6. Payment Processing +Payments are handled by **PayPal (Europe) S.à r.l. et Cie, S.C.A.** and **Stripe Payments Europe, Ltd.** +We do not store payment or credit card data. +Legal basis: Art. 6(1)(b) and (c) GDPR. + +Privacy policies: +- PayPal: https://www.paypal.com/de/webapps/mpp/ua/privacy-full +- Stripe: https://stripe.com/de/privacy + +--- + +## 7. Web Analytics with Matomo +We use **Matomo** (self-hosted) for anonymous usage analysis. +No data is shared with third parties. +IP addresses are anonymized. +Only technically necessary cookies are used. +Legal basis: Art. 6(1)(f) GDPR. + +--- + +## 8. Cookies +Only technically necessary cookies are used. +Legal basis: Art. 6(1)(f) GDPR. +No consent is required. + +--- + +## 9. Data Retention Periods +| Data Type | Retention Period | Reason | +|------------|------------------|--------| +| Photos | Deleted within 30 days after the booked storage period ends | Automatic deletion | +| User accounts (hosts) | Deleted after 24 months of inactivity | Contract completed | +| Payment data | 10 years | Legal retention obligations | +| Server logs | 7 days | IT security | +| Contact messages | Max. 6 months | After processing completed | + +--- + +## 10. Data Disclosure +Data is only shared with: +- Payment providers (PayPal, Stripe) +- Hosting provider (Hetzner) +- Public authorities when legally required + +No data is transferred outside the EU. + +--- + +## 11. Data Subject Rights +You have the following rights under GDPR: +- Right of access (Art. 15) +- Right to rectification (Art. 16) +- Right to erasure (Art. 17) +- Right to restriction of processing (Art. 18) +- Right to data portability (Art. 20) +- Right to object (Art. 21) + +Requests may be sent to: info@fotospiel.app + +--- + +## 12. Withdrawal of Consent +If processing is based on consent, you may withdraw it at any time with future effect. + +--- + +## 13. Data Security +We apply appropriate technical and organizational measures to secure your data, including encryption, access controls, and backups. + +--- + +## 14. Changes to this Privacy Policy +We may update this Privacy Policy to reflect legal or functional changes. +The current version is always available at [https://fotospiel.app/privacy](https://fotospiel.app/privacy). diff --git a/docs/legal/impressum-de.md b/docs/legal/impressum-de.md new file mode 100644 index 0000000..a45ca0b --- /dev/null +++ b/docs/legal/impressum-de.md @@ -0,0 +1,62 @@ +# Impressum + +**Angaben gemäß § 5 TMG** + +Sören Eberhardt-Biermann +handelnd unter **„Die Fotospiel.App“** +Schweriner Str. 15 +19306 Neustadt-Glewe +Deutschland + +**Kontakt** +Telefon: 0173 / 926 6802 +E-Mail: info@fotospiel.app + +**Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV:** +Sören Eberhardt-Biermann +Schweriner Str. 15 +19306 Neustadt-Glewe + +--- + +## Wirtschafts-Identifikationsnummer + +Wirtschafts-Identifikationsnummer gemäß § 139c Abgabenordnung: **DE428754098** +*(Hinweis: Keine Umsatzsteuer-Identifikationsnummer nach § 27a UStG vergeben.)* + +--- + +## Haftungsausschluss (Disclaimer) + +### Haftung für Inhalte +Die Inhalte dieser Website wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann der Anbieter jedoch keine Gewähr übernehmen. +Als Diensteanbieter ist der Anbieter gemäß § 7 Abs. 1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG ist der Anbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden entsprechender Rechtsverletzungen wird der Anbieter diese Inhalte umgehend entfernen. + +### Haftung für Links +Das Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte der Anbieter keinen Einfluss hat. Deshalb kann der Anbieter für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Bei Bekanntwerden von Rechtsverletzungen werden derartige Links umgehend entfernt. + +### Urheberrecht +Die durch den Seitenbetreiber erstellten Inhalte und Werke auf dieser Website unterliegen dem deutschen Urheberrecht. Beiträge Dritter sind als solche gekennzeichnet. +Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechts bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. +Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet. + +--- + +## Bild- und Grafiknachweise + +Sofern auf dieser Website Bilder, Icons oder Grafiken Dritter verwendet werden, sind deren Quellen und Lizenzhinweise entsprechend gekennzeichnet oder in einer separaten Quellenübersicht angegeben. + +--- + +## Alternative Streitbeilegung gemäß Art. 14 Abs. 1 ODR-VO und § 36 VSBG + +Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: +[https://ec.europa.eu/consumers/odr/](https://ec.europa.eu/consumers/odr/) + +Der Anbieter ist weder verpflichtet noch bereit, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen. + +--- + +## Hinweis zur Geschäftsform + +Der Anbieter betreibt die Website als Einzelunternehmer. Eine Eintragung im Handelsregister besteht nicht. diff --git a/docs/legal/impressum-en.md b/docs/legal/impressum-en.md new file mode 100644 index 0000000..e334086 --- /dev/null +++ b/docs/legal/impressum-en.md @@ -0,0 +1,73 @@ +# Legal Notice (Impressum) + +**Information according to § 5 German Telemedia Act (TMG)** + +Sören Eberhardt-Biermann +doing business as **"Die Fotospiel.App"** +Schweriner Str. 15 +19306 Neustadt-Glewe +Germany + +**Contact** +Phone: +49 173 9266802 +E-Mail: info@fotospiel.app + +**Responsible for the content according to § 18 (2) German State Media Treaty (MStV):** +Sören Eberhardt-Biermann +Schweriner Str. 15 +19306 Neustadt-Glewe +Germany + +--- + +## Business Identification Number + +Business Identification Number according to § 139c Fiscal Code (Abgabenordnung): **DE428754098** +*(Note: No VAT Identification Number according to § 27a German VAT Act has been issued.)* + +--- + +## Disclaimer + +### Liability for Content +The contents of this website were created with the greatest possible care. +However, the provider assumes no liability for the correctness, completeness, or up-to-date nature of the content. +As a service provider, the provider is responsible for its own content on these pages under the general laws in accordance with § 7 (1) TMG. +According to §§ 8 to 10 TMG, the provider is not obligated to monitor transmitted or stored third-party information or to investigate circumstances indicating illegal activity. +Obligations to remove or block the use of information in accordance with general laws remain unaffected. +However, liability in this regard is only possible from the time of knowledge of a specific infringement. +Upon becoming aware of such legal violations, the provider will remove this content immediately. + +### Liability for Links +This website contains links to external websites of third parties, the content of which the provider has no influence over. +Therefore, the provider cannot assume any liability for this external content. +The respective provider or operator of the linked pages is always responsible for their content. +Links will be removed immediately upon notification of any infringement. + +### Copyright +The content and works created by the site operator on this website are subject to German copyright law. +Contributions by third parties are marked as such. +Reproduction, editing, distribution, and any kind of exploitation outside the limits of copyright law require the written consent of the respective author or creator. +Downloads and copies of this site are permitted only for private, non-commercial use. + +--- + +## Image and Graphic Credits + +If third-party images, icons, or graphics are used on this website, their sources and license information are properly indicated or listed in a separate reference section. + +--- + +## Online Dispute Resolution according to Art. 14 (1) ODR-VO and § 36 VSBG + +The European Commission provides a platform for Online Dispute Resolution (ODR): +[https://ec.europa.eu/consumers/odr/](https://ec.europa.eu/consumers/odr/) + +The provider is neither obliged nor willing to participate in dispute resolution proceedings before a consumer arbitration board. + +--- + +## Note on Business Form + +The provider operates this website as a sole proprietor. +No entry in the commercial register exists. diff --git a/free-step1-home.png b/docs/screenshots/free-step1-home.png similarity index 100% rename from free-step1-home.png rename to docs/screenshots/free-step1-home.png diff --git a/free-step1-packages.png b/docs/screenshots/free-step1-packages.png similarity index 100% rename from free-step1-packages.png rename to docs/screenshots/free-step1-packages.png diff --git a/free-step2-packages.png b/docs/screenshots/free-step2-packages.png similarity index 100% rename from free-step2-packages.png rename to docs/screenshots/free-step2-packages.png diff --git a/paid-end-step1-packages.png b/docs/screenshots/paid-end-step1-packages.png similarity index 100% rename from paid-end-step1-packages.png rename to docs/screenshots/paid-end-step1-packages.png diff --git a/paid-res-step1-packages.png b/docs/screenshots/paid-res-step1-packages.png similarity index 100% rename from paid-res-step1-packages.png rename to docs/screenshots/paid-res-step1-packages.png diff --git a/PWA_Wireframes.txt b/docs/wireframes/PWA_Wireframes.txt similarity index 100% rename from PWA_Wireframes.txt rename to docs/wireframes/PWA_Wireframes.txt diff --git a/fotospiel_prp.bak b/fotospiel_prp.bak deleted file mode 100644 index ad0f986..0000000 --- a/fotospiel_prp.bak +++ /dev/null @@ -1,3740 +0,0 @@ -## 🏗️ **Multi-Tenant System-Architektur** - -Note: Event Type-aware UI -- Theme colors can come from `event_types.settings.palette` to skin headers/buttons per type. -- Emotion picker fetches type-filtered emotions via `/api/events/:slug/emotions`. -- Random task generator and gallery filters respect event type automatically via API. - - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ SYSTEM OVERVIEW (Multi-Tenant) │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 🔧 SUPER ADMIN 👰🤵 BRAUTPAARE 📱 GÄSTE PWA │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ System Admin │ │ Customer Admin │ │ React PWA │ │ -│ │ Panel │◄──►│ Panel │◄──►│ Event Photos │ │ -│ │ │ │ │ │ │ │ -│ │ • User Mgmt │ │ • Event Setup │ │ • Take Photos │ │ -│ │ • System Config │ │ • Photo Review │ │ • Upload Share │ │ -│ │ • Billing │ │ • Gallery Build │ │ • Live Feed │ │ -│ │ • Analytics │ │ • Export │ │ • Like/Comment │ │ -│ │ • Monitoring │ │ │ │ │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ -│ │ │ │ │ -│ └───────────────────────┼───────────────────────┘ │ -│ │ │ -│ ┌─────────────────────────────────────────────────────────────────────┐│ -│ │ LARAVEL BACKEND API (Multi-Tenant) ││ -│ │ • Tenant Isolation • User Management • Billing System ││ -│ │ • System Monitoring • Global Analytics • Platform Administration ││ -│ └─────────────────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -## 👑 **Super Admin System (Platform Management)** - -### **Multi-Tenant Database Schema:** -```sql --- Tenant/Customer Management -CREATE TABLE tenants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name VARCHAR(255) NOT NULL, -- "Familie Müller" - slug VARCHAR(255) UNIQUE NOT NULL, -- "familie-mueller" - domain VARCHAR(255) UNIQUE, -- Optional: custom domain - - -- Contact Information - contact_name VARCHAR(255) NOT NULL, - contact_email VARCHAR(255) NOT NULL, - contact_phone VARCHAR(255), - - -- Event-basierte Monetarisierung (keine Subscriptions) - event_credits_balance INTEGER DEFAULT 1, - free_event_granted_at TIMESTAMP, - - -- Limits & Quotas - max_photos_per_event INTEGER DEFAULT 500, - max_storage_mb INTEGER DEFAULT 1024, -- 1GB - - -- Feature Flags - features JSON, -- {"custom_branding": true, "api_access": false} - - -- Metadata - last_activity_at TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Erweiterte User-Tabelle für Multi-Tenancy -ALTER TABLE users ADD COLUMN tenant_id INTEGER; -ALTER TABLE users ADD COLUMN role ENUM('super_admin', 'tenant_admin', 'tenant_user') DEFAULT 'tenant_user'; -ALTER TABLE users ADD CONSTRAINT fk_users_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenants(id); - --- Events gehören zu Tenants -ALTER TABLE events ADD COLUMN tenant_id INTEGER NOT NULL; -ALTER TABLE events ADD CONSTRAINT fk_events_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenants(id); - --- System-weite Konfiguration -CREATE TABLE system_settings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key VARCHAR(255) UNIQUE NOT NULL, - value TEXT, - description TEXT, - is_public BOOLEAN DEFAULT 0, -- Für öffentliche API settings - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Platform Analytics -CREATE TABLE platform_analytics ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tenant_id INTEGER, - metric_name VARCHAR(100) NOT NULL, -- 'photos_uploaded', 'events_created' - metric_value INTEGER NOT NULL, - metric_date DATE NOT NULL, - metadata JSON, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - - FOREIGN KEY (tenant_id) REFERENCES tenants(id), - INDEX idx_analytics_date_metric (metric_date, metric_name), - INDEX idx_analytics_tenant (tenant_id, metric_date) -); - --- Event Purchases (one-time) und Guthaben-Ledger -CREATE TABLE event_purchases ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tenant_id INTEGER NOT NULL, - events_purchased INTEGER NOT NULL DEFAULT 1, - amount DECIMAL(10,2) NOT NULL, - currency VARCHAR(3) DEFAULT 'EUR', - provider ENUM('app_store', 'play_store', 'stripe', 'paypal') NOT NULL, - external_receipt_id VARCHAR(255), - status ENUM('pending', 'paid', 'failed', 'refunded', 'cancelled') DEFAULT 'pending', - purchased_at TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - - FOREIGN KEY (tenant_id) REFERENCES tenants(id), - INDEX idx_event_purchases_tenant (tenant_id, purchased_at) -); - -CREATE TABLE event_credits_ledger ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - tenant_id INTEGER NOT NULL, - delta INTEGER NOT NULL, - reason ENUM('initial_free','purchase','manual_adjustment','event_created','refund') NOT NULL, - related_purchase_id INTEGER, - note TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - - FOREIGN KEY (tenant_id) REFERENCES tenants(id), - FOREIGN KEY (related_purchase_id) REFERENCES event_purchases(id), - INDEX idx_ledger_tenant (tenant_id, created_at) -); -``` - -## 🛠️ **Super Admin Panel (Separate Filament Panel)** - -Note (Filament 4): -- Forms/Infolists components live under `Filament\Schema\Components` (updated below). -- Table row actions use `recordActions()`; actions are unified under `Filament\Actions`. -- Keep `Tables\Columns`/`Tables\Filters` as-is; columns/filters remain in Tables. - -### **Super Admin Panel Provider:** -```php -id('super-admin') - ->path('super-admin') - ->login() - ->colors([ - 'primary' => Color::Indigo, - 'secondary' => Color::Slate, - ]) - ->brandName('Fotobox Platform Admin') - ->brandLogo(asset('images/platform-logo.png')) - ->discoverResources(in: app_path('Filament/SuperAdmin/Resources'), for: 'App\\Filament\\SuperAdmin\\Resources') - ->discoverPages(in: app_path('Filament/SuperAdmin/Pages'), for: 'App\\Filament\\SuperAdmin\\Pages') - ->pages([ - Pages\Dashboard::class, - ]) - ->widgets([ - \App\Filament\SuperAdmin\Widgets\PlatformStatsWidget::class, - \App\Filament\SuperAdmin\Widgets\RevenueChartWidget::class, - \App\Filament\SuperAdmin\Widgets\TenantActivityWidget::class, - ]) - ->navigationGroups([ - 'Customer Management', - 'Platform Configuration', - 'Legal & Compliance', - 'Billing & Events', - 'System Monitoring', - 'Content Management' - ]) - ->authMiddleware([ - \App\Http\Middleware\SuperAdminOnly::class, - ]); - } -} -``` - -## 👥 **Tenant Management Resource** - -```php -schema([ - Schema\\Components\\Section::make('Kunden-Details') - ->schema([ - Schema\\Components\\TextInput::make('name') - ->label('Kundenname') - ->required() - ->maxLength(255), - - Schema\\Components\\TextInput::make('slug') - ->required() - ->unique(Tenant::class, 'slug', ignoreRecord: true) - ->helperText('Eindeutige URL für den Kunden'), - - Schema\\Components\\TextInput::make('domain') - ->label('Custom Domain (optional)') - ->url() - ->unique(Tenant::class, 'domain', ignoreRecord: true), - - Schema\\Components\\TextInput::make('contact_name') - ->label('Ansprechpartner') - ->required(), - - Schema\\Components\\TextInput::make('contact_email') - ->label('E-Mail') - ->email() - ->required(), - - Schema\\Components\\TextInput::make('contact_phone') - ->label('Telefon') - ->tel(), - ]) - ->columns(2), - - Schema\\Components\\Section::make('Subscription Management') - ->schema([ - Schema\\Components\\Select::make('plan_type') - ->label('Plan') - ->options([ - 'free' => 'Free (Testversion)', - 'basic' => 'Basic (29€/Monat)', - 'premium' => 'Premium (49€/Monat)', - 'enterprise' => 'Enterprise (Custom)' - ]) - ->required() - ->live(), - - Schema\\Components\\Select::make('subscription_status') - ->options([ - 'trial' => 'Trial', - 'active' => 'Aktiv', - 'suspended' => 'Suspendiert', - 'cancelled' => 'Gekündigt' - ]) - ->required(), - - Schema\\Components\\DateTimePicker::make('subscription_starts_at') - ->label('Abo Start'), - - Schema\\Components\\DateTimePicker::make('subscription_ends_at') - ->label('Abo Ende'), - - Schema\\Components\\DateTimePicker::make('trial_ends_at') - ->label('Trial Ende') - ->visible(fn ($get) => $get('subscription_status') === 'trial'), - ]) - ->columns(2), - - Schema\\Components\\Section::make('Limits & Kontingente') - ->schema([ - Schema\\Components\\TextInput::make('max_events') - ->label('Max. Events') - ->numeric() - ->default(1), - - Schema\\Components\\TextInput::make('max_photos_per_event') - ->label('Max. Fotos pro Event') - ->numeric() - ->default(500), - - Schema\\Components\\TextInput::make('max_storage_mb') - ->label('Max. Speicher (MB)') - ->numeric() - ->default(1024), - ]) - ->columns(3), - - Schema\\Components\\Section::make('Feature Flags') - ->schema([ - Schema\\Components\\KeyValue::make('features') - ->label('Aktivierte Features') - ->keyLabel('Feature') - ->valueLabel('Aktiviert') - ->default([ - 'custom_branding' => false, - 'api_access' => false, - 'advanced_analytics' => false, - 'white_label' => false - ]), - ]), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\TextColumn::make('name') - ->searchable() - ->sortable(), - - Tables\Columns\TextColumn::make('contact_email') - ->label('Kontakt') - ->searchable(), - - Tables\Columns\TextColumn::make('event_credits_balance') - ->label('Event-Guthaben') - ->badge() - - - - Tables\Columns\TextColumn::make('events_count') - ->label('Events') - ->counts('events'), - - Tables\Columns\TextColumn::make('total_photos_count') - ->label('Gesamt Fotos') - ->getStateUsing(fn ($record) => - $record->events()->withCount('photos')->get()->sum('photos_count') - ), - - Tables\Columns\TextColumn::make('free_event_granted_at') - ->label('Kostenloses Event am') - ->date('d.m.Y') - ->sortable(), - - Tables\Columns\TextColumn::make('last_activity_at') - ->label('Letzte Aktivität') - ->since() - ->sortable(), - ]) - ->filters([ - Tables\Filters\SelectFilter::make('plan_type') - ->options([ - 'free' => 'Free', - 'basic' => 'Basic', - 'premium' => 'Premium', - 'enterprise' => 'Enterprise' - ]), - - Tables\Filters\SelectFilter::make('subscription_status') - ->options([ - 'trial' => 'Trial', - 'active' => 'Aktiv', - 'suspended' => 'Suspendiert', - 'cancelled' => 'Gekündigt' - ]), - - Tables\Filters\Filter::make('expiring_soon') - ->label('Läuft bald ab') - ->query(fn ($query) => $query->where('subscription_ends_at', '<=', now()->addDays(30))), - ]) - ->recordActions([ - Actions\\ViewAction::make(), - Actions\\EditAction::make(), - Actions\\Action::make('login_as') - ->label('Als Kunde anmelden') - ->icon('heroicon-o-arrow-right-on-rectangle') - ->url(fn ($record) => "/admin/impersonate/{$record->id}") - ->openUrlInNewTab(), - - ]) - ->bulkActions([ - Actions\\BulkAction::make('send_notification') - ->label('Benachrichtigung senden') - ->icon('heroicon-o-envelope') - ->form([ - Schema\\Components\\TextInput::make('subject') - ->label('Betreff') - ->required(), - Schema\\Components\\Textarea::make('message') - ->label('Nachricht') - ->required() - ->rows(4), - ]) - ->action(function (array $data, Collection $records) { - // Send bulk notification to selected customers - foreach ($records as $tenant) { - // Send email notification - } - }), - ]) - ->defaultSort('created_at', 'desc'); - } - - public static function getPages(): array - { - return [ - 'index' => Pages\ListTenants::route('/'), - 'create' => Pages\CreateTenant::route('/create'), - 'edit' => Pages\EditTenant::route('/{record}/edit'), - 'view' => Pages\ViewTenant::route('/{record}'), - ]; - } -} -``` - -## 💰 **Subscription Plans Resource** - -```php -schema([ - Schema\\Components\\Section::make('Plan Details') - ->schema([ - Schema\\Components\\TextInput::make('name') - ->required() - ->maxLength(100), - - Schema\\Components\\TextInput::make('slug') - ->required() - ->unique(SubscriptionPlan::class, 'slug', ignoreRecord: true), - - Schema\\Components\\TextInput::make('price_monthly') - ->label('Monatspreis (€)') - ->numeric() - ->prefix('€'), - - Schema\\Components\\TextInput::make('price_yearly') - ->label('Jahrespreis (€)') - ->numeric() - ->prefix('€'), - - Schema\\Components\\Toggle::make('is_active') - ->label('Plan aktiv') - ->default(true), - ]) - ->columns(2), - - Schema\\Components\\Section::make('Plan Limits') - ->schema([ - Schema\\Components\\TextInput::make('max_events') - ->label('Max. Events') - ->numeric() - ->required(), - - Schema\\Components\\TextInput::make('max_photos_per_event') - ->label('Max. Fotos pro Event') - ->numeric() - ->required(), - - Schema\\Components\\TextInput::make('max_storage_gb') - ->label('Max. Speicher (GB)') - ->numeric() - ->required(), - ]) - ->columns(3), - - Schema\\Components\\Section::make('Features') - ->schema([ - Schema\\Components\\KeyValue::make('features') - ->label('Plan Features') - ->keyLabel('Feature') - ->valueLabel('Enthalten') - ->default([ - 'custom_branding' => false, - 'api_access' => false, - 'advanced_analytics' => false, - 'priority_support' => false, - 'white_label' => false, - 'custom_domain' => false - ]), - ]), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\TextColumn::make('name') - ->searchable() - ->sortable(), - - Tables\Columns\TextColumn::make('price_monthly') - ->label('Monatlich') - ->money('EUR'), - - Tables\Columns\TextColumn::make('price_yearly') - ->label('Jährlich') - ->money('EUR'), - - Tables\Columns\TextColumn::make('max_events') - ->label('Events'), - - Tables\Columns\TextColumn::make('max_photos_per_event') - ->label('Fotos/Event'), - - Tables\Columns\TextColumn::make('max_storage_gb') - ->label('Storage (GB)'), - - Tables\Columns\TextColumn::make('subscribers_count') - ->label('Kunden') - ->getStateUsing(fn ($record) => $record->tenants()->count()), - - Tables\Columns\IconColumn::make('is_active') - ->boolean(), - ]) - ->recordActions([ - Actions\\EditAction::make(), - Actions\\Action::make('duplicate') - ->label('Duplizieren') - ->icon('heroicon-o-document-duplicate') - ->action(function ($record) { - $newPlan = $record->replicate(); - $newPlan->name = $record->name . ' (Kopie)'; - $newPlan->slug = $record->slug . '-copy'; - $newPlan->save(); - }), - ]); - } -} -``` - -## 📊 **System Settings Resource** - -```php -schema([ - Schema\\Components\\Section::make('Setting Details') - ->schema([ - Schema\\Components\\TextInput::make('key') - ->required() - ->unique(SystemSetting::class, 'key', ignoreRecord: true) - ->helperText('Eindeutiger Key für die Einstellung'), - - Schema\\Components\\Textarea::make('value') - ->required() - ->rows(3) - ->helperText('JSON, String oder andere Werte'), - - Schema\\Components\\Textarea::make('description') - ->rows(2) - ->helperText('Beschreibung der Einstellung'), - - Schema\\Components\\Toggle::make('is_public') - ->label('Öffentlich verfügbar') - ->helperText('Über öffentliche API abrufbar'), - ]), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\TextColumn::make('key') - ->searchable() - ->sortable(), - - Tables\Columns\TextColumn::make('value') - ->limit(50) - ->searchable(), - - Tables\Columns\TextColumn::make('description') - ->limit(100), - - Tables\Columns\IconColumn::make('is_public') - ->boolean(), - - Tables\Columns\TextColumn::make('updated_at') - ->label('Zuletzt geändert') - ->dateTime('d.m.Y H:i') - ->sortable(), - ]) - ->filters([ - Tables\Filters\TernaryFilter::make('is_public') - ->label('Öffentliche Einstellungen'), - ]) - ->actions([ - Actions\\EditAction::make(), - ]) - ->groups([ - 'category' => Tables\Grouping\Group::make('key') - ->label('Kategorie') - ->getDescriptionFromRecordUsing(fn ($record) => explode('.', $record->key)[0]) - ]); - } - - // Predefined System Settings - public static function getDefaultSettings(): array - { - return [ - 'platform.max_file_size' => '10485760', // 10MB - 'platform.allowed_image_types' => 'jpeg,jpg,png,webp', - // Event-basierte Preisgestaltung - 'pricing.event_price_eur' => '9.99', - 'pricing.free_event_on_app_purchase' => 'true', - 'email.from_address' => 'noreply@fotobox-platform.de', - 'email.support_address' => 'support@fotobox-platform.de', - 'analytics.google_analytics_id' => '', - 'features.maintenance_mode' => 'false', - 'features.new_registrations' => 'true', - 'storage.default_disk' => 'local', - 'storage.cdn_url' => '', - ]; - } -} -``` - -## 📊 **Super Admin Widgets** - -### **Platform Statistics Widget:** -```php -count()) - ->description('Mindestens ein Event vorhanden') - ->descriptionIcon('heroicon-m-building-office') - ->color('success'), - - Stat::make('Gesamt Events', Event::count()) - ->description('Alle Events auf der Platform') - ->descriptionIcon('heroicon-m-calendar-days') - ->color('primary'), - - Stat::make('Gesamt Fotos', Photo::count()) - ->description('Hochgeladene Fotos') - ->descriptionIcon('heroicon-m-photo') - ->color('warning'), - - Stat::make('Monthly Recurring Revenue', - Tenant::where('subscription_status', 'active') - ->whereIn('plan_type', ['basic', 'premium']) - ->get() - ->sum(fn($tenant) => match($tenant->plan_type) { - 'basic' => 29, - 'premium' => 49, - default => 0 - }) - ) - ->description('€ MRR') - ->descriptionIcon('heroicon-m-currency-euro') - ->color('success'), - ]; - } -} -``` - -### **Revenue Chart Widget:** -```php -where('created_at', '>=', now()->subYear()) - ->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as month, SUM(amount) as revenue') - ->groupBy('month') - ->orderBy('month') - ->get(); - - return [ - 'datasets' => [ - [ - 'label' => 'Umsatz (€)', - 'data' => $data->pluck('revenue'), - 'borderColor' => 'rgb(99, 102, 241)', - 'backgroundColor' => 'rgba(99, 102, 241, 0.1)', - ], - ], - 'labels' => $data->pluck('month'), - ]; - } - - protected function getType(): string - { - return 'line'; - } -} -``` - -## 🔐 **Super Admin Middleware** - -```php -check() || auth()->user()->role !== 'super_admin') { - abort(403, 'Access denied. Super Admin privileges required.'); - } - - return $next($request); - } -} -``` - -## 🏢 **Tenant Scoping Middleware** - -```php -user()->role === 'super_admin') { - return $next($request); // Super Admin bypasses tenant scoping - } - - $tenant = auth()->user()->tenant; - - if (!$tenant) { - abort(403, 'No tenant associated with user.'); - } - - // Set global tenant scope - config(['app.current_tenant' => $tenant]); - - return $next($request); - } -} -``` - -## 🎯 **Super Admin Navigation Struktur** - -``` -🏠 Dashboard -├── 📊 Platform Overview -├── 💰 Revenue Summary -└── 🚨 System Alerts - -👥 Customer Management -├── 🏢 Tenants/Kunden -├── 👤 User Management -├── 📞 Support Tickets -└── 📧 Bulk Communications - -💳 Billing & Subscriptions -├── 💰 Subscription Plans -├── 🧾 Invoices & Payments -├── 📊 Revenue Analytics -└── 🎁 Promotions & Discounts - -⚙️ Platform Configuration -├── 🔧 System Settings -├── 🎨 Global Themes -├── # Hochzeits-Fotobox App - Project Requirements & Prompt (PRP) - -## 📋 Projekt-Übersicht - -**Projektname:** Emotionen-Fotobox für Deutsche Hochzeiten -**Ziel:** Eine Progressive Web App (PWA), die Hochzeitsgäste spielerisch dazu motiviert, emotionale Fotos zu erstellen und zu teilen -**Tech-Stack:** Laravel 12.x + Filament 4.x (Admin) + Vite + React + SQLite + Tailwind CSS -> Versions as of 2025-09-01: Laravel 12 (latest 12.21.x), Filament 4 (stable). If you must stay on Filament v3 for compatibility, pin to 3.3.x. -**Zielgruppe:** Deutsche Hochzeitsgäste aller Altersgruppen - -## 🎯 Kern-Funktionalitäten - -### 1. Emotions-basiertes Foto-System -- **6 Haupt-Emotionen:** Liebe 💕, Freude 😂, Rührung 🥺, Nostalgie 📸, Überraschung 😲, Stolz 🏆 -- **Aufgaben-Generator:** Pro Emotion 8-12 verschiedene Foto-Aufgaben -- **Schwierigkeitsgrade:** Easy (Einzelperson), Medium (Gruppe), Hard (Kreativ/Nachstellung) - -### 2. Social Media-inspiriertes UI -- **Instagram Stories-Layout:** Horizontale Emotion-Bubbles -- **TikTok-Style Kamera:** Vollbild mit Overlay-Aufgaben -- **Feed-Design:** Kachel-Grid mit Like-Funktion -- **Swipe-Navigation:** Intuitive Bedienung - -### 3. Real-time Features -- **Live-Photo-Feed:** Neue Fotos erscheinen sofort bei allen Gästen -- **Kollaborative Galerie:** Gemeinsame Fotosortierung nach Emotionen -- **Like-System:** Herzchen für beliebte Fotos - -## 🏗️ Technische Architektur - -### Backend: Laravel 12.x (PHP 8.4) - -#### Database Schema (SQLite) -```sql --- Event Types (wedding, christmas, birthday, corporate, ...) -CREATE TABLE event_types ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, -- JSON (translatable: {"de":"Hochzeit","en":"Wedding"}) - slug VARCHAR(100) UNIQUE NOT NULL, - icon VARCHAR(64), - settings JSON, -- e.g., {"palette": {...}} - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Events (Veranstaltungen) -CREATE TABLE events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, -- JSON (translatable) - date DATE NOT NULL, - slug VARCHAR(255) UNIQUE NOT NULL, - description TEXT, -- JSON (translatable) - settings JSON, -- Konfiguration für Emotionen, Farben etc. - event_type_id INTEGER NOT NULL, - is_active BOOLEAN DEFAULT 1, - default_locale VARCHAR(5) DEFAULT 'de', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (event_type_id) REFERENCES event_types(id) -); - --- Emotionen-Kategorien (reusable; linked to event types via pivot) -CREATE TABLE emotions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, -- JSON (translatable) - icon VARCHAR(50) NOT NULL, -- Emoji oder Icon-Klasse - color VARCHAR(7) NOT NULL, -- Hex-Farbe - description TEXT, -- JSON (translatable) - sort_order INTEGER DEFAULT 0, - is_active BOOLEAN DEFAULT 1 -); - --- Emotion↔EventType Zuordnung (many-to-many) -CREATE TABLE emotion_event_type ( - emotion_id INTEGER NOT NULL, - event_type_id INTEGER NOT NULL, - PRIMARY KEY (emotion_id, event_type_id), - FOREIGN KEY (emotion_id) REFERENCES emotions(id), - FOREIGN KEY (event_type_id) REFERENCES event_types(id) -); - -CREATE TABLE tasks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - emotion_id INTEGER NOT NULL, - event_type_id INTEGER, -- optional: directly target a type - title TEXT NOT NULL, -- JSON (translatable) - description TEXT NOT NULL, -- JSON (translatable) - difficulty ENUM('easy', 'medium', 'hard') DEFAULT 'easy', - example_text TEXT, -- JSON (translatable) - sort_order INTEGER DEFAULT 0, - is_active BOOLEAN DEFAULT 1, - -- multi-tenant scoping additions (see Tenant-Defined Tasks section) - tenant_id INTEGER, - scope TEXT DEFAULT 'global', -- 'global' | 'tenant' | 'event' - event_id INTEGER, - FOREIGN KEY (emotion_id) REFERENCES emotions(id) - -- optional FKs for scoping - ,FOREIGN KEY (event_type_id) REFERENCES event_types(id) - ,FOREIGN KEY (tenant_id) REFERENCES tenants(id) - ,FOREIGN KEY (event_id) REFERENCES events(id) -); - --- Hochgeladene Fotos -CREATE TABLE photos ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - event_id INTEGER NOT NULL, - emotion_id INTEGER NOT NULL, - task_id INTEGER, - guest_name VARCHAR(255) NOT NULL, - file_path VARCHAR(255) NOT NULL, - thumbnail_path VARCHAR(255) NOT NULL, - likes_count INTEGER DEFAULT 0, - is_featured BOOLEAN DEFAULT 0, -- Für Highlights - metadata JSON, -- EXIF-Daten, Gerätinfo etc. - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (event_id) REFERENCES events(id), - FOREIGN KEY (emotion_id) REFERENCES emotions(id), - FOREIGN KEY (task_id) REFERENCES tasks(id) -); - --- Photo-Likes (für Gast-Interaktion) -CREATE TABLE photo_likes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - photo_id INTEGER NOT NULL, - guest_name VARCHAR(255) NOT NULL, - ip_address VARCHAR(45), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (photo_id) REFERENCES photos(id), - UNIQUE(photo_id, guest_name, ip_address) -); -``` - -#### Laravel Models - -**Event Model:** -```php - -Event model additions (event type): -```php -// In App\Models\Event -protected $fillable = ["name","date","slug","description","settings","event_type_id","is_active"]; -public function type(){ return $this->belongsTo(\App\Models\EventType::class, "event_type_id"); } -``` - - 'date', - 'settings' => 'array', - 'is_active' => 'boolean' - ]; - - public function photos() - { - return $this->hasMany(Photo::class)->latest(); - } - - public function getRouteKeyName() - { - return 'slug'; - } -} -``` - -**Photo Model:** -```php - 'array' - ]; - - public function event() - { - return $this->belongsTo(Event::class); - } - - public function emotion() - { - return $this->belongsTo(Emotion::class); - } - - public function task() - { - return $this->belongsTo(Task::class); - } - - public function likes() - { - return $this->hasMany(PhotoLike::class); - } -} -``` - -#### API Routes (routes/api.php) -```php -middleware(['throttle:60,1'])->group(function () { - // Event-Details - Route::get('/', [EventController::class, 'show']); - - // Emotionen laden (per Event-Typ gefiltert + universal) - Route::get('/emotions', [EmotionController::class, 'index']); - - // Aufgaben pro Emotion - Route::get('/tasks/{emotion}', [TaskController::class, 'getByEmotion']); - Route::get('/tasks/random/{emotion}', [TaskController::class, 'getRandomTask']); - - // Foto-Management - Route::get('/photos', [PhotoController::class, 'index']); - Route::post('/photos', [PhotoController::class, 'store']); - Route::get('/photos/{photo}', [PhotoController::class, 'show']); - Route::post('/photos/{photo}/like', [PhotoController::class, 'toggleLike']); - - // Live-Feed für Real-time Updates - Route::get('/feed/latest', [LiveFeedController::class, 'getLatest']); - Route::get('/feed/stream', [LiveFeedController::class, 'stream']); // SSE -}); - -// Admin Routes (für Hochzeits-Setup) -Route::prefix('admin')->middleware(['auth:sanctum'])->group(function () { - Route::apiResource('events', EventController::class); - Route::apiResource('emotions', EmotionController::class); - Route::apiResource('tasks', TaskController::class); -}); -``` - -#### Controller-Beispiele - -**PhotoController:** -```php -validate([ - 'photo' => 'required|image|mimes:jpeg,png,jpg|max:10240', // 10MB - 'emotion_id' => 'required|exists:emotions,id', - 'task_id' => 'nullable|exists:tasks,id', - 'guest_name' => 'required|string|max:100|min:2' - ]); - - $photo = $request->file('photo'); - $filename = uniqid('photo_') . '.' . $photo->getClientOriginalExtension(); - - // Original-Bild speichern - $originalPath = $photo->storeAs('photos', $filename, 'public'); - - // Thumbnail erstellen (300x300 quadratisch) - $thumbnailPath = 'thumbnails/' . $filename; - $thumbnail = Image::make($photo) - ->fit(300, 300, function ($constraint) { - $constraint->upsize(); - }) - ->encode('jpg', 85); - - Storage::disk('public')->put($thumbnailPath, $thumbnail); - - // Metadata sammeln - $metadata = [ - 'original_name' => $photo->getClientOriginalName(), - 'size' => $photo->getSize(), - 'mime_type' => $photo->getMimeType(), - 'ip_address' => $request->ip(), - 'user_agent' => $request->userAgent() - ]; - - // In Datenbank speichern - $photoRecord = Photo::create([ - 'event_id' => $event->id, - 'emotion_id' => $request->emotion_id, - 'task_id' => $request->task_id, - 'guest_name' => $request->guest_name, - 'file_path' => $originalPath, - 'thumbnail_path' => $thumbnailPath, - 'metadata' => $metadata - ]); - - // Response mit Relations - $photoRecord->load(['emotion', 'task']); - - // Optional: Broadcasting für Live-Updates - // broadcast(new PhotoUploaded($photoRecord)); - - return response()->json([ - 'success' => true, - 'data' => $photoRecord, - 'message' => 'Foto erfolgreich hochgeladen!' - ], 201); - } - - public function index(Request $request, Event $event) - { - $query = $event->photos()->with(['emotion', 'task', 'likes']); - - // Filter nach Emotion - if ($request->has('emotion_id')) { - $query->where('emotion_id', $request->emotion_id); - } - - // Sortierung - $sortBy = $request->get('sort', 'latest'); - switch ($sortBy) { - case 'popular': - $query->withCount('likes')->orderByDesc('likes_count'); - break; - case 'oldest': - $query->oldest(); - break; - default: - $query->latest(); - } - - $photos = $query->paginate(20); - - return response()->json($photos); - } - - public function toggleLike(Request $request, Event $event, Photo $photo) - { - $request->validate([ - 'guest_name' => 'required|string|max:100' - ]); - - $like = PhotoLike::where([ - 'photo_id' => $photo->id, - 'guest_name' => $request->guest_name, - 'ip_address' => $request->ip() - ])->first(); - - if ($like) { - $like->delete(); - $photo->decrement('likes_count'); - $liked = false; - } else { - PhotoLike::create([ - 'photo_id' => $photo->id, - 'guest_name' => $request->guest_name, - 'ip_address' => $request->ip() - ]); - $photo->increment('likes_count'); - $liked = true; - } - - return response()->json([ - 'liked' => $liked, - 'likes_count' => $photo->fresh()->likes_count - ]); - } -} -``` - -### Frontend: React + Vite PWA - -Note: Event Type-aware UI -- Theme colors can come from `event_types.settings.palette` to skin headers/buttons per type. -- Emotion picker fetches type-filtered emotions via `/api/events/:slug/emotions`. -- Random task generator and gallery filters respect event type automatically via API. - -#### Projekt-Struktur -``` -resources/js/ -├── src/ -│ ├── components/ -│ │ ├── Camera/ -│ │ │ ├── CameraCapture.jsx -│ │ │ ├── PhotoPreview.jsx -│ │ │ └── TaskOverlay.jsx -│ │ ├── Emotions/ -│ │ │ ├── EmotionPicker.jsx -│ │ │ ├── EmotionBubble.jsx -│ │ │ └── TaskGenerator.jsx -│ │ ├── Gallery/ -│ │ │ ├── PhotoGrid.jsx -│ │ │ ├── PhotoModal.jsx -│ │ │ └── PhotoCard.jsx -│ │ ├── Layout/ -│ │ │ ├── Header.jsx -│ │ │ ├── Navigation.jsx -│ │ │ └── LoadingSpinner.jsx -│ │ └── Forms/ -│ │ ├── GuestNameForm.jsx -│ │ └── PhotoUpload.jsx -│ ├── pages/ -│ │ ├── GeneralLandingPage.jsx -│ │ ├── EventSpecificLandingPage.jsx -│ │ ├── HomePage.jsx -│ │ ├── CameraPage.jsx -│ │ ├── GalleryPage.jsx -│ ├── hooks/ -│ │ ├── useCamera.js -│ │ ├── usePhotoUpload.js -│ │ ├── useLocalStorage.js -│ │ └── useRealTimeFeed.js -│ ├── services/ -│ │ ├── api.js -│ │ ├── imageUtils.js -│ │ └── cacheManager.js -│ ├── context/ -│ │ ├── AppContext.jsx -│ │ └── EventContext.jsx -│ └── utils/ -│ ├── constants.js -│ ├── helpers.js -│ └── validation.js -``` - -#### Vite Configuration (vite.config.js) -```javascript -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { VitePWA } from 'vite-plugin-pwa' -import path from 'path' - -export default defineConfig({ - plugins: [ - react(), - VitePWA({ - registerType: 'prompt', - includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'safari-pinned-tab.svg'], - manifest: { - name: 'Hochzeits-Fotobox', - short_name: 'Fotobox', - description: 'Emotionale Hochzeitsfotos für unvergessliche Momente', - theme_color: '#ff6b9d', - background_color: '#ffffff', - display: 'standalone', - orientation: 'portrait-primary', - start_url: '/', - scope: '/', - icons: [ - { - src: '/icons/pwa-192x192.png', - sizes: '192x192', - type: 'image/png' - }, - { - src: '/icons/pwa-512x512.png', - sizes: '512x512', - type: 'image/png' - } - ] - }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], - runtimeCaching: [ - { - urlPattern: /^\/api\//, - handler: 'NetworkFirst', - options: { - cacheName: 'api-cache', - expiration: { - maxEntries: 100, - maxAgeSeconds: 60 * 60 * 24 // 24 hours - } - } - }, - { - urlPattern: /\.(png|jpg|jpeg|svg|gif)$/, - handler: 'CacheFirst', - options: { - cacheName: 'images-cache', - expiration: { - maxEntries: 200, - maxAgeSeconds: 60 * 60 * 24 * 7 // 1 week - } - } - } - ] - } - }) - ], - resolve: { - alias: { - '@': path.resolve(__dirname, './resources/js/src') - } - }, - build: { - outDir: 'public/build', - emptyOutDir: true, - manifest: true, - rollupOptions: { - input: 'resources/js/app.jsx' - } - }, - server: { - host: true, - hmr: { - host: 'localhost' - } - } -}) -``` - -#### React Haupt-Komponenten - -**App.jsx (Routing & Context)** -```jsx -import React from 'react' -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { EventProvider } from '@/context/EventContext' -import { AppProvider } from '@/context/AppContext' - -// Pages -import EventLanding from '@/pages/EventLanding' -import HomePage from '@/pages/HomePage' -import CameraPage from '@/pages/CameraPage' -import GalleryPage from '@/pages/GalleryPage' - -// Layout -import Layout from '@/components/Layout/Layout' - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - cacheTime: 1000 * 60 * 30, // 30 minutes - retry: 2 - } - } -}) - -function App() { - return ( - - - - - {/* Event Selection */} - } /> - - {/* Main App (requires event context) */} - - - - } /> - } /> - } /> - } /> - } /> - } /> - - - - } /> - - - - - ) -} - -export default App -``` - - -**GeneralLandingPage.jsx** -```jsx -import React from 'react'; -import { useNavigate } from 'react-router-dom'; - -function GeneralLandingPage() { - const navigate = useNavigate(); - - const handleQRScan = () => { - // Trigger device camera to scan QR code - // This would use a library like jsQR or react-qr-reader - console.log("Opening QR scanner..."); - }; - - const handleManualEntry = () => { - // Open modal for manual code entry - const code = prompt("Bitte gib den Event-Code ein:"); - if (code) { - // Validate and redirect to event - navigate(`/event/${code}`); - } - }; - - return ( -
- {/* Minimalist Header */} -
-
Event Date
-
Branding Logo
-
- - {/* Main Content */} -
-
-

Willkommen bei der Fotobox! 🎉

-

Dein Schlüssel zu unvergesslichen Hochzeitsmomenten.

-
- - {/* Central Card for Actions */} -
- {/* QR Code Scanner Emphasis */} -
-
- -
- - {/* Manual Code Entry (Fallback) */} -
- -
-
-
- - {/* Minimalist Footer */} -
- © {new Date().getFullYear()} Emotionen-Fotobox -
-
- ); -} - -export default GeneralLandingPage; -``` - -**EventSpecificLandingPage.jsx** -```jsx -import React, { useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; - -function EventSpecificLandingPage() { - const { eventSlug } = useParams(); - const navigate = useNavigate(); - const [guestName, setGuestName] = useState(''); - - const handleSubmit = (e) => { - e.preventDefault(); - if (guestName.trim()) { - // Store guest name in context or localStorage - localStorage.setItem('guestName', guestName); - // Redirect to main event page - navigate(`/event/${eventSlug}`); - } - }; - - return ( -
- {/* Header */} -
-
❤️📸
{/* Heart with Camera Icon */} -
01.01.2025
{/* Event Date */} -
- - {/* Main Content - Vertically Centered */} -
- {/* Title Section */} -
-

HOCHZEIT VON

-

MARIE & LUKAS

-

Fangt die schönsten Momente ein!

-
- - {/* Input Section */} -
-

Bereit für Emotionen?

-
- setGuestName(e.target.value)} - className="w-full p-3 mb-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-pink-300" - required - /> - -
-
-
- - {/* Footer */} -
-
❤️
-

Emotionen-Fotobox

-
-
- ); -} - -export default EventSpecificLandingPage; -``` - -**SettingsSheet.jsx (gear icon → bottom sheet)** -```jsx -import React from "react" -import { Link } from "react-router-dom" -import { useTranslation } from "react-i18next" - -export default function SettingsSheet({ open, onClose, onLangChange, currentLang }) { - const { t } = useTranslation() - if (!open) return null - return ( -
-
-
-
-
-

{t("ui:settings")}

- -
-
-
-
{t("ui:language")}
-
- - -
-
-
-
{t("ui:legal")}
-
    -
  • Impressum
  • -
  • Datenschutzerklärung
  • -
  • AGB
  • -
-
-
-
-
-
- ) -} -``` - -**LegalPage.jsx (renders platform-managed markdown)** -```jsx -import React from "react" -import { useParams } from "react-router-dom" -import { useQuery } from "@tanstack/react-query" -import ReactMarkdown from "react-markdown" -import rehypeSanitize from "rehype-sanitize" -import { useTranslation } from "react-i18next" - -export default function LegalPage(){ - const { slug } = useParams() - const { i18n } = useTranslation() - const lang = i18n.language || "de" - - const { data, isLoading, error } = useQuery({ - queryKey: ["legal", slug, lang], - queryFn: async () => { - const res = await fetch(`/api/legal/${slug}?lang=${lang}`) - if(!res.ok) throw new Error("Failed to load legal page") - return res.json() - } - }) - - if (isLoading) return
Loading…
- if (error) return
Error loading content.
- - return ( -
-

{data.title}

-
- {data.updated_at ? new Date(data.updated_at).toLocaleDateString(lang) : null} -
- - {data.body_markdown || ""} - -
- ) -} -``` -**Header.jsx (gear button opens SettingsSheet and changes language)** -```jsx -import React from "react" -import { Settings } from "lucide-react" -import { useTranslation } from "react-i18next" -import SettingsSheet from "@/components/Layout/SettingsSheet" - -export default function Header() { - const { i18n } = useTranslation() - const [open, setOpen] = React.useState(false) - - const onLangChange = (lng) => { - i18n.changeLanguage(lng) - localStorage.setItem("lang", lng) - setOpen(false) - } - - return ( -
-
-
Fotobox
- -
- setOpen(false)} - onLangChange={onLangChange} - currentLang={i18n.language || "de"} - /> -
- ) -} -``` -## 🌍 Internationalization (i18n) - -### Goals & Locales -- Support multi-lingual content across admin, API, and PWA. -- Initial locales: `de` (default) and `en`; easily extendable. - -### Backend (Laravel 12 + PHP 8.4) -- Dependency: `spatie/laravel-translatable` for JSON translations. -- App config (`config/app.php`): set `locale`, `fallback_locale`, and optionally `APP_SUPPORTED_LOCALES=de,en`. - -#### i18n Data Model -- Store translatable fields as JSON: - - `event_types.name` - - `events.name`, `events.description` - - `emotions.name`, `emotions.description` - - `tasks.title`, `tasks.description`, `tasks.example_text` - -#### Model Setup -```php -schema([ - Schema\Components\Select::make('slug') - ->label('Type')->options([ - 'imprint' => 'Impressum', - 'privacy' => 'Datenschutzerklärung', - 'terms' => 'AGB', - 'custom' => 'Custom', - ])->required(), - - Schema\Components\Tabs::make('Translations')->tabs([ - Schema\Components\Tabs\Tab::make('Deutsch')->schema([ - Schema\Components\TextInput::make('title.de')->label('Titel (DE)')->required(), - Schema\Components\Textarea::make('body_markdown.de')->label('Inhalt (DE)')->rows(12)->required(), - ]), - Schema\Components\Tabs\Tab::make('English')->schema([ - Schema\Components\TextInput::make('title.en')->label('Title (EN)')->required(), - Schema\Components\Textarea::make('body_markdown.en')->label('Content (EN)')->rows(12)->required(), - ]), - ])->columnSpanFull(), - - Schema\Components\Grid::make(3)->schema([ - Schema\Components\TextInput::make('version')->numeric()->default(1), - Schema\Components\DateTimePicker::make('effective_from'), - Schema\Components\Toggle::make('is_published')->label('Published'), - ]), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\BadgeColumn::make('slug')->label('Type')->colors([ - 'primary' => 'imprint','success'=>'privacy','warning'=>'terms','gray'=>'custom' - ]), - Tables\Columns\TextColumn::make('title')->label('Title')->formatStateUsing(fn($r)=>$r->getTranslation('title', app()->getLocale()))->limit(40), - Tables\Columns\TextColumn::make('version')->sortable(), - Tables\Columns\IconColumn::make('is_published')->boolean(), - Tables\Columns\TextColumn::make('updated_at')->dateTime('d.m.Y H:i')->sortable(), - ]) - ->headerActions([ - Actions\CreateAction::make(), - ]) - ->recordActions([ - Actions\EditAction::make(), - Actions\Action::make('preview')->label('Preview')->url(fn($r)=>url("/legal/{$r->slug}?lang=".app()->getLocale()))->openUrlInNewTab(), - Actions\DeleteAction::make(), - ]); - } -} -``` - -### API (public) -```php -// routes/api.php -use App\Http\Controllers\Api\LegalController; -Route::get('/legal/{slug}', [LegalController::class, 'show']); -``` - -```php -getLocale(); - $page = LegalPage::query() - ->where('slug', $slug) - ->where('is_published', true) - ->orderByDesc('version') - ->firstOrFail(); - - return response()->json([ - 'slug' => $page->slug, - 'title' => $page->getTranslation('title', $locale), - 'body_markdown' => $page->getTranslation('body_markdown', $locale), - 'version' => $page->version, - 'effective_from' => $page->effective_from, - 'updated_at' => $page->updated_at, - 'locale' => $locale, - ]); - } -} -``` - -### PWA UX -- Header gear icon opens a Settings sheet with: - - Language: DE/EN toggle - - Legal: Impressum, Datenschutzerklärung, AGB (links) -- Footer links (public pages): “Impressum · Datenschutz · AGB” (DE labels in DE locale) for easy/direct access. -- Routes: `/legal/imprint`, `/legal/privacy`, `/legal/terms` → render via API. - -React components (sketch): -```jsx -// routes -} /> - -// components/SettingsSheet.jsx (triggered by gear button) -// shows language selector and links to /legal/... routes -``` - -Workbox caching (vite-plugin-pwa): add runtime cache for `/api/legal/*` (CacheFirst with revalidate). - -Compliance (DE): ensure “Impressum” is labeled in German and reachable within ≤2 clicks; show last updated date; include provider details (name, address, contact, VAT/registry) in content. - -#### Locale Resolution (API) -- Precedence: `?lang` → `Accept-Language` → `event.default_locale` → fallback. -- Set `Content-Language` header. -```php -class SetLocale { /* see middleware example above */ } -``` - -### Admin (Filament v4) Forms -- Use Tabs for per-locale inputs (Spatie accepts arrays): -```php -Schema\Components\Tabs::make('Translations')->tabs([ - Schema\Components\Tabs\Tab::make('Deutsch')->schema([ - Schema\Components\TextInput::make('title.de')->label('Titel (DE)')->required(), - Schema\Components\Textarea::make('description.de')->label('Beschreibung (DE)')->rows(3), - Schema\Components\TextInput::make('example_text.de')->label('Beispiel (DE)'), - ]), - Schema\Components\Tabs\Tab::make('English')->schema([ - Schema\Components\TextInput::make('title.en')->label('Title (EN)'), - Schema\Components\Textarea::make('description.en')->label('Description (EN)')->rows(3), - Schema\Components\TextInput::make('example_text.en')->label('Example (EN)'), - ]), -]) -``` - -### CSV (Localized Fields) -- Extra headers allowed: `title:de`, `title:en`, `description:de`, `description:en`, `example_text:de`, `example_text:en`. -- If only base columns exist, treat them as the active import locale. -```csv -emotion,title:de,title:en,description:de,description:en,difficulty,is_active,scope,event_type -Freude,Sprung-Foto,Jump Photo,Alle springen!,Everyone jump!,medium,1,tenant,corporate -``` - -### Frontend (React PWA) -- Dependencies: `i18next`, `react-i18next`, `i18next-browser-languagedetector`. -- Initialize in `resources/js/src/i18n.js` and pass locale to API via `?lang=`. - -**HomePage.jsx (Emotions-Picker)** -```jsx -import React from 'react' -import { useNavigate } from 'react-router-dom' -import { useQuery } from '@tanstack/react-query' -import { useEvent } from '@/context/EventContext' -import { useApp } from '@/context/AppContext' -import EmotionBubble from '@/components/Emotions/EmotionBubble' -import PhotoGrid from '@/components/Gallery/PhotoGrid' -import LoadingSpinner from '@/components/Layout/LoadingSpinner' - -function HomePage() { - const navigate = useNavigate() - const { eventSlug, apiService } = useEvent() - const { guestName, setGuestName } = useApp() - - // Emotionen laden - const { data: emotions, isLoading: emotionsLoading } = useQuery({ - queryKey: ['emotions', eventSlug], - queryFn: () => apiService.getEmotions(), - enabled: !!eventSlug - }) - - // Neueste Fotos laden - const { data: recentPhotos, isLoading: photosLoading } = useQuery({ - queryKey: ['photos', eventSlug, 'recent'], - queryFn: () => apiService.getPhotos({ limit: 8, sort: 'latest' }), - enabled: !!eventSlug, - refetchInterval: 30000 // Alle 30 Sekunden aktualisieren - }) - - const handleEmotionClick = (emotionId) => { - if (!guestName) { - // Gast-Namen abfragen wenn noch nicht gesetzt - const name = prompt('Wie sollen wir dich nennen?', '') - if (name?.trim()) { - setGuestName(name.trim()) - } else { - return - } - } - navigate(`/event/${eventSlug}/camera/${emotionId}`) - } - - if (emotionsLoading) { - return - } - - return ( -
- {/* Header */} -
-

- Willkommen zur Fotobox! 📸 -

- {guestName && ( -

- Hallo, {guestName}! 👋 -

- )} -
- - {/* Emotions-Picker */} -
-
- {emotions?.map((emotion) => ( - handleEmotionClick(emotion.id)} - className="flex-shrink-0" - /> - ))} -
-
- - {/* "Alle anzeigen" Button */} -
- -
- - {/* Recent Photos Feed */} -
-

- Neueste Momente -

- - {photosLoading ? ( -
- {[...Array(4)].map((_, i) => ( -
- ))} -
- ) : ( - - )} -
-
- ) -} - -export default HomePage -``` - -**CameraPage.jsx (Foto-Aufnahme)** -```jsx -import React, { useState, useRef, useCallback } from 'react' -import { useParams, useNavigate } from 'react-router-dom' -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { useEvent } from '@/context/EventContext' -import { useApp } from '@/context/AppContext' -import CameraCapture from '@/components/Camera/CameraCapture' -import TaskOverlay from '@/components/Camera/TaskOverlay' -import PhotoPreview from '@/components/Camera/PhotoPreview' - -function CameraPage() { - const { emotionId } = useParams() - const navigate = useNavigate() - const { eventSlug, apiService } = useEvent() - const { guestName } = useApp() - const queryClient = useQueryClient() - - const [capturedPhoto, setCapturedPhoto] = useState(null) - const [currentTask, setCurrentTask] = useState(null) - const cameraRef = useRef(null) - - // Aktueller Task laden - const { data: tasks, refetch: refetchTasks } = useQuery({ - queryKey: ['tasks', eventSlug, emotionId], - queryFn: () => apiService.getRandomTask(emotionId), - enabled: !!emotionId && !!eventSlug, - onSuccess: (data) => { - if (data && !currentTask) { - setCurrentTask(data) - } - } - }) - - // Foto-Upload Mutation - const uploadMutation = useMutation({ - mutationFn: (photoData) => apiService.uploadPhoto(photoData), - onSuccess: () => { - // Cache invalidieren für sofortige Updates - queryClient.invalidateQueries(['photos', eventSlug]) - - // Zurück zur Startseite - navigate(`/event/${eventSlug}`) - }, - onError: (error) => { - console.error('Upload failed:', error) - alert('Upload fehlgeschlagen. Bitte versuche es erneut.') - } - }) - - const handleCapture = useCallback((photoBlob) => { - setCapturedPhoto(photoBlob) - }, []) - - const handleRetake = useCallback(() => { - setCapturedPhoto(null) - }, []) - - const handleUpload = useCallback(() => { - if (!capturedPhoto || !guestName || !currentTask) return - - const formData = new FormData() - formData.append('photo', capturedPhoto, 'photo.jpg') - formData.append('emotion_id', emotionId) - formData.append('task_id', currentTask.id) - formData.append('guest_name', guestName) - - uploadMutation.mutate(formData) - }, [capturedPhoto, guestName, emotionId, currentTask, uploadMutation]) - - const handleNewTask = useCallback(() => { - refetchTasks().then((result) => { - if (result.data) { - setCurrentTask(result.data) - } - }) - }, [refetchTasks]) - - // Redirect wenn kein Gast-Name gesetzt - React.useEffect(() => { - if (!guestName) { - navigate(`/event/${eventSlug}`) - } - }, [guestName, navigate, eventSlug]) - - if (!currentTask) { - return ( -
-
-
-

Aufgabe wird geladen...

-
-
- ) - } - - return ( -
- {!capturedPhoto ? ( - <> - {/* Live-Kamera */} - - - {/* Task-Overlay */} - navigate(`/event/${eventSlug}`)} - /> - - ) : ( - /* Foto-Vorschau */ - - )} -
- ) -} - -export default CameraPage -``` - -#### PWA Service Worker Features - -**Offline-Funktionalität:** -```javascript -// public/sw.js (Custom Service Worker) -const CACHE_NAME = 'fotobox-v1' -const OFFLINE_URL = '/offline.html' - -// Offline-Queue für Photo-Uploads -class PhotoUploadQueue { - constructor() { - this.queue = [] - this.isProcessing = false - } - - async add(photoData) { - this.queue.push(photoData) - await this.process() - } - - async process() { - if (this.isProcessing || this.queue.length === 0) return - - this.isProcessing = true - - while (this.queue.length > 0) { - const photoData = this.queue.shift() - - try { - const response = await fetch(photoData.url, { - method: 'POST', - body: photoData.formData - }) - - if (response.ok) { - // Upload erfolgreich - Cache invalidieren - await caches.delete('api-cache') - - // Client benachrichtigen - self.clients.matchAll().then(clients => { - clients.forEach(client => { - client.postMessage({ - type: 'PHOTO_UPLOADED', - data: photoData - }) - }) - }) - } else { - // Fehlgeschlagen - zurück in die Queue - this.queue.unshift(photoData) - break - } - } catch (error) { - // Netzwerkfehler - zurück in die Queue - this.queue.unshift(photoData) - break - } - } - - this.isProcessing = false - } -} - -const uploadQueue = new PhotoUploadQueue() - -// Network-first für API-Calls -self.addEventListener('fetch', event => { - if (event.request.url.includes('/api/')) { - event.respondWith( - fetch(event.request) - .then(response => { - // Response klonen für Cache - const responseClone = response.clone() - caches.open(CACHE_NAME).then(cache => { - cache.put(event.request, responseClone) - }) - return response - }) - .catch(() => { - // Fallback auf Cache - return caches.match(event.request) - }) - ) - } -}) -``` - -## 🎨 UI/UX Design-Spezifikationen - -### Farbschema -```javascript -// tailwind.config.js - Custom Colors -module.exports = { - theme: { - extend: { - colors: { - emotions: { - love: '#ff6b9d', // Liebe - Pink - joy: '#ffd93d', // Freude - Gelb - touched: '#6bcf7f', // Rührung - Grün - nostalgia: '#a78bfa', // Nostalgie - Lila - surprise: '#fb7185', // Überraschung - Rosa - pride: '#34d399' // Stolz - Türkis - }, - wedding: { - primary: '#ff6b9d', - secondary: '#a78bfa', - accent: '#ffd93d', - neutral: '#6b7280', - success: '#10b981', - error: '#ef4444' - } - }, - fontFamily: { - 'wedding': ['Poppins', 'Inter', 'sans-serif'] - }, - animation: { - 'bounce-soft': 'bounce 2s infinite', - 'pulse-slow': 'pulse 3s infinite', - 'wiggle': 'wiggle 1s ease-in-out infinite', - 'fade-in': 'fadeIn 0.5s ease-in-out', - 'slide-up': 'slideUp 0.3s ease-out' - } - } - } -} -``` - -### Responsive Breakpoints -- **Mobile First:** 320px - 768px (Hauptzielgruppe) -- **Tablet:** 768px - 1024px (Landscape-Modus) -- **Desktop:** 1024px+ (Admin-Interface) - -### Gestaltungs-Prinzipien -1. **Thumb-Friendly:** Alle wichtigen Buttons in Daumen-Reichweite -2. **High Contrast:** Gute Lesbarkeit auch bei schlechtem Licht -3. **Emotional Colors:** Jede Emotion hat eigene Farb-Identität -4. **Micro-Animations:** Subtile Feedback-Animationen -5. **Progressive Disclosure:** Komplexität schrittweise aufbauen - -## 📱 Komponenten-Spezifikationen - -### EmotionBubble Component -```jsx -import React from 'react' -import { motion } from 'framer-motion' - -const EmotionBubble = ({ emotion, onClick, isActive, className }) => { - return ( - onClick(emotion.id)} - > - {/* Emoji Icon */} - {emotion.icon} - - {/* Name */} - - {emotion.name} - - - {/* Pulse Animation für aktive Emotion */} - {isActive && ( - - )} - - ) -} -``` - -### CameraCapture Component -```jsx -import React, { useRef, useEffect, useState } from 'react' -import { Camera, RotateCcw, Zap, ZapOff } from 'lucide-react' - -const CameraCapture = React.forwardRef(({ onCapture }, ref) => { - const videoRef = useRef(null) - const canvasRef = useRef(null) - const [stream, setStream] = useState(null) - const [hasFlash, setHasFlash] = useState(false) - const [isFlashOn, setIsFlashOn] = useState(false) - const [facingMode, setFacingMode] = useState('user') // 'user' oder 'environment' - - // Kamera initialisieren - useEffect(() => { - startCamera() - return () => { - stopCamera() - } - }, [facingMode]) - - const startCamera = async () => { - try { - const constraints = { - video: { - facingMode: facingMode, - width: { ideal: 1080 }, - height: { ideal: 1920 } - } - } - - const mediaStream = await navigator.mediaDevices.getUserMedia(constraints) - setStream(mediaStream) - - if (videoRef.current) { - videoRef.current.srcObject = mediaStream - } - - // Flash-Unterstützung prüfen - const videoTrack = mediaStream.getVideoTracks()[0] - const capabilities = videoTrack.getCapabilities() - setHasFlash(capabilities.torch === true) - - } catch (error) { - console.error('Kamera-Zugriff fehlgeschlagen:', error) - alert('Kamera-Zugriff nicht möglich. Bitte Berechtigungen prüfen.') - } - } - - const stopCamera = () => { - if (stream) { - stream.getTracks().forEach(track => track.stop()) - setStream(null) - } - } - - const toggleFlash = async () => { - if (!hasFlash || !stream) return - - const videoTrack = stream.getVideoTracks()[0] - try { - await videoTrack.applyConstraints({ - advanced: [{ torch: !isFlashOn }] - }) - setIsFlashOn(!isFlashOn) - } catch (error) { - console.error('Flash toggle fehlgeschlagen:', error) - } - } - - const switchCamera = () => { - setFacingMode(prev => prev === 'user' ? 'environment' : 'user') - } - - const capturePhoto = () => { - if (!videoRef.current || !canvasRef.current) return - - const video = videoRef.current - const canvas = canvasRef.current - const ctx = canvas.getContext('2d') - - // Canvas-Größe an Video anpassen - canvas.width = video.videoWidth - canvas.height = video.videoHeight - - // Video-Frame auf Canvas zeichnen - ctx.drawImage(video, 0, 0, canvas.width, canvas.height) - - // Canvas zu Blob konvertieren - canvas.toBlob((blob) => { - if (blob && onCapture) { - onCapture(blob) - } - }, 'image/jpeg', 0.9) - } - - return ( -
- {/* Video Stream */} -
- ) -}) - -export default CameraCapture -``` - -### TaskOverlay Component -```jsx -import React from 'react' -import { motion } from 'framer-motion' -import { Shuffle, ArrowLeft, HelpCircle } from 'lucide-react' - -const TaskOverlay = ({ task, onNewTask, onBack, onShowHelp }) => { - if (!task) return null - - return ( - - {/* Background Gradient */} -
- - {/* Header Controls */} -
- - -
- - -
-
- - {/* Task Content */} -
- {/* Emotion Badge */} -
- {task.emotion?.icon} - {task.emotion?.name} -
- - {/* Task Title */} -

- {task.title} -

- - {/* Task Description */} -

- {task.description} -

- - {/* Difficulty Indicator */} -
- {[...Array(3)].map((_, i) => ( -
- ))} - - {task.difficulty} - -
- - {/* Example Hint */} - {task.example_text && ( - -

- 💡 {task.example_text} -

-
- )} -
-
- - ) -} - -const getDifficultyLevel = (difficulty) => { - switch (difficulty) { - case 'easy': return 1 - case 'medium': return 2 - case 'hard': return 3 - default: return 1 - } -} - -export default TaskOverlay -``` - -## 🧩 Tenant-Defined Tasks & CSV Import - -### Goals -- Allow tenants (customers) to create and manage their own photo tasks. -- Support CSV import for bulk task creation for both Tenant Admins and Super Admin. -- Keep global (platform) task templates that tenants can also use. - -### Schema Changes (Tenant Scoping for Tasks) -```sql --- Extend tasks to support tenant/event scoping -ALTER TABLE tasks ADD COLUMN tenant_id INTEGER NULL; -- owner tenant for custom tasks -ALTER TABLE tasks ADD COLUMN scope TEXT DEFAULT 'global' CHECK (scope IN ('global','tenant','event')); -ALTER TABLE tasks ADD COLUMN event_id INTEGER NULL; -- optional link for event-specific tasks -ALTER TABLE tasks ADD CONSTRAINT fk_tasks_tenant_id FOREIGN KEY (tenant_id) REFERENCES tenants(id); -ALTER TABLE tasks ADD CONSTRAINT fk_tasks_event_id FOREIGN KEY (event_id) REFERENCES events(id); - --- Prevent duplicate titles per emotion within the same tenant (optional) -CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_unique_per_tenant ON tasks(tenant_id, emotion_id, title); -``` - -Notes: -- Use `scope='global'` for Super Admin library tasks, `tenant` for customer-defined, and `event` for per-event overrides. -- `tenant_id`/`event_id` are NULL for `global` tasks. - -### CSV Format -- Encoding: UTF-8, delimiter: comma. -- Header columns: - - emotion (name or ID) - - title - - description - - difficulty (easy|medium|hard) - - example_text (optional) - - is_active (1|0, optional; default 1) - - sort_order (integer, optional) - - scope (global|tenant|event, optional; default depends on UI context) - - event_slug (required if scope=event)`n - event_type (slug, optional; defaults to current event type or chosen library type) - -Example: -```csv -emotion,title,description,difficulty,example_text,is_active,sort_order,scope,event_type,event_slug -Freude,Sprung-Foto,Alle springen gleichzeitig in die Luft!,medium,,1,10,tenant,corporate, -Liebe,Kuss-Foto,Macht ein romantisches Kuss-Foto,easy,Einfach küssen und lächeln!,1,5,global,wedding, -Überraschung,Plötzlicher Jubel,Erschreckt das Brautpaar mit Jubel,hard,,1,20,event,,mueller-hochzeit-2025 -``` - -Validation rules: -- emotion must exist (map by name → ID considering tenant visibility). -- difficulty in allowed set; length limits per model. -- If scope=event, event_slug must resolve to a tenant-owned event. - -### Filament v4 — Tenant Admin Task Resource (with CSV Import) -```php -schema([ - Schema\Components\Section::make('Aufgabe') - ->schema([ - Schema\Components\Select::make('emotion_id') - ->label('Emotion') - ->relationship('emotion', 'name') - ->required(), - Schema\Components\TextInput::make('title')->required()->maxLength(255), - Schema\Components\Textarea::make('description')->required()->rows(3), - Schema\Components\Select::make('difficulty') - ->options(['easy'=>'Easy','medium'=>'Medium','hard'=>'Hard'])->required(), - Schema\Components\TextInput::make('example_text'), - Schema\Components\Toggle::make('is_active')->default(true), - Schema\Components\TextInput::make('sort_order')->numeric()->default(0), - ])->columns(2), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\TextColumn::make('emotion.name')->label('Emotion')->sortable()->searchable(), - Tables\Columns\TextColumn::make('title')->searchable(), - Tables\Columns\BadgeColumn::make('difficulty')->colors(['easy'=>'success','medium'=>'warning','hard'=>'danger']), - Tables\Columns\IconColumn::make('is_active')->boolean(), - Tables\Columns\TextColumn::make('sort_order')->sortable(), - ]) - ->filters([ - Tables\Filters\SelectFilter::make('event_type_id') - ->label('Event Type') - ->options(fn () => \App\Models\EventType::orderBy('name')->pluck('name', 'id')->all()) - ->query(function ($query, $state) { - if (! $state) { return; } - $query->where(function($q) use ($state) { - $q->where('event_type_id', $state) - ->orWhereExists(function($sub) use ($state) { - $sub->selectRaw(1) - ->from('emotion_event_type as eet') - ->whereColumn('eet.emotion_id', 'tasks.emotion_id') - ->where('eet.event_type_id', $state); - }); - }); - }), - ]) - ->headerActions([ - Actions\CreateAction::make(), - Actions\Action::make('importCsv') - ->label('CSV Import') - ->icon('heroicon-o-arrow-up-tray') - ->form([ - Schema\Components\FileUpload::make('csv')->acceptedFileTypes(['text/csv','text/plain'])->required(), - Schema\Components\Select::make('scope')->options(['tenant'=>'Tenant','event'=>'Event'])->default('tenant')->required(), - Schema\Components\Select::make('event_id') - ->label('Event (für Scope=Event)') - ->relationship('event', 'name') - ->searchable() - ->visible(fn ($get) => $get('scope') === 'event'), - ]) - ->action(function(array $data) { - $path = $data['csv']; - $full = storage_path('app/public/'.$path); - $csv = Reader::createFromPath($full, 'r'); - $csv->setHeaderOffset(0); - DB::transaction(function() use ($csv, $data) { - foreach ($csv->getRecords() as $row) { - $emotion = Emotion::query()->where('name', $row['emotion'] ?? '')->first(); - if (! $emotion) { continue; } - Task::create([ - 'tenant_id' => auth()->user()->tenant_id, - 'event_id' => $data['scope'] === 'event' ? ($data['event_id'] ?? null) : null, - 'scope' => $data['scope'], - 'emotion_id' => $emotion->id, - 'title' => trim($row['title'] ?? ''), - 'description' => trim($row['description'] ?? ''), - 'difficulty' => $row['difficulty'] ?? 'easy', - 'example_text' => $row['example_text'] ?? null, - 'is_active' => (int)($row['is_active'] ?? 1) === 1, - 'sort_order' => (int)($row['sort_order'] ?? 0), - ]); - } - }); - }) - ->requiresConfirmation(), - ]) - ->recordActions([ - Actions\EditAction::make(), - Actions\DeleteAction::make(), - ]); - } - - public static function getEloquentQuery() - { - // Tenant scoping for tasks: show global + tenant-owned - return parent::getEloquentQuery()->where(function($q){ - $tenantId = auth()->user()->tenant_id; - $q->whereNull('tenant_id')->where('scope','global') - ->orWhere('tenant_id', $tenantId); - }); - } -} -``` - -**EmotionController (filtered by event type):** -```php -event_type_id; - $emotions = Emotion::query() - ->where('is_active', true) - ->where(function($q) use ($typeId) { - $q->whereIn('id', function($sub) use ($typeId) { - $sub->from('emotion_event_type') - ->select('emotion_id') - ->where('event_type_id', $typeId); - }); - }) - ->orderBy('sort_order') - ->get(); - - return response()->json($emotions); - } -} -``` - -**TaskController (respect event type and scope precedence):** -```php -user())->tenant_id; // optional auth - $typeId = $event->event_type_id; - - $query = Task::query() - ->where('emotion_id', $emotionId) - ->where('is_active', true) - ->where(function($q) use ($event, $tenantId, $typeId) { - $q->where('scope', 'event')->where('event_id', $event->id) - ->orWhere(function($q2) use ($tenantId, $typeId) { - $q2->where('scope','tenant') - ->where('tenant_id', $tenantId) - ->where(function($qq) use ($typeId){ - $qq->whereNull('event_type_id')->orWhere('event_type_id', $typeId); - }); - }) - ->orWhere(function($q3) use ($typeId) { - $q3->where('scope','global') - ->where(function($qq) use ($typeId){ - $qq->whereNull('event_type_id')->orWhere('event_type_id', $typeId); - }); - }); - }) - ->orderByRaw("CASE WHEN scope='event' THEN 1 WHEN scope='tenant' THEN 2 ELSE 3 END") - ->orderBy('sort_order') - ->get(); - - return response()->json($query); - } - - public function getRandomTask(Request $request, Event $event, $emotionId) - { - $request->merge(['limit' => 1]); - $tasks = $this->getByEmotion($request, $event, $emotionId)->getData(); - // pick a random from top precedence group - $collection = collect($tasks); - $topScope = $collection->min(fn($t) => $t->scope === 'event' ? 1 : ($t->scope === 'tenant' ? 2 : 3)); - $pool = $collection->filter(fn($t) => ($t->scope === 'event' ? 1 : ($t->scope === 'tenant' ? 2 : 3)) === $topScope); - return response()->json($pool->random()); - } -} -``` - - -### Super Admin — Event Types Resource -```php -columns([ - Tables\\Columns\\TextColumn::make("name")->searchable()->sortable(), - Tables\\Columns\\TextColumn::make("icon"), - Tables\\Columns\\TextColumn::make("color"), - ]) - ->filters([ - Tables\\Filters\\SelectFilter::make("event_type") - ->label("Event Type") - ->options(fn () => EventType::orderBy("name")->pluck("name","id")->all()) - ->query(function($query, $state){ - if (! $state) { return; } - $query->whereIn("id", function($sub) use ($state) { - $sub->from("emotion_event_type") - ->select("emotion_id") - ->where("event_type_id", $state); - }); - }), - ]) - ->recordActions([ - Actions\\EditAction::make(), - Actions\\DeleteAction::make(), - ]) - ->headerActions([ Actions\\CreateAction::make() ]); - } -} -``` - -// app/Filament/SuperAdmin/Resources/EventTypeResource.php - -namespace App\\Filament\\SuperAdmin\\Resources; - -use App\\Models\\EventType; -use Filament\\Resources\\Resource; -use Filament\\Tables; -use Filament\\Tables\\Table; -use Filament\\Forms\\Form; -use Filament\\Schema as Schema; -use Filament\\Actions; - -class EventTypeResource extends Resource -{ - protected static ?string $model = EventType::class; - protected static ?string $navigationIcon = "heroicon-o-rectangle-stack"; - protected static ?string $navigationGroup = "Platform Configuration"; - protected static ?string $label = "Event Types"; - - public static function form(Form $form): Form - { - return $form->schema([ - Schema\\Components\\TextInput::make("name")->required()->maxLength(100), - Schema\\Components\\TextInput::make("slug")->required()->maxLength(100)->unique(ignoreRecord: true), - Schema\\Components\\TextInput::make("icon")->maxLength(64), - Schema\\Components\\KeyValue::make("settings")->keyLabel("Key")->valueLabel("Value"), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\\Columns\\TextColumn::make("name")->searchable()->sortable(), - Tables\\Columns\\TextColumn::make("slug")->searchable(), - Tables\\Columns\\TextColumn::make("icon"), - ]) - ->recordActions([ - Actions\\EditAction::make(), - Actions\\DeleteAction::make(), - ]) - ->headerActions([ Actions\\CreateAction::make() ]); - } -} -``` - -### Filament v4 — Super Admin Global Task Library (with CSV Import) -```php -columns([ - Tables\Columns\TextColumn::make('scope')->badge(), - Tables\Columns\TextColumn::make('tenant.name')->label('Tenant')->toggleable(isToggledHidden: true), - Tables\Columns\TextColumn::make('emotion.name')->label('Emotion'), - Tables\Columns\TextColumn::make('title')->limit(60), - Tables\Columns\BadgeColumn::make('difficulty'), - Tables\Columns\IconColumn::make('is_active')->boolean(), - ] ->filters([ - Tables\\Filters\\SelectFilter::make('event_type_id') - ->label('Event Type') - ->options(fn () => \\App\\Models\\EventType::orderBy('name')->pluck('name','id')->all()) - ->query(function (,){ - if(! ){ return; } - ->where(function() use (){ - ->where('event_type_id',) - ->orWhereExists(function() use (){ - ->selectRaw(1) - ->from('emotion_event_type as eet') - ->whereColumn('eet.emotion_id','tasks.emotion_id') - ->where('eet.event_type_id',); - }); - }); - }), - ]) -) - ->headerActions([ - Actions\Action::make('importCsv') - ->label('CSV Import') - ->icon('heroicon-o-arrow-up-tray') - ->form([ - Schema\Components\FileUpload::make('csv')->acceptedFileTypes(['text/csv','text/plain'])->required(), - Schema\Components\Select::make('scope')->options(['global'=>'Global','tenant'=>'Tenant','event'=>'Event'])->default('global')->required(), - Schema\Components\Select::make('tenant_id')->relationship('tenant','name')->visible(fn($get)=>$get('scope')!=='global'), - Schema\Components\Select::make('event_id')->label('Event')->relationship('event','name')->visible(fn($get)=>$get('scope')==='event'), - ]) - ->action(function(array $data){ - $path = $data['csv']; - $full = storage_path('app/public/'.$path); - $csv = Reader::createFromPath($full, 'r'); - $csv->setHeaderOffset(0); - DB::transaction(function() use ($csv, $data) { - foreach ($csv->getRecords() as $row) { - $emotion = Emotion::query()->where('name', $row['emotion'] ?? '')->first(); - if (! $emotion) { continue; } - Task::create([ - 'scope' => $data['scope'], - 'tenant_id' => $data['scope']!=='global' ? ($data['tenant_id'] ?? null) : null, - 'event_id' => $data['scope']==='event' ? ($data['event_id'] ?? null) : null, - 'emotion_id' => $emotion->id, - 'title' => trim($row['title'] ?? ''), - 'description' => trim($row['description'] ?? ''), - 'difficulty' => $row['difficulty'] ?? 'easy', - 'example_text' => $row['example_text'] ?? null, - 'is_active' => (int)($row['is_active'] ?? 1) === 1, - 'sort_order' => (int)($row['sort_order'] ?? 0), - ]); - } - }); - }) - ->requiresConfirmation(), - ]); - } -} -``` - -Implementation notes: -- Add policies to restrict Tenant Admins from editing global tasks. -- When importing with scope=tenant/event in Super Admin, enforce that referenced tenant/event exist. -- Consider a background Job for large CSV files and a progress UI. - -## 🚀 Deployment & Setup-Anweisungen - -### Entwicklungs-Setup - -#### 1. Laravel Backend Setup -```bash -# Neues Laravel-Projekt erstellen -composer create-project laravel/laravel:^12.0 hochzeits-fotobox -cd hochzeits-fotobox - -# Abhängigkeiten installieren -composer require intervention/image -composer require pusher/pusher-php-server -composer require laravel/sanctum -composer require filament/filament:^4.0 -W -# Falls Filament v3 benoetigt wird (Kompatibilitaet): -# composer require filament/filament:^3.3 -W -composer require league/csv:^9.15 # CSV parsing for task imports -composer require spatie/laravel-translatable # JSON-based field translations (i18n) - -# Environment konfigurieren -cp .env.example .env -# Datenbankverbindung auf SQLite ändern: -# DB_CONNECTION=sqlite -# DB_DATABASE=/absolute/path/to/database/database.sqlite - -# SQLite-Datenbank erstellen -touch database/database.sqlite - -# Migrations erstellen und ausführen -php artisan make:migration create_events_table -php artisan make:migration create_emotions_table -php artisan make:migration create_event_types_table -php artisan make:migration create_emotion_event_type_table -php artisan make:migration add_event_type_id_to_events -php artisan make:migration add_event_type_and_scoping_to_tasks -php artisan make:migration add_i18n_translatables_json --table=events -php artisan make:migration add_i18n_translatables_json_to_emotions --table=emotions -php artisan make:migration add_i18n_translatables_json_to_tasks --table=tasks -php artisan make:migration create_tasks_table -php artisan make:migration create_photos_table -php artisan make:migration create_photo_likes_table - -php artisan migrate - -# Models und Controller generieren -php artisan make:model Event -c -php artisan make:model Emotion -c -php artisan make:model Task -c -php artisan make:model Photo -c -php artisan make:model PhotoLike - -# Seeder für Demo-Daten -php artisan make:seeder EmotionsSeeder -php artisan make:seeder TasksSeeder -php artisan make:seeder DemoEventSeeder - -# Storage-Link erstellen -php artisan storage:link - -# App-Key generieren -php artisan key:generate -``` - -#### 2. Frontend Setup -```bash -# NPM-Abhängigkeiten installieren -npm install - -# React und Tools -npm install react react-dom @vitejs/plugin-react -npm install @tanstack/react-query -npm install react-router-dom -npm install framer-motion -npm install lucide-react - -# Styling -npm install tailwindcss postcss autoprefixer -npm install @tailwindcss/forms @tailwindcss/aspect-ratio - -# PWA -npm install vite-plugin-pwa workbox-window - -# Utils -npm install date-fns -npm install clsx -npm install compressorjs -npm install react-markdown rehype-sanitize - -# Tailwind initialisieren -npx tailwindcss init -p - -# Verzeichnis-Struktur erstellen -mkdir -p resources/js/src/{components,pages,hooks,services,context,utils} -mkdir -p resources/js/src/components/{Camera,Emotions,Gallery,Layout,Forms} -mkdir -p public/icons -``` - -#### 3. Seeder-Daten - -**EmotionsSeeder.php:** -```php - 'Liebe', - 'icon' => '💕', - 'color' => '#ff6b9d', - 'description' => 'Romantische und liebevolle Momente', - 'sort_order' => 1 - ], - [ - 'name' => 'Freude', - 'icon' => '😂', - 'color' => '#ffd93d', - 'description' => 'Lustige und fröhliche Augenblicke', - 'sort_order' => 2 - ], - [ - 'name' => 'Rührung', - 'icon' => '🥺', - 'color' => '#6bcf7f', - 'description' => 'Emotionale und berührende Szenen', - 'sort_order' => 3 - ], - [ - 'name' => 'Nostalgie', - 'icon' => '📸', - 'color' => '#a78bfa', - 'description' => 'Erinnerungen und nostalgische Gefühle', - 'sort_order' => 4 - ], - [ - 'name' => 'Überraschung', - 'icon' => '😲', - 'color' => '#fb7185', - 'description' => 'Unerwartete und überraschende Momente', - 'sort_order' => 5 - ], - [ - 'name' => 'Stolz', - 'icon' => '🏆', - 'color' => '#34d399', - 'description' => 'Stolze und triumphale Augenblicke', - 'sort_order' => 6 - ] - ]; - - foreach ($emotions as $emotionData) { - Emotion::create($emotionData); - } - } -} -``` - -**TasksSeeder.php:** -```php - 'Liebe', - 'tasks' => [ - [ - 'title' => 'Kuss-Foto', - 'description' => 'Macht ein romantisches Kuss-Foto', - 'difficulty' => 'easy', - 'example_text' => 'Einfach küssen und lächeln!' - ], - [ - 'title' => 'Händchen halten', - 'description' => 'Zeigt eure Hände - verheiratet und verliebt', - 'difficulty' => 'easy', - 'example_text' => 'Ringe in Szene setzen' - ], - [ - 'title' => 'Liebes-Pose', - 'description' => 'Stellt die romantischste Szene aus einem Film nach', - 'difficulty' => 'medium', - 'example_text' => 'Titanic, Dirty Dancing oder eure Lieblings-Romanze' - ], - [ - 'title' => 'Herz formen', - 'description' => 'Formt mit euren Händen ein großes Herz', - 'difficulty' => 'easy', - 'example_text' => 'Gemeinsam über dem Kopf oder vor der Brust' - ] - ] - ], - - // Freude-Tasks - [ - 'emotion' => 'Freude', - 'tasks' => [ - [ - 'title' => 'Sprung-Foto', - 'description' => 'Alle springen gleichzeitig in die Luft!', - 'difficulty' => 'medium', - 'example_text' => 'Auf drei: eins, zwei, drei - SPRUNG!' - ], - [ - 'title' => 'Grimassen-Contest', - 'description' => 'Macht die verrücktesten Gesichter', - 'difficulty' => 'easy', - 'example_text' => 'Je alberner, desto besser!' - ], - [ - 'title' => 'Lach-Attacke', - 'description' => 'Alle lachen so laut sie können', - 'difficulty' => 'easy', - 'example_text' => 'Echtes Lachen ist am schönsten' - ], - [ - 'title' => 'Tanz-Pose', - 'description' => 'Zeigt euren besten Tanz-Move', - 'difficulty' => 'medium', - 'example_text' => 'Disco, Hip-Hop oder klassischer Walzer' - ] - ] - ], - - // Rührung-Tasks - [ - 'emotion' => 'Rührung', - 'tasks' => [ - [ - 'title' => 'Tränen der Freude', - 'description' => 'Zeigt eure emotionalen Gesichter', - 'difficulty' => 'easy', - 'example_text' => 'Echte Gefühle sind die schönsten' - ], - [ - 'title' => 'Familien-Umarmung', - 'description' => 'Große Gruppenumarmung mit der ganzen Familie', - 'difficulty' => 'medium', - 'example_text' => 'Alle kommen zusammen für eine warme Umarmung' - ], - [ - 'title' => 'Stille Momente', - 'description' => 'Ein ruhiges, besinnliches Foto', - 'difficulty' => 'medium', - 'example_text' => 'Manchmal sagt ein stiller Blick mehr als tausend Worte' - ] - ] - ], - - // Weitere Emotionen... - ]; - - foreach ($tasks as $emotionGroup) { - $emotion = Emotion::where('name', $emotionGroup['emotion'])->first(); - - if ($emotion) { - foreach ($emotionGroup['tasks'] as $taskData) { - Task::create([ - 'emotion_id' => $emotion->id, - 'title' => $taskData['title'], - 'description' => $taskData['description'], - 'difficulty' => $taskData['difficulty'], - 'example_text' => $taskData['example_text'] ?? null - ]); - } - } - } - } -} -``` - -### Production Deployment - -#### 1. Shared Hosting Setup -```bash -# .htaccess für Laravel in public_html -RewriteEngine On -RewriteCond %{REQUEST_URI} !^/public/ -RewriteRule ^(.*)$ /public/$1 [L] - -# Laravel-Optimierungen -php artisan config:cache -php artisan route:cache -php artisan view:cache - -# Frontend Build -npm run build -``` - -#### 2. VPS/Cloud Deployment (mit Docker) -```dockerfile -# Dockerfile -FROM php:8.4-apache - -# PHP Extensions -RUN docker-php-ext-install pdo pdo_sqlite gd - -# Apache Konfiguration -RUN a2enmod rewrite -COPY . /var/www/html -COPY docker/apache.conf /etc/apache2/sites-available/000-default.conf - -# Permissions -RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache - -# Frontend Build -RUN npm ci && npm run build - -EXPOSE 80 -``` - -#### 3. Produktions-Optimierungen - -**Laravel Optimierungen:** -```php -// config/app.php - Produktionseinstellungen -'debug' => false, -'log_level' => 'error', - -// config/cache.php - File-Cache für bessere Performance -'default' => 'file', - -// config/session.php - Datei-basierte Sessions -'driver' => 'file', -'lifetime' => 720, // 12 Stunden für Hochzeitsfeier -``` - -**Nginx Konfiguration:** -```nginx -server { - listen 80; - server_name your-domain.com; - root /var/www/hochzeits-fotobox/public; - - index index.php; - - # Gzip Kompression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml; - - # Caching für statische Assets - location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # PHP Processing - location ~ \.php$ { - fastcgi_pass unix:/run/php/php8.4-fpm.sock; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - include fastcgi_params; - } - - # Laravel Routes - location / { - try_files $uri $uri/ /index.php?$query_string; - } -} -``` - -## 🧪 Testing & Qualitätssicherung - -### Automatisierte Tests - -#### 1. Backend Tests (PHPUnit) -```php -create(); - $emotion = Emotion::factory()->create(); - $task = Task::factory()->create(['emotion_id' => $emotion->id]); - - $file = UploadedFile::fake()->image('test-photo.jpg', 1200, 1600); - - $response = $this->postJson("/api/events/{$event->slug}/photos", [ - 'photo' => $file, - 'emotion_id' => $emotion->id, - 'task_id' => $task->id, - 'guest_name' => 'Test Gast' - ]); - - $response->assertStatus(201) - ->assertJsonStructure([ - 'success', - 'data' => [ - 'id', - 'file_path', - 'thumbnail_path', - 'guest_name', - 'emotion', - 'task' - ] - ]); - - // Dateien wurden erstellt - Storage::disk('public')->assertExists('photos/' . $file->hashName()); - Storage::disk('public')->assertExists('thumbnails/' . $file->hashName()); - - // Datenbank-Eintrag - $this->assertDatabaseHas('photos', [ - 'event_id' => $event->id, - 'guest_name' => 'Test Gast', - 'emotion_id' => $emotion->id - ]); - } -} -``` - -#### 2. Frontend Tests (Jest + React Testing Library) -```javascript -// src/__tests__/EmotionBubble.test.jsx - -import React from 'react' -import { render, fireEvent, screen } from '@testing-library/react' -import EmotionBubble from '../components/Emotions/EmotionBubble' - -const mockEmotion = { - id: 1, - name: 'Freude', - icon: '😂', - color: '#ffd93d' -} - -describe('EmotionBubble', () => { - test('renders emotion correctly', () => { - render( - - ) - - expect(screen.getByText('😂')).toBeInTheDocument() - expect(screen.getByText('Freude')).toBeInTheDocument() - }) - - test('calls onClick when clicked', () => { - const handleClick = jest.fn() - - render( - - ) - - fireEvent.click(screen.getByRole('button')) - expect(handleClick).toHaveBeenCalledWith(1) - }) - - test('applies correct styling when active', () => { - render( - - ) - - const bubble = screen.getByRole('button') - expect(bubble).toHaveClass('ring-4') - }) -}) -``` - -### Performance-Monitoring - -#### 1. Laravel Telescope (Development) -```bash -composer require laravel/telescope --dev -php artisan telescope:install -php artisan migrate -``` - -#### 2. Frontend Performance (Web Vitals) -```javascript -// src/utils/analytics.js - -import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals' - -function sendToAnalytics(metric) { - // Production Analytics Service - if (process.env.NODE_ENV === 'production') { - console.log('Web Vital:', metric) - - // Beispiel: Sende zu Google Analytics - // gtag('event', 'web_vitals', { - // event_category: 'Web Vitals', - // event_action: metric.name, - // event_value: Math.round(metric.value), - // non_interaction: true, - // }) - } -} - -export function initializeWebVitals() { - getCLS(sendToAnalytics) - getFID(sendToAnalytics) - getFCP(sendToAnalytics) - getLCP(sendToAnalytics) - getTTFB(sendToAnalytics) -} -``` - -## 📊 Analytics & Monitoring - -### Event-Tracking -```javascript -// src/services/eventTracking.js - -class EventTracker { - constructor(eventSlug) { - this.eventSlug = eventSlug - this.events = [] - } - - track(action, data = {}) { - const event = { - timestamp: new Date().toISOString(), - action, - data, - eventSlug: this.eventSlug, - sessionId: this.getSessionId(), - userAgent: navigator.userAgent - } - - this.events.push(event) - this.sendToServer(event) - } - - // Wichtige Events - trackPhotoCapture(emotionId, taskId) { - this.track('photo_capture', { emotionId, taskId }) - } - - trackPhotoUpload(photoId, success) { - this.track('photo_upload', { photoId, success }) - } - - trackEmotionSelect(emotionId) { - this.track('emotion_select', { emotionId }) - } - - trackTaskSkip(taskId) { - this.track('task_skip', { taskId }) - } - - sendToServer(event) { - // API-Endpoint für Analytics - fetch(`/api/events/${this.eventSlug}/analytics`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(event) - -## Frontend PWA Status Update (Sep 2025) - -Implemented guest PWA per plan with React/Vite/Tailwind and PWA plugin. Highlights: - -- App Structure - - Routes: /event/:slug/home, /event/:slug/camera, /event/:slug/gallery, /event/:slug/achievements, /legal/:slug. - - Event home adopts the event landing romance gradient; dark-mode override added. - - Offline fonts bundled (Inter 400/600, Playfair Display 400/700, Great Vibes 400). - -- UI/UX - - Header settings as a sheet (language + theme). Dark/light/system theme with class-based Tailwind and no-flash boot. - - Camera page with floating capture bar: compact emotion scroller, large shutter with animation, last-capture thumbnail (opens Gallery), task lock badge. - - Gallery: grid, modal (Dialog), likes, skeleton placeholders, emotion filter and deep link. - - Achievements page (new): tabs People/Groups/Live. People aggregates claimed tasks by guest; Live shows recent task-claimed photos. i18n included. - - Navigation: replaced "Camera" tab with "Achievements" (trophy). Camera reachable via Home CTA. - -- Data & APIs (current usage) - - Events: GET /api/events/{slug} (returns localized fields and palette when available). - - Emotions: GET /api/events/{slug}/emotions. - - Tasks: GET /api/events/{slug}/tasks/random/{emotion}. - - Photos: GET /api/events/{slug}/photos, POST /api/events/{slug}/photos (compressed < 1.5MB), POST /api/events/{slug}/photos/{photo}/like. - - Legal: GET /api/legal/{slug} (markdown rendered on client). - -- PWA & Offline - - Compression to < 1.5MB before upload, EXIF stripped; offline queue with retry on "online"; toasts for success/queued/error. - - Workbox via vite-plugin-pwa; further SW Background Sync planned. - -- shadcn-style Layer - - Added Button, Card, Dialog, Sheet, Skeleton, Badge primitives for consistency. - -- Super Admin Login - - Switched to classic Laravel session login at /super-admin/login; Filament panel guarded by middleware. - -- QR & Landings - - Root landing with QR scanner (zxing) + manual code entry; event landing redesigned (typography + romance gradient). - -- Demo Data - - Seeder DemoAchievementsSeeder creates demo photos with ask_id for the Achievements page. - -### Gamification Direction (Short-Event) -- Focus on solved tasks: photo claims a task (client), future: peer votes to verify. -- Achievements ideas saved: docs/achievements/ideas.md. -- Planned endpoint: POST /api/events/{slug}/photos/{photo}/task-vote { verdict: 'pass'|'fail', guest_name } ? returns { pass, fail, verified }. - -### Open Items / Next -- Task verification voting (backend + modal UI) and badges (First/Finisher/Sampler/Assist etc.). -- SW Background Sync for uploads; install prompt + shortcuts. -- Admin: palette editor; (optional) groups/teams for Achievements. -- Accessibility polish, reduced-motion paths. - diff --git a/guestlense_articles.md b/guestlense_articles.md deleted file mode 100644 index 9724d8e..0000000 --- a/guestlense_articles.md +++ /dev/null @@ -1,601 +0,0 @@ -# Guestlense Artikel Sammlung - -Diese Datei enthält alle 7 Artikel von der Guestlense-Webseite (https://www.guestlense.com/articles), erfasst am 10. Oktober 2025. - -## Inhaltsverzeichnis - -1. [Wedding Photography on a Budget - A Complete Guide](#artikel-1) -2. [6 Top Alternatives to a Wedding Photographer](#artikel-2) -3. [How To Use QR Codes to Collect Wedding Pictures](#artikel-3) -4. [The Ultimate Guide to Wedding Guest Photo Sharing (Without the Headache)](#artikel-4) -5. [When to Send Wedding Invitations: The Ultimate Guide for Perfect Timing](#artikel-5) -6. [How to Become a Wedding Planner: A Step-by-Step Guide](#artikel-6) -7. [Wedding Pictures QR Code: The Modern Way to Collect and Share Memories](#artikel-7) - ---- - -## Artikel 1: Wedding Photography on a Budget - A Complete Guide - -**Veröffentlicht:** September 10, 2025 - -Capturing every moment of your big day is one of the best ways to create treasured memories. A professional photographer can take breathtaking pictures, but they often come with a hefty price tag. While it's nice to hire someone for this important role, for many, professional photography is simply not possible. The good news is that there are affordable wedding photography options that still offer incredible results. We've made this complete guide showing you how you can have wedding photography on a budget for high-quality, beautiful pictures you'll adore. - -### Why is Wedding Photography So Expensive? - -When planning your wedding, expenses add up quickly. Photography can take a major chunk out of your budget, but why is it so expensive? With a professional photographer, the cost can vary depending on several factors, including their experience, how long you'll use them, the photography style, and any extras you add to the service. On average, couples pay between $2,500 and $6,500. That's a lot of money! Many feel obligated to spend this amount because they falsely believe budget wedding photos will be low quality. You don't have to spend a lot to get quality pictures; you just need to know your options. - -### How to Get Affordable Wedding Photography on a Budget - -#### Choose an Affordable Photographer - -While many wedding photographers are expensive, if you're willing to use one with less experience or choose a smaller package, you could save a lot of money. To start, you'll want to determine the budget you have for the service so you can filter your options and negotiate on price. Having a clear idea of the type of photographs you'd like taken is also helpful, whether you enjoy artistic styles, candid shots, or more traditional themes. Take time to do your research and compare different photographers in your price range. Read reviews and ratings and look at their portfolio. Speak with potential candidates and don't be afraid to ask questions. You want to be on the same page financially, but also artistically. One option, instead of using a professional photographer, is using an experienced novice, who may not do photography professionally, but is highly proficient in creating top-quality images. Photography students are also a great choice, or semi-professionals. Cheap wedding photography can still be high-quality and professional. - -#### Explore DIY Wedding Photography - -Many couples choose to forego the professional photographer completely in exchange for DIY wedding photos. This option is great for those looking for more personal and intimate pictures. There's a ton of options that are cost-efficient and fun, making your wedding day photography a group affair. A few top options include: - -**Disposable Cameras ($50 to $200)** - Place cameras at every table and let guests take their own pictures. Some companies also offer disposable camera packages, which could be an excellent option. - -**Photo Booths ($100 to $500)** - Photo booths offer an immersive and interactive experience for guests, making them an excellent addition to any wedding. You can add props and backdrops for even greater personalization. - -**Use a Hashtag (Free)** - Creating a hashtag for your wedding that guests can use to tag photos is a great, cheap wedding photography option. They use their own phones, and you can create a digital album, where you can print or share the photos. - -**QR Codes for Wedding Photos ($49 to $900+)** - Similar to the hashtag option, a digital wedding guest book allows guests to scan a QR code and upload their photos instantly. Some companies even create a live slideshow that you can show during your reception, which constantly updates images in real-time. - -#### Ask a Friend - -If you have a friend or family member who takes high-quality photographs, they could be an excellent option. As long as they have a good camera and experience, you can still receive stunning images. Putting a loved one in charge of this important task is also beneficial since they have a deeper understanding of you and your fiancé's style, likes, and dislikes. You may find it easier to talk to them and go over different ideas. All in all, having a friend behind the camera can make your wedding day even more special and meaningful. - -Choosing affordable wedding photography doesn't mean you have to sacrifice quality; there's many low-cost options that provide beautiful results. Whether you choose to hire a professional or choose to take DIY wedding photos, as long as you're surrounded by the people who matter most, the day will be unforgettable. - -### FAQ - -#### How to do wedding photography on a budget? - -You can have cheap wedding photography without sacrificing quality by asking for shorter coverage, choosing a basic package, or by taking DIY wedding photos. Opting out of extras like getting a physical album or doing multiple sessions (i.e., engagement, rehearsal dinner, etc.) can make your wedding day photography more affordable. - -#### What if I can't afford a photographer for my wedding? - -If a professional photographer is out of your budget, there's still many affordable wedding photography options. Consider providing guests with digital cameras to take their own pictures, using a QR code and digital wedding guest book, setting up a photo booth station, or making your own hashtag. Depending on the option you choose, the price range can vary from free to a couple hundred dollars. - -#### If my budget is small can I still have quality wedding photos? - -Yes! You don't have to have a large budget to get beautiful, high-quality wedding photos. Consider using a friend or family member to take pictures or hiring a photography student. As long as they have experience and use a quality camera, you can still have breathtaking photos. - ---- - -## Artikel 2: 6 Top Alternatives to a Wedding Photographer - -**Veröffentlicht:** September 6, 2025 - -Let's face it, weddings are expensive. From the venue to the food and everything in between, keeping costs within your budget can be a challenge. One area that can be extremely costly is the wedding photographer. In fact, it's common to spend between $2,500 and $6,500 on this one element alone. Talk about a hit to your wallet. If you want to keep costs low without sacrificing the incredible memories of having wedding day photos, there are alternative wedding photographer options. You can still capture beautiful images and have money left over for the honeymoon! - -### Why Choose an Alternative Wedding Photographer - -There's many reasons why a couple may choose to forego a traditional wedding photographer for an alternative option. They include: -- To keep costs low -- Adding a unique element to the wedding -- Personalization -- Reduced stress - -No matter your reason, if you're thinking of ditching the traditional photographer, you'll want to check out these unique alternative wedding photography ideas. - -### 1. Use Disposable Cameras - -Disposable cameras have made a comeback and are considered a unique and fun addition to weddings. They let guests take their own photos, so you can see the day through their eyes. Not only are these cameras affordable, but they're easy to develop, so you can have your pictures quickly, instead of waiting weeks or months. Some companies even offer [disposable camera packages](https://cheecam.com/), which makes capturing your big day even easier. Couples with large and small budgets love the individuality and charm disposable cameras add to their wedding, with an extra bit of nostalgia that makes the day even more special. - -### 2. Set Up a Photo Booth - -Photo booths are timeless, interactive, and downright fun. Guests love using them, and they add a creative element to your reception. There's a ton of different photo booth options that incorporate digital backgrounds and physical props to take the experience to the next level. You can also use your own camera on a tripod and have someone help take pictures. Many couples use their photo booths as a special event spot, where guests can take time to connect with each other and have fun. You can make it extra special by adding lights, balloons, and flowers so it really stands out. You'll love looking through all the images, and your guests will get a one-of-a-kind keepsake they can display at home, in their cars, or at work. - -### 3. Using a Professional Friend Photographer - -If you love the idea of having a professional photographer but want to use someone you can trust, having a friend who specializes in photography is the perfect compromise. Many people enjoy photography as a hobby, and as long as they have a good camera and enough experience, they could be a great option. Be sure to talk to them beforehand and ask for examples of their work to make sure they're the right fit for your vision. Using a friend can keep costs low without sacrificing image quality. - -### 4. Use QR codes to collect guest photos - -A digital wedding guest book can be an excellent alternative to disposable cameras at weddings. Your guests simply scan a [QR code with their phone and upload their own photos](https://www.guestlense.com/occasions/weddings), videos, and even audio recordings. Companies like [Guestlense](https://www.guestlense.com/) allow for unlimited uploads and easy customization, so you can view and edit photos almost instantly. Plus, they're super affordable, making it easy to get the pictures you want without the high price tag. - -### 5. Use a Popcorn Camera - -A popcorn camera is when a single camera is passed around to all of your guests, and they can take their own pictures of the event. It's a great interactive option that gets everyone involved and creates unique photos that reflect each guest's unique personality. It's not only fun for those attending your wedding; you and your spouse will have a blast going through the candid shots and seeing the night from different perspectives. For the best results, be sure to provide clear guidelines of what you want and don't want photographed, and have someone assigned to keeping an eye on the camera throughout the night. - -### 6. Choose a Hybrid Approach - -You don't have to stick to just one option; you can create your own personalized hybrid approach to your wedding day photography. Maybe you like the idea of an interactive photo booth and a digital wedding guest book. You could also have your guests upload their pictures to a photo sharing app and use a friend who specializes in photography. When it comes to choosing alternative wedding photography ideas, it's okay to think outside the box and do what works best for you. After all, this is your wedding, and you want the day to reflect you and your spouse. - -Using an alternative wedding photographer is a fun and easy way to save money without compromising on professionalism or quality. Have fun with it and do what makes you happy. When it comes to your wedding, there's no rules, so you can't go wrong. Whether you choose to get your guests involved with digital cameras, use a photo booth or digital wedding guest book, utilize your friend's photography skills, or go with a hybrid approach, these alternative wedding photography ideas will help you choose the option that's right for your special day. - ---- - -## Artikel 3: How To Use QR Codes to Collect Wedding Pictures - -**Veröffentlicht:** September 3, 2025 - -One of the most popular ways to get one-of-a-kind wedding photos is by using a custom QR code. More and more couples are turning to this low-cost option in place of or in addition to a traditional photographer. With a QR code, your guests enjoy an interactive and immersive experience, using their own phones to capture your special day. You'll love getting to see pictures from different perspectives, so you can get glimpses of your wedding from multiple points of view. In this article, we'll explain how to make a QR code for wedding pictures so you can take advantage of this exciting tool. - -### What is a QR code and How Does It Work? - -A QR code is a custom barcode used to store information like pictures, website URLS, and more. They work like instant links connecting you with a pre-programmed destination. To use, scan the QR code with a smartphone camera. Each code has a unique square pattern, which the camera identifies and analyzes as binary code. It then uses that information to create a clickable link, which you simply press, and it leads you to the pre-set site. You can create your own QR code customized to your wedding's color scheme, print it, and display it for your guests to use. - -### How to Make a QR Code For Wedding Pictures - -Creating a [QR code for your wedding pictures](https://www.guestlense.com/) is easy and only takes two steps. To start, you'll want to create a shared digital photo album, either using cloud storage or a digital wedding guest book. While using a cloud-storage album is free, photo-sharing services can give a more streamlined and polished experience. You can use them to organize photos or create slideshows from the images. - -If you choose to use a cloud service like iCloud, Dropbox, or Google Photos, you'll want to enable sharing and set the access to "Anyone with the link can view." This step allows guests to add their own pictures. It's now time to generate the QR code. Using a QR code generator for wedding pictures is free using a service like Canva, Adobe Express, or QR Code Generator. Copy the unique URL from your cloud storage album and paste it into the QR code generator's text field. Once done, you can customize it with your own design and color for even greater personalization. - -If you choose a digital wedding guest book, the company creates the QR code for you. Your guests may also be able to upload videos and audio, and there are even live slideshow options. While these extras are great, they do come with a cost, which can vary depending on the company. - -### What is the Best QR Code for Wedding Pictures? - -There is no specific QR code that's best for weddings; it depends on the features you would like to use. Services like [Guestlense](https://www.guestlense.com/) have multiple tiers based on different option offerings. When choosing a QR code platform, consider: -- Your Budget -- Photo Quality -- Ease of Use -- Security and Privacy -- Additional Features - -No matter your budget, anyone can take advantage of a QR code for their wedding pictures. - -### How Guests Use a QR Code for Wedding Photos - -Once you've generated your unique QR code for your wedding photos, you can print it out and display it for your guests. Place it in key locations like: -- Ceremony programs -- Menus -- Welcome signs -- Guest table -- Wedding reception tables - -Having multiple signs with the QR code around the venue ensures everyone can access and use it. Placing directions next to the code is also a good idea, especially for older guests or those who aren't technologically savvy. Include a quick note with a message like " Scan to share and view wedding photos," or have a dedicated person to assist anyone who needs help. - -### How to View Photos from Your QR Code - -Once a guest scans the QR code and uploads a picture, you can view it in your digital photo album. The album will update throughout the event, giving you and your guests real-time access to view all of the pictures. You can organize your album into different photos, like the engagement party, rehearsal dinner, ceremony, and reception. Having all photos in the correct album makes it easier to view and makes the experience more enjoyable. - -### Tips for Making Your QR Code Successful - -Creating a QR code for your wedding is a fast and easy way to upload and view photos. Here are a few tips to keep in mind to ensure it's successful. - -#### Keep it simple - -Being able to customize your QR code is an exciting feature, but you want to remember that it's for your wedding. You want it to be elegant and match the theme and aesthetic so it feels cohesive. - -#### Prioritize privacy - -Privacy is always a top concern for online photos, and something you should take seriously. If you don't like the idea of your images being public, you can restrict access by adding a password-protected landing page or changing the settings to private. Just be sure to let guests know the password or how to access the album so they can upload and view their photos. - -#### Choose a dedicated platform - -If you're not great with technology, you may want to use a dedicated platform like [Guestlense](https://my.guestlense.com/register) for your QR code. They handle the entire process so you can sit back and enjoy your special day. - -Using a QR code for your wedding pictures is a great way to capture memories and get your guests involved in your big day. Whether you choose to generate it on your own or use a dedicated platform, it's an excellent option for anyone looking to add an extra special touch to their wedding. - ---- - -## Artikel 4: The Ultimate Guide to Wedding Guest Photo Sharing (Without the Headache) - -**Veröffentlicht:** March 19, 2025 - -Your wedding day is a beautiful blur of emotions, laughter, and little magical moments. But here's the thing—your photographer can't be everywhere at once. That's why some of the most priceless photos from your big day might come from the very people you invited to celebrate: your guests. - -From goofy dance floor selfies to behind-the-scenes snapshots during cocktail hour, your friends and family are walking, talking photojournalists. The only problem? How do you actually collect all those amazing guest photos without chasing everyone down afterward? - -If you're wondering about the best ways to get guests to share their photos, you're in the right place. We'll walk you through the why, the how, and the tools to make **wedding guest photo sharing** a breeze. Plus, we'll introduce you to **Guestlense**, a brilliant tool that simplifies everything. - -### Why Guest Photos Matter (Even When You Have a Pro Photographer) - -Hiring a wedding photographer is absolutely worth it—they'll capture the polished, frame-worthy moments. But your guests? They're the ones who'll snap: -- Your flower girl falling asleep mid-toast -- Your college friends starting an impromptu dance battle -- Your dad tearing up during the first look -- The moment your new spouse sneakily steals one more slice of cake - -These candid moments are pure gold. But without a solid plan, they'll get lost in camera rolls or buried in social feeds. - -That's why a streamlined photo-sharing plan is a must for modern weddings. - -### The Best Ways to Collect Wedding Photos from Guests - -Let's talk solutions. There are more ways than ever for guests to share their photos—and some are much better than others. Here are the top options ranked from easiest to most effort. - -#### 1. Use a Wedding Guest Photo Sharing App (Like Guestlense) - -If you want the easiest and most guest-friendly option, hands down, use a dedicated wedding photo sharing tool like [Guestlense](https://www.guestlense.com). - -**Here's why couples love it:** -- No app download required — Guests just scan a QR code and start uploading -- Real-time uploads — You can see photos as your wedding unfolds -- Private gallery — All your photos live in one secure, organized place -- Video uploads too — Because some moments need more than one frame - -It's perfect for couples who don't want to hassle with complicated apps. Just generate your event QR code, print it on signs or place cards, and let the magic happen. - -**Pro tip: Display your Guestlense QR code on the bar, the welcome table, or even in the bathroom mirror for maximum visibility.** - -#### 2. Create a Shared Album (Google Photos, Dropbox, iCloud) - -Another way to collect images is with a shared folder using a service like: -- [Google Photos](https://photos.google.com/) -- [Dropbox](https://www.dropbox.com/) -- [iCloud Shared Albums](https://support.apple.com/en-us/HT202786) - -Just create an album before the wedding, then share the link afterward via group text or email. - -**Pros:** -- Familiar to most guests -- Free (or low cost) -- Good for organizing lots of files - -**Cons:** -- Not very interactive or fun -- May require signing in or downloading an app -- You'll probably need to remind guests multiple times - -#### 3. Set Up a Wedding Hashtag (If You Love Social Media) - -Creating a custom wedding hashtag like `#JessAndCamSayIDo` can be a cute way to gather photos on Instagram or TikTok. Just make sure your hashtag is unique so your pictures don't get mixed up with someone else's event. - -**Here's the reality check:** -- Not everyone will use it -- Not everyone will post publicly -- Not everyone will upload clear or high-quality photos - -It's a nice complement to other methods, but shouldn't be your only strategy. - -#### 4. Use Disposable or Instant Cameras (If You Love Nostalgia) - -Disposable cameras on every table bring a fun, vintage vibe. Guests enjoy them, and it encourages people to snap photos who might not normally pull out their phones. - -**Downsides:** -- Printing and developing can get expensive -- The quality can be hit-or-miss -- You won't know what was captured until after the wedding - -For best results, pair a few instant cameras with a digital option like Guestlense. - -### How to Get Guests to Actually Upload Photos - -You've got the tool—now you need guests to use it. Here's how to make sure the uploads roll in smoothly. - -#### 1. Display Clear Signage - -Design a sign that says something like: **"Help us relive the day—upload your photos! Scan the QR code or visit [your guestbook link]."** - -Place signs at your entrance, guest tables, near the dance floor, and anywhere guests might hang out. - -#### 2. Make a Quick Announcement - -Have your DJ or MC remind everyone during dinner or just before dancing. A simple heads-up works wonders in encouraging participation. - -#### 3. Add It to Your Wedding Website and Invites - -Include your photo-sharing link on your wedding website and consider a small insert in your invitation suite. The more exposure guests have beforehand, the more likely they'll remember. - -#### 4. Send a Post-Wedding Reminder - -A few days after the big day, send a thank-you text or email that includes a gentle reminder and your upload link. Some of the best photos might still be sitting in your cousin's phone waiting to be shared. - -### Wedding Guest Photo Sharing FAQ - -**How do I collect wedding photos from guests without using social media?** -Use a tool like [Guestlense](https://www.guestlense.com) that lets guests upload photos and videos directly to a private gallery. No accounts, apps, or hashtags required. - -**Can I use Google Photos to collect guest photos?** -Yes, but it may not be the most intuitive option for every guest. If you go this route, keep instructions simple and provide clear links. - -**What's the easiest way to have guests upload wedding photos?** -Using a platform like Guestlense is by far the easiest. It works with a simple link or QR code, doesn't require an app, and keeps all your memories in one place. - -### Final Thoughts: Don't Miss the Magic Your Guests Capture - -When it's all said and done, **wedding guest photo sharing** isn't just about collecting more images. It's about seeing your day from the eyes of the people you love. Every blurry dance floor pic, every teary hug, every stolen kiss—they're all part of the story. - -Make it easy for guests to contribute. Whether you go with a dedicated platform like **Guestlense**, a shared folder, or a mix of methods, the key is simplicity. Give guests clear instructions, and don't be afraid to follow up. - -Because years from now, when the cake is gone and the dress is packed away, you'll be so glad you captured those little moments that made your day **yours**. - ---- - -## Artikel 5: When to Send Wedding Invitations: The Ultimate Guide for Perfect Timing - -**Veröffentlicht:** December 15, 2024 - -Timing is everything when it comes to wedding planning, and sending out your wedding invitations is no exception. Sending them too early might cause guests to forget, while sending them too late could lead to scheduling conflicts. This guide will help you nail the perfect timeline for sending your wedding invitations, ensuring your guests have plenty of time to RSVP and plan for your big day. - -### 1. The General Rule: 6 to 8 Weeks Before the Wedding - -For most weddings, the sweet spot for mailing invitations is **6 to 8 weeks before the wedding date**. This timeframe gives guests enough time to RSVP, arrange travel, and block off their calendars, without feeling rushed or forgetting the event. - -However, this general rule can vary depending on the type of wedding and your guests' circumstances. - -### 2. When to Send Save-the-Dates - -Save-the-dates are your first official communication with guests and serve as a heads-up about your wedding date and location. These should be sent: -- **8 to 12 Months in Advance** for destination weddings or peak travel seasons. -- **6 to 8 Months in Advance** for local weddings or smaller gatherings. - -Save-the-dates don't require detailed information, but they should include the date, location, and a note indicating that formal invitations will follow. - -### 3. Factors That Impact When to Send Wedding Invitations - -#### Destination Weddings - -If your wedding requires guests to travel long distances or book accommodations, you'll need to give them extra notice. -- **Mail Invitations:** 3 to 4 months in advance. -- **Save-the-Dates:** 12 months in advance. - -#### Holiday or Peak Season Weddings - -Weddings during holidays or busy seasons like summer require earlier communication. -- **Mail Invitations:** 8 to 10 weeks in advance. -- **Save-the-Dates:** 9 to 12 months in advance. - -#### Smaller, Local Weddings - -If most of your guests are local and the event is more intimate, you can stick to the standard timeline: -- **Mail Invitations:** 6 to 8 weeks in advance. -- **Save-the-Dates:** Optional but appreciated, especially for out-of-town guests. - -### 4. RSVP Deadlines: Setting the Right Date - -Your RSVP deadline should be approximately **2 to 4 weeks before the wedding**. This gives you time to: -- Finalize your headcount for catering and seating arrangements. -- Follow up with guests who haven't responded. - -Include the RSVP deadline on your invitation and make it easy for guests to respond by providing a stamped return envelope, an online RSVP option, or both. - -### 5. Sending Invitations for Other Wedding Events - -In addition to your wedding invitations, you may need to send invites for other events, like rehearsal dinners or post-wedding brunches. Here's when to send them: -- **Rehearsal Dinner Invitations:** 4 to 6 weeks before the wedding. -- **Post-Wedding Brunch Invitations:** Include these with the wedding invitation or send them 4 weeks in advance. - -### 6. How to Ensure Timely Delivery - -#### Prepare Early - -Start gathering addresses and designing your invitations well in advance. Aim to have your invitations printed and ready to mail at least **3 to 4 weeks before your mailing date**. - -#### Choose Reliable Shipping Methods - -Use a reputable postal service and consider hand-canceling your invitations at the post office to prevent damage. For international guests, send invitations at least **12 weeks in advance** to account for shipping delays. - -#### Double-Check Addresses - -Avoid returned invitations by verifying addresses ahead of time. Tools like Google Sheets or wedding planning apps can help you keep track of guest information. - -### 7. What to Include in Your Wedding Invitation Suite - -Your invitation suite should include all the essential details guests need to plan for your wedding: -- **Main Invitation:** Includes your names, wedding date, time, and venue. -- **Details Card:** Covers additional information like dress code, accommodations, and transportation. -- **RSVP Card:** Allows guests to confirm their attendance and specify meal preferences if applicable. -- **Wedding Website:** If you have a wedding website, include the URL for more details. - -### 8. Tips for Digital Invitations - -If you're opting for digital invitations, the same timing rules apply. Email invitations are ideal for casual or eco-friendly weddings and can streamline the RSVP process. - -### 9. Avoid These Common Mistakes - -#### Sending Invitations Too Late - -Waiting until the last minute can leave guests scrambling to adjust their schedules. - -#### Not Accounting for Travel Time - -If you have international or out-of-town guests, account for longer mailing and travel times. - -#### Overlooking RSVP Deadlines - -Failing to set or enforce an RSVP deadline can lead to unnecessary stress when finalizing your guest list. - -### 10. Summary Timeline for Sending Invitations - -Here's a quick reference for when to send wedding-related communications: -- **Save-the-Dates:** 6 to 12 months before the wedding. -- **Wedding Invitations:** 6 to 8 weeks before the wedding (3 to 4 months for destination weddings). -- **RSVP Deadline:** 2 to 4 weeks before the wedding. -- **Other Event Invitations:** 4 to 6 weeks before the event. - -### Final Thoughts - -Sending wedding invitations at the right time is key to ensuring a smooth planning process and a well-attended celebration. By following these guidelines, you'll give your guests ample time to prepare while keeping your wedding timeline on track. - -Planning a wedding is all about balance—timing, organization, and communication. With a little preparation, your invitations will set the tone for a day your guests will never forget! - ---- - -## Artikel 6: How to Become a Wedding Planner: A Step-by-Step Guide - -**Veröffentlicht:** December 15, 2024 - -Becoming a wedding planner is a rewarding career choice for those with a passion for creativity, organization, and love for celebrating life's special moments. If you've ever dreamed of orchestrating unforgettable weddings, here's a comprehensive guide to help you break into the industry. - -### 1. Understand the Role of a Wedding Planner - -Before diving in, it's crucial to know what being a wedding planner entails. Wedding planners are responsible for: -- Coordinating all aspects of a wedding, from venue selection to vendor management. -- Managing budgets and timelines. -- Problem-solving under pressure. -- Communicating with clients to bring their vision to life. - -This role requires a mix of creativity, logistical skills, and emotional intelligence to navigate high-stakes, emotionally charged events. - -### 2. Assess Your Skills and Passion - -Successful wedding planners possess a unique combination of traits, including: -- **Organization:** Managing multiple details simultaneously. -- **Creativity:** Designing beautiful and personalized weddings. -- **Interpersonal Skills:** Building trust with clients and vendors. -- **Problem-Solving:** Handling unexpected challenges gracefully. - -If these traits resonate with you, you're off to a great start. - -### 3. Gain Relevant Experience - -Experience is key in the wedding planning industry. Here's how to build it: -- **Start Small:** Plan events for friends or family. This allows you to build a portfolio and gain hands-on experience. -- **Volunteer:** Offer to assist established wedding planners. This provides exposure to the industry and helps you learn from seasoned professionals. -- **Work in Related Fields:** Roles in event planning, catering, or hospitality can provide valuable insights into the wedding industry. - -### 4. Educate Yourself - -While formal education isn't required to become a wedding planner, it can give you a competitive edge. Consider: -- **Certifications:** Programs like those offered by the *Wedding Planning Institute* or *The Bridal Society* can teach you industry-specific skills. -- **Workshops and Seminars:** These often cover topics like contract negotiation, budgeting, and design trends. -- **Business Skills:** Courses in marketing, accounting, or entrepreneurship can help you run your own wedding planning business. - -### 5. Build a Portfolio - -A strong portfolio showcases your creativity and organizational skills. Include: -- Photos of events you've planned. -- Client testimonials. -- Mood boards or design concepts. - -Even if you're just starting, mock weddings or styled shoots can help demonstrate your vision and capabilities. - -### 6. Establish Your Brand - -Your brand is how potential clients perceive you. Focus on: -- **Creating a Business Name and Logo:** Choose something memorable and professional. -- **Building a Website:** Showcase your portfolio, services, and contact information. -- **Social Media Presence:** Platforms like Instagram and Pinterest are vital for reaching brides and grooms. - -### 7. Network with Vendors - -Wedding planners work closely with vendors, including florists, photographers, caterers, and DJs. Build strong relationships by: -- Attending industry events and bridal expos. -- Reaching out to local vendors to introduce yourself. -- Collaborating on styled shoots to create mutual exposure. - -### 8. Start Small and Scale Up - -In the beginning, you may work on smaller weddings or offer discounted rates to build your client base. Over time: -- Raise your rates as you gain experience. -- Specialize in certain types of weddings (e.g., destination, luxury, or eco-friendly). -- Hire a team or assistant planners to expand your capacity. - -### 9. Stay Current with Trends - -The wedding industry evolves rapidly. Stay informed by: -- Following wedding blogs, magazines, and influencers. -- Attending conferences like *Wedding MBA*. -- Keeping an eye on popular platforms like Pinterest for emerging trends. - -### 10. Deliver Exceptional Service - -Word-of-mouth referrals are invaluable in the wedding planning business. To ensure glowing recommendations: -- Communicate clearly and regularly with your clients. -- Be proactive in solving problems. -- Go above and beyond to make each wedding memorable. - -### 11. Embrace Challenges and Celebrate Successes - -Wedding planning can be stressful, but it's also incredibly fulfilling. You'll face challenges like last-minute changes or difficult clients, but the joy of seeing a couple's dream wedding come to life makes it all worthwhile. - -### Final Thoughts - -Becoming a wedding planner requires dedication, creativity, and a love for creating unforgettable moments. By following these steps and staying committed to your craft, you can build a thriving career in the wedding planning industry. Whether you're orchestrating grand celebrations or intimate gatherings, you'll play a pivotal role in one of the most important days of a couple's life. - -Start small, dream big, and make your mark in the world of weddings! - ---- - -## Artikel 7: Wedding Pictures QR Code: The Modern Way to Collect and Share Memories - -**Veröffentlicht:** December 13, 2024 - -Gone are the days of disposable cameras on every table or waiting weeks to gather photos from your wedding guests. With technology evolving, the humble [QR code](https://digital.gov/resources/introduction-to-qr-codes/) has emerged as a game-changer for wedding photo sharing. Incorporating a QR code into your wedding day makes it effortless for guests to upload and access pictures in real-time, creating a seamless and interactive experience. Here's how you can use a wedding pictures QR code to elevate your big day. - -### What is a Wedding Pictures QR Code? - -[A wedding pictures QR code](https://www.guestlense.com/occasions/weddings) is a scannable code that directs your guests to a digital gallery or upload platform. Whether it's for uploading the photos they take during the event or accessing the official wedding album, QR codes simplify the process and ensure no moment is missed. Guests can use their smartphones to scan the code, instantly connecting them to your chosen platform. - -### Benefits of Using a QR Code for Wedding Pictures - -A QR code simplifies the process of gathering and sharing photos, making it accessible for all your guests. - -#### Easy Photo Collection - -QR codes eliminate the need for guests to email or message their photos. With one quick scan, they can upload pictures directly to a shared album or cloud storage. This convenience encourages more participation and ensures you get a wide variety of candid shots. - -#### Real-Time Sharing - -Want to relive the magic of your wedding day right away? QR codes enable guests to upload photos as the event unfolds. This feature allows you and your loved ones to enjoy snapshots of the celebration without waiting for the photographer's edits. - -#### Cost-Effective - -Instead of investing in multiple photographers or renting photo booths, a QR code lets you crowdsource pictures from every corner of the venue. You'll capture different perspectives without adding to your wedding budget. - -#### Eco-Friendly - -By going digital, you can skip printing physical instructions or distributing USB drives. A simple QR code on a sign, program, or table card reduces paper waste while enhancing guest engagement. - -### How to Create a Wedding Pictures QR Code - -You're now taking the first steps towards collecting guest photos. Congrats! Here's how you get started: - -#### 1. Choose a Photo-Sharing Platform - -Select a platform where guests can upload and view photos. Popular options include Google Photos, Dropbox, or specialized wedding apps like [Guestlense](http://www.guestlense.com), [Honcho](https://thehoncho.app/), or [Guestpix](https://guestpix.com). Guestlense is designed specifically for weddings, offering a user-friendly interface where guests can effortlessly upload photos and videos. It also allows you to curate a beautiful gallery to share with your loved ones after the big day. - -#### 2. Generate Your QR Code - -Use a free QR code generator online to create a custom code linking to your photo-sharing platform. Many tools allow you to add personalization, such as your wedding colors or names. - -#### 3. Test the QR Code - -Before the big day, test the QR code to ensure it's working correctly. Ask a few friends or family members to try scanning it and uploading photos. - -#### 4. Display the QR Code - -Print the QR code on your wedding invitations, programs, or signage at the venue. For a more creative touch, consider incorporating it into your table centerpieces or favors. - -### Creative Ideas for Using QR Codes at Your Wedding - -Make the most of your guestbook with fun ways your guests can interact and share memories. - -#### Interactive Photo Stations - -Set up a photo station with a QR code that directs guests to a gallery of the day's highlights. Include props and signs encouraging them to snap and share. - -#### Guestbook Integration - -Combine your digital gallery with a virtual guestbook. Guests can scan the QR code to leave heartfelt messages alongside their photos. Guestlense offers an integrated feature where guests can add personalized notes to their uploads, making your gallery even more meaningful. - -#### Thank You Cards - -After the wedding, include a QR code on your thank-you cards. This code can lead guests to a curated album of professional photos and candid moments from the day. With Guestlense, you can create a polished and easily accessible gallery to share with everyone who celebrated with you. - -### Tips for Success - -Just as it's important to pick the best platform, you need to find ways to get your guests involved. - -#### Make it Visible - -Place QR codes in high-traffic areas such as the reception entrance, dinner tables, or bar. - -#### Provide Instructions - -Include a short explanation like, "Scan to share your photos!" to guide less tech-savvy guests. - -#### Secure Your Gallery - -Use password protection or restrict access to ensure privacy. - -### Why Choose Guestlense for Your Wedding? - -[Guestlense](http://www.guestlense.com) takes the hassle out of collecting and sharing wedding photos. Unlike generic platforms, Guestlense is tailored for weddings, offering: -- **Real-Time Uploads:** See your wedding come to life as guests upload photos and videos instantly. -- **Beautiful Galleries:** Create a stunning, customizable gallery to showcase your memories. -- **Easy Sharing:** Provide a single QR code that's intuitive for guests of all ages to use. -- **Enhanced Privacy:** Keep your special moments secure with password-protected access. - -### Conclusion - -Using a wedding pictures QR code is a simple yet innovative way to capture the joy and excitement of your big day. Platforms like Guestlense make the process even more seamless, ensuring you'll have a dynamic and beautiful collection of memories. With Guestlense, you can encourage guests to actively participate in preserving your wedding's special moments, all while enjoying a stress-free experience. - ---- - -*Quelle: https://www.guestlense.com/articles - Erfasst am 10. Oktober 2025* \ No newline at end of file diff --git a/guestlense_articles_deutsch.md b/guestlense_articles_deutsch.md deleted file mode 100644 index 1ac8317..0000000 --- a/guestlense_articles_deutsch.md +++ /dev/null @@ -1,601 +0,0 @@ -# Guestlense Artikel Sammlung (Deutsch) - -Diese Datei enthält die deutschen Übersetzungen aller 7 Artikel von der Guestlense-Webseite (https://www.guestlense.com/articles), erfasst am 10. Oktober 2025. - -## Inhaltsverzeichnis - -1. [Hochzeitsfotografie mit kleinem Budget - Ein vollständiger Leitfaden](#artikel-1) -2. [6 Top-Alternativen zum Hochzeitsfotografen](#artikel-2) -3. [Wie man QR-Codes verwendet, um Hochzeitsbilder zu sammeln](#artikel-3) -4. [Der ultimative Leitfaden zur Hochzeitsgast-Foto-Teilung (Ohne Kopfschmerzen)](#artikel-4) -5. [Wann Hochzeitseinladungen verschickt werden sollten: Der ultimative Leitfaden für perfektes Timing](#artikel-5) -6. [Wie man Hochzeitsplaner wird: Eine Schritt-für-Schritt-Anleitung](#artikel-6) -7. [Hochzeitsbilder QR-Code: Die moderne Art, Erinnerungen zu sammeln und zu teilen](#artikel-7) - ---- - -## Artikel 1: Hochzeitsfotografie mit kleinem Budget - Ein vollständiger Leitfaden - -**Veröffentlicht:** 10. September 2025 - -Jeden Moment Ihres großen Tages einzufangen, ist eine der besten Möglichkeiten, geschätzte Erinnerungen zu schaffen. Ein professioneller Fotograf kann atemberaubende Bilder machen, aber sie kommen oft mit einem hohen Preis. Auch wenn es schön ist, jemanden für diese wichtige Rolle zu engagieren, ist professionelle Fotografie für viele einfach nicht möglich. Die gute Nachricht ist, dass es erschwingliche Hochzeitsfotografie-Optionen gibt, die dennoch unglaubliche Ergebnisse liefern. Wir haben diesen vollständigen Leitfaden erstellt, der Ihnen zeigt, wie Sie Hochzeitsfotografie mit kleinem Budget für hochwertige, schöne Bilder erhalten können, die Sie lieben werden. - -### Warum ist Hochzeitsfotografie so teuer? - -Bei der Planung Ihrer Hochzeit addieren sich die Ausgaben schnell. Fotografie kann einen großen Teil Ihres Budgets beanspruchen, aber warum ist sie so teuer? Bei einem professionellen Fotografen kann der Preis je nach mehreren Faktoren variieren, einschließlich seiner Erfahrung, wie lange Sie ihn nutzen werden, dem Fotografie-Stil und allen Extras, die Sie zum Service hinzufügen. Im Durchschnitt zahlen Paare zwischen 2.500 und 6.500 Euro. Das ist viel Geld! Viele fühlen sich verpflichtet, diesen Betrag auszugeben, weil sie fälschlicherweise glauben, dass Budget-Hochzeitsfotos von geringer Qualität sein werden. Sie müssen nicht viel ausgeben, um Qualitätsbilder zu bekommen; Sie müssen nur Ihre Optionen kennen. - -### Wie Sie erschwingliche Hochzeitsfotografie mit kleinem Budget bekommen - -#### Wählen Sie einen erschwinglichen Fotografen - -Während viele Hochzeitsfotografen teuer sind, könnten Sie viel Geld sparen, wenn Sie einen mit weniger Erfahrung wählen oder ein kleineres Paket auswählen. Zuerst sollten Sie das Budget bestimmen, das Sie für den Service haben, damit Sie Ihre Optionen filtern und über den Preis verhandeln können. Es ist auch hilfreich, eine klare Vorstellung von der Art von Fotos zu haben, die Sie machen lassen möchten, egal ob Sie künstlerische Stile, spontane Aufnahmen oder traditionellere Themen mögen. Nehmen Sie sich Zeit für Ihre Recherche und vergleichen Sie verschiedene Fotografen in Ihrer Preisklasse. Lesen Sie Bewertungen und Ratings und schauen Sie sich ihr Portfolio an. Sprechen Sie mit potenziellen Kandidaten und scheuen Sie sich nicht, Fragen zu stellen. Sie wollen finanziell auf derselben Seite sein, aber auch künstlerisch. Eine Option, anstatt einen professionellen Fotografen zu nutzen, ist einen erfahrenen Anfänger, der möglicherweise nicht professionell fotografiert, aber hochqualifiziert darin ist, erstklassige Bilder zu erstellen. Fotografie-Studenten sind ebenfalls eine gute Wahl, oder Semi-Professionelle. Günstige Hochzeitsfotografie kann dennoch hochwertig und professionell sein. - -#### Erkunden Sie DIY-Hochzeitsfotografie - -Viele Paare entscheiden sich dafür, den professionellen Fotografen komplett gegen DIY-Hochzeitsfotos einzutauschen. Diese Option ist großartig für diejenigen, die persönlichere und intimere Bilder suchen. Es gibt eine Menge kosteneffizienter und unterhaltsamer Optionen, die Ihre Hochzeitsfotografie zu einer Gruppenangelegenheit machen. Einige Top-Optionen sind: - -**Einwegkameras (50 bis 200 Euro)** - Platzieren Sie Kameras an jedem Tisch und lassen Sie die Gäste ihre eigenen Bilder machen. Einige Unternehmen bieten auch Einwegkamera-Pakete an, die eine ausgezeichnete Option sein könnten. - -**Fotoboxen (100 bis 500 Euro)** - Fotoboxen bieten ein immersives und interaktives Erlebnis für Gäste und sind eine ausgezeichnete Ergänzung zu jeder Hochzeit. Sie können Requisiten und Hintergründe für noch größere Personalisierung hinzufügen. - -**Verwenden Sie einen Hashtag (Kostenlos)** - Das Erstellen eines Hashtags für Ihre Hochzeit, den Gäste zum Markieren von Fotos verwenden können, ist eine großartige, günstige Hochzeitsfotografie-Option. Sie verwenden ihre eigenen Telefone und Sie können ein digitales Album erstellen, in dem Sie die Fotos drucken oder teilen können. - -**QR-Codes für Hochzeitsfotos (49 bis 900+ Euro)** - Ähnlich wie die Hashtag-Option ermöglicht ein digitales Hochzeitsgästebuch den Gästen, einen QR-Code zu scannen und ihre Fotos sofort hochzuladen. Einige Unternehmen erstellen sogar eine Live-Diashow, die Sie während Ihres Empfangs zeigen können und die Bilder in Echtzeit aktualisiert. - -#### Fragen Sie einen Freund - -Wenn Sie einen Freund oder Familienmitglied haben, der hochwertige Fotos macht, könnte er eine ausgezeichnete Option sein. Solange sie eine gute Kamera und Erfahrung haben, können Sie dennoch atemberaubende Bilder erhalten. Einen geliebten Menschen mit dieser wichtigen Aufgabe zu betrauen, ist auch vorteilhaft, da er ein tieferes Verständnis für Sie und den Stil, die Vorlieben und Abneigungen Ihres Verlobten hat. Sie finden es möglicherweise einfacher, mit ihnen zu sprechen und verschiedene Ideen zu besprechen. Alles in allem kann ein Freund hinter der Kamera Ihren Hochzeitstag noch spezieller und bedeutungsvoller machen. - -Die Wahl erschwinglicher Hochzeitsfotografie bedeutet nicht, dass Sie Qualität opfern müssen; es gibt viele kostengünstige Optionen, die schöne Ergebnisse liefern. Ob Sie sich dafür entscheiden, einen Profi zu engagieren oder DIY-Hochzeitsfotos zu machen, solange Sie von den Menschen umgeben sind, die Ihnen am wichtigsten sind, wird der Tag unvergesslich sein. - -### FAQ - -#### Wie macht man Hochzeitsfotografie mit kleinem Budget? - -Sie können günstige Hochzeitsfotografie ohne Qualitätsverlust haben, indem Sie kürzere Abdeckung verlangen, ein Basispaket wählen oder DIY-Hochzeitsfotos machen. Das Weglassen von Extras wie einem physischen Album oder mehreren Sessions (z.B. Verlobung, Probeessen usw.) kann Ihre Hochzeitsfotografie erschwinglicher machen. - -#### Was, wenn ich mir keinen Fotografen für meine Hochzeit leisten kann? - -Wenn ein professioneller Fotograf außerhalb Ihres Budgets liegt, gibt es dennoch viele erschwingliche Hochzeitsfotografie-Optionen. Ziehen Sie in Betracht, Gästen Digitalkameras zur Verfügung zu stellen, damit sie ihre eigenen Bilder machen, einen QR-Code und ein digitales Hochzeitsgästebuch zu verwenden, eine Fotobox-Station einzurichten oder Ihren eigenen Hashtag zu erstellen. Abhängig von der Option, die Sie wählen, kann der Preisbereich von kostenlos bis zu ein paar hundert Euro variieren. - -#### Kann ich mit kleinem Budget dennoch Qualitätshochzeitsfotos haben? - -Ja! Sie müssen kein großes Budget haben, um schöne, hochwertige Hochzeitsfotos zu bekommen. Ziehen Sie in Betracht, einen Freund oder Familienmitglied zum Fotografieren zu verwenden oder einen Fotografie-Studenten zu engagieren. Solange sie Erfahrung haben und eine Qualitätskamera verwenden, können Sie dennoch atemberaubende Fotos haben. - ---- - -## Artikel 2: 6 Top-Alternativen zum Hochzeitsfotografen - -**Veröffentlicht:** 6. September 2025 - -Seien wir ehrlich, Hochzeiten sind teuer. Von der Location über das Essen bis hin zu allem dazwischen kann es eine Herausforderung sein, die Kosten innerhalb Ihres Budgets zu halten. Ein Bereich, der extrem kostspielig sein kann, ist der Hochzeitsfotograf. Tatsächlich ist es üblich, zwischen 2.500 und 6.500 Euro für dieses eine Element allein auszugeben. Das ist ein Schlag für Ihr Portemonnaie. Wenn Sie Kosten niedrig halten wollen, ohne auf die unglaublichen Erinnerungen an Hochzeitsfotos zu verzichten, gibt es Alternativen zum Hochzeitsfotografen. Sie können dennoch schöne Bilder einfangen und haben Geld übrig für die Flitterwochen! - -### Warum eine Alternative zum Hochzeitsfotografen wählen - -Es gibt viele Gründe, warum ein Paar sich dafür entscheiden könnte, auf einen traditionellen Hochzeitsfotografen für eine Alternative zu verzichten. Sie beinhalten: -- Kosten niedrig halten -- Ein einzigartiges Element zur Hochzeit hinzufügen -- Personalisierung -- Reduzierter Stress - -Egal aus welchem Grund, wenn Sie darüber nachdenken, den traditionellen Fotografen zu streichen, sollten Sie sich diese einzigartigen alternativen Hochzeitsfotografie-Ideen anschauen. - -### 1. Verwenden Sie Einwegkameras - -Einwegkameras haben ein Comeback erlebt und werden als einzigartige und unterhaltsame Ergänzung zu Hochzeiten betrachtet. Sie lassen Gäste ihre eigenen Fotos machen, so dass Sie den Tag durch ihre Augen sehen können. Nicht nur sind diese Kameras erschwinglich, sondern sie sind auch einfach zu entwickeln, so dass Sie Ihre Bilder schnell haben können, anstatt Wochen oder Monate zu warten. Einige Unternehmen bieten sogar [Einwegkamera-Pakete](https://cheecam.com/) an, die das Einfangen Ihres großen Tages noch einfacher machen. Paare mit großen und kleinen Budgets lieben die Individualität und den Charme, den Einwegkameras ihrer Hochzeit hinzufügen, mit einem extra Hauch von Nostalgie, der den Tag noch spezieller macht. - -### 2. Richten Sie eine Fotobox ein - -Fotoboxen sind zeitlos, interaktiv und einfach nur unterhaltsam. Gäste lieben es, sie zu benutzen, und sie fügen Ihrer Feier ein kreatives Element hinzu. Es gibt eine Menge verschiedener Fotobox-Optionen, die digitale Hintergründe und physische Requisiten integrieren, um das Erlebnis auf die nächste Stufe zu heben. Sie können auch Ihre eigene Kamera auf einem Stativ verwenden und jemanden helfen lassen, Bilder zu machen. Viele Paare verwenden ihre Fotoboxen als speziellen Event-Spot, wo Gäste Zeit haben können, miteinander in Verbindung zu treten und Spaß zu haben. Sie können es extra speziell machen, indem Sie Lichter, Ballons und Blumen hinzufügen, damit es wirklich heraussticht. Sie werden es lieben, durch alle Bilder zu schauen, und Ihre Gäste werden ein einzigartiges Andenken bekommen, das sie zu Hause, im Auto oder bei der Arbeit zeigen können. - -### 3. Verwenden Sie einen professionellen Freund-Fotografen - -Wenn Sie die Idee lieben, einen professionellen Fotografen zu haben, aber jemanden vertrauen möchten, den Sie kennen, ist ein Freund, der sich auf Fotografie spezialisiert, der perfekte Kompromiss. Viele Menschen genießen Fotografie als Hobby, und solange sie eine gute Kamera und genug Erfahrung haben, könnten sie eine großartige Option sein. Sprechen Sie unbedingt vorher mit ihnen und bitten Sie um Beispiele ihrer Arbeit, um sicherzustellen, dass sie die richtige Passform für Ihre Vision sind. Die Nutzung eines Freundes kann Kosten niedrig halten, ohne die Bildqualität zu opfern. - -### 4. Verwenden Sie QR-Codes, um Gastfotos zu sammeln - -Ein digitales Hochzeitsgästebuch kann eine ausgezeichnete Alternative zu Einwegkameras auf Hochzeiten sein. Ihre Gäste scannen einfach einen [QR-Code mit ihrem Telefon und laden ihre eigenen Fotos](https://www.guestlense.com/occasions/weddings), Videos und sogar Audioaufnahmen hoch. Unternehmen wie [Guestlense](https://www.guestlense.com/) ermöglichen unbegrenzte Uploads und einfache Anpassung, so dass Sie Fotos fast sofort anschauen und bearbeiten können. Außerdem sind sie super erschwinglich, was es einfach macht, die Bilder zu bekommen, die Sie wollen, ohne den hohen Preis. - -### 5. Verwenden Sie eine Popcorn-Kamera - -Eine Popcorn-Kamera ist, wenn eine einzelne Kamera an alle Ihre Gäste weitergegeben wird und sie ihre eigenen Bilder vom Event machen können. Es ist eine großartige interaktive Option, die jeden einbezieht und einzigartige Fotos schafft, die die einzigartige Persönlichkeit jedes Gastes widerspiegeln. Es macht nicht nur Spaß für diejenigen, die Ihre Hochzeit besuchen; Sie und Ihr Ehepartner werden eine Blast haben, durch die spontanen Aufnahmen zu gehen und die Nacht aus verschiedenen Perspektiven zu sehen. Für beste Ergebnisse stellen Sie sicher, dass Sie klare Richtlinien geben, was Sie wollen und nicht wollen, dass fotografiert wird, und weisen Sie jemanden zu, der ein Auge auf die Kamera während der Nacht hält. - -### 6. Wählen Sie einen Hybrid-Ansatz - -Sie müssen nicht bei nur einer Option bleiben; Sie können Ihren eigenen personalisierten Hybrid-Ansatz für Ihre Hochzeitsfotografie erstellen. Vielleicht mögen Sie die Idee einer interaktiven Fotobox und eines digitalen Hochzeitsgästebuchs. Sie könnten auch Ihre Gäste bitten, ihre Bilder in eine Foto-Sharing-App hochzuladen und einen Freund zu verwenden, der sich auf Fotografie spezialisiert. Wenn es um die Wahl alternativer Hochzeitsfotografie-Ideen geht, ist es okay, außerhalb der Box zu denken und zu tun, was für Sie am besten funktioniert. Immerhin ist das Ihre Hochzeit und Sie wollen, dass der Tag Sie und Ihren Ehepartner widerspiegelt. - -Die Verwendung eines alternativen Hochzeitsfotografen ist eine unterhaltsame und einfache Möglichkeit, Geld zu sparen, ohne bei Professionalität oder Qualität Kompromisse zu machen. Haben Sie Spaß damit und tun Sie, was Sie glücklich macht. Wenn es um Ihre Hochzeit geht, gibt es keine Regeln, also können Sie nichts falsch machen. Ob Sie sich dafür entscheiden, Ihre Gäste mit Digitalkameras einzubeziehen, eine Fotobox oder digitales Hochzeitsgästebuch zu verwenden, die Fotografie-Fähigkeiten Ihres Freundes zu nutzen oder einen Hybrid-Ansatz zu wählen, diese alternativen Hochzeitsfotografie-Ideen werden Ihnen helfen, die Option zu wählen, die richtig für Ihren besonderen Tag ist. - ---- - -## Artikel 3: Wie man QR-Codes verwendet, um Hochzeitsbilder zu sammeln - -**Veröffentlicht:** 3. September 2025 - -Eine der beliebtesten Möglichkeiten, einzigartige Hochzeitsfotos zu bekommen, ist die Verwendung eines benutzerdefinierten QR-Codes. Immer mehr Paare wenden sich dieser kostengünstigen Option zu, anstatt oder zusätzlich zu einem traditionellen Fotografen. Mit einem QR-Code genießen Ihre Gäste ein interaktives und immersives Erlebnis und verwenden ihre eigenen Telefone, um Ihren besonderen Tag einzufangen. Sie werden es lieben, Bilder aus verschiedenen Perspektiven zu sehen, so dass Sie Blicke auf Ihre Hochzeit aus mehreren Blickwinkeln bekommen können. In diesem Artikel erklären wir, wie Sie einen QR-Code für Hochzeitsbilder erstellen, damit Sie von diesem spannenden Tool profitieren können. - -### Was ist ein QR-Code und wie funktioniert er? - -Ein QR-Code ist ein benutzerdefinierter Barcode, der verwendet wird, um Informationen wie Bilder, Website-URLs und mehr zu speichern. Sie funktionieren wie Instant-Links, die Sie mit einem voreingestellten Ziel verbinden. Um ihn zu verwenden, scannen Sie den QR-Code mit einer Smartphone-Kamera. Jeder Code hat ein einzigartiges quadratisches Muster, das die Kamera als Binärcode identifiziert und analysiert. Dann verwendet es diese Informationen, um einen anklickbaren Link zu erstellen, den Sie einfach drücken und der Sie zu der voreingestellten Seite führt. Sie können Ihren eigenen QR-Code erstellen, der auf das Farbschema Ihrer Hochzeit zugeschnitten ist, ihn ausdrucken und für Ihre Gäste zur Verwendung anzeigen. - -### Wie man einen QR-Code für Hochzeitsbilder erstellt - -Das Erstellen eines [QR-Codes für Ihre Hochzeitsbilder](https://www.guestlense.com/) ist einfach und dauert nur zwei Schritte. Zuerst sollten Sie ein gemeinsames digitales Fotoalbum erstellen, entweder unter Verwendung von Cloud-Speicher oder einem digitalen Hochzeitsgästebuch. Während die Verwendung eines Cloud-Speicher-Albums kostenlos ist, können Foto-Sharing-Services ein rationalisierteres und poliertes Erlebnis bieten. Sie können sie verwenden, um Fotos zu organisieren oder Diashows aus den Bildern zu erstellen. - -Wenn Sie sich für einen Cloud-Service wie iCloud, Dropbox oder Google Photos entscheiden, sollten Sie die Freigabe aktivieren und den Zugriff auf "Jeder mit dem Link kann ansehen" setzen. Dieser Schritt ermöglicht es Gästen, ihre eigenen Bilder hinzuzufügen. Jetzt ist es Zeit, den QR-Code zu generieren. Die Verwendung eines QR-Code-Generators für Hochzeitsbilder ist kostenlos mit einem Service wie Canva, Adobe Express oder QR Code Generator. Kopieren Sie die einzigartige URL von Ihrem Cloud-Speicher-Album und fügen Sie sie in das Textfeld des QR-Code-Generators ein. Sobald fertig, können Sie ihn mit Ihrem eigenen Design und Ihrer Farbe für noch größere Personalisierung anpassen. - -Wenn Sie sich für ein digitales Hochzeitsgästebuch entscheiden, erstellt das Unternehmen den QR-Code für Sie. Ihre Gäste können auch Videos und Audio hochladen und es gibt sogar Live-Diashow-Optionen. Während diese Extras großartig sind, kommen sie mit Kosten, die je nach Unternehmen variieren können. - -### Was ist der beste QR-Code für Hochzeitsbilder? - -Es gibt keinen spezifischen QR-Code, der am besten für Hochzeiten ist; es hängt von den Features ab, die Sie verwenden möchten. Services wie [Guestlense](https://www.guestlense.com/) haben mehrere Stufen basierend auf verschiedenen Optionen. Bei der Wahl einer QR-Code-Plattform sollten Sie berücksichtigen: -- Ihr Budget -- Fotoqualität -- Benutzerfreundlichkeit -- Sicherheit und Datenschutz -- Zusätzliche Features - -Egal welches Budget, jeder kann einen QR-Code für seine Hochzeitsbilder nutzen. - -### Wie Gäste einen QR-Code für Hochzeitsfotos verwenden - -Sobald Sie Ihren einzigartigen QR-Code für Ihre Hochzeitsfotos generiert haben, können Sie ihn ausdrucken und für Ihre Gäste anzeigen. Platzieren Sie ihn an wichtigen Orten wie: -- Zeremonie-Programmen -- Menüs -- Willkommensschildern -- Gästetisch -- Hochzeitsempfang-Tischen - -Mehrere Schilder mit dem QR-Code rund um den Veranstaltungsort zu haben, stellt sicher, dass jeder zugreifen und ihn verwenden kann. Richtungen neben dem Code zu platzieren, ist auch eine gute Idee, besonders für ältere Gäste oder diejenigen, die nicht technisch versiert sind. Fügen Sie eine kurze Notiz mit einer Nachricht wie "Scannen Sie, um Hochzeitsfotos zu teilen und anzuschauen" hinzu oder haben Sie eine dedizierte Person, die jedem hilft, der Hilfe braucht. - -### Wie man Fotos von seinem QR-Code anschaut - -Sobald ein Gast den QR-Code scannt und ein Bild hochlädt, können Sie es in Ihrem digitalen Fotoalbum anschauen. Das Album wird während der Veranstaltung aktualisiert und gibt Ihnen und Ihren Gästen Echtzeit-Zugang, um alle Bilder anzuschauen. Sie können Ihr Album in verschiedene Fotos organisieren, wie die Verlobungsparty, Probeessen, Zeremonie und Empfang. Alle Fotos in den richtigen Alben zu haben, macht es einfacher anzuschauen und macht das Erlebnis angenehmer. - -### Tipps für den Erfolg Ihres QR-Codes - -Das Erstellen eines QR-Codes für Ihre Hochzeit ist eine schnelle und einfache Möglichkeit, Fotos hochzuladen und anzuschauen. Hier sind ein paar Tipps, die Sie beachten sollten, um sicherzustellen, dass es erfolgreich ist. - -#### Halten Sie es einfach - -Die Möglichkeit, Ihren QR-Code anzupassen, ist eine spannende Funktion, aber Sie wollen daran denken, dass es für Ihre Hochzeit ist. Sie wollen, dass er elegant ist und zum Thema und zur Ästhetik passt, damit er zusammenhängend wirkt. - -#### Priorisieren Sie Datenschutz - -Datenschutz ist immer ein wichtiges Anliegen für Online-Fotos und etwas, das Sie ernst nehmen sollten. Wenn Ihnen die Idee nicht gefällt, dass Ihre Bilder öffentlich sind, können Sie den Zugriff einschränken, indem Sie eine passwortgeschützte Landing Page hinzufügen oder die Einstellungen auf privat ändern. Stellen Sie nur sicher, dass Sie den Gästen das Passwort mitteilen oder wie sie auf das Album zugreifen können, damit sie ihre Fotos hochladen und anschauen können. - -#### Wählen Sie eine dedizierte Plattform - -Wenn Sie nicht großartig mit Technologie sind, möchten Sie vielleicht eine dedizierte Plattform wie [Guestlense](https://my.guestlense.com/register) für Ihren QR-Code verwenden. Sie kümmern sich um den gesamten Prozess, damit Sie sich zurücklehnen und Ihren besonderen Tag genießen können. - -Die Verwendung eines QR-Codes für Ihre Hochzeitsbilder ist eine großartige Möglichkeit, Erinnerungen einzufangen und Ihre Gäste in Ihren großen Tag einzubeziehen. Ob Sie ihn selbst generieren oder eine dedizierte Plattform verwenden, es ist eine ausgezeichnete Option für jeden, der seiner Hochzeit eine extra spezielle Note hinzufügen möchte. - ---- - -## Artikel 4: Der ultimative Leitfaden zur Hochzeitsgast-Foto-Teilung (Ohne Kopfschmerzen) - -**Veröffentlicht:** 19. März 2025 - -Ihr Hochzeitstag ist ein schöner Wirbel aus Emotionen, Lachen und kleinen magischen Momenten. Aber hier ist die Sache - Ihr Fotograf kann nicht überall gleichzeitig sein. Deshalb könnten einige der unbezahlbarsten Fotos von Ihrem großen Tag von genau den Menschen kommen, die Sie eingeladen haben, um zu feiern: Ihren Gästen. - -Von albernen Tanzfläche-Selfies bis zu Hinter-den-Kulissen-Schnappschüssen während der Cocktailstunde sind Ihre Freunde und Familie wandernde, sprechende Fotojournalisten. Das einzige Problem? Wie sammelt man all diese fantastischen Gastfotos, ohne danach jeden zu jagen? - -Wenn Sie sich über die besten Möglichkeiten wundern, Gäste ihre Fotos teilen zu lassen, sind Sie hier richtig. Wir führen Sie durch das Warum, das Wie und die Tools, um **Hochzeitsgast-Foto-Sharing** zum Kinderspiel zu machen. Außerdem stellen wir Ihnen **Guestlense** vor, ein brillantes Tool, das alles vereinfacht. - -### Warum Gastfotos wichtig sind (Auch wenn Sie einen Profi-Fotografen haben) - -Einen Hochzeitsfotografen zu engagieren, lohnt sich absolut - sie werden die polierten, rahmenwürdigen Momente einfangen. Aber Ihre Gäste? Sie sind diejenigen, die knipsen werden: -- Ihre Blumenmädchen, die während des Toasts einschlafen -- Ihre College-Freunde, die eine spontane Tanzschlacht starten -- Ihren Vater, der während des ersten Blicks weint -- Den Moment, wo Ihr neuer Ehepartner verstohlen noch ein Stück Kuchen stiehlt - -Diese spontanen Momente sind pures Gold. Aber ohne einen soliden Plan werden sie in Kamerarollen verloren gehen oder in sozialen Feeds begraben. - -Deshalb ist ein rationalisierter Foto-Sharing-Plan ein Muss für moderne Hochzeiten. - -### Die besten Möglichkeiten, Hochzeitsfotos von Gästen zu sammeln - -Lassen Sie uns über Lösungen sprechen. Es gibt mehr Möglichkeiten als je zuvor für Gäste, ihre Fotos zu teilen - und einige sind viel besser als andere. Hier sind die Top-Optionen, von einfachster bis zu aufwendigster. - -#### 1. Verwenden Sie eine Hochzeitsgast-Foto-Sharing-App (Wie Guestlense) - -Wenn Sie die einfachste und gastfreundlichste Option wollen, verwenden Sie ohne Zweifel ein dediziertes Hochzeits-Foto-Sharing-Tool wie [Guestlense](https://www.guestlense.com). - -**Hier ist warum Paare es lieben:** -- Kein App-Download erforderlich - Gäste scannen einfach einen QR-Code und fangen an hochzuladen -- Echtzeit-Uploads - Sie können Fotos sehen, während Ihre Hochzeit sich entfaltet -- Private Galerie - Alle Ihre Fotos leben an einem sicheren, organisierten Ort -- Video-Uploads auch - Weil einige Momente mehr als einen Rahmen brauchen - -Es ist perfekt für Paare, die sich nicht mit komplizierten Apps herumschlagen wollen. Generieren Sie einfach Ihren Event-QR-Code, drucken Sie ihn auf Schilder oder Platzkarten und lassen Sie die Magie geschehen. - -**Pro-Tipp: Zeigen Sie Ihren Guestlense-QR-Code an der Bar, dem Willkommenstisch oder sogar im Badezimmerspiegel für maximale Sichtbarkeit.** - -#### 2. Erstellen Sie ein gemeinsames Album (Google Photos, Dropbox, iCloud) - -Eine andere Möglichkeit, Bilder zu sammeln, ist mit einem gemeinsamen Ordner unter Verwendung eines Services wie: -- [Google Photos](https://photos.google.com/) -- [Dropbox](https://www.dropbox.com/) -- [iCloud Shared Albums](https://support.apple.com/en-us/HT202786) - -Erstellen Sie einfach ein Album vor der Hochzeit, dann teilen Sie den Link danach über Gruppennachricht oder E-Mail. - -**Vorteile:** -- Vertraut für die meisten Gäste -- Kostenlos (oder niedrige Kosten) -- Gut für die Organisation vieler Dateien - -**Nachteile:** -- Nicht sehr interaktiv oder unterhaltsam -- Kann Anmeldung oder App-Download erfordern -- Sie werden Gäste wahrscheinlich mehrmals erinnern müssen - -#### 3. Richten Sie einen Hochzeits-Hashtag ein (Wenn Sie Social Media lieben) - -Das Erstellen eines benutzerdefinierten Hochzeits-Hashtags wie `#JessAndCamSayIDo` kann eine süße Möglichkeit sein, Fotos auf Instagram oder TikTok zu sammeln. Stellen Sie nur sicher, dass Ihr Hashtag einzigartig ist, damit Ihre Bilder nicht mit denen von jemand anderem vermischt werden. - -**Hier ist die Realitätsprüfung:** -- Nicht jeder wird ihn verwenden -- Nicht jeder wird öffentlich posten -- Nicht jeder wird klare oder hochwertige Fotos hochladen - -Es ist eine schöne Ergänzung zu anderen Methoden, aber sollte nicht Ihre einzige Strategie sein. - -#### 4. Verwenden Sie Einweg- oder Sofortbildkameras (Wenn Sie Nostalgie lieben) - -Einwegkameras auf jedem Tisch bringen eine unterhaltsame, vintage Stimmung. Gäste genießen sie und es ermutigt Leute, Fotos zu knipsen, die normalerweise nicht ihr Telefon herausziehen würden. - -**Nachteile:** -- Drucken und Entwickeln kann teuer werden -- Die Qualität kann Glückssache sein -- Sie werden nicht wissen, was eingefangen wurde, bis nach der Hochzeit - -Für beste Ergebnisse kombinieren Sie ein paar Sofortbildkameras mit einer digitalen Option wie Guestlense. - -### Wie Sie Gäste dazu bringen, tatsächlich Fotos hochzuladen - -Sie haben das Tool - jetzt müssen Gäste es verwenden. Hier ist wie Sie sicherstellen, dass die Uploads reibungslos hereinrollen. - -#### 1. Zeigen Sie klare Beschilderung - -Entwerfen Sie ein Schild, das etwas sagt wie: **"Helfen Sie uns, den Tag wiederzuerleben - laden Sie Ihre Fotos hoch! Scannen Sie den QR-Code oder besuchen Sie [Ihren Gästebuch-Link]."** - -Platzieren Sie Schilder am Eingang, Gästetischen, in der Nähe der Tanzfläche und überall wo Gäste abhängen könnten. - -#### 2. Machen Sie eine schnelle Ansage - -Lassen Sie Ihren DJ oder MC während des Essens oder kurz vor dem Tanzen jeden erinnern. Ein einfacher Hinweis wirkt Wunder bei der Ermutigung zur Teilnahme. - -#### 3. Fügen Sie es zu Ihrer Hochzeitswebsite und Einladungen hinzu - -Fügen Sie Ihren Foto-Sharing-Link auf Ihrer Hochzeitswebsite hinzu und ziehen Sie eine kleine Einlage in Ihrer Einladungssuite in Betracht. Je mehr Exposition Gäste vorher haben, desto wahrscheinlicher werden sie sich erinnern. - -#### 4. Senden Sie eine Nachhochzeit-Erinnerung - -Ein paar Tage nach dem großen Tag senden Sie eine Dankes-Nachricht oder E-Mail, die eine sanfte Erinnerung und Ihren Upload-Link enthält. Einige der besten Fotos sitzen vielleicht immer noch im Telefon Ihres Cousins und warten darauf, geteilt zu werden. - -### Hochzeitsgast-Foto-Sharing FAQ - -**Wie sammle ich Hochzeitsfotos von Gästen ohne Social Media zu verwenden?** -Verwenden Sie ein Tool wie [Guestlense](https://www.guestlense.com), das Gästen ermöglicht, Fotos und Videos direkt in eine private Galerie hochzuladen. Keine Accounts, Apps oder Hashtags erforderlich. - -**Kann ich Google Photos verwenden, um Gastfotos zu sammeln?** -Ja, aber es ist möglicherweise nicht die intuitivste Option für jeden Gast. Wenn Sie diese Route gehen, halten Sie Anweisungen einfach und geben Sie klare Links. - -**Was ist die einfachste Möglichkeit, Gäste Hochzeitsfotos hochladen zu lassen?** -Die Verwendung einer Plattform wie Guestlense ist bei weitem die einfachste. Sie funktioniert mit einem einfachen Link oder QR-Code, erfordert keine App und hält alle Ihre Erinnerungen an einem Ort. - -### Abschließende Gedanken: Verpassen Sie nicht die Magie, die Ihre Gäste einfangen - -Wenn alles gesagt und getan ist, geht es beim **Hochzeitsgast-Foto-Sharing** nicht nur darum, mehr Bilder zu sammeln. Es geht darum, Ihren Tag aus den Augen der Menschen zu sehen, die Sie lieben. Jedes verschwommene Tanzfläche-Bild, jede tränenreiche Umarmung, jeder gestohlene Kuss - sie sind alle Teil der Geschichte. - -Machen Sie es Gästen leicht, beizutragen. Ob Sie mit einer dedizierten Plattform wie **Guestlense**, einem gemeinsamen Ordner oder einer Mischung von Methoden gehen, der Schlüssel ist Einfachheit. Geben Sie Gästen klare Anweisungen und scheuen Sie sich nicht, nachzufassen. - -Denn Jahre später, wenn der Kuchen weg ist und das Kleid weggepackt ist, werden Sie so froh sein, dass Sie diese kleinen Momente eingefangen haben, die Ihren Tag zu **Ihrem** gemacht haben. - ---- - -## Artikel 5: Wann Hochzeitseinladungen verschickt werden sollten: Der ultimative Leitfaden für perfektes Timing - -**Veröffentlicht:** 15. Dezember 2024 - -Timing ist alles bei der Hochzeitsplanung und das Versenden Ihrer Hochzeitseinladungen ist keine Ausnahme. Sie zu früh zu versenden, könnte dazu führen, dass Gäste vergessen, während zu spät zu versenden zu Terminüberschneidungen führen könnte. Dieser Leitfaden hilft Ihnen, das perfekte Timing für das Versenden Ihrer Hochzeitseinladungen zu finden und sicherzustellen, dass Ihre Gäste genug Zeit haben, RSVP zu geben und für Ihren großen Tag zu planen. - -### 1. Die allgemeine Regel: 6 bis 8 Wochen vor der Hochzeit - -Für die meisten Hochzeiten ist der Sweet Spot für das Versenden von Einladungen **6 bis 8 Wochen vor dem Hochzeitsdatum**. Dieser Zeitrahmen gibt Gästen genug Zeit, RSVP zu geben, Reisen zu arrangieren und ihre Kalender zu blockieren, ohne sich gehetzt zu fühlen oder die Veranstaltung zu vergessen. - -Allerdings kann diese allgemeine Regel je nach Art der Hochzeit und den Umständen Ihrer Gäste variieren. - -### 2. Wann Save-the-Dates verschickt werden sollten - -Save-the-Dates sind Ihre erste offizielle Kommunikation mit Gästen und dienen als Vorwarnung über Ihr Hochzeitsdatum und den Ort. Diese sollten versendet werden: -- **8 bis 12 Monate im Voraus** für Zielhochzeiten oder Spitzenreisezeiten. -- **6 bis 8 Monate im Voraus** für lokale Hochzeiten oder kleinere Zusammenkünfte. - -Save-the-Dates erfordern keine detaillierten Informationen, aber sie sollten das Datum, den Ort und eine Notiz enthalten, dass formelle Einladungen folgen werden. - -### 3. Faktoren, die beeinflussen, wann Hochzeitseinladungen verschickt werden sollten - -#### Zielhochzeiten - -Wenn Ihre Hochzeit erfordert, dass Gäste lange Strecken reisen oder Unterkünfte buchen, müssen Sie ihnen extra Vorlaufzeit geben. -- **Einladungen versenden:** 3 bis 4 Monate im Voraus. -- **Save-the-Dates:** 12 Monate im Voraus. - -#### Ferien- oder Spitzenzeiten-Hochzeiten - -Hochzeiten während Feiertagen oder beschäftigten Zeiten wie Sommer erfordern frühere Kommunikation. -- **Einladungen versenden:** 8 bis 10 Wochen im Voraus. -- **Save-the-Dates:** 9 bis 12 Monate im Voraus. - -#### Kleinere, lokale Hochzeiten - -Wenn die meisten Ihrer Gäste lokal sind und die Veranstaltung intimer ist, können Sie beim Standard-Zeitplan bleiben: -- **Einladungen versenden:** 6 bis 8 Wochen im Voraus. -- **Save-the-Dates:** Optional aber geschätzt, besonders für auswärtige Gäste. - -### 4. RSVP-Fristen: Das richtige Datum setzen - -Ihre RSVP-Frist sollte ungefähr **2 bis 4 Wochen vor der Hochzeit** sein. Das gibt Ihnen Zeit, um: -- Ihre Kopfzahl für Catering und Sitzordnung zu finalisieren. -- Bei Gästen nachzufassen, die nicht geantwortet haben. - -Fügen Sie die RSVP-Frist auf Ihrer Einladung hinzu und machen Sie es Gästen leicht zu antworten, indem Sie einen frankierten Rückumschlag, eine Online-RSVP-Option oder beides bereitstellen. - -### 5. Einladungen für andere Hochzeitsveranstaltungen versenden - -Zusätzlich zu Ihren Hochzeitseinladungen müssen Sie möglicherweise Einladungen für andere Veranstaltungen versenden, wie Probeessen oder Nachhochzeitsbrunches. Hier ist wann Sie sie versenden sollten: -- **Probeessen-Einladungen:** 4 bis 6 Wochen vor der Hochzeit. -- **Nachhochzeitsbrunch-Einladungen:** Fügen Sie diese mit der Hochzeitseinladung hinzu oder versenden Sie sie 4 Wochen im Voraus. - -### 6. Wie Sie zeitgerechte Lieferung sicherstellen - -#### Bereiten Sie früh vor - -Beginnen Sie früh mit dem Sammeln von Adressen und dem Entwerfen Ihrer Einladungen. Streben Sie an, Ihre Einladungen gedruckt und versandbereit zu haben, mindestens **3 bis 4 Wochen vor Ihrem Versanddatum**. - -#### Wählen Sie zuverlässige Versandmethoden - -Verwenden Sie einen seriösen Postservice und ziehen Sie Hand-Stornierung Ihrer Einladungen am Postamt in Betracht, um Schäden zu vermeiden. Für internationale Gäste versenden Sie Einladungen mindestens **12 Wochen im Voraus**, um Versandverzögerungen zu berücksichtigen. - -#### Überprüfen Sie Adressen doppelt - -Vermeiden Sie zurückgesendete Einladungen, indem Sie Adressen im Voraus überprüfen. Tools wie Google Sheets oder Hochzeitsplanungs-Apps können Ihnen helfen, Gästeinformationen im Auge zu behalten. - -### 7. Was in Ihrer Hochzeitseinladungssuite enthalten sein sollte - -Ihre Einladungssuite sollte alle wesentlichen Details enthalten, die Gäste brauchen, um für Ihre Hochzeit zu planen: -- **Haupteinladung:** Enthält Ihre Namen, Hochzeitsdatum, Uhrzeit und Veranstaltungsort. -- **Details-Karte:** Deckt zusätzliche Informationen wie Dresscode, Unterkünfte und Transport ab. -- **RSVP-Karte:** Ermöglicht Gästen, ihre Teilnahme zu bestätigen und Essenspräferenzen anzugeben, falls zutreffend. -- **Hochzeitswebsite:** Wenn Sie eine Hochzeitswebsite haben, fügen Sie die URL für mehr Details hinzu. - -### 8. Tipps für digitale Einladungen - -Wenn Sie sich für digitale Einladungen entscheiden, gelten die gleichen Timing-Regeln. E-Mail-Einladungen sind ideal für lässige oder umweltfreundliche Hochzeiten und können den RSVP-Prozess rationalisieren. - -### 9. Vermeiden Sie diese häufigen Fehler - -#### Einladungen zu spät versenden - -Bis zur letzten Minute zu warten, kann Gäste in Terminschwierigkeiten bringen. - -#### Reisezeit nicht berücksichtigen - -Wenn Sie internationale oder auswärtige Gäste haben, berücksichtigen Sie längere Versand- und Reisezeiten. - -#### RSVP-Fristen übersehen - -Das Setzen oder Durchsetzen einer RSVP-Frist zu versäumen, kann zu unnötigem Stress führen, wenn Sie Ihre Gästeliste finalisieren. - -### 10. Zusammenfassungs-Zeitplan für das Versenden von Einladungen - -Hier ist eine schnelle Referenz für wann Sie hochzeitsbezogene Kommunikation versenden sollten: -- **Save-the-Dates:** 6 bis 12 Monate vor der Hochzeit. -- **Hochzeitseinladungen:** 6 bis 8 Wochen vor der Hochzeit (3 bis 4 Monate für Zielhochzeiten). -- **RSVP-Frist:** 2 bis 4 Wochen vor der Hochzeit. -- **Andere Veranstaltungseinladungen:** 4 bis 6 Wochen vor der Veranstaltung. - -### Abschließende Gedanken - -Das Versenden von Hochzeitseinladungen zur richtigen Zeit ist der Schlüssel, um einen reibungslosen Planungsprozess und eine gut besuchte Feier sicherzustellen. Indem Sie diese Richtlinien befolgen, geben Sie Ihren Gästen ausreichend Zeit zur Vorbereitung, während Sie Ihren Hochzeitszeitplan auf Kurs halten. - -Die Planung einer Hochzeit handelt alles von Balance - Timing, Organisation und Kommunikation. Mit ein wenig Vorbereitung werden Ihre Einladungen den Ton für einen Tag setzen, den Ihre Gäste nie vergessen werden! - ---- - -## Artikel 6: Wie man Hochzeitsplaner wird: Eine Schritt-für-Schritt-Anleitung - -**Veröffentlicht:** 15. Dezember 2024 - -Hochzeitsplaner zu werden, ist eine lohnende Berufswahl für diejenigen mit einer Leidenschaft für Kreativität, Organisation und die Liebe, Lebensmomente zu feiern. Wenn Sie jemals davon geträumt haben, unvergessliche Hochzeiten zu orchestrieren, hier ist ein umfassender Leitfaden, der Ihnen hilft, in die Branche einzusteigen. - -### 1. Die Rolle eines Hochzeitsplaners verstehen - -Bevor Sie eintauchen, ist es wichtig zu wissen, was es bedeutet, Hochzeitsplaner zu sein. Hochzeitsplaner sind verantwortlich für: -- Koordination aller Aspekte einer Hochzeit, von der Veranstaltungsort-Auswahl bis zum Vendor-Management. -- Verwaltung von Budgets und Zeitplänen. -- Problemlösung unter Druck. -- Kommunikation mit Kunden, um ihre Vision zum Leben zu erwecken. - -Diese Rolle erfordert eine Mischung aus Kreativität, logistischen Fähigkeiten und emotionaler Intelligenz, um mit hochriskanten, emotional aufgeladenen Veranstaltungen umzugehen. - -### 2. Ihre Fähigkeiten und Leidenschaft bewerten - -Erfolgreiche Hochzeitsplaner besitzen eine einzigartige Kombination von Eigenschaften, einschließlich: -- **Organisation:** Gleichzeitiges Management mehrerer Details. -- **Kreativität:** Gestaltung schöner und personalisierter Hochzeiten. -- **Zwischenmenschliche Fähigkeiten:** Vertrauen zu Kunden und Vendoren aufbauen. -- **Problemlösung:** Unerwartete Herausforderungen anmutig handhaben. - -Wenn diese Eigenschaften bei Ihnen anklingen, sind Sie auf einem großartigen Weg. - -### 3. Relevante Erfahrung sammeln - -Erfahrung ist der Schlüssel in der Hochzeitsplanungsbranche. Hier ist wie Sie sie aufbauen: -- **Beginnen Sie klein:** Planen Sie Veranstaltungen für Freunde oder Familie. Das ermöglicht es Ihnen, ein Portfolio aufzubauen und praktische Erfahrung zu sammeln. -- **Freiwilligenarbeit:** Bieten Sie an, etablierten Hochzeitsplanern zu assistieren. Das gibt Ihnen Einblick in die Branche und hilft Ihnen, von erfahrenen Profis zu lernen. -- **Arbeit in verwandten Bereichen:** Rollen in der Veranstaltungsplanung, Catering oder Gastfreundschaft können wertvolle Einblicke in die Hochzeitsbranche geben. - -### 4. Sich selbst weiterbilden - -Während formale Bildung nicht erforderlich ist, um Hochzeitsplaner zu werden, kann sie Ihnen einen Wettbewerbsvorteil geben. Ziehen Sie in Betracht: -- **Zertifizierungen:** Programme wie die vom *Wedding Planning Institute* oder *The Bridal Society* können Ihnen branchenspezifische Fähigkeiten beibringen. -- **Workshops und Seminare:** Diese decken oft Themen wie Vertragsverhandlung, Budgetierung und Design-Trends ab. -- **Geschäftsfähigkeiten:** Kurse in Marketing, Buchhaltung oder Unternehmertum können Ihnen helfen, Ihr eigenes Hochzeitsplanungsgeschäft zu führen. - -### 5. Ein Portfolio aufbauen - -Ein starkes Portfolio zeigt Ihre Kreativität und Organisationsfähigkeiten. Fügen Sie hinzu: -- Fotos von Veranstaltungen, die Sie geplant haben. -- Kunden-Testimonials. -- Mood Boards oder Design-Konzepte. - -Auch wenn Sie gerade erst anfangen, können Mock-Hochzeiten oder gestylte Shootings helfen, Ihre Vision und Fähigkeiten zu demonstrieren. - -### 6. Ihre Marke etablieren - -Ihre Marke ist, wie potenzielle Kunden Sie wahrnehmen. Fokussieren Sie sich auf: -- **Erstellung eines Geschäftsnamens und Logos:** Wählen Sie etwas einprägsames und professionelles. -- **Aufbau einer Website:** Zeigen Sie Ihr Portfolio, Services und Kontaktinformationen. -- **Sozialmedien-Präsenz:** Plattformen wie Instagram und Pinterest sind wichtig, um Bräute und Bräutigame zu erreichen. - -### 7. Mit Vendoren vernetzen - -Hochzeitsplaner arbeiten eng mit Vendoren zusammen, einschließlich Floristen, Fotografen, Caterern und DJs. Bauen Sie starke Beziehungen auf, indem Sie: -- Branchenveranstaltungen und Braut-Messen besuchen. -- Lokale Vendoren kontaktieren, um sich vorzustellen. -- Bei gestylten Shootings zusammenarbeiten, um gegenseitige Exposition zu schaffen. - -### 8. Klein anfangen und skalieren - -Am Anfang arbeiten Sie möglicherweise an kleineren Hochzeiten oder bieten ermäßigte Preise, um Ihre Kundenbasis aufzubauen. Mit der Zeit: -- Erhöhen Sie Ihre Preise, während Sie Erfahrung sammeln. -- Spezialisieren Sie sich auf bestimmte Arten von Hochzeiten (z.B. Ziel-, Luxus- oder Öko-Hochzeiten). -- Stellen Sie ein Team oder Assistenten-Planer ein, um Ihre Kapazität zu erweitern. - -### 9. Mit Trends auf dem Laufenden bleiben - -Die Hochzeitsbranche entwickelt sich schnell. Bleiben Sie informiert, indem Sie: -- Hochzeitsblogs, Magazine und Influencer folgen. -- Konferenzen wie *Wedding MBA* besuchen. -- Populäre Plattformen wie Pinterest für aufkommende Trends im Auge behalten. - -### 10. Außergewöhnlichen Service liefern - -Mundpropaganda-Referenzen sind unschätzbar im Hochzeitsplanungsgeschäft. Um glühende Empfehlungen sicherzustellen: -- Kommunizieren Sie klar und regelmäßig mit Ihren Kunden. -- Seien Sie proaktiv beim Lösen von Problemen. -- Gehen Sie über das Übliche hinaus, um jede Hochzeit unvergesslich zu machen. - -### 11. Herausforderungen annehmen und Erfolge feiern - -Hochzeitsplanung kann stressig sein, aber sie ist auch unglaublich erfüllend. Sie werden Herausforderungen wie Last-Minute-Änderungen oder schwierige Kunden gegenüberstehen, aber die Freude, die Traumhochzeit eines Paares zum Leben zu erwecken, macht alles wett. - -### Abschließende Gedanken - -Hochzeitsplaner zu werden, erfordert Hingabe, Kreativität und eine Liebe für das Schaffen unvergesslicher Momente. Indem Sie diese Schritte befolgen und sich Ihrer Kunst verschreiben, können Sie eine blühende Karriere in der Hochzeitsplanungsbranche aufbauen. Ob Sie große Feiern oder intime Zusammenkünfte orchestrieren, Sie werden eine zentrale Rolle an einem der wichtigsten Tage im Leben eines Paares spielen. - -Fangen Sie klein an, träumen Sie groß und hinterlassen Sie Ihre Spuren in der Welt der Hochzeiten! - ---- - -## Artikel 7: Hochzeitsbilder QR-Code: Die moderne Art, Erinnerungen zu sammeln und zu teilen - -**Veröffentlicht:** 13. Dezember 2024 - -Die Tage der Einwegkameras auf jedem Tisch oder wochenlanges Warten, um Fotos von Ihren Hochzeitsgästen zu sammeln, sind vorbei. Mit der sich entwickelnden Technologie ist der bescheidene [QR-Code](https://digital.gov/resources/introduction-to-qr-codes/) als Game-Changer für das Hochzeitsfoto-Sharing aufgetaucht. Das Integrieren eines QR-Codes in Ihren Hochzeitstag macht es mühelos für Gäste, Bilder in Echtzeit hochzuladen und darauf zuzugreifen, und schafft ein nahtloses und interaktives Erlebnis. Hier ist wie Sie einen Hochzeitsbilder-QR-Code verwenden können, um Ihren großen Tag zu verbessern. - -### Was ist ein Hochzeitsbilder-QR-Code? - -Ein [Hochzeitsbilder-QR-Code](https://www.guestlense.com/occasions/weddings) ist ein scannbarer Code, der Ihre Gäste zu einer digitalen Galerie oder Upload-Plattform führt. Ob es für das Hochladen der Fotos ist, die sie während der Veranstaltung machen, oder für den Zugriff auf das offizielle Hochzeitsalbum, QR-Codes vereinfachen den Prozess und stellen sicher, dass kein Moment verpasst wird. Gäste können ihr Smartphone verwenden, um den Code zu scannen, und werden sofort mit Ihrer gewählten Plattform verbunden. - -### Vorteile der Verwendung eines QR-Codes für Hochzeitsbilder - -Ein QR-Code vereinfacht den Prozess des Sammelns und Teilens von Fotos und macht ihn für alle Ihre Gäste zugänglich. - -#### Einfache Fotosammlung - -QR-Codes eliminieren die Notwendigkeit für Gäste, ihre Fotos per E-Mail zu senden oder zu simsen. Mit einem schnellen Scan können sie Bilder direkt in ein gemeinsames Album oder Cloud-Speicher hochladen. Diese Bequemlichkeit fördert mehr Teilnahme und stellt sicher, dass Sie eine große Vielfalt an spontanen Aufnahmen bekommen. - -#### Echtzeit-Sharing - -Wollen Sie die Magie Ihres Hochzeitstages sofort wiedererleben? QR-Codes ermöglichen es Gästen, Fotos hochzuladen, während sich die Veranstaltung entfaltet. Diese Funktion ermöglicht es Ihnen und Ihren Liebsten, Schnappschüsse der Feier zu genießen, ohne auf die Bearbeitungen des Fotografen warten zu müssen. - -#### Kosteneffektiv - -Anstatt in mehrere Fotografen oder das Mieten von Fotoboxen zu investieren, lässt ein QR-Code Sie Fotos aus jeder Ecke des Veranstaltungsortes crowdsourcen. Sie werden verschiedene Perspektiven einfangen, ohne Ihr Hochzeitsbudget zu belasten. - -#### Umweltfreundlich - -Indem Sie digital gehen, können Sie physische Anweisungen drucken oder USB-Sticks verteilen überspringen. Ein einfacher QR-Code auf einem Schild, Programm oder Tischkarte reduziert Papierabfall und verbessert gleichzeitig das Gäste-Engagement. - -### Wie man einen Hochzeitsbilder-QR-Code erstellt - -Sie nehmen jetzt die ersten Schritte zur Sammlung von Gastfotos. Glückwunsch! Hier ist wie Sie starten: - -#### 1. Wählen Sie eine Foto-Sharing-Plattform - -Wählen Sie eine Plattform, wo Gäste Fotos hochladen und anschauen können. Beliebte Optionen sind Google Photos, Dropbox oder spezialisierte Hochzeits-Apps wie [Guestlense](http://www.guestlense.com), [Honcho](https://thehoncho.app/) oder [Guestpix](https://guestpix.com). Guestlense ist speziell für Hochzeiten entwickelt und bietet eine benutzerfreundliche Schnittstelle, wo Gäste mühelos Fotos und Videos hochladen können. Es ermöglicht Ihnen auch, eine schöne Galerie zu kuratieren, die Sie nach dem großen Tag mit Ihren Liebsten teilen können. - -#### 2. Generieren Sie Ihren QR-Code - -Verwenden Sie einen kostenlosen Online-QR-Code-Generator, um einen benutzerdefinierten Code zu erstellen, der zu Ihrer Foto-Sharing-Plattform linkt. Viele Tools ermöglichen es Ihnen, Personalisierung hinzuzufügen, wie Ihre Hochzeitsfarben oder Namen. - -#### 3. Testen Sie den QR-Code - -Vor dem großen Tag testen Sie den QR-Code, um sicherzustellen, dass er korrekt funktioniert. Bitten Sie ein paar Freunde oder Familienmitglieder, ihn zu scannen und Fotos hochzuladen. - -#### 4. Zeigen Sie den QR-Code an - -Drucken Sie den QR-Code auf Ihre Hochzeitseinladungen, Programme oder Beschilderung am Veranstaltungsort. Für eine kreativere Note ziehen Sie in Betracht, ihn in Ihre Tischdekoration oder Gefälligkeiten zu integrieren. - -### Kreative Ideen für die Verwendung von QR-Codes auf Ihrer Hochzeit - -Machen Sie das Beste aus Ihrem Gästebuch mit unterhaltsamen Möglichkeiten, wie Ihre Gäste interagieren und Erinnerungen teilen können. - -#### Interaktive Fotostationen - -Richten Sie eine Fotostation mit einem QR-Code ein, der Gäste zu einer Galerie der Highlights des Tages führt. Fügen Sie Requisiten und Schilder hinzu, die sie ermutigen, zu knipsen und zu teilen. - -#### Gästebuch-Integration - -Kombinieren Sie Ihre digitale Galerie mit einem virtuellen Gästebuch. Gäste können den QR-Code scannen, um herzliche Nachrichten neben ihren Fotos zu hinterlassen. Guestlense bietet eine integrierte Funktion, wo Gäste personalisierte Notizen zu ihren Uploads hinzufügen können, was Ihre Galerie noch bedeutungsvoller macht. - -#### Dankeskarten - -Nach der Hochzeit fügen Sie einen QR-Code auf Ihre Dankeskarten hinzu. Dieser Code kann Gäste zu einem kuratierten Album professioneller Fotos und spontaner Momente vom Tag führen. Mit Guestlense können Sie eine polierte und leicht zugängliche Galerie erstellen, die Sie mit allen teilen können, die mit Ihnen gefeiert haben. - -### Tipps für Erfolg - -Genau wie es wichtig ist, die beste Plattform zu wählen, müssen Sie Wege finden, Ihre Gäste einzubeziehen. - -#### Machen Sie ihn sichtbar - -Platzieren Sie QR-Codes in stark frequentierten Bereichen wie dem Empfangseingang, Esstischen oder der Bar. - -#### Geben Sie Anweisungen - -Fügen Sie eine kurze Erklärung wie "Scannen Sie, um Ihre Fotos zu teilen!" hinzu, um weniger technisch versierte Gäste zu leiten. - -#### Sichern Sie Ihre Galerie - -Verwenden Sie Passwortschutz oder beschränken Sie den Zugriff, um Datenschutz sicherzustellen. - -### Warum Guestlense für Ihre Hochzeit wählen? - -[Guestlense](http://www.guestlense.com) nimmt den Ärger aus dem Sammeln und Teilen von Hochzeitsfotos. Im Gegensatz zu generischen Plattformen ist Guestlense auf Hochzeiten zugeschnitten und bietet: -- **Echtzeit-Uploads:** Sehen Sie Ihre Hochzeit zum Leben erwachen, während Gäste Fotos und Videos sofort hochladen. -- **Schöne Galerien:** Erstellen Sie eine atemberaubende, anpassbare Galerie, um Ihre Erinnerungen zu präsentieren. -- **Einfaches Teilen:** Stellen Sie einen einzelnen QR-Code bereit, der für Gäste jeden Alters intuitiv zu verwenden ist. -- **Verbesserter Datenschutz:** Halten Sie Ihre besonderen Momente sicher mit passwortgeschütztem Zugriff. - -### Fazit - -Die Verwendung eines Hochzeitsbilder-QR-Codes ist eine einfache, aber innovative Möglichkeit, die Freude und Aufregung Ihres großen Tages einzufangen. Plattformen wie Guestlense machen den Prozess noch nahtloser und stellen sicher, dass Sie eine dynamische und schöne Sammlung von Erinnerungen haben werden. Mit Guestlense können Sie Gäste aktiv daran beteiligen, die besonderen Momente Ihrer Hochzeit zu bewahren, während Sie ein stressfreies Erlebnis genießen. - ---- - -*Quelle: https://www.guestlense.com/articles - Erfasst am 10. Oktober 2025* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 013639e..cbc9d7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,8 @@ "dependencies": { "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.1.0", + "@modelcontextprotocol/server-puppeteer": "^2025.5.12", + "@modelcontextprotocol/server-sequential-thinking": "^2025.7.1", "@paypal/react-paypal-js": "^8.9.2", "@playwright/mcp": "^0.0.37", "@radix-ui/react-accordion": "^1.2.12", @@ -32,6 +34,7 @@ "@tanstack/react-query": "^5.90.2", "@types/react": "^19.0.3", "@types/react-dom": "^19.0.2", + "@upstash/context7-mcp": "^1.0.21", "@vitejs/plugin-react": "^4.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -1833,7 +1836,6 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.19.1.tgz", "integrity": "sha512-3Y2h3MZKjec1eAqSTBclATlX+AbC6n1LgfVzRMJLt3v6w0RCYgwLrjbxPDbhsYHt6Wdqc/aCceNJYgj448ELQQ==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -1853,6 +1855,68 @@ "node": ">=18" } }, + "node_modules/@modelcontextprotocol/server-puppeteer": { + "version": "2025.5.12", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server-puppeteer/-/server-puppeteer-2025.5.12.tgz", + "integrity": "sha512-uG5QIxnnAkL6m7snXcsgC3k8ZnkYlikYZN016re6TEwtXwKUoJjRMzsqbwqw45IqP+7pXvN9vuPJ/OuHu70jow==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "1.0.1", + "puppeteer": "^23.4.0" + }, + "bin": { + "mcp-server-puppeteer": "dist/index.js" + } + }, + "node_modules/@modelcontextprotocol/server-puppeteer/node_modules/@modelcontextprotocol/sdk": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.1.tgz", + "integrity": "sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@modelcontextprotocol/server-sequential-thinking": { + "version": "2025.7.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server-sequential-thinking/-/server-sequential-thinking-2025.7.1.tgz", + "integrity": "sha512-gEMck99hpP+us6CTGACerxOlCsVL+e53kBev5E64m4yaQhnjIj/+vTttapc7Xc1TsvPnzSmtCwKYvcFPZ0tg/w==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "0.5.0", + "chalk": "^5.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "mcp-server-sequential-thinking": "dist/index.js" + } + }, + "node_modules/@modelcontextprotocol/server-sequential-thinking/node_modules/@modelcontextprotocol/sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", + "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@modelcontextprotocol/server-sequential-thinking/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.39.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.7.tgz", @@ -2058,6 +2122,40 @@ "node": ">=18" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3949,6 +4047,12 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -4096,6 +4200,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", @@ -4367,6 +4481,30 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@upstash/context7-mcp": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-1.0.21.tgz", + "integrity": "sha512-kO9kGxt/ZgbSi7rarysNasc92l1b6RoaHUSDOJ8/SzDkLvE9VL0K2F8lC4w6eFDc+B4rfW2uSPeBCNC65nYnxw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.5", + "commander": "^14.0.0", + "undici": "^6.6.3", + "zod": "^3.24.2" + }, + "bin": { + "context7-mcp": "dist/index.js" + } + }, + "node_modules/@upstash/context7-mcp/node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -4477,7 +4615,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dev": true, "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -4491,7 +4628,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -4501,7 +4637,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -4629,7 +4764,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -4639,7 +4773,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -4721,7 +4854,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -4954,7 +5086,6 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "dev": true, "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -4976,14 +5107,89 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", - "dev": true, "license": "Apache-2.0" }, + "node_modules/bare-fs": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.11.tgz", + "integrity": "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.0.tgz", + "integrity": "sha512-c+RCqMSZbkz97Mw1LWR0gcOqwK82oyYKfLoHJ8k13ybi1+I80ffdDzUy0TdAburdrR/kI0/VuN8YgEnJqX+Nyw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5009,6 +5215,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -5025,7 +5240,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -5124,11 +5338,19 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5196,7 +5418,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5286,6 +5507,28 @@ "node": ">=18" } }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -5525,7 +5768,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -5538,7 +5780,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5563,7 +5804,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.6.0" @@ -5580,7 +5820,6 @@ "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -5594,7 +5833,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.1", @@ -5630,7 +5868,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5902,6 +6139,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/degenerator/node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5915,7 +6178,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -5946,6 +6208,12 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "license": "BSD-3-Clause" + }, "node_modules/diff": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", @@ -6026,7 +6294,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -6083,12 +6350,20 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -6126,7 +6401,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6143,7 +6417,6 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -6383,7 +6656,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -6399,6 +6671,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.37.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", @@ -6574,7 +6867,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -6628,7 +6920,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -6648,7 +6939,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -6658,7 +6948,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6668,7 +6957,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bare-events": "^2.7.0" @@ -6678,7 +6966,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -6691,7 +6978,6 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -6738,7 +7024,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "dev": true, "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -6781,7 +7066,6 @@ "version": "7.5.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 16" @@ -6797,7 +7081,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6807,7 +7090,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6817,7 +7099,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -6826,18 +7107,51 @@ "node": ">= 0.6" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -6874,7 +7188,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -6894,6 +7207,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -6981,7 +7303,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -7102,7 +7423,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7112,7 +7432,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7350,6 +7669,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7652,7 +7994,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -7669,7 +8010,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7679,7 +8019,6 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -7693,7 +8032,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -7800,7 +8138,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7813,7 +8150,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -7844,7 +8180,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -7881,7 +8216,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -7899,11 +8233,19 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -7931,7 +8273,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -8252,7 +8593,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -8460,7 +8800,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isobject": { @@ -8510,7 +8849,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8666,14 +9004,12 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -9049,7 +9385,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -9201,7 +9536,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9211,7 +9545,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -9362,6 +9695,12 @@ "node": ">= 18" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9453,12 +9792,20 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -9686,7 +10033,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -9699,7 +10045,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -9833,6 +10178,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-manager-detector": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.0.tgz", @@ -9844,7 +10221,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -9857,7 +10233,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -9896,7 +10271,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9923,7 +10297,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9960,6 +10333,12 @@ "node": ">= 14.16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9982,7 +10361,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -10272,6 +10650,15 @@ "dev": true, "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-polyfill": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", @@ -10317,7 +10704,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -10327,22 +10713,98 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz", + "integrity": "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==", + "deprecated": "< 24.15.0 is no longer supported", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1367902", + "puppeteer-core": "23.11.1", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -10383,7 +10845,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -10393,7 +10854,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -10409,7 +10869,6 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -10746,7 +11205,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -10858,7 +11316,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -10875,7 +11332,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -10946,7 +11402,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11002,7 +11457,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/saxes": { @@ -11037,7 +11491,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -11060,7 +11513,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11070,7 +11522,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -11083,7 +11534,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "dev": true, "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -11154,7 +11604,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/shadcn": { @@ -11245,7 +11694,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11258,7 +11706,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11375,6 +11822,44 @@ "dev": true, "license": "MIT" }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sortobject": { "version": "4.17.0", "resolved": "https://registry.npmjs.org/sortobject/-/sortobject-4.17.0.tgz", @@ -11392,7 +11877,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11418,7 +11903,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -11472,7 +11956,6 @@ "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "dev": true, "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -11791,6 +12274,31 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -11814,12 +12322,17 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -11947,7 +12460,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -12056,7 +12568,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -12071,7 +12582,6 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -12081,7 +12591,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -12168,6 +12677,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -12224,6 +12739,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/undici": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", + "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -12258,7 +12816,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -12308,7 +12865,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -12387,7 +12943,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -13785,7 +14340,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -13977,14 +14531,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -14112,6 +14664,16 @@ "node": ">=8" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -14155,7 +14717,6 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -14165,7 +14726,6 @@ "version": "3.24.6", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.24.1" diff --git a/package.json b/package.json index 8235423..e695da5 100644 --- a/package.json +++ b/package.json @@ -26,18 +26,20 @@ "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", "i18next-scanner": "^4.6.0", + "jsdom": "^25.0.1", "playwright": "^1.55.1", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.1.0", "prettier-plugin-tailwindcss": "^0.6.11", "shadcn": "^3.3.1", "typescript-eslint": "^8.23.0", - "vitest": "^2.1.5", - "jsdom": "^25.0.1" + "vitest": "^2.1.5" }, "dependencies": { "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.1.0", + "@modelcontextprotocol/server-puppeteer": "^2025.5.12", + "@modelcontextprotocol/server-sequential-thinking": "^2025.7.1", "@paypal/react-paypal-js": "^8.9.2", "@playwright/mcp": "^0.0.37", "@radix-ui/react-accordion": "^1.2.12", @@ -63,6 +65,7 @@ "@tanstack/react-query": "^5.90.2", "@types/react": "^19.0.3", "@types/react-dom": "^19.0.2", + "@upstash/context7-mcp": "^1.0.21", "@vitejs/plugin-react": "^4.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/playwright-report/index.html b/playwright-report/index.html deleted file mode 100644 index 941a2a8..0000000 --- a/playwright-report/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - Playwright Test Report - - - - -
- - - \ No newline at end of file diff --git a/stripe.exe b/stripe.exe deleted file mode 100644 index 743291c..0000000 Binary files a/stripe.exe and /dev/null differ