diff --git a/src/common/constants.ts b/src/common/constants.ts index 9132d1c4..6b6a3178 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -1,3 +1,4 @@ export const color = { primary: "blue" as any, + primaryIcon: "#007aff" as any, }; diff --git a/src/common/shareState.ts b/src/common/shareState.ts index 71068d35..f05f59dd 100644 --- a/src/common/shareState.ts +++ b/src/common/shareState.ts @@ -24,8 +24,6 @@ export const isSignInState = atom(false); export const currencyState = atomWithStorage("currency", "฿"); export const pageSettingState = atomWithStorage("pageSetting", { - isMenuOnRightSide: false, - isLightMenu: false, isDarkTheme: false, }); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 97b3c928..522faa91 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Outlet, useNavigate, useOutletContext } from "react-router-dom"; +import { Outlet, useLocation, useNavigate, useOutletContext } from "react-router-dom"; import styled from "styled-components"; import pj from "../../package.json"; import { accountsState, categoriesState, totalAccountsBalanceState } from "../common/shareState"; @@ -13,6 +13,7 @@ import Menu from "./menu/Menu"; import { Box, Container, Heading } from "@radix-ui/themes"; import { color } from "../common/constants"; import { useAtom, useAtomValue } from "jotai"; +import { AnimatePresence } from "framer-motion"; const Header = styled.div` padding-top: 12px; @@ -43,6 +44,7 @@ const Layout: React.FC = () => { const [isLoading, setIsLoading] = React.useState(true); const [isSignIn, redirectToSignIn, isSignInLoading] = useSignIn(); const navigate = useNavigate(); + const location = useLocation(); const [backToHomeParam, setBackToHomeParam] = React.useState(null); React.useEffect(() => { @@ -87,9 +89,20 @@ const Layout: React.FC = () => { - + }> - + + + diff --git a/src/components/bases/Drawer.tsx b/src/components/bases/Drawer.tsx index 7fbe7413..e47fb958 100644 --- a/src/components/bases/Drawer.tsx +++ b/src/components/bases/Drawer.tsx @@ -1,150 +1,110 @@ import React from "react"; -import styled, { keyframes } from "styled-components"; -import { pageSettingState } from "../../common/shareState"; -import { DotsVerticalIcon } from "@radix-ui/react-icons"; -import { useAtomValue } from "jotai"; +import styled from "styled-components"; +import { motion, AnimatePresence } from "framer-motion"; -interface StyledProps { - $isMenuOnRightSide: boolean; - $isShow: boolean; - $isDarkTheme: boolean; +interface BackgroundStyledProps { + onClick?: (event: React.MouseEvent) => void; } -interface DrawStyledProps { - $isMenuOnRightSide: boolean; - $isLightMenu: boolean; -} - -const animationDuration = 0.5; +const animationDuration = 0.25; const zIndex = 1000; -const slideIn = keyframes` - 0% {left: -100%} - 100% {left: 0;} -`; -const slideInRtl = keyframes` - 0% {right: -100%} - 100% {right: 0;} -`; -const fadeIn = keyframes` - 0% {background-color: rgba(0, 0, 0, 0);} - 100% {background-color: rgba(0, 0, 0, 0.8);} -`; -const slideOut = keyframes` - 100% {left: -100%;} -`; -const slideOutRtl = keyframes` - 100% {right: -100%;} -`; -const fadeOut = keyframes` - 100% {background-color: rgba(0, 0, 0, 0);} -`; -const Panel = styled.div` +const PanelContainer = styled(motion.div)` + box-sizing: border-box; z-index: ${zIndex}; + bottom: 80px; position: fixed; - top: 0; - left: ${(props) => (props.$isMenuOnRightSide ? "initial" : 0)}; - right: ${(props) => (props.$isMenuOnRightSide ? 0 : "initial")}; - border-radius: ${(props) => (props.$isMenuOnRightSide ? "8px 0 0 8px" : "0 8px 8px 0")}; - height: 100%; - width: 30%; - min-width: 350px; - background-color: ${(props) => (props.$isDarkTheme ? "#363636" : "white")}; - animation: ${(props) => - props.$isShow - ? props.$isMenuOnRightSide - ? slideInRtl - : slideIn - : props.$isMenuOnRightSide - ? slideOutRtl - : slideOut} - ${animationDuration}s forwards; + overflow-y: auto; + max-height: 65vh; + padding: var(--space-4); + background-color: var(--gray-2); + border-radius: 16px; + left: 50%; + width: min(420px, calc(100vw - 24px)); + + @media (max-width: 768px) { + width: calc(100vw - 16px); + } `; -const Background = styled.div` +const panelMotion = { + initial: { y: "100%", x: "-50%", opacity: 0 }, + animate: { y: 0, x: "-50%", opacity: 1 }, + exit: { y: "100%", x: "-50%", opacity: 0 }, +}; + +const backdropMotion = { + initial: { opacity: 0, backdropFilter: "blur(0px)" }, + animate: { opacity: 1, backdropFilter: "blur(8px)" }, + exit: { opacity: 0, backdropFilter: "blur(0px)" }, +}; + +const Background = styled(motion.div)` z-index: ${zIndex - 1}; position: fixed; top: 0; - left: ${(props) => (props.$isMenuOnRightSide ? "initial" : 0)}; - right: ${(props) => (props.$isMenuOnRightSide ? 0 : "initial")}; height: 100%; width: 100%; background-color: rgba(0, 0, 0, 0.8); - animation: ${(props) => (props.$isShow ? fadeIn : fadeOut)} ${animationDuration}s forwards; -`; - -const Draw = styled.div` - position: fixed; - cursor: pointer; - top: 40%; - width: 24px; - height: 100px; - background-color: ${(props) => (props.$isLightMenu ? "white" : "#363636")}; - color: ${(props) => (props.$isLightMenu ? "black" : "white")}; - border-radius: ${(props) => (props.$isMenuOnRightSide ? "8px 0 0 8px" : "0 8px 8px 0")}; - z-index: 10; - right: ${(props) => (props.$isMenuOnRightSide ? "0" : "initial")}; -`; -const Icon = styled(DotsVerticalIcon)` - margin-top: 38px; - width: 24px; - height: 24px; + will-change: opacity, backdrop-filter; + -webkit-backdrop-filter: blur(0px); + pointer-events: auto; `; interface OwnProps { preventCloseIdOrClassList?: string[]; + open?: boolean; + onOpenChange?: (open: boolean) => void; } type DrawerProps = React.PropsWithChildren; const Drawer: React.FC = (props) => { - const { isMenuOnRightSide, isLightMenu, isDarkTheme } = useAtomValue(pageSettingState); - const [isShowPanel, setIsShowPanel] = React.useState(false); - const [isAnimationUnmount, setIsAnimationUnmount] = React.useState(false); - const btnClickHandler = () => { - setIsShowPanel(true); - }; + const { open, onOpenChange, preventCloseIdOrClassList, children } = props; + const isControlled = open !== undefined && onOpenChange !== undefined; + const [internalIsShow, setInternalIsShow] = React.useState(false); + const isShowPanel = isControlled ? open : internalIsShow; + const closePanelHandler = (event: React.MouseEvent) => { const target = event.target as HTMLElement; + const targetClassName = typeof target.className === "string" ? target.className : ""; const shouldPrevent = - props.preventCloseIdOrClassList?.some( - (x) => x === target.id || target.className.includes(x) + preventCloseIdOrClassList?.some( + (x) => x === target.id || targetClassName.includes(x) ) ?? false; if (shouldPrevent) { return; } - setIsAnimationUnmount(true); - setTimeout(() => { - setIsShowPanel(false); - setIsAnimationUnmount(false); - }, animationDuration * 1000); + if (isControlled) { + onOpenChange(false); + } else { + setInternalIsShow(false); + } }; return ( - <> - - - - {isShowPanel ? ( - - - {props.children} - - - ) : null} - + + {isShowPanel && ( + <> + + + {children} + + + )} + ); }; diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index a287b574..f77c5438 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -1,14 +1,22 @@ -import { ReactElement } from "react"; -import { Link } from "react-router-dom"; +import React from "react"; +import { + GearIcon, + HomeIcon, + ExitIcon, + CardStackIcon, + ReaderIcon, + IdCardIcon, +} from "@radix-ui/react-icons"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import styled from "styled-components"; import Account from "../../service/model/Account"; import BalanceWithCurrency from "../bases/BalanceWithCurrency"; import Drawer from "../bases/Drawer"; import Switch from "../bases/Switch"; -import { Box, Container, Flex, Grid, Separator, Text } from "@radix-ui/themes"; -import React from "react"; +import { Box, Flex, Grid, Separator, Text } from "@radix-ui/themes"; import { useAtom } from "jotai"; import { isHideBalanceOnMenuState } from "../../common/shareState"; +import { color } from "../../common/constants"; interface MenuProps { accounts: Account[]; @@ -17,72 +25,105 @@ interface MenuProps { version: string; } +const BottomMenuWrapper = styled.div` + position: fixed; + left: 50%; + bottom: 10px; + transform: translateX(-50%); + width: min(420px, calc(100vw - 24px)); + z-index: 1000; + padding-bottom: env(safe-area-inset-bottom, 0px); + + @media (max-width: 768px) { + width: calc(100vw - 16px); + transform: translateX(-50%); + } +`; + +const BottomMenuBar = styled.div` + border-radius: 18px; + background: var(--gray-2); + border: none; + box-shadow: none; +`; + +const TabButton = styled.button` + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + background: transparent; + border: none; + font: inherit; + color: inherit; + cursor: pointer; + padding: 0; +`; + const Version = styled.div` - position: absolute; - bottom: 16px; - right: 0; - font-size: 0.5em; - margin-right: 5px; + font-size: 10px; + opacity: 0.7; `; -const SignOut = styled.a` + +const ActionText = styled.span` cursor: pointer; - color: inherit; `; -const hideBalanceSwitchId = "hideBalanceSwitch"; -const preventDrawerCloseIdList = ["rt-SwitchButton", "rt-SwitchThumb", hideBalanceSwitchId]; + +type ActiveTab = "none" | "home" | "account" | "settings"; const Menu: React.FC = (props) => { const [isHideBalance, setIsHideBalance] = useAtom(isHideBalanceOnMenuState); + const [activeTab, setActiveTab] = React.useState("none"); + const navigate = useNavigate(); + const location = useLocation(); + + React.useEffect(() => { + if (location.pathname === "/") { + setActiveTab("home"); + return; + } + + setActiveTab("none"); + }, [location.pathname]); + const onHideBalanceChangeHandler = () => { - setIsHideBalance((prevState) => { - const nextState = !prevState; + setIsHideBalance((prev) => !prev); + }; - return nextState; - }); + const navigateToHome = () => { + setActiveTab("home"); + navigate("/"); + }; + + const closeSheet = () => { + setActiveTab(location.pathname === "/" ? "home" : "none"); + }; + + const handleBottomDrawerOpenChange = (open: boolean) => { + if (!open) { + closeSheet(); + } + }; + + const handleDrawerTabClick = (tab: Extract) => { + setActiveTab((prev) => + prev === tab ? (location.pathname === "/" ? "home" : "none") : tab + ); }; - const getMenuTitle = (title: string) => ( - - - {title.toUpperCase()} - - - ); - const getMenuContent = (title: string, link: string) => ( - - - - - - - {title} - - - - ); - const getMenuContentWithChildren = (element: ReactElement) => ( - - - - - {element} - - ); return ( - - - - - - ACCOUNTS - - + <> + + + ACCOUNTS + + {props.accounts.map((x) => ( {x.name} - + = (props) => { = (props) => { isRtl /> - {getMenuTitle("page")} - {getMenuContent("Home", "/")} - {getMenuTitle("Setting")} - {getMenuContent("Account", "/account/setting")} - {getMenuContent("Category", "/category/setting")} - {getMenuContent("Page", "/page/setting")} - {getMenuTitle("Misc")} - {getMenuContentWithChildren( - Sign out - )} - - v{props.version} - + + + + SETTINGS + + + + + + Account + + + + + + Category + + + + + + Page + + + + + Sign out + + + v{props.version} + + + + + + + + + + + Home + + + handleDrawerTabClick("account")}> + + + Account + + + handleDrawerTabClick("settings")}> + + + Settings + + + + + + ); }; diff --git a/src/components/setting/PageSetting.tsx b/src/components/setting/PageSetting.tsx index bcd80e17..e9f2a95c 100644 --- a/src/components/setting/PageSetting.tsx +++ b/src/components/setting/PageSetting.tsx @@ -6,26 +6,6 @@ import AnimatedPage from "../AnimatedPage"; const PageSetting: React.FC = () => { const [pageSetting, setPageSetting] = useAtom(pageSettingState); - const onMoveMenuToRightSideChangeHandler = () => { - setPageSetting((prevState) => { - const nextState = { - ...prevState, - isMenuOnRightSide: !prevState.isMenuOnRightSide, - }; - - return nextState; - }); - }; - const onMenuColorChangeHandler = () => { - setPageSetting((prevState) => { - const nextState = { - ...prevState, - isLightMenu: !prevState.isLightMenu, - }; - - return nextState; - }); - }; const onThemeChangeHandler = () => { setPageSetting((prevState) => { const nextState = { @@ -40,29 +20,7 @@ const PageSetting: React.FC = () => { return ( - - Move menu to the right - - - - - Change menu to light color - - - - + Change to dark theme