Files
fotospiel-app/resources/js/guest/components/GalleryPreview.tsx
Codex Agent a949c8d3af - 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.
2025-10-19 11:41:03 +02:00

125 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { Link } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/card';
import { getDeviceId } from '../lib/device';
import { usePollGalleryDelta } from '../polling/usePollGalleryDelta';
type Props = { token: string };
export default function GalleryPreview({ token }: Props) {
const { photos, loading } = usePollGalleryDelta(token);
const [mode, setMode] = React.useState<'latest' | 'popular' | 'myphotos'>('latest');
const items = React.useMemo(() => {
let arr = photos.slice();
// MyPhotos filter (requires session_id matching)
if (mode === 'myphotos') {
const deviceId = getDeviceId();
arr = arr.filter((photo: any) => photo.session_id === deviceId);
}
// Sorting
if (mode === 'popular') {
arr.sort((a: any, b: any) => (b.likes_count ?? 0) - (a.likes_count ?? 0));
} else {
arr.sort((a: any, b: any) => new Date(b.created_at ?? 0).getTime() - new Date(a.created_at ?? 0).getTime());
}
return arr.slice(0, 4); // 2x2 = 4 items
}, [photos, mode]);
// Helper function to generate photo title (must be before return)
function getPhotoTitle(photo: any): string {
if (photo.task_id) {
return `Task: ${photo.task_title || 'Unbekannte Aufgabe'}`;
}
if (photo.emotion_id) {
return `Emotion: ${photo.emotion_name || 'Gefühl'}`;
}
// Fallback based on creation time or placeholder
const now = new Date();
const created = new Date(photo.created_at || now);
const hours = created.getHours();
if (hours < 12) return 'Morgenmoment';
if (hours < 18) return 'Nachmittagslicht';
return 'Abendstimmung';
}
return (
<div>
<div className="mb-4 flex items-center justify-between">
<div className="inline-flex rounded-full bg-white/80 backdrop-blur-sm border border-pink-200 p-1 shadow-sm">
<button
onClick={() => setMode('latest')}
className={`px-4 py-2 text-sm font-medium rounded-full transition-all duration-200 ${
mode === 'latest'
? 'bg-gradient-to-r from-pink-500 to-pink-600 text-white shadow-lg scale-105'
: 'text-gray-600 hover:bg-pink-50 hover:text-pink-700'
}`}
>
Newest
</button>
<button
onClick={() => setMode('popular')}
className={`px-4 py-2 text-sm font-medium rounded-full transition-all duration-200 ${
mode === 'popular'
? 'bg-gradient-to-r from-pink-500 to-pink-600 text-white shadow-lg scale-105'
: 'text-gray-600 hover:bg-pink-50 hover:text-pink-700'
}`}
>
Popular
</button>
<button
onClick={() => setMode('myphotos')}
className={`px-4 py-2 text-sm font-medium rounded-full transition-all duration-200 ${
mode === 'myphotos'
? 'bg-gradient-to-r from-pink-500 to-pink-600 text-white shadow-lg scale-105'
: 'text-gray-600 hover:bg-pink-50 hover:text-pink-700'
}`}
>
My Photos
</button>
</div>
<Link to={`/e/${encodeURIComponent(token)}/gallery?mode=${mode}`} className="text-sm text-pink-600 hover:text-pink-700 font-medium">
Alle ansehen
</Link>
</div>
{loading && <p className="text-sm text-muted-foreground">Lädt</p>}
{!loading && items.length === 0 && (
<Card>
<CardContent className="p-3 text-sm text-muted-foreground">
Noch keine Fotos. Starte mit deinem ersten Upload!
</CardContent>
</Card>
)}
<div className="grid grid-cols-2 gap-3">
{items.map((p: any) => (
<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}
alt={p.title || 'Foto'}
className="aspect-square w-full rounded-xl object-cover shadow-lg hover:shadow-xl transition-shadow duration-200"
loading="lazy"
/>
{/* Photo Title */}
<div className="mt-2">
<div className="text-xs font-medium text-gray-900 line-clamp-2 bg-white/80 px-2 py-1 rounded-md">
{p.title || getPhotoTitle(p)}
</div>
{p.likes_count > 0 && (
<div className="mt-1 flex items-center gap-1 text-xs text-pink-600">
{p.likes_count}
</div>
)}
</div>
</div>
</Link>
))}
</div>
</div>
);
}