router working with navigation and links

This commit is contained in:
2026-03-03 19:58:37 -06:00
parent bf1f26d851
commit 0c7164b768
11 changed files with 72 additions and 73 deletions

5
src/about/AboutPage.tsx Normal file
View File

@@ -0,0 +1,5 @@
export default function AboutPage() {
return <>
Hello
</>
}

View File

@@ -2,6 +2,7 @@ body {
margin: 0; margin: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: black;
overflow: hidden; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',

View File

@@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import './index.scss';
import reportWebVitals from './reportWebVitals';
import {Canvas} from "./index/Canvas";
import {Nav} from "./nav/Nav";
import {RouterProvider} from "./router/RouterContext"; import {RouterProvider} from "./router/RouterContext";
import {Route} from "./router/Route"; import {Route} from "./router/Route";
const Canvas = React.lazy(() => import("./index/Canvas"));
const About = React.lazy(() => import("./about/AboutPage"));
const Nav = React.lazy(() => import("./nav/Nav"));
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
); );
@@ -18,6 +19,9 @@ root.render(
<Route path="/"> <Route path="/">
<Canvas/> <Canvas/>
</Route> </Route>
<Route path="/about">
<About/>
</Route>
</RouterProvider> </RouterProvider>
</React.StrictMode> </React.StrictMode>
); );
@@ -25,4 +29,4 @@ root.render(
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals(); //reportWebVitals();

View File

@@ -1,7 +1,7 @@
import {motion} from "framer-motion"; import {motion} from "framer-motion";
import "./canvas.scss"; import "./canvas.scss";
export function Canvas() { export default function Canvas() {
return ( return (
<main> <main>
<motion.h1 className="welcome" animate={{ rotate: 360 }}> <motion.h1 className="welcome" animate={{ rotate: 360 }}>

View File

@@ -7,13 +7,14 @@ import "./nav.scss";
import {motion} from "framer-motion"; import {motion} from "framer-motion";
import {Link} from "../router/Link"; import {Link} from "../router/Link";
export function Nav() { export default function Nav() {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
return <> return <>
<nav className="header"> <nav className="header">
<MenuButton active={active} setActive={setActive}/> <MenuButton active={active} setActive={setActive}/>
<Title/> <Title/>
<Link href="/about" className="nav-item">About</Link>
</nav> </nav>
<Menu active={active}/> <Menu active={active}/>
<img className="wave" alt="wave" src={wave}/> <img className="wave" alt="wave" src={wave}/>
@@ -24,11 +25,11 @@ function MenuButton(props : { active: boolean, setActive: (_:boolean) => void })
const {active, setActive} = props; const {active, setActive} = props;
return ( return (
<div className="menu-btn click"> <div className="menu-btn">
<button> <button>
<FontAwesomeIcon icon={faBars} onClick={function() { <FontAwesomeIcon icon={faBars} onClick={function() {
setActive(!active); setActive(!active);
}} /> }}/>
</button> </button>
</div> </div>
); );
@@ -36,7 +37,7 @@ function MenuButton(props : { active: boolean, setActive: (_:boolean) => void })
function Title() { function Title() {
return ( return (
<Link href="/" className="name click"> <Link href="/" className="name">
<motion.h1>Tymur Arsentiev</motion.h1> <motion.h1>Tymur Arsentiev</motion.h1>
</Link> </Link>
); );

View File

@@ -1,11 +1,11 @@
$padding: 15px; $padding: 15px;
$menu: #282c34; $menu: #282c34;
.click:hover { .menu-btn:hover {
opacity: 90%; opacity: 90%;
} }
.click:active { .menu-btn:active {
opacity: 80%; opacity: 80%;
} }
@@ -25,7 +25,6 @@ $menu: #282c34;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
color: white;
} }
.header { .header {
@@ -40,7 +39,6 @@ $menu: #282c34;
.name { .name {
align-self: center; align-self: center;
flex-grow: 1; flex-grow: 1;
text-decoration: underline;
} }
.wave { .wave {
position: absolute; position: absolute;

View File

@@ -1,7 +1,10 @@
import "./link.scss";
import React, {PropsWithChildren} from "react"; import React, {PropsWithChildren} from "react";
import {useRouter} from "./RouterContext"; import {useRouter} from "./RouterContext";
import {Path} from "./router";
export function Link(props: {className: string, href: string, replace?: boolean } & PropsWithChildren) { export function Link(props: {className: string, href: Path, replace?: boolean } & PropsWithChildren) {
const {children, className, href, replace} = props; const {children, className, href, replace} = props;
const router = useRouter(); const router = useRouter();
@@ -14,5 +17,5 @@ export function Link(props: {className: string, href: string, replace?: boolean
} }
} }
return <a onClick={onClick} href={href} className={className}>{children}</a> return <a onClick={onClick} href={href} className={`${className} link click`}>{children}</a>
} }

View File

@@ -6,7 +6,5 @@ export function Route(props: PropsWithChildren & {path: Path}) {
const {path, children} = props; const {path, children} = props;
const router = useRouter(); const router = useRouter();
return <> return router.current === path ? <>{children}</> : <></>
{ router.current?.currentPage === path && children}
</>
} }

View File

@@ -1,62 +1,36 @@
import {createContext, PropsWithChildren, useContext, useEffect, useSyncExternalStore} from "react"; import {createContext, PropsWithChildren, useContext, useEffect, useState, useSyncExternalStore} from "react";
import {HistoryState, Path, Router} from "./router"; import {DEFAULT, Path, Router} from "./router";
import * as repl from "node:repl";
const DEFAULT : Router = {
current: {
currentPage: window.location.pathname as Path
},
pushPage: function(){},
replacePage: function(){}
};
const RouterContext = createContext<Router>(DEFAULT); const RouterContext = createContext<Router>(DEFAULT);
export function useRouter() { export function useRouter() {
return useContext(RouterContext); return useContext(RouterContext);
} }
function subscribe(onChange: () => void) {
window.addEventListener("popstate", onChange);
// return unsubscribe function
return function() {
window.removeEventListener("popstate", onChange);
}
}
function getSnapshot(): HistoryState {
return window.history.state ?? DEFAULT;
}
function pushState(path: string) {
window.history.pushState({
currentPage: path as Path
},
"",
path);
}
function replaceState(path: string) {
window.history.replaceState({
currentPage: path as Path
},
"",
path);
}
export function RouterProvider(props : PropsWithChildren) { export function RouterProvider(props : PropsWithChildren) {
const state = useSyncExternalStore<HistoryState>(subscribe, getSnapshot); // path is replaced on every state push/replace/pop
const [path, setPath] = useState<Path>(window.location.pathname as Path);
// start history with initial page // handle the back/forward button
useEffect(() => { useEffect(() => {
replaceState(window.location.pathname); function subscribe(ev: PopStateEvent) {
setPath(ev.state as Path);
}
window.addEventListener("popstate", subscribe);
return () => window.removeEventListener("popstate", subscribe);
}, []); }, []);
return <RouterContext.Provider value={{ return <RouterContext.Provider value={{
replacePage: replaceState, pushPage: function(newPath: Path) {
pushPage: pushState, window.history.pushState(newPath,"", newPath);
current: state setPath(newPath as Path);
},
replacePage: function(newPath: Path) {
window.history.replaceState(newPath, "", newPath);
setPath(newPath as Path);
},
current: path
}}> }}>
{props.children} {props.children}
</RouterContext.Provider> </RouterContext.Provider>

12
src/router/link.scss Normal file
View File

@@ -0,0 +1,12 @@
.link {
color: white;
text-decoration: underline;
}
.link:hover {
opacity: 90%;
}
.link:active {
opacity: 80%;
}

View File

@@ -1,10 +1,13 @@
export type Router = { export type Router = {
replacePage: (path: string) => void; replacePage: (path: Path) => void;
pushPage: (path: string) => void; pushPage: (path: Path) => void;
current: HistoryState current: Path
} }
export type Path = `/${string}`; export type Path = `/${string}`;
export type HistoryState = {
currentPage: Path export const DEFAULT : Router = {
}; current: window.location.pathname as Path,
pushPage: function(){},
replacePage: function(){}
};