router working with navigation and links
This commit is contained in:
5
src/about/AboutPage.tsx
Normal file
5
src/about/AboutPage.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default function AboutPage() {
|
||||||
|
return <>
|
||||||
|
Hello
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
@@ -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}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
@@ -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
12
src/router/link.scss
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
opacity: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:active {
|
||||||
|
opacity: 80%;
|
||||||
|
}
|
||||||
@@ -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(){}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user