mirror of
https://github.com/Melledy/Grasscutter.git
synced 2024-11-23 11:26:07 +00:00
Implement navigation and the page system
This commit is contained in:
parent
30c8d01c9e
commit
e0b1f275dd
@ -13,12 +13,15 @@
|
||||
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
"events": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.3",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/events": "^3.0.0",
|
||||
|
||||
"vite": "^4.2.0",
|
||||
"vite-plugin-svgr": "^2.4.0",
|
||||
|
92
src/handbook/src/backend/events.ts
Normal file
92
src/handbook/src/backend/events.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { Page } from "@backend/types";
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
const navigation = new EventEmitter();
|
||||
|
||||
let navStack: Page[] = [];
|
||||
let currentPage: number | null = -1;
|
||||
|
||||
/**
|
||||
* The initial setup function for this file.
|
||||
*/
|
||||
export function setup(): void {
|
||||
// Check if the window's href is a page.
|
||||
const page = window.location.href.split("/").pop();
|
||||
if (page == undefined) return;
|
||||
|
||||
// Convert the page to a Page type.
|
||||
const pageName = page.charAt(0).toUpperCase() + page.slice(1);
|
||||
const pageType = pageName as Page;
|
||||
|
||||
// Navigate to the page.
|
||||
navigate(pageType, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a navigation listener.
|
||||
*
|
||||
* @param listener The listener to add.
|
||||
*/
|
||||
export function addNavListener(listener: (page: Page) => void) {
|
||||
navigation.on("navigate", listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a navigation listener.
|
||||
*
|
||||
* @param listener The listener to remove.
|
||||
*/
|
||||
export function removeNavListener(listener: (page: Page) => void) {
|
||||
navigation.off("navigate", listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to a page.
|
||||
* Returns the last page.
|
||||
*
|
||||
* @param page The page to navigate to.
|
||||
* @param update Whether to update the state or not.
|
||||
*/
|
||||
export function navigate(page: Page, update: boolean = true): Page | null {
|
||||
// Navigate to the new page.
|
||||
const lastPage = currentPage;
|
||||
navigation.emit("navigate", page);
|
||||
|
||||
if (update) {
|
||||
// Set the current page.
|
||||
navStack.push(page);
|
||||
currentPage = navStack.length - 1;
|
||||
// Add the page to the window history.
|
||||
window.history.pushState(page, page, "/" + page.toLowerCase());
|
||||
}
|
||||
|
||||
return lastPage ? navStack[lastPage] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back or forward in the navigation stack.
|
||||
*
|
||||
* @param forward Whether to go forward or not.
|
||||
*/
|
||||
export function go(forward: boolean): void {
|
||||
if (currentPage == undefined) return;
|
||||
|
||||
// Get the new page.
|
||||
const newPage = forward ? currentPage + 1 : currentPage - 1;
|
||||
if (newPage < 0 || newPage >= navStack.length) return;
|
||||
|
||||
// Navigate to the new page.
|
||||
currentPage = newPage;
|
||||
navigation.emit("navigate", navStack[newPage]);
|
||||
|
||||
// Update the window history.
|
||||
window.history.pushState(navStack[newPage], navStack[newPage], "/" + navStack[newPage].toLowerCase());
|
||||
}
|
||||
|
||||
// This is the global event system.
|
||||
export default emitter;
|
||||
// @ts-ignore
|
||||
window["emitter"] = emitter;
|
||||
// @ts-ignore
|
||||
window["navigate"] = navigate;
|
1
src/handbook/src/backend/types.ts
Normal file
1
src/handbook/src/backend/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type Page = "Home";
|
147
src/handbook/src/css/pages/HomePage.scss
Normal file
147
src/handbook/src/css/pages/HomePage.scss
Normal file
@ -0,0 +1,147 @@
|
||||
.HomePage {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
background-color: var(--background-color);
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.HomePage_Top {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.HomePage_Title {
|
||||
margin-top: 31px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.HomePage_Buttons {
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
|
||||
max-width: 1376px;
|
||||
max-height: 256px;
|
||||
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.HomePage_Bottom {
|
||||
display: flex;
|
||||
|
||||
height: 50%;
|
||||
max-height: 125px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.HomePage_Box {
|
||||
display: flex;
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.HomePage_Disclaimer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: var(--secondary-color);
|
||||
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
max-width: 630px;
|
||||
max-height: 93px;
|
||||
|
||||
margin: 0 0 0 60px;
|
||||
border-radius: 10px;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 11px;
|
||||
|
||||
:nth-child(1) {
|
||||
font-size: 24px;
|
||||
max-height: 30px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
max-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.HomePage_Discord {
|
||||
max-height: 40px;
|
||||
max-width: 150px;
|
||||
|
||||
gap: 8px;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 44px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.HomePage_Text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--secondary-color);
|
||||
|
||||
max-width: 300px;
|
||||
max-height: 80px;
|
||||
|
||||
margin: 13px 60px 0 0;
|
||||
border-radius: 10px;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 11px;
|
||||
}
|
||||
|
||||
.HomePage_Credits {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
|
||||
max-height: 18px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
:nth-child(1) {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
font-size: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.HomePage_Links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
color: var(--text-primary-color);
|
||||
text-decoration: none;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
@ -1,147 +1,4 @@
|
||||
.Content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
background-color: var(--background-color);
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.Content_Top {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.Content_Title {
|
||||
margin-top: 31px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.Content_Buttons {
|
||||
width: 100%;
|
||||
height: 40%;
|
||||
|
||||
max-width: 1376px;
|
||||
max-height: 256px;
|
||||
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.Content_Bottom {
|
||||
display: flex;
|
||||
|
||||
height: 50%;
|
||||
max-height: 125px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.Content_Box {
|
||||
display: flex;
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.Content_Disclaimer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: var(--secondary-color);
|
||||
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
max-width: 630px;
|
||||
max-height: 93px;
|
||||
|
||||
margin: 0 0 0 60px;
|
||||
border-radius: 10px;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 11px;
|
||||
|
||||
:nth-child(1) {
|
||||
font-size: 24px;
|
||||
max-height: 30px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
max-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.Content_Discord {
|
||||
max-height: 40px;
|
||||
max-width: 150px;
|
||||
|
||||
gap: 8px;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 44px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.Content_Text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--secondary-color);
|
||||
|
||||
max-width: 300px;
|
||||
max-height: 80px;
|
||||
|
||||
margin: 13px 60px 0 0;
|
||||
border-radius: 10px;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 11px;
|
||||
}
|
||||
|
||||
.Content_Credits {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
|
||||
max-height: 18px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
:nth-child(1) {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:nth-child(2) {
|
||||
font-size: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.Content_Links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
color: var(--text-primary-color);
|
||||
text-decoration: none;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import * as events from "@backend/events";
|
||||
|
||||
import App from "@components/App";
|
||||
|
||||
// Call initial setup functions.
|
||||
events.setup();
|
||||
|
||||
// Render the application.
|
||||
createRoot(document.getElementById(
|
||||
"root") as HTMLElement).render(
|
||||
|
66
src/handbook/src/ui/pages/HomePage.tsx
Normal file
66
src/handbook/src/ui/pages/HomePage.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
|
||||
import HomeButton from "@app/ui/widgets/HomeButton";
|
||||
|
||||
import { ReactComponent as DiscordLogo } from "@icons/discord.svg";
|
||||
|
||||
import "@css/pages/HomePage.scss";
|
||||
|
||||
class HomePage extends React.Component<any, any> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={"HomePage"}>
|
||||
<div className={"HomePage_Top"}>
|
||||
<h1 className={"HomePage_Title"}>Welcome back, Traveler~</h1>
|
||||
|
||||
<div className={"HomePage_Buttons"}>
|
||||
<HomeButton name={"Commands"} anchor={"commands"} />
|
||||
<HomeButton name={"Characters"} anchor={"avatars"} />
|
||||
<HomeButton name={"Items"} anchor={"items"} />
|
||||
<HomeButton name={"Entities"} anchor={"monsters"} />
|
||||
<HomeButton name={"Scenes"} anchor={"scenes"} />
|
||||
</div>
|
||||
|
||||
<div className={"HomePage_Buttons"}>
|
||||
<HomeButton name={"Quests"} anchor={"quests"} />
|
||||
<HomeButton name={"Achievements"} anchor={"achievements"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"HomePage_Bottom"}>
|
||||
<div className={"HomePage_Box HomePage_Disclaimer"}>
|
||||
<div>
|
||||
<p>This tool is not affiliated with HoYoverse.</p>
|
||||
<p>Genshin Impact, game HomePage and materials are</p>
|
||||
<p>trademarks and copyrights of HoYoverse.</p>
|
||||
</div>
|
||||
|
||||
<div className={"HomePage_Discord"}>
|
||||
<DiscordLogo />
|
||||
<p>Join the Community!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"HomePage_Text"}>
|
||||
<div className={"HomePage_Credits"}>
|
||||
<p>Credits</p>
|
||||
<p>(hover to see info)</p>
|
||||
</div>
|
||||
|
||||
<div className={"HomePage_Links"}>
|
||||
<a href={"https://paimon.moe"}>paimon.moe</a>
|
||||
<a href={"https://gitlab.com/Dimbreath/AnimeGameData"}>Anime Game Data</a>
|
||||
<a href={"https://genshin-impact.fandom.com"}>Genshin Impact Wiki</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HomePage;
|
@ -1,65 +1,52 @@
|
||||
import React from "react";
|
||||
|
||||
import HomeButton from "@app/ui/widgets/HomeButton";
|
||||
import HomePage from "@pages/HomePage";
|
||||
|
||||
import { ReactComponent as DiscordLogo } from "@icons/discord.svg";
|
||||
import type { Page } from "@backend/types";
|
||||
|
||||
import "@css/views/Content.scss";
|
||||
import { addNavListener, removeNavListener } from "@backend/events";
|
||||
|
||||
class Content extends React.Component<any, any> {
|
||||
constructor(props: any) {
|
||||
interface IProps {
|
||||
initial?: Page;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
current: Page;
|
||||
}
|
||||
|
||||
class Content extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
current: props.initial ?? "Home"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the specified page.
|
||||
*
|
||||
* @param page The page to navigate to.
|
||||
* @private
|
||||
*/
|
||||
private navigate(page: Page): void {
|
||||
this.setState({ current: page });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
addNavListener(this.navigate.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
removeNavListener(this.navigate.bind(this));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={"Content"}>
|
||||
<div className={"Content_Top"}>
|
||||
<h1 className={"Content_Title"}>Welcome back, Traveler~</h1>
|
||||
|
||||
<div className={"Content_Buttons"}>
|
||||
<HomeButton name={"Commands"} anchor={"commands"} />
|
||||
<HomeButton name={"Characters"} anchor={"avatars"} />
|
||||
<HomeButton name={"Items"} anchor={"items"} />
|
||||
<HomeButton name={"Entities"} anchor={"monsters"} />
|
||||
<HomeButton name={"Scenes"} anchor={"scenes"} />
|
||||
</div>
|
||||
|
||||
<div className={"Content_Buttons"}>
|
||||
<HomeButton name={"Quests"} anchor={"quests"} />
|
||||
<HomeButton name={"Achievements"} anchor={"achievements"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"Content_Bottom"}>
|
||||
<div className={"Content_Box Content_Disclaimer"}>
|
||||
<div>
|
||||
<p>This tool is not affiliated with HoYoverse.</p>
|
||||
<p>Genshin Impact, game content and materials are</p>
|
||||
<p>trademarks and copyrights of HoYoverse.</p>
|
||||
</div>
|
||||
|
||||
<div className={"Content_Discord"}>
|
||||
<DiscordLogo />
|
||||
<p>Join the Community!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"Content_Text"}>
|
||||
<div className={"Content_Credits"}>
|
||||
<p>Credits</p>
|
||||
<p>(hover to see info)</p>
|
||||
</div>
|
||||
|
||||
<div className={"Content_Links"}>
|
||||
<a href={"https://paimon.moe"}>paimon.moe</a>
|
||||
<a href={"https://gitlab.com/Dimbreath/AnimeGameData"}>Anime Game Data</a>
|
||||
<a href={"https://genshin-impact.fandom.com"}>Genshin Impact Wiki</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
switch (this.state.current) {
|
||||
default: return undefined;
|
||||
case "Home": return <HomePage />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,8 @@
|
||||
"@css/*": ["src/css/*"],
|
||||
"@components/*": ["src/ui/*"],
|
||||
"@icons/*": ["src/icons/*"],
|
||||
"@views/*": ["src/ui/views/*"]
|
||||
"@views/*": ["src/ui/views/*"],
|
||||
"@pages/*": ["src/ui/pages/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
|
Loading…
Reference in New Issue
Block a user