From 47a96a9c3f70d21e7ce4489b1306e380ae8393f2 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Mon, 25 Sep 2023 17:31:51 -0600 Subject: [PATCH] mobile page menu --- .../choose-username/ChooseUsername.tsx | 2 - .../layout/RootLayout/DesktopUserControls.tsx | 32 ++++ .../components/layout/RootLayout/PageMenu.tsx | 156 ++++++++++++++---- .../layout/RootLayout/PageMenuLink.tsx | 34 +++- .../layout/RootLayout/UserControls.tsx | 106 ------------ .../layout/RootLayout/UserControlsMenu.tsx | 64 +++++++ .../components/layout/RootLayout/index.tsx | 8 +- .../RootLayout/useForceChooseUsername.ts | 17 ++ packages/hub/src/hooks/useUsername.ts | 6 + 9 files changed, 282 insertions(+), 143 deletions(-) create mode 100644 packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx delete mode 100644 packages/hub/src/components/layout/RootLayout/UserControls.tsx create mode 100644 packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx create mode 100644 packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts create mode 100644 packages/hub/src/hooks/useUsername.ts diff --git a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx index 22d7ddaaca..d2dbe35474 100644 --- a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx +++ b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx @@ -26,8 +26,6 @@ const Mutation = graphql` `; export const ChooseUsername: FC = () => { - const toast = useToast(); - type FormShape = { username: string; }; diff --git a/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx b/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx new file mode 100644 index 0000000000..ac80d16ff6 --- /dev/null +++ b/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx @@ -0,0 +1,32 @@ +import { signIn, useSession } from "next-auth/react"; +import { FC } from "react"; + +import { Button, Dropdown, DropdownMenu } from "@quri/ui"; + +import { DropdownWithArrow } from "./DropdownWithArrow"; +import { UserControlsMenu } from "./UserControlsMenu"; +import { useUsername } from "@/hooks/useUsername"; + +export const DesktopUserControls: FC = () => { + const username = useUsername(); + + return username ? ( +
+ ( + + + + )} + > + + +
+ ) : ( + + ); +}; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 6aa7489a09..c7801269e2 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -1,44 +1,144 @@ -import { FC } from "react"; -import { useSession } from "next-auth/react"; +import { signIn, useSession } from "next-auth/react"; import Link from "next/link"; +import { FC, useState } from "react"; +import { FaPlus } from "react-icons/fa"; -import { BookOpenIcon, CodeBracketIcon } from "@quri/ui"; +import { + BookOpenIcon, + DotsHorizontalIcon, + DropdownMenu, + DropdownMenuAsyncActionItem, + DropdownMenuHeader, + DropdownMenuSeparator, + SignOutIcon, + UserCircleIcon, +} from "@quri/ui"; -import { UserControls } from "./UserControls"; -import { PageMenuLink } from "./PageMenuLink"; -import { aboutRoute, newModelRoute } from "@/routes"; +import { useUsername } from "@/hooks/useUsername"; import { SQUIGGLE_DOCS_URL } from "@/lib/common"; -import { FaPlus } from "react-icons/fa"; +import { aboutRoute, newModelRoute } from "@/routes"; +import { DesktopUserControls } from "./DesktopUserControls"; +import { MenuLinkModeProps, PageMenuLink } from "./PageMenuLink"; +import { UserControlsMenu } from "./UserControlsMenu"; +import { useForceChooseUsername } from "./useForceChooseUsername"; -export const PageMenu: FC = () => { +const AboutMenuLink: FC = (props) => { const { data: session } = useSession(); + if (session) { + return null; + } + return ; +}; + +const DocsMenuLink: FC = (props) => ( + +); + +const NewModelMenuLink: FC = (props) => { + const { data: session } = useSession(); + if (!session) { + return null; + } + return ( + + ); +}; + +const DesktopMenu: FC = () => { + return ( +
+ + + + +
+ ); +}; + +const MobileMenu: FC = () => { + const username = useUsername(); + const [open, setOpen] = useState(false); + + const Icon = username ? UserCircleIcon : DotsHorizontalIcon; + + const close = () => setOpen(false); + return ( +
+
+ setOpen(true)} + /> +
+ {open && ( + <> + {/* overlay */} +
+ {/* sidebar panel */} +
+ + Menu + + + + + + {username ? ( + + ) : ( + <> + User Actions + + + + )} + +
+ + )} +
+ ); +}; + +export const PageMenu: FC = () => { + useForceChooseUsername(); return (
- + Squiggle Hub
-
- {!session && } - - {session && ( - - )} - +
+ +
+
+
); diff --git a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx index 0086019701..c5e39aea69 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx @@ -2,15 +2,36 @@ import Link from "next/link"; import { FC } from "react"; import { IconProps } from "@/relative-values/components/ui/icons/Icon"; +import { DropdownMenuLinkItem } from "@/components/ui/DropdownMenuLinkItem"; +import { EmptyIcon } from "@quri/ui"; -export const PageMenuLink: FC<{ +export type MenuLinkModeProps = + | { + mode: "desktop"; + close?: undefined; + } + | { + mode: "mobile"; + close: () => void; + }; + +type Props = { title: string; href: string; icon?: FC; external?: boolean; -}> = ({ title, icon, href, external }) => { +} & MenuLinkModeProps; + +export const PageMenuLink: FC = ({ + mode, + close, + title, + icon, + href, + external, +}) => { const Icon = icon; - return ( + return mode === "desktop" ? ( } {title} + ) : ( + ); }; diff --git a/packages/hub/src/components/layout/RootLayout/UserControls.tsx b/packages/hub/src/components/layout/RootLayout/UserControls.tsx deleted file mode 100644 index e7e3224801..0000000000 --- a/packages/hub/src/components/layout/RootLayout/UserControls.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Session } from "next-auth"; -import { signIn, signOut } from "next-auth/react"; -import { FC } from "react"; -import { newDefinitionRoute, newGroupRoute } from "@/routes"; -import Link from "next/link"; -import { IconProps } from "@/relative-values/components/ui/icons/Icon"; - -import { - Button, - Dropdown, - DropdownMenu, - DropdownMenuActionItem, - DropdownMenuHeader, - ActionItemInternal, - DropdownMenuSeparator, - SignOutIcon, - UserCircleIcon, - ScaleIcon, - GroupIcon, -} from "@quri/ui"; - -import { chooseUsernameRoute, userRoute } from "@/routes"; -import { DropdownWithArrow } from "./DropdownWithArrow"; -import { DropdownMenuLinkItem } from "@/components/ui/DropdownMenuLinkItem"; -import { - DISCORD_URL, - GITHUB_DISCUSSION_URL, - NEWSLETTER_URL, -} from "@/lib/common"; - -export const MenuLink: FC<{ - title: string; - icon?: FC; - href: string; - external?: boolean; - close: () => void; -}> = ({ title, icon, href, external, close }) => { - return ( - close()} - > - - - ); -}; - -export const UserControls: FC<{ session: Session | null }> = ({ session }) => { - if ( - session?.user && - !session?.user.username && - !window.location.href.includes(chooseUsernameRoute()) - ) { - // Next's redirect() is broken for components included from the root layout - // https://github.com/vercel/next.js/issues/42556 (it's closed but not really solved) - window.location.href = chooseUsernameRoute(); - } - const { username } = session?.user || { username: undefined }; - - return !!username ? ( -
- void }) => ( - - User Actions - - - { - signOut(); - close(); - }} - icon={SignOutIcon} - title="Sign Out" - /> - - Experimental - - - - - )} - > - - -
- ) : ( - - ); -}; diff --git a/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx b/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx new file mode 100644 index 0000000000..1076f4db63 --- /dev/null +++ b/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx @@ -0,0 +1,64 @@ +import { signOut } from "next-auth/react"; +import { FC } from "react"; + +import { + DropdownMenu, + DropdownMenuHeader, + DropdownMenuSeparator, + UserCircleIcon, + DropdownMenuActionItem, + SignOutIcon, + ScaleIcon, + GroupIcon, +} from "@quri/ui"; + +import { DropdownMenuLinkItem } from "@/components/ui/DropdownMenuLinkItem"; +import { userRoute, newDefinitionRoute, newGroupRoute } from "@/routes"; + +type Props = { + close: () => void; + username: string; + mode: "desktop" | "mobile"; +}; + +// this component is shared between DesktopUserControls, which is a normal Dropdown, and MobileUserControls, which is a fake dropdown. +// In both cases, it should be wrapped in DropdownMenu. +export const UserControlsMenu: FC = ({ close, username, mode }) => { + return ( + <> + + {mode === "desktop" ? "User Actions" : `@${username}`} + + + + { + signOut(); + close(); + }} + icon={SignOutIcon} + title="Sign Out" + /> + + Experimental + + + + + ); +}; diff --git a/packages/hub/src/components/layout/RootLayout/index.tsx b/packages/hub/src/components/layout/RootLayout/index.tsx index f8cc79e4c2..4ffe9f3691 100644 --- a/packages/hub/src/components/layout/RootLayout/index.tsx +++ b/packages/hub/src/components/layout/RootLayout/index.tsx @@ -1,10 +1,10 @@ +import { clsx } from "clsx"; import { FC, PropsWithChildren } from "react"; -import { PageMenu } from "./PageMenu"; -import { PageFooter } from "./PageFooter"; -import { usePathname } from "next/navigation"; import { isModelRoute, isModelSubroute } from "@/routes"; -import { clsx } from "clsx"; +import { usePathname } from "next/navigation"; +import { PageFooter } from "./PageFooter"; +import { PageMenu } from "./PageMenu"; export const RootLayout: FC = ({ children }) => { const pathname = usePathname(); diff --git a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts new file mode 100644 index 0000000000..ec90ad14cc --- /dev/null +++ b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts @@ -0,0 +1,17 @@ +import { useSession } from "next-auth/react"; + +import { chooseUsernameRoute } from "@/routes"; + +export function useForceChooseUsername() { + const { data: session } = useSession(); + + if ( + session?.user && + !session?.user.username && + !window.location.href.includes(chooseUsernameRoute()) + ) { + // Next's redirect() is broken for components included from the root layout + // https://github.com/vercel/next.js/issues/42556 (it's closed but not really solved) + window.location.href = chooseUsernameRoute(); + } +} diff --git a/packages/hub/src/hooks/useUsername.ts b/packages/hub/src/hooks/useUsername.ts new file mode 100644 index 0000000000..6efb10083b --- /dev/null +++ b/packages/hub/src/hooks/useUsername.ts @@ -0,0 +1,6 @@ +import { useSession } from "next-auth/react"; + +export function useUsername(): string | undefined { + const { data: session } = useSession(); + return session?.user?.username; +}