This commit is contained in:
@ -3,6 +3,7 @@
|
||||
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||
import { useSettingsStore } from "@/store/settingsStore";
|
||||
import React from "react";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { TooltipProvider } from "./ui/Tooltip";
|
||||
import { ThemeProvider } from "./ui/theme-provider";
|
||||
|
||||
@ -52,7 +53,15 @@ export default class AppProvider extends React.Component {
|
||||
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
||||
<TooltipProvider>{props.children}</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<ToastContainer
|
||||
className="z-50"
|
||||
position="top-right"
|
||||
theme="dark"
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
{props.children}
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Image from "next/image";
|
||||
import Footer from "./Footer";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function Container({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<ToastContainer
|
||||
className="z-50"
|
||||
position="top-right"
|
||||
theme="dark"
|
||||
pauseOnFocusLoss={false}
|
||||
/>
|
||||
<div className="m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
||||
<div className="fixed left-0 top-0 z-0 h-full w-full blur-sm">
|
||||
<Image
|
||||
className="object-fill object-center"
|
||||
alt="Background image"
|
||||
src={"/assets/background.webp"}
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div className="z-[9999] m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
||||
<Navbar />
|
||||
<div className="w-full flex-1">{children}</div>
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
ServerIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { GlobeAltIcon } from "@heroicons/react/24/outline";
|
||||
import { GlobeAltIcon, TvIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
import Avatar from "./Avatar";
|
||||
import { Button } from "./ui/button";
|
||||
@ -129,6 +129,12 @@ export default function Navbar() {
|
||||
icon={<GlobeAltIcon height={23} width={23} />}
|
||||
href="/ranking/global/1"
|
||||
/>
|
||||
<NavbarButton
|
||||
ariaLabel="View the overlay builder"
|
||||
text="Overlay"
|
||||
icon={<TvIcon height={23} width={23} />}
|
||||
href="/overlay/builder"
|
||||
/>
|
||||
<NavbarButton
|
||||
ariaLabel="View analytics for Scoresaber"
|
||||
text="Analytics"
|
||||
|
24
src/components/input/Input.tsx
Normal file
24
src/components/input/Input.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Input as Inputtt } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
|
||||
type InputProps = {
|
||||
label: string;
|
||||
id: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export function Input({ label, id, defaultValue, onChange }: InputProps) {
|
||||
return (
|
||||
<>
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
<Inputtt
|
||||
id={id}
|
||||
defaultValue={defaultValue}
|
||||
onChange={(e) => {
|
||||
onChange && onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
45
src/components/input/RadioInput.tsx
Normal file
45
src/components/input/RadioInput.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Label } from "../ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
|
||||
|
||||
type RadioProps = {
|
||||
id: string;
|
||||
defaultValue: string;
|
||||
label?: string;
|
||||
items: {
|
||||
id: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
}[];
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export function RadioInput({
|
||||
id,
|
||||
defaultValue,
|
||||
label,
|
||||
items,
|
||||
onChange,
|
||||
}: RadioProps) {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{id && label && <Label htmlFor={id}>{label}</Label>}
|
||||
<RadioGroup
|
||||
id={id}
|
||||
defaultValue={defaultValue}
|
||||
className="mt-2"
|
||||
onValueChange={(value) => onChange && onChange(value)}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={item.id} id={item.id}>
|
||||
{item.value}
|
||||
</RadioGroupItem>
|
||||
<Label htmlFor={item.id}>{item.value}</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
27
src/components/input/SwitchInput.tsx
Normal file
27
src/components/input/SwitchInput.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
|
||||
type SwitchProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
defaultChecked?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export function SwitchInput({
|
||||
id,
|
||||
label,
|
||||
defaultChecked,
|
||||
onChange,
|
||||
}: SwitchProps) {
|
||||
return (
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Switch
|
||||
id={id}
|
||||
defaultChecked={defaultChecked}
|
||||
onCheckedChange={(value) => onChange && onChange(value)}
|
||||
/>
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
34
src/components/overlay/PlayerStats.tsx
Normal file
34
src/components/overlay/PlayerStats.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { formatNumber } from "@/utils/numberUtils";
|
||||
import { GlobeAltIcon } from "@heroicons/react/20/solid";
|
||||
import Image from "next/image";
|
||||
import CountyFlag from "../CountryFlag";
|
||||
|
||||
type PlayerStatsProps = {
|
||||
player: ScoresaberPlayer;
|
||||
};
|
||||
|
||||
export default function PlayerStats({ player }: PlayerStatsProps) {
|
||||
return (
|
||||
<div className="flex gap-2 p-2">
|
||||
<Image
|
||||
alt="Player profile picture"
|
||||
className="rounded-md"
|
||||
src={player.profilePicture}
|
||||
width={180}
|
||||
height={180}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl font-bold">{formatNumber(player.pp, 2)}pp</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<GlobeAltIcon width={25} height={25} />
|
||||
<p className="text-3xl">#{formatNumber(player.rank)}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CountyFlag className="w-[25px]" countryCode={player.country} />
|
||||
<p className="text-3xl">#{formatNumber(player.countryRank)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
20
src/components/overlay/ScoreStats.tsx
Normal file
20
src/components/overlay/ScoreStats.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { useOverlayDataStore } from "@/store/overlayDataStore";
|
||||
import { formatNumber } from "@/utils/numberUtils";
|
||||
import useStore from "@/utils/useStore";
|
||||
|
||||
export default function ScoreStats() {
|
||||
const dataStore = useStore(useOverlayDataStore, (store) => store);
|
||||
if (!dataStore) return null;
|
||||
const { scoreStats } = dataStore;
|
||||
if (!scoreStats) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-2">
|
||||
<p className="text-2xl">{formatNumber(scoreStats.score)}</p>
|
||||
<p className="text-2xl">Combo: {formatNumber(scoreStats.combo)}</p>
|
||||
<p className="text-2xl">
|
||||
{scoreStats.rank} {scoreStats.accuracy.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
46
src/components/overlay/SongInfo.tsx
Normal file
46
src/components/overlay/SongInfo.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useOverlayDataStore } from "@/store/overlayDataStore";
|
||||
import { songDifficultyToColor } from "@/utils/songUtils";
|
||||
import useStore from "@/utils/useStore";
|
||||
import clsx from "clsx";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function SongInfo() {
|
||||
const dataStore = useStore(useOverlayDataStore, (store) => store);
|
||||
if (!dataStore) return null;
|
||||
const { paused, songInfo } = dataStore;
|
||||
if (!songInfo) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"flex transform-gpu gap-2 p-2 transition-all",
|
||||
paused ? "grayscale" : "grayscale-0",
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
className="rounded-md"
|
||||
alt="Song Image"
|
||||
src={songInfo.art}
|
||||
width={120}
|
||||
height={120}
|
||||
/>
|
||||
<div className="flex flex-col justify-between pb-2 pt-1">
|
||||
<div>
|
||||
<p className="text-xl font-bold">{songInfo.songName}</p>
|
||||
<p className="text-md">{songInfo.songMapper}</p>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<p
|
||||
className="text-md rounded-md p-[3px]"
|
||||
style={{
|
||||
backgroundColor: songDifficultyToColor(songInfo.difficulty),
|
||||
}}
|
||||
>
|
||||
{songInfo.difficulty}
|
||||
</p>
|
||||
<p className="text-md">!bsr {songInfo.bsr}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/utils/utils"
|
||||
import { cn } from "@/utils/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -10,12 +10,12 @@ const Card = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
|
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/utils/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/utils/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
44
src/components/ui/radio-group.tsx
Normal file
44
src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/utils/utils"
|
||||
|
||||
const RadioGroup = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
)
|
||||
})
|
||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||
|
||||
const RadioGroupItem = React.forwardRef<
|
||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
})
|
||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/utils/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
Reference in New Issue
Block a user