new desighn #deploy
This commit is contained in:
parent
98b8abd56c
commit
9067a4c6c0
248
src/features/event/components/ArtistsSection.tsx
Normal file
248
src/features/event/components/ArtistsSection.tsx
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
|
type ArtistSection = {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
link: string;
|
||||||
|
status: 'Wartet auf Künstler' | 'Link generiert' | 'Fertig' | 'Künstler einladen';
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mode: 'create' | 'edit';
|
||||||
|
eventId?: string | number | null;
|
||||||
|
|
||||||
|
artistSections: ArtistSection[];
|
||||||
|
setArtistSections: React.Dispatch<React.SetStateAction<ArtistSection[]>>;
|
||||||
|
|
||||||
|
sectionDates: string[];
|
||||||
|
setSectionDates: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
|
sectionDateRefs: React.MutableRefObject<Array<HTMLInputElement | null>>;
|
||||||
|
|
||||||
|
promotionSlots: any[] | null;
|
||||||
|
|
||||||
|
generatingLinkIndex: number | null;
|
||||||
|
setGeneratingLinkIndex: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
|
|
||||||
|
generateCollabLink: (eventId: string | number, slotIndex: number, artistName: string, artistEmail: string) => Promise<any>;
|
||||||
|
|
||||||
|
setError: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
formatDateForDisplay: (iso: string) => string;
|
||||||
|
|
||||||
|
onOpenPreview: (slot: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArtistsSection: React.FC<Props> = ({
|
||||||
|
mode,
|
||||||
|
eventId,
|
||||||
|
artistSections,
|
||||||
|
setArtistSections,
|
||||||
|
sectionDates,
|
||||||
|
setSectionDates,
|
||||||
|
sectionDateRefs,
|
||||||
|
promotionSlots,
|
||||||
|
generatingLinkIndex,
|
||||||
|
setGeneratingLinkIndex,
|
||||||
|
generateCollabLink,
|
||||||
|
setError,
|
||||||
|
formatDateForDisplay,
|
||||||
|
onOpenPreview,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{artistSections.map((section, idx) => {
|
||||||
|
const slot = promotionSlots?.find((s) => s.slotIndex === idx + 2);
|
||||||
|
const hasArtistData = !!slot?.artistDescription || (Array.isArray(slot?.media) && slot.media.length > 0);
|
||||||
|
const canOpenPreview = mode === 'edit' && !!slot && hasArtistData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={idx} className="space-y-2 rounded-md border p-3">
|
||||||
|
<div className="flex items-center justify-between text-sm font-medium">
|
||||||
|
<span>{`Künstler ${idx + 1}`}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{artistSections[idx].status}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>{formatDateForDisplay(sectionDates[idx + 1]) || 'Noch kein Datum gewählt'}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const ref = sectionDateRefs.current[idx + 1];
|
||||||
|
if (ref) {
|
||||||
|
if (ref.showPicker) {
|
||||||
|
ref.showPicker();
|
||||||
|
} else {
|
||||||
|
ref.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Datum wählen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
ref={(el) => (sectionDateRefs.current[idx + 1] = el)}
|
||||||
|
id={`section_date_${idx + 2}`}
|
||||||
|
type="date"
|
||||||
|
className="hidden"
|
||||||
|
value={sectionDates[idx + 1]}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = [...sectionDates];
|
||||||
|
next[idx + 1] = e.target.value;
|
||||||
|
setSectionDates(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor={`artist_name_${idx}`}>Künstlername *</Label>
|
||||||
|
<Input
|
||||||
|
id={`artist_name_${idx}`}
|
||||||
|
type="text"
|
||||||
|
placeholder="Name des Künstlers"
|
||||||
|
value={section.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
const next = [...artistSections];
|
||||||
|
next[idx] = { ...next[idx], name: e.target.value };
|
||||||
|
setArtistSections(next);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
if (mode !== 'edit' || !eventId) {
|
||||||
|
setError('Bitte Event zuerst speichern, bevor Links generiert werden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setGeneratingLinkIndex(idx);
|
||||||
|
const raw = await generateCollabLink(eventId, idx + 2, section.name, section.email || '');
|
||||||
|
|
||||||
|
const res: any = (() => {
|
||||||
|
if (!raw) return null;
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw[0] ?? null;
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return Array.isArray(parsed) ? parsed[0] ?? null : parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw as any;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const link = res?.url || (res?.token ? `/collab/${res.token}` : '');
|
||||||
|
if (!link) {
|
||||||
|
setError('Kollaborationslink konnte nicht erzeugt werden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = [...artistSections];
|
||||||
|
next[idx] = { ...next[idx], link, status: 'Link generiert' };
|
||||||
|
setArtistSections(next);
|
||||||
|
} catch (e) {
|
||||||
|
setError('Kollaborationslink konnte nicht erzeugt werden.');
|
||||||
|
} finally {
|
||||||
|
setGeneratingLinkIndex((current) => (current === idx ? null : current));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!section.name || !eventId || mode !== 'edit' || generatingLinkIndex === idx}
|
||||||
|
>
|
||||||
|
{generatingLinkIndex === idx && (
|
||||||
|
<span className="h-3 w-3 mr-1 rounded-full border-2 border-current border-t-transparent animate-spin" />
|
||||||
|
)}
|
||||||
|
Link generieren
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{section.link && (
|
||||||
|
<div className="flex flex-1 items-center gap-2 w-full">
|
||||||
|
<Input readOnly className="text-xs flex-1" value={section.link} onFocus={(e) => e.target.select()} />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(section.link || '');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Clipboard copy failed', e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kopieren
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-1">
|
||||||
|
<Button type="button" variant="outline" size="sm" disabled={!canOpenPreview} onClick={() => slot && onOpenPreview(slot)}>
|
||||||
|
Künstlerdaten ansehen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setArtistSections((prev) => {
|
||||||
|
const next = [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
link: '',
|
||||||
|
status: 'Wartet auf Künstler' as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Künstler hinzufügen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={artistSections.length <= 1}
|
||||||
|
onClick={() => {
|
||||||
|
if (artistSections.length <= 1) return;
|
||||||
|
setArtistSections((prev) => prev.slice(0, -1));
|
||||||
|
setSectionDates((prev) => {
|
||||||
|
if (prev.length <= 2) return prev;
|
||||||
|
return prev.slice(0, -1);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Letzten Künstler entfernen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistsSection;
|
||||||
@ -4,8 +4,13 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { CalendarDays, Megaphone, Users } from 'lucide-react';
|
||||||
import { createEvent, fetchEventById, updateEvent, getEventInfoFromUrl, generateCollabLink, updateArtistCollabDataByOp } from '@/features/event/api/eventService';
|
import { createEvent, fetchEventById, updateEvent, getEventInfoFromUrl, generateCollabLink, updateArtistCollabDataByOp } from '@/features/event/api/eventService';
|
||||||
import { useApiFetch } from '@/utils/apiFetch';
|
import { useApiFetch } from '@/utils/apiFetch';
|
||||||
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
import EventStammdatenSection from '@/features/event/components/EventStammdatenSection';
|
||||||
|
import PromotionTemplatesSection from '@/features/event/components/PromotionTemplatesSection';
|
||||||
|
import ArtistsSection from '@/features/event/components/ArtistsSection';
|
||||||
|
|
||||||
interface EventFormInlineProps {
|
interface EventFormInlineProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
@ -55,6 +60,9 @@ const EventFormInline: React.FC<EventFormInlineProps> = ({ onSuccess, onCancel,
|
|||||||
const [extractingFromUrl, setExtractingFromUrl] = useState(false);
|
const [extractingFromUrl, setExtractingFromUrl] = useState(false);
|
||||||
const [promotionOffsetDays, setPromotionOffsetDays] = useState<string>('2');
|
const [promotionOffsetDays, setPromotionOffsetDays] = useState<string>('2');
|
||||||
const [generatingLinkIndex, setGeneratingLinkIndex] = useState<number | null>(null);
|
const [generatingLinkIndex, setGeneratingLinkIndex] = useState<number | null>(null);
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [activeTab, setActiveTab] = useState<'stammdaten' | 'promotion' | 'artists'>('stammdaten');
|
||||||
|
const navExpanded = !isMobile;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -469,427 +477,155 @@ const EventFormInline: React.FC<EventFormInlineProps> = ({ onSuccess, onCancel,
|
|||||||
return fields.some((key) => (previewDraft as any)[key] !== (previewSlot as any)[key]);
|
return fields.some((key) => (previewDraft as any)[key] !== (previewSlot as any)[key]);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const sidebarWidthClass = navExpanded ? 'w-56' : 'w-14';
|
||||||
|
|
||||||
|
const NavItemButton = ({
|
||||||
|
tab,
|
||||||
|
label,
|
||||||
|
Icon,
|
||||||
|
}: {
|
||||||
|
tab: 'stammdaten' | 'promotion' | 'artists';
|
||||||
|
label: string;
|
||||||
|
Icon: React.ComponentType<{ className?: string }>;
|
||||||
|
}) => {
|
||||||
|
const isActive = activeTab === tab;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={
|
||||||
|
navExpanded
|
||||||
|
? `relative w-full h-10 justify-start gap-3 rounded-xl px-3 text-sm transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-primary/10 text-primary shadow-sm'
|
||||||
|
: 'text-foreground/80 hover:bg-primary/5 hover:text-foreground'
|
||||||
|
}`
|
||||||
|
: `relative w-10 h-10 justify-center rounded-xl px-0 transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-primary/10 text-primary shadow-sm'
|
||||||
|
: 'text-foreground/80 hover:bg-primary/5 hover:text-foreground'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
navExpanded
|
||||||
|
? `absolute left-0 top-1/2 -translate-y-1/2 h-6 w-1 rounded-r ${isActive ? 'bg-primary' : 'bg-transparent'}`
|
||||||
|
: 'hidden'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{navExpanded && <span className="truncate">{label}</span>}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border p-4">
|
<div className="rounded-lg border p-4">
|
||||||
<form onSubmit={handleSubmit} className="space-y-3">
|
<form onSubmit={handleSubmit} className="relative">
|
||||||
{/* Event-URL ganz oben */}
|
<div className="sticky top-0 z-20 -mx-4 px-4 py-2 border-b bg-background">
|
||||||
<div className="space-y-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Label htmlFor="ticketUrl">Event-URL</Label>
|
<div className="text-sm font-medium">
|
||||||
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-center">
|
{mode === 'edit' ? 'Event bearbeiten' : 'Event erstellen'}
|
||||||
<Input
|
|
||||||
id="ticketUrl"
|
|
||||||
name="ticketUrl"
|
|
||||||
type="text"
|
|
||||||
placeholder="https://..."
|
|
||||||
value={form.ticketUrl}
|
|
||||||
onChange={handleInput}
|
|
||||||
readOnly={mode === 'edit'}
|
|
||||||
/>
|
|
||||||
{mode === 'create' && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="shrink-0 flex items-center gap-2"
|
|
||||||
onClick={handleExtractFromUrl}
|
|
||||||
disabled={!form.ticketUrl || extractingFromUrl}
|
|
||||||
>
|
|
||||||
{extractingFromUrl && (
|
|
||||||
<span className="h-4 w-4 rounded-full border-2 border-current border-t-transparent animate-spin" />
|
|
||||||
)}
|
|
||||||
<span>{extractingFromUrl ? 'Lade…' : 'Daten aus URL laden'}</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{loadingInitial ? 'Lade…' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && <div className="text-sm text-destructive pt-1">{error}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex gap-4 pt-4">
|
||||||
<Label htmlFor="name">Name *</Label>
|
<aside className={`sticky top-20 self-start shrink-0 ${sidebarWidthClass} transition-[width] duration-200`}>
|
||||||
<Input id="name" name="name" value={form.name} onChange={handleInput} required />
|
<div className="rounded-2xl border bg-background p-2 shadow-sm">
|
||||||
</div>
|
{navExpanded && <div className="text-xs text-muted-foreground px-2 pb-2">Menü</div>}
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description">Beschreibung *</Label>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
value={form.description}
|
|
||||||
onChange={handleInput}
|
|
||||||
rows={6}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="begin_date">Datum *</Label>
|
<NavItemButton tab="stammdaten" label="Stammdaten" Icon={CalendarDays} />
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<NavItemButton tab="promotion" label="Promotion" Icon={Megaphone} />
|
||||||
<span>{formatDateForDisplay(form.begin_date) || 'Noch kein Datum gewählt'}</span>
|
<NavItemButton tab="artists" label="Künstler" Icon={Users} />
|
||||||
<Button
|
</div>
|
||||||
type="button"
|
</div>
|
||||||
variant="outline"
|
</aside>
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
<div className="flex-1 min-w-0">
|
||||||
if (beginDateRef.current) {
|
{activeTab === 'stammdaten' && (
|
||||||
if (beginDateRef.current.showPicker) {
|
<EventStammdatenSection
|
||||||
beginDateRef.current.showPicker();
|
form={form}
|
||||||
} else {
|
mode={mode}
|
||||||
beginDateRef.current.focus();
|
extractingFromUrl={extractingFromUrl}
|
||||||
}
|
onExtractFromUrl={handleExtractFromUrl}
|
||||||
}
|
onInput={handleInput}
|
||||||
|
beginDateRef={beginDateRef}
|
||||||
|
setForm={setForm}
|
||||||
|
formatDateForDisplay={formatDateForDisplay}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'promotion' && (
|
||||||
|
<PromotionTemplatesSection
|
||||||
|
promotionOffsetDays={promotionOffsetDays}
|
||||||
|
setPromotionOffsetDays={setPromotionOffsetDays}
|
||||||
|
promotionStartDate={promotionStartDate}
|
||||||
|
handlePromotionStartChange={handlePromotionStartChange}
|
||||||
|
promotionStartRef={promotionStartRef}
|
||||||
|
formatDateForDisplay={formatDateForDisplay}
|
||||||
|
mediaUrl={form.mediaUrl}
|
||||||
|
onMediaUrlChange={(url) => {
|
||||||
|
setForm((prev) => ({ ...prev, mediaUrl: url }));
|
||||||
|
setMediaPreview(url || null);
|
||||||
}}
|
}}
|
||||||
>
|
fileInputRef={fileInputRef}
|
||||||
Datum wählen
|
onFileSelected={(file) => {
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
ref={beginDateRef}
|
|
||||||
id="begin_date"
|
|
||||||
name="begin_date"
|
|
||||||
type="date"
|
|
||||||
className="hidden"
|
|
||||||
value={form.begin_date}
|
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, begin_date: e.target.value }))}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="admissionTimes">Einlasszeiten *</Label>
|
|
||||||
<Input
|
|
||||||
id="admissionTimes"
|
|
||||||
name="admissionTimes"
|
|
||||||
type="text"
|
|
||||||
placeholder="z.B. Einlass 19:00, Beginn 20:00"
|
|
||||||
value={form.admissionTimes}
|
|
||||||
onChange={handleInput}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="ticketPrice">Ticketpreis *</Label>
|
|
||||||
<Input
|
|
||||||
id="ticketPrice"
|
|
||||||
name="ticketPrice"
|
|
||||||
type="text"
|
|
||||||
placeholder="z.B. 10 € / 8 € erm."
|
|
||||||
value={form.ticketPrice}
|
|
||||||
onChange={handleInput}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 pt-4 mt-4 border-t">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label>Promotion</Label>
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
<span>Letzter Post vor Event</span>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
className="h-7 w-16 text-xs"
|
|
||||||
value={promotionOffsetDays}
|
|
||||||
onChange={(e) => setPromotionOffsetDays(e.target.value)}
|
|
||||||
/>
|
|
||||||
<span>Tage</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sektion 1: Event-Promotion (nur Datum) */}
|
|
||||||
<div className="space-y-3 rounded-md border p-3">
|
|
||||||
<div className="text-sm font-medium">Sektion 1 – Event-Promotion</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<span>{formatDateForDisplay(promotionStartDate) || 'Noch kein Datum gewählt'}</span>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (promotionStartRef.current) {
|
|
||||||
if (promotionStartRef.current.showPicker) {
|
|
||||||
promotionStartRef.current.showPicker();
|
|
||||||
} else {
|
|
||||||
promotionStartRef.current.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Datum wählen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
ref={promotionStartRef}
|
|
||||||
id="promotion_start"
|
|
||||||
name="promotion_start"
|
|
||||||
type="date"
|
|
||||||
className="hidden"
|
|
||||||
value={promotionStartDate}
|
|
||||||
onChange={(e) => handlePromotionStartChange(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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={form.mediaUrl}
|
|
||||||
onChange={(e) => {
|
|
||||||
setForm({ ...form, mediaUrl: e.target.value });
|
|
||||||
setMediaPreview(e.target.value || null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<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;
|
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
setMediaPreview(url);
|
setMediaPreview(url);
|
||||||
}}
|
}}
|
||||||
|
mediaPreview={mediaPreview}
|
||||||
/>
|
/>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Sektion 2–N: Künstler 1–N */}
|
{activeTab === 'artists' && (
|
||||||
{artistSections.map((section, idx) => {
|
<ArtistsSection
|
||||||
const slot = promotionSlots?.find((s) => s.slotIndex === idx + 2);
|
mode={mode}
|
||||||
const hasArtistData = !!slot?.artistDescription || (Array.isArray(slot?.media) && slot.media.length > 0);
|
eventId={eventId}
|
||||||
const canOpenPreview = mode === 'edit' && !!slot && hasArtistData;
|
artistSections={artistSections}
|
||||||
|
setArtistSections={setArtistSections}
|
||||||
return (
|
sectionDates={sectionDates}
|
||||||
<div key={idx} className="space-y-2 rounded-md border p-3">
|
setSectionDates={setSectionDates}
|
||||||
<div className="flex items-center justify-between text-sm font-medium">
|
sectionDateRefs={sectionDateRefs}
|
||||||
<span>{`Sektion ${idx + 2} – Künstler ${idx + 1}`}</span>
|
promotionSlots={promotionSlots}
|
||||||
<span className="text-xs text-muted-foreground">{artistSections[idx].status}</span>
|
generatingLinkIndex={generatingLinkIndex}
|
||||||
</div>
|
setGeneratingLinkIndex={setGeneratingLinkIndex}
|
||||||
<div className="space-y-3">
|
generateCollabLink={generateCollabLink}
|
||||||
<div className="space-y-1">
|
setError={setError}
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
formatDateForDisplay={formatDateForDisplay}
|
||||||
<span>{formatDateForDisplay(sectionDates[idx + 1]) || 'Noch kein Datum gewählt'}</span>
|
onOpenPreview={(slot) => {
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
const ref = sectionDateRefs.current[idx + 1];
|
|
||||||
if (ref) {
|
|
||||||
if (ref.showPicker) {
|
|
||||||
ref.showPicker();
|
|
||||||
} else {
|
|
||||||
ref.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Datum wählen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
ref={(el) => (sectionDateRefs.current[idx + 1] = el)}
|
|
||||||
id={`section_date_${idx + 2}`}
|
|
||||||
type="date"
|
|
||||||
className="hidden"
|
|
||||||
value={sectionDates[idx + 1]}
|
|
||||||
onChange={(e) => {
|
|
||||||
const next = [...sectionDates];
|
|
||||||
next[idx + 1] = e.target.value;
|
|
||||||
setSectionDates(next);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label htmlFor={`artist_name_${idx}`}>Künstlername *</Label>
|
|
||||||
<Input
|
|
||||||
id={`artist_name_${idx}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Name des Künstlers"
|
|
||||||
value={section.name}
|
|
||||||
onChange={(e) => {
|
|
||||||
const next = [...artistSections];
|
|
||||||
next[idx] = { ...next[idx], name: e.target.value };
|
|
||||||
setArtistSections(next);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={async () => {
|
|
||||||
if (mode !== 'edit' || !eventId) {
|
|
||||||
setError('Bitte Event zuerst speichern, bevor Links generiert werden.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setGeneratingLinkIndex(idx);
|
|
||||||
const raw = await generateCollabLink(eventId, idx + 2, section.name, section.email || '');
|
|
||||||
|
|
||||||
const res: any = (() => {
|
|
||||||
if (!raw) return null;
|
|
||||||
if (Array.isArray(raw)) {
|
|
||||||
return raw[0] ?? null;
|
|
||||||
}
|
|
||||||
if (typeof raw === 'string') {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
return Array.isArray(parsed) ? parsed[0] ?? null : parsed;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return raw as any;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const link = res?.url || (res?.token ? `/collab/${res.token}` : '');
|
|
||||||
if (!link) {
|
|
||||||
setError('Kollaborationslink konnte nicht erzeugt werden.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const next = [...artistSections];
|
|
||||||
next[idx] = { ...next[idx], link, status: 'Link generiert' };
|
|
||||||
setArtistSections(next);
|
|
||||||
} catch (e) {
|
|
||||||
setError('Kollaborationslink konnte nicht erzeugt werden.');
|
|
||||||
} finally {
|
|
||||||
setGeneratingLinkIndex((current) => (current === idx ? null : current));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!section.name || !eventId || mode !== 'edit' || generatingLinkIndex === idx}
|
|
||||||
>
|
|
||||||
{generatingLinkIndex === idx && (
|
|
||||||
<span className="h-3 w-3 mr-1 rounded-full border-2 border-current border-t-transparent animate-spin" />
|
|
||||||
)}
|
|
||||||
Link generieren
|
|
||||||
</Button>
|
|
||||||
{section.link && (
|
|
||||||
<div className="flex flex-1 items-center gap-2 w-full">
|
|
||||||
<Input
|
|
||||||
readOnly
|
|
||||||
className="text-xs flex-1"
|
|
||||||
value={section.link}
|
|
||||||
onFocus={(e) => e.target.select()}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
await navigator.clipboard.writeText(section.link || '');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Clipboard copy failed', e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Kopieren
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-1">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={!canOpenPreview}
|
|
||||||
onClick={() => {
|
|
||||||
if (!slot) return;
|
|
||||||
setPreviewSlot(slot);
|
setPreviewSlot(slot);
|
||||||
setPreviewDraft(slot ? { ...slot } : null);
|
setPreviewDraft(slot ? { ...slot } : null);
|
||||||
setIsPreviewEditing(false);
|
setIsPreviewEditing(false);
|
||||||
setIsPreviewOpen(true);
|
setIsPreviewOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
Künstlerdaten ansehen
|
)}
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 pt-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setArtistSections((prev) => {
|
|
||||||
const next = [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
link: '',
|
|
||||||
status: 'Wartet auf Künstler' as 'Wartet auf Künstler' | 'Link generiert' | 'Fertig' | 'Künstler einladen',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// sectionDates wird über useEffect/recalcPromotionDatesFromEvent anhand der neuen Länge neu berechnet
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Künstler hinzufügen
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={artistSections.length <= 1}
|
|
||||||
onClick={() => {
|
|
||||||
if (artistSections.length <= 1) return;
|
|
||||||
setArtistSections((prev) => prev.slice(0, -1));
|
|
||||||
setSectionDates((prev) => {
|
|
||||||
if (prev.length <= 2) return prev;
|
|
||||||
return prev.slice(0, -1);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Letzten Künstler entfernen
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className="text-sm text-destructive">{error}</div>}
|
<div className="sticky bottom-0 z-20 -mx-4 mt-4 px-4 py-3 border-t bg-background">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="text-xs text-muted-foreground pt-1">* Pflichtfelder</div>
|
<div className="text-xs text-muted-foreground">* Pflichtfelder</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-col sm:flex-row gap-2 pt-2">
|
|
||||||
<Button type="submit" className="flex-1" disabled={isSaveDisabled}>
|
|
||||||
Speichern
|
|
||||||
</Button>
|
|
||||||
{onCancel && (
|
{onCancel && (
|
||||||
<Button type="button" variant="outline" className="flex-1" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button type="submit" disabled={isSaveDisabled}>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
141
src/features/event/components/EventStammdatenSection.tsx
Normal file
141
src/features/event/components/EventStammdatenSection.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|
||||||
|
type FormState = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
begin_date: string;
|
||||||
|
mediaUrl: string;
|
||||||
|
ticketUrl: string;
|
||||||
|
admissionTimes: string;
|
||||||
|
ticketPrice: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
form: FormState;
|
||||||
|
mode: 'create' | 'edit';
|
||||||
|
extractingFromUrl: boolean;
|
||||||
|
onExtractFromUrl: () => void;
|
||||||
|
onInput: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
|
beginDateRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
setForm: React.Dispatch<React.SetStateAction<FormState>>;
|
||||||
|
formatDateForDisplay: (iso: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventStammdatenSection: React.FC<Props> = ({
|
||||||
|
form,
|
||||||
|
mode,
|
||||||
|
extractingFromUrl,
|
||||||
|
onExtractFromUrl,
|
||||||
|
onInput,
|
||||||
|
beginDateRef,
|
||||||
|
setForm,
|
||||||
|
formatDateForDisplay,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ticketUrl">Event-URL</Label>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-center">
|
||||||
|
<Input
|
||||||
|
id="ticketUrl"
|
||||||
|
name="ticketUrl"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://..."
|
||||||
|
value={form.ticketUrl}
|
||||||
|
onChange={onInput}
|
||||||
|
readOnly={mode === 'edit'}
|
||||||
|
/>
|
||||||
|
{mode === 'create' && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="shrink-0 flex items-center gap-2"
|
||||||
|
onClick={onExtractFromUrl}
|
||||||
|
disabled={!form.ticketUrl || extractingFromUrl}
|
||||||
|
>
|
||||||
|
{extractingFromUrl && (
|
||||||
|
<span className="h-4 w-4 rounded-full border-2 border-current border-t-transparent animate-spin" />
|
||||||
|
)}
|
||||||
|
<span>{extractingFromUrl ? 'Lade…' : 'Daten aus URL laden'}</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Name *</Label>
|
||||||
|
<Input id="name" name="name" value={form.name} onChange={onInput} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">Beschreibung *</Label>
|
||||||
|
<Textarea id="description" name="description" value={form.description} onChange={onInput} rows={6} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="begin_date">Datum *</Label>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>{formatDateForDisplay(form.begin_date) || 'Noch kein Datum gewählt'}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (beginDateRef.current) {
|
||||||
|
if (beginDateRef.current.showPicker) {
|
||||||
|
beginDateRef.current.showPicker();
|
||||||
|
} else {
|
||||||
|
beginDateRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Datum wählen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
ref={beginDateRef}
|
||||||
|
id="begin_date"
|
||||||
|
name="begin_date"
|
||||||
|
type="date"
|
||||||
|
className="hidden"
|
||||||
|
value={form.begin_date}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, begin_date: e.target.value }))}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="admissionTimes">Einlasszeiten *</Label>
|
||||||
|
<Input
|
||||||
|
id="admissionTimes"
|
||||||
|
name="admissionTimes"
|
||||||
|
type="text"
|
||||||
|
placeholder="z.B. Einlass 19:00, Beginn 20:00"
|
||||||
|
value={form.admissionTimes}
|
||||||
|
onChange={onInput}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ticketPrice">Ticketpreis *</Label>
|
||||||
|
<Input
|
||||||
|
id="ticketPrice"
|
||||||
|
name="ticketPrice"
|
||||||
|
type="text"
|
||||||
|
placeholder="z.B. 10 € / 8 € erm."
|
||||||
|
value={form.ticketPrice}
|
||||||
|
onChange={onInput}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventStammdatenSection;
|
||||||
127
src/features/event/components/PromotionTemplatesSection.tsx
Normal file
127
src/features/event/components/PromotionTemplatesSection.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
promotionOffsetDays: string;
|
||||||
|
setPromotionOffsetDays: (v: string) => void;
|
||||||
|
promotionStartDate: string;
|
||||||
|
handlePromotionStartChange: (iso: string) => void;
|
||||||
|
promotionStartRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
formatDateForDisplay: (iso: string) => string;
|
||||||
|
|
||||||
|
mediaUrl: string;
|
||||||
|
onMediaUrlChange: (url: string) => void;
|
||||||
|
|
||||||
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
onFileSelected: (file: File) => void;
|
||||||
|
|
||||||
|
mediaPreview: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PromotionTemplatesSection: React.FC<Props> = ({
|
||||||
|
promotionOffsetDays,
|
||||||
|
setPromotionOffsetDays,
|
||||||
|
promotionStartDate,
|
||||||
|
handlePromotionStartChange,
|
||||||
|
promotionStartRef,
|
||||||
|
formatDateForDisplay,
|
||||||
|
mediaUrl,
|
||||||
|
onMediaUrlChange,
|
||||||
|
fileInputRef,
|
||||||
|
onFileSelected,
|
||||||
|
mediaPreview,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">Promotion Templates</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<span>Letzter Post vor Event</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
className="h-7 w-16 text-xs"
|
||||||
|
value={promotionOffsetDays}
|
||||||
|
onChange={(e) => setPromotionOffsetDays(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span>Tage</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 rounded-md border p-3">
|
||||||
|
<div className="text-sm font-medium">Sektion 1 – Event-Promotion</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>{formatDateForDisplay(promotionStartDate) || 'Noch kein Datum gewählt'}</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (promotionStartRef.current) {
|
||||||
|
if (promotionStartRef.current.showPicker) {
|
||||||
|
promotionStartRef.current.showPicker();
|
||||||
|
} else {
|
||||||
|
promotionStartRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Datum wählen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
ref={promotionStartRef}
|
||||||
|
id="promotion_start"
|
||||||
|
name="promotion_start"
|
||||||
|
type="date"
|
||||||
|
className="hidden"
|
||||||
|
value={promotionStartDate}
|
||||||
|
onChange={(e) => handlePromotionStartChange(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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="text-xs text-muted-foreground">
|
||||||
|
Dieser Bereich wird später konkretisiert (Templates, Textbausteine, Scheduling etc.).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromotionTemplatesSection;
|
||||||
Loading…
x
Reference in New Issue
Block a user