Files
snakkimo-cmt/src/components/CreateModal.jsx
admin a8ff541117 Add create modal for words and pictures tables
- + Button in TableView for tables with a create form (words, pictures)
- Words: form with titel_de/en/sv + difficulty_level → POST /words
- Pictures: design field + image uploader → POST /pictures then POST /pictures/:id/upload
- Image drag-drop area with preview before upload, sends multipart to Hetzner via API
- New record prepended to table on success
- apiPost + apiUpload helpers added to api.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:17:55 +02:00

204 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef } from 'react';
import { apiPost, apiUpload } from '../lib/api';
// Per-table create form definitions
const CREATE_FORMS = {
words: {
title: 'Neues Wort',
fields: [
{ key: 'titel_de', label: 'Titel DE', type: 'text', required: true, placeholder: 'z.B. Apfel' },
{ key: 'titel_en', label: 'Titel EN', type: 'text', required: false, placeholder: 'z.B. Apple' },
{ key: 'titel_sv', label: 'Titel SV', type: 'text', required: false, placeholder: 'z.B. Äpple' },
{ key: 'difficulty_level', label: 'Schwierigkeitsgrad', type: 'number', required: false, min: 1, max: 10 },
],
},
pictures: {
title: 'Neues Bild',
fields: [
{ key: 'design', label: 'Design', type: 'text', required: true, placeholder: 'z.B. red apple on white background' },
],
hasImageUpload: true,
},
};
export function hasCreateForm(tableKey) {
return !!CREATE_FORMS[tableKey];
}
function FieldInput({ field, value, onChange }) {
if (field.type === 'number') {
return (
<input
type="number"
value={value ?? ''}
min={field.min}
max={field.max}
required={field.required}
onChange={e => onChange(e.target.value === '' ? null : Number(e.target.value))}
placeholder={field.placeholder}
className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
);
}
return (
<input
type="text"
value={value ?? ''}
required={field.required}
onChange={e => onChange(e.target.value)}
placeholder={field.placeholder}
className="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
/>
);
}
export default function CreateModal({ tableKey, meta, onClose, onCreated }) {
const formDef = CREATE_FORMS[tableKey];
const [values, setValues] = useState({});
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const fileRef = useRef();
if (!formDef) return null;
function handleFileChange(e) {
const f = e.target.files[0];
if (!f) return;
setFile(f);
const url = URL.createObjectURL(f);
setPreview(url);
}
async function handleSubmit(e) {
e.preventDefault();
setError('');
setSaving(true);
try {
// Build payload — skip nulls/empty strings for optional fields
const body = {};
formDef.fields.forEach(f => {
if (values[f.key] != null && values[f.key] !== '') {
body[f.key] = values[f.key];
}
});
// Step 1: Create record
const created = await apiPost(meta.endpoint, body);
// Step 2: Upload image if provided
let final = created;
if (formDef.hasImageUpload && file) {
const fd = new FormData();
fd.append('file', file);
final = await apiUpload(`${meta.endpoint}/${created.id}/upload`, fd);
}
onCreated(final);
onClose();
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
}
return (
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/40 backdrop-blur-sm p-4 overflow-y-auto">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md my-8">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200">
<div>
<span className="text-xl mr-2">{meta.icon}</span>
<span className="font-semibold text-slate-800">{formDef.title}</span>
</div>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 text-2xl leading-none"
>
×
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit}>
<div className="px-6 py-5 space-y-4">
{formDef.fields.map(field => (
<div key={field.key}>
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
{field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</label>
<FieldInput
field={field}
value={values[field.key]}
onChange={val => setValues(prev => ({ ...prev, [field.key]: val }))}
/>
</div>
))}
{formDef.hasImageUpload && (
<div>
<label className="block text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">
Bild hochladen
</label>
<div
onClick={() => fileRef.current?.click()}
className="border-2 border-dashed border-slate-300 rounded-xl p-4 text-center cursor-pointer hover:border-indigo-400 hover:bg-indigo-50 transition-colors"
>
{preview ? (
<div className="space-y-2">
<img
src={preview}
alt="Vorschau"
className="mx-auto max-h-32 rounded-lg object-contain"
/>
<p className="text-xs text-slate-500">{file.name}</p>
</div>
) : (
<div className="space-y-1">
<div className="text-3xl">🖼</div>
<p className="text-sm text-slate-500">Klicken zum Auswählen</p>
<p className="text-xs text-slate-400">JPG, PNG, WebP · max 20 MB</p>
</div>
)}
</div>
<input
ref={fileRef}
type="file"
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
</div>
)}
{error && (
<div className="bg-red-50 text-red-700 text-sm rounded-lg px-3 py-2">{error}</div>
)}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-slate-200 flex justify-end gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 border border-slate-300 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
type="submit"
disabled={saving}
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white font-medium rounded-lg transition-colors"
>
{saving ? 'Anlegen…' : 'Anlegen'}
</button>
</div>
</form>
</div>
</div>
);
}