mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Implement custom error page (#647)
This commit is contained in:
@@ -1,32 +1,82 @@
|
|||||||
import {
|
import {
|
||||||
Alien,
|
Alien,
|
||||||
|
Baseball,
|
||||||
|
Basketball,
|
||||||
CastleTurret,
|
CastleTurret,
|
||||||
|
DiceFive,
|
||||||
GameController,
|
GameController,
|
||||||
Ghost,
|
Ghost,
|
||||||
|
IconContext,
|
||||||
Joystick,
|
Joystick,
|
||||||
Lego,
|
Lego,
|
||||||
|
Medal,
|
||||||
|
PuzzlePiece,
|
||||||
|
Rocket,
|
||||||
Skull,
|
Skull,
|
||||||
SoccerBall,
|
SoccerBall,
|
||||||
|
Star,
|
||||||
Strategy,
|
Strategy,
|
||||||
Sword,
|
Sword,
|
||||||
|
Target,
|
||||||
|
ThumbsUp,
|
||||||
TreasureChest,
|
TreasureChest,
|
||||||
Trophy
|
Trophy,
|
||||||
|
User,
|
||||||
|
Volleyball
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import React from "react";
|
import React, {useEffect} from "react";
|
||||||
|
|
||||||
export default function IconBackgroundPattern() {
|
export default function IconBackgroundPattern() {
|
||||||
return <div className="absolute w-full h-full opacity-50">
|
|
||||||
<GameController size={32} className="absolute fill-primary top-[10%] left-[10%] rotate-[350deg]"/>
|
const minW = 250;
|
||||||
<SoccerBall size={34} className="absolute fill-primary top-[50%] left-[35%] rotate-[60deg]"/>
|
const maxW = 1920;
|
||||||
<Joystick size={40} className="absolute top-[30%] left-[50%] rotate-[90deg]"/>
|
const minS = 16;
|
||||||
<Strategy size={36} className="absolute fill-primary top-[50%] left-[70%] rotate-[30deg]"/>
|
const maxS = 40;
|
||||||
<Sword size={28} className="absolute top-[70%] left-[10%] rotate-[60deg]"/>
|
|
||||||
<Alien size={34} className="absolute fill-primary top-[10%] left-[85%] rotate-[15deg]"/>
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
<CastleTurret size={30} className="absolute top-[5%] left-[40%] rotate-[320deg]"/>
|
const [iconSize, setIconSize] = React.useState(minS);
|
||||||
<Ghost size={38} className="absolute fill-primary top-[40%] left-[5%] rotate-[300deg]"/>
|
|
||||||
<Skull size={32} className="absolute top-[80%] left-[30%] rotate-[90deg]"/>
|
useEffect(() => {
|
||||||
<Trophy size={36} className="absolute fill-primary top-[10%] left-[60%] rotate-[45deg]"/>
|
const updateSize = () => {
|
||||||
<Lego size={28} className="absolute top-[30%] left-[20%] rotate-[30deg]"/>
|
if (containerRef.current) {
|
||||||
<TreasureChest size={40} className="absolute top-[70%] left-[50%] rotate-[75deg]"/>
|
setIconSize(getResponsiveSize(containerRef.current.offsetWidth));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updateSize();
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
|
return () => window.removeEventListener('resize', updateSize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getResponsiveSize = (width: number) => {
|
||||||
|
const w = Math.max(minW, Math.min(width, maxW));
|
||||||
|
return minS + ((w - minW) / (maxW - minW)) * (maxS - minS);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div ref={containerRef} className="absolute w-full h-full opacity-50">
|
||||||
|
<IconContext.Provider value={{size: iconSize}}>
|
||||||
|
<GameController className="absolute fill-primary top-[8%] left-[8%] rotate-[350deg]"/>
|
||||||
|
<SoccerBall className="absolute fill-primary top-[48%] left-[96%] rotate-[60deg]"/>
|
||||||
|
<Joystick className="absolute top-[28%] left-[52%] rotate-[90deg]"/>
|
||||||
|
<Strategy className="absolute fill-primary top-[52%] left-[68%] rotate-[30deg]"/>
|
||||||
|
<Sword className="absolute top-[72%] left-[12%] rotate-[60deg]"/>
|
||||||
|
<Alien className="absolute fill-primary top-[12%] left-[88%] rotate-[15deg]"/>
|
||||||
|
<CastleTurret className="absolute top-[6%] left-[38%] rotate-[320deg]"/>
|
||||||
|
<Ghost className="absolute fill-primary top-[38%] left-[6%] rotate-[300deg]"/>
|
||||||
|
<Skull className="absolute top-[82%] left-[28%] rotate-[90deg]"/>
|
||||||
|
<Trophy className="absolute fill-primary top-[12%] left-[62%] rotate-[45deg]"/>
|
||||||
|
<Lego className="absolute top-[32%] left-[18%] rotate-[30deg]"/>
|
||||||
|
<TreasureChest className="absolute top-[68%] left-[48%] rotate-[75deg]"/>
|
||||||
|
<Basketball className="absolute fill-primary top-[22%] left-[37%] rotate-[10deg]"/>
|
||||||
|
<Baseball className="absolute top-[92%] left-[82%] rotate-[340deg]"/>
|
||||||
|
<DiceFive className="absolute top-[62%] left-[22%] rotate-[120deg]"/>
|
||||||
|
<Medal className="absolute fill-primary top-[18%] left-[28%] rotate-[300deg]"/>
|
||||||
|
<PuzzlePiece className="absolute top-[42%] left-[78%] rotate-[45deg]"/>
|
||||||
|
<Rocket className="absolute fill-primary top-[88%] left-[52%] rotate-[15deg]"/>
|
||||||
|
<Star className="absolute top-[28%] left-[72%] rotate-[60deg]"/>
|
||||||
|
<Target className="absolute fill-primary top-[68%] left-[62%] rotate-[330deg]"/>
|
||||||
|
<ThumbsUp className="absolute top-[82%] left-[12%] rotate-[80deg]"/>
|
||||||
|
<User className="absolute fill-primary top-[38%] left-[62%] rotate-[20deg]"/>
|
||||||
|
<Volleyball className="absolute top-[78%] left-[92%] rotate-[100deg]"/>
|
||||||
|
</IconContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@ import SearchView from "Frontend/views/SearchView";
|
|||||||
import RecentlyAddedView from "Frontend/views/RecentlyAddedView";
|
import RecentlyAddedView from "Frontend/views/RecentlyAddedView";
|
||||||
import LibraryView from "Frontend/views/LibraryView";
|
import LibraryView from "Frontend/views/LibraryView";
|
||||||
import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js";
|
import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js";
|
||||||
|
import ErrorView from "Frontend/views/ErrorView";
|
||||||
|
|
||||||
export const {router, routes} = new RouterConfigurationBuilder()
|
export const {router, routes} = new RouterConfigurationBuilder()
|
||||||
.withReactRoutes([
|
.withReactRoutes([
|
||||||
@@ -145,6 +146,11 @@ export const {router, routes} = new RouterConfigurationBuilder()
|
|||||||
element: <EmailConfirmationView/>,
|
element: <EmailConfirmationView/>,
|
||||||
handle: {title: 'Confirm Email'}
|
handle: {title: 'Confirm Email'}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <ErrorView/>,
|
||||||
|
handle: {title: 'Error'}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import {Button} from "@heroui/react";
|
||||||
|
import {useNavigate} from "react-router";
|
||||||
|
import {
|
||||||
|
Alien,
|
||||||
|
Compass,
|
||||||
|
Cube,
|
||||||
|
DiceFive,
|
||||||
|
FlagCheckered,
|
||||||
|
GameController,
|
||||||
|
Ghost,
|
||||||
|
Icon,
|
||||||
|
IconContext,
|
||||||
|
Joystick,
|
||||||
|
MagicWand,
|
||||||
|
PuzzlePiece,
|
||||||
|
RocketLaunch,
|
||||||
|
Skull,
|
||||||
|
SmileyXEyes,
|
||||||
|
Sword
|
||||||
|
} from "@phosphor-icons/react";
|
||||||
|
import React, {ReactElement, useState} from "react";
|
||||||
|
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
|
||||||
|
import IconBackgroundPattern from "Frontend/components/general/IconBackgroundPattern";
|
||||||
|
|
||||||
|
type ErrorText = {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
buttonText: string;
|
||||||
|
icon: ReactElement<Icon>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ErrorView() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const errorTexts: ErrorText[] = [
|
||||||
|
{
|
||||||
|
"title": "404 – Level Not Found!",
|
||||||
|
"subtitle": "You’ve wandered off the map. This level doesn’t exist—or maybe it’s still in development.",
|
||||||
|
"buttonText": "Go back to the main menu",
|
||||||
|
"icon": <Joystick/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – Quest Failed",
|
||||||
|
"subtitle": "The path you seek does not exist. Maybe it was just a side quest after all.",
|
||||||
|
"buttonText": "Return to the guild hall",
|
||||||
|
"icon": <Compass/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – You’ve encountered a glitch in the system!",
|
||||||
|
"subtitle": "The page you’re looking for couldn’t load. Don’t worry, no coins were lost.",
|
||||||
|
"buttonText": "Retry mission",
|
||||||
|
"icon": <Alien/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – Game Cartridge Not Inserted",
|
||||||
|
"subtitle": "This page failed to load. Did you blow on the cartridge and try again?",
|
||||||
|
"buttonText": "Reset the console",
|
||||||
|
"icon": <DiceFive/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – You are in the wrong zone…",
|
||||||
|
"subtitle": "This area is off-limits… or was never meant to be explored. Tread carefully.",
|
||||||
|
"buttonText": "Find a safe path",
|
||||||
|
"icon": <SmileyXEyes/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – You Missed the Jump!",
|
||||||
|
"subtitle": "The platform you were trying to reach isn’t here. Maybe it was a hidden level?",
|
||||||
|
"buttonText": "Respawn at Start",
|
||||||
|
"icon": <GameController/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – Signal Lost in Deep Space",
|
||||||
|
"subtitle": "We've lost contact with this page. All we have is static and void.",
|
||||||
|
"buttonText": "Return to Command Center",
|
||||||
|
"icon": <RocketLaunch/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – The Page Has Vanished in a Puff of Smoke",
|
||||||
|
"subtitle": "A forbidden spell may have erased the page from existence. Try another path.",
|
||||||
|
"buttonText": "Return to the Grimoire",
|
||||||
|
"icon": <MagicWand/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – Block Not Found",
|
||||||
|
"subtitle": "The page you're looking for hasn't been crafted yet. Gather more resources and try again.",
|
||||||
|
"buttonText": "Back to Base",
|
||||||
|
"icon": <Cube/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – Puzzle Piece Missing",
|
||||||
|
"subtitle": "This page doesn’t quite fit. Try rotating it… or just go back.",
|
||||||
|
"buttonText": "Solve a different puzzle",
|
||||||
|
"icon": <PuzzlePiece/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – You Took a Wrong Turn!",
|
||||||
|
"subtitle": "You drifted off course and into the digital void.",
|
||||||
|
"buttonText": "Return to the Starting Line",
|
||||||
|
"icon": <FlagCheckered/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – This Page Didn’t Survive",
|
||||||
|
"subtitle": "Only ruins remain. Whatever was here is long gone.",
|
||||||
|
"buttonText": "Search for safe house",
|
||||||
|
"icon": <Skull/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – Instance Not Found",
|
||||||
|
"subtitle": "This dungeon has been removed or doesn’t exist on this realm.",
|
||||||
|
"buttonText": "Return to your stronghold",
|
||||||
|
"icon": <Sword/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "404 – The Page Was… Never Really Here…",
|
||||||
|
"subtitle": "You were warned not to look. But you clicked anyway.",
|
||||||
|
"buttonText": "Turn Back Now",
|
||||||
|
"icon": <Ghost/>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const [errorText] = useState<ErrorText>(errorTexts[Math.floor(Math.random() * errorTexts.length)]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 items-center justify-center h-full">
|
||||||
|
<IconBackgroundPattern/>
|
||||||
|
<GameyfinLogo className="h-10 fill-foreground mb-4"/>
|
||||||
|
<h1 className="text-4xl font-bold">{errorText.title}</h1>
|
||||||
|
<p className="text-lg">{errorText.subtitle}</p>
|
||||||
|
<IconContext.Provider value={{size: 20, weight: "fill"}}>
|
||||||
|
<Button startContent={errorText.icon}
|
||||||
|
color="primary"
|
||||||
|
size="lg"
|
||||||
|
className="mt-4"
|
||||||
|
onPress={() => navigate('/', {replace: true})}>
|
||||||
|
{errorText.buttonText}
|
||||||
|
</Button>
|
||||||
|
</IconContext.Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user