From cf8af35f9fceadd2c0dddb90705bf7a25414d5e4 Mon Sep 17 00:00:00 2001 From: martin Date: Tue, 27 Jan 2026 15:18:52 +0100 Subject: [PATCH] mobile template api integration #deploy --- src/features/event/api/templatesService.ts | 68 +++++++++ .../components/PromotionTemplatesSection.tsx | 135 ++++++++++++------ 2 files changed, 157 insertions(+), 46 deletions(-) create mode 100644 src/features/event/api/templatesService.ts diff --git a/src/features/event/api/templatesService.ts b/src/features/event/api/templatesService.ts new file mode 100644 index 0000000..dbb8e3e --- /dev/null +++ b/src/features/event/api/templatesService.ts @@ -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 => { + 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 => { + 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[]) : []; +}; diff --git a/src/features/event/components/PromotionTemplatesSection.tsx b/src/features/event/components/PromotionTemplatesSection.tsx index e9f8dca..0a0f6d0 100644 --- a/src/features/event/components/PromotionTemplatesSection.tsx +++ b/src/features/event/components/PromotionTemplatesSection.tsx @@ -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 = ({ 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([]); + const [loadingTemplates, setLoadingTemplates] = useState(false); - const [eventPromoTemplateId, setEventPromoTemplateId] = useState(templates[0]?.id ?? ''); - const [eventLineupTemplateId, setEventLineupTemplateId] = useState(templates[2]?.id ?? ''); - const [artistPromoTemplateId, setArtistPromoTemplateId] = useState(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(''); + const [eventLineupTemplateId, setEventLineupTemplateId] = useState(''); + const [artistPromoTemplateId, setArtistPromoTemplateId] = useState(''); const importZipInputRef = useRef(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 = ({ 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 = ({ variant="outline" className="w-full justify-between" onClick={() => setOpen(true)} + disabled={loadingTemplates || templates.length === 0} > {selected?.name || 'Template wählen'} Auswählen @@ -201,6 +236,9 @@ const PromotionTemplatesSection: React.FC = ({
{selected?.description || ''}
+ {!loadingTemplates && templates.length === 0 && ( +
Keine Templates verfügbar
+ )} @@ -209,7 +247,10 @@ const PromotionTemplatesSection: React.FC = ({ return (
-
Promotion Templates
+
+
Promotion Templates
+
{loadingTemplates ? 'Lade…' : ''}
+
Sektion 1 – Event-Promotion
@@ -219,6 +260,7 @@ const PromotionTemplatesSection: React.FC = ({ value={eventPromoTemplateId} onValueChange={setEventPromoTemplateId} selected={selectedEventPromo} + templates={eventPromoTemplates} />
@@ -261,6 +303,7 @@ const PromotionTemplatesSection: React.FC = ({ value={eventLineupTemplateId} onValueChange={setEventLineupTemplateId} selected={selectedEventLineup} + templates={eventLineupTemplates} />
@@ -271,11 +314,11 @@ const PromotionTemplatesSection: React.FC = ({ value={artistPromoTemplateId} onValueChange={setArtistPromoTemplateId} selected={selectedArtistPromo} + templates={artistPromoTemplates} />
- {/*
Templates verwalten
*/}