mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 16:20:03 +00:00
Added dark/light mode (next: custom themes)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
import {ThemeProvider as NextThemesProvider} from "next-themes"
|
||||
import {type ThemeProviderProps} from "next-themes/dist/types"
|
||||
|
||||
export function ThemeProvider({children, ...props}: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import {cva, type VariantProps} from "class-variance-authority"
|
||||
|
||||
import {cn} from "Frontend/@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border border-slate-200 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-slate-950 dark:border-slate-800 dark:[&>svg]:text-slate-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
|
||||
destructive:
|
||||
"border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({className, variant, ...props}, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({variant}), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({className, ...props}, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({className, ...props}, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export {Alert, AlertTitle, AlertDescription}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import {cn} from "Frontend/@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({className, ...props}, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({className, ...props}, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({className, ...props}, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-slate-100 dark:bg-slate-800",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export {Avatar, AvatarImage, AvatarFallback}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import {Slot} from "@radix-ui/react-slot"
|
||||
import {cva, type VariantProps} from "class-variance-authority"
|
||||
|
||||
import {cn} from "Frontend/@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-slate-900 text-slate-50 hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90",
|
||||
destructive:
|
||||
"bg-red-500 text-slate-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/90",
|
||||
outline:
|
||||
"border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50",
|
||||
secondary:
|
||||
"bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80",
|
||||
ghost: "hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-slate-50",
|
||||
link: "text-slate-900 underline-offset-4 hover:underline dark:text-slate-50",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({className, variant, size, asChild = false, ...props}, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({variant, size, className}))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export {Button, buttonVariants}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import {cn} from "Frontend/@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({className, ...props}, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border border-slate-200 bg-white text-slate-950 shadow-sm dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({className, ...props}, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({className, ...props}, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({className, ...props}, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-slate-500 dark:text-slate-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
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"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({className, ...props}, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export {Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent}
|
||||
@@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import {Check, ChevronRight, Circle} from "lucide-react"
|
||||
|
||||
import {cn} from "Frontend/@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({className, inset, children, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-800 dark:data-[state=open]:bg-slate-800",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4"/>
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({className, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({className, sideOffset = 4, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({className, inset, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({className, children, checked, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4"/>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({className, children, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-slate-100 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-800 dark:focus:text-slate-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current"/>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({className, inset, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({className, ...props}, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-800", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
|
||||
import {cn} from "Frontend/@/lib/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-slate-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:bg-slate-950 dark:ring-offset-slate-950 dark:placeholder:text-slate-400 dark:focus-visible:ring-slate-300",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export {Input}
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import {cva, type VariantProps} from "class-variance-authority"
|
||||
|
||||
import {cn} from "Frontend/@/lib/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}
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import {cn} from "Frontend/@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({className, sideOffset = 4, ...props}, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export {Tooltip, TooltipTrigger, TooltipContent, TooltipProvider}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useAtom} from "jotai"
|
||||
import {atomWithStorage} from "jotai/utils"
|
||||
|
||||
import {Theme} from "@/registry/themes"
|
||||
import {Theme} from "Frontend/@/registry/themes"
|
||||
|
||||
type Config = {
|
||||
theme: Theme["name"]
|
||||
|
||||
+7
-2
@@ -2,15 +2,20 @@ import router from 'Frontend/routes.js';
|
||||
import {AuthProvider} from 'Frontend/util/auth.js';
|
||||
import {RouterProvider} from 'react-router-dom';
|
||||
import "./main.css";
|
||||
import {ThemeProvider} from "@material-tailwind/react";
|
||||
import {IconContext} from "@phosphor-icons/react";
|
||||
import {StrictMode} from "react";
|
||||
import {ThemeProvider} from "Frontend/@/components/theme-provider";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<StrictMode>
|
||||
<AuthProvider>
|
||||
<ThemeProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<IconContext.Provider value={{size: 20}}>
|
||||
<RouterProvider router={router}/>
|
||||
</IconContext.Provider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {useField} from "formik";
|
||||
import {Input as MaterialInput, Typography} from "@material-tailwind/react";
|
||||
import {XCircle} from "@phosphor-icons/react";
|
||||
import {Input as ShadcnInput} from "Frontend/@/components/ui/input";
|
||||
import {Label} from "Frontend/@/components/ui/label";
|
||||
|
||||
// @ts-ignore
|
||||
const Input = ({label, ...props}) => {
|
||||
@@ -8,25 +9,21 @@ const Input = ({label, ...props}) => {
|
||||
const [field, meta] = useField(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MaterialInput
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Label htmlFor={label}>{label}</Label>
|
||||
<ShadcnInput
|
||||
{...props}
|
||||
{...field}
|
||||
label={label}
|
||||
error={meta.touched && !!meta.error}
|
||||
success={meta.touched && !meta.error}
|
||||
crossOrigin=""
|
||||
id={label}
|
||||
/>
|
||||
{(meta.touched && !!meta.error) ?
|
||||
<Typography
|
||||
variant="small"
|
||||
color="red"
|
||||
className="ml-3 -mt-5 flex flex-row items-center gap-1"
|
||||
<small
|
||||
className="flex flex-row items-center gap-1 text-red-500"
|
||||
>
|
||||
<XCircle weight="fill" size={14}/> {meta.error}
|
||||
</Typography> : <></>
|
||||
</small> : <></>
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import {useState} from "react";
|
||||
import {Button, Menu, MenuHandler, MenuItem, MenuList} from "@material-tailwind/react";
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {Avatar} from "@hilla/react-components/Avatar";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {CaretDown, CaretUp, GearFine, IconContext, Question, SignOut, User} from "@phosphor-icons/react";
|
||||
import {GearFine, Question, SignOut, User} from "@phosphor-icons/react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "Frontend/@/components/ui/dropdown-menu";
|
||||
import {Avatar, AvatarFallback} from "Frontend/@/components/ui/avatar";
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -36,38 +41,23 @@ export default function ProfileMenu() {
|
||||
];
|
||||
|
||||
return (
|
||||
<Menu open={isMenuOpen} handler={setIsMenuOpen} placement="bottom-end">
|
||||
<MenuHandler>
|
||||
<Button
|
||||
variant="text"
|
||||
className="flex items-center gap-1 rounded-full py-0.5 pr-2 pl-0.5 lg:ml-auto"
|
||||
>
|
||||
<Avatar
|
||||
name={state.user?.name}
|
||||
abbr={state.user?.name?.substring(0, 2)}
|
||||
/>
|
||||
<IconContext.Provider value={{size: 12}}>
|
||||
{isMenuOpen ? <CaretUp/> : <CaretDown/>}
|
||||
</IconContext.Provider>
|
||||
</Button>
|
||||
</MenuHandler>
|
||||
<MenuList className="p-1">
|
||||
<DropdownMenu open={isMenuOpen}>
|
||||
<DropdownMenuTrigger>
|
||||
<Avatar>
|
||||
<AvatarFallback>{state.user?.name?.substring(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{profileMenuItems.map(({label, icon, onClick, showIf, color}) => {
|
||||
return (
|
||||
(showIf === undefined || showIf === true) ?
|
||||
<MenuItem
|
||||
key={label}
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-2 rounded ${
|
||||
color ? `hover:${color}/10 focus:${color}/10 active:${color}/10` : ""
|
||||
}`}
|
||||
>
|
||||
<DropdownMenuItem key={label} onClick={onClick}>
|
||||
{icon}
|
||||
<p color={color ? color : ""}>{label}</p>
|
||||
</MenuItem> : null
|
||||
</DropdownMenuItem> : null
|
||||
);
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export class Theme {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly background: string,
|
||||
public readonly primary: string,
|
||||
public readonly secondary?: string,
|
||||
public readonly tertiary?: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import {Theme} from "Frontend/components/theming/Theme";
|
||||
import {Typography} from "@material-tailwind/react";
|
||||
import {Theme} from "Frontend/@/registry/themes";
|
||||
|
||||
export default function ThemePreview({theme}: { theme: Theme }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<svg width="228" height="120" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="background" d="M0 0H228V120H0V0Z" fill={theme.background}/>
|
||||
<path id="background" d="M0 0H228V120H0V0Z" fill={theme.cssVars.dark.background}/>
|
||||
<rect id="background-secondary" x="29" y="54" width="144" height="53" rx="2" fill="#30363D"/>
|
||||
<rect x="184" y="54" width="32" height="36" rx="2" fill="#30363D"/>
|
||||
<rect opacity="0.3" x="29" y="59" width="144" height="12" fill="#2EA043"/>
|
||||
@@ -18,7 +17,7 @@ export default function ThemePreview({theme}: { theme: Theme }) {
|
||||
<rect x="53" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
|
||||
<rect x="93" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
|
||||
</svg>
|
||||
<Typography variant="paragraph">{theme.name}</Typography>
|
||||
<p>{theme.name}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {useConfig} from "@/hooks/use-config"
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const [config] = useConfig()
|
||||
|
||||
React.useEffect(() => {
|
||||
document.body.classList.forEach((className) => {
|
||||
if (className.match(/^theme.*/)) {
|
||||
document.body.classList.remove(className)
|
||||
}
|
||||
})
|
||||
|
||||
const theme = segment === "themes" ? config.theme : null
|
||||
if (theme) {
|
||||
return document.body.classList.add(`theme-${theme}`)
|
||||
}
|
||||
}, [segment, config])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import {Theme} from "Frontend/components/theming/Theme";
|
||||
|
||||
export class Themes {
|
||||
public static LIGHT_DEFAULT = new Theme(
|
||||
"Light default",
|
||||
"#ffffff",
|
||||
"#000000"
|
||||
)
|
||||
|
||||
public static DARK_DEFAULT = new Theme(
|
||||
"Dark default",
|
||||
"#161B22",
|
||||
"#ffffff"
|
||||
)
|
||||
|
||||
public static all = [
|
||||
Themes.LIGHT_DEFAULT,
|
||||
Themes.DARK_DEFAULT
|
||||
];
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {ReactNode, useState} from "react";
|
||||
import {Form, Formik, FormikBag, FormikHelpers} from "formik";
|
||||
import {Button, Spinner, Step, Stepper} from "@material-tailwind/react";
|
||||
import {ArrowLeft, ArrowRight, Check} from "@phosphor-icons/react";
|
||||
import {ArrowLeft, ArrowRight, Check, SpinnerGap} from "@phosphor-icons/react";
|
||||
import {Button} from "Frontend/@/components/ui/button";
|
||||
|
||||
const Wizard = ({children, initialValues, onSubmit}: {
|
||||
children: ReactNode,
|
||||
@@ -51,16 +51,14 @@ const Wizard = ({children, initialValues, onSubmit}: {
|
||||
{formik => (
|
||||
<Form className="flex flex-col grow">
|
||||
<div className="w-full mb-8">
|
||||
<Stepper
|
||||
activeStep={stepNumber}
|
||||
>
|
||||
<p>Step {stepNumber + 1} of {steps.length}</p>
|
||||
{/*<Stepper activeStep={stepNumber}>
|
||||
{steps.map((child, index) => (
|
||||
<Step key={index}>
|
||||
{/*// @ts-ignore*/}
|
||||
{child.props.icon}
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</Stepper>*/}
|
||||
</div>
|
||||
<div className="flex grow">
|
||||
{step}
|
||||
@@ -76,7 +74,7 @@ const Wizard = ({children, initialValues, onSubmit}: {
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ?
|
||||
<Spinner className="size-5"/> : isLastStep ? <Check/> : <ArrowRight/>
|
||||
<SpinnerGap className="animate-spin"/> : isLastStep ? <Check/> : <ArrowRight/>
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,4 @@ export default function WizardStep({children, icon, validationSchema}: {
|
||||
onSubmit?: (...values: any) => Promise<void>
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
|
||||
//const WizardStep = ({children}: { children: Component }) => children;
|
||||
//export default WizardStep;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {useState} from "react";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {Alert, Button, Card, Input, Spinner, Typography} from "@material-tailwind/react";
|
||||
import {XCircle} from "@phosphor-icons/react";
|
||||
import {SpinnerGap, XCircle} from "@phosphor-icons/react";
|
||||
import {Card} from "Frontend/@/components/ui/card";
|
||||
import {Alert, AlertDescription, AlertTitle} from "Frontend/@/components/ui/alert";
|
||||
import {Button} from "Frontend/@/components/ui/button";
|
||||
import {Input} from "Frontend/@/components/ui/input";
|
||||
|
||||
export default function LoginView() {
|
||||
const {state, login} = useAuth();
|
||||
@@ -19,21 +22,20 @@ export default function LoginView() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="fixed h-full w-full bg-gradient-to-br from-gf-primary to-gf-secondary"></div>
|
||||
<Card className="m-auto p-12" shadow={true}>
|
||||
<div className="flex size-full bg-gradient-to-br from-gf-primary to-gf-secondary">
|
||||
<Card className="m-auto p-12">
|
||||
<img
|
||||
className="h-28 w-full content-center"
|
||||
src="/images/Logo.svg"
|
||||
/>
|
||||
<div className="mt-8 mb-2 w-80 max-w-screen-lg sm:w-96">
|
||||
{hasError &&
|
||||
<Alert
|
||||
icon={<XCircle color="white" weight="fill"/>}
|
||||
className="mb-4 bg-red-500"
|
||||
>
|
||||
Wrong username and/or password
|
||||
</Alert>}
|
||||
<Alert className="mb-4" variant="destructive">
|
||||
<XCircle weight="fill" className="size-4"/>
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>Wrong username and/or password</AlertDescription>
|
||||
</Alert>
|
||||
}
|
||||
<form
|
||||
className="mb-1 flex flex-col gap-6"
|
||||
onSubmit={async e => {
|
||||
@@ -51,44 +53,32 @@ export default function LoginView() {
|
||||
}}
|
||||
>
|
||||
<label htmlFor="username">
|
||||
<Typography variant="h6" color="blue-gray" className="-mb-3">
|
||||
<h6 color="blue-gray" className="-mb-3">
|
||||
Username
|
||||
</Typography>
|
||||
</h6>
|
||||
</label>
|
||||
<Input
|
||||
onChange={(event) => {
|
||||
onChange={(event: any) => {
|
||||
setUsername(event.target.value);
|
||||
}}
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
placeholder=""
|
||||
size="lg"
|
||||
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
labelProps={{
|
||||
className: "before:content-none after:content-none",
|
||||
}}
|
||||
crossOrigin="" //TODO: see https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
/>
|
||||
<label htmlFor="current-password">
|
||||
<Typography variant="h6" color="blue-gray" className="-mb-3">
|
||||
<h6 color="blue-gray" className="-mb-3">
|
||||
Password
|
||||
</Typography>
|
||||
</h6>
|
||||
</label>
|
||||
<Input
|
||||
onChange={(event) => {
|
||||
onChange={(event: any) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
id="current-password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder=""
|
||||
size="lg"
|
||||
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
labelProps={{
|
||||
className: "before:content-none after:content-none",
|
||||
}}
|
||||
crossOrigin="" //TODO: see https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<Link to="#">Forgot password?</Link>
|
||||
@@ -98,7 +88,7 @@ export default function LoginView() {
|
||||
className="w-28 h-12 flex justify-center"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? <Spinner className="h-6 w-6"/> : "Log in"}
|
||||
{loading ? <SpinnerGap className="size-6 animate-spin"/> : "Log in"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {useRouteMetadata} from 'Frontend/util/routing.js';
|
||||
import {useEffect} from 'react';
|
||||
import {Navbar} from "@material-tailwind/react";
|
||||
import ProfileMenu from "Frontend/components/ProfileMenu";
|
||||
import {Outlet} from "react-router-dom";
|
||||
import {Card} from "Frontend/@/components/ui/card";
|
||||
|
||||
export default function MainLayout() {
|
||||
const currentTitle = `Gameyfin - ${useRouteMetadata()?.title}` ?? 'Gameyfin';
|
||||
@@ -12,7 +12,7 @@ export default function MainLayout() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar className="sticky top-0 z-10 h-max max-w-full rounded-none px-4 py-2">
|
||||
<Card className="sticky top-0 z-10 h-max max-w-full rounded-none px-4 py-2">
|
||||
<div className="flex items-center justify-end text-blue-gray-900">
|
||||
<img
|
||||
className="absolute w-full content-center h-8"
|
||||
@@ -21,7 +21,7 @@ export default function MainLayout() {
|
||||
/>
|
||||
<ProfileMenu/>
|
||||
</div>
|
||||
</Navbar>
|
||||
</Card>
|
||||
|
||||
<Outlet/>
|
||||
</>
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import Wizard from "Frontend/components/wizard/Wizard";
|
||||
import WizardStep from "Frontend/components/wizard/WizardStep";
|
||||
import {Card, Typography} from "@material-tailwind/react";
|
||||
import Input from "Frontend/components/Input";
|
||||
import {GearFine, HandWaving, Palette, User} from "@phosphor-icons/react";
|
||||
import {Themes} from "Frontend/components/theming/Themes";
|
||||
import ThemePreview from "Frontend/components/theming/ThemePreview";
|
||||
import {Theme, themes} from "Frontend/@/registry/themes";
|
||||
import {Card} from "Frontend/@/components/ui/card";
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
@@ -14,7 +14,7 @@ function WelcomeStep() {
|
||||
return (
|
||||
<div className="flex flex-col size-full items-center">
|
||||
<div className="flex flex-col w-1/2 min-w-[468px] gap-12 items-center">
|
||||
<Typography variant="h4">Welcome to Gameyfin 👋</Typography>
|
||||
<h4>Welcome to Gameyfin 👋</h4>
|
||||
<p className="place-content-center text-justify">
|
||||
Gameyfin is a cutting-edge software tailored for gamers seeking efficient management of their
|
||||
video
|
||||
@@ -28,7 +28,7 @@ function WelcomeStep() {
|
||||
user-friendly
|
||||
design and adaptability, offering ample customization options to meet diverse user preferences.
|
||||
</p>
|
||||
<Typography variant="h5">Let's get started!</Typography>
|
||||
<h5>Let's get started!</h5>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -38,7 +38,7 @@ function ThemeStep() {
|
||||
return (
|
||||
<div className="flex flex-col size-full items-center">
|
||||
<div className="size-full grid grid-cols-3 grid-rows-2 w-1/2 min-w-[468px] gap-12 items-center">
|
||||
{Themes.all.map((theme => (
|
||||
{themes.map(((theme: Theme) => (
|
||||
<ThemePreview key={theme.name} theme={theme}/>
|
||||
)))}
|
||||
</div>
|
||||
@@ -50,9 +50,9 @@ function UserStep() {
|
||||
return (
|
||||
<div className="flex flex-col size-full items-center">
|
||||
<div className="flex flex-col w-1/2 min-w-[468px] gap-12 items-center">
|
||||
<Typography variant="h4">Create your account</Typography>
|
||||
<Typography className="-mt-8">This will set up the initial admin user account.</Typography>
|
||||
<div className="mb-1 flex flex-col w-full gap-6">
|
||||
<h4>Create your account</h4>
|
||||
<p className="-mt-8">This will set up the initial admin user account.</p>
|
||||
<div className="mb-1 flex flex-col w-full gap-6 items-center">
|
||||
<Input
|
||||
label="Username"
|
||||
name="username"
|
||||
@@ -78,17 +78,16 @@ function SettingsStep() {
|
||||
return (
|
||||
<div className="flex flex-col size-full items-center">
|
||||
<div className="flex flex-col w-1/2 min-w-[468px] gap-12 items-center">
|
||||
<Typography variant="h4">Settings</Typography>
|
||||
<Typography>Configure your settings</Typography>
|
||||
<h4>Settings</h4>
|
||||
<p>Configure your settings</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SetupView = () => (
|
||||
<div className="flex h-screen">
|
||||
<div className="fixed size-full bg-gradient-to-br from-gf-primary to-gf-secondary"></div>
|
||||
<Card className="w-3/4 h-3/4 min-w-[500px] m-auto p-8" shadow={true}>
|
||||
<div className="flex size-full bg-gradient-to-br from-gf-primary to-gf-secondary">
|
||||
<Card className="w-3/4 h-3/4 min-w-[500px] m-auto p-8">
|
||||
<Wizard
|
||||
initialValues={{username: '', password: '', passwordRepeat: ''}}
|
||||
onSubmit={async (values: any) =>
|
||||
|
||||
Reference in New Issue
Block a user