From 63bf19c9e307980008a706723d39251a5f2cbaa5 Mon Sep 17 00:00:00 2001 From: Alex Lau Date: Sun, 17 Mar 2024 19:15:28 +0800 Subject: [PATCH] Menu scaffolding + useAnimationUnmountHook --- package-lock.json | 9 + package.json | 1 + src/assets/icon.svg | 250 ++++++++++++++++++++++++ src/assets/theme-light-dark.svg | 1 + src/components/About/index.tsx | 4 +- src/components/Layout/index.tsx | 59 ++++-- src/components/NavigationBar/index.tsx | 11 ++ src/components/NavigationMenu/index.tsx | 44 +++++ src/contexts/AppContext.ts | 7 + src/hooks/useAnimationEndUnmount.ts | 29 +++ tailwind.config.js | 9 + 11 files changed, 402 insertions(+), 22 deletions(-) create mode 100644 src/assets/icon.svg create mode 100644 src/assets/theme-light-dark.svg create mode 100644 src/components/NavigationBar/index.tsx create mode 100644 src/components/NavigationMenu/index.tsx create mode 100644 src/contexts/AppContext.ts create mode 100644 src/hooks/useAnimationEndUnmount.ts diff --git a/package-lock.json b/package-lock.json index 46085ae..e4919c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@reduxjs/toolkit": "^2.2.1", + "clsx": "^2.1.0", "localforage": "^1.10.0", "match-sorter": "^6.3.4", "react": "^18.2.0", @@ -2406,6 +2407,14 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/package.json b/package.json index fc911a8..5b004db 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@reduxjs/toolkit": "^2.2.1", + "clsx": "^2.1.0", "localforage": "^1.10.0", "match-sorter": "^6.3.4", "react": "^18.2.0", diff --git a/src/assets/icon.svg b/src/assets/icon.svg new file mode 100644 index 0000000..eb3cf11 --- /dev/null +++ b/src/assets/icon.svg @@ -0,0 +1,250 @@ + + + + + + + + + + + + diff --git a/src/assets/theme-light-dark.svg b/src/assets/theme-light-dark.svg new file mode 100644 index 0000000..d901206 --- /dev/null +++ b/src/assets/theme-light-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/About/index.tsx b/src/components/About/index.tsx index 65a2974..dee2e11 100644 --- a/src/components/About/index.tsx +++ b/src/components/About/index.tsx @@ -7,7 +7,7 @@ export default function About() {

Alex Lau

-

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur posuere tincidunt nulla in egestas. Etiam in aliquam ipsum, in sagittis ex. Duis fermentum commodo malesuada. Sed consequat magna eu nunc tempus ultrices. Vivamus nisi sem, varius non mi quis, tempor gravida ipsum. Vestibulum sodales, leo et ultrices pellentesque, lorem ipsum pharetra quam, quis dapibus neque felis eu diam. Nunc pellentesque massa eu nibh congue condimentum. Donec eget sem vitae neque dignissim eleifend. Aliquam quis lacus ante. Sed in lectus et dui elementum scelerisque sit amet eu leo. Fusce volutpat dolor et est viverra porttitor.

@@ -17,7 +17,7 @@ export default function About() {

Mauris malesuada purus tristique pretium convallis. Mauris rutrum leo vel metus malesuada, et ullamcorper libero vulputate. Maecenas pharetra vitae est non hendrerit. Fusce nec nulla enim. Etiam rutrum sem a neque venenatis tincidunt. Vestibulum sed eros purus. Integer cursus dictum ullamcorper. Maecenas eu metus ut nulla ornare tristique sit amet mollis risus.

-

+
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 25318d8..e871cb5 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -1,30 +1,49 @@ +import { useState } from 'react'; import { Outlet } from "react-router-dom"; +import clsx from 'clsx'; + +import MyIcon from '../../assets/icon.svg?react'; +import DarkMode from '../../assets/theme-light-dark.svg?react'; + +import NavigationBar from "../NavigationBar"; +import NavigationMenu from '../NavigationMenu'; +import { AppContext } from '../../contexts/AppContext'; export default function Layout() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const value = { + isMenuOpen, + setIsMenuOpen, + }; + const blurClassName = isMenuOpen ? ' blur-[3px]' : ''; + return ( -
-
-
-
- Something goes here -
- Something goes here -
- Something goes here + +
+
+
+
+ +
+ + +
+
+
+ +
-
- +
+
+

Wisdom has been chasing me, but I have alawys been faster.

+

+ Built with React & Tailwind CSS
+ 2024 Alex Lau. All rights reserved. +

-
-
-

The greatest glory in living lies not in never falling, but in rising every time we fall.

-

- Built with React & Tailwind CSS
- 2024 Alex Lau. All rights reserved. -

-
+
-
+ ) } \ No newline at end of file diff --git a/src/components/NavigationBar/index.tsx b/src/components/NavigationBar/index.tsx new file mode 100644 index 0000000..7cc4a71 --- /dev/null +++ b/src/components/NavigationBar/index.tsx @@ -0,0 +1,11 @@ +import { Link } from "react-router-dom"; + +export default function NavigationBar() { + return ( + + ) +} \ No newline at end of file diff --git a/src/components/NavigationMenu/index.tsx b/src/components/NavigationMenu/index.tsx new file mode 100644 index 0000000..741f59a --- /dev/null +++ b/src/components/NavigationMenu/index.tsx @@ -0,0 +1,44 @@ +import { useContext } from "react" +import { createPortal } from "react-dom"; +import clsx from "clsx"; + +import { AppContext } from "../../contexts/AppContext"; +import { useAnimationEndUnmmount } from "../../hooks/useAnimationEndUnmount"; + +function Menu({ onMenuClose, isMenuOpen } : { + onMenuClose: () => void, + isMenuOpen: boolean, +}) { + const shouldRender = useAnimationEndUnmmount('menu', isMenuOpen); + + // TODO: Check why enter-effect cannot use transition, but have to rely on `animate-open-menu` + return shouldRender && createPortal( + ( + + ), + document.body, + ); +} + +export default function NavigationMenu() { + const { isMenuOpen, setIsMenuOpen } = useContext(AppContext); + + return ( + <> +
setIsMenuOpen(true)}> + Menu +
+ { setIsMenuOpen(false)} isMenuOpen={isMenuOpen} />} + + ) +} \ No newline at end of file diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts new file mode 100644 index 0000000..15978c3 --- /dev/null +++ b/src/contexts/AppContext.ts @@ -0,0 +1,7 @@ +import { createContext } from "react"; + +export const AppContext = createContext({ + isMenuOpen: false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setIsMenuOpen: (_: boolean) => {}, +}); \ No newline at end of file diff --git a/src/hooks/useAnimationEndUnmount.ts b/src/hooks/useAnimationEndUnmount.ts new file mode 100644 index 0000000..cfe70fb --- /dev/null +++ b/src/hooks/useAnimationEndUnmount.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; + +export function useAnimationEndUnmmount(id: string, isMounted: boolean) { + const [shouldRender, setShouldRender] = useState(false); + + useEffect(() => { + const element = document.getElementById(id); + + if (isMounted && !shouldRender) { + setShouldRender(true); + } else if (!isMounted && shouldRender) { + if (!element) { + throw new Error('useAnimationEndUnmmount cannot find element id'); + } + + const handleAnimationEnd = () => { + setShouldRender(false); + }; + + element.addEventListener('transitionend', handleAnimationEnd); + + return () => { + element.removeEventListener('transitionend', handleAnimationEnd) + }; + } + }, [id, isMounted, shouldRender]); + + return shouldRender; +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index a87480c..237d380 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -7,6 +7,14 @@ export default { theme: { extend: { keyframes: { + open: { + "0%": { + transform: 'scaleY(0)', + }, + "100%": { + transform: 'scaleY(1)', + } + }, typing: { "0%": { width: "0%", @@ -45,6 +53,7 @@ export default { }, }, animation: { + 'open-menu': 'open 0.25s', typewriter: "typing 2s steps(18), cursor .7s infinite", 'text-slide-4': 'typing 2s linear alternate infinite, text-slide-4 16s steps(1) infinite', // 16s for text-slide-4 because 2s * 2 (alternate animation) }