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 { 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';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
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 = {
|
type Props = {
|
||||||
mediaUrl: string;
|
mediaUrl: string;
|
||||||
@ -20,6 +23,7 @@ type PromotionTemplate = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
type?: string;
|
||||||
previewUrl?: string;
|
previewUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,58 +35,86 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
mediaPreview,
|
mediaPreview,
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const templates: PromotionTemplate[] = useMemo(
|
const apiFetch = useApiFetch();
|
||||||
() => [
|
const [allTemplates, setAllTemplates] = useState<PromotionTemplate[]>([]);
|
||||||
{
|
const [loadingTemplates, setLoadingTemplates] = useState(false);
|
||||||
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 [eventPromoTemplateId, setEventPromoTemplateId] = useState<string>(templates[0]?.id ?? '');
|
const eventPromoTemplates = useMemo(() => allTemplates.filter((t) => t.type === 'event_promo' || !t.type), [allTemplates]);
|
||||||
const [eventLineupTemplateId, setEventLineupTemplateId] = useState<string>(templates[2]?.id ?? '');
|
const eventLineupTemplates = useMemo(() => allTemplates.filter((t) => t.type === 'event_lineup'), [allTemplates]);
|
||||||
const [artistPromoTemplateId, setArtistPromoTemplateId] = useState<string>(templates[3]?.id ?? '');
|
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 importZipInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [importingTemplate, setImportingTemplate] = useState(false);
|
const [importingTemplate, setImportingTemplate] = useState(false);
|
||||||
|
|
||||||
const selectedEventPromo = templates.find((t) => t.id === eventPromoTemplateId) || null;
|
const selectedEventPromo = allTemplates.find((t) => t.id === eventPromoTemplateId) || null;
|
||||||
const selectedEventLineup = templates.find((t) => t.id === eventLineupTemplateId) || null;
|
const selectedEventLineup = allTemplates.find((t) => t.id === eventLineupTemplateId) || null;
|
||||||
const selectedArtistPromo = templates.find((t) => t.id === artistPromoTemplateId) || 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) => {
|
const handleImportZip = async (file: File) => {
|
||||||
setImportingTemplate(true);
|
setImportingTemplate(true);
|
||||||
try {
|
try {
|
||||||
// Placeholder: Upload to backend endpoint will be added later
|
const result = await uploadTemplateZip(apiFetch, file);
|
||||||
await new Promise((r) => setTimeout(r, 400));
|
|
||||||
|
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 {
|
} finally {
|
||||||
setImportingTemplate(false);
|
setImportingTemplate(false);
|
||||||
}
|
}
|
||||||
@ -93,11 +125,13 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
value,
|
value,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
selected,
|
selected,
|
||||||
|
templates,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
onValueChange: (next: string) => void;
|
onValueChange: (next: string) => void;
|
||||||
selected: PromotionTemplate | null;
|
selected: PromotionTemplate | null;
|
||||||
|
templates: PromotionTemplate[];
|
||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@ -112,6 +146,7 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-between"
|
className="w-full justify-between"
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
|
disabled={loadingTemplates || templates.length === 0}
|
||||||
>
|
>
|
||||||
<span className="truncate">{selected?.name || 'Template wählen'}</span>
|
<span className="truncate">{selected?.name || 'Template wählen'}</span>
|
||||||
<span className="text-muted-foreground">Auswä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 className="mt-1 text-xs text-muted-foreground leading-snug hidden md:block">{selected?.description || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!loadingTemplates && templates.length === 0 && (
|
||||||
|
<div className="mt-2 text-xs text-muted-foreground">Keine Templates verfügbar</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -209,7 +247,10 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-sm font-medium">Promotion Templates</div>
|
<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="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>
|
||||||
@ -219,6 +260,7 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
value={eventPromoTemplateId}
|
value={eventPromoTemplateId}
|
||||||
onValueChange={setEventPromoTemplateId}
|
onValueChange={setEventPromoTemplateId}
|
||||||
selected={selectedEventPromo}
|
selected={selectedEventPromo}
|
||||||
|
templates={eventPromoTemplates}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2 pt-2">
|
<div className="space-y-2 pt-2">
|
||||||
@ -261,6 +303,7 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
value={eventLineupTemplateId}
|
value={eventLineupTemplateId}
|
||||||
onValueChange={setEventLineupTemplateId}
|
onValueChange={setEventLineupTemplateId}
|
||||||
selected={selectedEventLineup}
|
selected={selectedEventLineup}
|
||||||
|
templates={eventLineupTemplates}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -271,11 +314,11 @@ const PromotionTemplatesSection: React.FC<Props> = ({
|
|||||||
value={artistPromoTemplateId}
|
value={artistPromoTemplateId}
|
||||||
onValueChange={setArtistPromoTemplateId}
|
onValueChange={setArtistPromoTemplateId}
|
||||||
selected={selectedArtistPromo}
|
selected={selectedArtistPromo}
|
||||||
|
templates={artistPromoTemplates}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3 rounded-md border p-3">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
ref={importZipInputRef}
|
ref={importZipInputRef}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user