311 lines
12 KiB
TypeScript
311 lines
12 KiB
TypeScript
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;
|