Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ export {};

const GA_ID = import.meta.env.VITE_GA_ID as string | undefined;

class RootErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error?: unknown }
> {
state = { hasError: false as boolean, error: undefined as unknown };

static getDerivedStateFromError(error: unknown) {
return { hasError: true, error };
}

componentDidCatch(error: unknown) {
console.error("[HPL] Uncaught render error:", error);
}

render() {
if (this.state.hasError) {
return (
<div className="min-h-screen bg-slate-950 text-slate-100 flex items-center justify-center p-6">
<div className="max-w-xl space-y-3 rounded-xl border border-slate-800 bg-slate-900/30 p-6">
<div className="text-xs font-mono uppercase tracking-widest text-slate-400">
HPL Recovery Mode
</div>
<div className="text-lg font-semibold">Something crashed during render.</div>
<div className="text-sm text-slate-300">
Check the console for details. If this is production, we can add a safer fallback path.
</div>
</div>
</div>
);
}
return this.props.children;
}
}


function initGA(measurementId: string) {
// 1) Create dataLayer + gtag stub immediately
window.dataLayer = window.dataLayer ?? [];
Expand Down Expand Up @@ -70,6 +105,8 @@ if (GA_ID) initGA(GA_ID);

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RouterProvider router={router} />
<RootErrorBoundary>
<RouterProvider router={router} />
</RootErrorBoundary>
</React.StrictMode>
);
45 changes: 36 additions & 9 deletions src/pages/admin/pages/AdminNotesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ export function AdminNotesPage() {
await refreshNotes();
};

const EXCERPT_SOFT_LIMIT = 280;
const excerptLength = form.excerpt.length;
const overLimit = excerptLength > EXCERPT_SOFT_LIMIT;

return (
<div className="space-y-6">
{/* Header */}
Expand Down Expand Up @@ -317,16 +321,39 @@ export function AdminNotesPage() {
</div>

<div className="space-y-2">
<label className="text-xs uppercase tracking-widest text-zinc-500">
Excerpt
<label className="block space-y-1">
<span className="text-xs font-mono uppercase tracking-widest text-zinc-400">
Excerpt (preview surface)
</span>

<textarea
name="excerpt"
value={form.excerpt}
onChange={handleChange}
className={`
h-24 w-full resize-y rounded-lg
border bg-zinc-950/40 px-3 py-2 text-zinc-100
${overLimit ? "border-vesper/60 ring-1 ring-vesper/40" : "border-zinc-800"}
`}
placeholder="Short summary shown in lists"
/>

<div
className={`
flex justify-between text-xs font-mono
${overLimit ? "text-vesper" : "text-zinc-500"}
`}
>
<span>
Used on cards and previews. Full body lives in MD source.
</span>

<span>
{excerptLength} / {EXCERPT_SOFT_LIMIT}
{overLimit && " · preview overflow"}
</span>
</div>
</label>
<textarea
name="excerpt"
value={form.excerpt}
onChange={handleChange}
className="h-24 w-full rounded-lg border border-zinc-800 bg-zinc-950/40 px-3 py-2 text-zinc-100"
placeholder="Short summary shown in lists"
/>
</div>

<div className="flex flex-wrap gap-2">
Expand Down