feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
188
apps/web/src/modules/organization/home/charts/pie.tsx
Normal file
188
apps/web/src/modules/organization/home/charts/pie.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import * as React from "react";
|
||||
import { Label, Pie, PieChart as RechartsPieChart, Sector } from "recharts";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@turbostarter/ui-web/card";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartStyle,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@turbostarter/ui-web/chart";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@turbostarter/ui-web/select";
|
||||
|
||||
import type { ChartConfig } from "@turbostarter/ui-web/chart";
|
||||
import type { PieSectorDataItem } from "recharts/types/polar/Pie";
|
||||
|
||||
const desktopData = [
|
||||
{ month: "january", desktop: 186, fill: "var(--chart-1)" },
|
||||
{ month: "february", desktop: 305, fill: "var(--chart-2)" },
|
||||
{ month: "march", desktop: 237, fill: "var(--chart-3)" },
|
||||
{ month: "april", desktop: 173, fill: "var(--chart-4)" },
|
||||
{ month: "may", desktop: 209, fill: "var(--chart-5)" },
|
||||
];
|
||||
|
||||
const chartConfig = {
|
||||
january: {
|
||||
label: dayjs().month(0).format("MMMM"),
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
february: {
|
||||
label: dayjs().month(1).format("MMMM"),
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
march: {
|
||||
label: dayjs().month(2).format("MMMM"),
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
april: {
|
||||
label: dayjs().month(3).format("MMMM"),
|
||||
color: "var(--chart-4)",
|
||||
},
|
||||
may: {
|
||||
label: dayjs().month(4).format("MMMM"),
|
||||
color: "var(--chart-5)",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function PieChart() {
|
||||
const { t, i18n } = useTranslation(["common", "dashboard"]);
|
||||
const id = "pie-interactive";
|
||||
const [activeMonth, setActiveMonth] = React.useState(
|
||||
desktopData[0]?.month ?? "january",
|
||||
);
|
||||
|
||||
const activeIndex = React.useMemo(
|
||||
() => desktopData.findIndex((item) => item.month === activeMonth),
|
||||
[activeMonth],
|
||||
);
|
||||
const months = desktopData.map((item) => item.month);
|
||||
|
||||
return (
|
||||
<Card data-chart={id} className="flex flex-col">
|
||||
<ChartStyle id={id} config={chartConfig} />
|
||||
<CardHeader className="flex-row items-start space-y-0 pb-0">
|
||||
<div className="grid gap-0.5">
|
||||
<CardTitle className="text-xl">{t("chart.pie")}</CardTitle>
|
||||
<CardDescription>{t("chart.period")}</CardDescription>
|
||||
</div>
|
||||
<Select value={activeMonth} onValueChange={setActiveMonth}>
|
||||
<SelectTrigger
|
||||
className="ml-auto w-[130px] rounded-lg pl-3"
|
||||
aria-label={t("selectMonth")}
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder={t("selectMonth")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end" className="rounded-xl">
|
||||
{months.map((key) => {
|
||||
const config = chartConfig[key as keyof typeof chartConfig];
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
key={key}
|
||||
value={key}
|
||||
className="rounded-lg [&_span]:flex"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="flex h-3 w-3 shrink-0 rounded-sm"
|
||||
style={{
|
||||
backgroundColor:
|
||||
"color" in config ? config.color : undefined,
|
||||
}}
|
||||
/>
|
||||
{config.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 justify-center pb-0">
|
||||
<ChartContainer
|
||||
id={id}
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square w-full max-w-[300px]"
|
||||
>
|
||||
<RechartsPieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={desktopData}
|
||||
dataKey="desktop"
|
||||
nameKey="month"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={({
|
||||
outerRadius = 0,
|
||||
...props
|
||||
}: PieSectorDataItem) => (
|
||||
<g>
|
||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
||||
<Sector
|
||||
{...props}
|
||||
outerRadius={outerRadius + 25}
|
||||
innerRadius={outerRadius + 12}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
const data = desktopData[activeIndex];
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-3xl font-bold"
|
||||
>
|
||||
{data.desktop.toLocaleString(i18n.language)}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy ?? 0) + 24}
|
||||
className="fill-muted-foreground"
|
||||
>
|
||||
{t("visitors")}
|
||||
</tspan>
|
||||
</text>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</RechartsPieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user