- Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env
hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads, attach packages, and surface localized success/error states. - Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/ PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent creation, webhooks, and the wizard CTA. - Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/ useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages, Checkout) with localized copy and experiment tracking. - Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations. - Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke test for the hero CTA while reconciling outstanding checklist items.
This commit is contained in:
@@ -16,8 +16,8 @@ interface EmotionPickerProps {
|
||||
}
|
||||
|
||||
export default function EmotionPicker({ onSelect }: EmotionPickerProps) {
|
||||
const { token: slug } = useParams<{ token: string }>();
|
||||
const eventKey = slug ?? '';
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const eventKey = token ?? '';
|
||||
const navigate = useNavigate();
|
||||
const [emotions, setEmotions] = useState<Emotion[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getDeviceId } from '../lib/device';
|
||||
import { usePollGalleryDelta } from '../polling/usePollGalleryDelta';
|
||||
|
||||
type Props = { slug: string };
|
||||
type Props = { token: string };
|
||||
|
||||
export default function GalleryPreview({ slug }: Props) {
|
||||
const { photos, loading } = usePollGalleryDelta(slug);
|
||||
export default function GalleryPreview({ token }: Props) {
|
||||
const { photos, loading } = usePollGalleryDelta(token);
|
||||
const [mode, setMode] = React.useState<'latest' | 'popular' | 'myphotos'>('latest');
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
@@ -82,7 +81,7 @@ export default function GalleryPreview({ slug }: Props) {
|
||||
My Photos
|
||||
</button>
|
||||
</div>
|
||||
<Link to={`/e/${encodeURIComponent(slug)}/gallery?mode=${mode}`} className="text-sm text-pink-600 hover:text-pink-700 font-medium">
|
||||
<Link to={`/e/${encodeURIComponent(token)}/gallery?mode=${mode}`} className="text-sm text-pink-600 hover:text-pink-700 font-medium">
|
||||
Alle ansehen →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -97,7 +96,7 @@ export default function GalleryPreview({ slug }: Props) {
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{items.map((p: any) => (
|
||||
<Link key={p.id} to={`/e/${encodeURIComponent(slug)}/gallery?photoId=${p.id}`} className="block">
|
||||
<Link key={p.id} to={`/e/${encodeURIComponent(token)}/gallery?photoId=${p.id}`} className="block">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={p.thumbnail_path || p.file_path}
|
||||
|
||||
@@ -68,12 +68,12 @@ function renderEventAvatar(name: string, icon: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function Header({ slug, title = '' }: { slug?: string; title?: string }) {
|
||||
export default function Header({ eventToken, title = '' }: { eventToken?: string; title?: string }) {
|
||||
const statsContext = useOptionalEventStats();
|
||||
const identity = useOptionalGuestIdentity();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!slug) {
|
||||
if (!eventToken) {
|
||||
const guestName = identity?.name && identity?.hydrated ? identity.name : null;
|
||||
return (
|
||||
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40">
|
||||
@@ -95,7 +95,7 @@ export default function Header({ slug, title = '' }: { slug?: string; title?: st
|
||||
|
||||
const { event, status } = useEventData();
|
||||
const guestName =
|
||||
identity && identity.eventKey === slug && identity.hydrated && identity.name ? identity.name : null;
|
||||
identity && identity.eventKey === eventToken && identity.hydrated && identity.name ? identity.name : null;
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
@@ -114,7 +114,7 @@ export default function Header({ slug, title = '' }: { slug?: string; title?: st
|
||||
}
|
||||
|
||||
const stats =
|
||||
statsContext && statsContext.eventKey === slug ? statsContext : undefined;
|
||||
statsContext && statsContext.eventKey === eventToken ? statsContext : undefined;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40">
|
||||
|
||||
@@ -272,17 +272,17 @@ function SummaryCards({ data }: { data: AchievementsPayload }) {
|
||||
);
|
||||
}
|
||||
|
||||
function PersonalActions({ slug }: { slug: string }) {
|
||||
function PersonalActions({ token }: { token: string }) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button asChild>
|
||||
<Link to={`/e/${encodeURIComponent(slug)}/upload`} className="flex items-center gap-2">
|
||||
<Link to={`/e/${encodeURIComponent(token)}/upload`} className="flex items-center gap-2">
|
||||
<Camera className="h-4 w-4" />
|
||||
Neues Foto hochladen
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link to={`/e/${encodeURIComponent(slug)}/tasks`} className="flex items-center gap-2">
|
||||
<Link to={`/e/${encodeURIComponent(token)}/tasks`} className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Aufgabe ziehen
|
||||
</Link>
|
||||
@@ -292,7 +292,7 @@ function PersonalActions({ slug }: { slug: string }) {
|
||||
}
|
||||
|
||||
export default function AchievementsPage() {
|
||||
const { token: slug } = useParams<{ token: string }>();
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const identity = useGuestIdentity();
|
||||
const [data, setData] = useState<AchievementsPayload | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -302,12 +302,12 @@ export default function AchievementsPage() {
|
||||
const personalName = identity.hydrated && identity.name ? identity.name : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
if (!token) return;
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetchAchievements(slug, personalName, controller.signal)
|
||||
fetchAchievements(token, personalName, controller.signal)
|
||||
.then((payload) => {
|
||||
setData(payload);
|
||||
if (!payload.personal) {
|
||||
@@ -322,11 +322,11 @@ export default function AchievementsPage() {
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
return () => controller.abort();
|
||||
}, [slug, personalName]);
|
||||
}, [token, personalName]);
|
||||
|
||||
const hasPersonal = Boolean(data?.personal);
|
||||
|
||||
if (!slug) {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ export default function AchievementsPage() {
|
||||
{data.personal.photos} Fotos | {data.personal.tasks} Aufgaben | {data.personal.likes} Likes
|
||||
</CardDescription>
|
||||
</div>
|
||||
<PersonalActions slug={slug} />
|
||||
<PersonalActions token={token} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Page } from './_util';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { usePollGalleryDelta } from '../polling/usePollGalleryDelta';
|
||||
@@ -12,8 +12,8 @@ import PhotoLightbox from './PhotoLightbox';
|
||||
import { fetchEvent, getEventPackage, fetchStats, type EventData, type EventPackage, type EventStats } from '../services/eventApi';
|
||||
|
||||
export default function GalleryPage() {
|
||||
const { token: slug } = useParams<{ token?: string }>();
|
||||
const { photos, loading, newCount, acknowledgeNew } = usePollGalleryDelta(slug ?? '');
|
||||
const { token } = useParams<{ token?: string }>();
|
||||
const { photos, loading, newCount, acknowledgeNew } = usePollGalleryDelta(token ?? '');
|
||||
const [filter, setFilter] = React.useState<GalleryFilter>('latest');
|
||||
const [currentPhotoIndex, setCurrentPhotoIndex] = React.useState<number | null>(null);
|
||||
const [hasOpenedPhoto, setHasOpenedPhoto] = useState(false);
|
||||
@@ -38,15 +38,15 @@ export default function GalleryPage() {
|
||||
|
||||
// Load event and package info
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
if (!token) return;
|
||||
|
||||
const loadEventData = async () => {
|
||||
try {
|
||||
setEventLoading(true);
|
||||
const [eventData, packageData, statsData] = await Promise.all([
|
||||
fetchEvent(slug),
|
||||
getEventPackage(slug),
|
||||
fetchStats(slug),
|
||||
fetchEvent(token),
|
||||
getEventPackage(token),
|
||||
fetchStats(token),
|
||||
]);
|
||||
setEvent(eventData);
|
||||
setEventPackage(packageData);
|
||||
@@ -59,7 +59,7 @@ export default function GalleryPage() {
|
||||
};
|
||||
|
||||
loadEventData();
|
||||
}, [slug]);
|
||||
}, [token]);
|
||||
|
||||
const myPhotoIds = React.useMemo(() => {
|
||||
try {
|
||||
@@ -99,7 +99,7 @@ export default function GalleryPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
if (!token) {
|
||||
return <Page title="Galerie"><p>Event nicht gefunden.</p></Page>;
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@ export default function GalleryPage() {
|
||||
currentIndex={currentPhotoIndex}
|
||||
onClose={() => setCurrentPhotoIndex(null)}
|
||||
onIndexChange={(index: number) => setCurrentPhotoIndex(index)}
|
||||
slug={slug}
|
||||
token={token}
|
||||
/>
|
||||
)}
|
||||
</Page>
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function HomePage() {
|
||||
|
||||
<EmotionPicker />
|
||||
|
||||
<GalleryPreview slug={token} />
|
||||
<GalleryPreview token={token} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,11 @@ export default function LandingPage() {
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const targetKey = data.join_token ?? data.slug ?? normalized;
|
||||
const targetKey = data.join_token ?? '';
|
||||
if (!targetKey) {
|
||||
setErrorKey('eventClosed');
|
||||
return;
|
||||
}
|
||||
const storedName = readGuestName(targetKey);
|
||||
if (!storedName) {
|
||||
nav(`/setup/${encodeURIComponent(targetKey)}`);
|
||||
|
||||
@@ -23,15 +23,15 @@ interface Props {
|
||||
currentIndex?: number;
|
||||
onClose?: () => void;
|
||||
onIndexChange?: (index: number) => void;
|
||||
slug?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexChange, slug }: Props) {
|
||||
export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexChange, token }: Props) {
|
||||
const params = useParams<{ token?: string; photoId?: string }>();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const photoId = params.photoId;
|
||||
const eventSlug = params.token || slug;
|
||||
const eventToken = params.token || token;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [standalonePhoto, setStandalonePhoto] = useState<Photo | null>(null);
|
||||
@@ -53,7 +53,7 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
|
||||
|
||||
// Fetch single photo for standalone mode
|
||||
useEffect(() => {
|
||||
if (isStandalone && photoId && !standalonePhoto && eventSlug) {
|
||||
if (isStandalone && photoId && !standalonePhoto && eventToken) {
|
||||
const fetchPhoto = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -80,7 +80,7 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
|
||||
} else if (!isStandalone) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isStandalone, photoId, eventSlug, standalonePhoto, location.state, t]);
|
||||
}, [isStandalone, photoId, eventToken, standalonePhoto, location.state, t]);
|
||||
|
||||
// Update likes when photo changes
|
||||
React.useEffect(() => {
|
||||
@@ -133,7 +133,7 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
|
||||
|
||||
// Load task info if photo has task_id and event key is available
|
||||
React.useEffect(() => {
|
||||
if (!photo?.task_id || !eventSlug) {
|
||||
if (!photo?.task_id || !eventToken) {
|
||||
setTask(null);
|
||||
setTaskLoading(false);
|
||||
return;
|
||||
@@ -144,7 +144,7 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
|
||||
(async () => {
|
||||
setTaskLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(eventSlug)}/tasks`);
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/tasks`);
|
||||
if (res.ok) {
|
||||
const tasks = await res.json();
|
||||
const foundTask = tasks.find((t: any) => t.id === taskId);
|
||||
@@ -175,7 +175,7 @@ export default function PhotoLightbox({ photos, currentIndex, onClose, onIndexCh
|
||||
setTaskLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [photo?.task_id, eventSlug, t]);
|
||||
}, [photo?.task_id, eventToken, t]);
|
||||
|
||||
async function onLike() {
|
||||
if (liked || !photo) return;
|
||||
|
||||
@@ -28,8 +28,8 @@ const TASK_PROGRESS_TARGET = 5;
|
||||
const TIMER_VIBRATION = [0, 60, 120, 60];
|
||||
|
||||
export default function TaskPickerPage() {
|
||||
const { token: slug } = useParams<{ token: string }>();
|
||||
const eventKey = slug ?? '';
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const eventKey = token ?? '';
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
@@ -92,12 +92,12 @@ export default function TaskPickerPage() {
|
||||
map.set(task.emotion.slug, task.emotion.name);
|
||||
}
|
||||
});
|
||||
return Array.from(map.entries()).map(([slugValue, name]) => ({ slug: slugValue, name }));
|
||||
return Array.from(map.entries()).map(([slugValue, name]) => ({ slug: tokenValue, name }));
|
||||
}, [tasks]);
|
||||
|
||||
const filteredTasks = React.useMemo(() => {
|
||||
if (selectedEmotion === 'all') return tasks;
|
||||
return tasks.filter((task) => task.emotion?.slug === selectedEmotion);
|
||||
return tasks.filter((task) => task.emotion?.token === selectedEmotion);
|
||||
}, [tasks, selectedEmotion]);
|
||||
|
||||
const selectRandomTask = React.useCallback(
|
||||
|
||||
@@ -56,13 +56,13 @@ const DEFAULT_PREFS: CameraPreferences = {
|
||||
};
|
||||
|
||||
export default function UploadPage() {
|
||||
const { token: slug } = useParams<{ token: string }>();
|
||||
const eventKey = slug ?? '';
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const eventKey = token ?? '';
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { appearance } = useAppearance();
|
||||
const isDarkMode = appearance === 'dark';
|
||||
const { markCompleted } = useGuestTaskProgress(slug);
|
||||
const { markCompleted } = useGuestTaskProgress(token);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const taskIdParam = searchParams.get('task');
|
||||
@@ -138,7 +138,7 @@ export default function UploadPage() {
|
||||
|
||||
// Load task metadata
|
||||
useEffect(() => {
|
||||
if (!slug || !taskId) {
|
||||
if (!token || !taskId) {
|
||||
setTaskError(t('upload.loadError.title'));
|
||||
setLoadingTask(false);
|
||||
return;
|
||||
@@ -545,7 +545,7 @@ export default function UploadPage() {
|
||||
if (!supportsCamera && !task) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title={t('upload.cameraTitle')} />
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6">
|
||||
<Alert>
|
||||
<AlertDescription>{t('upload.cameraUnsupported.message')}</AlertDescription>
|
||||
@@ -559,7 +559,7 @@ export default function UploadPage() {
|
||||
if (loadingTask) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title={t('upload.cameraTitle')} />
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6 flex flex-col items-center justify-center text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-pink-500 mb-4" />
|
||||
<p className="text-sm text-muted-foreground">{t('upload.preparing')}</p>
|
||||
@@ -572,7 +572,7 @@ export default function UploadPage() {
|
||||
if (!canUpload) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title={t('upload.cameraTitle')} />
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6">
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
@@ -638,7 +638,7 @@ export default function UploadPage() {
|
||||
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title={t('upload.cameraTitle')} />
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="relative flex flex-col gap-4 pb-4">
|
||||
<div className="absolute left-0 right-0 top-0" aria-hidden="true">
|
||||
{renderPrimer()}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Photo = { id: number; file_path?: string; thumbnail_path?: string; created_at?: string };
|
||||
|
||||
export function usePollGalleryDelta(slug: string) {
|
||||
export function usePollGalleryDelta(token: string) {
|
||||
const [photos, setPhotos] = useState<Photo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [newCount, setNewCount] = useState(0);
|
||||
@@ -13,14 +13,14 @@ export function usePollGalleryDelta(slug: string) {
|
||||
);
|
||||
|
||||
async function fetchDelta() {
|
||||
if (!slug) {
|
||||
if (!token) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const qs = latestAt.current ? `?since=${encodeURIComponent(latestAt.current)}` : '';
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(slug)}/photos${qs}`, {
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(token)}/photos${qs}`, {
|
||||
headers: { 'Cache-Control': 'no-store' },
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ export function usePollGalleryDelta(slug: string) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) {
|
||||
if (!token) {
|
||||
setPhotos([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -107,7 +107,7 @@ export function usePollGalleryDelta(slug: string) {
|
||||
return () => {
|
||||
if (timer.current) window.clearInterval(timer.current);
|
||||
};
|
||||
}, [slug, visible]);
|
||||
}, [token, visible]);
|
||||
|
||||
function acknowledgeNew() { setNewCount(0); }
|
||||
return { loading, photos, newCount, acknowledgeNew };
|
||||
|
||||
@@ -6,7 +6,7 @@ type SyncManager = { register(tag: string): Promise<void>; };
|
||||
|
||||
export type QueueItem = {
|
||||
id?: number;
|
||||
slug: string;
|
||||
eventToken: string;
|
||||
fileName: string;
|
||||
blob: Blob;
|
||||
emotion_id?: number | null;
|
||||
@@ -77,7 +77,7 @@ async function attemptUpload(it: QueueItem): Promise<boolean> {
|
||||
if (!navigator.onLine) return false;
|
||||
try {
|
||||
const json = await createUpload(
|
||||
`/api/v1/events/${encodeURIComponent(it.slug)}/photos`,
|
||||
`/api/v1/events/${encodeURIComponent(it.eventToken)}/photos`,
|
||||
it,
|
||||
getDeviceId(),
|
||||
(pct) => {
|
||||
|
||||
@@ -97,7 +97,7 @@ function EventBoundary({ token }: { token: string }) {
|
||||
<LocaleProvider defaultLocale={eventLocale} storageKey={localeStorageKey}>
|
||||
<EventStatsProvider eventKey={token}>
|
||||
<div className="pb-16">
|
||||
<Header slug={token} />
|
||||
<Header eventToken={token} />
|
||||
<div className="px-4 py-3">
|
||||
<Outlet />
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@ function SetupLayout() {
|
||||
<LocaleProvider defaultLocale={eventLocale} storageKey={localeStorageKey}>
|
||||
<EventStatsProvider eventKey={token}>
|
||||
<div className="pb-0">
|
||||
<Header slug={token} />
|
||||
<Header eventToken={token} />
|
||||
<Outlet />
|
||||
</div>
|
||||
</EventStatsProvider>
|
||||
|
||||
@@ -87,7 +87,7 @@ function safeString(value: unknown): string {
|
||||
}
|
||||
|
||||
export async function fetchAchievements(
|
||||
slug: string,
|
||||
eventToken: string,
|
||||
guestName?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<AchievementsPayload> {
|
||||
@@ -96,7 +96,7 @@ export async function fetchAchievements(
|
||||
params.set('guest_name', guestName.trim());
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/events/${encodeURIComponent(slug)}/achievements?${params.toString()}`, {
|
||||
const response = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/achievements?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Device-Id': getDeviceId(),
|
||||
|
||||
@@ -183,8 +183,8 @@ export async function fetchStats(eventKey: string): Promise<EventStats> {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getEventPackage(slug: string): Promise<EventPackage | null> {
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(slug)}/package`);
|
||||
export async function getEventPackage(eventToken: string): Promise<EventPackage | null> {
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/package`);
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return null;
|
||||
throw new Error('Failed to load event package');
|
||||
|
||||
@@ -70,14 +70,14 @@ export async function likePhoto(id: number): Promise<number> {
|
||||
return json.likes_count ?? json.data?.likes_count ?? 0;
|
||||
}
|
||||
|
||||
export async function uploadPhoto(slug: string, file: File, taskId?: number, emotionSlug?: string): Promise<number> {
|
||||
export async function uploadPhoto(eventToken: string, file: File, taskId?: number, emotionSlug?: string): Promise<number> {
|
||||
const formData = new FormData();
|
||||
formData.append('photo', file, `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(slug)}/upload`, {
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/upload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
|
||||
Reference in New Issue
Block a user