Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stats by year #112

Merged
merged 47 commits into from
Dec 24, 2024
Merged
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
811f1c4
Create page.tsx
koenoe Dec 13, 2024
83a460a
feat(stats): more styling
koenoe Dec 14, 2024
f6aeb62
Update page.tsx
koenoe Dec 14, 2024
8bb83b8
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 14, 2024
7bde657
feat(stats): more styling
koenoe Dec 14, 2024
ade9bcf
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 15, 2024
e724c91
feat(stats): add graph
koenoe Dec 15, 2024
ba1ee9e
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 18, 2024
f90fb5b
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 18, 2024
e8a802e
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 19, 2024
ea33545
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 19, 2024
458c9ab
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 19, 2024
d55b041
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 19, 2024
922340e
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 19, 2024
7c9f77f
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 20, 2024
8b6af55
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 20, 2024
98abfd5
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 20, 2024
fb23023
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 20, 2024
43dec66
wip
koenoe Dec 20, 2024
cf87901
Merge branch 'main' of github.com:koenoe/tvseri.es into feat/stats
koenoe Dec 20, 2024
e7514a6
feat(stats): wip
koenoe Dec 20, 2024
317008e
feat(stats): wip
koenoe Dec 21, 2024
11ea9b4
feat(stats): wip
koenoe Dec 21, 2024
ba48c9a
feat(stats): tweaks
koenoe Dec 21, 2024
6f1bed6
feat(stats): wip
koenoe Dec 21, 2024
7bf15cf
Update MostWatchedProviders.tsx
koenoe Dec 21, 2024
920351e
feat(stats): world map
koenoe Dec 21, 2024
e312613
feat(stats): world map
koenoe Dec 22, 2024
60bf90b
feat(stats): list and grid
koenoe Dec 22, 2024
4f8c059
feat(stats): cleanup
koenoe Dec 22, 2024
85ae8f5
feat(stats): make stuff for real yo
koenoe Dec 23, 2024
4712a08
feat(stats): watched per week FOR REALIO
koenoe Dec 23, 2024
bd025cd
feat(stats): cleanup
koenoe Dec 23, 2024
fc0b768
feat(stats): genres with real data!
koenoe Dec 23, 2024
c9c4dfb
Update MostWatchedGenresContainer.tsx
koenoe Dec 23, 2024
a903d10
feat(stats): streaming services with real data
koenoe Dec 23, 2024
c746fb1
feat(stats): world map
koenoe Dec 23, 2024
c56797a
feat(stats): world map tweaks
koenoe Dec 23, 2024
a598b43
feat(stats): more data
koenoe Dec 23, 2024
b90d0bc
feat(stats): use date-fns
koenoe Dec 24, 2024
37dfe99
feat(stats): last bits
koenoe Dec 24, 2024
24b66e6
Delete getWeekNumberFromDate.ts
koenoe Dec 24, 2024
2ef967e
Update index.ts
koenoe Dec 24, 2024
ee67615
fix: watch providers
koenoe Dec 24, 2024
7c4c32e
fix: watch providers prio
koenoe Dec 24, 2024
73e4dd7
Update index.ts
koenoe Dec 24, 2024
27d6433
fix: watch providers prio
koenoe Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(stats): last bits
koenoe committed Dec 24, 2024
commit 37dfe9963bc3df948cb47f54b45b9570e0f4ae43
27 changes: 24 additions & 3 deletions src/app/u/[username]/stats/[year]/page.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ import BlockSeriesFinished from '@/components/Stats/BlockSeriesFinished';
import BlockSeriesInProgress from '@/components/Stats/BlockSeriesInProgress';
import BlockTotalRuntime from '@/components/Stats/BlockTotalRuntime';
import BlockWatchlist from '@/components/Stats/BlockWatchlist';
import Grid from '@/components/Stats/Grid';
import InProgressByYear from '@/components/Stats/InProgress';
import MostWatchedGenresContainer from '@/components/Stats/MostWatchedGenresContainer';
import MostWatchedProvidersContainer from '@/components/Stats/MostWatchedProvidersContainer';
import PopularNotWatched from '@/components/Stats/PopularNotWatched';
@@ -145,10 +147,10 @@ export default async function StatsByYearPage({ params }: Props) {
</div>
<div className="mt-20">
<div className="mb-6 flex items-center gap-x-6">
<h2 className="text-md lg:text-lg">Finished in {year}</h2>
<h2 className="text-md lg:text-lg">Watched</h2>
<div className="h-[3px] flex-grow bg-white/10" />
</div>
<div className="grid grid-cols-4 gap-4 md:grid-cols-6 lg:grid-cols-8 xl:gap-6 2xl:grid-cols-10 [&>*]:!h-full [&>*]:!w-full">
<Grid>
<Suspense
fallback={
<>
@@ -160,7 +162,26 @@ export default async function StatsByYearPage({ params }: Props) {
>
<WatchedByYear year={year} userId={user.id} />
</Suspense>
</Grid>
</div>
<div className="mt-20">
<div className="mb-6 flex items-center gap-x-6">
<h2 className="text-md lg:text-lg">In progress</h2>
<div className="h-[3px] flex-grow bg-white/10" />
</div>
<Grid>
<Suspense
fallback={
<>
{[...Array(36)].map((_, index) => (
<SkeletonPoster key={index} />
))}
</>
}
>
<InProgressByYear year={year} userId={user.id} />
</Suspense>
</Grid>
</div>
<div className="relative mt-20 w-full">
<div className="mb-8 flex items-center gap-x-6">
@@ -178,7 +199,7 @@ export default async function StatsByYearPage({ params }: Props) {
fallback={
<SkeletonList
className="mt-20"
size="small"
size="medium"
scrollBarClassName="h-[3px] rounded-none"
/>
}
26 changes: 21 additions & 5 deletions src/components/Stats/BlockSeriesInProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { cache } from 'react';

import { cachedWatchedByYear } from '@/lib/cached';
import { getCacheItem, setCacheItem } from '@/lib/db/cache';
import { getListItemsCount } from '@/lib/db/list';

import Block from './Block';

export default async function BlockSeriesInProgress({
userId,
year,
}: Readonly<{
type Input = Readonly<{
userId: string;
year: number;
}>) {
}>;

export const cachedInProgressCount = cache(async ({ userId, year }: Input) => {
const key = `total-in-progress:${userId}_${year}`;
const cachedValue = await getCacheItem<number>(key);
if (cachedValue) {
return cachedValue;
}

const [count, items] = await Promise.all([
getListItemsCount({
listId: 'WATCHED',
@@ -26,5 +34,13 @@ export default async function BlockSeriesInProgress({
const uniqueSeriesCount = uniqueSeries.size;
const inProgressCount = uniqueSeriesCount - count;

await setCacheItem<number>(key, inProgressCount, { ttl: 900 });

return inProgressCount;
});

export default async function BlockSeriesInProgress({ userId, year }: Input) {
const inProgressCount = await cachedInProgressCount({ userId, year });

return <Block label="In progress" value={inProgressCount.toLocaleString()} />;
}
26 changes: 21 additions & 5 deletions src/components/Stats/BlockTotalRuntime.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
import { cache } from 'react';

import { cachedWatchedByYear } from '@/lib/cached';
import { getCacheItem, setCacheItem } from '@/lib/db/cache';
import formatRuntime from '@/utils/formatRuntime';

import Block from './Block';

export default async function BlockTotalRuntime({
userId,
year,
}: Readonly<{
type Input = Readonly<{
userId: string;
year: number;
}>) {
}>;

export const cachedTotalRuntime = cache(async ({ userId, year }: Input) => {
const key = `total-runtime:${userId}_${year}`;
const cachedValue = await getCacheItem<number>(key);
if (cachedValue) {
return cachedValue;
}

const items = await cachedWatchedByYear({ userId, year });
const totalRuntime = items.reduce(
(sum, item) => sum + (item.runtime || 0),
0,
);

await setCacheItem<number>(key, totalRuntime, { ttl: 900 });

return totalRuntime;
});

export default async function BlockTotalRuntime({ userId, year }: Input) {
const totalRuntime = await cachedTotalRuntime({ userId, year });

return <Block label="Total runtime" value={formatRuntime(totalRuntime)} />;
}
9 changes: 9 additions & 0 deletions src/components/Stats/Grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type ReactNode } from 'react';

export default function Grid({ children }: Readonly<{ children: ReactNode }>) {
return (
<div className="grid grid-cols-4 gap-4 md:grid-cols-6 lg:grid-cols-8 xl:gap-6 2xl:grid-cols-10 [&>*]:!h-full [&>*]:!w-full">
{children}
</div>
);
}
52 changes: 52 additions & 0 deletions src/components/Stats/InProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { cachedWatchedByYear } from '@/lib/cached';
import { getListItems, type ListItem } from '@/lib/db/list';

import Poster from '../Tiles/Poster';

export default async function InProgressByYear({
priority,
year,
userId,
}: Readonly<{ priority?: boolean; year: number; userId: string }>) {
const [{ items: watchedTvSeries }, watchedItems] = await Promise.all([
getListItems({
listId: 'WATCHED',
userId,
startDate: new Date(`${year}-01-01`),
endDate: new Date(`${year}-12-31`),
options: {
limit: 100,
},
}),
cachedWatchedByYear({
userId,
year,
}),
]);

const completedIds = watchedTvSeries.map((series) => series.id);

const items = watchedItems
.filter((item) => !completedIds.includes(item.seriesId))
.map(
(item) =>
({
id: item.seriesId,
slug: item.slug,
title: item.title,
posterImage: item.posterImage,
}) as ListItem,
);

const uniqueItems = [
...new Map(items.map((item) => [item.id, item])).values(),
];

return (
<>
{uniqueItems.map((item) => (
<Poster key={item.slug} item={item} priority={priority} size="small" />
))}
</>
);
}
4 changes: 2 additions & 2 deletions src/components/Stats/PopularNotWatched.tsx
Original file line number Diff line number Diff line change
@@ -21,13 +21,13 @@ export default async function PopularNotWatched({

return (
<List
title={<h2 className="text-md lg:text-lg">Missed in {year}</h2>}
title={<h2 className="text-md lg:text-lg">Popular unwatched</h2>}
scrollRestoreKey="top-rated-shows-unwatched"
scrollBarClassName="h-[3px] rounded-none"
{...rest}
>
{unwatchedSeries.map((item) => (
<Poster key={item.id} item={item} priority={priority} size="small" />
<Poster key={item.id} item={item} priority={priority} size="medium" />
))}
</List>
);
12 changes: 10 additions & 2 deletions src/components/Tiles/Poster.tsx
Original file line number Diff line number Diff line change
@@ -36,7 +36,15 @@ export const posterStyles = cva(
'lg:h-[225px]',
'lg:w-[150px]',
],
default: [
medium: [
'h-[250px]',
'w-[167px]',
'lg:h-[275px]',
'lg:w-[183px]',
'xl:h-[325px]',
'xl:w-[217px]',
],
large: [
'h-[275px]',
'w-[183px]',
'lg:h-[300px]',
@@ -47,7 +55,7 @@ export const posterStyles = cva(
},
},
defaultVariants: {
size: 'default',
size: 'large',
},
},
);
59 changes: 17 additions & 42 deletions src/lib/db/watched/index.ts
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { Resource } from 'sst';

import { fetchTvSeriesSeason } from '@/lib/tmdb';
import { generateTmdbImageUrl } from '@/lib/tmdb/helpers';
import { buildPosterImageUrl, generateTmdbImageUrl } from '@/lib/tmdb/helpers';
import type { TvSeries } from '@/types/tv-series';
import { type WatchProvider } from '@/types/watch-provider';

@@ -76,6 +76,16 @@ const createWatchedItem = ({
watchedAt,
});

const normalizeWatchedItem = (item: WatchedItem) => ({
...item,
posterImage: item.posterPath
? buildPosterImageUrl(item.posterPath)
: undefined,
watchProviderLogoImage: item.watchProviderLogoPath
? generateTmdbImageUrl(item.watchProviderLogoPath, 'w92')
: undefined,
});

export const markWatched = async ({
userId,
tvSeries,
@@ -438,7 +448,11 @@ export const getWatchedForTvSeries = async (
const result = await client.send(command);

return {
items: result.Items?.map((item) => unmarshall(item) as WatchedItem) ?? [],
items:
result.Items?.map((item) => {
const unmarshalled = unmarshall(item) as WatchedItem;
return normalizeWatchedItem(unmarshalled);
}) ?? [],
nextCursor: result.LastEvaluatedKey
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString(
'base64url',
@@ -554,40 +568,6 @@ export const getWatchedCountForSeason = async (
return result.Count ?? 0;
};

export const getRecentlyWatched = async (
input: Readonly<{
userId: string;
options?: PaginationOptions;
}>,
) => {
const { limit = 20, cursor } = input.options ?? {};

const command = new QueryCommand({
TableName: Resource.Watched.name,
IndexName: 'gsi2',
KeyConditionExpression: 'gsi2pk = :pk',
ExpressionAttributeValues: marshall({
':pk': `USER#${input.userId}#WATCHED`,
}),
Limit: limit,
ScanIndexForward: false, // newest first
ExclusiveStartKey: cursor
? JSON.parse(Buffer.from(cursor, 'base64url').toString())
: undefined,
});

const result = await client.send(command);

return {
items: result.Items?.map((item) => unmarshall(item) as WatchedItem) ?? [],
nextCursor: result.LastEvaluatedKey
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString(
'base64url',
)
: null,
};
};

export const getWatchedByDate = async (
input: Readonly<{
userId: string;
@@ -620,12 +600,7 @@ export const getWatchedByDate = async (
items:
result.Items?.map((item) => {
const unmarshalled = unmarshall(item) as WatchedItem;
return {
...unmarshalled,
watchProviderLogoImage: unmarshalled.watchProviderLogoPath
? generateTmdbImageUrl(unmarshalled.watchProviderLogoPath, 'w92')
: undefined,
};
return normalizeWatchedItem(unmarshalled);
}) ?? [],
nextCursor: result.LastEvaluatedKey
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString(
26 changes: 26 additions & 0 deletions src/utils/getWeekNumberFromDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default function getWeekNumber(date: Date): number {
// Copying date so the original date won't be modified
const tempDate = new Date(date.valueOf());

// ISO week date weeks start on Monday, so correct the day number
const dayNum = (date.getDay() + 6) % 7;

// Set the target to the nearest Thursday (current date + 4 - current day number)
tempDate.setDate(tempDate.getDate() - dayNum + 3);

// ISO 8601 week number of the year for this date
const firstThursday = tempDate.valueOf();

// Set the target to the first day of the year
// First set the target to January 1st
tempDate.setMonth(0, 1);

// If this is not a Thursday, set the target to the next Thursday
if (tempDate.getDay() !== 4) {
tempDate.setMonth(0, 1 + ((4 - tempDate.getDay() + 7) % 7));
}

// The weeknumber is the number of weeks between the first Thursday of the year
// and the Thursday in the target week
return 1 + Math.ceil((firstThursday - tempDate.valueOf()) / 604800000); // 604800000 = number of milliseconds in a week
}