Merge remote-tracking branch 'origin/master'
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 5s
Some checks failed
Deploy Website / docker (ubuntu-latest) (push) Failing after 5s
This commit is contained in:
commit
24f4910364
@ -9,6 +9,7 @@ on:
|
|||||||
- projects/website/**
|
- projects/website/**
|
||||||
- projects/common/**
|
- projects/common/**
|
||||||
- .gitea/workflows/deploy-website.yml
|
- .gitea/workflows/deploy-website.yml
|
||||||
|
- bun.lockb
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
|
BIN
bun.lockb
Executable file → Normal file
BIN
bun.lockb
Executable file → Normal file
Binary file not shown.
@ -36,7 +36,7 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
"framer-motion": "^11.5.4",
|
"framer-motion": "^11.11.10",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"ky": "^1.7.2",
|
"ky": "^1.7.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
|
@ -6,9 +6,9 @@ import RealtimeScores from "@/components/home/realtime-scores";
|
|||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
return (
|
return (
|
||||||
<main className="-mt-3 w-screen min-h-screen bg-[#0f0f0f]">
|
<main className="-mt-3 w-screen min-h-screen bg-gradient-to-b from-[#0f0f0f] via-[#1a1a1a] to-[#0f0f0f]">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="max-w-screen-2xl mt-48 flex flex-col gap-56">
|
<div className="max-w-screen-2xl mt-48 mb-14 flex flex-col gap-64">
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<DataCollection />
|
<DataCollection />
|
||||||
<Friends />
|
<Friends />
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Footer from "@/components/footer";
|
|
||||||
import { PreloadResources } from "@/components/preload-resources";
|
import { PreloadResources } from "@/components/preload-resources";
|
||||||
import { QueryProvider } from "@/components/providers/query-provider";
|
import { QueryProvider } from "@/components/providers/query-provider";
|
||||||
import { ThemeProvider } from "@/components/providers/theme-provider";
|
import { ThemeProvider } from "@/components/providers/theme-provider";
|
||||||
@ -14,6 +13,8 @@ import { Colors } from "@/common/colors";
|
|||||||
import OfflineNetwork from "@/components/offline-network";
|
import OfflineNetwork from "@/components/offline-network";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { ApiHealth } from "@/components/api/api-health";
|
import { ApiHealth } from "@/components/api/api-health";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import { getBuildInformation } from "@/common/website-utils";
|
||||||
|
|
||||||
const siteFont = localFont({
|
const siteFont = localFont({
|
||||||
src: "./fonts/JetBrainsMono.ttf",
|
src: "./fonts/JetBrainsMono.ttf",
|
||||||
@ -66,6 +67,7 @@ export default function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const { buildId, buildTimeShort } = getBuildInformation();
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${siteFont.className} antialiased w-full h-full`}>
|
<body className={`${siteFont.className} antialiased w-full h-full`}>
|
||||||
@ -84,7 +86,8 @@ export default function RootLayout({
|
|||||||
<div className="mt-3 z-[1] m-auto flex flex-col flex-grow items-center w-full md:max-w-[1600px]">
|
<div className="mt-3 z-[1] m-auto flex flex-col flex-grow items-center w-full md:max-w-[1600px]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
{/*<Footer />*/}
|
||||||
|
<Footer buildId={buildId} buildTimeShort={buildTimeShort} />
|
||||||
</main>
|
</main>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { MapDifficulty } from "@ssr/common/score/map-difficulty";
|
import { MapDifficulty } from "@ssr/common/score/map-difficulty";
|
||||||
|
|
||||||
type Difficulty = {
|
export type Difficulty = {
|
||||||
/**
|
/**
|
||||||
* The name of the difficulty
|
* The name of the difficulty
|
||||||
*/
|
*/
|
||||||
@ -63,14 +63,21 @@ export function getScoreBadgeFromAccuracy(acc: number): ScoreBadge {
|
|||||||
return scoreBadges[scoreBadges.length - 1];
|
return scoreBadges[scoreBadges.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random difficulty, except ExpertPlus.
|
||||||
|
*/
|
||||||
|
export function getRandomDifficulty(): Difficulty {
|
||||||
|
return difficulties[Math.floor(Math.random() * (difficulties.length - 1))];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a {@link Difficulty} from its name
|
* Gets a {@link Difficulty} from its name
|
||||||
*
|
*
|
||||||
* @param diff the name of the difficulty
|
* @param diff the name of the difficulty
|
||||||
* @returns the difficulty
|
* @returns the difficulty
|
||||||
*/
|
*/
|
||||||
export function getDifficulty(diff: MapDifficulty) {
|
export function getDifficulty(diff: Difficulty | MapDifficulty) {
|
||||||
const difficulty = difficulties.find(d => d.name === diff);
|
const difficulty = difficulties.find(d => d.name === (typeof diff === "string" ? diff : diff.name));
|
||||||
if (!difficulty) {
|
if (!difficulty) {
|
||||||
throw new Error(`Unknown difficulty: ${diff}`);
|
throw new Error(`Unknown difficulty: ${diff}`);
|
||||||
}
|
}
|
||||||
|
@ -19,3 +19,10 @@ export function validateUrl(url: string) {
|
|||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
@ -1,78 +1,176 @@
|
|||||||
import { getBuildInformation } from "@/common/website-utils";
|
"use client";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
type NavbarItem = {
|
import Link from "next/link";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { cn } from "@/common/utils";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { SiGithub, SiX } from "react-icons/si";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
type FooterLink = {
|
||||||
|
/**
|
||||||
|
* The name of this link.
|
||||||
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
link: string;
|
|
||||||
openInNewTab?: boolean;
|
/**
|
||||||
|
* The href for this link.
|
||||||
|
*/
|
||||||
|
href: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The optional name to show
|
||||||
|
* when the screen size is small.
|
||||||
|
*/
|
||||||
|
shortName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: NavbarItem[] = [
|
type SocialLinkType = {
|
||||||
{
|
/**
|
||||||
name: "Home",
|
* The name of this social link.
|
||||||
link: "/",
|
*/
|
||||||
},
|
name: string;
|
||||||
{
|
|
||||||
name: "Source",
|
/**
|
||||||
link: "https://git.fascinated.cc/Fascinated/scoresaber-reloadedv3",
|
* The logo for this social link.
|
||||||
openInNewTab: true,
|
*/
|
||||||
},
|
logo: ReactElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The href for this social link.
|
||||||
|
*/
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const links: {
|
||||||
|
[category: string]: FooterLink[];
|
||||||
|
} = {
|
||||||
|
Resources: [
|
||||||
|
{
|
||||||
|
name: "Swagger Docs",
|
||||||
|
shortName: "Swagger",
|
||||||
|
href: "/swagger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Source Code",
|
||||||
|
shortName: "Source",
|
||||||
|
href: "https://git.fascinated.cc/Fascinated/scoresaber-reloadedv3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "System Status",
|
||||||
|
shortName: "Status",
|
||||||
|
href: "https://status.fascinated.cc/status/scoresaber-reloaded",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
App: [
|
||||||
|
{
|
||||||
|
name: "Score Feed",
|
||||||
|
href: "/scores/live",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Top Scores",
|
||||||
|
href: "/scores/top/weekly",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const socialLinks: SocialLinkType[] = [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Twitter",
|
||||||
link: "https://x.com/ssr_reloaded",
|
logo: <SiX className="size-5 lg:size-6" />,
|
||||||
openInNewTab: true,
|
href: "https://x.com/ssr_reloaded",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Discord",
|
name: "Discord",
|
||||||
link: "https://discord.gg/kmNfWGA4A8",
|
logo: <img className="size-6 lg:size-7" src="/assets/logos/discord.svg" />,
|
||||||
openInNewTab: true,
|
href: "https://discord.gg/kmNfWGA4A8",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Status",
|
name: "GitHub",
|
||||||
link: "https://status.fascinated.cc/status/scoresaber-reloaded",
|
logo: <SiGithub className="size-5 lg:size-6" />,
|
||||||
openInNewTab: true,
|
href: "https://git.fascinated.cc/Fascinated/scoresaber-reloadedv3",
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Swagger",
|
|
||||||
link: "/swagger",
|
|
||||||
openInNewTab: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Score Feed",
|
|
||||||
link: "/scores/live",
|
|
||||||
openInNewTab: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Top Scores",
|
|
||||||
link: "/scores/top/weekly",
|
|
||||||
openInNewTab: false,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer({ buildId, buildTimeShort }: { buildId: string; buildTimeShort: string | undefined }) {
|
||||||
const { buildId, buildTime, buildTimeShort } = getBuildInformation();
|
const isHome: boolean = usePathname() === "/";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center w-full flex-col gap-1 mt-6">
|
<footer
|
||||||
<div className="flex items-center gap-2 text-input text-sm">
|
className={cn(
|
||||||
<p>Build: {buildId}</p>
|
"px-10 min-h-80 py-5 flex flex-col gap-10 lg:gap-0 justify-between border-t border-muted select-none",
|
||||||
<p className="hidden md:block">({buildTime})</p>
|
isHome ? "bg-[#121212]" : "mt-5 bg-[#121212]/60"
|
||||||
<p className="none md:hidden">({buildTimeShort})</p>
|
)}
|
||||||
|
>
|
||||||
|
{/* Top Section */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
{/* Branding & Social Links */}
|
||||||
|
<div className="w-full max-w-screen-2xl flex flex-col gap-7 lg:flex-row justify-between items-center lg:items-start">
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{/* Branding */}
|
||||||
|
<div className="flex flex-col gap-2 text-center items-center lg:text-left lg:items-start">
|
||||||
|
<Link
|
||||||
|
className="flex gap-3 items-center hover:opacity-75 transition-all transform-gpu"
|
||||||
|
href="/"
|
||||||
|
draggable={false}
|
||||||
|
>
|
||||||
|
<img className="size-9" src="/assets/logos/scoresaber.png" alt="Scoresaber Logo" />
|
||||||
|
<h1 className="text-xl font-bold text-pp">ScoreSaber Reloaded</h1>
|
||||||
|
</Link>
|
||||||
|
<p className="max-w-md text-sm opacity-85">
|
||||||
|
ScoreSaber Reloaded is a new way to view your scores and get more stats about you and your plays
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Links */}
|
||||||
|
<div className="flex gap-4 justify-center lg:justify-start items-center">
|
||||||
|
{socialLinks.map(link => (
|
||||||
|
<Link
|
||||||
|
key={link.name}
|
||||||
|
className="hover:opacity-75 transition-all transform-gpu"
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
draggable={false}
|
||||||
|
>
|
||||||
|
{link.logo}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
|
<div className="flex gap-20 md:gap-32 transition-all transform-gpu">
|
||||||
|
{Object.entries(links).map(([title, links]) => (
|
||||||
|
<div key={title} className="flex flex-col gap-0.5">
|
||||||
|
<h1 className="pb-1 text-lg font-semibold text-ssr">{title}</h1>
|
||||||
|
{links.map(link => {
|
||||||
|
const external: boolean = !link.href.startsWith("/");
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={link.name}
|
||||||
|
className="flex gap-2 items-center hover:opacity-75 transition-all transform-gpu"
|
||||||
|
href={link.href}
|
||||||
|
target={external ? "_blank" : undefined}
|
||||||
|
draggable={false}
|
||||||
|
>
|
||||||
|
<span className={cn("hidden sm:flex", !link.shortName && "flex")}>{link.name}</span>
|
||||||
|
{link.shortName && <span className="flex sm:hidden">{link.shortName}</span>}
|
||||||
|
{external && <ExternalLink className="w-3.5 h-3.5" />}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-wrap items-center justify-center bg-secondary/95 divide-x divide-input text-sm py-2">
|
|
||||||
{items.map((item, index) => {
|
{/* Bottom Section */}
|
||||||
return (
|
<div className="flex justify-center">
|
||||||
<Link
|
{/* Build Info */}
|
||||||
key={index}
|
<p className="text-sm opacity-50">
|
||||||
className="px-2 text-ssr hover:brightness-[66%] transition-all transform-gpu"
|
Build {buildId} ({buildTimeShort})
|
||||||
href={item.link}
|
</p>
|
||||||
target={item.openInNewTab ? "_blank" : undefined}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,22 @@ import { Database } from "lucide-react";
|
|||||||
|
|
||||||
export default function DataCollection() {
|
export default function DataCollection() {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 -mt-32 flex flex-col gap-10 select-none">
|
<div className="px-5 -mt-40 flex flex-col gap-10 select-none">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
<div className="flex gap-3.5 items-center">
|
<div className="flex gap-3 items-center text-pp">
|
||||||
<Database className="size-7 text-pp" />
|
<Database className="p-2 size-11 bg-ssr/15 rounded-lg" />
|
||||||
<h1 className="text-4xl font-bold text-ssr">Data Collection</h1>
|
<h1 className="text-3xl sm:text-4xl font-bold">Data Collection</h1>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="max-w-[900px]">
|
<div className="max-w-[900px]">
|
||||||
<img
|
<img
|
||||||
className="w-full h-full rounded-xl border border-ssr/20"
|
className="w-full h-full rounded-2xl border border-ssr/20"
|
||||||
src="/assets/home/data-collection.png"
|
src="/assets/home/data-collection.png"
|
||||||
alt="Data Collection"
|
alt="Data Collection"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
@ -1,25 +1,35 @@
|
|||||||
import { Database } from "lucide-react";
|
import { UsersRound } from "lucide-react";
|
||||||
|
import { cn } from "@/common/utils";
|
||||||
|
|
||||||
export default function Friends() {
|
export default function Friends() {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 -mt-20 flex flex-col gap-10 items-end select-none">
|
<div className="px-5 -mt-20 flex flex-col gap-10 items-end select-none">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-2.5 items-end">
|
<div className="flex flex-col gap-2.5 text-right items-end">
|
||||||
<div className="flex gap-3.5 items-center">
|
<div className="flex flex-row-reverse gap-3 items-center text-purple-600">
|
||||||
<Database className="size-7 text-pp" />
|
<UsersRound className="p-2 size-11 bg-purple-800/15 rounded-lg" />
|
||||||
<h1 className="text-4xl font-bold text-ssr">Friends</h1>
|
<h1 className="text-3xl sm:text-4xl font-bold">Friends</h1>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="max-w-[900px]">
|
<div
|
||||||
<img
|
className={cn(
|
||||||
className="w-full h-full rounded-xl border border-ssr/20"
|
"relative",
|
||||||
src="/assets/home/friends.png"
|
"before:absolute before:-left-36 before:-top-28 before:size-[32rem] before:bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] before:from-purple-600 before:rounded-full before:blur-3xl before:opacity-30 before:z-[1]"
|
||||||
alt="Friends"
|
)}
|
||||||
draggable={false}
|
>
|
||||||
/>
|
<div className={cn("relative max-w-[900px] z-20")}>
|
||||||
|
<img
|
||||||
|
className="w-full h-full rounded-2xl border border-ssr/20"
|
||||||
|
src="/assets/home/friends.png"
|
||||||
|
alt="Friends"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import AnimatedShinyText from "@/components/ui/animated-shiny-text";
|
import AnimatedShinyText from "@/components/ui/animated-shiny-text";
|
||||||
import { ArrowRight, UserSearch } from "lucide-react";
|
import { ArrowRight, UserSearch } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@ -5,15 +7,23 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { SiGithub } from "react-icons/si";
|
import { SiGithub } from "react-icons/si";
|
||||||
import { BorderBeam } from "@/components/ui/border-beam";
|
import { BorderBeam } from "@/components/ui/border-beam";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export default function HeroSection() {
|
export default function HeroSection() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3.5 text-center items-center select-none">
|
<div className="flex flex-col gap-3.5 text-center items-center select-none">
|
||||||
<Alert />
|
<motion.div
|
||||||
<Title />
|
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 />
|
<Buttons />
|
||||||
<AppPreview />
|
<AppPreview />
|
||||||
<Separator className="my-14 w-screen" />
|
<Separator className="my-12 w-screen" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -42,7 +52,7 @@ function Title() {
|
|||||||
ScoreSaber Reloaded
|
ScoreSaber Reloaded
|
||||||
</h1>
|
</h1>
|
||||||
<p className="max-w-sm md:max-w-xl md:text-lg opacity-85">
|
<p className="max-w-sm md:max-w-xl md:text-lg opacity-85">
|
||||||
Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays
|
ScoreSaber Reloaded is a new way to view your scores and get more stats about you and your plays
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -50,8 +60,13 @@ function Title() {
|
|||||||
|
|
||||||
function Buttons() {
|
function Buttons() {
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex gap-4">
|
<motion.div
|
||||||
<Link href="https://discord.gg/kmNfWGA4A8" target="_blank">
|
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="/search" target="_blank">
|
||||||
<Button className="max-w-52 flex gap-2.5 bg-pp hover:bg-pp/85 text-white text-base">
|
<Button className="max-w-52 flex gap-2.5 bg-pp hover:bg-pp/85 text-white text-base">
|
||||||
<UserSearch className="size-6" />
|
<UserSearch className="size-6" />
|
||||||
<span>Player Search</span>
|
<span>Player Search</span>
|
||||||
@ -64,19 +79,24 @@ function Buttons() {
|
|||||||
<span>Join our Discord</span>
|
<span>Join our Discord</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppPreview() {
|
function AppPreview() {
|
||||||
return (
|
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" />
|
<BorderBeam colorFrom="#6773ff" colorTo="#4858ff" />
|
||||||
<img
|
<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"
|
src="/assets/home/app-preview.png"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,122 @@
|
|||||||
import { Database } from "lucide-react";
|
import { ChartNoAxesCombined, Database, Flame } from "lucide-react";
|
||||||
|
import { cn, getRandomInteger } from "@/common/utils";
|
||||||
|
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { Difficulty, getDifficulty, getRandomDifficulty } 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: "Rainnny",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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() {
|
export default function RealtimeScores() {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 -mt-20 flex flex-col gap-10 items-end select-none">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative px-5 -mt-20 flex flex-col lg:flex-row-reverse gap-10 select-none",
|
||||||
|
"before:absolute before:-left-40 before:-bottom-36 before:size-[28rem] before:bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] before:from-yellow-600 before:rounded-full before:blur-3xl before:opacity-30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-2.5 items-end">
|
<div className="flex flex-col gap-2.5 text-right items-end">
|
||||||
<div className="flex gap-3.5 items-center">
|
<div className="flex flex-row-reverse gap-3 items-center text-yellow-400">
|
||||||
<Database className="size-7 text-pp" />
|
<Flame className="p-2 size-11 bg-yellow-800/15 rounded-lg" />
|
||||||
<h1 className="text-4xl font-bold text-ssr">Realtime Scores</h1>
|
<h1 className="text-3xl sm:text-4xl font-bold">Realtime Scores</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="opacity-85">posidonium novum ancillae ius conclusionemque splendide vel.</p>
|
<p className="max-w-2xl lg: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>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="max-w-[900px]">
|
<div className="w-full flex flex-col justify-center items-center overflow-hidden">
|
||||||
<img
|
<AnimatedList className="w-full max-w-[32rem] h-96 divide-y divide-muted" delay={1500}>
|
||||||
className="w-full h-full rounded-xl border border-ssr/20"
|
{scores.map((score, index) => (
|
||||||
src="/assets/home/realtime-scores.png"
|
<Score key={index} {...score} />
|
||||||
alt="Realtime Scores"
|
))}
|
||||||
draggable={false}
|
</AnimatedList>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Score({ songArt, songName, songAuthor, setBy }: ScoreProps) {
|
||||||
|
const difficulty: Difficulty = getRandomDifficulty();
|
||||||
|
return (
|
||||||
|
<figure className="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 text-center 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(difficulty).color + "f0", // Transparency value (in hex 0-255)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{difficulty.name}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Database } from "lucide-react";
|
import { ChartNoAxesCombined, Database } from "lucide-react";
|
||||||
import { kyFetch } from "@ssr/common/utils/utils";
|
import { kyFetch } from "@ssr/common/utils/utils";
|
||||||
import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
|
import { AppStatistics } from "@ssr/common/types/backend/app-statistics";
|
||||||
import { Config } from "@ssr/common/config";
|
import { Config } from "@ssr/common/config";
|
||||||
@ -10,11 +10,13 @@ export default async function SiteStats() {
|
|||||||
<div className="px-5 -mt-20 flex flex-col gap-10 select-none">
|
<div className="px-5 -mt-20 flex flex-col gap-10 select-none">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
<div className="flex gap-3.5 items-center">
|
<div className="flex gap-3 items-center text-orange-600">
|
||||||
<Database className="size-7 text-pp" />
|
<ChartNoAxesCombined className="p-2 size-11 bg-orange-800/15 rounded-lg" />
|
||||||
<h1 className="text-4xl font-bold text-ssr">Site Statistics</h1>
|
<h1 className="text-3xl sm:text-4xl font-bold">Site Statistics</h1>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
@ -13,7 +13,7 @@ export default function Statistic({ icon, title, value }: Statistic) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 text-center items-center text-lg">
|
<div className="flex flex-col gap-2 text-center items-center text-lg">
|
||||||
{icon}
|
{icon}
|
||||||
<h1 className="font-semibold text-ssr">{title}</h1>
|
<h1 className="font-semibold text-orange-400/85">{title}</h1>
|
||||||
<span>
|
<span>
|
||||||
<CountUp end={value} duration={1.2} enableScrollSpy scrollSpyOnce />
|
<CountUp end={value} duration={1.2} enableScrollSpy scrollSpyOnce />
|
||||||
</span>
|
</span>
|
||||||
|
59
projects/website/src/components/ui/animated-list.tsx
Normal file
59
projects/website/src/components/ui/animated-list.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
@ -8,6 +11,10 @@ const config: Config = {
|
|||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
screens: {
|
||||||
|
xs: "475px",
|
||||||
|
...defaultTheme.screens,
|
||||||
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
pp: "#4858ff",
|
pp: "#4858ff",
|
||||||
|
Reference in New Issue
Block a user