Skip to content

Commit 8bff017

Browse files
committed
feat: experiemental home page
1 parent bdce1a3 commit 8bff017

File tree

6 files changed

+979
-3
lines changed

6 files changed

+979
-3
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"use client";
2+
3+
import type {
4+
ProcessedMiniChartData,
5+
Website,
6+
} from "@databuddy/shared/types/website";
7+
import {
8+
CodeIcon,
9+
EyeIcon,
10+
MinusIcon,
11+
PlusIcon,
12+
TrendDownIcon,
13+
TrendUpIcon,
14+
} from "@phosphor-icons/react";
15+
import Link from "next/link";
16+
import { FaviconImage } from "@/components/analytics/favicon-image";
17+
import { Button } from "@/components/ui/button";
18+
import { Card } from "@/components/ui/card";
19+
import { Skeleton } from "@/components/ui/skeleton";
20+
import { cn } from "@/lib/utils";
21+
22+
interface CompactWebsiteCardProps {
23+
website: Website;
24+
chartData?: ProcessedMiniChartData;
25+
activeUsers?: number;
26+
}
27+
28+
function formatNumber(num: number) {
29+
if (num >= 1_000_000) {
30+
return `${(num / 1_000_000).toFixed(1)}M`;
31+
}
32+
if (num >= 1000) {
33+
return `${(num / 1000).toFixed(1)}K`;
34+
}
35+
return num.toString();
36+
}
37+
38+
function CompactWebsiteCard({
39+
website,
40+
chartData,
41+
activeUsers,
42+
}: CompactWebsiteCardProps) {
43+
const hasData = chartData && chartData.totalViews > 0;
44+
const trend = chartData?.trend;
45+
46+
return (
47+
<Link
48+
className="group block rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
49+
href={`/websites/${website.id}`}
50+
>
51+
<Card className="flex h-full items-center gap-3 p-3 transition-colors group-hover:border-primary/60">
52+
<FaviconImage
53+
altText={`${website.name} favicon`}
54+
className="shrink-0"
55+
domain={website.domain}
56+
size={32}
57+
/>
58+
<div className="min-w-0 flex-1">
59+
<p className="truncate font-medium text-foreground text-sm">
60+
{website.name || website.domain}
61+
</p>
62+
<p className="truncate text-muted-foreground text-xs">
63+
{website.domain}
64+
</p>
65+
</div>
66+
{activeUsers !== undefined && activeUsers > 0 && (
67+
<div className="flex shrink-0 items-center gap-1 rounded-full bg-success/10 px-1.5 py-0.5 text-success text-xs tabular-nums">
68+
<span className="relative flex size-1.5">
69+
<span className="absolute inline-flex size-full animate-ping rounded-full bg-success opacity-75" />
70+
<span className="relative inline-flex size-1.5 rounded-full bg-success" />
71+
</span>
72+
{activeUsers}
73+
</div>
74+
)}
75+
<div className="flex shrink-0 flex-col items-end gap-0.5">
76+
{hasData ? (
77+
<>
78+
<span className="flex items-center gap-1 font-medium text-foreground text-xs tabular-nums">
79+
<EyeIcon
80+
className="size-3.5 text-muted-foreground"
81+
weight="duotone"
82+
/>
83+
{formatNumber(chartData.totalViews)}
84+
</span>
85+
{trend && (
86+
<span
87+
className={cn(
88+
"flex items-center gap-0.5 text-xs tabular-nums",
89+
trend.type === "up" && "text-success",
90+
trend.type === "down" && "text-destructive",
91+
trend.type === "neutral" && "text-muted-foreground"
92+
)}
93+
>
94+
{trend.type === "up" && (
95+
<TrendUpIcon className="size-3" weight="fill" />
96+
)}
97+
{trend.type === "down" && (
98+
<TrendDownIcon className="size-3" weight="fill" />
99+
)}
100+
{trend.type === "neutral" && <MinusIcon className="size-3" />}
101+
{trend.type === "up" && "+"}
102+
{trend.type === "down" && "-"}
103+
{trend.value.toFixed(0)}%
104+
</span>
105+
)}
106+
</>
107+
) : (
108+
<span className="flex items-center gap-1 text-amber-500 text-xs">
109+
<CodeIcon className="size-3.5" weight="duotone" />
110+
Setup
111+
</span>
112+
)}
113+
</div>
114+
</Card>
115+
</Link>
116+
);
117+
}
118+
119+
function CompactWebsiteCardSkeleton() {
120+
return (
121+
<Card className="flex items-center gap-3 p-3">
122+
<Skeleton className="size-8 shrink-0 rounded" />
123+
<div className="min-w-0 flex-1 space-y-1">
124+
<Skeleton className="h-4 w-24" />
125+
<Skeleton className="h-3 w-32" />
126+
</div>
127+
<div className="flex flex-col items-end gap-1">
128+
<Skeleton className="h-3.5 w-10" />
129+
<Skeleton className="h-3 w-8" />
130+
</div>
131+
</Card>
132+
);
133+
}
134+
135+
interface CompactWebsiteGridProps {
136+
websites: Website[];
137+
chartData?: Record<string, ProcessedMiniChartData>;
138+
activeUsers?: Record<string, number>;
139+
isLoading?: boolean;
140+
onAddWebsiteAction: () => void;
141+
}
142+
143+
export function CompactWebsiteGrid({
144+
websites,
145+
chartData,
146+
activeUsers,
147+
isLoading,
148+
onAddWebsiteAction,
149+
}: CompactWebsiteGridProps) {
150+
if (isLoading) {
151+
return (
152+
<div className="space-y-3">
153+
<div className="flex items-center justify-between">
154+
<h2 className="font-semibold text-foreground text-sm">
155+
Your Websites
156+
</h2>
157+
</div>
158+
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
159+
{Array.from({ length: 6 }).map((_, i) => (
160+
<CompactWebsiteCardSkeleton key={`skeleton-${i}`} />
161+
))}
162+
</div>
163+
</div>
164+
);
165+
}
166+
167+
return (
168+
<div className="space-y-3">
169+
<div className="flex items-center justify-between">
170+
<h2 className="font-semibold text-foreground text-sm">Your Websites</h2>
171+
<Link
172+
className="text-muted-foreground text-xs hover:text-foreground"
173+
href="/websites"
174+
>
175+
View all
176+
</Link>
177+
</div>
178+
{websites.length === 0 ? (
179+
<Card className="flex flex-col items-center justify-center gap-3 p-8 text-center">
180+
<div className="flex size-12 items-center justify-center rounded-full bg-accent">
181+
<PlusIcon
182+
className="size-6 text-accent-foreground"
183+
weight="duotone"
184+
/>
185+
</div>
186+
<div>
187+
<p className="font-medium text-foreground text-sm">
188+
No websites yet
189+
</p>
190+
<p className="text-muted-foreground text-xs">
191+
Add your first website to start tracking analytics
192+
</p>
193+
</div>
194+
<Button onClick={onAddWebsiteAction} size="sm">
195+
<PlusIcon className="size-4" />
196+
Add Website
197+
</Button>
198+
</Card>
199+
) : (
200+
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
201+
{websites.map((website) => (
202+
<CompactWebsiteCard
203+
activeUsers={activeUsers?.[website.id]}
204+
chartData={chartData?.[website.id]}
205+
key={website.id}
206+
website={website}
207+
/>
208+
))}
209+
<button
210+
className="flex items-center justify-center gap-2 rounded border border-dashed bg-card p-3 text-muted-foreground text-sm transition-colors hover:border-primary/60 hover:text-foreground"
211+
onClick={onAddWebsiteAction}
212+
type="button"
213+
>
214+
<PlusIcon className="size-4" />
215+
Add Website
216+
</button>
217+
</div>
218+
)}
219+
</div>
220+
);
221+
}

0 commit comments

Comments
 (0)