176 lines
6.1 KiB
TypeScript
176 lines
6.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Save } from 'lucide-react';
|
|
import { PanelShell, Label, Field, HelperText } from '../panels/PanelShell';
|
|
import { staticMicrositeApi, type MicrositeV2Template } from '../../../services/api';
|
|
|
|
const inputStyle: React.CSSProperties = {
|
|
width: '100%',
|
|
padding: '8px 10px',
|
|
borderRadius: 'var(--radius-button)',
|
|
border: '1px solid var(--border)',
|
|
background: 'var(--input-background)',
|
|
color: 'var(--foreground)',
|
|
fontSize: 'var(--text-sm)',
|
|
boxSizing: 'border-box',
|
|
};
|
|
|
|
export function UploadMicrositeV2TemplatePanel({
|
|
template,
|
|
onClose,
|
|
onSaved,
|
|
}: {
|
|
template: MicrositeV2Template | null;
|
|
onClose: () => void;
|
|
onSaved: () => void;
|
|
}) {
|
|
const isEdit = Boolean(template);
|
|
|
|
const [slug, setSlug] = useState(template?.slug || '');
|
|
const [name, setName] = useState(template?.name || '');
|
|
const [description, setDescription] = useState(template?.description || '');
|
|
const [templateHtml, setTemplateHtml] = useState<File | null>(null);
|
|
const [templateJson, setTemplateJson] = useState<File | null>(null);
|
|
const [thumbnail, setThumbnail] = useState<File | null>(null);
|
|
const [assetsZip, setAssetsZip] = useState<File | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
setError('');
|
|
try {
|
|
let templateId = template?._id;
|
|
|
|
if (!isEdit) {
|
|
if (!slug.trim() || !name.trim()) {
|
|
throw new Error('Slug and name are required');
|
|
}
|
|
const createRes = await staticMicrositeApi.createTemplate({
|
|
slug: slug.trim(),
|
|
name: name.trim(),
|
|
description: description.trim() || undefined,
|
|
});
|
|
if (createRes.error || !createRes.data) throw new Error(createRes.error || 'Create failed');
|
|
templateId = createRes.data._id;
|
|
} else if (name !== template?.name || description !== template?.description) {
|
|
const updateRes = await staticMicrositeApi.updateTemplate(template!._id, {
|
|
name: name.trim(),
|
|
description: description.trim(),
|
|
});
|
|
if (updateRes.error) throw new Error(updateRes.error);
|
|
}
|
|
|
|
if (!templateId) throw new Error('Missing template id');
|
|
|
|
const hasFiles = templateHtml || templateJson || thumbnail || assetsZip;
|
|
if (!isEdit && (!templateHtml || !templateJson)) {
|
|
throw new Error('template.html and template.json are required for new templates');
|
|
}
|
|
|
|
if (hasFiles) {
|
|
const uploadRes = await staticMicrositeApi.uploadTemplateFiles(templateId, {
|
|
templateHtml: templateHtml || undefined,
|
|
templateJson: templateJson || undefined,
|
|
thumbnail: thumbnail || undefined,
|
|
assetsZip: assetsZip || undefined,
|
|
});
|
|
if (uploadRes.error) throw new Error(uploadRes.error);
|
|
}
|
|
|
|
onSaved();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Save failed');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<PanelShell
|
|
title={isEdit ? 'Edit template' : 'New microsite template'}
|
|
description="Upload template.html, template.json, thumbnail, and optional assets zip."
|
|
onClose={onClose}
|
|
panelWidth="min(520px, 100vw)"
|
|
ctaLabel={saving ? 'Saving…' : 'Save'}
|
|
ctaDisabled={saving}
|
|
onCtaClick={() => void handleSave()}
|
|
>
|
|
{error ? <p style={{ color: 'var(--destructive)', fontSize: 'var(--text-sm)', marginBottom: '12px' }}>{error}</p> : null}
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
<div>
|
|
<Label text="Slug" />
|
|
<Field>
|
|
<input
|
|
type="text"
|
|
value={slug}
|
|
disabled={isEdit}
|
|
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
|
|
placeholder="event-up"
|
|
style={inputStyle}
|
|
/>
|
|
</Field>
|
|
<HelperText text="Unique id used in S3 paths. Cannot change after create." />
|
|
</div>
|
|
|
|
<div>
|
|
<Label text="Name" />
|
|
<Field>
|
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} style={inputStyle} />
|
|
</Field>
|
|
</div>
|
|
|
|
<div>
|
|
<Label text="Description" />
|
|
<Field>
|
|
<textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={2} style={{ ...inputStyle, resize: 'vertical' }} />
|
|
</Field>
|
|
</div>
|
|
|
|
<div>
|
|
<Label text={`template.html ${isEdit ? '(optional)' : '(required)'}`} />
|
|
<input type="file" accept=".html,text/html" onChange={(e) => setTemplateHtml(e.target.files?.[0] || null)} />
|
|
</div>
|
|
|
|
<div>
|
|
<Label text={`template.json ${isEdit ? '(optional)' : '(required)'}`} />
|
|
<input type="file" accept=".json,application/json" onChange={(e) => setTemplateJson(e.target.files?.[0] || null)} />
|
|
</div>
|
|
|
|
<div>
|
|
<Label text="Thumbnail (optional)" />
|
|
<input type="file" accept="image/*" onChange={(e) => setThumbnail(e.target.files?.[0] || null)} />
|
|
</div>
|
|
|
|
<div>
|
|
<Label text="Assets zip (optional)" />
|
|
<input type="file" accept=".zip,application/zip" onChange={(e) => setAssetsZip(e.target.files?.[0] || null)} />
|
|
<HelperText text="Zip contents are extracted to templates/{slug}/assets/" />
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
disabled={saving}
|
|
onClick={() => void handleSave()}
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
padding: '8px 14px',
|
|
borderRadius: 'var(--radius-button)',
|
|
border: 'none',
|
|
background: 'var(--primary)',
|
|
color: 'var(--primary-foreground)',
|
|
cursor: saving ? 'wait' : 'pointer',
|
|
fontSize: 'var(--text-sm)',
|
|
alignSelf: 'flex-start',
|
|
}}
|
|
>
|
|
<Save size={14} />
|
|
{saving ? 'Saving…' : 'Save template'}
|
|
</button>
|
|
</div>
|
|
</PanelShell>
|
|
);
|
|
}
|