Applied the fixes warranted by the adversarial review of PR #21 (all findings graded in the gadfly store): Store (highest impact): - buildDSN always merges busy_timeout/journal_mode/foreign_keys pragmas into the DSN, even for file: URIs or paths with a query string — the prior passthrough silently disabled FK enforcement for those PANSY_DB values. Also escape '#' in the path and tighten in-memory detection to an exact ':memory:' token or mode=memory param (no substring misfire). - migrate.go: detect duplicate migration versions with a clear error; wrap appliedVersions' rows.Err() with the store: prefix. API: - SetTrustedProxies failure now falls back to trust-none instead of leaving gin's trust-everyone default (X-Forwarded-For spoofing). - SPA: 404 embedded directories (was a directory listing), 404 bare /api, add X-Content-Type-Options: nosniff, cache index.html bytes once at startup, single fs.Stat existence check, shared writeAPIError helper. - Move gin.SetMode out of package init() into New(). Config: validate PANSY_PORT range (fall back to 8080 with a warning). Web: - api.ts: serialize the request body before the fetch try so a JSON.stringify failure isn't misreported as a network error. - AppShell: move state-specific/conflicting utilities into active/inactiveProps so concatenated Tailwind classes don't collide. Tests: added store DSN-pragma/memory-detection cases and SPA directory-404 case. go build/vet/test clean (CGO off); web tsc + build clean; re-verified in a browser (SPA, deep links, /assets 404, nav). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
import { Link, Outlet } from '@tanstack/react-router'
|
|
|
|
const navLinks = [
|
|
{ to: '/gardens', label: 'Gardens' },
|
|
{ to: '/plants', label: 'Plants' },
|
|
] as const
|
|
|
|
// TanStack Router concatenates the base className with activeProps/inactiveProps,
|
|
// so state-specific and conflicting utilities (text-muted vs text-fg) live in the
|
|
// state props — never in the base — to avoid ambiguous overrides.
|
|
const navLinkBase = 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors'
|
|
const navLinkActive = 'bg-border/60 text-fg'
|
|
const navLinkInactive = 'text-muted hover:bg-border/60 hover:text-fg'
|
|
|
|
/** Top-level chrome: a sticky nav bar plus the routed page in an <Outlet>. */
|
|
export function AppShell() {
|
|
return (
|
|
<div className="flex min-h-full flex-col">
|
|
<header className="sticky top-0 z-10 border-b border-border bg-surface/90 backdrop-blur">
|
|
<nav className="mx-auto flex max-w-5xl items-center gap-4 px-4 py-3">
|
|
<Link to="/gardens" className="text-lg font-semibold text-accent-strong">
|
|
🌱 pansy
|
|
</Link>
|
|
<div className="flex flex-1 items-center gap-1">
|
|
{navLinks.map((l) => (
|
|
<Link
|
|
key={l.to}
|
|
to={l.to}
|
|
className={navLinkBase}
|
|
activeProps={{ className: navLinkActive }}
|
|
inactiveProps={{ className: navLinkInactive }}
|
|
>
|
|
{l.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
<Link
|
|
to="/login"
|
|
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted transition-colors hover:text-fg"
|
|
>
|
|
Sign in
|
|
</Link>
|
|
</nav>
|
|
</header>
|
|
|
|
<main className="mx-auto w-full max-w-5xl flex-1 px-4 py-6">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|