Improving route handling
This commit is contained in:
58
package-lock.json
generated
58
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"jotai": "^2.20.0",
|
"jotai": "^2.20.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-router-dom": "^7.17.0",
|
||||||
"sass": "^1.99.0"
|
"sass": "^1.99.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1247,6 +1248,19 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2209,6 +2223,44 @@
|
|||||||
"react": "^19.2.6"
|
"react": "^19.2.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
|
||||||
|
"integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
|
||||||
|
"integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -2287,6 +2339,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"jotai": "^2.20.0",
|
"jotai": "^2.20.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-router-dom": "^7.17.0",
|
||||||
"sass": "^1.99.0"
|
"sass": "^1.99.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
20
src/App.jsx
20
src/App.jsx
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAtomValue, useAtom } from 'jotai';
|
||||||
import { activeSectionState, themeState } from './state/atoms';
|
import { activeSectionState, themeState } from './state/atoms';
|
||||||
import Navbar from './components/Navbar';
|
import Navbar from './components/Navbar';
|
||||||
import Hero from './components/Hero';
|
import Hero from './components/Hero';
|
||||||
@@ -19,11 +20,26 @@ const SECTIONS = {
|
|||||||
const EXIT_DURATION = 200;
|
const EXIT_DURATION = 200;
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const activeSection = useAtomValue(activeSectionState);
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [activeSection, setActiveSection] = useAtom(activeSectionState);
|
||||||
const theme = useAtomValue(themeState);
|
const theme = useAtomValue(themeState);
|
||||||
const [displayedSection, setDisplayedSection] = useState(activeSection);
|
const [displayedSection, setDisplayedSection] = useState(activeSection);
|
||||||
const timerRef = useRef(null);
|
const timerRef = useRef(null);
|
||||||
|
|
||||||
|
// Sync URL path with activeSection
|
||||||
|
useEffect(() => {
|
||||||
|
const path = location.pathname.substring(1) || 'home';
|
||||||
|
if (Object.keys(SECTIONS).includes(path)) {
|
||||||
|
if (path !== activeSection) {
|
||||||
|
setActiveSection(path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Invalid path, redirect to home
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}, [location.pathname, activeSection, setActiveSection, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (theme === 'light') {
|
if (theme === 'light') {
|
||||||
document.documentElement.dataset.theme = 'light';
|
document.documentElement.dataset.theme = 'light';
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ export default function Hero() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={styles.cta}>
|
<div className={styles.cta}>
|
||||||
<a href="#projects" className={styles.btnPrimary}>
|
<a href="/projects" className={styles.btnPrimary}>
|
||||||
See my work
|
See my work
|
||||||
</a>
|
</a>
|
||||||
<a href="#contact" className={styles.btnGhost}>
|
<a href="/contact" className={styles.btnGhost}>
|
||||||
Get in touch
|
Get in touch
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAtom } from 'jotai';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { navOpenState, activeSectionState } from '../state/atoms';
|
import { navOpenState, activeSectionState } from '../state/atoms';
|
||||||
import ThemeToggle from './ThemeToggle';
|
import ThemeToggle from './ThemeToggle';
|
||||||
import styles from '../styles/components/Navbar.module.scss';
|
import styles from '../styles/components/Navbar.module.scss';
|
||||||
@@ -12,31 +13,27 @@ const NAV_LINKS = [
|
|||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [isOpen, setIsOpen] = useAtom(navOpenState);
|
const [isOpen, setIsOpen] = useAtom(navOpenState);
|
||||||
const [activeSection, setActiveSection] = useAtom(activeSectionState);
|
const activeSection = useAtomValue(activeSectionState);
|
||||||
|
|
||||||
function handleNav(section) {
|
|
||||||
setActiveSection(section);
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
<button className={styles.logo} onClick={() => handleNav('home')}>
|
<Link to="/" className={styles.logo} onClick={() => setIsOpen(false)}>
|
||||||
<span className={styles.logoBracket}><</span>
|
<span className={styles.logoBracket}><</span>
|
||||||
human id="veikko" class="lintujarvi"
|
human id="veikko" class="lintujarvi"
|
||||||
<span className={styles.logoBracket}>/></span>
|
<span className={styles.logoBracket}>/></span>
|
||||||
</button>
|
</Link>
|
||||||
|
|
||||||
<ul className={`${styles.links} ${isOpen ? styles.open : ''}`}>
|
<ul className={`${styles.links} ${isOpen ? styles.open : ''}`}>
|
||||||
{NAV_LINKS.map(({ label, section }) => (
|
{NAV_LINKS.map(({ label, section }) => (
|
||||||
<li key={section}>
|
<li key={section}>
|
||||||
<button
|
<Link
|
||||||
onClick={() => handleNav(section)}
|
to={`/${section}`}
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
className={activeSection === section ? styles.active : undefined}
|
className={activeSection === section ? styles.active : undefined}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ const PROJECTS = [
|
|||||||
star: false,
|
star: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Pro bono project - Runosaari festival website',
|
name: 'Runosaari festival website - Pro bono',
|
||||||
desc: "Supporting local culture initiatives have always been close to my heart. I'm proudly helping Runosaari festival with their website and online presence already for the sixth year!",
|
desc: "Supporting local culture initiatives have always been close to my heart. I'm proudly helping Runosaari festival with their website and online presence already for the sixth year!",
|
||||||
tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'],
|
tags: ['React', 'Vite', 'Apache2', 'Ubuntu Server'],
|
||||||
href: '#',
|
href: 'https://www.runosaari.fi/',
|
||||||
star: false,
|
star: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
import './styles/main.scss';
|
import './styles/main.scss';
|
||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<Router>
|
||||||
<App />
|
<App />
|
||||||
|
</Router>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,4 +4,12 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||||
|
entryFileNames: 'assets/[name]-[hash].js',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user