diff --git a/src/components/CreateModal.jsx b/src/components/CreateModal.jsx new file mode 100644 index 0000000..25486d5 --- /dev/null +++ b/src/components/CreateModal.jsx @@ -0,0 +1,203 @@ +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 ( + 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 ( + 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 ( +
+
+ {/* Header */} +
+
+ {meta.icon} + {formDef.title} +
+ +
+ + {/* Form */} +
+
+ {formDef.fields.map(field => ( +
+ + setValues(prev => ({ ...prev, [field.key]: val }))} + /> +
+ ))} + + {formDef.hasImageUpload && ( +
+ +
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 ? ( +
+ Vorschau +

{file.name}

+
+ ) : ( +
+
🖼️
+

Klicken zum Auswählen

+

JPG, PNG, WebP · max 20 MB

+
+ )} +
+ +
+ )} + + {error && ( +
{error}
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+
+ ); +} diff --git a/src/lib/api.js b/src/lib/api.js index acc76e7..fd58c85 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -62,6 +62,29 @@ export async function apiPatch(endpoint, id, body) { }); } +export async function apiPost(endpoint, body) { + return apiFetch(endpoint, { + method: 'POST', + body: JSON.stringify(body), + }); +} + export async function apiFetchOne(path) { return apiFetch(path); } + +// Multipart upload — does NOT set Content-Type (browser sets boundary automatically) +export async function apiUpload(path, formData) { + const token = getToken(); + const res = await fetch(`${API_URL}${path}`, { + method: 'POST', + headers: token ? { Authorization: `Bearer ${token}` } : {}, + body: formData, + }); + if (res.status === 401) { logout(); window.location.href = '/login'; return; } + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || `HTTP ${res.status}`); + } + return res.json(); +} diff --git a/src/pages/TableView.jsx b/src/pages/TableView.jsx index b1543e8..66ef77f 100644 --- a/src/pages/TableView.jsx +++ b/src/pages/TableView.jsx @@ -2,6 +2,7 @@ import { useEffect, useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import Layout from '../components/Layout'; import RecordModal from '../components/RecordModal'; +import CreateModal, { hasCreateForm } from '../components/CreateModal'; import { fetchAll } from '../lib/api'; import { TABLES, STATUS_COLORS } from '../lib/tables'; @@ -86,6 +87,7 @@ export default function TableView() { const [textFilter, setTextFilter] = useState(''); const [highlightId, setHighlightId] = useState(null); const [modalRecord, setModalRecord] = useState(null); + const [showCreate, setShowCreate] = useState(false); useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -124,6 +126,10 @@ export default function TableView() { setRows(prev => prev.map(r => (r.id === updated.id ? { ...r, ...updated } : r))); } + function handleRecordCreated(newRecord) { + setRows(prev => [newRecord, ...prev]); + } + if (!meta) return

Unbekannte Tabelle: {tableKey}

; return ( @@ -135,6 +141,14 @@ export default function TableView() { ({filtered.length}{rows.length === 500 ? '+' : ''} von {rows.length}{rows.length === 500 ? '+' : ''}) + {hasCreateForm(tableKey) && ( + + )} {/* Filters */} @@ -226,6 +240,15 @@ export default function TableView() { onSaved={handleRecordSaved} /> )} + + {showCreate && ( + setShowCreate(false)} + onCreated={handleRecordCreated} + /> + )} ); }