Skip to content
Closed
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
32 changes: 29 additions & 3 deletions client/src/api/admin/sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ export type SiteResponse = {
trackIp?: boolean;
};

export type SiteMetrics = {
users: number;
pageviews: number;
bounceRate: number;
sessionDuration: number;
pagesPerSession: number;
sessions: number;
usersChange: number;
pageviewsChange: number;
sessionsChange: number;
bounceRateChange: number;
sessionDurationChange: number;
pagesPerSessionChange: number;
};

export type GetSitesFromOrgResponse = {
organization: {
id: string;
Expand All @@ -54,6 +69,7 @@ export type GetSitesFromOrgResponse = {
blockBots: boolean;
sessionsLast24Hours: number;
isOwner: boolean;
metrics?: SiteMetrics;
}>;
subscription: {
monthlyEventCount: number;
Expand All @@ -65,11 +81,21 @@ export type GetSitesFromOrgResponse = {
};
};

export function useGetSitesFromOrg(organizationId?: string) {
export function useGetSitesFromOrg(
organizationId?: string,
options?: { includeMetrics?: boolean; timePeriod?: "24h" | "7d" | "30d" }
) {
const includeMetrics = options?.includeMetrics ?? false;
const timePeriod = options?.timePeriod ?? "24h";

return useQuery<GetSitesFromOrgResponse>({
queryKey: ["get-sites-from-org", organizationId],
queryKey: ["get-sites-from-org", organizationId, includeMetrics, timePeriod],
queryFn: () => {
return authedFetch(`/get-sites-from-org/${organizationId}`);
const params = new URLSearchParams();
if (includeMetrics) params.append("includeMetrics", "true");
if (timePeriod) params.append("timePeriod", timePeriod);
const queryString = params.toString();
return authedFetch(`/get-sites-from-org/${organizationId}${queryString ? `?${queryString}` : ""}`);
},
staleTime: 60000, // 1 minute
enabled: !!organizationId,
Expand Down
166 changes: 139 additions & 27 deletions client/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,65 @@
"use client";

import { Plus } from "lucide-react";
import { useState } from "react";
import { LayoutGrid, Plus, Table2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useUserOrganizations } from "../api/admin/organizations";
import { useGetSitesFromOrg } from "../api/admin/sites";
import { CreateOrganizationDialog } from "../components/CreateOrganizationDialog";
import { NoOrganization } from "../components/NoOrganization";
import { OrganizationSelector } from "../components/OrganizationSelector";
import { SiteCard } from "../components/SiteCard";
import { SitesOverviewTable } from "../components/SitesOverviewTable";
import { SitesSummaryStats } from "../components/SitesSummaryStats";
import { StandardPage } from "../components/StandardPage";
import { Button } from "../components/ui/button";
import { Card, CardDescription, CardTitle } from "../components/ui/card";
import { useSetPageTitle } from "../hooks/useSetPageTitle";
import { authClient } from "../lib/auth";
import { AddSite } from "./components/AddSite";

type ViewMode = "cards" | "table";
type TimePeriod = "24h" | "7d" | "30d";

export default function Home() {
useSetPageTitle("Rybbit · Home");

const { data: activeOrganization, isPending } = authClient.useActiveOrganization();

const { data: sites, refetch: refetchSites, isLoading: isLoadingSites } = useGetSitesFromOrg(activeOrganization?.id);
// Load view mode and time period from localStorage
const [viewMode, setViewMode] = useState<ViewMode>("cards");
const [timePeriod, setTimePeriod] = useState<TimePeriod>("24h");
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
const savedViewMode = localStorage.getItem("sitesViewMode") as ViewMode;
const savedTimePeriod = localStorage.getItem("sitesTimePeriod") as TimePeriod;
if (savedViewMode) setViewMode(savedViewMode);
if (savedTimePeriod) setTimePeriod(savedTimePeriod);
}, []);

const handleViewModeChange = (mode: ViewMode) => {
setViewMode(mode);
if (isClient) {
localStorage.setItem("sitesViewMode", mode);
}
};

const handleTimePeriodChange = (period: TimePeriod) => {
setTimePeriod(period);
if (isClient) {
localStorage.setItem("sitesTimePeriod", period);
}
};

const {
data: sites,
refetch: refetchSites,
isLoading: isLoadingSites,
} = useGetSitesFromOrg(activeOrganization?.id, {
includeMetrics: viewMode === "table",
timePeriod: timePeriod,
});

const {
data: userOrganizationsData,
Expand Down Expand Up @@ -53,41 +92,114 @@ export default function Home() {
refetchSites();
};

const timePeriodLabel = {
"24h": "Last 24 hours",
"7d": "Last 7 days",
"30d": "Last 30 days",
};

return (
<StandardPage>
<div className="flex justify-between items-center my-4">
<div>
<OrganizationSelector />
</div>
{/* <div className="text-2xl font-bold">{sites?.length} Websites</div> */}
<AddSite disabled={!canAddSites} />
</div>

{/* Organization required message */}
{hasNoOrganizations && <NoOrganization />}

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Sites list */}

{sites?.sites?.map(site => {
return <SiteCard key={site.siteId} siteId={site.siteId} domain={site.domain} />;
})}

{/* No websites message */}
{hasNoSites && (
<Card className="col-span-full p-6 flex flex-col items-center text-center">
<CardTitle className="mb-2 text-xl">No websites yet</CardTitle>
<CardDescription className="mb-4">Add your first website to start tracking analytics</CardDescription>
<AddSite
trigger={
<Button variant="success" disabled={!canAddSites}>
<Plus className="h-4 w-4" />
Add Website
</Button>
}
/>
</Card>
)}
</div>
{/* View controls */}
{shouldShowSites && sites?.sites && sites.sites.length > 0 && (
<div className="flex flex-col sm:flex-row gap-3 sm:justify-between sm:items-center mb-4">
{/* View mode toggle */}
<div className="flex gap-2 bg-neutral-100 dark:bg-neutral-800 p-1 rounded-lg w-fit">
<Button
variant={viewMode === "cards" ? "default" : "ghost"}
size="sm"
onClick={() => handleViewModeChange("cards")}
className="gap-2"
>
<LayoutGrid size={16} />
Cards
</Button>
<Button
variant={viewMode === "table" ? "default" : "ghost"}
size="sm"
onClick={() => handleViewModeChange("table")}
className="gap-2"
>
<Table2 size={16} />
Table
</Button>
</div>

{/* Time period selector */}
<div className="flex gap-2 bg-neutral-100 dark:bg-neutral-800 p-1 rounded-lg w-fit">
{(["24h", "7d", "30d"] as TimePeriod[]).map(period => (
<Button
key={period}
variant={timePeriod === period ? "default" : "ghost"}
size="sm"
onClick={() => handleTimePeriodChange(period)}
>
{timePeriodLabel[period]}
</Button>
))}
</div>
</div>
)}

{/* Cards view */}
{viewMode === "cards" && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sites?.sites?.map(site => {
return <SiteCard key={site.siteId} siteId={site.siteId} domain={site.domain} />;
})}

{/* No websites message */}
{hasNoSites && (
<Card className="col-span-full p-6 flex flex-col items-center text-center">
<CardTitle className="mb-2 text-xl">No websites yet</CardTitle>
<CardDescription className="mb-4">Add your first website to start tracking analytics</CardDescription>
<AddSite
trigger={
<Button variant="success" disabled={!canAddSites}>
<Plus className="h-4 w-4" />
Add Website
</Button>
}
/>
</Card>
)}
</div>
)}

{/* Table view */}
{viewMode === "table" && (
<div>
{hasNoSites ? (
<Card className="p-6 flex flex-col items-center text-center">
<CardTitle className="mb-2 text-xl">No websites yet</CardTitle>
<CardDescription className="mb-4">Add your first website to start tracking analytics</CardDescription>
<AddSite
trigger={
<Button variant="success" disabled={!canAddSites}>
<Plus className="h-4 w-4" />
Add Website
</Button>
}
/>
</Card>
) : (
<>
<SitesSummaryStats sites={sites?.sites ?? []} isLoading={isLoadingSites} />
<SitesOverviewTable sites={sites?.sites ?? []} isLoading={isLoadingSites} />
</>
)}
</div>
)}

<CreateOrganizationDialog
open={createOrgDialogOpen}
Expand Down
Loading