Instantly share code, notes, and snippets.
Created
May 30, 2020 15:39
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save silviogutierrez/ad2049cf8feee0749d85fccb1854d675 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from "react"; | |
import {Link} from "react-router"; | |
import {motion} from "framer-motion"; | |
import {reverse} from "@client/generated"; | |
import * as style from "@client/style"; | |
import {Icon} from "@client/components/Icon"; | |
import {Platform, Icon as IconLiterals} from "@client/constants"; | |
import {Profile} from "@client/models"; | |
const variants = { | |
menu: { | |
open: { | |
opacity: 1, | |
display: "block", | |
transition: {duration: 0.3, delayChildren: 0.1}, | |
}, | |
closed: { | |
opacity: 0, | |
transition: {delay: 1, delayChildren: 0.3}, | |
transitionEnd: { | |
display: "none", | |
}, | |
}, | |
}, | |
logo: { | |
open: { | |
opacity: 1, | |
transition: { | |
when: "beforeChildren", | |
delay: 0.3, | |
duration: 0.1, | |
staggerChildren: 0.5, | |
}, | |
}, | |
closed: { | |
opacity: 0, | |
}, | |
}, | |
menuContent: { | |
open: (height = 1000) => ({ | |
clipPath: `circle(${ | |
height * 2 + 200 | |
}px at 20px calc(env(safe-area-inset-top) + 20px))`, | |
transition: { | |
type: "spring", | |
stiffness: 40, | |
restDelta: 2, | |
}, | |
}), | |
closed: { | |
clipPath: "circle(0px at 20px calc(env(safe-area-inset-top) + 20px))", | |
transition: { | |
delay: 0.5, | |
type: "spring", | |
stiffness: 400, | |
damping: 40, | |
}, | |
}, | |
}, | |
menuItems: { | |
open: { | |
transition: {staggerChildren: 0.07, delayChildren: 0.4}, | |
}, | |
closed: { | |
transition: {staggerChildren: 0.05, staggerDirection: -1}, | |
}, | |
}, | |
menuItemComplex: { | |
open: { | |
y: 0, | |
opacity: 1, | |
transition: { | |
y: {stiffness: 1000, velocity: -100}, | |
}, | |
}, | |
closed: { | |
y: 50, | |
opacity: 0, | |
transition: { | |
y: {stiffness: 1000}, | |
}, | |
}, | |
}, | |
menuItemSimple: { | |
open: { | |
opacity: 1, | |
}, | |
closed: { | |
opacity: 0, | |
}, | |
}, | |
} as const; | |
const colors = ["#EF8165", "#50BD9C", "#EAAE63", "#9582C4", "#488EA5"]; | |
namespace styles { | |
export const menu = style.style({ | |
zIndex: style.zIndexes.menu, | |
position: "absolute", | |
width: "100vw", | |
height: "100vh", | |
backgroundColor: "rgba(0,0,0,0.3)", | |
}); | |
export const isMenuOpen = style.style({}); | |
export const menuContent = style.style({ | |
paddingTop: ["0px", "calc(env(safe-area-inset-top))"], | |
minWidth: 250, | |
width: "20%", | |
backgroundColor: "#FFF", | |
height: "100%", | |
display: "flex", | |
flexDirection: "column", | |
alignItems: "flex-start", | |
overflow: "hidden", | |
}); | |
export const logo = style.style({ | |
height: 27, | |
paddingTop: 10, | |
paddingRight: 10, | |
alignSelf: "flex-end", | |
}); | |
export const menuItems = style.style(style.csstips.verticallySpaced(15), { | |
paddingLeft: 25, | |
paddingTop: 20, | |
paddingBottom: 20, | |
flex: 1, | |
overflowY: "scroll", | |
// We want the scrollbar to appear on the items and not the whole menu. | |
// And all the way to the right. | |
width: "100%", | |
// But we want the children not to take up the full width so that on | |
// very large screens the scaling isn't huge. Size them to fit content. | |
display: "flex", | |
flexDirection: "column", | |
alignItems: "flex-start", | |
}); | |
export const menuItem = style.style({}); | |
export const menuItemLink = style.style({ | |
display: "flex", | |
alignItems: "center", | |
cursor: "pointer", | |
}); | |
export const menuItemIcon = style.style({ | |
width: 40, | |
height: 40, | |
borderRadius: 10, | |
border: "1.5px solid currentColor", | |
flex: "40px 0", | |
marginRight: 10, | |
display: "flex", | |
justifyContent: "center", | |
alignItems: "center", | |
}); | |
export const menuItemTitle = style.style({ | |
flex: 1, | |
textAlign: "left", | |
fontWeight: 500, | |
}); | |
} | |
type MenuLink = { | |
icon: IconLiterals; | |
link: string; | |
title: string; | |
showIf?: (profile: Profile | null, platform: Platform) => boolean; | |
external?: boolean; | |
}; | |
const homePage = { | |
icon: "home", | |
link: reverse("home_page"), | |
title: "Home Page", | |
showIf: () => window.cordova == null, | |
external: true, | |
} as const; | |
const AUTHENTICATED_LINKS: MenuLink[] = [ | |
{ | |
icon: "custom-0283-contacts", | |
link: "/journal/", | |
title: "Journal", | |
}, | |
{ | |
icon: "0393-calendar-31", | |
link: "/planner/", | |
title: "Planner", | |
}, | |
{ | |
icon: "leaf", | |
link: "/foods/", | |
title: "My Foods", | |
}, | |
{ | |
icon: "star", | |
link: "/recipes/", | |
title: "Recipes", | |
}, | |
{ | |
icon: "user", | |
link: "/profile/", | |
title: "Profile", | |
}, | |
{ | |
icon: "bar-chart-o", | |
link: "/reports/", | |
title: "Reports", | |
}, | |
{ | |
icon: "0291-users", | |
link: "/manager/", | |
title: "Manager", | |
showIf: (profile) => profile != null && profile.is_manager, | |
}, | |
{ | |
icon: "send", | |
link: "/invite/", | |
title: "Invite", | |
}, | |
{ | |
icon: "envelope-o", | |
link: "/contact/", | |
title: "Contact", | |
}, | |
homePage, | |
{ | |
icon: "custom-0767-exit-right", | |
link: "/logout/", | |
title: "Logout", | |
}, | |
]; | |
export const ANONYMOUS_LINKS: MenuLink[] = [ | |
{ | |
icon: "custom-0763-enter-right", | |
link: "/login/", | |
title: "Login", | |
}, | |
{ | |
icon: "pencil-square-o", | |
link: "/register/", | |
title: "Register", | |
}, | |
{ | |
icon: "calculator", | |
link: "/calculator/", | |
title: "Calculator", | |
}, | |
{ | |
icon: "envelope-o", | |
link: "/contact/", | |
title: "Contact", | |
}, | |
homePage, | |
]; | |
interface Props { | |
isAuthenticated: boolean; | |
toggleMenu: () => void; | |
isMenuOpen: boolean; | |
profile: Profile | null; | |
platform: Platform; | |
} | |
export const Menu = (props: Props) => { | |
const handleClick = () => props.toggleMenu(); | |
const swallowClick = (event: React.MouseEvent<HTMLDivElement>) => | |
event.stopPropagation(); | |
return ( | |
<motion.div | |
initial={false} | |
animate={props.isMenuOpen === true ? "open" : "closed"} | |
variants={variants.menu} | |
className={style.classes( | |
styles.menu, | |
props.isMenuOpen && styles.isMenuOpen, | |
)} | |
onClick={handleClick} | |
> | |
<motion.div | |
className={styles.menuContent} | |
variants={variants.menuContent} | |
onClick={swallowClick} | |
> | |
<Logo isMenuOpen={props.isMenuOpen} /> | |
<motion.ul className={styles.menuItems} variants={variants.menuItems}> | |
{(props.isAuthenticated ? AUTHENTICATED_LINKS : ANONYMOUS_LINKS) | |
.filter( | |
(link) => | |
link.showIf == null || | |
link.showIf(props.profile, props.platform) === true, | |
) | |
.map((item, index) => { | |
const i = index % colors.length; | |
const itemStyle = { | |
color: colors[i], | |
// backgroundColor: bgs[i], | |
}; | |
const content = ( | |
<> | |
<div | |
style={itemStyle} | |
className={styles.menuItemIcon} | |
> | |
<Icon large="relative" icon={item.icon} /> | |
</div> | |
<div className={styles.menuItemTitle}> | |
{item.title} | |
</div> | |
</> | |
); | |
const commonProps = { | |
className: styles.menuItemLink, | |
onClick: handleClick, | |
}; | |
const link = | |
item.external === true ? ( | |
<a href={item.link} {...commonProps}> | |
{content} | |
</a> | |
) : ( | |
<Link to={item.link} {...commonProps}> | |
{content} | |
</Link> | |
); | |
return ( | |
<motion.li | |
key={item.link} | |
className={styles.menuItem} | |
variants={ | |
variants.menuItemComplex | |
} | |
whileHover={{scale: 1.1}} | |
whileTap={{scale: 0.95}} | |
> | |
{link} | |
</motion.li> | |
); | |
})} | |
</motion.ul> | |
</motion.div> | |
</motion.div> | |
); | |
}; | |
interface LogoProps { | |
isMenuOpen: boolean; | |
} | |
const Logo = (props: LogoProps) => ( | |
<motion.svg | |
initial={false} | |
className={styles.logo} | |
animate={props.isMenuOpen === true ? "open" : "closed"} | |
variants={variants.logo} | |
viewBox="0 0 181 61" | |
> | |
<motion.path | |
initial="closed" | |
d="M113.1,26 C113.386288,27.4487218 113.52035,28.9234021 113.5,30.4 C113.472355,42.7435186 103.443556,52.7275127 91.1000375,52.6998882 C78.7565188,52.6722638 68.7725081,42.6434817 68.8001119,30.299963 C68.8277158,17.9564442 78.8564812,7.97241675 91.2,8 C96.1576181,8.02719484 100.96977,9.67807899 104.9,12.7 L110.6,7 C98.8399439,-2.61729776 81.7710663,-2.02053704 70.711382,8.39458264 C59.6516976,18.8097023 58.0322912,35.8120618 66.9269572,48.1277531 C75.8216232,60.4434445 92.470938,64.2517275 105.83509,57.0274096 C119.199243,49.8030917 125.132224,33.7873984 119.7,19.6 L113.1,26 Z" | |
variants={{ | |
open: { | |
rotate: 0, | |
}, | |
closed: { | |
rotate: 180, | |
originX: "center", | |
originY: "center", | |
}, | |
}} | |
fill="#373A3C" | |
/> | |
<motion.path | |
initial="closed" | |
variants={{ | |
open: { | |
scale: 1, | |
}, | |
closed: { | |
originX: "center", | |
originY: "center", | |
scale: 0, | |
}, | |
}} | |
d="M113.1,9.3 L92.6,29.7 L88,25.1 L82.1,31 L92.6,41.5 L118.1,16.2 C116.782176,13.6663504 115.097326,11.3412568 113.1,9.3 Z" | |
id="Path" | |
fill="#F57E5F" | |
/> | |
<polygon | |
fill="#373A3C" | |
points="180.9 0.3 159.2 41.1 159.2 60.2 151.1 60.2 151.1 41.1 129.6 0.1 138.8 0.1 155.2 29.3 171.7 0.2" | |
/> | |
<path | |
d="M19.9,60.9 C6.2,60.9 0,51 0,41.1 L8.1,41.1 C8.1,51.8 17,52.9 19.7,52.9 C25.538796,52.9171573 30.3069981,48.2380805 30.4,42.4 L30.4,0.3 L38.5,0.3 L38.5,41.6 C38.6889161,46.5507122 36.8949236,51.372218 33.5158418,54.9953447 C30.1367599,58.6184713 25.451852,60.7437865 20.5,60.9 L19.9,60.9 Z" | |
fill="#373A3C" | |
/> | |
</motion.svg> | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment