mobile test5
#deploy
This commit is contained in:
parent
af78237985
commit
882df7cb17
@ -649,12 +649,6 @@ const EventFormInline: React.FC<EventFormInlineProps> = ({ onSuccess, onCancel,
|
|||||||
|
|
||||||
{activeTab === 'promotion' && (
|
{activeTab === 'promotion' && (
|
||||||
<PromotionTemplatesSection
|
<PromotionTemplatesSection
|
||||||
promotionOffsetDays={promotionOffsetDays}
|
|
||||||
setPromotionOffsetDays={setPromotionOffsetDays}
|
|
||||||
promotionStartDate={promotionStartDate}
|
|
||||||
handlePromotionStartChange={handlePromotionStartChange}
|
|
||||||
promotionStartRef={promotionStartRef}
|
|
||||||
formatDateForDisplay={formatDateForDisplay}
|
|
||||||
mediaUrl={form.mediaUrl}
|
mediaUrl={form.mediaUrl}
|
||||||
onMediaUrlChange={(url) => {
|
onMediaUrlChange={(url) => {
|
||||||
setForm((prev) => ({ ...prev, mediaUrl: url }));
|
setForm((prev) => ({ ...prev, mediaUrl: url }));
|
||||||
@ -693,7 +687,16 @@ const EventFormInline: React.FC<EventFormInlineProps> = ({ onSuccess, onCancel,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'schedule' && <ScheduleSection />}
|
{activeTab === 'schedule' && (
|
||||||
|
<ScheduleSection
|
||||||
|
promotionOffsetDays={promotionOffsetDays}
|
||||||
|
setPromotionOffsetDays={setPromotionOffsetDays}
|
||||||
|
promotionStartDate={promotionStartDate}
|
||||||
|
handlePromotionStartChange={handlePromotionStartChange}
|
||||||
|
promotionStartRef={promotionStartRef}
|
||||||
|
formatDateForDisplay={formatDateForDisplay}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useMemo, useRef, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
type Props = {
|
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;
|
mediaUrl: string;
|
||||||
onMediaUrlChange: (url: string) => void;
|
onMediaUrlChange: (url: string) => void;
|
||||||
|
|
||||||
@ -20,69 +14,151 @@ type Props = {
|
|||||||
mediaPreview: string | null;
|
mediaPreview: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PromotionTemplate = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
previewUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const PromotionTemplatesSection: React.FC<Props> = ({
|
const PromotionTemplatesSection: React.FC<Props> = ({
|
||||||
promotionOffsetDays,
|
|
||||||
setPromotionOffsetDays,
|
|
||||||
promotionStartDate,
|
|
||||||
handlePromotionStartChange,
|
|
||||||
promotionStartRef,
|
|
||||||
formatDateForDisplay,
|
|
||||||
mediaUrl,
|
mediaUrl,
|
||||||
onMediaUrlChange,
|
onMediaUrlChange,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
onFileSelected,
|
onFileSelected,
|
||||||
mediaPreview,
|
mediaPreview,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
const templates: PromotionTemplate[] = useMemo(
|
||||||
<div className="space-y-4">
|
() => [
|
||||||
<div className="flex items-center justify-between">
|
{
|
||||||
<div className="text-sm font-medium">Promotion Templates</div>
|
id: 'tpl_event_minimal',
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
name: 'Event Minimal',
|
||||||
<span>Letzter Post vor Event</span>
|
description: 'Cleanes Event-Template mit Fokus auf Titel + Datum.',
|
||||||
<Input
|
previewUrl: '/images/templates/event_minimal_preview.png',
|
||||||
type="number"
|
},
|
||||||
min={1}
|
{
|
||||||
className="h-7 w-16 text-xs"
|
id: 'tpl_event_bold',
|
||||||
value={promotionOffsetDays}
|
name: 'Event Bold',
|
||||||
onChange={(e) => setPromotionOffsetDays(e.target.value)}
|
description: 'Große Typo, geeignet für starke Keyvisuals.',
|
||||||
/>
|
previewUrl: '/images/templates/event_bold_preview.png',
|
||||||
<span>Tage</span>
|
},
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{label}</Label>
|
||||||
|
<div className="grid gap-3 md:grid-cols-[1fr,280px] items-start">
|
||||||
|
<Select value={value} onValueChange={onValueChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Template wählen" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{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 className="text-xs text-muted-foreground leading-tight line-clamp-2">{tpl.description}</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">{selected?.description || ''}</div>
|
||||||
|
</div>
|
||||||
|
</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="space-y-3 rounded-md border p-3">
|
||||||
<div className="text-sm font-medium">Sektion 1 – Event-Promotion</div>
|
<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">
|
<TemplateSelect
|
||||||
<span>{formatDateForDisplay(promotionStartDate) || 'Noch kein Datum gewählt'}</span>
|
label="Template"
|
||||||
<Button
|
value={eventPromoTemplateId}
|
||||||
type="button"
|
onValueChange={setEventPromoTemplateId}
|
||||||
variant="outline"
|
selected={selectedEventPromo}
|
||||||
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">
|
<div className="space-y-2 pt-2">
|
||||||
<Label>Bild / Video</Label>
|
<Label>Bild / Video</Label>
|
||||||
@ -117,6 +193,52 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
</div>
|
</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">
|
<div className="text-xs text-muted-foreground">
|
||||||
Dieser Bereich wird später konkretisiert (Templates, Textbausteine, Scheduling etc.).
|
Dieser Bereich wird später konkretisiert (Templates, Textbausteine, Scheduling etc.).
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,10 +1,79 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
|
||||||
const ScheduleSection: React.FC = () => {
|
type Props = {
|
||||||
|
promotionOffsetDays: string;
|
||||||
|
setPromotionOffsetDays: (v: string) => void;
|
||||||
|
promotionStartDate: string;
|
||||||
|
handlePromotionStartChange: (iso: string) => void;
|
||||||
|
promotionStartRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
formatDateForDisplay: (iso: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScheduleSection: React.FC<Props> = ({
|
||||||
|
promotionOffsetDays,
|
||||||
|
setPromotionOffsetDays,
|
||||||
|
promotionStartDate,
|
||||||
|
handlePromotionStartChange,
|
||||||
|
promotionStartRef,
|
||||||
|
formatDateForDisplay,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-sm font-medium">Zeitplan</div>
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-sm text-muted-foreground">Hier kommt später alles rund um Scheduling / Zeitplan rein.</div>
|
<div className="text-sm font-medium">Zeitplan</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">Promotion Start (Event Promo)</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="text-xs text-muted-foreground">
|
||||||
|
Dieses Datum ist der Start für die Event-Promo (Slot 1). Die restlichen Promo-Slots werden daraus abgeleitet.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -31,8 +31,8 @@
|
|||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214 22% 86%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214 22% 86%;
|
||||||
--ring: 200 100% 45%;
|
--ring: 200 100% 45%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
@ -43,7 +43,7 @@
|
|||||||
--sidebar-primary-foreground: 210 40% 98%;
|
--sidebar-primary-foreground: 210 40% 98%;
|
||||||
--sidebar-accent: 210 40% 96.1%;
|
--sidebar-accent: 210 40% 96.1%;
|
||||||
--sidebar-accent-foreground: 220 40% 15%;
|
--sidebar-accent-foreground: 220 40% 15%;
|
||||||
--sidebar-border: 214.3 31.8% 91.4%;
|
--sidebar-border: 214 22% 86%;
|
||||||
--sidebar-ring: 200 100% 45%;
|
--sidebar-ring: 200 100% 45%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +72,8 @@
|
|||||||
--destructive: 0 84.2% 62%;
|
--destructive: 0 84.2% 62%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 215 20% 28%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 215 20% 28%;
|
||||||
--ring: 200 100% 45%;
|
--ring: 200 100% 45%;
|
||||||
|
|
||||||
--sidebar-background: 220 40% 17%;
|
--sidebar-background: 220 40% 17%;
|
||||||
@ -82,7 +82,7 @@
|
|||||||
--sidebar-primary-foreground: 210 40% 98%;
|
--sidebar-primary-foreground: 210 40% 98%;
|
||||||
--sidebar-accent: 217.2 32.6% 17.5%;
|
--sidebar-accent: 217.2 32.6% 17.5%;
|
||||||
--sidebar-accent-foreground: 210 40% 98%;
|
--sidebar-accent-foreground: 210 40% 98%;
|
||||||
--sidebar-border: 217.2 32.6% 17.5%;
|
--sidebar-border: 215 20% 28%;
|
||||||
--sidebar-ring: 200 100% 45%;
|
--sidebar-ring: 200 100% 45%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user