mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 15:05:35 +00:00
Mobile swipeable menu (#13)
* feat(mobile): swipe right to open menu * refactor: css for side nav bar * refactor: side menu desktop & mobile
This commit is contained in:
83
package-lock.json
generated
83
package-lock.json
generated
@@ -11,7 +11,6 @@
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/react-toggle": "^4.0.5",
|
||||
"accept-language": "^3.0.18",
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^10.16.16",
|
||||
"i18next": "^23.7.11",
|
||||
@@ -30,6 +29,7 @@
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-tabs": "^6.0.2",
|
||||
"react-toggle": "^4.1.3",
|
||||
"sass": "^1.69.5",
|
||||
@@ -4032,11 +4032,6 @@
|
||||
"has-symbols": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
@@ -4065,16 +4060,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
||||
@@ -4463,17 +4448,6 @@
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
@@ -4779,14 +4753,6 @@
|
||||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
@@ -5814,25 +5780,6 @@
|
||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
@@ -5841,19 +5788,6 @@
|
||||
"is-callable": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "10.16.16",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.16.tgz",
|
||||
@@ -7351,6 +7285,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -7359,6 +7294,7 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -8128,11 +8064,6 @@
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
|
||||
"integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -8291,6 +8222,14 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-swipeable": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.1.tgz",
|
||||
"integrity": "sha512-RKB17JdQzvECfnVj9yDZsiYn3vH0eyva/ZbrCZXZR0qp66PBRhtg4F9yJcJTWYT5Adadi+x4NoG53BxKHwIYLQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.3 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-tabs": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz",
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@types/react-toggle": "^4.0.5",
|
||||
"accept-language": "^3.0.18",
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"framer-motion": "^10.16.16",
|
||||
"i18next": "^23.7.11",
|
||||
@@ -35,6 +34,7 @@
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-tabs": "^6.0.2",
|
||||
"react-toggle": "^4.1.3",
|
||||
"sass": "^1.69.5",
|
||||
|
||||
@@ -3,22 +3,24 @@ import { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
export default function ButtonLink({
|
||||
href = '#',
|
||||
title = '',
|
||||
onClick,
|
||||
children,
|
||||
style = {},
|
||||
className = '',
|
||||
}: {
|
||||
}: Readonly<{
|
||||
href?: string;
|
||||
title?;
|
||||
onClick?: (...args: any) => any;
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
}) {
|
||||
}>) {
|
||||
const handleClick = (event) => {
|
||||
if (!href || href === '#') {
|
||||
event.preventDefault();
|
||||
}
|
||||
onClick && onClick();
|
||||
onClick && onClick?.();
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
@@ -26,6 +28,7 @@ export default function ButtonLink({
|
||||
onClick={handleClick}
|
||||
style={style}
|
||||
className={className}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
font-size: 0.9em;
|
||||
color: $grey;
|
||||
text-align: center;
|
||||
padding: 0.75em 0 0.25em;
|
||||
padding-top: 0.75em;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import ButtonLink from 'components/ButtonLink';
|
||||
import Footer from 'components/Footer/Footer';
|
||||
import MobileCategoriesModal from 'components/MobileCategoriesModal';
|
||||
import CreateItem from 'components/QuickActions/CreateItem';
|
||||
import EditItem from 'components/QuickActions/EditItem';
|
||||
import RemoveItem from 'components/QuickActions/RemoveItem';
|
||||
@@ -10,15 +10,20 @@ import useActiveCategory from 'hooks/useActiveCategory';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import LinkTag from 'next/link';
|
||||
import { BiSearchAlt } from 'react-icons/bi';
|
||||
import { RxHamburgerMenu } from 'react-icons/rx';
|
||||
import quickActionStyles from '../QuickActions/quickactions.module.scss';
|
||||
import LinkItem from './LinkItem';
|
||||
import styles from './links.module.scss';
|
||||
|
||||
interface LinksProps {
|
||||
isMobile: boolean;
|
||||
openSideMenu: () => void;
|
||||
}
|
||||
|
||||
export default function Links({ isMobile }: Readonly<LinksProps>) {
|
||||
export default function Links({
|
||||
isMobile,
|
||||
openSideMenu,
|
||||
}: Readonly<LinksProps>) {
|
||||
const { t } = useTranslation('home');
|
||||
const { activeCategory } = useActiveCategory();
|
||||
|
||||
@@ -35,7 +40,14 @@ export default function Links({ isMobile }: Readonly<LinksProps>) {
|
||||
return (
|
||||
<div className={styles['links-wrapper']}>
|
||||
<h2 className={styles['category-header']}>
|
||||
{isMobile && <MobileCategoriesModal />}
|
||||
{isMobile && (
|
||||
<ButtonLink
|
||||
onClick={openSideMenu}
|
||||
title='Open side nav bar'
|
||||
>
|
||||
<RxHamburgerMenu size={'1.5em'} />
|
||||
</ButtonLink>
|
||||
)}
|
||||
<span className={styles['category-name']}>
|
||||
{name}
|
||||
{links.length > 0 && (
|
||||
|
||||
@@ -31,9 +31,13 @@
|
||||
|
||||
& h2 {
|
||||
color: $blue;
|
||||
margin-bottom: 0.25em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 500;
|
||||
|
||||
& svg {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& .links-count {
|
||||
color: $grey;
|
||||
font-weight: 300;
|
||||
@@ -44,7 +48,7 @@
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
gap: 0.4em;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -188,3 +192,9 @@
|
||||
animation: rotate 1s both reverse infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.links-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import PATHS from 'constants/paths';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import useModal from 'hooks/useModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { RxHamburgerMenu } from 'react-icons/rx';
|
||||
import BlockWrapper from './BlockWrapper/BlockWrapper';
|
||||
import ButtonLink from './ButtonLink';
|
||||
import Modal from './Modal/Modal';
|
||||
import Categories from './SideMenu/Categories/Categories';
|
||||
|
||||
export default function MobileCategoriesModal() {
|
||||
const { t } = useTranslation();
|
||||
const mobileModal = useModal();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonLink
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
onClick={mobileModal.open}
|
||||
>
|
||||
<RxHamburgerMenu
|
||||
size={'1.5em'}
|
||||
style={{ marginRight: '.5em' }}
|
||||
/>
|
||||
</ButtonLink>
|
||||
<AnimatePresence>
|
||||
{mobileModal.isShowing && (
|
||||
<Modal close={mobileModal.close}>
|
||||
<BlockWrapper style={{ minHeight: '0', flex: '1' }}>
|
||||
<ButtonLink href={PATHS.CATEGORY.CREATE}>
|
||||
{t('common:category.create')}
|
||||
</ButtonLink>
|
||||
<Categories />
|
||||
</BlockWrapper>
|
||||
</Modal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,44 @@
|
||||
import BlockWrapper from 'components/BlockWrapper/BlockWrapper';
|
||||
import Categories from './Categories/Categories';
|
||||
import Favorites from './Favorites/Favorites';
|
||||
import NavigationLinks from './NavigationLinks';
|
||||
import UserCard from './UserCard/UserCard';
|
||||
import styles from './sidemenu.module.scss';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useSwipeable } from 'react-swipeable';
|
||||
import styles from './side-menu.module.scss';
|
||||
|
||||
export default function SideMenu() {
|
||||
return (
|
||||
<div className={styles['side-menu']}>
|
||||
<BlockWrapper>
|
||||
<Favorites />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper style={{ minHeight: '0', flex: '1' }}>
|
||||
<Categories />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<NavigationLinks />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<UserCard />
|
||||
</BlockWrapper>
|
||||
</div>
|
||||
interface SideMenuProps {
|
||||
close?: (...args: any) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function SideMenu({ close, children }: Readonly<SideMenuProps>) {
|
||||
const handlers = useSwipeable({
|
||||
trackMouse: true,
|
||||
onSwipedLeft: close,
|
||||
});
|
||||
|
||||
const handleWrapperClick = (event) =>
|
||||
event.target.classList?.[0] === styles['side-menu-wrapper'] &&
|
||||
close &&
|
||||
close();
|
||||
|
||||
return createPortal(
|
||||
<motion.div
|
||||
className={styles['side-menu-wrapper']}
|
||||
onClick={handleWrapperClick}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }}
|
||||
{...handlers}
|
||||
>
|
||||
<motion.div
|
||||
className={styles['side-menu-container']}
|
||||
initial={{ opacity: 0, x: '-100%' }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '-100%', transition: { duration: 0.1 } }}
|
||||
transition={{ type: 'tween' }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
20
src/components/SideMenu/side-menu.module.scss
Normal file
20
src/components/SideMenu/side-menu.module.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
@import 'styles/colors.scss';
|
||||
|
||||
.side-menu-wrapper {
|
||||
z-index: 9999;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: $black-blur;
|
||||
box-shadow: 0 0 1em 0 $black-blur;
|
||||
}
|
||||
|
||||
.side-menu-container {
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
background: $light-grey;
|
||||
padding: 0.75em;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import SearchModal from 'components/SearchModal/SearchModal';
|
||||
import PATHS from 'constants/paths';
|
||||
import useActiveCategory from 'hooks/useActiveCategory';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import styles from './sidemenu.module.scss';
|
||||
import styles from './side-nav.module.scss';
|
||||
|
||||
export default function NavigationLinks() {
|
||||
const { t } = useTranslation();
|
||||
25
src/components/SideNavigation/SideNavigation.tsx
Normal file
25
src/components/SideNavigation/SideNavigation.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import BlockWrapper from 'components/BlockWrapper/BlockWrapper';
|
||||
import Categories from './Categories/Categories';
|
||||
import Favorites from './Favorites/Favorites';
|
||||
import NavigationLinks from './NavigationLinks';
|
||||
import UserCard from './UserCard/UserCard';
|
||||
import styles from './side-nav.module.scss';
|
||||
|
||||
export default function SideNavigation() {
|
||||
return (
|
||||
<div className={styles['side-menu']}>
|
||||
<BlockWrapper>
|
||||
<Favorites />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper style={{ minHeight: '0', flex: '1' }}>
|
||||
<Categories />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<NavigationLinks />
|
||||
</BlockWrapper>
|
||||
<BlockWrapper>
|
||||
<UserCard />
|
||||
</BlockWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,6 @@
|
||||
.side-menu {
|
||||
height: 100%;
|
||||
width: 325px;
|
||||
padding: 0 25px 0 10px;
|
||||
border-right: 1px solid $lightest-grey;
|
||||
margin-right: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
@@ -2,20 +2,24 @@ import clsx from 'clsx';
|
||||
import Links from 'components/Links/Links';
|
||||
import PageTransition from 'components/PageTransition';
|
||||
import SideMenu from 'components/SideMenu/SideMenu';
|
||||
import UserCard from 'components/SideMenu/UserCard/UserCard';
|
||||
import SideNavigation from 'components/SideNavigation/SideNavigation';
|
||||
import * as Keys from 'constants/keys';
|
||||
import PATHS from 'constants/paths';
|
||||
import ActiveCategoryContext from 'contexts/activeCategoryContext';
|
||||
import CategoriesContext from 'contexts/categoriesContext';
|
||||
import FavoritesContext from 'contexts/favoritesContext';
|
||||
import GlobalHotkeysContext from 'contexts/globalHotkeysContext';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useMediaQuery } from 'hooks/useMediaQuery';
|
||||
import useModal from 'hooks/useModal';
|
||||
import { getServerSideTranslation } from 'i18n';
|
||||
import getUserCategories from 'lib/category/getUserCategories';
|
||||
import sortCategoriesByNextId from 'lib/category/sortCategoriesByNextId';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useSwipeable } from 'react-swipeable';
|
||||
import styles from 'styles/home.module.scss';
|
||||
import { CategoryWithLinks, LinkWithCategory } from 'types/types';
|
||||
import { withAuthentication } from 'utils/session';
|
||||
|
||||
@@ -25,10 +29,63 @@ interface HomePageProps {
|
||||
}
|
||||
|
||||
export default function HomePage(props: Readonly<HomePageProps>) {
|
||||
const router = useRouter();
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
const { isShowing, open, close } = useModal();
|
||||
const handlers = useSwipeable({
|
||||
trackMouse: true,
|
||||
onSwipedRight: open,
|
||||
});
|
||||
|
||||
const [globalHotkeysEnable, setGlobalHotkeysEnabled] =
|
||||
useEffect(() => {
|
||||
if (!isMobile && isShowing) {
|
||||
close();
|
||||
}
|
||||
}, [close, isMobile, isShowing]);
|
||||
|
||||
return (
|
||||
<PageTransition
|
||||
className={clsx('App', 'flex-row')}
|
||||
hideLangageSelector
|
||||
>
|
||||
<HomeProviders
|
||||
categories={props.categories}
|
||||
activeCategory={props.activeCategory}
|
||||
>
|
||||
<div
|
||||
className={styles['swipe-handler']}
|
||||
{...handlers}
|
||||
>
|
||||
{!isMobile && (
|
||||
<div className={styles['side-bar']}>
|
||||
<SideNavigation />
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{isShowing && (
|
||||
<SideMenu close={close}>
|
||||
<SideNavigation />
|
||||
</SideMenu>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<Links
|
||||
isMobile={isMobile}
|
||||
openSideMenu={open}
|
||||
/>
|
||||
</div>
|
||||
</HomeProviders>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeProviders(
|
||||
props: Readonly<{
|
||||
children: ReactNode;
|
||||
categories: CategoryWithLinks[];
|
||||
activeCategory: CategoryWithLinks;
|
||||
}>,
|
||||
) {
|
||||
const router = useRouter();
|
||||
const [globalHotkeysEnabled, setGlobalHotkeysEnabled] =
|
||||
useState<boolean>(true);
|
||||
const [categories, setCategories] = useState<CategoryWithLinks[]>(
|
||||
props.categories,
|
||||
@@ -36,10 +93,13 @@ export default function HomePage(props: Readonly<HomePageProps>) {
|
||||
const [activeCategory, setActiveCategory] =
|
||||
useState<CategoryWithLinks | null>(props.activeCategory || categories?.[0]);
|
||||
|
||||
const handleChangeCategory = (category: CategoryWithLinks) => {
|
||||
setActiveCategory(category);
|
||||
router.push(`${PATHS.HOME}?categoryId=${category.id}`);
|
||||
};
|
||||
const handleChangeCategory = useCallback(
|
||||
(category: CategoryWithLinks) => {
|
||||
setActiveCategory(category);
|
||||
router.push(`${PATHS.HOME}?categoryId=${category.id}`);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
const favorites = useMemo<LinkWithCategory[]>(
|
||||
() =>
|
||||
@@ -52,44 +112,47 @@ export default function HomePage(props: Readonly<HomePageProps>) {
|
||||
[categories],
|
||||
);
|
||||
|
||||
const categoriesContextValue = useMemo(
|
||||
() => ({ categories, setCategories }),
|
||||
[categories],
|
||||
);
|
||||
const activeCategoryContextValue = useMemo(
|
||||
() => ({ activeCategory, setActiveCategory: handleChangeCategory }),
|
||||
[activeCategory, handleChangeCategory],
|
||||
);
|
||||
const favoritesContextValue = useMemo(() => ({ favorites }), [favorites]);
|
||||
const globalHotkeysContextValue = useMemo(
|
||||
() => ({
|
||||
globalHotkeysEnabled: globalHotkeysEnabled,
|
||||
setGlobalHotkeysEnabled,
|
||||
}),
|
||||
[globalHotkeysEnabled],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
Keys.OPEN_CREATE_LINK_KEY,
|
||||
() => {
|
||||
router.push(`${PATHS.LINK.CREATE}?categoryId=${activeCategory.id}`);
|
||||
},
|
||||
{ enabled: globalHotkeysEnable },
|
||||
{ enabled: globalHotkeysEnabled },
|
||||
);
|
||||
useHotkeys(
|
||||
Keys.OPEN_CREATE_CATEGORY_KEY,
|
||||
() => {
|
||||
router.push(PATHS.CATEGORY.CREATE);
|
||||
},
|
||||
{ enabled: globalHotkeysEnable },
|
||||
{ enabled: globalHotkeysEnabled },
|
||||
);
|
||||
|
||||
return (
|
||||
<PageTransition
|
||||
className={clsx('App', 'flex-row')}
|
||||
hideLangageSelector
|
||||
>
|
||||
<CategoriesContext.Provider value={{ categories, setCategories }}>
|
||||
<ActiveCategoryContext.Provider
|
||||
value={{ activeCategory, setActiveCategory: handleChangeCategory }}
|
||||
>
|
||||
<FavoritesContext.Provider value={{ favorites }}>
|
||||
<GlobalHotkeysContext.Provider
|
||||
value={{
|
||||
globalHotkeysEnabled: globalHotkeysEnable,
|
||||
setGlobalHotkeysEnabled,
|
||||
}}
|
||||
>
|
||||
{isMobile ? <UserCard /> : <SideMenu />}
|
||||
<Links isMobile={isMobile} />
|
||||
</GlobalHotkeysContext.Provider>
|
||||
</FavoritesContext.Provider>
|
||||
</ActiveCategoryContext.Provider>
|
||||
</CategoriesContext.Provider>
|
||||
</PageTransition>
|
||||
<CategoriesContext.Provider value={categoriesContextValue}>
|
||||
<ActiveCategoryContext.Provider value={activeCategoryContextValue}>
|
||||
<FavoritesContext.Provider value={favoritesContextValue}>
|
||||
<GlobalHotkeysContext.Provider value={globalHotkeysContextValue}>
|
||||
{props.children}
|
||||
</GlobalHotkeysContext.Provider>
|
||||
</FavoritesContext.Provider>
|
||||
</ActiveCategoryContext.Provider>
|
||||
</CategoriesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
@media (max-width: 768px) {
|
||||
.form-container {
|
||||
width: 100%;
|
||||
margin-top: 5em;
|
||||
margin-top: 1em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ kbd {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.App {
|
||||
padding: 0.75em;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
23
src/styles/home.module.scss
Normal file
23
src/styles/home.module.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
@import 'colors.scss';
|
||||
|
||||
.swipe-handler {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background-color: $light-grey;
|
||||
box-shadow: 0 0 1em 0 $black-blur;
|
||||
padding: 0.75em;
|
||||
}
|
||||
|
||||
.side-bar {
|
||||
padding-right: 0.75em;
|
||||
border-right: 1px solid $lightest-grey;
|
||||
margin-right: 5px;
|
||||
}
|
||||
Reference in New Issue
Block a user