diff --git a/src/handbook/package.json b/src/handbook/package.json index 97869f8e7..dd44a0801 100644 --- a/src/handbook/package.json +++ b/src/handbook/package.json @@ -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", diff --git a/src/handbook/src/backend/events.ts b/src/handbook/src/backend/events.ts new file mode 100644 index 000000000..353424d93 --- /dev/null +++ b/src/handbook/src/backend/events.ts @@ -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; diff --git a/src/handbook/src/backend/types.ts b/src/handbook/src/backend/types.ts new file mode 100644 index 000000000..99a89e967 --- /dev/null +++ b/src/handbook/src/backend/types.ts @@ -0,0 +1 @@ +export type Page = "Home"; diff --git a/src/handbook/src/css/pages/HomePage.scss b/src/handbook/src/css/pages/HomePage.scss new file mode 100644 index 000000000..0edc9ce17 --- /dev/null +++ b/src/handbook/src/css/pages/HomePage.scss @@ -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; + } +} diff --git a/src/handbook/src/css/views/Content.scss b/src/handbook/src/css/views/Content.scss index dcc43a6c4..2a2abba8b 100644 --- a/src/handbook/src/css/views/Content.scss +++ b/src/handbook/src/css/views/Content.scss @@ -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; - } } diff --git a/src/handbook/src/main.tsx b/src/handbook/src/main.tsx index 0cb2b7b3d..980517cd2 100644 --- a/src/handbook/src/main.tsx +++ b/src/handbook/src/main.tsx @@ -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( diff --git a/src/handbook/src/ui/pages/HomePage.tsx b/src/handbook/src/ui/pages/HomePage.tsx new file mode 100644 index 000000000..d5bab462a --- /dev/null +++ b/src/handbook/src/ui/pages/HomePage.tsx @@ -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 { + constructor(props: any) { + super(props); + } + + render() { + return ( +
+
+

Welcome back, Traveler~

+ +
+ + + + + +
+ +
+ + +
+
+ +
+
+
+

This tool is not affiliated with HoYoverse.

+

Genshin Impact, game HomePage and materials are

+

trademarks and copyrights of HoYoverse.

+
+ +
+ +

Join the Community!

+
+
+ +
+
+

Credits

+

(hover to see info)

+
+ +
+ paimon.moe + Anime Game Data + Genshin Impact Wiki +
+
+
+
+ ); + } +} + +export default HomePage; diff --git a/src/handbook/src/ui/views/Content.tsx b/src/handbook/src/ui/views/Content.tsx index d5c1c148a..b15c0ac64 100644 --- a/src/handbook/src/ui/views/Content.tsx +++ b/src/handbook/src/ui/views/Content.tsx @@ -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 { - constructor(props: any) { +interface IProps { + initial?: Page; +} + +interface IState { + current: Page; +} + +class Content extends React.Component { + 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 ( -
-
-

Welcome back, Traveler~

- -
- - - - - -
- -
- - -
-
- -
-
-
-

This tool is not affiliated with HoYoverse.

-

Genshin Impact, game content and materials are

-

trademarks and copyrights of HoYoverse.

-
- -
- -

Join the Community!

-
-
- -
-
-

Credits

-

(hover to see info)

-
- - -
-
-
- ); + switch (this.state.current) { + default: return undefined; + case "Home": return ; + } } } diff --git a/src/handbook/tsconfig.json b/src/handbook/tsconfig.json index 644d7615b..afb01a26c 100644 --- a/src/handbook/tsconfig.json +++ b/src/handbook/tsconfig.json @@ -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"],