diff --git a/bun.lockb b/bun.lockb index 49524e8..3c73c56 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/projects/website/package.json b/projects/website/package.json index e08d955..bbbabb1 100644 --- a/projects/website/package.json +++ b/projects/website/package.json @@ -36,7 +36,7 @@ "cross-env": "^7.0.3", "dexie": "^4.0.8", "dexie-react-hooks": "^1.1.7", - "framer-motion": "^11.5.4", + "framer-motion": "^11.11.10", "js-cookie": "^3.0.5", "ky": "^1.7.2", "lucide-react": "^0.453.0", diff --git a/projects/website/src/app/(pages)/page.tsx b/projects/website/src/app/(pages)/page.tsx index 5b99ff6..f2e0ccb 100644 --- a/projects/website/src/app/(pages)/page.tsx +++ b/projects/website/src/app/(pages)/page.tsx @@ -6,9 +6,9 @@ import RealtimeScores from "@/components/home/realtime-scores"; export default async function HomePage() { return ( -
+
-
+
diff --git a/projects/website/src/common/utils.ts b/projects/website/src/common/utils.ts index e6e733e..5b003f0 100644 --- a/projects/website/src/common/utils.ts +++ b/projects/website/src/common/utils.ts @@ -19,3 +19,10 @@ export function validateUrl(url: string) { return false; } } + +export function getRandomInteger(min: number, max: number): number { + min = Math.ceil(min); + max = Math.floor(max); + + return Math.floor(Math.random() * (max - min)) + min; +} diff --git a/projects/website/src/components/home/data-collection.tsx b/projects/website/src/components/home/data-collection.tsx index 5c8f5bd..bd75f11 100644 --- a/projects/website/src/components/home/data-collection.tsx +++ b/projects/website/src/components/home/data-collection.tsx @@ -2,20 +2,22 @@ import { Database } from "lucide-react"; export default function DataCollection() { return ( -
+
{/* Header */}
-
- -

Data Collection

+
+ +

Data Collection

-

posidonium novum ancillae ius conclusionemque splendide vel.

+

+ posidonium novum ancillae ius conclusionemque splendide vel. +

{/* Content */}
Data Collection {/* Header */} -
-
- -

Friends

+
+
+ +

Friends

-

posidonium novum ancillae ius conclusionemque splendide vel.

+

+ posidonium novum ancillae ius conclusionemque splendide vel. +

{/* Content */}
Friends - - + <motion.div + className="flex flex-col gap-3.5 text-center items-center" + initial={{ opacity: 0, y: -40 }} + animate={{ opacity: 1, y: 0 }} + transition={{ duration: 0.5, ease: "easeOut" }} + > + <Alert /> + <Title /> + </motion.div> <Buttons /> <AppPreview /> - <Separator className="my-14 w-screen" /> + <Separator className="my-12 w-screen" /> </div> ); } @@ -50,7 +60,12 @@ function Title() { function Buttons() { return ( - <div className="mt-4 flex gap-4"> + <motion.div + className="mt-4 flex flex-col xs:flex-row gap-4 items-center" + initial={{ opacity: 0, y: -20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.35, duration: 0.7, ease: "easeOut" }} + > <Link href="https://discord.gg/kmNfWGA4A8" target="_blank"> <Button className="max-w-52 flex gap-2.5 bg-pp hover:bg-pp/85 text-white text-base"> <UserSearch className="size-6" /> @@ -64,19 +79,24 @@ function Buttons() { <span>Join our Discord</span> </Button> </Link> - </div> + </motion.div> ); } function AppPreview() { return ( - <div className="mx-5 my-24 relative max-w-[1280px] shadow-[0_3rem_20rem_-15px_rgba(15,15,15,0.6)] shadow-pp/50 rounded-xl overflow-hidden"> + <motion.div + className="mx-5 my-20 relative max-w-[1280px] shadow-[0_3rem_20rem_-15px_rgba(15,15,15,0.6)] shadow-pp/50 rounded-2xl overflow-hidden" + initial={{ opacity: 0, y: -35 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.45, duration: 0.7, ease: "easeOut" }} + > <BorderBeam colorFrom="#6773ff" colorTo="#4858ff" /> <img - className="w-full h-full border-4 border-pp/20 rounded-xl" + className="w-full h-full border-4 border-pp/20 rounded-2xl" src="/assets/home/app-preview.png" draggable={false} /> - </div> + </motion.div> ); } diff --git a/projects/website/src/components/home/realtime-scores.tsx b/projects/website/src/components/home/realtime-scores.tsx index 76dd43b..6edf32b 100644 --- a/projects/website/src/components/home/realtime-scores.tsx +++ b/projects/website/src/components/home/realtime-scores.tsx @@ -1,26 +1,116 @@ import { Database } from "lucide-react"; +import { getRandomInteger } from "@/common/utils"; +import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; +import { getDifficulty } from "@/common/song-utils"; +import { AnimatedList } from "@/components/ui/animated-list"; + +type ScoreProps = { + songArt: string; + songName: string; + songAuthor: string; + setBy: string; +}; + +let scores: ScoreProps[] = [ + { + songArt: "https://cdn.scoresaber.com/covers/B1D3FA6D5305837DF59B5E629A412DEBC68BBB46.png", + songName: "LORELEI", + songAuthor: "Camellia", + setBy: "ImFascinated", + }, + { + songArt: "https://cdn.scoresaber.com/covers/7C44CDC1E33E2F5F929867B29CEB3860C3716DDC.png", + songName: "Time files", + songAuthor: "xi", + setBy: "Minion", + }, + { + songArt: "https://cdn.scoresaber.com/covers/8E4B7917C01E5987A5B3FF13FAA3CA8F27D21D34.png", + songName: "RATATA", + songAuthor: "Skrillex, Missy Elliot & Mr. Oizo", + setBy: "Minion", + }, + { + songArt: "https://cdn.scoresaber.com/covers/98F73BD330852EAAEBDC695140EAC8F2027AEEC8.png", + songName: "Invasion of Amorphous Trepidation", + songAuthor: "Diabolic Phantasma", + setBy: "Bello", + }, + { + songArt: "https://cdn.scoresaber.com/covers/666EEAC0F3EEE2278DCB971AC1D27421A0335801.png", + songName: "Yotsuya-san ni Yoroshiku", + songAuthor: "Eight", + setBy: "ACC | NoneTaken", + }, +]; +scores = Array.from({ length: 32 }, () => scores).flat(); export default function RealtimeScores() { return ( - <div className="px-5 -mt-20 flex flex-col gap-10 items-end select-none"> + <div className="px-5 -mt-20 flex flex-row-reverse gap-10 select-none"> {/* Header */} - <div className="flex flex-col gap-2.5 items-end"> - <div className="flex gap-3.5 items-center"> - <Database className="size-7 text-pp" /> - <h1 className="text-4xl font-bold text-ssr">Realtime Scores</h1> + <div className="flex flex-col gap-2.5 text-right items-end"> + <div className="flex flex-row-reverse gap-3 items-center text-yellow-400"> + <Database className="p-2 size-11 bg-yellow-800/15 rounded-lg" /> + <h1 className="text-3xl sm:text-4xl font-bold">Realtime Scores</h1> </div> - <p className="opacity-85">posidonium novum ancillae ius conclusionemque splendide vel.</p> + <p className="max-w-5xl text-sm sm:text-base opacity-85"> + <span className="text-lg font-semibold text-yellow-500">Nec detracto voluptatibus!</span> Vulputate duis + dolorum iuvaret disputationi ceteros te noluisse himenaeos bibendum dolores molestiae lorem elaboraret porro + brute tation simul laudem netus odio has in tibique. + </p> </div> {/* Content */} - <div className="max-w-[900px]"> - <img - className="w-full h-full rounded-xl border border-ssr/20" - src="/assets/home/realtime-scores.png" - alt="Realtime Scores" - draggable={false} - /> + <div className="w-full flex flex-col justify-center items-center"> + <AnimatedList className="h-96 divide-y divide-muted overflow-hidden"> + {scores.map((score, index) => ( + <Score key={index} {...score} /> + ))} + </AnimatedList> </div> </div> ); } + +function Score({ songArt, songName, songAuthor, setBy }: ScoreProps) { + return ( + <figure className="w-[32rem] py-2 flex flex-col text-sm"> + {/* Set By */} + <span> + Set by <span className="text-ssr">{setBy}</span> + </span> + + {/* Score */} + <div className="py-3 flex gap-5 items-center"> + {/* Position & Time */} + <div className="w-24 flex flex-col gap-1 items-center"> + <div className="flex gap-2 items-center"> + <GlobeAmericasIcon className="size-5" /> + <span className="text-ssr">#{getRandomInteger(1, 900)}</span> + </div> + <span>just now</span> + </div> + + {/* Song Art & Difficulty */} + <div className="relative"> + <img className="size-16 rounded-md" src={songArt} alt={`Song art for ${songName} by ${songAuthor}`} /> + <div + className="absolute inset-x-0 bottom-0 py-px flex justify-center text-xs rounded-t-lg" + style={{ + backgroundColor: getDifficulty("Hard").color + "f0", // Transparency value (in hex 0-255) + }} + > + Hard + </div> + </div> + + {/* Song Name & Author */} + <div className="flex flex-col gap-1"> + <h1 className="text-ssr">{songName}</h1> + <p className="opacity-75">{songAuthor}</p> + </div> + </div> + </figure> + ); +} diff --git a/projects/website/src/components/home/site-stats.tsx b/projects/website/src/components/home/site-stats.tsx index 3dc17e3..f6b0367 100644 --- a/projects/website/src/components/home/site-stats.tsx +++ b/projects/website/src/components/home/site-stats.tsx @@ -10,11 +10,13 @@ export default async function SiteStats() { <div className="px-5 -mt-20 flex flex-col gap-10 select-none"> {/* Header */} <div className="flex flex-col gap-2.5"> - <div className="flex gap-3.5 items-center"> - <Database className="size-7 text-pp" /> - <h1 className="text-4xl font-bold text-ssr">Site Statistics</h1> + <div className="flex gap-3 items-center text-orange-600"> + <Database className="p-2 size-11 bg-orange-800/15 rounded-lg" /> + <h1 className="text-3xl sm:text-4xl font-bold">Site Statistics</h1> </div> - <p className="opacity-85">posidonium novum ancillae ius conclusionemque splendide vel.</p> + <p className="max-w-5xl text-sm sm:text-base opacity-85"> + posidonium novum ancillae ius conclusionemque splendide vel. + </p> </div> {/* Content */} diff --git a/projects/website/src/components/home/statistic.tsx b/projects/website/src/components/home/statistic.tsx index e63d3ee..e8f1312 100644 --- a/projects/website/src/components/home/statistic.tsx +++ b/projects/website/src/components/home/statistic.tsx @@ -13,7 +13,7 @@ export default function Statistic({ icon, title, value }: Statistic) { return ( <div className="flex flex-col gap-2 text-center items-center text-lg"> {icon} - <h1 className="font-semibold text-ssr">{title}</h1> + <h1 className="font-semibold text-orange-400/85">{title}</h1> <span> <CountUp end={value} duration={1.2} enableScrollSpy scrollSpyOnce /> </span> diff --git a/projects/website/src/components/ui/animated-list.tsx b/projects/website/src/components/ui/animated-list.tsx new file mode 100644 index 0000000..4f045d8 --- /dev/null +++ b/projects/website/src/components/ui/animated-list.tsx @@ -0,0 +1,59 @@ +"use client"; + +import React, { ReactElement, useEffect, useMemo, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; + +export interface AnimatedListProps { + className?: string; + children: React.ReactNode; + delay?: number; +} + +export const AnimatedList = React.memo( + ({ className, children, delay = 1000 }: AnimatedListProps) => { + const [index, setIndex] = useState(0); + const childrenArray = React.Children.toArray(children); + + useEffect(() => { + const interval = setInterval(() => { + setIndex((prevIndex) => (prevIndex + 1) % childrenArray.length); + }, delay); + + return () => clearInterval(interval); + }, [childrenArray.length, delay]); + + const itemsToShow = useMemo( + () => childrenArray.slice(0, index + 1).reverse(), + [index, childrenArray], + ); + + return ( + <div className={`flex flex-col items-center gap-4 ${className}`}> + <AnimatePresence> + {itemsToShow.map((item) => ( + <AnimatedListItem key={(item as ReactElement).key}> + {item} + </AnimatedListItem> + ))} + </AnimatePresence> + </div> + ); + }, +); + +AnimatedList.displayName = "AnimatedList"; + +export function AnimatedListItem({ children }: { children: React.ReactNode }) { + const animations = { + initial: { scale: 0, opacity: 0 }, + animate: { scale: 1, opacity: 1, originY: 0 }, + exit: { scale: 0, opacity: 0 }, + transition: { type: "spring", stiffness: 350, damping: 40 }, + }; + + return ( + <motion.div {...animations} layout className="mx-auto w-full"> + {children} + </motion.div> + ); +} diff --git a/projects/website/tailwind.config.ts b/projects/website/tailwind.config.ts index 0c5b861..e13c1e6 100644 --- a/projects/website/tailwind.config.ts +++ b/projects/website/tailwind.config.ts @@ -1,5 +1,8 @@ import type { Config } from "tailwindcss"; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const defaultTheme = require("tailwindcss/defaultTheme"); + const config: Config = { darkMode: ["class"], content: [ @@ -8,6 +11,10 @@ const config: Config = { "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { + screens: { + xs: "475px", + ...defaultTheme.screens, + }, extend: { colors: { pp: "#4858ff",