diff --git a/frontend/package.json b/frontend/package.json index e9e7e1c..fe3787a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,9 +17,11 @@ "@codemirror/lang-html": "^6.4.6", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", diff --git a/frontend/src/components/hosted/AppContainer.tsx b/frontend/src/components/hosted/AppContainer.tsx index 377574b..839bf10 100644 --- a/frontend/src/components/hosted/AppContainer.tsx +++ b/frontend/src/components/hosted/AppContainer.tsx @@ -1,4 +1,4 @@ -import { SignUp, UserButton, useUser } from "@clerk/clerk-react"; +import { SignUp, useUser } from "@clerk/clerk-react"; import App from "../../App"; import { useEffect, useRef, useState } from "react"; import { AlertDialog } from "@radix-ui/react-alert-dialog"; @@ -6,6 +6,7 @@ import { AlertDialogContent } from "../ui/alert-dialog"; import FullPageSpinner from "../custom-ui/FullPageSpinner"; import { useAuthenticatedFetch } from "./useAuthenticatedFetch"; import { useStore } from "../../store/store"; +import AvatarDropdown from "./AvatarDropdown"; function AppContainer() { const [showPopup, setShowPopup] = useState(false); @@ -32,6 +33,7 @@ function AppContainer() { if (isInitRequestInProgress.current) return; isInitRequestInProgress.current = true; + // TODO: Handle when the user is not signed in const user = await authenticatedFetch( "https://screenshot-to-code-saas.onrender.com/users/create", // "http://localhost:8001/users/create", @@ -44,8 +46,6 @@ function AppContainer() { setSubscriberTier(user.subscriber_tier); } - console.log(user); - isInitRequestInProgress.current = false; }; @@ -60,7 +60,7 @@ function AppContainer() { - + } /> diff --git a/frontend/src/components/hosted/AvatarDropdown.tsx b/frontend/src/components/hosted/AvatarDropdown.tsx new file mode 100644 index 0000000..65dda6f --- /dev/null +++ b/frontend/src/components/hosted/AvatarDropdown.tsx @@ -0,0 +1,78 @@ +import { useClerk, useUser } from "@clerk/clerk-react"; +import { Avatar, AvatarImage, AvatarFallback } from "../ui/avatar"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuLabel, +} from "../ui/dropdown-menu"; +import { useStore } from "../../store/store"; +import { capitalize } from "./utils"; + +export default function AvatarDropdown() { + const { user, isLoaded, isSignedIn } = useUser(); + const { signOut } = useClerk(); + const subscriberTier = useStore((state) => state.subscriberTier); + const isFreeUser = subscriberTier === "free"; + + // If Clerk is still loading or user is logged out, don't show anything + if (!isLoaded || !isSignedIn) return null; + + return ( + <> + + + + + {user?.firstName} + + + + {/* Free users */} + {isFreeUser && ( + + + Buy credits + + + )} + {/* Paying user */} + {!isFreeUser && ( + <> + + {capitalize(subscriberTier) + " Subscriber"} + + + + + Email support + + + + + Upgrade plan + + + + + Cancel subscription + + + + )} + {/* All users */} + + signOut()}>Log out + + + + ); +} diff --git a/frontend/src/components/hosted/utils.ts b/frontend/src/components/hosted/utils.ts new file mode 100644 index 0000000..9fbf352 --- /dev/null +++ b/frontend/src/components/hosted/utils.ts @@ -0,0 +1,6 @@ +export function capitalize(str: string): string { + if (str.length === 0) { + return str; + } + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +} diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..991f56e --- /dev/null +++ b/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0e4dccf --- /dev/null +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,203 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { + CheckIcon, + ChevronRightIcon, + DotFilledIcon, +} from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 5068854..678fe44 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -814,6 +814,17 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-avatar@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" + integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-checkbox@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz#98f22c38d5010dd6df4c5744cac74087e3275f4b" @@ -909,6 +920,20 @@ "@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-escape-keydown" "1.0.3" +"@radix-ui/react-dropdown-menu@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63" + integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-menu" "2.0.6" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-focus-guards@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" @@ -963,6 +988,31 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-menu@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e" + integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-popover@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"