Skip to content

Commit 6d12d02

Browse files
committed
fix: billing provider in layout
1 parent 40718f4 commit 6d12d02

File tree

3 files changed

+263
-7
lines changed

3 files changed

+263
-7
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"use client";
2+
3+
import {
4+
DotsThreeIcon,
5+
GlobeIcon,
6+
HeartbeatIcon,
7+
PencilIcon,
8+
TrashIcon,
9+
} from "@phosphor-icons/react";
10+
import { useMutation } from "@tanstack/react-query";
11+
import Link from "next/link";
12+
import { useState } from "react";
13+
import { toast } from "sonner";
14+
import { Badge } from "@/components/ui/badge";
15+
import { Button } from "@/components/ui/button";
16+
import {
17+
DropdownMenu,
18+
DropdownMenuContent,
19+
DropdownMenuItem,
20+
DropdownMenuTrigger,
21+
} from "@/components/ui/dropdown-menu";
22+
import { orpc } from "@/lib/orpc";
23+
24+
const granularityLabels: Record<string, string> = {
25+
minute: "Every minute",
26+
ten_minutes: "Every 10 minutes",
27+
thirty_minutes: "Every 30 minutes",
28+
hour: "Hourly",
29+
six_hours: "Every 6 hours",
30+
twelve_hours: "Every 12 hours",
31+
day: "Daily",
32+
};
33+
34+
type MonitorRowProps = {
35+
schedule: {
36+
id: string;
37+
websiteId: string | null;
38+
url: string | null;
39+
name: string | null;
40+
granularity: string;
41+
cron: string;
42+
isPaused: boolean;
43+
createdAt: Date | string;
44+
updatedAt: Date | string;
45+
website: {
46+
id: string;
47+
name: string | null;
48+
domain: string;
49+
} | null;
50+
};
51+
onEdit: () => void;
52+
onDelete: () => void;
53+
onRefetch: () => void;
54+
};
55+
56+
export function MonitorRow({
57+
schedule,
58+
onEdit,
59+
onDelete,
60+
onRefetch,
61+
}: MonitorRowProps) {
62+
const [isPausing, setIsPausing] = useState(false);
63+
64+
const pauseMutation = useMutation({
65+
...orpc.uptime.pauseSchedule.mutationOptions(),
66+
});
67+
const resumeMutation = useMutation({
68+
...orpc.uptime.resumeSchedule.mutationOptions(),
69+
});
70+
const deleteMutation = useMutation({
71+
...orpc.uptime.deleteSchedule.mutationOptions(),
72+
});
73+
74+
const handleTogglePause = async () => {
75+
setIsPausing(true);
76+
try {
77+
if (schedule.isPaused) {
78+
await resumeMutation.mutateAsync({ scheduleId: schedule.id });
79+
toast.success("Monitor resumed");
80+
} else {
81+
await pauseMutation.mutateAsync({ scheduleId: schedule.id });
82+
toast.success("Monitor paused");
83+
}
84+
onRefetch();
85+
} catch (error) {
86+
const errorMessage =
87+
error instanceof Error ? error.message : "Failed to update monitor";
88+
toast.error(errorMessage);
89+
} finally {
90+
setIsPausing(false);
91+
}
92+
};
93+
94+
const handleDelete = async () => {
95+
try {
96+
await deleteMutation.mutateAsync({ scheduleId: schedule.id });
97+
toast.success("Monitor deleted");
98+
onDelete();
99+
} catch (error) {
100+
const errorMessage =
101+
error instanceof Error ? error.message : "Failed to delete monitor";
102+
toast.error(errorMessage);
103+
}
104+
};
105+
106+
const isWebsiteMonitor = !!schedule.websiteId;
107+
const displayName = isWebsiteMonitor
108+
? schedule.website?.name || schedule.website?.domain || "Unknown"
109+
: schedule.name || schedule.url || "Unknown";
110+
const displayUrl = isWebsiteMonitor ? schedule.website?.domain : schedule.url;
111+
112+
return (
113+
<div className="flex items-center gap-4 border-b p-4 transition-colors last:border-b-0 hover:bg-accent/50">
114+
<div className="flex size-10 shrink-0 items-center justify-center rounded border bg-secondary-brighter">
115+
<HeartbeatIcon
116+
className="text-accent-foreground"
117+
size={20}
118+
weight="duotone"
119+
/>
120+
</div>
121+
<div className="min-w-0 flex-1">
122+
{isWebsiteMonitor ? (
123+
<Link
124+
className="group block"
125+
href={`/websites/${schedule.websiteId}/pulse`}
126+
>
127+
<h3 className="truncate font-semibold text-base text-foreground transition-colors group-hover:text-primary">
128+
{displayName}
129+
</h3>
130+
</Link>
131+
) : (
132+
<h3 className="truncate font-semibold text-base text-foreground">
133+
{displayName}
134+
</h3>
135+
)}
136+
<div className="mt-1 flex items-center gap-4 text-muted-foreground text-sm">
137+
<div className="flex items-center gap-1.5">
138+
<GlobeIcon className="size-3.5 shrink-0" weight="duotone" />
139+
<span className="truncate">{displayUrl}</span>
140+
</div>
141+
<span></span>
142+
<span>
143+
{granularityLabels[schedule.granularity] || schedule.granularity}
144+
</span>
145+
</div>
146+
</div>
147+
<Badge
148+
className={
149+
schedule.isPaused
150+
? "border-amber-500/20 bg-amber-500/10 text-amber-600"
151+
: "border-green-500/20 bg-green-500/10 text-green-600"
152+
}
153+
variant={schedule.isPaused ? "secondary" : "default"}
154+
>
155+
{schedule.isPaused ? "Paused" : "Active"}
156+
</Badge>
157+
<DropdownMenu>
158+
<DropdownMenuTrigger asChild>
159+
<Button size="sm" variant="ghost">
160+
<DotsThreeIcon size={20} />
161+
</Button>
162+
</DropdownMenuTrigger>
163+
<DropdownMenuContent align="end">
164+
<DropdownMenuItem onClick={onEdit}>
165+
<PencilIcon size={16} />
166+
Edit
167+
</DropdownMenuItem>
168+
<DropdownMenuItem
169+
disabled={
170+
isPausing || pauseMutation.isPending || resumeMutation.isPending
171+
}
172+
onClick={handleTogglePause}
173+
>
174+
<HeartbeatIcon size={16} />
175+
{schedule.isPaused ? "Resume" : "Pause"}
176+
</DropdownMenuItem>
177+
<DropdownMenuItem
178+
className="text-destructive focus:text-destructive"
179+
disabled={deleteMutation.isPending}
180+
onClick={handleDelete}
181+
>
182+
<TrashIcon size={16} />
183+
Delete
184+
</DropdownMenuItem>
185+
</DropdownMenuContent>
186+
</DropdownMenu>
187+
</div>
188+
);
189+
}

apps/dashboard/app/demo/layout.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1+
import { AutumnProvider } from "autumn-js/react";
12
import { Sidebar } from "@/components/layout/sidebar";
3+
import { BillingProvider } from "@/components/providers/billing-provider";
24

35
export default function DemoLayout({
46
children,
57
}: {
68
children: React.ReactNode;
79
}) {
810
return (
9-
<div className="h-screen overflow-hidden text-foreground">
10-
<Sidebar />
11-
<div className="relative h-screen pl-0 md:pl-76 lg:pl-84">
12-
<div className="h-screen overflow-y-auto overflow-x-hidden pt-16 md:pt-0">
13-
{children}
11+
<AutumnProvider
12+
backendUrl={process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"}
13+
>
14+
<BillingProvider>
15+
<div className="h-screen overflow-hidden text-foreground">
16+
<Sidebar />
17+
<div className="relative h-screen pl-0 md:pl-76 lg:pl-84">
18+
<div className="h-screen overflow-y-auto overflow-x-hidden pt-16 md:pt-0">
19+
{children}
20+
</div>
21+
</div>
1422
</div>
15-
</div>
16-
</div>
23+
</BillingProvider>
24+
</AutumnProvider>
1725
);
1826
}

apps/dashboard/components/layout/navigation/navigation-config.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import {
2222
IdentificationCardIcon,
2323
KeyIcon,
2424
MapPinIcon,
25+
MonitorIcon,
2526
PlayIcon,
2627
PlugIcon,
2728
PlusIcon,
29+
PulseIcon,
2830
ReceiptIcon,
2931
RepeatIcon,
3032
RoadHorizonIcon,
@@ -324,6 +326,55 @@ const createCategoryConfig = (
324326
navigationMap: Record<string, NavigationSection[]>
325327
) => ({ categories, defaultCategory, navigationMap });
326328

329+
export const createPulseNavigation = (
330+
monitors: Array<{
331+
id: string;
332+
websiteId: string | null;
333+
url: string | null;
334+
name: string | null;
335+
website: { id: string; name: string | null; domain: string } | null;
336+
}>
337+
): NavigationSection[] => {
338+
const monitorItems = monitors.map((monitor) => {
339+
const isWebsiteMonitor = !!monitor.websiteId;
340+
const displayName = isWebsiteMonitor
341+
? monitor.website?.name || monitor.website?.domain || "Unknown"
342+
: monitor.name || monitor.url || "Unknown";
343+
const href = isWebsiteMonitor
344+
? `/websites/${monitor.websiteId}/pulse`
345+
: "/pulse";
346+
return createNavItem(displayName, HeartbeatIcon, href, {
347+
highlight: true,
348+
domain: monitor.website?.domain || monitor.url || undefined,
349+
});
350+
});
351+
352+
return [
353+
createNavSection("Monitors", MonitorIcon, [
354+
createNavItem("All Monitors", MonitorIcon, "/pulse", {
355+
highlight: true,
356+
}),
357+
...monitorItems,
358+
]),
359+
createNavSection("Status Pages", GlobeSimpleIcon, [
360+
createNavItem("Status Pages", GlobeSimpleIcon, "/pulse/status-pages", {
361+
disabled: true,
362+
tag: "soon",
363+
}),
364+
]),
365+
];
366+
};
367+
368+
export const createLoadingPulseNavigation = (): NavigationSection[] =>
369+
createLoadingNavigation(
370+
"Monitors",
371+
MonitorIcon,
372+
"All Monitors",
373+
"/pulse",
374+
"Loading monitors...",
375+
HeartbeatIcon
376+
);
377+
327378
export const categoryConfig = {
328379
main: createCategoryConfig(
329380
[
@@ -333,6 +384,12 @@ export const categoryConfig = {
333384
icon: GlobeSimpleIcon,
334385
production: true,
335386
},
387+
{
388+
id: "pulse",
389+
name: "Pulse",
390+
icon: PulseIcon,
391+
production: true,
392+
},
336393
{
337394
id: "organizations",
338395
name: "Organizations",
@@ -362,6 +419,7 @@ export const categoryConfig = {
362419
"websites",
363420
{
364421
websites: [],
422+
pulse: [],
365423
organizations: organizationNavigation,
366424
billing: billingNavigation,
367425
settings: personalNavigation,
@@ -400,6 +458,7 @@ const CATEGORY_PATH_MAP = [
400458
{ pattern: "/organizations", category: "organizations" as const },
401459
{ pattern: "/billing", category: "billing" as const },
402460
{ pattern: "/settings", category: "settings" as const },
461+
{ pattern: "/pulse", category: "pulse" as const },
403462
] as const;
404463

405464
export const getContextConfig = (pathname: string) => {

0 commit comments

Comments
 (0)