mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 08:15:37 +00:00
Update to Hilla 24
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import * as React from "react"
|
||||
import {cva, type VariantProps} from "class-variance-authority"
|
||||
import {cn} from "Frontend/util/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
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,29 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-content1 group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,29 @@
|
||||
import {Outlet, useNavigate} from 'react-router-dom';
|
||||
import "./main.css";
|
||||
import {NextUIProvider} from "@nextui-org/react";
|
||||
import {ThemeProvider as NextThemesProvider} from "next-themes";
|
||||
import {themeNames} from "Frontend/theming/themes";
|
||||
import {AuthProvider} from "Frontend/util/auth";
|
||||
import {IconContext} from "@phosphor-icons/react";
|
||||
import {Toaster} from "Frontend/@/components/ui/sonner";
|
||||
import client from "Frontend/generated/connect-client.default";
|
||||
import {ErrorHandlingMiddleware} from "Frontend/util/middleware";
|
||||
|
||||
export default function App() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
client.middlewares = [ErrorHandlingMiddleware];
|
||||
|
||||
return (
|
||||
<NextUIProvider className="size-full" navigate={navigate}>
|
||||
<NextThemesProvider attribute="class" themes={themeNames()} defaultTheme="gameyfin-violet-dark">
|
||||
<AuthProvider>
|
||||
<IconContext.Provider value={{size: 20}}>
|
||||
<Outlet/>
|
||||
<Toaster/>
|
||||
</IconContext.Provider>
|
||||
</AuthProvider>
|
||||
</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {useField} from "formik";
|
||||
import {XCircle} from "@phosphor-icons/react";
|
||||
import {Input as NextUiInput} from "@nextui-org/react";
|
||||
|
||||
// @ts-ignore
|
||||
const Input = ({label, ...props}) => {
|
||||
// @ts-ignore
|
||||
const [field, meta] = useField(props);
|
||||
|
||||
return (
|
||||
<div className="grid w-full max-w-sm items-center gap-1.5">
|
||||
<NextUiInput
|
||||
{...props}
|
||||
{...field}
|
||||
id={label}
|
||||
placeholder={label}
|
||||
isInvalid={meta.touched && !!meta.error}
|
||||
errorMessage={
|
||||
<small className="flex flex-row items-center gap-1 text-danger">
|
||||
<XCircle weight="fill" size={14}/> {meta.error}
|
||||
</small>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Input;
|
||||
@@ -0,0 +1,68 @@
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {GearFine, Question, SignOut, User} from "@phosphor-icons/react";
|
||||
import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@nextui-org/react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const {state, logout} = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const profileMenuItems = [
|
||||
{
|
||||
label: "My Profile",
|
||||
icon: <User/>,
|
||||
onClick: () => navigate('/profile')
|
||||
},
|
||||
{
|
||||
label: "Administration",
|
||||
icon: <GearFine/>,
|
||||
onClick: () => alert("Administration"),
|
||||
showIf: state.user?.roles?.some(a => a?.includes("ADMIN"))
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
icon: <Question/>,
|
||||
onClick: () => window.open("https://github.com/gameyfin/gameyfin/tree/v2", "_blank")
|
||||
},
|
||||
{
|
||||
label: "Sign Out",
|
||||
icon: <SignOut/>,
|
||||
onClick: () => logout(),
|
||||
color: "primary"
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar showFallback
|
||||
radius="full"
|
||||
as="button"
|
||||
className="transition-transform size-8"
|
||||
classNames={{
|
||||
base: "gradient-primary",
|
||||
icon: "text-background/80"
|
||||
}}
|
||||
/>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
{/* @ts-ignore */}
|
||||
{profileMenuItems.map(({label, icon, onClick, showIf, color}) => {
|
||||
return (
|
||||
(showIf === undefined || showIf === true) ?
|
||||
<DropdownItem
|
||||
key={label}
|
||||
onClick={onClick}
|
||||
startContent={<div color={color}>{icon}</div>}
|
||||
/* @ts-ignore */
|
||||
color={color ? color : ""}
|
||||
className={`text-${color} hover:bg-primary/20`}
|
||||
>
|
||||
{label}
|
||||
</DropdownItem> : null
|
||||
);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function GameyfinLogo({className}: {
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 365.58 336.34" className={className}>
|
||||
<polygon points="190.1 49.13 190.1 69.24 207.98 44.13 190.1 49.13"/>
|
||||
<polygon points="365.58 0 263.22 28.66 205.64 95.97 365.58 51.18 365.58 0"/>
|
||||
<polygon
|
||||
points="190.1 283.11 248.6 266.73 248.6 149.74 365.58 116.99 365.58 73.12 190.1 122.25 190.1 283.11"/>
|
||||
<polygon
|
||||
points="58.49 144.48 155.98 117.18 175.48 89.79 175.48 53.23 0 102.36 0 336.34 58.49 254.15 58.49 144.48"/>
|
||||
<polygon
|
||||
points="116.99 199.59 116.99 245.09 65.81 259.42 0 336.34 175.48 287.2 175.48 170.22 131.61 182.5 116.99 199.59"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {Theme} from "Frontend/theming/theme";
|
||||
import {Tooltip} from "@nextui-org/react";
|
||||
|
||||
export default function ThemePreview({theme, mode, isSelected}: {
|
||||
theme: Theme,
|
||||
mode: "light" | "dark",
|
||||
isSelected?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Tooltip content={<p className="capitalize">{theme.name?.replace("-", " ")}</p>} placement="bottom">
|
||||
<div className={`
|
||||
${theme.name}-${mode}
|
||||
bg-primary p-6 border-2 rounded-full
|
||||
${isSelected ? "border-foreground" : "border-foreground-200 hover:border-focus"}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {useTheme} from "next-themes";
|
||||
import React, {useLayoutEffect, useState} from "react";
|
||||
import {Switch} from "@nextui-org/react";
|
||||
import {Moon, SunDim} from "@phosphor-icons/react";
|
||||
import {themes} from "Frontend/theming/themes";
|
||||
import {Theme} from "Frontend/theming/theme";
|
||||
import ThemePreview from "Frontend/components/theming/ThemePreview";
|
||||
|
||||
export function ThemeSelector() {
|
||||
|
||||
const {theme, setTheme} = useTheme();
|
||||
const [isSelected, setIsSelected] = useState(theme ? theme.includes("light") : false);
|
||||
const [currentTheme, setCurrentTheme] = useState(theme?.substring(0, theme?.lastIndexOf("-")));
|
||||
|
||||
useLayoutEffect(() => setTheme(`${currentTheme}-${mode()}`), [isSelected]);
|
||||
|
||||
function mode(): "light" | "dark" {
|
||||
return isSelected ? "light" : "dark";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col size-full items-center">
|
||||
<div className="w-full flex flex-row items-center justify-center gap-4 mb-16">
|
||||
<Switch
|
||||
size="lg"
|
||||
startContent={<SunDim size={32}/>}
|
||||
endContent={<Moon size={32}/>}
|
||||
isSelected={isSelected}
|
||||
onValueChange={() => {
|
||||
setIsSelected(!isSelected);
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-flow-col auto-cols-auto auto-cols-max-4 gap-8">
|
||||
{
|
||||
//min-w-[468px]
|
||||
themes.map(((t: Theme) => (
|
||||
<div
|
||||
key={t.name}
|
||||
onClick={() => {
|
||||
setCurrentTheme(t.name);
|
||||
setTheme(`${t.name}-${mode()}`);
|
||||
}}>
|
||||
<ThemePreview
|
||||
theme={t}
|
||||
mode={mode()}
|
||||
isSelected={currentTheme === t.name}/>
|
||||
</div>
|
||||
)))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, {ReactNode, useState} from "react";
|
||||
import {Form, Formik, FormikBag, FormikHelpers} from "formik";
|
||||
import {ArrowLeft, ArrowRight, Check} from "@phosphor-icons/react";
|
||||
import {Button} from "@nextui-org/react";
|
||||
import {Step, Stepper} from "@material-tailwind/react";
|
||||
|
||||
const Wizard = ({children, initialValues, onSubmit}: {
|
||||
children: ReactNode,
|
||||
initialValues: any,
|
||||
onSubmit: (values: any, bag: FormikHelpers<any> | FormikBag<any, any>) => Promise<any>
|
||||
}) => {
|
||||
const [stepNumber, setStepNumber] = useState(0);
|
||||
const steps = React.Children.toArray(children);
|
||||
const [snapshot, setSnapshot] = useState(initialValues);
|
||||
|
||||
const step = steps[stepNumber];
|
||||
const totalSteps = steps.length;
|
||||
const isFirstStep = stepNumber === 0;
|
||||
const isLastStep = stepNumber === totalSteps - 1;
|
||||
|
||||
const next = (values: any) => {
|
||||
setSnapshot(values);
|
||||
setStepNumber(Math.min(stepNumber + 1, totalSteps - 1));
|
||||
};
|
||||
|
||||
const previous = (values: any) => {
|
||||
setSnapshot(values);
|
||||
setStepNumber(Math.max(stepNumber - 1, 0));
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: any, bag: FormikBag<any, any> | FormikHelpers<any>) => {
|
||||
/*// @ts-ignore*/
|
||||
if (step.props.onSubmit) {
|
||||
/*// @ts-ignore*/
|
||||
await step.props.onSubmit(values, bag);
|
||||
}
|
||||
if (isLastStep) {
|
||||
return onSubmit(values, bag);
|
||||
} else {
|
||||
await bag.setTouched({});
|
||||
next(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={snapshot}
|
||||
onSubmit={handleSubmit}
|
||||
/*// @ts-ignore*/
|
||||
validationSchema={step.props.validationSchema}
|
||||
>
|
||||
{(formik: { values: any; isSubmitting: any; }) => (
|
||||
<Form className="flex flex-col h-full">
|
||||
<div className="w-full mb-8">
|
||||
{/*<p>Step {stepNumber + 1} of {steps.length}</p>*/}
|
||||
<Stepper activeStep={stepNumber} activeLineClassName="bg-primary"
|
||||
lineClassName="bg-foreground"
|
||||
placeholder={undefined}
|
||||
onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined}>
|
||||
{steps.map((child, index) => (
|
||||
<Step key={index}
|
||||
className="bg-foreground text-background"
|
||||
activeClassName="bg-primary"
|
||||
completedClassName="bg-primary"
|
||||
placeholder={undefined}
|
||||
onPointerEnterCapture={undefined}
|
||||
onPointerLeaveCapture={undefined}>
|
||||
{/*@ts-ignore*/}
|
||||
{child.props.icon}
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</div>
|
||||
<div className="flex grow">
|
||||
{step}
|
||||
</div>
|
||||
<div className="left-8 right-8 absolute bottom-8 -z-1">
|
||||
<div className="flex justify-between">
|
||||
<Button color="primary" onClick={() => previous(formik.values)} disabled={isFirstStep}>
|
||||
<ArrowLeft/>
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : isLastStep ? <Check/> : <ArrowRight/>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wizard;
|
||||
@@ -0,0 +1,11 @@
|
||||
import {JSX, ReactNode} from "react";
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export default function WizardStep({children, icon, validationSchema}: {
|
||||
children: ReactNode,
|
||||
icon: JSX.Element,
|
||||
validationSchema?: Yup.Schema,
|
||||
onSubmit?: (...values: any) => Promise<void>
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Gameyfin</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#outlet {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="outlet"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import {StrictMode} from "react";
|
||||
import {RouterProvider} from "react-router-dom";
|
||||
import router from "./routes";
|
||||
|
||||
const container = document.getElementById('outlet')!;
|
||||
const root = createRoot(container);
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router}/>
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.gradient-primary {
|
||||
@apply bg-gradient-to-br from-primary-400 to-primary-700;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {protectRoutes} from '@vaadin/hilla-react-auth';
|
||||
import {createBrowserRouter, RouteObject} from 'react-router-dom';
|
||||
import LoginView from "Frontend/views/LoginView";
|
||||
import MainLayout from "Frontend/views/MainLayout";
|
||||
import TestView from "Frontend/views/TestView";
|
||||
import SetupView from "Frontend/views/SetupView";
|
||||
import ProfileView from "Frontend/views/ProfileView";
|
||||
import {ThemeSelector} from "Frontend/components/theming/ThemeSelector";
|
||||
import App from "Frontend/App";
|
||||
|
||||
export const routes = protectRoutes([
|
||||
{
|
||||
element: <App/>,
|
||||
handle: {requiresLogin: false},
|
||||
children: [
|
||||
{
|
||||
element: <MainLayout/>,
|
||||
handle: {requiresLogin: true},
|
||||
children: [
|
||||
{
|
||||
index: true, element: <TestView/>
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
element: <ProfileView/>,
|
||||
children: [
|
||||
{path: 'appearance', element: <ThemeSelector/>}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login', element: <LoginView/>, handle: {requiresLogin: false}
|
||||
},
|
||||
{
|
||||
path: '/setup', element: <SetupView/>, handle: {requiresLogin: false}
|
||||
}
|
||||
],
|
||||
}
|
||||
]) as RouteObject[];
|
||||
|
||||
export default createBrowserRouter(routes);
|
||||
@@ -0,0 +1,21 @@
|
||||
export type Theme = {
|
||||
name?: string,
|
||||
colors: {
|
||||
background?: string,
|
||||
foreground?: string,
|
||||
primary: {
|
||||
50: string,
|
||||
100: string,
|
||||
200: string,
|
||||
300: string,
|
||||
400: string,
|
||||
500: string,
|
||||
600: string,
|
||||
700: string,
|
||||
800: string,
|
||||
900: string,
|
||||
DEFAULT: string
|
||||
},
|
||||
focus?: string,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {GameyfinClassic} from "./themes/gameyfin-classic";
|
||||
import {GameyfinBlue} from "./themes/gameyfin-blue";
|
||||
import {GameyfinViolet} from "./themes/gameyfin-violet";
|
||||
import {Purple} from "./themes/purple";
|
||||
import {Neutral} from "./themes/neutral";
|
||||
import {Slate} from "./themes/slate";
|
||||
import {Red} from "./themes/red";
|
||||
import {Rose} from "./themes/rose";
|
||||
import {Blue} from "./themes/blue";
|
||||
import {Yellow} from "./themes/yellow";
|
||||
import {Violet} from "./themes/violet";
|
||||
import {Orange} from "./themes/orange";
|
||||
import {Theme} from "./theme";
|
||||
import {ConfigTheme, ConfigThemes} from "@nextui-org/react";
|
||||
|
||||
|
||||
function light(c: Theme): ConfigTheme {
|
||||
let t: Theme = structuredClone(c);
|
||||
delete t.name;
|
||||
(t as ConfigTheme).extend = "light";
|
||||
return t;
|
||||
}
|
||||
|
||||
function dark(c: Theme): ConfigTheme {
|
||||
let t: Theme = structuredClone(c);
|
||||
delete t.name;
|
||||
(t as ConfigTheme).extend = "dark";
|
||||
return t;
|
||||
}
|
||||
|
||||
export function compileThemes(themes: Theme[]): ConfigThemes {
|
||||
let compiledThemes: any = {};
|
||||
|
||||
themes.forEach((c: Theme) => {
|
||||
compiledThemes[`${c.name}-light`] = light(c);
|
||||
compiledThemes[`${c.name}-dark`] = dark(c);
|
||||
})
|
||||
|
||||
return compiledThemes;
|
||||
}
|
||||
|
||||
export function themeNames(): string[] {
|
||||
return Object.keys(compileThemes(themes));
|
||||
}
|
||||
|
||||
export const themes: Theme[] = [GameyfinBlue, GameyfinViolet, GameyfinClassic, Neutral, Slate, Red, Rose, Orange, Purple, Blue, Yellow, Violet];
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const Blue: Theme = {
|
||||
name: 'blue',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#e3eeff',
|
||||
100: '#b6cdfe',
|
||||
200: '#88abf7',
|
||||
300: '#5b8af1',
|
||||
400: '#2d69ec',
|
||||
500: '#134fd2',
|
||||
600: '#0b3da4',
|
||||
700: '#052c77',
|
||||
800: '#001a4a',
|
||||
900: '#00091e',
|
||||
DEFAULT: '#2563EB'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const GameyfinBlue: Theme = {
|
||||
name: 'gameyfin-blue',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#e7eaff',
|
||||
100: '#bdc3f9',
|
||||
200: '#919bee',
|
||||
300: '#6672e5',
|
||||
400: '#3c4add',
|
||||
500: '#2231c3',
|
||||
600: '#1a2699',
|
||||
700: '#101b6f',
|
||||
800: '#070f45',
|
||||
900: '#02041d',
|
||||
DEFAULT: '#2332c8'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const GameyfinClassic: Theme = {
|
||||
name: 'gameyfin-classic',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#e1ffec',
|
||||
100: '#b8f7cf',
|
||||
200: '#8ef0b2',
|
||||
300: '#62ea94',
|
||||
400: '#38e476',
|
||||
500: '#20ca5d',
|
||||
600: '#159d47',
|
||||
700: '#0b7032',
|
||||
800: '#02431d',
|
||||
900: '#001804',
|
||||
DEFAULT: '#16A34A'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const GameyfinViolet: Theme = {
|
||||
name: 'gameyfin-violet',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f3ebff',
|
||||
100: '#d5c7ed',
|
||||
200: '#b7a4dd',
|
||||
300: '#9a7fce',
|
||||
400: '#7d5abe',
|
||||
500: '#6441a5',
|
||||
600: '#4e3281',
|
||||
700: '#37235d',
|
||||
800: '#21153a',
|
||||
900: '#0d0519',
|
||||
DEFAULT: '#6441a5'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const Neutral: Theme = {
|
||||
name: 'neutral',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#fceff2',
|
||||
100: '#ddd7d9',
|
||||
200: '#c1bfbf',
|
||||
300: '#a6a6a6',
|
||||
400: '#8c8c8c',
|
||||
500: '#737373',
|
||||
600: '#595959',
|
||||
700: '#413f40',
|
||||
800: '#292526',
|
||||
900: '#16090d',
|
||||
DEFAULT: '#525252'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const Orange: Theme = {
|
||||
name: 'orange',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#ffedde',
|
||||
100: '#ffcdb2',
|
||||
200: '#fbac84',
|
||||
300: '#f78c54',
|
||||
400: '#f46c25',
|
||||
500: '#da520b',
|
||||
600: '#ab3f07',
|
||||
700: '#7a2d03',
|
||||
800: '#4b1900',
|
||||
900: '#1f0600',
|
||||
DEFAULT: '#EA580C'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from "../theme";
|
||||
|
||||
export const Purple: Theme = {
|
||||
name: 'purple',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#ffe6ff',
|
||||
100: '#f2b9f9',
|
||||
200: '#e78df3',
|
||||
300: '#dc5fed',
|
||||
400: '#d132e6',
|
||||
500: '#b91acd',
|
||||
600: '#9012a0',
|
||||
700: '#670b73',
|
||||
800: '#3f0547',
|
||||
900: '#19001b',
|
||||
DEFAULT: '#DD62ED'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const Red: Theme = {
|
||||
name: 'red',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#ffe5e5',
|
||||
100: '#f9bbbb',
|
||||
200: '#ef9090',
|
||||
300: '#e76464',
|
||||
400: '#df3939',
|
||||
500: '#c62020',
|
||||
600: '#9b1718',
|
||||
700: '#6f0f11',
|
||||
800: '#450708',
|
||||
900: '#1e0000',
|
||||
DEFAULT: '#DC2626'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from "../theme";
|
||||
|
||||
export const Rose: Theme = {
|
||||
name: 'rose',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#ffe4ed',
|
||||
100: '#fbb9c8',
|
||||
200: '#f28da4',
|
||||
300: '#ec607f',
|
||||
400: '#e5345b',
|
||||
500: '#cb1a41',
|
||||
600: '#9f1233',
|
||||
700: '#730b23',
|
||||
800: '#470415',
|
||||
900: '#1e0006',
|
||||
DEFAULT: '#E11D48'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from "../theme";
|
||||
|
||||
export const Slate: Theme = {
|
||||
name: 'slate',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eaf3ff',
|
||||
100: '#cfd7e4',
|
||||
200: '#b2bdcd',
|
||||
300: '#95a3b7',
|
||||
400: '#7788a1',
|
||||
500: '#5e6f88',
|
||||
600: '#48566a',
|
||||
700: '#323e4d',
|
||||
800: '#1d2531',
|
||||
900: '#040d17',
|
||||
DEFAULT: '#475569'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const Violet: Theme = {
|
||||
name: 'violet',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f2e7ff',
|
||||
100: '#d4bdf8',
|
||||
200: '#b592ee',
|
||||
300: '#9867e5',
|
||||
400: '#7b3cdd',
|
||||
500: '#6122c3',
|
||||
600: '#4b1a99',
|
||||
700: '#36126e',
|
||||
800: '#200944',
|
||||
900: '#0d021c',
|
||||
DEFAULT: '#6D28D9'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Theme} from '../theme';
|
||||
|
||||
export const Yellow: Theme = {
|
||||
name: 'yellow',
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#fffadb',
|
||||
100: '#feefae',
|
||||
200: '#fce47f',
|
||||
300: '#fbd94e',
|
||||
400: '#face1e',
|
||||
500: '#e1b505',
|
||||
600: '#af8c00',
|
||||
700: '#7d6400',
|
||||
800: '#4c3c00',
|
||||
900: '#1c1400',
|
||||
DEFAULT: '#FACC15'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import {configureAuth} from '@vaadin/hilla-react-auth';
|
||||
import {UserEndpoint} from 'Frontend/generated/endpoints';
|
||||
|
||||
// Configure auth to use `UserInfoService.getUserInfo`
|
||||
const auth = configureAuth(UserEndpoint.getUserInfo);
|
||||
|
||||
// Export auth provider and useAuth hook, which are automatically
|
||||
// typed to the result of `UserInfoService.getUserInfo`
|
||||
export const useAuth = auth.useAuth;
|
||||
export const AuthProvider = auth.AuthProvider;
|
||||
@@ -0,0 +1,31 @@
|
||||
import {Middleware, MiddlewareContext, MiddlewareNext} from '@vaadin/hilla-frontend';
|
||||
import {toast} from "sonner";
|
||||
import {getReasonPhrase} from "http-status-codes";
|
||||
|
||||
export const ErrorHandlingMiddleware: Middleware = async function (
|
||||
context: MiddlewareContext,
|
||||
next: MiddlewareNext
|
||||
) {
|
||||
const {endpoint, method} = context;
|
||||
|
||||
let originalResponse = (await next(context));
|
||||
|
||||
if (!originalResponse.ok) {
|
||||
// .clone() is necessary because response.json() is one-time only and Hilla accesses it in its internal error handler
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/Response/clone
|
||||
let response: Response = originalResponse.clone();
|
||||
|
||||
//Ignore calls to UserEndpoint.getUserInfo since they are managed by Hilla and called on initial load
|
||||
if (endpoint == "UserEndpoint" && method == "getUserInfo") return originalResponse;
|
||||
|
||||
let json: any = await response.json();
|
||||
|
||||
if (json.type == "dev.hilla.exception.EndpointException") {
|
||||
toast.error(`${getReasonPhrase(response.status)}`, {description: `${json.message}`});
|
||||
} else {
|
||||
toast.error(`${getReasonPhrase(response.status)}`, {description: `${endpoint}.${method}`})
|
||||
}
|
||||
}
|
||||
|
||||
return originalResponse;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMatches } from 'react-router-dom';
|
||||
|
||||
type RouteMetadata = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the `handle` object containing the metadata for the current route,
|
||||
* or undefined if the route does not have defined a handle.
|
||||
*/
|
||||
export function useRouteMetadata(): RouteMetadata | undefined {
|
||||
const matches = useMatches();
|
||||
const match = matches[matches.length - 1];
|
||||
return match?.handle as RouteMetadata | undefined;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {type ClassValue, clsx} from "clsx"
|
||||
import {twMerge} from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function cssVar(variable: string) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`);
|
||||
}
|
||||
|
||||
export function hsl(hsl: string) {
|
||||
return `hsl(${hsl}`;
|
||||
}
|
||||
|
||||
export function rand(min: number, max: number) {
|
||||
const minCeiled = Math.ceil(min);
|
||||
const maxFloored = Math.floor(max);
|
||||
return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {useLayoutEffect, useState} from "react";
|
||||
import {XCircle} from "@phosphor-icons/react";
|
||||
import {Button, Card, CardBody, CardHeader, Input, Link} from "@nextui-org/react";
|
||||
import {Alert, AlertDescription, AlertTitle} from "Frontend/@/components/ui/alert";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export default function LoginView() {
|
||||
const {state, login} = useAuth();
|
||||
const [hasError, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [password, setPassword] = useState<string>();
|
||||
const [url, setUrl] = useState<string>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (state.user) {
|
||||
const path = url ? new URL(url, document.baseURI).pathname : '/'
|
||||
navigate(path, {replace: true});
|
||||
}
|
||||
}, [state.user]);
|
||||
|
||||
return (
|
||||
<div className="flex size-full gradient-primary">
|
||||
<Card className="m-auto p-12">
|
||||
<CardHeader>
|
||||
<img
|
||||
className="h-28 w-full content-center"
|
||||
src="/images/Logo.svg"
|
||||
alt="Gameyfin Logo"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBody className="mt-8 mb-2 w-80 max-w-screen-lg sm:w-96">
|
||||
{hasError &&
|
||||
<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 => {
|
||||
e.preventDefault();
|
||||
if (typeof username === "string" && password != null) {
|
||||
setLoading(true);
|
||||
const {defaultUrl, error, redirectUrl} = await login(username, password);
|
||||
if (error) {
|
||||
setError(true);
|
||||
} else {
|
||||
setUrl(redirectUrl ?? defaultUrl ?? '/');
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label htmlFor="username">
|
||||
<h6 color="blue-gray" className="-mb-3">
|
||||
Username
|
||||
</h6>
|
||||
</label>
|
||||
<Input
|
||||
onChange={(event: any) => {
|
||||
setUsername(event.target.value);
|
||||
}}
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
placeholder=""
|
||||
/>
|
||||
<label htmlFor="current-password">
|
||||
<h6 color="blue-gray" className="-mb-3">
|
||||
Password
|
||||
</h6>
|
||||
</label>
|
||||
<Input
|
||||
onChange={(event: any) => {
|
||||
setPassword(event.target.value);
|
||||
}}
|
||||
id="current-password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder=""
|
||||
/>
|
||||
<div className="flex justify-between items-center">
|
||||
<Link color="foreground" underline="always">Forgot password?</Link>
|
||||
<Button color="primary" type="submit" isLoading={loading}>
|
||||
{loading ? "" : "Log in"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {useRouteMetadata} from 'Frontend/util/routing.js';
|
||||
import {useEffect} from 'react';
|
||||
import ProfileMenu from "Frontend/components/ProfileMenu";
|
||||
import {Divider, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem} from "@nextui-org/react";
|
||||
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
|
||||
import * as PackageJson from "../../../../package.json";
|
||||
import {Outlet, useNavigate} from "react-router-dom";
|
||||
|
||||
export default function MainLayout() {
|
||||
const currentTitle = `Gameyfin - ${useRouteMetadata()?.title}` ?? 'Gameyfin';
|
||||
useEffect(() => {
|
||||
document.title = currentTitle;
|
||||
}, [currentTitle]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-svh">
|
||||
<Navbar maxWidth="2xl" className="shadow">
|
||||
<NavbarBrand as="button" onClick={() => navigate('/')}>
|
||||
<GameyfinLogo className="h-10 fill-foreground"/>
|
||||
</NavbarBrand>
|
||||
<NavbarContent justify="end">
|
||||
<NavbarItem>
|
||||
<ProfileMenu/>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex-row relative m-auto max-w-[1536px] align-self-center px-2 pt-4">
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider/>
|
||||
|
||||
<footer className="flex flex-row items-center justify-between py-4 px-12">
|
||||
<p>Gameyfin {PackageJson.version}</p>
|
||||
<p>
|
||||
© {(new Date()).getFullYear()} 
|
||||
<Link href="https://github.com/gameyfin/gameyfin/graphs/contributors" target="_blank">
|
||||
Gameyfin contributors
|
||||
</Link>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**/
|
||||
@@ -0,0 +1,47 @@
|
||||
import {Listbox, ListboxItem} from "@nextui-org/react";
|
||||
import {GearFine, Palette, User} from "@phosphor-icons/react";
|
||||
import {Outlet, useNavigate} from "react-router-dom";
|
||||
import {useState} from "react";
|
||||
|
||||
export default function ProfileView() {
|
||||
const navigate = useNavigate();
|
||||
const [selectedKeys, setSelectedKeys] = useState(new Set(["profile"]));
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
title: "My Profile",
|
||||
key: "profile",
|
||||
icon: <User/>,
|
||||
action: () => navigate('/profile')
|
||||
},
|
||||
{
|
||||
title: "Appearance",
|
||||
key: "appearance",
|
||||
icon: <Palette/>,
|
||||
action: () => navigate('appearance')
|
||||
},
|
||||
{
|
||||
title: "Manage account",
|
||||
icon: <GearFine/>,
|
||||
key: "account-management",
|
||||
action: () => navigate('account-management')
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-col pr-8">
|
||||
<Listbox className="min-w-60">
|
||||
{menuItems.map((i) => (
|
||||
<ListboxItem key={i.key} onPress={i.action} startContent={i.icon}>
|
||||
{i.title}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import Wizard from "Frontend/components/wizard/Wizard";
|
||||
import WizardStep from "Frontend/components/wizard/WizardStep";
|
||||
import Input from "Frontend/components/Input";
|
||||
import {GearFine, HandWaving, Palette, User} from "@phosphor-icons/react";
|
||||
import {Card} from "@nextui-org/react";
|
||||
import {SetupEndpoint} from "Frontend/generated/endpoints";
|
||||
import {ThemeSelector} from "Frontend/components/theming/ThemeSelector";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {toast} from "sonner";
|
||||
|
||||
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">
|
||||
<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
|
||||
game collections. <br/><br/> With its intuitive interface and comprehensive features, Gameyfin
|
||||
simplifies the organization of game libraries. Users can effortlessly add games through manual
|
||||
input
|
||||
or
|
||||
automated recognition, categorize them based on various criteria like genre or platform, track
|
||||
in-game
|
||||
progress, and share achievements with friends. <br/><br/> Notably, Gameyfin stands out for its
|
||||
user-friendly
|
||||
design and adaptability, offering ample customization options to meet diverse user preferences.
|
||||
</p>
|
||||
<h5>Let's get started!</h5>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeStep() {
|
||||
return (
|
||||
<ThemeSelector/>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<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"
|
||||
type="text"
|
||||
/>
|
||||
<Input
|
||||
label="E-Mail"
|
||||
name="email"
|
||||
type="email"
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
/>
|
||||
<Input
|
||||
label="Password (repeat)"
|
||||
name="passwordRepeat"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<h4>Settings</h4>
|
||||
<p>Configure your settings</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupView() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex size-full gradient-primary">
|
||||
<Card className="w-3/4 h-3/4 min-w-[500px] m-auto p-8">
|
||||
<Wizard
|
||||
initialValues={{username: '', email: '', password: '', passwordRepeat: ''}}
|
||||
onSubmit={
|
||||
async (values: any) => {
|
||||
await SetupEndpoint.registerSuperAdmin({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
email: values.email
|
||||
});
|
||||
toast.success("Setup finished", {description: "Have fun with Gameyfin!"});
|
||||
navigate('/login');
|
||||
}
|
||||
}
|
||||
>
|
||||
<WizardStep icon={<HandWaving/>}>
|
||||
<WelcomeStep/>
|
||||
</WizardStep>
|
||||
<WizardStep icon={<Palette/>}>
|
||||
<ThemeStep/>
|
||||
</WizardStep>
|
||||
<WizardStep
|
||||
validationSchema={Yup.object({
|
||||
username: Yup.string()
|
||||
.required('Required'),
|
||||
password: Yup.string()
|
||||
.min(8, 'Password must be at least 8 characters long')
|
||||
.required('Required'),
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required('Required'),
|
||||
passwordRepeat: Yup.string()
|
||||
.equals([Yup.ref('password')], 'Passwords do not match')
|
||||
.required('Required')
|
||||
})}
|
||||
icon={<User/>}
|
||||
>
|
||||
<UserStep/>
|
||||
</WizardStep>
|
||||
<WizardStep icon={<GearFine/>}>
|
||||
<SettingsStep/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SetupView;
|
||||
@@ -0,0 +1,44 @@
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button} from "@nextui-org/react";
|
||||
import {toast} from "sonner";
|
||||
import {SystemEndpoint} from "Frontend/generated/endpoints.js";
|
||||
|
||||
export default function TestView() {
|
||||
return (
|
||||
<div className="grow justify-center mt-12">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<Link to="/setup">Setup</Link>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Button onPress={
|
||||
() => toast("Normal", {
|
||||
description: "Description",
|
||||
action: {
|
||||
label: "OK",
|
||||
onClick: () => {
|
||||
},
|
||||
}
|
||||
})}>Toast (Normal)</Button>
|
||||
<Button onPress={
|
||||
() => toast.success("Success", {
|
||||
description: "Description",
|
||||
action: {
|
||||
label: "OK",
|
||||
onClick: () => {
|
||||
},
|
||||
}
|
||||
})}>Toast (Success)</Button>
|
||||
<Button onPress={
|
||||
() => toast.error("Error", {
|
||||
description: "Description",
|
||||
action: {
|
||||
label: "OK",
|
||||
onClick: () => {
|
||||
},
|
||||
}
|
||||
})}>Toast (Error)</Button>
|
||||
</div>
|
||||
<Button onPress={() => SystemEndpoint.restart()}>Restart</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import de.grimsi.gameyfin.users.UserService
|
||||
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||
import de.grimsi.gameyfin.users.dto.UserRegistration
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
import dev.hilla.Endpoint
|
||||
import dev.hilla.exception.EndpointException
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import com.vaadin.hilla.exception.EndpointException
|
||||
|
||||
@Endpoint
|
||||
class SetupEndpoint(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.grimsi.gameyfin.system
|
||||
|
||||
import de.grimsi.gameyfin.config.Roles
|
||||
import dev.hilla.Endpoint
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@Endpoint
|
||||
|
||||
@@ -4,7 +4,7 @@ import de.grimsi.gameyfin.config.Roles
|
||||
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||
import de.grimsi.gameyfin.users.dto.UserRegistration
|
||||
import de.grimsi.gameyfin.users.entities.User
|
||||
import dev.hilla.Endpoint
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
|
||||
Reference in New Issue
Block a user