Kunst_Gegen_Bares_Kimaschine/src/features/event/components/PromotionTemplatesSection.tsx
2026-01-21 17:49:11 +01:00

311 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useMemo, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { useIsMobile } from '@/hooks/use-mobile';
type Props = {
mediaUrl: string;
onMediaUrlChange: (url: string) => void;
fileInputRef: React.RefObject<HTMLInputElement | null>;
onFileSelected: (file: File) => void;
mediaPreview: string | null;
};
type PromotionTemplate = {
id: string;
name: string;
description: string;
previewUrl?: string;
};
const PromotionTemplatesSection: React.FC<Props> = ({
mediaUrl,
onMediaUrlChange,
fileInputRef,
onFileSelected,
mediaPreview,
}) => {
const isMobile = useIsMobile();
const templates: PromotionTemplate[] = useMemo(
() => [
{
id: 'tpl_event_minimal',
name: 'Event Minimal',
description: 'Cleanes Event-Template mit Fokus auf Titel + Datum.',
previewUrl: '/images/templates/event_minimal_preview.png',
},
{
id: 'tpl_event_bold',
name: 'Event Bold',
description: 'Große Typo, geeignet für starke Keyvisuals.',
previewUrl: '/images/templates/event_bold_preview.png',
},
{
id: 'tpl_lineup_grid',
name: 'Lineup Grid',
description: 'Lineup als Grid-Layout, gut für mehrere Artists.',
previewUrl: '/images/templates/lineup_grid_preview.png',
},
{
id: 'tpl_artist_focus',
name: 'Artist Focus',
description: 'Portrait/Keyvisual im Fokus, Name + Slot unten.',
previewUrl: '/images/templates/artist_focus_preview.png',
},
{
id: 'tpl_artist_story',
name: 'Artist Story',
description: 'Story-Format mit kurzem Teaser + Social Handles.',
previewUrl: '/images/templates/artist_story_preview.png',
},
],
[]
);
const [eventPromoTemplateId, setEventPromoTemplateId] = useState<string>(templates[0]?.id ?? '');
const [eventLineupTemplateId, setEventLineupTemplateId] = useState<string>(templates[2]?.id ?? '');
const [artistPromoTemplateId, setArtistPromoTemplateId] = useState<string>(templates[3]?.id ?? '');
const importZipInputRef = useRef<HTMLInputElement | null>(null);
const [importingTemplate, setImportingTemplate] = useState(false);
const selectedEventPromo = templates.find((t) => t.id === eventPromoTemplateId) || null;
const selectedEventLineup = templates.find((t) => t.id === eventLineupTemplateId) || null;
const selectedArtistPromo = templates.find((t) => t.id === artistPromoTemplateId) || null;
const handleImportZip = async (file: File) => {
setImportingTemplate(true);
try {
// Placeholder: Upload to backend endpoint will be added later
await new Promise((r) => setTimeout(r, 400));
} finally {
setImportingTemplate(false);
}
};
const TemplateSelect = ({
label,
value,
onValueChange,
selected,
}: {
label: string;
value: string;
onValueChange: (next: string) => void;
selected: PromotionTemplate | null;
}) => {
const [open, setOpen] = useState(false);
return (
<div className="space-y-2">
<Label>{label}</Label>
<div className="grid gap-3 md:grid-cols-[1fr,280px] items-start">
{isMobile ? (
<>
<Button
type="button"
variant="outline"
className="w-full justify-between"
onClick={() => setOpen(true)}
>
<span className="truncate">{selected?.name || 'Template wählen'}</span>
<span className="text-muted-foreground">Auswählen</span>
</Button>
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent side="bottom" className="p-0">
<div className="p-4">
<SheetHeader>
<SheetTitle>{label}</SheetTitle>
</SheetHeader>
</div>
<div className="max-h-[70vh] overflow-y-auto px-4 pb-4">
<div className="space-y-2">
{templates.map((tpl) => {
const isActive = tpl.id === value;
return (
<button
key={tpl.id}
type="button"
className={`w-full rounded-md border p-3 text-left transition-colors ${
isActive ? 'bg-primary/10 border-primary/30' : 'hover:bg-muted/40'
}`}
onClick={() => {
onValueChange(tpl.id);
setOpen(false);
}}
>
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded border bg-muted/40 overflow-hidden flex items-center justify-center shrink-0">
{tpl.previewUrl ? (
<img src={tpl.previewUrl} alt="" className="h-full w-full object-cover" />
) : (
<div className="h-full w-full" />
)}
</div>
<div className="min-w-0">
<div className="text-sm font-medium leading-tight truncate">{tpl.name}</div>
</div>
</div>
</button>
);
})}
</div>
</div>
</SheetContent>
</Sheet>
</>
) : (
<Select value={value} onValueChange={onValueChange}>
<SelectTrigger>
<SelectValue placeholder="Template wählen" />
</SelectTrigger>
<SelectContent className="w-[var(--radix-select-trigger-width)] max-w-[calc(100vw-1.5rem)] overflow-x-hidden">
{templates.map((tpl) => (
<SelectItem key={tpl.id} value={tpl.id}>
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded border bg-muted/40 overflow-hidden flex items-center justify-center">
{tpl.previewUrl ? (
<img src={tpl.previewUrl} alt="" className="h-full w-full object-cover" />
) : (
<div className="h-full w-full" />
)}
</div>
<div className="min-w-0">
<div className="text-sm font-medium leading-tight truncate">{tpl.name}</div>
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
<div className="rounded-md border bg-background p-3">
<div className="text-xs text-muted-foreground">Vorschau</div>
<div className="mt-2 flex gap-3">
<div className="h-16 w-16 rounded border bg-muted/40 overflow-hidden flex items-center justify-center shrink-0">
{selected?.previewUrl ? (
<img src={selected.previewUrl} alt="" className="h-full w-full object-cover" />
) : (
<div className="h-full w-full" />
)}
</div>
<div className="min-w-0">
<div className="text-sm font-medium leading-tight">{selected?.name || 'Kein Template gewählt'}</div>
<div className="mt-1 text-xs text-muted-foreground leading-snug hidden md:block">{selected?.description || ''}</div>
</div>
</div>
</div>
</div>
</div>
);
};
return (
<div className="space-y-4">
<div className="text-sm font-medium">Promotion Templates</div>
<div className="space-y-3 rounded-md border p-3">
<div className="text-sm font-medium">Sektion 1 Event-Promotion</div>
<TemplateSelect
label="Template"
value={eventPromoTemplateId}
onValueChange={setEventPromoTemplateId}
selected={selectedEventPromo}
/>
<div className="space-y-2 pt-2">
<Label>Bild / Video</Label>
<div className="flex flex-col sm:flex-row gap-2 items-start sm:items-center">
<Input placeholder="Media-URL (Bild oder Video)" value={mediaUrl} onChange={(e) => onMediaUrlChange(e.target.value)} />
<div className="flex items-center gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*,video/*"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) return;
onFileSelected(file);
}}
/>
<Button type="button" variant="outline" onClick={() => fileInputRef.current?.click()}>
Datei auswählen
</Button>
</div>
</div>
{mediaPreview && (
<div className="mt-2">
{mediaPreview.match(/\.mp4$|\.webm$|\.ogg$/i) ? (
<video src={mediaPreview} className="h-32 rounded border object-cover" controls />
) : (
<img src={mediaPreview} alt="Event Media" className="h-32 rounded border object-cover" />
)}
</div>
)}
</div>
</div>
<div className="space-y-3 rounded-md border p-3">
<div className="text-sm font-medium">Sektion 2 Event Lineup</div>
<TemplateSelect
label="Template"
value={eventLineupTemplateId}
onValueChange={setEventLineupTemplateId}
selected={selectedEventLineup}
/>
</div>
<div className="space-y-3 rounded-md border p-3">
<div className="text-sm font-medium">Sektion 3 Artist Promo</div>
<TemplateSelect
label="Template"
value={artistPromoTemplateId}
onValueChange={setArtistPromoTemplateId}
selected={selectedArtistPromo}
/>
</div>
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
{/* <div className="text-sm font-medium">Templates verwalten</div> */}
<div className="flex items-center gap-2">
<input
ref={importZipInputRef}
type="file"
accept=".zip"
className="hidden"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
await handleImportZip(file);
e.target.value = '';
}}
/>
<Button
type="button"
variant="outline"
disabled={importingTemplate}
onClick={() => importZipInputRef.current?.click()}
>
Neues Template importieren
</Button>
</div>
</div>
<div className="text-xs text-muted-foreground">
Dieser Bereich wird später konkretisiert (Templates, Textbausteine, Scheduling etc.).
</div>
</div>
);
};
export default PromotionTemplatesSection;