From e98bf8d7eaa99d8291eff2f9b9acbc6ec8693557 Mon Sep 17 00:00:00 2001 From: Haoyu Xu Date: Fri, 24 Feb 2023 19:27:24 -0500 Subject: [PATCH] feat(directory): add layout scaffold --- directory/src/App.css | 20 ++- directory/src/App.jsx | 10 +- directory/src/component/dropdown.css | 98 +++++++++++++ directory/src/component/dropdown.jsx | 47 ++++++ directory/src/component/popup.css | 68 +++++++++ directory/src/component/popup.jsx | 38 +++++ directory/src/component/return_button.css | 50 +++++++ directory/src/component/return_button.jsx | 33 +++++ directory/src/context/useLanguageContext.jsx | 27 ++++ directory/src/context/useTitleContext.jsx | 25 ++++ directory/src/i18n.json | 39 +++++ directory/src/routes/index.jsx | 25 +++- directory/src/routes/path/home.jsx | 12 +- directory/src/routes/path/news.css | 0 directory/src/routes/path/news.jsx | 27 ++++ directory/src/routes/path/operator.jsx | 10 +- directory/src/routes/root.css | 146 +++++++++++++++++++ directory/src/routes/root.jsx | 123 +++++++++++++--- 18 files changed, 765 insertions(+), 33 deletions(-) create mode 100644 directory/src/component/dropdown.css create mode 100644 directory/src/component/dropdown.jsx create mode 100644 directory/src/component/popup.css create mode 100644 directory/src/component/popup.jsx create mode 100644 directory/src/component/return_button.css create mode 100644 directory/src/component/return_button.jsx create mode 100644 directory/src/context/useLanguageContext.jsx create mode 100644 directory/src/context/useTitleContext.jsx create mode 100644 directory/src/i18n.json create mode 100644 directory/src/routes/path/news.css create mode 100644 directory/src/routes/path/news.jsx diff --git a/directory/src/App.css b/directory/src/App.css index 6291c1f..c8237e0 100644 --- a/directory/src/App.css +++ b/directory/src/App.css @@ -1,17 +1,24 @@ -@import url("https://fonts.googleapis.com/css2?family=Josefin+Sans&family=Noto+Sans+SC&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@400;500&family=Noto+Sans+SC&display=swap'); :root { + --text-color: rgba(255, 255, 255, 0.87); + --border-color: #707070; + --link-highlight-color: #33b5e5; + --drawer-background-color: rgba(0, 0, 0, 0.5); + --root-background-color: #131313; + font-family: "Josefin Sans", "Noto Sans SC", sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; - color: rgba(255, 255, 255, 0.87); - background-color: #131313; + color: var(--text-color); + background-color: var(--root-background-color); min-height: 100vh; font-synthesis: none; text-rendering: optimizeLegibility; + } #root { @@ -20,6 +27,11 @@ flex-wrap: nowrap; align-content: flex-start; justify-content: flex-start; - align-items: flex-start; + align-items: stretch; min-height: 100vh; } + +a { + color: var(--text-color); + text-decoration: none; +} \ No newline at end of file diff --git a/directory/src/App.jsx b/directory/src/App.jsx index 1a8eaf9..60c986a 100644 --- a/directory/src/App.jsx +++ b/directory/src/App.jsx @@ -11,8 +11,8 @@ import ErrorPage from "@/routes/error-page"; import routes from "@/routes"; import '@/App.css'; import 'reset-css'; - -document.title = import.meta.env.VITE_APP_TITLE; +import { TitleProvider } from '@/context/useTitleContext'; +import { LanguageProvider } from '@/context/useLanguageContext'; const router = createBrowserRouter( createRoutesFromElements( @@ -40,6 +40,10 @@ const router = createBrowserRouter( ReactDOM.createRoot(document.getElementById('root')).render( - + + + + + ) \ No newline at end of file diff --git a/directory/src/component/dropdown.css b/directory/src/component/dropdown.css new file mode 100644 index 0000000..17274db --- /dev/null +++ b/directory/src/component/dropdown.css @@ -0,0 +1,98 @@ +.dropdown { + position: relative; + display: inline-block; + user-select: none; + z-index: 2; + padding: 0.5rem; +} + +.dropdown .text { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; +} + +.dropdown .content { + padding-right: 0.5rem; +} + +.dropdown .icon { + width: 0.5em; + height: 0.5em; + display: inline-block; + vertical-align: middle; + border-left: 0.15em solid var(--text-color); + border-bottom: 0.15em solid var(--text-color); + border-right: 0.15em solid var(--text-color); + border-top: 0.15em solid var(--text-color); + transform: translate(0, -0.15em) rotate(-45deg); + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; +} + +.dropdown .icon.active { + animation: icon-flash 2s cubic-bezier(0.65, 0.05, 0.36, 1) infinite; +} + +.dropdown .menu { + opacity: 0; + position: absolute; + background-color: var(--background-color); + color: var(--text-color); + width: max-content; + z-index: -1; + top: 2rem; + right: 0; + gap: 0.5rem; + display: flex; + align-items: center; + flex-direction: column; + flex-wrap: nowrap; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + overflow: hidden; + padding: 0.5rem; + border: 1px solid var(--border-color); + visibility: hidden; +} + +.dropdown .menu.active { + visibility: visible; + opacity: 1; + z-index: 2; +} + +.dropdown .menu .item { + cursor: pointer; + padding: 0.5rem; + font-size: 1rem; + width: max-content; + height: max-content; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + flex-wrap: nowrap; + transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; +} + +.dropdown .menu .item:hover, .dropdown .menu .item:focus, .dropdown .menu .item.active { + color: var(--link-highlight-color); +} + +.dropdown .overlay { + z-index: 1; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; +} + +@keyframes icon-flash { + 50% { + opacity: 0.2; + } +} \ No newline at end of file diff --git a/directory/src/component/dropdown.jsx b/directory/src/component/dropdown.jsx new file mode 100644 index 0000000..87a907e --- /dev/null +++ b/directory/src/component/dropdown.jsx @@ -0,0 +1,47 @@ +import { + useState, + useEffect +} from 'react' +import './dropdown.css' + +export default function Dropdown(props) { + const [hidden, setHidden] = useState(true) + + const toggleDropdown = () => { + setHidden(!hidden) + } + + return ( + <> +
+
toggleDropdown()} + > + {props.text} + +
+ + + + ) +} \ No newline at end of file diff --git a/directory/src/component/popup.css b/directory/src/component/popup.css new file mode 100644 index 0000000..32fa6c6 --- /dev/null +++ b/directory/src/component/popup.css @@ -0,0 +1,68 @@ +.popup-text { + cursor: pointer; +} + +.popup { + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + opacity: 0; + z-index: -1; + border: unset; + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 1.2rem; +} + +.popup .wrapper { + display: flex; + flex-direction: column; + align-items: stretch; + flex-wrap: nowrap; + width: 480px; + height: fit-content; + margin: 0 auto; + background-color: var(--root-background-color); + border: 1px solid var(--border-color); + padding: 2rem; +} + +.popup.active { + opacity: 1; + z-index: 1; +} + +.popup .title { + font-size: 3rem; + font-weight: 700; + margin-bottom: 1rem; + border-bottom: 1px solid var(--border-color); + display: flex; + flex-direction: row; + justify-content: space-between; + align-content: center; + align-items: center; +} + +.popup .overlay { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: -1; + opacity: 0; + background-color: var(--root-background-color); + transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; +} + +.popup .overlay.active { + opacity: 0.5; + visibility: visible; +} diff --git a/directory/src/component/popup.jsx b/directory/src/component/popup.jsx new file mode 100644 index 0000000..6de2148 --- /dev/null +++ b/directory/src/component/popup.jsx @@ -0,0 +1,38 @@ +import { + useState, + useEffect +} from 'react' +import './popup.css' +import ReturnButton from '@/component/return_button'; + +export default function Popup(props) { + const [hidden, setHidden] = useState(true) + + const toggle = () => { + setHidden(!hidden) + } + + return ( + <> +
+
+
+ {props.title} + +
+
+ {props.children} +
+
+
toggle()} /> +
+ + {props.title} + + + ) +} \ No newline at end of file diff --git a/directory/src/component/return_button.css b/directory/src/component/return_button.css new file mode 100644 index 0000000..daf20e6 --- /dev/null +++ b/directory/src/component/return_button.css @@ -0,0 +1,50 @@ +.return-button { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0.6rem 0; + width: 3rem; + cursor: pointer; +} + +.return-button .bar-wrapper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; +} + +.return-button .bar { + width: 1rem; + height: 0.4rem; + background-color: var(--text-color); + transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; +} + +.return-button .bar-arrow-left { + border-top: 0.24rem solid transparent; + border-bottom: 0.24rem solid transparent; + border-right: 0.3rem solid var(--text-color); +} + +.return-button .bar-arrow-right { + border-top: 0.24rem solid transparent; + border-bottom: 0.24rem solid transparent; + border-left: 0.3rem solid var(--text-color); +} + +.return-button .bar-wrapper:nth-child(1) { + transform: translateY(-0.1rem) rotate(45deg); +} + +.return-button .bar-wrapper:nth-child(2) { + transform: translateY(-0.1rem) translate(90%, -100%) rotate(-45deg); +} + +.return-button .bar-wrapper:nth-child(3) { + transform: translateY(-0.1rem) translateY(150%) rotate(315deg); +} + +.return-button .bar-wrapper:nth-child(4) { + transform: translateY(-0.1rem) translate(90%, 50%) rotate(225deg); +} diff --git a/directory/src/component/return_button.jsx b/directory/src/component/return_button.jsx new file mode 100644 index 0000000..ace8039 --- /dev/null +++ b/directory/src/component/return_button.jsx @@ -0,0 +1,33 @@ +import './return_button.css' + +export default function ReturnButton(props) { + + return ( + <> +
props.onClick()} + > +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + ) +} \ No newline at end of file diff --git a/directory/src/context/useLanguageContext.jsx b/directory/src/context/useLanguageContext.jsx new file mode 100644 index 0000000..758f503 --- /dev/null +++ b/directory/src/context/useLanguageContext.jsx @@ -0,0 +1,27 @@ +import { createContext, useState, useEffect } from "react" +import i18n from '@/i18n' + +export const LanguageContext = createContext() + +export function LanguageProvider(props) { + const drawerTextDefaultLang = "en-US" + const [language, setLanguage] = useState(i18n.available.includes(navigator.language) ? navigator.language : "en-US") // language will be default to en-US if not available + const [drawerAlternateLang, setDrawerAlternateLang] = useState(null) // drawerAlternateLang will be default to zh-CN if language is en-* + + useEffect(() => { + setDrawerAlternateLang(language.startsWith("en") ? "zh-CN" : language) + }, [language]) + + return ( + + {props.children} + + ) +} \ No newline at end of file diff --git a/directory/src/context/useTitleContext.jsx b/directory/src/context/useTitleContext.jsx new file mode 100644 index 0000000..c464d7d --- /dev/null +++ b/directory/src/context/useTitleContext.jsx @@ -0,0 +1,25 @@ +import { createContext, useState, useEffect, useContext } from "react" +import { LanguageContext } from '@/context/useLanguageContext'; + +export const TitleContext = createContext() + +export function TitleProvider(props) { + const { i18n, language } = useContext(LanguageContext) + const [fakeTitle, setTitle] = useState('dynamic_compile') + const [title, setActualTitle] = useState(null) + + useEffect(() => { + let newTitle = fakeTitle + if (i18n.key[fakeTitle]) { + newTitle = i18n.key[fakeTitle][language] + } + document.title = `${newTitle} - ${import.meta.env.VITE_APP_TITLE}`; + setActualTitle(newTitle) + }, [fakeTitle, language]) + + return ( + + {props.children} + + ) +} \ No newline at end of file diff --git a/directory/src/i18n.json b/directory/src/i18n.json new file mode 100644 index 0000000..ca88210 --- /dev/null +++ b/directory/src/i18n.json @@ -0,0 +1,39 @@ +{ + "available": [ + "zh-CN", "en-US" + ], + "key": { + "dynamic_compile": { + "zh-CN": "干员动态集录", + "en-US": "Dynamic Compile" + }, + "home": { + "zh-CN": "首页", + "en-US": "Home" + }, + "news": { + "zh-CN": "新闻", + "en-US": "News" + }, + "offical_page": { + "zh-CN": "官方页面", + "en-US": "Offical Page" + }, + "privacy_policy": { + "zh-CN": "隐私政策", + "en-US": "Privacy Policy" + }, + "contact_us": { + "zh-CN": "联系我们", + "en-US": "Contact Us" + }, + "zh-CN": { + "zh-CN": "简体中文", + "en-US": "Chinese (Simplified)" + }, + "en-US": { + "zh-CN": "英语", + "en-US": "English" + } + } +} \ No newline at end of file diff --git a/directory/src/routes/index.jsx b/directory/src/routes/index.jsx index 232b2e6..9b5bb9a 100644 --- a/directory/src/routes/index.jsx +++ b/directory/src/routes/index.jsx @@ -1,16 +1,31 @@ import Home from "@/routes/path/home"; import Operator from "@/routes/path/operator"; +import News from "@/routes/path/news"; export default [ { - path: "", + path: "/", index: true, - name: "Home", - element: + name: "home", + element: , + inDrawer: true }, { path: "operator/:key", index: false, - name: "Operator", - element: + name: "operator", + element: , + inDrawer: false + }, { + path: "news", + index: false, + name: "news", + element: , + inDrawer: true + }, { + path: "https://ak.hypergryph.com/archive/dynamicCompile/", + index: false, + name: "offical_page", + element: , + inDrawer: true } ] \ No newline at end of file diff --git a/directory/src/routes/path/home.jsx b/directory/src/routes/path/home.jsx index 211b8c9..e1bf073 100644 --- a/directory/src/routes/path/home.jsx +++ b/directory/src/routes/path/home.jsx @@ -1,20 +1,28 @@ import { useState, - useEffect + useEffect, + useContext } from 'react' import './home.css' +import { TitleContext } from '@/context/useTitleContext'; export default function Home(props) { + const { title, setTitle } = useContext(TitleContext) useEffect(() => { fetch("/_assets/directory.json").then(res => res.json()).then(data => { console.log(data) }) }, []) + useEffect(() => { + setTitle('dynamic_compile') + }, []) + return (
- 1233 + {title} +
22s diff --git a/directory/src/routes/path/news.css b/directory/src/routes/path/news.css new file mode 100644 index 0000000..e69de29 diff --git a/directory/src/routes/path/news.jsx b/directory/src/routes/path/news.jsx new file mode 100644 index 0000000..e5f138a --- /dev/null +++ b/directory/src/routes/path/news.jsx @@ -0,0 +1,27 @@ +import { + useState, + useEffect, + useContext +} from 'react' +import './news.css' +import { TitleContext } from '@/context/useTitleContext'; + +export default function News(props) { + const { title, setTitle } = useContext(TitleContext) + + useEffect(() => { + setTitle('news') + }, []) + + return ( +
+
+ 1 + +
+
+ 2 +
+
+ ) +} \ No newline at end of file diff --git a/directory/src/routes/path/operator.jsx b/directory/src/routes/path/operator.jsx index 92565c2..21c3091 100644 --- a/directory/src/routes/path/operator.jsx +++ b/directory/src/routes/path/operator.jsx @@ -1,15 +1,23 @@ import { useState, - useEffect + useEffect, + useContext } from 'react' import './operator.css' +import { TitleContext } from '@/context/useTitleContext'; export default function Operator(props) { + const { title, setTitle } = useContext(TitleContext) + + useEffect(() => { + setTitle('Chen') + }, []) return (
1 +
2 diff --git a/directory/src/routes/root.css b/directory/src/routes/root.css index 6fb3e55..a9d4550 100644 --- a/directory/src/routes/root.css +++ b/directory/src/routes/root.css @@ -1,3 +1,149 @@ .main { flex-grow: 1; +} + +.header { + width: auto; + position: sticky; + top: 0; + right: 0; + padding: 1rem; + z-index: 1; + height: 3rem; + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + justify-content: flex-start; +} + +.header .dropdown { + margin-left: auto; +} + +.navButton { + padding: 0.5rem; + font-size: 2rem; + width: 2rem; + height: 2rem; + cursor: pointer; + display: flex; + justify-content: center; + align-items: flex-start; + flex-direction: column; + z-index: 2; +} + +.navButton .bar { + width: 2rem; + height: 0.2rem; + background-color: var(--text-color); + transition: transform cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; +} + +.navButton .bar:nth-child(1) { + transform: translate(0, -200%); +} + +.navButton .bar:nth-child(3) { + transform: translate(0, 200%); +} + +.navButton.active .bar:nth-child(1) { + transform: translate(0, 100%) rotateZ(45deg) scaleX(0.5) translate(-50%); +} + +.navButton.active .bar:nth-child(2) { + transform: rotateZ(-45deg); +} + +.navButton.active .bar:nth-child(3) { + transform: translate(0, -100%) rotateZ(45deg) scaleX(0.5) translate(50%); +} + +.drawer { + position: fixed; + top: 0; + left: -15rem; + width: 15rem; + height: 100%; + z-index: 0; + pointer-events: none; + transition: left cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + text-align: center; + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 1rem; +} + +.drawer .links { + padding: 8rem 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + background-color: var(--drawer-background-color); + height: 100%; + width: 15rem; +} + +.drawer .overlay { + height: 100%; + flex-grow: 1; + z-index: 2; +} + +.drawer.active { + pointer-events: all; + left: 0; + width: 100vw; +} + +.drawer a { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.2rem; + color: var(--text-color); + font-size: 0.8rem; + font-weight: 500; + transition: color cubic-bezier(0.65, 0.05, 0.36, 1) 0.3s; + text-transform: uppercase; +} + +.drawer a:hover { + color: var(--link-highlight-color); +} + +.main { + margin: 0 auto; + width: 90%; + max-width: 100rem; +} + +.footer .section { + border-top: 1px solid var(--border-color); + padding: 1rem 0; + display: flex; + align-content: center; + justify-content: center; + align-items: center; + flex-wrap: nowrap; +} + +.footer .links { + flex-direction: row; + gap: 1rem; +} + +.footer .links .separator { + height: 1rem; + border-left: 2px solid var(--border-color); +} + +.footer .copyright { + flex-direction: column; + gap: 0.5rem; + font-size: 12px; } \ No newline at end of file diff --git a/directory/src/routes/root.jsx b/directory/src/routes/root.jsx index fbbad28..42265fe 100644 --- a/directory/src/routes/root.jsx +++ b/directory/src/routes/root.jsx @@ -1,6 +1,7 @@ import { useState, - useEffect + useEffect, + useContext } from 'react' import { Outlet, @@ -11,21 +12,100 @@ import { useNavigate, } from "react-router-dom"; import './root.css' +import routes from '@/routes' +import { TitleContext } from '@/context/useTitleContext'; +import { LanguageContext } from '@/context/useLanguageContext'; +import Dropdown from '@/component/dropdown'; +import Popup from '@/component/popup'; +import ReturnButton from '@/component/return_button'; export default function Root(props) { + const { title, setTitle } = useContext(TitleContext) + const [drawerHidden, setDrawerHidden] = useState(true) + const { + language, setLanguage, + drawerTextDefaultLang, + drawerAlternateLang, + i18n + } = useContext(LanguageContext) + const [drawerDestinations, setDrawerDestinations] = useState(null) + + const toggleDrawer = () => { + setDrawerHidden(!drawerHidden) + } + + const renderDrawerDestinations = () => { + return routes.filter((item) => item.inDrawer).map((item) => { + if (typeof item.element.type === 'string') { + return ( + +
+ {i18n.key[item.name][drawerTextDefaultLang]} +
+
+ {i18n.key[item.name][drawerAlternateLang]} +
+
+ ) + } else { + return ( + +
+ {i18n.key[item.name][drawerTextDefaultLang]} +
+
+ {i18n.key[item.name][drawerAlternateLang]} +
+
+ ) + } + }) + } + + useEffect(() => { + setDrawerDestinations(renderDrawerDestinations()) + }, [drawerAlternateLang]) + return ( <> -
-
Nav button
- +
+
toggleDrawer()} + > +
+
+
+
+ { + return { + name: i18n.key[item][language], + value: item + } + })} + onClick={(item) => { + setLanguage(item.value) + }} + />
+
-
Dynamic Compile
+
{title}
{/* All Operator @@ -34,16 +114,23 @@ export default function Root(props) {
-