Merge pull request #58 from sai80082/ui-enhance
New Workflow for PR's and Heatmap for past one year assets
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
name: Build and Tag Docker Image on PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract PR number
|
||||
id: pr-number
|
||||
run: echo "PR_NUMBER=${{ github.event.number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
docker build --build-arg VERSION=pr-${{ env.PR_NUMBER }} \
|
||||
-t ghcr.io/${{ github.repository }}:pr-${{ env.PR_NUMBER }} .
|
||||
@@ -17,6 +17,7 @@
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
@@ -30,6 +31,7 @@
|
||||
"@tanstack/react-table": "^8.20.1",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/react-calendar-heatmap": "^1.6.7",
|
||||
"axios": "^1.7.4",
|
||||
"chrono-node": "^1.4.9",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
@@ -44,6 +46,7 @@
|
||||
"pg": "^8.12.0",
|
||||
"qs": "^6.13.0",
|
||||
"react": "^18",
|
||||
"react-calendar-heatmap": "^1.9.0",
|
||||
"react-day-picker": "9.0.8",
|
||||
"react-dom": "^18",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getHeatMapData } from "@/handlers/api/analytics.handler";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
|
||||
type HeatMapEntry = {
|
||||
date: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export default function AssetHeatMap() {
|
||||
const { exImmichUrl } = useConfig();
|
||||
|
||||
const [heatMapData, setHeatMapData] = useState<HeatMapEntry[][]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [weeksPerMonth, setWeeksPerMonth] = useState<number[]>([]); // Store weeks per month
|
||||
|
||||
const fetchHeatMapData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getHeatMapData();
|
||||
setHeatMapData(formatHeatMapData(data));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchHeatMapData();
|
||||
}, []);
|
||||
|
||||
const currentDate = new Date();
|
||||
const months: string[] = [];
|
||||
|
||||
// Build an array of the last 12 months
|
||||
for (let i = 0; i < 12; i++) {
|
||||
months.push(currentDate.toLocaleString("default", { month: "short" }));
|
||||
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||
}
|
||||
months.reverse();
|
||||
|
||||
// Flatten heatMapData to calculate min and max counts
|
||||
const flattenedData = heatMapData.flat();
|
||||
const minCount = Math.min(...flattenedData.map((entry) => entry.count));
|
||||
const maxCount = Math.max(...flattenedData.map((entry) => entry.count));
|
||||
|
||||
// Check if a week contains a date with the first day of the month
|
||||
const hasFirstDayOfMonth = (arr: HeatMapEntry[]) => {
|
||||
return arr.some((entry) => new Date(entry.date).getDate() === 1);
|
||||
};
|
||||
|
||||
const getColor = (count: number) => {
|
||||
if (count === -1) return "";
|
||||
|
||||
// Calculate thresholds based on the data range
|
||||
const range = maxCount - minCount;
|
||||
const threshold1 = minCount + range * 0.2;
|
||||
const threshold2 = minCount + range * 0.4;
|
||||
const threshold3 = minCount + range * 0.6;
|
||||
const threshold4 = minCount + range * 0.8;
|
||||
if (count === 0) return "bg-zinc-800";
|
||||
if (count <= threshold1) return "bg-green-200";
|
||||
if (count <= threshold2) return "bg-green-400";
|
||||
if (count <= threshold3) return "bg-green-500";
|
||||
if (count <= threshold4) return "bg-green-600";
|
||||
return "bg-green-800";
|
||||
};
|
||||
|
||||
const formatHeatMapData = (data: HeatMapEntry[]) => {
|
||||
const chunks = [];
|
||||
const weeksPerMonthTemp: number[] = [];
|
||||
let previousMonthIndex = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 7) {
|
||||
const thisWeek = data.slice(i, i + 7);
|
||||
chunks.push(thisWeek);
|
||||
|
||||
if (hasFirstDayOfMonth(thisWeek)) {
|
||||
const changeWeek = (i - previousMonthIndex) / 7;
|
||||
previousMonthIndex = i;
|
||||
weeksPerMonthTemp.push(changeWeek);
|
||||
}
|
||||
}
|
||||
weeksPerMonthTemp.reverse();
|
||||
setWeeksPerMonth(weeksPerMonthTemp);
|
||||
return chunks;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full">
|
||||
<h2 className="text-xl font-bold mb-4">Past Year</h2>
|
||||
{loading ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table-auto border-separate border-spacing-1 w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{weeksPerMonth.map((weeks, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className="text-center"
|
||||
colSpan={weeks}
|
||||
>
|
||||
{months[index]}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{heatMapData[0]?.map((_, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{heatMapData.map((column, colIndex) => (
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`h-6 ${getColor(column[rowIndex]?.count ?? -1)} rounded-lg`}
|
||||
style={{ width: `calc(${100 / 60}%)` }}
|
||||
>
|
||||
{
|
||||
column[rowIndex]?.date ? (
|
||||
<a
|
||||
href={`${exImmichUrl}/search?query=%7B%22takenAfter%22%3A%22${column[rowIndex]?.date ?? "N/A"}T00%3A00%3A00.000Z%22%2C%22takenBefore%22%3A%22${column[rowIndex]?.date ?? "N/A"}T23%3A59%3A59.999Z%22%7D`}
|
||||
className="block h-full w-full"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
className="h-full w-full"
|
||||
title={`Date: ${column[rowIndex]?.date ?? "N/A"}, Count: ${column[rowIndex]?.count ?? 0}`}
|
||||
/>
|
||||
</a>) : <div
|
||||
className="h-full w-full"
|
||||
/>
|
||||
}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
@@ -0,0 +1,100 @@
|
||||
import { CartesianGrid, LabelList, Line, LineChart, XAxis, YAxis } from "recharts";
|
||||
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { useMemo } from "react";
|
||||
import { CHART_COLORS } from "@/config/constants/chart.constant";
|
||||
|
||||
export interface ILineChartData {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface ChartProps {
|
||||
data: ILineChartData[];
|
||||
topLabel?: string;
|
||||
loading?: boolean;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
export function LineChartDots({
|
||||
data: _data,
|
||||
topLabel,
|
||||
loading,
|
||||
errorMessage,
|
||||
}: ChartProps) {
|
||||
const data = useMemo(
|
||||
() =>
|
||||
_data.map((item, index) => ({
|
||||
...item,
|
||||
fill: CHART_COLORS[index] || "#000000",
|
||||
})),
|
||||
[_data]
|
||||
);
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
let config: ChartConfig = {};
|
||||
data.map((data) => {
|
||||
config[data.label as string] = {
|
||||
label: data.label,
|
||||
color: "hsl(var(--chart-1))",
|
||||
};
|
||||
});
|
||||
return config;
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return <p>{errorMessage}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={data}
|
||||
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<YAxis /> {/* Added YAxis for the count */}
|
||||
<XAxis padding={{ left: 20 }} />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent indicator="line" nameKey="label" hideLabel />
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
dataKey="value"
|
||||
type="natural"
|
||||
stroke="#FF6B6B"
|
||||
strokeWidth={2}
|
||||
dot={{
|
||||
fill: "var(--color-visitors)",
|
||||
}}
|
||||
activeDot={{
|
||||
r: 6,
|
||||
}}
|
||||
>
|
||||
<LabelList
|
||||
position="top"
|
||||
offset={12}
|
||||
className="fill-foreground"
|
||||
fontSize={12}
|
||||
dataKey="label"
|
||||
formatter={(value: keyof typeof chartConfig) =>
|
||||
chartConfig[value]?.label
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp } from "lucide-react"
|
||||
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart"
|
||||
import { useMemo } from "react"
|
||||
import { CHART_COLORS } from "@/config/constants/chart.constant"
|
||||
|
||||
export const description = "A radar chart with dots"
|
||||
export interface IRadarChartData {
|
||||
label: string;
|
||||
value: number;
|
||||
color?: string; // Add color property here
|
||||
}
|
||||
|
||||
interface ChartProps {
|
||||
data: IRadarChartData[];
|
||||
topLabel?: string;
|
||||
loading?: boolean;
|
||||
errorMessage?: string | null;
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
label: {
|
||||
label: "label",
|
||||
color: "#FF0000",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function RadarChartDots({ data: _data, topLabel, loading, errorMessage }: ChartProps) {
|
||||
const chartData = _data
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
<CardContent className="pb-0 border-none">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square max-h-[250px] border-none"
|
||||
>
|
||||
<RadarChart data={chartData}>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<PolarAngleAxis dataKey="label" />
|
||||
<PolarGrid stroke="#FFFFFF" strokeDasharray="3 3" /> {/* Set grid color to white */}
|
||||
<Radar
|
||||
dataKey="value"
|
||||
fill="#FF0000"
|
||||
fillOpacity={0.8}
|
||||
dot={({ cx, cy, payload }) => (
|
||||
<circle
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={4}
|
||||
fill={payload.color} // Set color dynamically
|
||||
fillOpacity={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -36,5 +36,10 @@ export const ASSET_VIDEO_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/vi
|
||||
|
||||
export const SEARCH_PLACES_PATH = BASE_PROXY_ENDPOINT + "/search/places";
|
||||
|
||||
//Analytics
|
||||
|
||||
export const ASSET_STATISTICS = BASE_PROXY_ENDPOINT + "/assets/statistics";
|
||||
export const LIVE_PHOTO_STATISTICS = BASE_API_ENDPOINT + "/analytics/statistics/livephoto";
|
||||
export const HEATMAP_DATA = BASE_API_ENDPOINT + "/analytics/statistics/heatmap";
|
||||
// Common
|
||||
export const GET_FILTERS = BASE_API_ENDPOINT + "/filters/asset-filters";
|
||||
export const GET_FILTERS = BASE_API_ENDPOINT + "/filters/asset-filters";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EXIF_DISTRIBUTION_PATH } from "@/config/routes";
|
||||
import { ASSET_STATISTICS, EXIF_DISTRIBUTION_PATH, HEATMAP_DATA, LIVE_PHOTO_STATISTICS } from "@/config/routes";
|
||||
import API from "@/lib/api";
|
||||
|
||||
export type ISupportedEXIFColumns =
|
||||
@@ -7,3 +7,15 @@ export type ISupportedEXIFColumns =
|
||||
export const getExifDistribution = async (column: ISupportedEXIFColumns) => {
|
||||
return API.get(EXIF_DISTRIBUTION_PATH(column));
|
||||
};
|
||||
|
||||
export const getAssetStatistics = async () => {
|
||||
return API.get(ASSET_STATISTICS);
|
||||
}
|
||||
|
||||
export const getLivePhotoStatistics = async () => {
|
||||
return API.get(LIVE_PHOTO_STATISTICS);
|
||||
}
|
||||
|
||||
export const getHeatMapData = async () => {
|
||||
return API.get(HEATMAP_DATA);
|
||||
}
|
||||
@@ -1,7 +1,20 @@
|
||||
import { Inter } from "next/font/google";
|
||||
import PageLayout from "@/components/layouts/PageLayout";
|
||||
import Header from "@/components/shared/Header";
|
||||
import EXIFDistribution, { IEXIFDistributionProps } from "@/components/analytics/exif/EXIFDistribution";
|
||||
import EXIFDistribution, {
|
||||
IEXIFDistributionProps,
|
||||
} from "@/components/analytics/exif/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";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
@@ -42,36 +55,86 @@ const exifCharts: IEXIFDistributionProps[] = [
|
||||
description: "Distribution of ISO",
|
||||
},
|
||||
{
|
||||
column: 'exposureTime',
|
||||
title: 'Exposure Time',
|
||||
description: 'Distribution of exposure time'
|
||||
column: "exposureTime",
|
||||
title: "Exposure Time",
|
||||
description: "Distribution of exposure time",
|
||||
},
|
||||
{
|
||||
column: 'lensModel',
|
||||
title: 'Lens Model',
|
||||
description: 'Distribution of lens model'
|
||||
column: "lensModel",
|
||||
title: "Lens Model",
|
||||
description: "Distribution of lens model",
|
||||
},
|
||||
{
|
||||
column: 'projectionType',
|
||||
title: 'Projection Type',
|
||||
description: 'Distribution of projection type'
|
||||
}
|
||||
]
|
||||
column: "projectionType",
|
||||
title: "Projection Type",
|
||||
description: "Distribution of projection type",
|
||||
},
|
||||
];
|
||||
export default function ExifDataAnalytics() {
|
||||
const [statistics, setStatistics] = useState({ images: 0, videos: 0, total: 0 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [livePhotoStatistics, setLivePhotoStatistics] = useState({ total: 0 });
|
||||
|
||||
const fetchLivePhotoStatistics = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getLivePhotoStatistics();
|
||||
const livePhotoData = Array.isArray(data) && data.length ? data[0].value : 0;
|
||||
setLivePhotoStatistics({ total: livePhotoData });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStatisticsData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getAssetStatistics();
|
||||
setStatistics(data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatisticsData();
|
||||
fetchLivePhotoStatistics();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<Header
|
||||
leftComponent="Exif Data"
|
||||
/>
|
||||
<Header leftComponent="Exif Data" />
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{["Total", "Images", "Videos"].map((type, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-lg font-mono">{type}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center text-lg font-mono">
|
||||
{loading ? "Loading..." : statistics[type.toLowerCase() as keyof typeof statistics].toLocaleString()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-lg font-mono">Live Photos</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-center text-lg font-mono">
|
||||
{loading ? "Loading..." : livePhotoStatistics.total.toLocaleString()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<AssetHeatMap />
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{exifCharts.map((chart) => (
|
||||
<EXIFDistribution
|
||||
<EXIFDistribution
|
||||
key={chart.column}
|
||||
column={chart.column}
|
||||
title={chart.title}
|
||||
description={chart.description}
|
||||
/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { assets, exif } from "@/schema";
|
||||
import { and, count, desc, eq, gte, isNotNull, ne, sql, } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
// Helper function to format date as YYYY-MM-DD
|
||||
function formatDate(date: Date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
function fillMissingDates(data: any[]) {
|
||||
const today = new Date();
|
||||
|
||||
// Set the start date as one year ago, excluding the current month in the previous year
|
||||
const oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth() + 1, 1); // First day of the next month last year
|
||||
|
||||
const existingDates = new Set(data.map(item => item.date));
|
||||
|
||||
const filledData = [];
|
||||
|
||||
// Iterate from today to one year ago, but skip the current month from last year
|
||||
for (let d = new Date(today); d >= oneYearAgo; d.setDate(d.getDate() - 1)) {
|
||||
const dateStr = formatDate(d);
|
||||
|
||||
// Skip dates in the current month of the previous year
|
||||
if (d.getFullYear() === today.getFullYear() - 1 && d.getMonth() === today.getMonth()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingDates.has(dateStr)) {
|
||||
filledData.push(data.find(item => item.date === dateStr));
|
||||
} else {
|
||||
filledData.push({ date: dateStr, count: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the filled data in descending order
|
||||
filledData.sort((a, b) => a.date.localeCompare(b.date));
|
||||
return filledData;
|
||||
}
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
|
||||
try {
|
||||
const currentUser = await getCurrentUser(req);
|
||||
const dataFromDB = await db.select({
|
||||
date: sql`DATE(${assets.fileCreatedAt})`.as('date'),
|
||||
count: count(),
|
||||
})
|
||||
.from(assets)
|
||||
.where(
|
||||
and(
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
gte(assets.fileCreatedAt, sql`CURRENT_DATE - INTERVAL '1 YEAR'`))
|
||||
)
|
||||
.groupBy(sql`DATE(${assets.fileCreatedAt})`)
|
||||
.orderBy(sql`DATE(${assets.fileCreatedAt}) DESC`)
|
||||
const updatedData = fillMissingDates(dataFromDB);
|
||||
return res.status(200).json(updatedData);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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";
|
||||
import { and, count, desc, eq, isNotNull, ne } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
|
||||
try {
|
||||
const currentUser = await getCurrentUser(req);
|
||||
const dataFromDB = await db.select({
|
||||
value: count(),
|
||||
})
|
||||
.from(assets)
|
||||
.where(and(
|
||||
eq(assets.ownerId, currentUser.id),
|
||||
isNotNull(assets.livePhotoVideoId))
|
||||
);
|
||||
return res.status(200).json(dataFromDB);
|
||||
} catch (error: any) {
|
||||
res.status(500).json({
|
||||
error: error?.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user