mobile template api integration
#deploy
This commit is contained in:
parent
7715cbfb6f
commit
cf8af35f9f
68
src/features/event/api/templatesService.ts
Normal file
68
src/features/event/api/templatesService.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { getWebhookUrl } from '@/lib/config';
|
||||
|
||||
export type TemplateUploadResult = {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type TemplateRecord = {
|
||||
id: string | number;
|
||||
name: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
preview_img?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export const uploadTemplateZip = async (apiFetch: Function, file: File): Promise<TemplateUploadResult> => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await apiFetch(`${getWebhookUrl()}/template`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
skipAuthHandling: true,
|
||||
});
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
const raw = await response.json();
|
||||
const data = Array.isArray(raw) ? raw[0] ?? {} : raw ?? {};
|
||||
return {
|
||||
success: data.success !== false,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const trimmed = (text || '').trim();
|
||||
return {
|
||||
success: true,
|
||||
message: trimmed || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchTemplates = async (apiFetch: Function): Promise<TemplateRecord[]> => {
|
||||
const response = await apiFetch(`${getWebhookUrl()}/templates`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
skipAuthHandling: true,
|
||||
});
|
||||
|
||||
const raw = await response.json();
|
||||
|
||||
const data: any =
|
||||
typeof raw === 'string'
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
})()
|
||||
: raw;
|
||||
|
||||
return Array.isArray(data) ? (data as TemplateRecord[]) : data ? ([data] as TemplateRecord[]) : [];
|
||||
};
|
||||
@ -1,10 +1,13 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, 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';
|
||||
import { useApiFetch } from '@/utils/apiFetch';
|
||||
import { fetchTemplates, uploadTemplateZip } from '@/features/event/api/templatesService';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type Props = {
|
||||
mediaUrl: string;
|
||||
@ -20,6 +23,7 @@ type PromotionTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type?: string;
|
||||
previewUrl?: string;
|
||||
};
|
||||
|
||||
@ -31,58 +35,86 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
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 apiFetch = useApiFetch();
|
||||
const [allTemplates, setAllTemplates] = useState<PromotionTemplate[]>([]);
|
||||
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||
|
||||
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 eventPromoTemplates = useMemo(() => allTemplates.filter((t) => t.type === 'event_promo' || !t.type), [allTemplates]);
|
||||
const eventLineupTemplates = useMemo(() => allTemplates.filter((t) => t.type === 'event_lineup'), [allTemplates]);
|
||||
const artistPromoTemplates = useMemo(() => allTemplates.filter((t) => t.type === 'artist_promo'), [allTemplates]);
|
||||
|
||||
const [eventPromoTemplateId, setEventPromoTemplateId] = useState<string>('');
|
||||
const [eventLineupTemplateId, setEventLineupTemplateId] = useState<string>('');
|
||||
const [artistPromoTemplateId, setArtistPromoTemplateId] = useState<string>('');
|
||||
|
||||
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 selectedEventPromo = allTemplates.find((t) => t.id === eventPromoTemplateId) || null;
|
||||
const selectedEventLineup = allTemplates.find((t) => t.id === eventLineupTemplateId) || null;
|
||||
const selectedArtistPromo = allTemplates.find((t) => t.id === artistPromoTemplateId) || null;
|
||||
|
||||
const loadTemplates = useCallback(async () => {
|
||||
setLoadingTemplates(true);
|
||||
try {
|
||||
const list = await fetchTemplates(apiFetch);
|
||||
|
||||
const mapped: PromotionTemplate[] = list.map((t: any) => ({
|
||||
id: String(t.id),
|
||||
name: String(t.name || ''),
|
||||
description: String(t.description || ''),
|
||||
type: typeof t.type === 'string' ? t.type : undefined,
|
||||
previewUrl: typeof t.preview_img === 'string' ? t.preview_img : undefined,
|
||||
}));
|
||||
|
||||
setAllTemplates(mapped);
|
||||
|
||||
// Initial defaults if nothing selected yet
|
||||
if (!eventPromoTemplateId && (mapped.find((x) => x.type === 'event_promo') || mapped[0])) {
|
||||
const firstEvent = mapped.find((x) => x.type === 'event_promo') || mapped[0];
|
||||
if (firstEvent?.id) setEventPromoTemplateId(firstEvent.id);
|
||||
}
|
||||
if (!eventLineupTemplateId) {
|
||||
const firstLineup = mapped.find((x) => x.type === 'event_lineup');
|
||||
if (firstLineup?.id) setEventLineupTemplateId(firstLineup.id);
|
||||
}
|
||||
if (!artistPromoTemplateId) {
|
||||
const firstArtist = mapped.find((x) => x.type === 'artist_promo');
|
||||
if (firstArtist?.id) setArtistPromoTemplateId(firstArtist.id);
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || 'Templates konnten nicht geladen werden');
|
||||
setAllTemplates([]);
|
||||
} finally {
|
||||
setLoadingTemplates(false);
|
||||
}
|
||||
}, [apiFetch, artistPromoTemplateId, eventLineupTemplateId, eventPromoTemplateId]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
if (cancelled) return;
|
||||
await loadTemplates();
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [loadTemplates]);
|
||||
|
||||
const handleImportZip = async (file: File) => {
|
||||
setImportingTemplate(true);
|
||||
try {
|
||||
// Placeholder: Upload to backend endpoint will be added later
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
const result = await uploadTemplateZip(apiFetch, file);
|
||||
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.message || 'Template-Import fehlgeschlagen');
|
||||
}
|
||||
|
||||
toast.success(result?.message || 'Template importiert');
|
||||
await loadTemplates();
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || 'Template-Import fehlgeschlagen';
|
||||
toast.error(msg);
|
||||
} finally {
|
||||
setImportingTemplate(false);
|
||||
}
|
||||
@ -93,11 +125,13 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
value,
|
||||
onValueChange,
|
||||
selected,
|
||||
templates,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onValueChange: (next: string) => void;
|
||||
selected: PromotionTemplate | null;
|
||||
templates: PromotionTemplate[];
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@ -112,6 +146,7 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
variant="outline"
|
||||
className="w-full justify-between"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={loadingTemplates || templates.length === 0}
|
||||
>
|
||||
<span className="truncate">{selected?.name || 'Template wählen'}</span>
|
||||
<span className="text-muted-foreground">Auswählen</span>
|
||||
@ -201,6 +236,9 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
<div className="mt-1 text-xs text-muted-foreground leading-snug hidden md:block">{selected?.description || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
{!loadingTemplates && templates.length === 0 && (
|
||||
<div className="mt-2 text-xs text-muted-foreground">Keine Templates verfügbar</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -209,7 +247,10 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm font-medium">Promotion Templates</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-medium">Promotion Templates</div>
|
||||
<div className="text-xs text-muted-foreground">{loadingTemplates ? 'Lade…' : ''}</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-md border p-3">
|
||||
<div className="text-sm font-medium">Sektion 1 – Event-Promotion</div>
|
||||
@ -219,6 +260,7 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
value={eventPromoTemplateId}
|
||||
onValueChange={setEventPromoTemplateId}
|
||||
selected={selectedEventPromo}
|
||||
templates={eventPromoTemplates}
|
||||
/>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
@ -261,6 +303,7 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
value={eventLineupTemplateId}
|
||||
onValueChange={setEventLineupTemplateId}
|
||||
selected={selectedEventLineup}
|
||||
templates={eventLineupTemplates}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -271,11 +314,11 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
||||
value={artistPromoTemplateId}
|
||||
onValueChange={setArtistPromoTemplateId}
|
||||
selected={selectedArtistPromo}
|
||||
templates={artistPromoTemplates}
|
||||
/>
|
||||
</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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user