diff --git a/packages/skin-database/app/(modern)/scroll/SkinScroller.tsx b/packages/skin-database/app/(modern)/scroll/SkinScroller.tsx new file mode 100644 index 00000000..27505159 --- /dev/null +++ b/packages/skin-database/app/(modern)/scroll/SkinScroller.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useState, useLayoutEffect } from "react"; + +export type ClientSkin = { + screenshotUrl: string; + fileName: string; + md5: string; + readmeStart: string; +}; + +type Props = { + initialSkins: ClientSkin[]; + getSkins: (offset: number) => Promise; +}; + +export default function SkinScroller({ initialSkins, getSkins }: Props) { + const [skins, setSkins] = useState(initialSkins); + const [visibleSkinIndex, setVisibleSkinIndex] = useState(0); + const [fetching, setFetching] = useState(false); + const [containerRef, setContainerRef] = useState(null); + + useLayoutEffect(() => { + if (containerRef == null) { + return; + } + + function onSnap(e) { + const md5 = e.snapTargetBlock.getAttribute("skin-md5"); + const index = parseInt(e.snapTargetBlock.getAttribute("skin-index")); + setVisibleSkinIndex(index); + } + + containerRef.addEventListener("scrollsnapchange", onSnap); + + return () => { + containerRef.removeEventListener("scrollsnapchange", onSnap); + }; + }, [containerRef]); + + useLayoutEffect(() => { + if (fetching) { + return; + } + if (visibleSkinIndex + 5 >= skins.length) { + setFetching(true); + getSkins(skins.length).then((newSkins) => { + setSkins([...skins, ...newSkins]); + setFetching(false); + }); + } + }, [visibleSkinIndex, skins, fetching]); + + return ( +
+ {skins.map((skin, i) => { + return ( +
+ {skin.fileName} +
+

+ {skin.fileName} +

+

+ {skin.readmeStart} +

+
+
+ ); + })} +
+ ); +} diff --git a/packages/skin-database/app/(modern)/scroll/page.tsx b/packages/skin-database/app/(modern)/scroll/page.tsx new file mode 100644 index 00000000..5bc0c14f --- /dev/null +++ b/packages/skin-database/app/(modern)/scroll/page.tsx @@ -0,0 +1,40 @@ +import ClassicSkinResolver from "../../../api/graphql/resolvers/ClassicSkinResolver"; +import SkinsConnection from "../../../api/graphql/SkinsConnection"; +import UserContext from "../../../data/UserContext"; +import "./scroll.css"; +import SkinScroller, { ClientSkin } from "./SkinScroller"; + +async function getClientSkins(offset: number): Promise { + "use server"; + const ctx = new UserContext(); + const connection = new SkinsConnection(10, offset, "MUSEUM", null); + const skins = await connection.nodes(ctx); + if (skins == null) { + return []; + } + const classicSkins = skins.filter( + (skin): skin is ClassicSkinResolver => skin instanceof ClassicSkinResolver + ); + const clientSkins: ClientSkin[] = await Promise.all( + classicSkins.map(async (skin) => { + const url = await skin.screenshot_url(); + const readmeText = await skin.readme_text(); + return { + screenshotUrl: url, + md5: skin.md5(), + fileName: await skin.filename(true), + readmeStart: readmeText ? readmeText.slice(0, 200) : "", + }; + }) + ); + return clientSkins; +} + +/** + * A tik-tok style scroll page where we display one skin at a time in full screen + */ +export default async function ScrollPage() { + const initialSkins = await getClientSkins(0); + + return ; +} diff --git a/packages/skin-database/app/(modern)/scroll/scroll.css b/packages/skin-database/app/(modern)/scroll/scroll.css new file mode 100644 index 00000000..0e2250f5 --- /dev/null +++ b/packages/skin-database/app/(modern)/scroll/scroll.css @@ -0,0 +1,5 @@ +body { + margin: 0; /* Remove default margin */ + height: 100vh; /* Set body height to viewport height */ + background-color: rgb(0, 0, 0); +}