From d6bd5d2f6da8d317ad842643ac5b12d7f56817e5 Mon Sep 17 00:00:00 2001 From: Sai charan Guntupalli <63032681+sai80082@users.noreply.github.com> Date: Wed, 4 Sep 2024 21:42:39 +0530 Subject: [PATCH 1/7] Create pr-release.yml --- .github/workflows/pr-release.yml | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/pr-release.yml diff --git a/.github/workflows/pr-release.yml b/.github/workflows/pr-release.yml new file mode 100644 index 0000000..4fda809 --- /dev/null +++ b/.github/workflows/pr-release.yml @@ -0,0 +1,33 @@ +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 }} . + docker push ghcr.io/${{ github.repository }}:pr-${{ env.PR_NUMBER }} From dbfb10b73ae1b8ab0c38a55b40c3c6448de933e0 Mon Sep 17 00:00:00 2001 From: sai80082 Date: Thu, 5 Sep 2024 22:30:21 +0530 Subject: [PATCH 2/7] feat: new heatmap in analytics --- package.json | 3 + .../analytics/exif/contributiongraph.tsx | 53 ++++++++++ src/components/ui/hover-card.tsx | 27 +++++ src/components/ui/linechardots.tsx | 100 ++++++++++++++++++ src/components/ui/radarchartdpts.tsx | 78 ++++++++++++++ src/pages/analytics/exif.tsx | 89 +++++++++++++--- 6 files changed, 334 insertions(+), 16 deletions(-) create mode 100644 src/components/analytics/exif/contributiongraph.tsx create mode 100644 src/components/ui/hover-card.tsx create mode 100644 src/components/ui/linechardots.tsx create mode 100644 src/components/ui/radarchartdpts.tsx diff --git a/package.json b/package.json index f616276..9c6c2c0 100644 --- a/package.json +++ b/package.json @@ -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", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -43,6 +45,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", diff --git a/src/components/analytics/exif/contributiongraph.tsx b/src/components/analytics/exif/contributiongraph.tsx new file mode 100644 index 0000000..59a6a26 --- /dev/null +++ b/src/components/analytics/exif/contributiongraph.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +// Function to generate the last 365 days of contribution data +const generateContributions = () => { + const contributions: number[][] = Array.from({ length: 7 }, () => Array(52).fill(0)); + for (let week = 0; week < 52; week++) { + for (let day = 0; day < 7; day++) { + // Generate a random number of contributions for each day (0 to 5 for example) + contributions[day][week] = Math.floor(Math.random() * 6); + } + } + return contributions; +}; + +// Function to determine the background color based on the contribution count +const getColor = (count: number) => { + if (count === 0) return ''; + if (count === 1) return 'bg-green-200'; + if (count === 2) return 'bg-green-400'; + if (count === 3) return 'bg-green-500'; + if (count === 4) return 'bg-green-600'; + return 'bg-green-800'; +}; + +const ContributionGraph: React.FC = () => { + const contributions = generateContributions(); + + return ( +
+

GitHub Contribution Graph - Past Year

+
+ + + {contributions.map((dayContributions, dayIndex) => ( + + {dayContributions.map((count, weekIndex) => ( + + ))} + + ))} + +
+
+
+ ); +}; + +export default ContributionGraph; diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx new file mode 100644 index 0000000..863ff01 --- /dev/null +++ b/src/components/ui/hover-card.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/src/components/ui/linechardots.tsx b/src/components/ui/linechardots.tsx new file mode 100644 index 0000000..748b1c6 --- /dev/null +++ b/src/components/ui/linechardots.tsx @@ -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

Loading...

; + } + + if (errorMessage) { + return

{errorMessage}

; + } + + return ( + + + + {/* Added YAxis for the count */} + + + } + /> + + + chartConfig[value]?.label + } + /> + + + + ); +} diff --git a/src/components/ui/radarchartdpts.tsx b/src/components/ui/radarchartdpts.tsx new file mode 100644 index 0000000..51ddc39 --- /dev/null +++ b/src/components/ui/radarchartdpts.tsx @@ -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 ( + + + + + + } /> + + {/* Set grid color to white */} + ( + + )} + /> + + + + + ) +} diff --git a/src/pages/analytics/exif.tsx b/src/pages/analytics/exif.tsx index fc944dd..cd25596 100644 --- a/src/pages/analytics/exif.tsx +++ b/src/pages/analytics/exif.tsx @@ -1,7 +1,18 @@ 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 ContributionGraph from "@/components/analytics/exif/contributiongraph"; const inter = Inter({ subsets: ["latin"] }); @@ -42,36 +53,82 @@ 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() { return ( -
+
+
+ + + Total Files + + + {(() => { + const a = 700000; + return a.toLocaleString(); + })()} + + + + + Images + + + {(() => { + const a = 600000; + return a.toLocaleString(); + })()} + + + + + Videos + + + {(() => { + const a = 100000; + return a.toLocaleString(); + })()} + + + + + Live + + + {(() => { + const a = 50000; + return a.toLocaleString(); + })()} + + +
+
{exifCharts.map((chart) => ( - ))} +
); From 7df379336d461200d2621f4dd159ab652633bd69 Mon Sep 17 00:00:00 2001 From: sai80082 Date: Thu, 12 Sep 2024 13:58:29 +0000 Subject: [PATCH 3/7] feat: New api endpoint for statistics --- .../analytics/exif/contributiongraph.tsx | 64 +++++++++++++++++-- src/config/routes.ts | 6 +- src/handlers/api/analytics.handler.ts | 6 +- src/pages/analytics/exif.tsx | 46 +++++++++---- 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/src/components/analytics/exif/contributiongraph.tsx b/src/components/analytics/exif/contributiongraph.tsx index 59a6a26..1e9c0a1 100644 --- a/src/components/analytics/exif/contributiongraph.tsx +++ b/src/components/analytics/exif/contributiongraph.tsx @@ -1,4 +1,26 @@ -import React from 'react'; +import React, { useEffect, useState } from "react"; +import { getAssetStatistics } from "@/handlers/api/analytics.handler"; + +// List of month labels +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const weeksPerMonth = [4, 4, 4, 4, 5, 4, 4, 4, 5, 4, 4, 5]; +// Function to map weeks to months +const getMonthLabels = () => { +// Approximate number of weeks per month + const monthLabels = []; + let cumulativeWeeks = 0; + + for (let i = 0; i < months.length; i++) { + monthLabels.push({ + month: months[i], + startWeek: cumulativeWeeks, + span: weeksPerMonth[i], + }); + cumulativeWeeks += weeksPerMonth[i]; + } + + return monthLabels; +}; // Function to generate the last 365 days of contribution data const generateContributions = () => { @@ -14,7 +36,7 @@ const generateContributions = () => { // Function to determine the background color based on the contribution count const getColor = (count: number) => { - if (count === 0) return ''; + if (count === 0) return 'bg-zinc-900'; if (count === 1) return 'bg-green-200'; if (count === 2) return 'bg-green-400'; if (count === 3) return 'bg-green-500'; @@ -24,12 +46,46 @@ const getColor = (count: number) => { const ContributionGraph: React.FC = () => { const contributions = generateContributions(); + const [statistics, setStatistics] = useState({}); + const [loading, setLoading] = useState(false); + + const fetchData = async () => { + setLoading(true); // Set loading to true when starting to fetch data + return getAssetStatistics() + .then((data) => { + setStatistics(data); // Update statistics with the returned data + }) + .finally(() => setLoading(false)); // Stop loading after data is fetched + }; + + const monthLabels = getMonthLabels(); return (
-

GitHub Contribution Graph - Past Year

+

Past Year

+ {/* Month labels */} + {/*
+ {monthLabels.map(({ month, startWeek, span }) => ( +
+ {month} +
+ ))} +
*/} + + + + {months.map((month, index) => ( + + ))} + + + {contributions.map((dayContributions, dayIndex) => ( @@ -38,7 +94,7 @@ const ContributionGraph: React.FC = () => { key={weekIndex} className={`h-6 ${getColor(count)} rounded-lg`} style={{ width: `calc(${100 / 52}%)` }} - title={`${count} contributions`} + title={`${count} Images`} > ))} diff --git a/src/config/routes.ts b/src/config/routes.ts index 4aa8025..dfd0072 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -34,4 +34,8 @@ export const ASSET_VIDEO_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/vi // Location -export const SEARCH_PLACES_PATH = BASE_PROXY_ENDPOINT + "/search/places"; \ No newline at end of file +export const SEARCH_PLACES_PATH = BASE_PROXY_ENDPOINT + "/search/places"; + +//Analytics + +export const ASSET_STATISTICS = BASE_PROXY_ENDPOINT + "/assets/statistics"; \ No newline at end of file diff --git a/src/handlers/api/analytics.handler.ts b/src/handlers/api/analytics.handler.ts index 8215137..933cdd8 100644 --- a/src/handlers/api/analytics.handler.ts +++ b/src/handlers/api/analytics.handler.ts @@ -1,4 +1,4 @@ -import { EXIF_DISTRIBUTION_PATH } from "@/config/routes"; +import { ASSET_STATISTICS, EXIF_DISTRIBUTION_PATH } from "@/config/routes"; import API from "@/lib/api"; export type ISupportedEXIFColumns = @@ -7,3 +7,7 @@ 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); +} \ No newline at end of file diff --git a/src/pages/analytics/exif.tsx b/src/pages/analytics/exif.tsx index cd25596..ed32d6a 100644 --- a/src/pages/analytics/exif.tsx +++ b/src/pages/analytics/exif.tsx @@ -13,6 +13,8 @@ import { CardTitle, } from "@/components/ui/card"; import ContributionGraph from "@/components/analytics/exif/contributiongraph"; +import { useEffect, useState } from "react"; +import { getAssetStatistics } from "@/handlers/api/analytics.handler"; const inter = Inter({ subsets: ["latin"] }); @@ -69,48 +71,65 @@ const exifCharts: IEXIFDistributionProps[] = [ }, ]; export default function ExifDataAnalytics() { + const [statistics, setStatistics] = useState({ images: 0, videos: 0, total: 0 }); + const [loading, setLoading] = useState(false); + + + const fetchStatisticsData = async () => { + setLoading(true); // Set loading to true when starting to fetch data + return getAssetStatistics() + .then((data) => { + console.log("asdasdadasd") + setStatistics(data); // Update statistics with the returned data + }) + .finally(() => setLoading(false)); // Stop loading after data is fetched + }; + + useEffect(() => { + fetchStatisticsData(); + }, []); return (
- Total Files + Total Files - + {(() => { - const a = 700000; + const a = statistics.total; return a.toLocaleString(); })()} - Images + Images - + {(() => { - const a = 600000; + const a = statistics.images; return a.toLocaleString(); })()} - Videos + Videos - + {(() => { - const a = 100000; + const a = statistics.videos; return a.toLocaleString(); })()} - Live + Live - + {(() => { const a = 50000; return a.toLocaleString(); @@ -118,7 +137,8 @@ export default function ExifDataAnalytics() {
- + +
{exifCharts.map((chart) => ( ))} - +
); From 360c19699e75e22b95fc7d6e97ff9c05114e4d1e Mon Sep 17 00:00:00 2001 From: sai80082 Date: Fri, 13 Sep 2024 20:09:35 +0530 Subject: [PATCH 4/7] feat: heatmap based on the assets in past year --- .../analytics/exif/AssetHeatMap.tsx | 140 ++++++++++++++++++ .../analytics/exif/contributiongraph.tsx | 109 -------------- src/config/routes.ts | 4 +- src/handlers/api/analytics.handler.ts | 10 +- src/pages/analytics/exif.tsx | 84 +++++------ .../api/analytics/statistics/heatmap.tsx | 65 ++++++++ .../api/analytics/statistics/livephoto.tsx | 31 ++++ 7 files changed, 283 insertions(+), 160 deletions(-) create mode 100644 src/components/analytics/exif/AssetHeatMap.tsx delete mode 100644 src/components/analytics/exif/contributiongraph.tsx create mode 100644 src/pages/api/analytics/statistics/heatmap.tsx create mode 100644 src/pages/api/analytics/statistics/livephoto.tsx diff --git a/src/components/analytics/exif/AssetHeatMap.tsx b/src/components/analytics/exif/AssetHeatMap.tsx new file mode 100644 index 0000000..b47be3e --- /dev/null +++ b/src/components/analytics/exif/AssetHeatMap.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from "react"; +import { getHeatMapData } from "@/handlers/api/analytics.handler"; + +type HeatMapEntry = { + date: string; + count: number; +}; + +export default function AssetHeatMap() { + const [heatMapData, setHeatMapData] = useState([]); + const [loading, setLoading] = useState(false); + const [weeksPerMonth, setWeeksPerMonth] = useState([]); // 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 ( +
+

Past Year

+ {loading ? ( +

Loading...

+ ) : ( +
+
{month}
+ + + {weeksPerMonth.map((weeks, index) => ( + + ))} + + + + {heatMapData[0]?.map((_, rowIndex) => ( + + {heatMapData.map((column, colIndex) => ( + + ))} + + ))} + +
+ {months[index]} +
+ { + column[rowIndex]?.date ? ( + +
+ ) :
+ } +
+
+ )} +
+ ); +} diff --git a/src/components/analytics/exif/contributiongraph.tsx b/src/components/analytics/exif/contributiongraph.tsx deleted file mode 100644 index 1e9c0a1..0000000 --- a/src/components/analytics/exif/contributiongraph.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { getAssetStatistics } from "@/handlers/api/analytics.handler"; - -// List of month labels -const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; -const weeksPerMonth = [4, 4, 4, 4, 5, 4, 4, 4, 5, 4, 4, 5]; -// Function to map weeks to months -const getMonthLabels = () => { -// Approximate number of weeks per month - const monthLabels = []; - let cumulativeWeeks = 0; - - for (let i = 0; i < months.length; i++) { - monthLabels.push({ - month: months[i], - startWeek: cumulativeWeeks, - span: weeksPerMonth[i], - }); - cumulativeWeeks += weeksPerMonth[i]; - } - - return monthLabels; -}; - -// Function to generate the last 365 days of contribution data -const generateContributions = () => { - const contributions: number[][] = Array.from({ length: 7 }, () => Array(52).fill(0)); - for (let week = 0; week < 52; week++) { - for (let day = 0; day < 7; day++) { - // Generate a random number of contributions for each day (0 to 5 for example) - contributions[day][week] = Math.floor(Math.random() * 6); - } - } - return contributions; -}; - -// Function to determine the background color based on the contribution count -const getColor = (count: number) => { - if (count === 0) return 'bg-zinc-900'; - if (count === 1) return 'bg-green-200'; - if (count === 2) return 'bg-green-400'; - if (count === 3) return 'bg-green-500'; - if (count === 4) return 'bg-green-600'; - return 'bg-green-800'; -}; - -const ContributionGraph: React.FC = () => { - const contributions = generateContributions(); - const [statistics, setStatistics] = useState({}); - const [loading, setLoading] = useState(false); - - const fetchData = async () => { - setLoading(true); // Set loading to true when starting to fetch data - return getAssetStatistics() - .then((data) => { - setStatistics(data); // Update statistics with the returned data - }) - .finally(() => setLoading(false)); // Stop loading after data is fetched - }; - - const monthLabels = getMonthLabels(); - - return ( -
-

Past Year

-
- {/* Month labels */} - {/*
- {monthLabels.map(({ month, startWeek, span }) => ( -
- {month} -
- ))} -
*/} - - - - - {months.map((month, index) => ( - - ))} - - - - - {contributions.map((dayContributions, dayIndex) => ( - - {dayContributions.map((count, weekIndex) => ( - - ))} - - ))} - -
{month}
-
-
- ); -}; - -export default ContributionGraph; diff --git a/src/config/routes.ts b/src/config/routes.ts index dfd0072..1ecb845 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -38,4 +38,6 @@ export const SEARCH_PLACES_PATH = BASE_PROXY_ENDPOINT + "/search/places"; //Analytics -export const ASSET_STATISTICS = BASE_PROXY_ENDPOINT + "/assets/statistics"; \ No newline at end of file +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"; \ No newline at end of file diff --git a/src/handlers/api/analytics.handler.ts b/src/handlers/api/analytics.handler.ts index 933cdd8..0daf406 100644 --- a/src/handlers/api/analytics.handler.ts +++ b/src/handlers/api/analytics.handler.ts @@ -1,4 +1,4 @@ -import { ASSET_STATISTICS, 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 = @@ -10,4 +10,12 @@ export const getExifDistribution = async (column: ISupportedEXIFColumns) => { 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); } \ No newline at end of file diff --git a/src/pages/analytics/exif.tsx b/src/pages/analytics/exif.tsx index ed32d6a..64af274 100644 --- a/src/pages/analytics/exif.tsx +++ b/src/pages/analytics/exif.tsx @@ -12,9 +12,9 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import ContributionGraph from "@/components/analytics/exif/contributiongraph"; +import AssetHeatMap from "@/components/analytics/exif/AssetHeatMap"; import { useEffect, useState } from "react"; -import { getAssetStatistics } from "@/handlers/api/analytics.handler"; +import { getAssetStatistics, getLivePhotoStatistics } from "@/handlers/api/analytics.handler"; const inter = Inter({ subsets: ["latin"] }); @@ -73,72 +73,58 @@ const exifCharts: IEXIFDistributionProps[] = [ 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); // Set loading to true when starting to fetch data - return getAssetStatistics() - .then((data) => { - console.log("asdasdadasd") - setStatistics(data); // Update statistics with the returned data - }) - .finally(() => setLoading(false)); // Stop loading after data is fetched + setLoading(true); + try { + const data = await getAssetStatistics(); + setStatistics(data); + } finally { + setLoading(false); + } }; useEffect(() => { fetchStatisticsData(); + fetchLivePhotoStatistics(); }, []); + return (
+ {["Total", "Images", "Videos"].map((type, i) => ( + + + {type} + + + {loading ? "Loading..." : statistics[type.toLowerCase() as keyof typeof statistics].toLocaleString()} + + + ))} - Total Files + Live Photos - {(() => { - const a = statistics.total; - return a.toLocaleString(); - })()} - - - - - Images - - - {(() => { - const a = statistics.images; - return a.toLocaleString(); - })()} - - - - - Videos - - - {(() => { - const a = statistics.videos; - return a.toLocaleString(); - })()} - - - - - Live - - - {(() => { - const a = 50000; - return a.toLocaleString(); - })()} + {loading ? "Loading..." : livePhotoStatistics.total.toLocaleString()}
- - +
{exifCharts.map((chart) => ( 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(eq(assets.ownerId, currentUser.id)) + .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, + }); + } +} diff --git a/src/pages/api/analytics/statistics/livephoto.tsx b/src/pages/api/analytics/statistics/livephoto.tsx new file mode 100644 index 0000000..8eb670c --- /dev/null +++ b/src/pages/api/analytics/statistics/livephoto.tsx @@ -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, + }); + } +} From 5e41c88a89425fd51052e548580ba7ef6ac87013 Mon Sep 17 00:00:00 2001 From: sai80082 Date: Fri, 13 Sep 2024 20:13:22 +0530 Subject: [PATCH 5/7] fix: get heatmap for past one year only --- .../api/analytics/statistics/heatmap.tsx | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/pages/api/analytics/statistics/heatmap.tsx b/src/pages/api/analytics/statistics/heatmap.tsx index 25e2275..d330b1a 100644 --- a/src/pages/api/analytics/statistics/heatmap.tsx +++ b/src/pages/api/analytics/statistics/heatmap.tsx @@ -2,42 +2,42 @@ import { db } from "@/config/db"; import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; import { assets, exif } from "@/schema"; -import { and, count, desc, eq, isNotNull, ne, sql, } from "drizzle-orm"; +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[]) { +} + +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 }); - } + 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)); + filledData.sort((a, b) => a.date.localeCompare(b.date)); return filledData; } export default async function handler( @@ -52,7 +52,11 @@ export default async function handler( count: count(), }) .from(assets) - .where(eq(assets.ownerId, currentUser.id)) + .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); From c994c123b4f0ea6fbcb990ad8a9422a1ce3a86a3 Mon Sep 17 00:00:00 2001 From: sai80082 Date: Fri, 13 Sep 2024 21:18:09 +0530 Subject: [PATCH 6/7] fix: use external url for the heatmap redirection --- src/components/analytics/exif/AssetHeatMap.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/analytics/exif/AssetHeatMap.tsx b/src/components/analytics/exif/AssetHeatMap.tsx index b47be3e..99178e2 100644 --- a/src/components/analytics/exif/AssetHeatMap.tsx +++ b/src/components/analytics/exif/AssetHeatMap.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { getHeatMapData } from "@/handlers/api/analytics.handler"; +import { useConfig } from "@/contexts/ConfigContext"; type HeatMapEntry = { date: string; @@ -7,6 +8,8 @@ type HeatMapEntry = { }; export default function AssetHeatMap() { + const { exImmichUrl } = useConfig(); + const [heatMapData, setHeatMapData] = useState([]); const [loading, setLoading] = useState(false); const [weeksPerMonth, setWeeksPerMonth] = useState([]); // Store weeks per month @@ -115,7 +118,7 @@ export default function AssetHeatMap() { { column[rowIndex]?.date ? ( From 53b267f447c02bf0a76a15a938dfd7574e5975bc Mon Sep 17 00:00:00 2001 From: Sai charan Guntupalli <63032681+sai80082@users.noreply.github.com> Date: Sat, 14 Sep 2024 12:59:48 +0530 Subject: [PATCH 7/7] Update pr-release.yml --- .github/workflows/pr-release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr-release.yml b/.github/workflows/pr-release.yml index 4fda809..0cdede3 100644 --- a/.github/workflows/pr-release.yml +++ b/.github/workflows/pr-release.yml @@ -30,4 +30,3 @@ jobs: run: | docker build --build-arg VERSION=pr-${{ env.PR_NUMBER }} \ -t ghcr.io/${{ github.repository }}:pr-${{ env.PR_NUMBER }} . - docker push ghcr.io/${{ github.repository }}:pr-${{ env.PR_NUMBER }}