diff --git a/README.md b/README.md index 534b102..56eef5e 100644 --- a/README.md +++ b/README.md @@ -85,14 +85,16 @@ bun run dev - [x] Manage People - [x] Smart Merge -- [ ] Manage Albums +- [x] Manage Albums - [ ] Bulk Delete - [ ] Bulk Edit - [ ] Filters - [x] Potential Albums -- [ ] Statistics + - [x] People in Album +- [x] Missing Location +- [x] Statistics - [x] EXIF Data - - [ ] Assets Overtime Chart + - [x] Assets Overtime Chart **Tech Related** diff --git a/src/components/analytics/exif/EXIFDistribution.tsx b/src/components/analytics/exif/EXIFDistribution.tsx index 807b209..b53ac40 100644 --- a/src/components/analytics/exif/EXIFDistribution.tsx +++ b/src/components/analytics/exif/EXIFDistribution.tsx @@ -2,15 +2,17 @@ import { Card } from "@/components/ui/card"; import PieChart, { IPieChartData } from "@/components/ui/pie-chart"; import { getExifDistribution, ISupportedEXIFColumns } from "@/handlers/api/analytics.handler"; import React, { useEffect, useState } from "react"; +import { ValueType } from "recharts/types/component/DefaultTooltipContent"; export interface IEXIFDistributionProps { column: ISupportedEXIFColumns; title: string; description: string; + tooltipValueFormatter?: (value?: number | string | undefined | ValueType) => string; } export default function EXIFDistribution( - { column, title, description }: IEXIFDistributionProps + { column, title, description, tooltipValueFormatter }: IEXIFDistributionProps ) { const [chartData, setChartData] = useState([]); const [loading, setLoading] = useState(false); @@ -26,13 +28,12 @@ export default function EXIFDistribution( fetchData(); }, [column]); - return ( - + ); } diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index b58b494..431f037 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -114,6 +114,7 @@ const ChartTooltipContent = React.forwardRef< indicator?: "line" | "dot" | "dashed" nameKey?: string labelKey?: string + valueFormatter?: (value: number | string | undefined | ValueType) => string } >( ( @@ -126,6 +127,7 @@ const ChartTooltipContent = React.forwardRef< hideIndicator = false, label, labelFormatter, + valueFormatter = (value) => value?.toLocaleString(), labelClassName, formatter, color, @@ -243,7 +245,7 @@ const ChartTooltipContent = React.forwardRef< {item.value && ( - {item.value.toLocaleString()} + {valueFormatter(item.value)} )} diff --git a/src/components/ui/pie-chart.tsx b/src/components/ui/pie-chart.tsx index f8b58ed..8a33e34 100644 --- a/src/components/ui/pie-chart.tsx +++ b/src/components/ui/pie-chart.tsx @@ -2,17 +2,18 @@ import { CHART_COLORS, PIE_CONFIG } from "@/config/constants/chart.constant"; import React, { useMemo } from "react"; import { Label, Pie, PieChart as PieChartRoot } from "recharts"; import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart"; -import { int } from "drizzle-orm/mysql-core"; +import { ValueType } from "recharts/types/component/DefaultTooltipContent"; -export interface IPieChartData { label: string; value: number } +export interface IPieChartData { label: string; value: number; valueLabel?: string } interface ChartProps { data: IPieChartData[]; topLabel?: string; loading?: boolean; errorMessage?: string | null; + tooltipValueFormatter?: (value: number | string | undefined | ValueType) => string; } -export default function PieChart({ data: _data, topLabel, loading, errorMessage }: ChartProps) { +export default function PieChart({ data: _data, topLabel, loading, errorMessage, tooltipValueFormatter }: ChartProps) { const topItem = _data[0]; const data = useMemo(() => _data.map((item, index) => ({ @@ -45,7 +46,12 @@ export default function PieChart({ data: _data, topLabel, loading, errorMessage } + content={} /> - {topItem.value.toLocaleString()} + {(topItem.valueLabel || topItem.value).toLocaleString()} { return API.get(EXIF_DISTRIBUTION_PATH(column)); diff --git a/src/pages/analytics/exif.tsx b/src/pages/analytics/exif.tsx index 6a98b65..7e6a4f7 100644 --- a/src/pages/analytics/exif.tsx +++ b/src/pages/analytics/exif.tsx @@ -7,18 +7,22 @@ import EXIFDistribution, { import { Card, CardContent, - CardDescription, - CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import AssetHeatMap from "@/components/analytics/exif/AssetHeatMap"; import { useEffect, useState } from "react"; import { getAssetStatistics, getLivePhotoStatistics } from "@/handlers/api/analytics.handler"; +import { humanizeBytes } from "@/helpers/string.helper"; -const inter = Inter({ subsets: ["latin"] }); const exifCharts: IEXIFDistributionProps[] = [ + { + column: "storage", + title: "Storage", + description: "Distribution of storage", + tooltipValueFormatter: (value) => humanizeBytes(value as number * 1000000), + }, { column: "make", title: "Make", @@ -132,6 +136,7 @@ export default function ExifDataAnalytics() { column={chart.column} title={chart.title} description={chart.description} + tooltipValueFormatter={chart.tooltipValueFormatter} /> ))} diff --git a/src/pages/api/analytics/exif/[property].ts b/src/pages/api/analytics/exif/[property].ts index 13e99ed..0fe51d9 100644 --- a/src/pages/api/analytics/exif/[property].ts +++ b/src/pages/api/analytics/exif/[property].ts @@ -1,5 +1,4 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import { CHART_COLORS } from "@/config/constants/chart.constant"; import { db } from "@/config/db"; import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; import { assets, exif } from "@/schema"; @@ -16,7 +15,7 @@ const columnMap = { iso: exif.iso, exposureTime: exif.exposureTime, lensModel: exif.lensModel, - projectionType: exif.projectionType + projectionType: exif.projectionType, } export default async function handler( diff --git a/src/pages/api/analytics/exif/storage.ts b/src/pages/api/analytics/exif/storage.ts new file mode 100644 index 0000000..285ab28 --- /dev/null +++ b/src/pages/api/analytics/exif/storage.ts @@ -0,0 +1,33 @@ +import { db } from "@/config/db"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { humanizeBytes } from "@/helpers/string.helper"; +import { assets, exif, users } from "@/schema"; +import { Value } from "@radix-ui/react-select"; +import { desc, eq } from "drizzle-orm"; +import { sum } from "drizzle-orm"; +import { NextApiResponse } from "next"; + +import { NextApiRequest } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const currentUser = await getCurrentUser(req); + if (!currentUser) { + return res.status(401).json({ error: "Unauthorized" }); + } + const dbStorage = await db.select({ + label: users.name, + value: sum(exif.fileSizeInByte) + }) + .from(assets) + .leftJoin(exif, eq(assets.id, exif.assetId)) + .leftJoin(users, eq(assets.ownerId, users.id)) + .orderBy(desc(sum(exif.fileSizeInByte))) + .groupBy(assets.ownerId, users.name) + + const cleanedData = dbStorage.map((item) => ({ + label: item.label, + value: Math.round((parseInt(item.value ?? "0") / 1000000)), + valueLabel: humanizeBytes(parseInt(item.value ?? "0")), + })); + res.status(200).json(cleanedData); +} \ No newline at end of file