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
6 changes: 6 additions & 0 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ export default defineConfig([
globals: globals.browser,
},
},
{
files: ['src/components/ui/**/*.{ts,tsx}'],
rules: {
'react-refresh/only-export-components': 'off',
},
},
])
38 changes: 38 additions & 0 deletions client/src/api/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { apiFetch } from "./client";

export type Ring = "inner_circle" | "network" | "community" | "acquaintances";

export type DashboardPerson = {
id: number;
name: string;
ring: Ring;
last_connected_at: string | null;
};

export type ReconnectReminder = {
id: number;
due_at: string;
reason: string;
snoozed_until: string | null;
person: DashboardPerson;
};

export type UpcomingDate = {
name: string;
month: number;
day: number;
days_until: number;
};

export type UpcomingGroup = {
person: DashboardPerson;
upcoming_dates: UpcomingDate[];
};

export function fetchReconnectReminders(): Promise<ReconnectReminder[]> {
return apiFetch<ReconnectReminder[]>("/api/dashboard/reconnect");
}

export function fetchUpcomingGroups(): Promise<UpcomingGroup[]> {
return apiFetch<UpcomingGroup[]>("/api/dashboard/upcoming");
}
13 changes: 13 additions & 0 deletions client/src/api/people.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { apiFetch } from "./client";
import type { Ring } from "./dashboard";

export type Person = {
id: number;
name: string;
ring: Ring;
last_connected_at: string | null;
};

export function fetchPeople(): Promise<Person[]> {
return apiFetch<Person[]>("api/people");
}
34 changes: 34 additions & 0 deletions client/src/components/StatCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { cn } from "@/lib/utils";

const TONE_STYLES = {
default: "text-sapphire",
orange: "text-orange-dark",
} as const;

type Tone = keyof typeof TONE_STYLES;

export function StatCard({
label,
value,
tone = "default",
className,
}: {
label: string;
value: number | string;
tone?: Tone;
className?: string;
}) {
return (
<div
className={cn(
"rounded-md border bg-card px-3.5 py-3",
className,
)}
>
<p className="text-meta text-muted-foreground mb-1">{label}</p>
<p className={cn("text-stat font-medium tabular-nums", TONE_STYLES[tone])}>
{value}
</p>
</div>
);
}
24 changes: 24 additions & 0 deletions client/src/dashboard/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { fetchReconnectReminders, fetchUpcomingGroups } from "@/api/dashboard";
import { fetchPeople } from "@/api/people";

export function useReconnectReminders() {
return useQuery({
queryKey: ["dashboard", "reconnect"],
queryFn: fetchReconnectReminders,
});
}

export function useUpcomingGroups() {
return useQuery({
queryKey: ["dashboard", "upcoming"],
queryFn: fetchUpcomingGroups,
});
}

export function usePeople() {
return useQuery({
queryKey: ["people"],
queryFn: fetchPeople,
});
}
48 changes: 43 additions & 5 deletions client/src/routes/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
import { useCurrentUser } from "@/auth/hooks";
import {
useReconnectReminders,
useUpcomingGroups,
usePeople,
} from "@/dashboard/hooks";
import { useDocumentTitle } from "@/lib/use-document-title";
import { getGreeting } from "@/lib/greeting";
import { StatCard } from "@/components/StatCard";

function displayNameFromEmail(email: string | undefined): string {
if (!email) return "";
const localPart = email.split("@")[0] ?? "";
return localPart.charAt(0).toUpperCase() + localPart.slice(1);
}

export default function DashboardPage() {
useDocumentTitle("Dashboard");

const { data: user } = useCurrentUser();
const reconnectQuery = useReconnectReminders();
const upcomingQuery = useUpcomingGroups();
const peopleQuery = usePeople();

const displayName = displayNameFromEmail(user?.email);
const greeting = getGreeting();
const heading = displayName ? `${greeting}, ${displayName}` : greeting;

const reconnectCount = reconnectQuery.data?.length ?? 0;
const upcomingCount = upcomingQuery.data?.length ?? 0;
const peopleCount = peopleQuery.data?.length ?? 0;

return (
<div className="space-y-2">
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
<p className="text-sm text-muted-foreground">
Reconnect prompts and upcoming events land here in M6.
</p>
<div className="space-y-6">
<h1 className="text-stat font-medium tracking-tight">{heading}</h1>

<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-3">
<StatCard
label="Time to reconnect"
value={reconnectCount}
tone="orange"
/>
<StatCard label="Coming up soon" value={upcomingCount} />
<StatCard label="In your cirle" value={peopleCount} />
</div>
</div>
);
}
4 changes: 1 addition & 3 deletions client/src/test/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ describe("App auth shell", () => {
await user.click(screen.getByRole("button", { name: /sign in/i }));

await waitFor(() => {
expect(
screen.getByRole("heading", { name: /dashboard/i }),
).toBeInTheDocument();
expect(screen.getByText(/time to reconnect/i)).toBeInTheDocument();
});
});

Expand Down
12 changes: 12 additions & 0 deletions client/src/test/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ export const handlers = [
authState.user = null;
return new HttpResponse(null, { status: 204 });
}),

http.get(`${API_BASE_URL}/api/dashboard/reconnect`, () => {
return HttpResponse.json([]);
}),

http.get(`${API_BASE_URL}/api/dashboard/upcoming`, () => {
return HttpResponse.json([]);
}),

http.get(`${API_BASE_URL}/api/people`, () => {
return HttpResponse.json([]);
}),
];
Loading