prod setup
This commit is contained in:
122
src/components/admin/RawJsonEditor.tsx
Normal file
122
src/components/admin/RawJsonEditor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user