new desighn #deploy

This commit is contained in:
martin 2026-01-20 17:07:09 +01:00
parent 98b8abd56c
commit 9067a4c6c0
4 changed files with 663 additions and 411 deletions

View 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;

View File

@ -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 </div>
id="ticketUrl" <div className="text-xs text-muted-foreground">
name="ticketUrl" {loadingInitial ? 'Lade…' : ''}
type="text" </div>
placeholder="https://..." </div>
value={form.ticketUrl} {error && <div className="text-sm text-destructive pt-1">{error}</div>}
onChange={handleInput} </div>
readOnly={mode === 'edit'}
/> <div className="flex gap-4 pt-4">
{mode === 'create' && ( <aside className={`sticky top-20 self-start shrink-0 ${sidebarWidthClass} transition-[width] duration-200`}>
<Button <div className="rounded-2xl border bg-background p-2 shadow-sm">
type="button" {navExpanded && <div className="text-xs text-muted-foreground px-2 pb-2">Menü</div>}
variant="outline"
className="shrink-0 flex items-center gap-2" <div className="space-y-1">
onClick={handleExtractFromUrl} <NavItemButton tab="stammdaten" label="Stammdaten" Icon={CalendarDays} />
disabled={!form.ticketUrl || extractingFromUrl} <NavItemButton tab="promotion" label="Promotion" Icon={Megaphone} />
> <NavItemButton tab="artists" label="Künstler" Icon={Users} />
{extractingFromUrl && ( </div>
<span className="h-4 w-4 rounded-full border-2 border-current border-t-transparent animate-spin" /> </div>
)} </aside>
<span>{extractingFromUrl ? 'Lade…' : 'Daten aus URL laden'}</span>
</Button> <div className="flex-1 min-w-0">
{activeTab === 'stammdaten' && (
<EventStammdatenSection
form={form}
mode={mode}
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}
onFileSelected={(file) => {
const url = URL.createObjectURL(file);
setMediaPreview(url);
}}
mediaPreview={mediaPreview}
/>
)}
{activeTab === 'artists' && (
<ArtistsSection
mode={mode}
eventId={eventId}
artistSections={artistSections}
setArtistSections={setArtistSections}
sectionDates={sectionDates}
setSectionDates={setSectionDates}
sectionDateRefs={sectionDateRefs}
promotionSlots={promotionSlots}
generatingLinkIndex={generatingLinkIndex}
setGeneratingLinkIndex={setGeneratingLinkIndex}
generateCollabLink={generateCollabLink}
setError={setError}
formatDateForDisplay={formatDateForDisplay}
onOpenPreview={(slot) => {
setPreviewSlot(slot);
setPreviewDraft(slot ? { ...slot } : null);
setIsPreviewEditing(false);
setIsPreviewOpen(true);
}}
/>
)} )}
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="sticky bottom-0 z-20 -mx-4 mt-4 px-4 py-3 border-t bg-background">
<Label htmlFor="name">Name *</Label> <div className="flex items-center justify-between gap-3">
<Input id="name" name="name" value={form.name} onChange={handleInput} required /> <div className="text-xs text-muted-foreground">* Pflichtfelder</div>
</div> <div className="flex gap-2">
{onCancel && (
<div className="space-y-2"> <Button type="button" variant="outline" onClick={onCancel}>
<Label htmlFor="description">Beschreibung *</Label> Abbrechen
<Textarea
id="description"
name="description"
value={form.description}
onChange={handleInput}
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={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> </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);
setMediaPreview(url);
}}
/>
<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>
)} )}
<Button type="submit" disabled={isSaveDisabled}>
Speichern
</Button>
</div> </div>
</div> </div>
{/* Sektion 2N: Künstler 1N */}
{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>{`Sektion ${idx + 2} 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={() => {
if (!slot) return;
setPreviewSlot(slot);
setPreviewDraft(slot ? { ...slot } : null);
setIsPreviewEditing(false);
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>
{error && <div className="text-sm text-destructive">{error}</div>}
<div className="text-xs text-muted-foreground pt-1">* Pflichtfelder</div>
<div className="flex flex-col sm:flex-row gap-2 pt-2">
<Button type="submit" className="flex-1" disabled={isSaveDisabled}>
Speichern
</Button>
{onCancel && (
<Button type="button" variant="outline" className="flex-1" onClick={onCancel}>
Abbrechen
</Button>
)}
</div> </div>
</form> </form>

View 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;

View 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;