Skip to content

Commit adc94c4

Browse files
committed
cleanup layout shifts
1 parent 85a631f commit adc94c4

File tree

5 files changed

+158
-96
lines changed

5 files changed

+158
-96
lines changed

.agents/skills/databuddy/references/codebase-map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Use this file when the task spans multiple packages or when the right edit locat
1010
- Next.js dashboard application
1111
- Default dev port: `3000`
1212
- Talks to the backend through oRPC using [`apps/dashboard/lib/orpc.ts`](/Users/iza/Dev/Databuddy/apps/dashboard/lib/orpc.ts)
13+
- **Status pages:** Public page lives under `app/status/[slug]`; **dashboard** edit surface is `app/(main)/monitors/status-pages/[id]/page.tsx`. For stable chrome (no tab strip pop-in), keep `Tabs` always mounted and gate **content** with `useFeatureAccess` + `FeatureLockedPanel`—do not wrap the whole `Tabs` in `FeatureAccessGate` with a list-only `loadingFallback`. Reserve `PageHeader` `right` with skeletons until `statusPage` loads; use a static `description` and breadcrumb fallback `"Status page"` so the header does not reflow on fetch.
1314
- Typical work:
1415
- UI components and pages
1516
- client hooks
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Skeleton } from "@/components/ui/skeleton";
2+
3+
export default function StatusPageDetailLoading() {
4+
return (
5+
<div className="flex h-full min-h-0 flex-col">
6+
<div className="relative flex min-h-[88px] shrink-0 items-center justify-between gap-2 border-b p-3 sm:p-4">
7+
<div className="flex min-w-0 flex-1 items-center gap-3">
8+
<Skeleton className="size-[52px] shrink-0 rounded" />
9+
<div className="min-w-0 flex-1 space-y-2">
10+
<Skeleton className="h-7 max-w-xs rounded" />
11+
<Skeleton className="h-4 max-w-md rounded" />
12+
</div>
13+
</div>
14+
<div
15+
aria-hidden="true"
16+
className="flex max-w-full shrink-0 flex-wrap items-center justify-end gap-2"
17+
>
18+
<Skeleton className="h-9 w-22 rounded sm:w-24" />
19+
<Skeleton className="size-9 rounded" />
20+
<Skeleton className="h-9 w-29 rounded sm:w-32" />
21+
</div>
22+
</div>
23+
24+
<div className="flex h-10 shrink-0 items-center gap-2 border-border border-b bg-accent/30 px-3">
25+
<Skeleton className="h-4 w-24 rounded" />
26+
<span className="text-muted-foreground/40">/</span>
27+
<Skeleton className="h-4 w-28 rounded" />
28+
</div>
29+
30+
<div className="flex h-10 shrink-0 gap-1 border-border border-b bg-accent/30 px-2">
31+
<Skeleton className="h-8 max-w-28 flex-1 rounded" />
32+
<Skeleton className="h-8 max-w-32 flex-1 rounded" />
33+
</div>
34+
35+
<div className="min-h-0 flex-1 overflow-hidden p-4">
36+
<div className="space-y-3">
37+
<Skeleton className="h-24 w-full rounded border bg-card" />
38+
<Skeleton className="h-24 w-full rounded border bg-card" />
39+
</div>
40+
</div>
41+
</div>
42+
);
43+
}

apps/dashboard/app/(main)/monitors/status-pages/[id]/page.tsx

Lines changed: 111 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@ import {
1010
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1111
import Link from "next/link";
1212
import { useParams } from "next/navigation";
13-
import { useState } from "react";
13+
import { useState, type ReactNode } from "react";
1414
import { toast } from "sonner";
1515
import { PageHeader } from "@/app/(main)/websites/_components/page-header";
1616
import { EmptyState } from "@/components/empty-state";
1717
import { ErrorBoundary } from "@/components/error-boundary";
18-
import { FeatureAccessGate } from "@/components/feature-access-gate";
18+
import { FeatureLockedPanel } from "@/components/feature-access-gate";
1919
import { PageNavigation } from "@/components/layout/page-navigation";
2020
import { Badge } from "@/components/ui/badge";
2121
import { Button } from "@/components/ui/button";
2222
import { List } from "@/components/ui/composables/list";
23+
import { Skeleton } from "@/components/ui/skeleton";
2324
import { DeleteDialog } from "@/components/ui/delete-dialog";
2425
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
26+
import { useFeatureAccess } from "@/hooks/use-feature-access";
2527
import { getStatusPageUrl } from "@/lib/app-url";
2628
import { orpc } from "@/lib/orpc";
2729
import { cn } from "@/lib/utils";
@@ -56,6 +58,9 @@ export default function StatusPageDetailsPage() {
5658

5759
const statusPage = statusPageQuery.data;
5860

61+
const { hasAccess, isLoading: isFeatureAccessLoading } =
62+
useFeatureAccess("monitors");
63+
5964
const monitorToRemoveData = statusPage?.monitors.find(
6065
(m: StatusPageMonitor) => m.id === monitorToRemove
6166
);
@@ -70,18 +75,66 @@ export default function StatusPageDetailsPage() {
7075
});
7176
};
7277

78+
let monitorsBody: ReactNode;
79+
if (isFeatureAccessLoading) {
80+
monitorsBody = <List.DefaultLoading />;
81+
} else if (!hasAccess) {
82+
monitorsBody = <FeatureLockedPanel flagKey="monitors" />;
83+
} else if (statusPageQuery.isLoading) {
84+
monitorsBody = <List.DefaultLoading />;
85+
} else if (statusPageQuery.isError) {
86+
monitorsBody = (
87+
<div className="flex flex-1 items-center justify-center py-16">
88+
<EmptyState
89+
action={{
90+
label: "Retry",
91+
onClick: () => statusPageQuery.refetch(),
92+
}}
93+
description="Something went wrong while loading the status page."
94+
icon={<BrowserIcon weight="duotone" />}
95+
title="Failed to load"
96+
variant="error"
97+
/>
98+
</div>
99+
);
100+
} else if (statusPage?.monitors.length === 0) {
101+
monitorsBody = (
102+
<div className="flex flex-1 items-center justify-center py-16">
103+
<EmptyState
104+
action={{
105+
label: "Add Monitor",
106+
onClick: () => setIsAddDialogOpen(true),
107+
}}
108+
description="Add monitors to this status page to display their uptime and latency."
109+
icon={<HeartbeatIcon weight="duotone" />}
110+
title="No monitors added"
111+
variant="minimal"
112+
/>
113+
</div>
114+
);
115+
} else {
116+
monitorsBody = (
117+
<List className="rounded bg-card">
118+
{statusPage?.monitors.map((monitor: StatusPageMonitor) => (
119+
<StatusPageMonitorRow
120+
key={monitor.id}
121+
monitor={monitor}
122+
onRemoveRequestAction={(id) => setMonitorToRemove(id)}
123+
statusPageId={statusPageId}
124+
/>
125+
))}
126+
</List>
127+
);
128+
}
129+
73130
return (
74131
<ErrorBoundary>
75132
<div className="flex h-full min-h-0 flex-col">
76133
<PageHeader
77-
description={
78-
statusPage
79-
? `Manage monitors for ${statusPage.name}`
80-
: "Manage status page monitors"
81-
}
134+
description="Manage monitors and what appears on your public status page."
82135
icon={<BrowserIcon />}
83136
right={
84-
statusPage && (
137+
statusPage ? (
85138
<>
86139
<Button asChild size="sm" variant="outline">
87140
<Link
@@ -114,102 +167,67 @@ export default function StatusPageDetailsPage() {
114167
Add Monitor
115168
</Button>
116169
</>
170+
) : (
171+
<div
172+
aria-hidden="true"
173+
className="flex max-w-full shrink-0 flex-wrap items-center justify-end gap-2"
174+
>
175+
<Skeleton className="h-9 w-22 rounded sm:w-24" />
176+
<Skeleton className="size-9 rounded" />
177+
<Skeleton className="h-9 w-29 rounded sm:w-32" />
178+
</div>
117179
)
118180
}
119-
title={statusPage?.name ?? "Status Page"}
181+
title={statusPage?.name ?? "Status page"}
120182
/>
121183

122184
<PageNavigation
123185
breadcrumb={{ label: "Status Pages", href: "/monitors/status-pages" }}
124-
currentPage={statusPage?.name ?? "Loading..."}
186+
currentPage={statusPage?.name ?? "Status page"}
125187
variant="breadcrumb"
126188
/>
127189

128-
<FeatureAccessGate
129-
flagKey="monitors"
130-
loadingFallback={<List.DefaultLoading />}
190+
<Tabs
191+
className="flex min-h-0 flex-1 flex-col gap-0"
192+
defaultValue="monitors"
193+
variant="navigation"
131194
>
132-
<Tabs
133-
className="flex min-h-0 flex-1 flex-col gap-0"
134-
defaultValue="monitors"
135-
variant="navigation"
195+
<TabsList>
196+
<TabsTrigger value="monitors">
197+
<HeartbeatIcon size={16} weight="duotone" />
198+
Monitors
199+
</TabsTrigger>
200+
<TabsTrigger disabled value="incidents">
201+
<SirenIcon size={16} weight="duotone" />
202+
Incidents
203+
<Badge className="px-1.5 py-0" variant="secondary">
204+
Soon
205+
</Badge>
206+
</TabsTrigger>
207+
</TabsList>
208+
209+
<TabsContent
210+
className="min-h-0 flex-1 overflow-y-auto"
211+
value="monitors"
136212
>
137-
<TabsList>
138-
<TabsTrigger value="monitors">
139-
<HeartbeatIcon size={16} weight="duotone" />
140-
Monitors
141-
</TabsTrigger>
142-
<TabsTrigger disabled value="incidents">
143-
<SirenIcon size={16} weight="duotone" />
144-
Incidents
145-
<Badge className="px-1.5 py-0" variant="secondary">
146-
Soon
147-
</Badge>
148-
</TabsTrigger>
149-
</TabsList>
150-
151-
<TabsContent
152-
className="min-h-0 flex-1 overflow-y-auto"
153-
value="monitors"
154-
>
155-
{statusPageQuery.isLoading ? (
156-
<List.DefaultLoading />
157-
) : statusPageQuery.isError ? (
158-
<div className="flex flex-1 items-center justify-center py-16">
159-
<EmptyState
160-
action={{
161-
label: "Retry",
162-
onClick: () => statusPageQuery.refetch(),
163-
}}
164-
description="Something went wrong while loading the status page."
165-
icon={<BrowserIcon weight="duotone" />}
166-
title="Failed to load"
167-
variant="error"
168-
/>
169-
</div>
170-
) : statusPage?.monitors.length === 0 ? (
171-
<div className="flex flex-1 items-center justify-center py-16">
172-
<EmptyState
173-
action={{
174-
label: "Add Monitor",
175-
onClick: () => setIsAddDialogOpen(true),
176-
}}
177-
description="Add monitors to this status page to display their uptime and latency."
178-
icon={<HeartbeatIcon weight="duotone" />}
179-
title="No monitors added"
180-
variant="minimal"
181-
/>
182-
</div>
183-
) : (
184-
<List className="rounded bg-card">
185-
{statusPage?.monitors.map((monitor: StatusPageMonitor) => (
186-
<StatusPageMonitorRow
187-
key={monitor.id}
188-
monitor={monitor}
189-
onRemoveRequestAction={(id) => setMonitorToRemove(id)}
190-
statusPageId={statusPageId}
191-
/>
192-
))}
193-
</List>
194-
)}
195-
</TabsContent>
196-
197-
<TabsContent
198-
className="min-h-0 flex-1 overflow-y-auto"
199-
value="incidents"
200-
>
201-
<div className="flex flex-1 items-center justify-center py-16">
202-
<EmptyState
203-
description="Incident management is coming soon. You'll be able to create and track incidents directly from here."
204-
icon={<SirenIcon weight="duotone" />}
205-
showPlusBadge={false}
206-
title="Coming Soon"
207-
variant="minimal"
208-
/>
209-
</div>
210-
</TabsContent>
211-
</Tabs>
212-
</FeatureAccessGate>
213+
{monitorsBody}
214+
</TabsContent>
215+
216+
<TabsContent
217+
className="min-h-0 flex-1 overflow-y-auto"
218+
value="incidents"
219+
>
220+
<div className="flex flex-1 items-center justify-center py-16">
221+
<EmptyState
222+
description="Incident management is coming soon. You'll be able to create and track incidents directly from here."
223+
icon={<SirenIcon weight="duotone" />}
224+
showPlusBadge={false}
225+
title="Coming Soon"
226+
variant="minimal"
227+
/>
228+
</div>
229+
</TabsContent>
230+
</Tabs>
213231

214232
<AddMonitorDialog
215233
existingMonitorIds={

apps/dashboard/app/status/[slug]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async function getStatusData(slug: string, days: number) {
2323
{
2424
revalidate: 60,
2525
tags: ["status-page", `status-page-${slug}`],
26-
},
26+
}
2727
)();
2828
}
2929

apps/dashboard/components/feature-access-gate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ interface FeatureAccessGateProps {
1212
loadingFallback?: ReactNode;
1313
}
1414

15-
function LockedState({ flagKey }: { flagKey: string }) {
15+
export function FeatureLockedPanel({ flagKey }: { flagKey: string }) {
1616
const label = getFeatureLabel(flagKey);
1717
const description = getFeatureDescription(flagKey);
1818

@@ -54,7 +54,7 @@ export function FeatureAccessGate({
5454
}
5555

5656
if (!hasAccess) {
57-
return <LockedState flagKey={flagKey} />;
57+
return <FeatureLockedPanel flagKey={flagKey} />;
5858
}
5959

6060
return <>{children}</>;

0 commit comments

Comments
 (0)