prod setup

This commit is contained in:
Johan LEROY
2026-05-04 11:00:44 +02:00
parent 9e9bc0f23d
commit d30e8e9376
45 changed files with 1998 additions and 188 deletions

View File

@@ -0,0 +1,122 @@
import { useEffect, useRef, useState } from 'react';
type Props = {
section: string;
initial: unknown;
};
type Status =
| { kind: 'idle' }
| { kind: 'saving' }
| { kind: 'saved' }
| { kind: 'error'; message: string }
| { kind: 'invalid' };
export function RawJsonEditor({ section, initial }: Props) {
const [value, setValue] = useState(() => JSON.stringify(initial, null, 2));
const [status, setStatus] = useState<Status>({ kind: 'idle' });
const taRef = useRef<HTMLTextAreaElement>(null);
const format = () => {
try {
const parsed = JSON.parse(value);
setValue(JSON.stringify(parsed, null, 2));
setStatus({ kind: 'idle' });
} catch {
setStatus({ kind: 'invalid' });
}
};
const save = async () => {
let body: unknown;
try {
body = JSON.parse(value);
} catch {
setStatus({ kind: 'invalid' });
return;
}
setStatus({ kind: 'saving' });
try {
const res = await fetch(`/api/content/${section}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) {
setStatus({ kind: 'error', message: data.message || data.error || 'erreur' });
return;
}
setStatus({ kind: 'saved' });
setTimeout(() => setStatus({ kind: 'idle' }), 2500);
} catch (e) {
setStatus({ kind: 'error', message: (e as Error).message });
}
};
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
save();
}
};
const ta = taRef.current;
ta?.addEventListener('keydown', onKey as EventListener);
return () => ta?.removeEventListener('keydown', onKey as EventListener);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
<div className="space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<p className="font-mono text-xs text-[color:var(--color-text-dim)]">
Mode avancé JSON brut, validé Zod côté serveur.
</p>
<div className="flex flex-wrap items-center gap-2">
<StatusPill status={status} />
<button type="button" onClick={format} className="btn-ghost text-xs py-1.5">
Formater
</button>
<button type="button" onClick={save} className="btn-primary text-xs py-1.5">
Enregistrer
</button>
</div>
</div>
<textarea
ref={taRef}
value={value}
onChange={(e) => setValue(e.target.value)}
spellCheck={false}
className="w-full min-h-[400px] h-[calc(100vh-300px)] max-h-[80vh] bg-[color:var(--color-surface)] border border-[color:var(--color-border)] focus:border-[color:var(--color-cyan)] focus:outline-none p-4 rounded text-sm font-mono text-[color:var(--color-text)] resize-none"
/>
</div>
);
}
function StatusPill({ status }: { status: Status }) {
if (status.kind === 'idle') return null;
if (status.kind === 'saving')
return (
<span className="font-mono text-xs text-[color:var(--color-text-muted)]">
enregistrement
</span>
);
if (status.kind === 'saved')
return (
<span className="font-mono text-xs text-[color:var(--color-cyan)] glow-text-cyan">
enregistré
</span>
);
if (status.kind === 'invalid')
return (
<span className="font-mono text-xs text-[color:var(--color-magenta)]">
JSON invalide
</span>
);
return (
<span className="font-mono text-xs text-[color:var(--color-magenta)]" title={status.message}>
{status.message.slice(0, 60)}
</span>
);
}