diff --git a/apps/web/app/(app)/settings/page.tsx b/apps/web/app/(app)/settings/page.tsx index 26e646fc..9d47b838 100644 --- a/apps/web/app/(app)/settings/page.tsx +++ b/apps/web/app/(app)/settings/page.tsx @@ -1,6 +1,6 @@ "use client" import { Logo } from "@ui/assets/Logo" -import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" +import { UserProfileMenu } from "@/components/user-profile-menu" import { useAuth } from "@lib/auth-context" import { motion } from "motion/react" import NovaOrb from "@/components/nova/nova-orb" @@ -14,6 +14,7 @@ import Support from "@/components/settings/support" import { ErrorBoundary } from "@/components/error-boundary" import { useRouter } from "next/navigation" import { useIsMobile } from "@hooks/use-mobile" +import { useLocalStorageUsername } from "@hooks/use-local-storage-username" import { analytics } from "@/lib/analytics" import { Sun } from "lucide-react" @@ -137,6 +138,7 @@ export default function SettingsPage() { const hasInitialized = useRef(false) const router = useRouter() const isMobile = useIsMobile() + const localStorageUsername = useLocalStorageUsername() useEffect(() => { if (hasInitialized.current) return @@ -163,28 +165,57 @@ export default function SettingsPage() { window.addEventListener("hashchange", handleHashChange) return () => window.removeEventListener("hashchange", handleHashChange) }, []) + + const headerDisplayName = + user?.displayUsername || localStorageUsername || user?.name || "" + const headerPossessive = headerDisplayName + ? `${headerDisplayName.split(" ")[0]}'s` + : "" + return (
-
- -
- {user && ( - - - {user?.name?.charAt(0)} - +
+ +
-
+
{!isMobile && ( { diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index a94d7e2d..38c5c68c 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -1,22 +1,18 @@ "use client" import { Logo } from "@ui/assets/Logo" -import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" import { useAuth } from "@lib/auth-context" import { LayoutGridIcon, Plus, SearchIcon, - LogOut, Settings, Home, Code2, Sun, ExternalLink, - HelpCircle, MenuIcon, MessageCircleIcon, - RotateCcw, } from "lucide-react" import { Button } from "@ui/components/button" import { cn } from "@lib/utils" @@ -30,13 +26,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@ui/components/dropdown-menu" -import { authClient } from "@lib/auth" import { useProject } from "@/stores" import { useRouter } from "next/navigation" import Link from "next/link" import { SpaceSelector } from "./space-selector" import { useIsMobile } from "@hooks/use-mobile" -import { useOrgOnboarding } from "@hooks/use-org-onboarding" +import { useLocalStorageUsername } from "@hooks/use-local-storage-username" +import { UserProfileMenu } from "@/components/user-profile-menu" import { FeedbackModal } from "./feedback-modal" import { useViewMode } from "@/lib/view-mode-context" import { useQueryState } from "nuqs" @@ -53,7 +49,6 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) { const { selectedProjects, setSelectedProjects } = useProject() const router = useRouter() const isMobile = useIsMobile() - const { resetOrgOnboarded } = useOrgOnboarding() const [feedbackOpen, setFeedbackOpen] = useQueryState( "feedback", feedbackParam, @@ -61,18 +56,11 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) { const isFeedbackOpen = feedbackOpen ?? false const { viewMode, setViewMode } = useViewMode() - const handleTryOnboarding = () => { - resetOrgOnboarded() - router.push("/onboarding?step=input&flow=welcome") - } - const handleFeedback = () => setFeedbackOpen(true) + const localStorageUsername = useLocalStorageUsername() const displayName = - user?.displayUsername || - (typeof window !== "undefined" && localStorage.getItem("username")) || - (typeof window !== "undefined" && localStorage.getItem("userName")) || - "" + user?.displayUsername || localStorageUsername || user?.name || "" const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My" return (
@@ -316,96 +304,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) { )} - {user && ( - - - - - -
-

- {user?.name} -

-

{user?.email}

-
- - router.push("/settings")} - className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2" - > - - Settings - - - - Restart Onboarding - - - - - - Help & Support - - - - - - Discord - - - Discord - - - - { - authClient.signOut() - router.push("/login/new") - }} - className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2" - > - - Logout - -
-
- )} +
-
- - {displayName && ( -
-

- {userName} -

-

- supermemory -

-
+ {user && ( - - - {user?.name?.charAt(0)} - + )} ) diff --git a/apps/web/components/share-modal.tsx b/apps/web/components/share-modal.tsx index 4f09c7e5..f565b56b 100644 --- a/apps/web/components/share-modal.tsx +++ b/apps/web/components/share-modal.tsx @@ -14,6 +14,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog" import { XIcon, Download, Copy, Check } from "lucide-react" import { GradientLogo } from "@ui/assets/Logo" import { useAuth } from "@lib/auth-context" +import { useLocalStorageUsername } from "@hooks/use-local-storage-username" import { toast } from "sonner" import * as htmlToImage from "html-to-image" @@ -32,6 +33,7 @@ const XIcon2 = ({ className }: { className?: string }) => ( viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" > ( viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" > ( viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" > (null) + const localStorageUsername = useLocalStorageUsername() const displayName = - user?.displayUsername || - (typeof window !== "undefined" ? localStorage.getItem("username") : null) || - (typeof window !== "undefined" ? localStorage.getItem("userName") : null) || - "" + user?.displayUsername || localStorageUsername || user?.name || "" const userName = displayName ? `${displayName.split(" ")[0]}'s` : "Your" const capturePreview = useCallback(async (): Promise => { diff --git a/apps/web/components/user-profile-menu.tsx b/apps/web/components/user-profile-menu.tsx new file mode 100644 index 00000000..6ffbf108 --- /dev/null +++ b/apps/web/components/user-profile-menu.tsx @@ -0,0 +1,141 @@ +"use client" + +import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" +import { useAuth } from "@lib/auth-context" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@ui/components/dropdown-menu" +import { authClient } from "@lib/auth" +import { useRouter } from "next/navigation" +import { LogOut, Settings, RotateCcw, HelpCircle } from "lucide-react" +import { cn } from "@lib/utils" +import { dmSansClassName } from "@/lib/fonts" +import { useOrgOnboarding } from "@hooks/use-org-onboarding" + +export function UserProfileMenu({ + className, + avatarClassName, +}: { + className?: string + avatarClassName?: string +}) { + const { user } = useAuth() + const router = useRouter() + const { resetOrgOnboarded } = useOrgOnboarding() + + const handleTryOnboarding = () => { + resetOrgOnboarded() + router.push("/onboarding?step=input&flow=welcome") + } + + const handleSignOut = () => { + void (async () => { + try { + await authClient.signOut() + } catch { + // still navigate if the request fails (offline, etc.) + } + router.push("/login/new") + })() + } + + if (!user) return null + + return ( + + + + + +
+

{user.name}

+

{user.email}

+
+ + router.push("/settings")} + className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2" + > + + Settings + + + + Restart Onboarding + + + + + + Help & Support + + + + + + Discord + + + + + + Logout + +
+
+ ) +} diff --git a/apps/web/globals.css b/apps/web/globals.css index e1adf870..f0597bfb 100644 --- a/apps/web/globals.css +++ b/apps/web/globals.css @@ -10,19 +10,47 @@ :root { --color-placeholder-onboarding: #525966; + --sm-scrollbar-thumb: rgb(41 57 82 / 0.55); + --sm-scrollbar-thumb-hover: rgb(55 90 130 / 0.75); + --sm-scrollbar-thumb-active: rgb(70 110 160 / 0.85); +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--sm-scrollbar-thumb) transparent; +} + +*::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: var(--sm-scrollbar-thumb); + border-radius: 100px; + border: 2px solid transparent; + background-clip: padding-box; +} + +*::-webkit-scrollbar-thumb:hover { + background-color: var(--sm-scrollbar-thumb-hover); +} + +*::-webkit-scrollbar-thumb:active { + background-color: var(--sm-scrollbar-thumb-active); +} + +*::-webkit-scrollbar-corner { + background: transparent; } .scrollbar-thin { scrollbar-width: thin; - scrollbar-color: #293952 transparent; -} - -.scrollbar-thin::-webkit-scrollbar { - width: 6px; -} - -.scrollbar-thin::-webkit-scrollbar-track { - background: transparent; + scrollbar-color: var(--sm-scrollbar-thumb) transparent; } .sm-tweet-theme .react-tweet-theme { diff --git a/biome.json b/biome.json index c4e233d6..27f83933 100644 --- a/biome.json +++ b/biome.json @@ -100,6 +100,16 @@ } } } + }, + { + "includes": ["**/globals.css"], + "linter": { + "rules": { + "complexity": { + "noImportantStyles": "off" + } + } + } } ], "vcs": { diff --git a/packages/hooks/use-local-storage-username.ts b/packages/hooks/use-local-storage-username.ts new file mode 100644 index 00000000..43743fd3 --- /dev/null +++ b/packages/hooks/use-local-storage-username.ts @@ -0,0 +1,19 @@ +import * as React from "react" + +/** + * `username` / `userName` keys written by the app. Populated only after mount so + * server HTML and the first client render stay aligned (no hydration mismatch). + */ +export function useLocalStorageUsername(): string { + const [value, setValue] = React.useState("") + + React.useEffect(() => { + setValue( + localStorage.getItem("username") ?? + localStorage.getItem("userName") ?? + "", + ) + }, []) + + return value +}