Intro and Hero are their own sections, and Intro comes first
The page reads hero-then-intro and this deliberately does not. The intro is the thing that gets edited; the hero is a path that changes once a season. Ordering by what somebody came to do beats mirroring the layout of the page they are describing. One PATCH per card, carrying only that card's field. `take()` on the server treats an absent key as leave-it-alone, so sending the whole object from either card would have each one saving the other's unsaved edits — a half-finished intro riding along with a hero somebody meant to change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+42
-18
@@ -92,13 +92,18 @@ export const HomePanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const saveContent = () =>
|
||||
send('content', () =>
|
||||
clientRef.current.patch<{ data: Home }>('/fofos-admin/home', {
|
||||
hero_url: heroUrl.trim() || null,
|
||||
intro_text: intro.trim() || null,
|
||||
}),
|
||||
);
|
||||
/*
|
||||
One PATCH per card, carrying only that card's field.
|
||||
|
||||
`take()` on the server treats an absent key as "leave it alone", so sending the whole object from
|
||||
either card would have each one saving the other's unsaved edits. Two cards, two independent
|
||||
saves, and a half-finished intro cannot ride along with a hero somebody meant to change.
|
||||
*/
|
||||
const saveIntro = () =>
|
||||
send('intro', () => clientRef.current.patch<{ data: Home }>('/fofos-admin/home', { intro_text: intro.trim() || null }));
|
||||
|
||||
const saveHero = () =>
|
||||
send('hero', () => clientRef.current.patch<{ data: Home }>('/fofos-admin/home', { hero_url: heroUrl.trim() || null }));
|
||||
|
||||
const add = () =>
|
||||
send('add', async () => {
|
||||
@@ -137,15 +142,23 @@ export const HomePanel = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const contentDirty = heroUrl.trim() !== (home.hero.url ?? '') || intro.trim() !== (home.intro_text ?? '');
|
||||
const introDirty = intro.trim() !== (home.intro_text ?? '');
|
||||
const heroDirty = heroUrl.trim() !== (home.hero.url ?? '');
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col gap-4 overflow-auto p-5">
|
||||
{/*
|
||||
Intro first, then Hero.
|
||||
|
||||
The page reads hero-then-intro, and this does not, deliberately: the intro is the thing that
|
||||
gets edited, and the hero is a path that changes once a season. Ordering by what somebody came
|
||||
to do beats mirroring the layout of the page they are describing.
|
||||
*/}
|
||||
<section className="flex flex-col gap-3 rounded-lg border border-border p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">Hero</h3>
|
||||
<p className="text-xs text-muted-foreground">The video or image across the top of the page.</p>
|
||||
<h3 className="text-sm font-medium">Intro</h3>
|
||||
<p className="text-xs text-muted-foreground">The paragraph under the hero.</p>
|
||||
</div>
|
||||
{saved && (
|
||||
<span className="flex shrink-0 items-center gap-1 text-xs text-success">
|
||||
@@ -155,6 +168,23 @@ export const HomePanel = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* HTML in and HTML out — it is what the site's database already holds and renders. */}
|
||||
<RichText value={intro} onChange={setIntro} placeholder="The paragraph under the hero." />
|
||||
|
||||
<div className="flex justify-end border-t border-border pt-3">
|
||||
<Button size="sm" onClick={() => void saveIntro()} disabled={busy !== null || !introDirty}>
|
||||
{busy === 'intro' ? <Loader2 className="size-3.5 animate-spin" /> : <Check className="size-3.5" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-3 rounded-lg border border-border p-4">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-medium">Hero</h3>
|
||||
<p className="text-xs text-muted-foreground">The video or image across the top of the page.</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
value={heroUrl}
|
||||
onChange={(event) => setHeroUrl(event.target.value)}
|
||||
@@ -172,17 +202,11 @@ export const HomePanel = () => {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium">Intro</p>
|
||||
{/* HTML in and HTML out — it is what the site's database already holds and renders. */}
|
||||
<RichText value={intro} onChange={setIntro} placeholder="The paragraph under the hero." />
|
||||
</div>
|
||||
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
|
||||
<div className="flex justify-end border-t border-border pt-3">
|
||||
<Button size="sm" onClick={() => void saveContent()} disabled={busy !== null || !contentDirty}>
|
||||
{busy === 'content' ? <Loader2 className="size-3.5 animate-spin" /> : <Check className="size-3.5" />}
|
||||
<Button size="sm" onClick={() => void saveHero()} disabled={busy !== null || !heroDirty}>
|
||||
{busy === 'hero' ? <Loader2 className="size-3.5 animate-spin" /> : <Check className="size-3.5" />}
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user