legal documents improved, gäste-pwa uploads optimiert: client-side compression/resize.
This commit is contained in:
@@ -2353,7 +2353,7 @@ class EventPublicController extends BaseController
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'photo' => ['required', 'image', 'max:6144'], // 6 MB
|
||||
'photo' => ['required', 'image', 'max:12288'], // 12 MB
|
||||
'emotion_id' => ['nullable', 'integer'],
|
||||
'emotion_slug' => ['nullable', 'string'],
|
||||
'task_id' => ['nullable', 'integer'],
|
||||
|
||||
@@ -8,7 +8,7 @@ Schweriner Str. 15
|
||||
19306 Neustadt-Glewe
|
||||
Mobil 0173 / 9266802
|
||||
W-IdNr. DE 428754098
|
||||
E-Mail: [Kontakt siehe Impressum](https://fotospiel.app/impressum)
|
||||
E-Mail: [Kontakt siehe Impressum](/de/impressum)
|
||||
|
||||
---
|
||||
|
||||
@@ -57,11 +57,10 @@ Der Anbieter verwendet Inhalte ausschließlich zur technischen Bereitstellung (S
|
||||
## 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 **Paddle** 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:
|
||||
– Paddle (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.
|
||||
3. Die Zahlung erfolgt im Voraus über **Paddle** (z. B. Kreditkarte, Apple Pay, Google Pay).
|
||||
4. Bei Nutzung dieses Dienstes gelten zusätzlich die AGB und Datenschutzhinweise des Anbieters:
|
||||
– Paddle.com Market Ltd., London, Vereinigtes Königreich
|
||||
5. Der Anbieter erhält von diesem Dienst nur die für die Abwicklung erforderlichen Zahlungs- und Statusinformationen.
|
||||
6. Rechnungen werden elektronisch bereitgestellt.
|
||||
|
||||
---
|
||||
@@ -82,7 +81,7 @@ Ein konkreter Verfügbarkeitsgrad wird nicht geschuldet.
|
||||
---
|
||||
|
||||
## 10. Datenschutz
|
||||
1. Personenbezogene Daten werden gemäß der **Datenschutzerklärung** verarbeitet ([https://fotospiel.app/datenschutz](https://fotospiel.app/datenschutz)).
|
||||
1. Personenbezogene Daten werden gemäß der **Datenschutzerklärung** verarbeitet ([/de/datenschutz](/de/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.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Schweriner Str. 15
|
||||
19306 Neustadt-Glewe, Germany
|
||||
Mobile +49 173 9266802
|
||||
Business ID: DE 428754098
|
||||
Email: [Contact via imprint](https://fotospiel.app/impressum)
|
||||
Email: [Contact via imprint](/en/imprint)
|
||||
|
||||
---
|
||||
|
||||
@@ -57,11 +57,10 @@ The Provider uses such content solely for technical purposes (storage, display,
|
||||
## 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 **Paddle** or **Stripe Checkout** (credit card, Apple Pay, Google Pay, etc.).
|
||||
4. The payment process is governed by the respective provider’s terms:
|
||||
– Paddle (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.
|
||||
3. Payment is made in advance via **Paddle** (e.g., credit card, Apple Pay, Google Pay).
|
||||
4. The payment process is governed by the provider’s terms:
|
||||
– Paddle.com Market Ltd., London, United Kingdom
|
||||
5. The Provider only receives the transaction and payment status data necessary for processing.
|
||||
6. Invoices are issued electronically.
|
||||
|
||||
---
|
||||
@@ -82,7 +81,7 @@ No specific uptime is guaranteed.
|
||||
---
|
||||
|
||||
## 10. Data Protection
|
||||
1. Personal data is processed in accordance with the **Privacy Policy** ([https://fotospiel.app/datenschutz](https://fotospiel.app/datenschutz)).
|
||||
1. Personal data is processed in accordance with the **Privacy Policy** ([/en/privacy](/en/privacy)).
|
||||
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.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Schweriner Str. 15
|
||||
Deutschland
|
||||
|
||||
E-Mail: info@fotospiel.app
|
||||
Website: [https://fotospiel.app](https://fotospiel.app)
|
||||
Website: [/de/](/de/)
|
||||
|
||||
---
|
||||
|
||||
@@ -21,9 +21,9 @@ Die Nutzung der Fotospiel App ist grundsätzlich nur mit den personenbezogenen D
|
||||
---
|
||||
|
||||
## 3. Arten der verarbeiteten Daten
|
||||
- Veranstalterdaten: Name, E-Mail-Adresse, Zahlungsinformationen (über Paddle/Stripe), Eventdaten (Titel, Datum, Aufgaben, Bilder)
|
||||
- Veranstalterdaten: Name, E-Mail-Adresse, Zahlungsinformationen (über Paddle), Eventdaten (Titel, Datum, Aufgaben, Bilder)
|
||||
- Nutzerdaten (Gäste): hochgeladene Fotos, Anzeigename (frei wählbar), Reaktionen/Likes
|
||||
- Technische Daten: IP-Adresse, Browsertyp, Zeitstempel, Geräteinformationen
|
||||
- Technische Daten: IP-Adresse, Browsertyp, Zeitstempel, Geräteinformationen, anonyme Sitzungskennung (session_id)
|
||||
- Kommunikationsdaten: Inhalte von Kontaktanfragen über das Formular oder per E-Mail
|
||||
|
||||
---
|
||||
@@ -33,7 +33,7 @@ Die Nutzung der Fotospiel App ist grundsätzlich nur mit den personenbezogenen D
|
||||
|------------------------|----------------|---------------|
|
||||
| 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 Paddle und Stripe |
|
||||
| Abrechnung und Zahlungsabwicklung | Art. 6 Abs. 1 lit. b, lit. c DSGVO | Nutzung der Dienste von Paddle |
|
||||
| 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 |
|
||||
@@ -48,14 +48,13 @@ Die Verarbeitung erfolgt ausschließlich innerhalb der EU.
|
||||
---
|
||||
|
||||
## 6. Zahlungsabwicklung
|
||||
Die Zahlungsabwicklung erfolgt über **Paddle (Europe) S.à r.l. et Cie, S.C.A.** und **Stripe Payments Europe, Ltd.**
|
||||
Bei der Zahlung werden personenbezogene Daten an diese Dienstleister übermittelt.
|
||||
Die Zahlungsabwicklung erfolgt über **Paddle.com Market Ltd.**
|
||||
Bei der Zahlung werden personenbezogene Daten an diesen Dienstleister übermittelt.
|
||||
Wir speichern keine Zahlungs- oder Kreditkartendaten.
|
||||
Rechtsgrundlage: Art. 6 Abs. 1 lit. b und lit. c DSGVO.
|
||||
|
||||
Datenschutzhinweise der Anbieter:
|
||||
- Paddle: https://www.paypal.com/de/webapps/mpp/ua/privacy-full
|
||||
- Stripe: https://stripe.com/de/privacy
|
||||
- Paddle: https://www.paddle.com/legal/privacy
|
||||
|
||||
---
|
||||
|
||||
@@ -63,7 +62,8 @@ Datenschutzhinweise der Anbieter:
|
||||
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.
|
||||
Im Rahmen der Gästebereiche der App wird eine anonyme Sitzungskennung (**session_id**) verwendet, die in einem technisch notwendigen Cookie bzw. im lokalen Speicher (Local Storage) des Browsers gespeichert wird, um Uploads, Likes und Aufgaben einem Gerät bzw. einer Sitzung zuzuordnen. Diese Kennung enthält keine Klardaten wie Namen oder E-Mail-Adressen und verliert spätestens mit Ablauf der Event- bzw. Galeriedauer ihre Gültigkeit.
|
||||
Es werden ausschließlich technisch notwendige Cookies gesetzt.
|
||||
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO.
|
||||
|
||||
---
|
||||
@@ -88,7 +88,7 @@ Eine Einwilligung ist nicht erforderlich.
|
||||
|
||||
## 10. Weitergabe an Dritte
|
||||
Eine Weitergabe erfolgt nur an:
|
||||
- Zahlungsdienstleister (Paddle, Stripe)
|
||||
- Zahlungsdienstleister (Paddle)
|
||||
- Hoster (Hetzner)
|
||||
- Gesetzlich erforderliche Stellen (z. B. Finanzbehörden)
|
||||
|
||||
@@ -121,4 +121,4 @@ Wir setzen technische und organisatorische Maßnahmen zur Sicherung Ihrer Daten
|
||||
|
||||
## 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.
|
||||
Die jeweils aktuelle Fassung ist unter [/de/datenschutz](/de/datenschutz) abrufbar.
|
||||
|
||||
@@ -10,7 +10,7 @@ Schweriner Str. 15
|
||||
Germany
|
||||
|
||||
Email: info@fotospiel.app
|
||||
Website: [https://fotospiel.app](https://fotospiel.app)
|
||||
Website: [/en/](/en/)
|
||||
|
||||
---
|
||||
|
||||
@@ -21,9 +21,9 @@ Use of the Fotospiel App requires only the personal data necessary to host and p
|
||||
---
|
||||
|
||||
## 3. Types of Data Processed
|
||||
- Organizer data: name, email address, payment information (via Paddle/Stripe), event details (title, date, photo tasks, photos)
|
||||
- Organizer data: name, email address, payment information (via Paddle), 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
|
||||
- Technical data: IP address, browser type, timestamp, device information, anonymous session identifier (session_id)
|
||||
- Communication data: messages sent via contact form or email
|
||||
|
||||
---
|
||||
@@ -33,7 +33,7 @@ Use of the Fotospiel App requires only the personal data necessary to host and p
|
||||
|----------|--------------|-------------|
|
||||
| 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 Paddle and Stripe services |
|
||||
| Payment processing and invoicing | Art. 6(1)(b), (c) GDPR | Use of Paddle 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 |
|
||||
@@ -48,13 +48,12 @@ All processing takes place within the EU.
|
||||
---
|
||||
|
||||
## 6. Payment Processing
|
||||
Payments are handled by **Paddle (Europe) S.à r.l. et Cie, S.C.A.** and **Stripe Payments Europe, Ltd.**
|
||||
Payments are handled by **Paddle.com Market Ltd.**
|
||||
We do not store payment or credit card data.
|
||||
Legal basis: Art. 6(1)(b) and (c) GDPR.
|
||||
|
||||
Privacy policies:
|
||||
- Paddle: https://www.paypal.com/de/webapps/mpp/ua/privacy-full
|
||||
- Stripe: https://stripe.com/de/privacy
|
||||
- Paddle: https://www.paddle.com/legal/privacy
|
||||
|
||||
---
|
||||
|
||||
@@ -62,6 +61,7 @@ Privacy policies:
|
||||
We use **Matomo** (self-hosted) for anonymous usage analysis.
|
||||
No data is shared with third parties.
|
||||
IP addresses are anonymized.
|
||||
In the guest areas of the app, an anonymous session identifier (**session_id**) is used and stored in a technically necessary cookie or in the browser’s local storage to associate uploads, likes, and tasks with a device or session. This identifier does not contain clear data such as names or email addresses and becomes invalid at the latest when the event or gallery storage period ends.
|
||||
Only technically necessary cookies are used.
|
||||
Legal basis: Art. 6(1)(f) GDPR.
|
||||
|
||||
@@ -87,7 +87,7 @@ No consent is required.
|
||||
|
||||
## 10. Data Disclosure
|
||||
Data is only shared with:
|
||||
- Payment providers (Paddle, Stripe)
|
||||
- Payment providers (Paddle)
|
||||
- Hosting provider (Hetzner)
|
||||
- Public authorities when legally required
|
||||
|
||||
@@ -120,4 +120,4 @@ We apply appropriate technical and organizational measures to secure your data,
|
||||
|
||||
## 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).
|
||||
The current version is always available at [/en/privacy](/en/privacy).
|
||||
|
||||
@@ -32,6 +32,7 @@ import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
|
||||
import { buildLimitSummaries, type LimitSummaryCard } from '../lib/limitSummaries';
|
||||
import { resolveUploadErrorDialog, type UploadErrorDialog } from '../lib/uploadErrorDialog';
|
||||
import { useEventStats } from '../context/EventStatsContext';
|
||||
import { compressPhoto, formatBytes } from '../lib/image';
|
||||
|
||||
interface Task {
|
||||
id: number;
|
||||
@@ -201,6 +202,8 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
let active = true;
|
||||
|
||||
async function loadTask() {
|
||||
if (taskId === null) return;
|
||||
|
||||
const currentTaskId = taskId;
|
||||
const fallbackTitle = t('upload.taskInfo.fallbackTitle').replace('{id}', `${currentTaskId}`);
|
||||
const fallbackDescription = t('upload.taskInfo.fallbackDescription');
|
||||
@@ -553,19 +556,57 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
const handleUsePhoto = useCallback(async () => {
|
||||
if (!eventKey || !reviewPhoto || !task || !canUpload) return;
|
||||
setMode('uploading');
|
||||
setUploadProgress(5);
|
||||
setUploadProgress(2);
|
||||
setUploadError(null);
|
||||
setUploadWarning(null);
|
||||
setStatusMessage(t('upload.status.preparing'));
|
||||
|
||||
if (uploadProgressTimerRef.current) {
|
||||
window.clearInterval(uploadProgressTimerRef.current);
|
||||
uploadProgressTimerRef.current = null;
|
||||
}
|
||||
uploadProgressTimerRef.current = window.setInterval(() => {
|
||||
setUploadProgress((prev) => (prev < 90 ? prev + 5 : prev));
|
||||
}, 400);
|
||||
|
||||
const maxEdge = 2400;
|
||||
const targetBytes = 4_000_000;
|
||||
let fileForUpload = reviewPhoto.file;
|
||||
|
||||
try {
|
||||
const photoId = await uploadPhoto(eventKey, reviewPhoto.file, task.id, emotionSlug || undefined);
|
||||
setStatusMessage(t('upload.status.optimizing', 'Foto wird optimiert…'));
|
||||
const optimized = await compressPhoto(reviewPhoto.file, {
|
||||
maxEdge,
|
||||
targetBytes,
|
||||
qualityStart: 0.82,
|
||||
});
|
||||
|
||||
fileForUpload = optimized;
|
||||
setUploadProgress(10);
|
||||
|
||||
if (optimized.size < reviewPhoto.file.size - 50_000) {
|
||||
const saved = formatBytes(reviewPhoto.file.size - optimized.size);
|
||||
setUploadWarning(
|
||||
t('upload.optimizedNotice', 'Wir haben dein Foto verkleinert, damit der Upload schneller klappt. Eingespart: {saved}')
|
||||
.replace('{saved}', saved)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Image optimization failed, uploading original', e);
|
||||
setUploadWarning(t('upload.optimizedFallback', 'Optimierung nicht möglich – wir laden das Original hoch.'));
|
||||
}
|
||||
|
||||
try {
|
||||
const photoId = await uploadPhoto(eventKey, fileForUpload, task.id, emotionSlug || undefined, {
|
||||
maxRetries: 2,
|
||||
onProgress: (percent) => {
|
||||
setUploadProgress(Math.max(15, Math.min(98, percent)));
|
||||
setStatusMessage(t('upload.status.uploading'));
|
||||
},
|
||||
onRetry: (attempt) => {
|
||||
setUploadWarning(
|
||||
t('upload.retrying', 'Verbindung holperig – neuer Versuch ({attempt}).')
|
||||
.replace('{attempt}', `${attempt}`)
|
||||
);
|
||||
},
|
||||
});
|
||||
setUploadProgress(100);
|
||||
setStatusMessage(t('upload.status.completed'));
|
||||
markCompleted(task.id);
|
||||
@@ -580,6 +621,12 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
setErrorDialog(dialog);
|
||||
setUploadError(dialog.description);
|
||||
|
||||
if (uploadErr.status === 422 || uploadErr.code === 'validation_error') {
|
||||
setUploadWarning(
|
||||
t('upload.errors.tooLargeHint', 'Das Foto war zu groß. Bitte erneut versuchen – wir verkleinern es automatisch.')
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
uploadErr.code === 'photo_limit_exceeded'
|
||||
|| uploadErr.code === 'upload_device_limit'
|
||||
@@ -592,10 +639,6 @@ const [canUpload, setCanUpload] = useState(true);
|
||||
|
||||
setMode('review');
|
||||
} finally {
|
||||
if (uploadProgressTimerRef.current) {
|
||||
window.clearInterval(uploadProgressTimerRef.current);
|
||||
uploadProgressTimerRef.current = null;
|
||||
}
|
||||
setStatusMessage('');
|
||||
}
|
||||
}, [emotionSlug, markCompleted, navigateAfterUpload, reviewPhoto, eventKey, stopStream, task, canUpload, t]);
|
||||
|
||||
@@ -90,49 +90,117 @@ export async function likePhoto(id: number): Promise<number> {
|
||||
return json.likes_count ?? json.data?.likes_count ?? 0;
|
||||
}
|
||||
|
||||
export async function uploadPhoto(eventToken: string, file: File, taskId?: number, emotionSlug?: string): Promise<number> {
|
||||
type UploadOptions = {
|
||||
onProgress?: (percent: number) => void;
|
||||
signal?: AbortSignal;
|
||||
maxRetries?: number;
|
||||
onRetry?: (attempt: number) => void;
|
||||
};
|
||||
|
||||
export async function uploadPhoto(
|
||||
eventToken: string,
|
||||
file: File,
|
||||
taskId?: number,
|
||||
emotionSlug?: string,
|
||||
options: UploadOptions = {}
|
||||
): Promise<number> {
|
||||
const formData = new FormData();
|
||||
formData.append('photo', file, `photo-${Date.now()}.jpg`);
|
||||
formData.append('photo', file, file.name || `photo-${Date.now()}.jpg`);
|
||||
if (taskId) formData.append('task_id', taskId.toString());
|
||||
if (emotionSlug) formData.append('emotion_slug', emotionSlug);
|
||||
formData.append('device_id', getDeviceId());
|
||||
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/upload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
// Don't set Content-Type for FormData - let browser handle it with boundary
|
||||
});
|
||||
const maxRetries = options.maxRetries ?? 2;
|
||||
const url = `/api/v1/events/${encodeURIComponent(eventToken)}/upload`;
|
||||
const headers = getCsrfHeaders();
|
||||
|
||||
if (!res.ok) {
|
||||
let payload: any = null;
|
||||
const attemptUpload = (attempt: number): Promise<any> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.responseType = 'json';
|
||||
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
xhr.setRequestHeader(key, value);
|
||||
});
|
||||
|
||||
if (options.signal) {
|
||||
const onAbort = () => xhr.abort();
|
||||
options.signal.addEventListener('abort', onAbort, { once: true });
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable && options.onProgress) {
|
||||
const percent = Math.min(99, Math.round((event.loaded / event.total) * 100));
|
||||
options.onProgress(percent);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
const status = xhr.status;
|
||||
const payload = xhr.response ?? null;
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
resolve(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const error: UploadError = new Error(
|
||||
payload?.error?.message ?? `Upload failed: ${status}`
|
||||
);
|
||||
error.code = payload?.error?.code ?? (status === 0 ? 'network_error' : 'upload_failed');
|
||||
error.status = status;
|
||||
if (payload?.error?.meta) {
|
||||
error.meta = payload.error.meta as Record<string, unknown>;
|
||||
}
|
||||
reject(error);
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
const error: UploadError = new Error('Network error during upload');
|
||||
error.code = 'network_error';
|
||||
reject(error);
|
||||
};
|
||||
|
||||
xhr.ontimeout = () => {
|
||||
const error: UploadError = new Error('Upload timed out');
|
||||
error.code = 'timeout';
|
||||
reject(error);
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
payload = await res.clone().json();
|
||||
} catch {}
|
||||
const json = await attemptUpload(attempt + 1);
|
||||
return json?.photo_id ?? json?.id ?? json?.data?.id ?? 0;
|
||||
} catch (error) {
|
||||
const err = error as UploadError;
|
||||
|
||||
if (res.status === 419) {
|
||||
const csrfError: UploadError = new Error(
|
||||
'CSRF token mismatch during upload. Please refresh the page and try again.'
|
||||
);
|
||||
csrfError.code = 'csrf_mismatch';
|
||||
csrfError.status = res.status;
|
||||
throw csrfError;
|
||||
if (attempt < maxRetries && (err.code === 'network_error' || (err.status ?? 0) >= 500)) {
|
||||
options.onRetry?.(attempt + 1);
|
||||
const delay = 300 * (attempt + 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Map CSRF mismatch specifically for caller handling
|
||||
if ((err.status ?? 0) === 419) {
|
||||
err.code = 'csrf_mismatch';
|
||||
}
|
||||
|
||||
// Flag common validation failure for file size/validation
|
||||
if ((err.status ?? 0) === 422 && !err.code) {
|
||||
err.code = 'validation_error';
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
const error: UploadError = new Error(
|
||||
payload?.error?.message ?? `Upload failed: ${res.status}`
|
||||
);
|
||||
error.code = payload?.error?.code ?? 'upload_failed';
|
||||
error.status = res.status;
|
||||
if (payload?.error?.meta) {
|
||||
error.meta = payload.error.meta as Record<string, unknown>;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.photo_id ?? json.id ?? json.data?.id ?? 0;
|
||||
throw new Error('Upload failed after retries');
|
||||
}
|
||||
|
||||
export async function createPhotoShareLink(eventToken: string, photoId: number): Promise<{ slug: string; url: string; expires_at?: string }> {
|
||||
|
||||
@@ -126,12 +126,24 @@ Route::prefix('{locale}')
|
||||
Route::get('/impressum', [LegalPageController::class, 'show'])
|
||||
->defaults('slug', 'impressum')
|
||||
->name('impressum');
|
||||
Route::get('/imprint', [LegalPageController::class, 'show'])
|
||||
->where('locale', 'en')
|
||||
->defaults('slug', 'impressum')
|
||||
->name('imprint');
|
||||
Route::get('/datenschutz', [LegalPageController::class, 'show'])
|
||||
->defaults('slug', 'datenschutz')
|
||||
->name('datenschutz');
|
||||
Route::get('/privacy', [LegalPageController::class, 'show'])
|
||||
->where('locale', 'en')
|
||||
->defaults('slug', 'datenschutz')
|
||||
->name('privacy');
|
||||
Route::get('/agb', [LegalPageController::class, 'show'])
|
||||
->defaults('slug', 'agb')
|
||||
->name('agb');
|
||||
Route::get('/terms', [LegalPageController::class, 'show'])
|
||||
->where('locale', 'en')
|
||||
->defaults('slug', 'agb')
|
||||
->name('terms');
|
||||
|
||||
Route::get('/buy/{packageId}', [MarketingController::class, 'buyPackages'])
|
||||
->name('buy.packages')
|
||||
|
||||
Reference in New Issue
Block a user