trending: limited thumbnails to 1 per video

nextui
Guus van Meerveld 8 months ago
parent 61b54c4081
commit dc9d17ec74

@ -5,21 +5,5 @@
"printWidth": 80, "printWidth": 80,
"arrowParens": "always", "arrowParens": "always",
"importOrderSeparation": true, "importOrderSeparation": true,
"importOrder": [ "importOrder": ["^next./", "^@nextui-org/."]
"^next.*",
"^react$",
"^react.*",
"^axios.*",
"^@emotion/.*",
"^@mui/material/.*",
"^@mui/.*",
"^@src/.*",
"^@models/.*",
"^@interfaces/.*",
"^@utils/.*",
"^@components/.*",
".*sass$",
".*css$",
"^@svg/.*"
]
} }

@ -0,0 +1,3 @@
{
"prettier.configPath": ".prettierrc.json"
}

@ -22,6 +22,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tanstack/react-query-devtools": "^5.27.8", "@tanstack/react-query-devtools": "^5.27.8",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/next-pwa": "^5.6.9", "@types/next-pwa": "^5.6.9",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",

@ -3,18 +3,17 @@
import { Component } from "@/typings/component"; import { Component } from "@/typings/component";
import { useClient } from "@/hooks/useClient"; import { useClient } from "@/hooks/useClient";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Card, CardFooter, CardBody } from "@nextui-org/card";
import { Image } from "@nextui-org/image";
import { Button } from "@nextui-org/button"; import { Button } from "@nextui-org/button";
import { CircularProgress } from "@nextui-org/progress"; import { CircularProgress } from "@nextui-org/progress";
import { Divider } from "@nextui-org/divider";
import Link from "next/link"; import { Spacer } from "@nextui-org/spacer";
import formatNumber from "@/utils/formatNumbers"; import { VideoCard } from "./VideoCard";
export const Trending: Component = ({}) => { export const Trending: Component = ({}) => {
const client = useClient(); const client = useClient();
const { isLoading, error, data } = useQuery({ const { isLoading, error, refetch, data } = useQuery({
queryKey: ["trending"], queryKey: ["trending"],
queryFn: () => client.getTrending("NL") queryFn: () => client.getTrending("NL")
}); });
@ -26,45 +25,24 @@ export const Trending: Component = ({}) => {
<CircularProgress aria-label="Loading trending page..." /> <CircularProgress aria-label="Loading trending page..." />
</div> </div>
)} )}
{error && (
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<h1 className="text-xl">
An error occurred loading the trending page
</h1>
<h2 className="text-lg">{error.toString()}</h2>
<Spacer y={2} />
<Button color="primary" onClick={() => refetch()}>
Retry
</Button>
</div>
</div>
)}
{data && ( {data && (
<div className="grid gap-4 py-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 py-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{data.map((video) => ( {data.map((video) => (
<Link key={video.id} href={`/watch?v=${video.id}`}> <VideoCard data={video} />
<Card radius="lg">
<CardBody>
<Image
alt={video.title}
className="object-cover"
height={400}
src={video.thumbnails[0].url}
width={600}
/>
<p className="text-small absolute bottom-3 right-3 bg-content2 p-1">
20:41
</p>
</CardBody>
<Divider />
<CardFooter>
<div className="max-w-full">
<p title={video.title} className="truncate">
{video.title}
</p>
<div className="flex flex-row gap-2 justify-start overflow-scroll">
<p className="text-small tracking-tight text-default-400">
{video.author.name}
</p>
<p className="text-small tracking-tight text-default-400">
{video.uploaded.toLocaleDateString()}
</p>
<p className="text-small tracking-tight text-default-400">
Views: {formatNumber(video.views)}
</p>
</div>
</div>
</CardFooter>
</Card>
</Link>
))} ))}
</div> </div>
)} )}

@ -0,0 +1,50 @@
import { TrendingVideo } from "@/client/typings/trending";
import { Component } from "@/typings/component";
import { Card, CardFooter, CardBody } from "@nextui-org/card";
import { Image } from "@nextui-org/image";
import { Divider } from "@nextui-org/divider";
import Link from "next/link";
import formatNumber from "@/utils/formatNumbers";
export const VideoCard: Component<{ data: TrendingVideo }> = ({
data: video
}) => {
return (
<Link key={video.id} href={`/watch?v=${video.id}`}>
<Card radius="lg">
<CardBody>
<Image
alt={video.title}
className="object-cover"
height={400}
src={video.thumbnail}
width={600}
/>
<p className="text-small rounded-md z-10 absolute bottom-5 right-5 bg-content2 p-1">
{video.duration}
</p>
</CardBody>
<Divider />
<CardFooter>
<div className="max-w-full">
<p title={video.title} className="truncate">
{video.title}
</p>
<div className="flex flex-row gap-2 justify-start overflow-scroll">
<p className="text-small tracking-tight text-default-400">
{video.author.name}
</p>
<p className="text-small tracking-tight text-default-400">
{video.uploaded.toLocaleDateString()}
</p>
<p className="text-small tracking-tight text-default-400">
Views: {formatNumber(video.views)}
</p>
</div>
</div>
</CardFooter>
</Card>
</Link>
);
};

@ -1,23 +1,31 @@
import { TrendingVideo } from "@/client/typings/trending"; import { TrendingVideo } from "@/client/typings/trending";
import { Thumbnail } from "@/client/typings/thumbnail";
import InvidiousTrending from "./typings/trending"; import InvidiousTrending from "./typings/trending";
import InvidiousThumbnail from "./typings/thumbnail";
export default class Transformer { export default class Transformer {
public static thumbnails(data: InvidiousThumbnail[]): Thumbnail[] {
return data.map((thumbnail) => ({ url: thumbnail.url }));
}
public static trending(data: InvidiousTrending[]): TrendingVideo[] { public static trending(data: InvidiousTrending[]): TrendingVideo[] {
return data.map((video) => ({ return data.map((video) => {
author: { id: video.authorId, name: video.author }, const thumbnail = video.videoThumbnails.find(
duration: video.lengthSeconds * 1000, (thumbnail) =>
id: video.videoId, thumbnail.quality == "default" ||
title: video.title, thumbnail.quality == "medium" ||
thumbnails: Transformer.thumbnails(video.videoThumbnails), thumbnail.quality == "middle"
uploaded: new Date(video.published * 1000 ?? 0), );
views: video.viewCount
})); if (thumbnail === undefined)
throw new Error(
`Invidious: Missing thumbnail for video with id ${video.videoId}`
);
return {
author: { id: video.authorId, name: video.author },
duration: video.lengthSeconds * 1000,
id: video.videoId,
title: video.title,
thumbnail: thumbnail.url,
uploaded: new Date(video.published * 1000 ?? 0),
views: video.viewCount
};
});
} }
} }

@ -1,7 +1,19 @@
import z from "zod"; import z from "zod";
const qualityTypes = [
"maxres",
"maxresdefault",
"sddefault",
"high",
"medium",
"default",
"start",
"middle",
"end"
] as const;
export const ThumbnailModel = z.object({ export const ThumbnailModel = z.object({
quality: z.string(), quality: z.enum(qualityTypes),
url: z.string().url(), url: z.string().url(),
width: z.number(), width: z.number(),
height: z.number() height: z.number()

@ -26,7 +26,7 @@ export default class Transformer {
views: video.views, views: video.views,
id: videoId, id: videoId,
uploaded: new Date(video.uploaded), uploaded: new Date(video.uploaded),
thumbnails: [{ url: video.thumbnail }], thumbnail: video.thumbnail,
title: video.title, title: video.title,
author: { id: channelId, name: video.uploaderName } author: { id: channelId, name: video.uploaderName }
}; };

@ -1,3 +0,0 @@
export interface Thumbnail {
url: string;
}

@ -1,8 +1,6 @@
import { Thumbnail } from "./thumbnail";
export interface TrendingVideo { export interface TrendingVideo {
title: string; title: string;
thumbnails: Thumbnail[]; thumbnail: string;
id: string; id: string;
author: { author: {
name: string; name: string;

@ -29,7 +29,7 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.0" leven "^3.1.0"
"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.23.5": "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5":
version "7.23.5" version "7.23.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
@ -63,7 +63,16 @@
json5 "^2.2.3" json5 "^2.2.3"
semver "^6.3.1" semver "^6.3.1"
"@babel/generator@^7.23.6": "@babel/generator@7.17.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad"
integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==
dependencies:
"@babel/types" "^7.17.0"
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.23.0", "@babel/generator@^7.23.6":
version "7.23.6" version "7.23.6"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e"
integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==
@ -245,7 +254,7 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
"@babel/helper-validator-identifier@^7.22.20": "@babel/helper-validator-identifier@^7.16.7", "@babel/helper-validator-identifier@^7.22.20":
version "7.22.20" version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
@ -282,7 +291,7 @@
chalk "^2.4.2" chalk "^2.4.2"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.24.0": "@babel/parser@^7.20.5", "@babel/parser@^7.23.0", "@babel/parser@^7.24.0":
version "7.24.0" version "7.24.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac"
integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==
@ -947,6 +956,22 @@
"@babel/parser" "^7.24.0" "@babel/parser" "^7.24.0"
"@babel/types" "^7.24.0" "@babel/types" "^7.24.0"
"@babel/traverse@7.23.2":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
dependencies:
"@babel/code-frame" "^7.22.13"
"@babel/generator" "^7.23.0"
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-function-name" "^7.23.0"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/parser" "^7.23.0"
"@babel/types" "^7.23.0"
debug "^4.1.0"
globals "^11.1.0"
"@babel/traverse@^7.24.0": "@babel/traverse@^7.24.0":
version "7.24.0" version "7.24.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e"
@ -963,7 +988,15 @@
debug "^4.3.1" debug "^4.3.1"
globals "^11.1.0" globals "^11.1.0"
"@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.4.4": "@babel/types@7.17.0":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
dependencies:
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@babel/types@^7.17.0", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.4.4":
version "7.24.0" version "7.24.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf"
integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==
@ -2955,6 +2988,18 @@
dependencies: dependencies:
"@tanstack/query-core" "5.27.5" "@tanstack/query-core" "5.27.5"
"@trivago/prettier-plugin-sort-imports@^4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz#725f411646b3942193a37041c84e0b2116339789"
integrity sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==
dependencies:
"@babel/generator" "7.17.7"
"@babel/parser" "^7.20.5"
"@babel/traverse" "7.23.2"
"@babel/types" "7.17.0"
javascript-natural-sort "0.7.1"
lodash "^4.17.21"
"@types/estree@0.0.39": "@types/estree@0.0.39":
version "0.0.39" version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
@ -4804,6 +4849,11 @@ jake@^10.8.5:
filelist "^1.0.4" filelist "^1.0.4"
minimatch "^3.1.2" minimatch "^3.1.2"
javascript-natural-sort@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
integrity sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==
jest-worker@^26.2.1: jest-worker@^26.2.1:
version "26.6.2" version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
@ -5032,7 +5082,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
lodash@^4.17.20: lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -5932,6 +5982,11 @@ source-map-support@~0.5.20:
buffer-from "^1.0.0" buffer-from "^1.0.0"
source-map "^0.6.0" source-map "^0.6.0"
source-map@^0.5.0:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
source-map@^0.6.0, source-map@~0.6.1: source-map@^0.6.0, source-map@~0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"

Loading…
Cancel
Save