diff --git a/remotion/Composition.tsx b/remotion/Composition.tsx index 17df693..bc7e733 100644 --- a/remotion/Composition.tsx +++ b/remotion/Composition.tsx @@ -43,7 +43,7 @@ export const IntroComposition = ({ scenes }: { scenes: Scene[] }) => { fontFamily: "sans-serif", }} > - +
BASE_API_ENDPOINT + "/people/" + personId + "/info"; \ No newline at end of file +export const GET_PERSON_INFO = (personId: string) => BASE_API_ENDPOINT + "/people/" + personId + "/info"; + +// Rewind +export const REWIND_STATS = BASE_API_ENDPOINT + "/rewind/stats"; \ No newline at end of file diff --git a/src/handlers/api/common.handler.ts b/src/handlers/api/common.handler.ts index 3626e32..921887b 100644 --- a/src/handlers/api/common.handler.ts +++ b/src/handlers/api/common.handler.ts @@ -1,4 +1,4 @@ -import { GET_FILTERS, SEARCH_PLACES_PATH } from "@/config/routes"; +import { GET_FILTERS, REWIND_STATS, SEARCH_PLACES_PATH } from "@/config/routes"; import API from "@/lib/api"; import { IPlace } from "@/types/common"; @@ -9,4 +9,8 @@ export const searchPlaces = async (name: string): Promise => { export const getFilters = async () => { return API.get(GET_FILTERS); -} \ No newline at end of file +} + +export const getRewindStats = async () => { + return API.get(REWIND_STATS); +} \ No newline at end of file diff --git a/src/pages/api/rewind/stats.ts b/src/pages/api/rewind/stats.ts new file mode 100644 index 0000000..3a32c31 --- /dev/null +++ b/src/pages/api/rewind/stats.ts @@ -0,0 +1,125 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { and, desc, gt, isNotNull, isNull, ne, sql } from "drizzle-orm"; +import { eq } from "drizzle-orm"; +import { db } from "@/config/db"; +import { count } from "drizzle-orm"; +import { assets } from "@/schema/assets.schema"; +import { assetFaces } from "@/schema/assetFaces.schema"; +import { exif, person, users } from "@/schema"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { albums } from "@/schema/albums.schema"; +import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "GET") { + return res.status(405).json({ message: "Method Not Allowed" }); + } + + const { year = "2024" } = req.query as { year: string }; + + const user = await getCurrentUser(req); + + if (!user) { + return res.status(403).json({ message: "Forbidden" }); + } + + const numberOfPhotos = await db.select({ count: count(assets.id) }).from(assets).where(and( + eq(assets.ownerId, user.id), + eq(assets.isVisible, true), + eq(assets.isArchived, false), + )); + + // Get list of countries visited in the year + const countriesInYear = await db + .select({ name: exif.country, count: count(assets.id) }) + .from(exif) + .leftJoin(assets, eq(assets.id, exif.assetId)) + .where(and( + eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year), + eq(assets.ownerId, user.id), + eq(assets.isVisible, true), + eq(assets.isArchived, false), + )) + .groupBy(exif.country); + + const countries = countriesInYear.filter(country => !!country.name).map(country => country.name); + + const countOfCities = await db + .select({ name: exif.city, count: count(assets.id) }) + .from(exif) + .leftJoin(assets, eq(assets.id, exif.assetId)) + .where(and( + eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year), + eq(assets.ownerId, user.id), + eq(assets.isVisible, true), + eq(assets.isArchived, false), + )) + .groupBy(exif.city); + + const cities = countOfCities.filter(city => !!city.name).map(city => city.name); + + const albumsWithMostPhotos = await db + .select({ + name: albums.albumName, + count: count(assets.id), + cover: albums.albumThumbnailAssetId + }) + .from(albumsAssetsAssets) + .leftJoin(albums, eq(albums.id, albumsAssetsAssets.albumsId)) + .leftJoin(assets, eq(assets.id, albumsAssetsAssets.assetsId)) + .leftJoin(exif, eq(exif.assetId, assets.id)) + .where(and( + eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year), + eq(assets.ownerId, user.id), + eq(assets.isVisible, true), + eq(assets.isArchived, false), + )) + .orderBy(desc(count(assets.id))) + .limit(4) + .groupBy(albums.id); + + const peopleWithMostPhotos = await db + .select({ + name: person.name, + count: count(assets.id), + id: person.id, + cover: person.faceAssetId + }) + .from(assetFaces) + .leftJoin(assets, eq(assets.id, assetFaces.assetId)) + .leftJoin(person, eq(person.id, assetFaces.personId)) + .leftJoin(exif, eq(exif.assetId, assets.id)) + .where(and( + eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year), + eq(assets.ownerId, user.id), + eq(assets.isVisible, true), + eq(assets.isArchived, false), + isNotNull(person.name), + )) + .orderBy(desc(count(assets.id))) + .limit(2) + .groupBy(person.id); + + const favoritedAssets = await db + .select({ id: assets.id }) + .from(assets) + .leftJoin(exif, eq(exif.assetId, assets.id)) + .where(and( + eq(assets.isVisible, true), + eq(assets.isFavorite, true), + eq(assets.isArchived, false), + eq(assets.ownerId, user.id), + eq(sql`EXTRACT(YEAR FROM ${exif.dateTimeOriginal})`, year), + )) + .limit(4); + + return res.status(200).json({ + numberOfPhotos: numberOfPhotos[0].count, + countriesInYear: countries, + countOfCities: cities.length, + albumsWithMostPhotos, + peopleWithMostPhotos: peopleWithMostPhotos, + favoritedAssets: favoritedAssets.map(asset => asset.id) + }); +} \ No newline at end of file diff --git a/src/pages/rewind.tsx b/src/pages/rewind.tsx index de91892..41108bc 100644 --- a/src/pages/rewind.tsx +++ b/src/pages/rewind.tsx @@ -1,33 +1,132 @@ -import React from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Player } from '@remotion/player'; import { IntroComposition } from '../../remotion/Composition'; +import PageLayout from '@/components/layouts/PageLayout'; +import Header from '@/components/shared/Header'; +import { Button } from '@/components/ui/button'; +import { getRewindStats } from '@/handlers/api/common.handler'; +import { ASSET_PREVIEW_PATH, ASSET_THUMBNAIL_PATH, PERSON_THUBNAIL_PATH } from '@/config/routes'; -const scenes = [ - { message: "Welcome to Immich Replay Varun!" }, - { message: "2024! So much memories!" }, - { message: "You've been to 10 new countries!" }, - { message: "And 25 new cities!" }, - { message: "Here are some of my favorite memories from this year!" }, - { message: "The most memory filled album of all time!" }, - { message: "You made so much memory with Soundarapandian"}, +interface IStats { + numberOfPhotos: number; + countriesInYear: any[]; + countOfCities: number; + albumsWithMostPhotos: any[]; + peopleWithMostPhotos: any[]; + favoritedAssets: string[]; +} + +const getScenes = (stats: IStats) => [ + { + message: "Welcome Varun!", + type: "WELCOME", + emoji: "👋", + data: {}, + + }, + { + message: "2024! So much memories!", + type: "TITLE", + emoji: "📅", + data: { + photos: stats.numberOfPhotos, + }, + + }, + { + message: `You've been to ${stats.countriesInYear.length} new countries!`, type: "COUNTRY", emoji: "🌍", data: { + countries: stats.countriesInYear, + }, + + }, + { + message: `And ${stats.countOfCities} new cities!`, type: "CITY", emoji: "🏙️", data: { + cities: stats.countOfCities, + }, + + }, + { + message: "Here are some of my favorite memories from this year!", type: "MEMORY", emoji: "🎉", data: { + photos: stats.albumsWithMostPhotos, + images: stats.favoritedAssets.map((assetId) => ASSET_THUMBNAIL_PATH(assetId)), + }, + + }, + { + message: "The most memory filled album of all time!", type: "ALBUM", emoji: "📸", data: { + photos: stats.albumsWithMostPhotos, + albums: stats.albumsWithMostPhotos.map((album) => ({ + name: album.name, + cover: ASSET_THUMBNAIL_PATH(album.cover), + })), + }, + + }, + { + message: "You made so much memory with", type: "FRIEND", emoji: "👯", data: { + friends: stats.peopleWithMostPhotos.map((person) => ({ + name: person.name, + cover: PERSON_THUBNAIL_PATH(person.id), + })), + }, + }, + { + message: "Lets see what 2025 has in store!", type: "END", emoji: "🚀", data: { + + }, + }, ] + + export default function RewindPage() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchStats = async () => { + getRewindStats().then((stats) => { + setStats(stats); + setLoading(false); + }).catch((error) => { + setError(error); + setLoading(false); + }); + } + + const scenes = useMemo(() => { + if (!stats) return []; + return getScenes(stats); + }, [stats]); + + useEffect(() => { + fetchStats(); + }, []); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + return ( -
- -
+ +
+
+ +
+
+ +
+ ) }