mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
chore(nova): ux improvments (#820)
This commit is contained in:
parent
7b64ce1e10
commit
7cfaacc806
9 changed files with 314 additions and 162 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
import { Logo } from "@ui/assets/Logo"
|
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 { useAuth } from "@lib/auth-context"
|
||||||
import { motion } from "motion/react"
|
import { motion } from "motion/react"
|
||||||
import NovaOrb from "@/components/nova/nova-orb"
|
import NovaOrb from "@/components/nova/nova-orb"
|
||||||
|
|
@ -14,6 +14,7 @@ import Support from "@/components/settings/support"
|
||||||
import { ErrorBoundary } from "@/components/error-boundary"
|
import { ErrorBoundary } from "@/components/error-boundary"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useIsMobile } from "@hooks/use-mobile"
|
import { useIsMobile } from "@hooks/use-mobile"
|
||||||
|
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
|
||||||
import { analytics } from "@/lib/analytics"
|
import { analytics } from "@/lib/analytics"
|
||||||
import { Sun } from "lucide-react"
|
import { Sun } from "lucide-react"
|
||||||
|
|
||||||
|
|
@ -137,6 +138,7 @@ export default function SettingsPage() {
|
||||||
const hasInitialized = useRef(false)
|
const hasInitialized = useRef(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
const localStorageUsername = useLocalStorageUsername()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasInitialized.current) return
|
if (hasInitialized.current) return
|
||||||
|
|
@ -163,28 +165,57 @@ export default function SettingsPage() {
|
||||||
window.addEventListener("hashchange", handleHashChange)
|
window.addEventListener("hashchange", handleHashChange)
|
||||||
return () => window.removeEventListener("hashchange", handleHashChange)
|
return () => window.removeEventListener("hashchange", handleHashChange)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const headerDisplayName =
|
||||||
|
user?.displayUsername || localStorageUsername || user?.name || ""
|
||||||
|
const headerPossessive = headerDisplayName
|
||||||
|
? `${headerDisplayName.split(" ")[0]}'s`
|
||||||
|
: ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col overflow-hidden">
|
<div className="h-screen flex flex-col overflow-hidden">
|
||||||
<header className="flex justify-between items-center px-4 md:px-6 py-3 shrink-0">
|
<header className="relative z-20 flex justify-between items-center gap-3 px-4 md:px-6 py-3 shrink-0">
|
||||||
<button
|
<nav
|
||||||
type="button"
|
className={cn(
|
||||||
onClick={() => router.push("/")}
|
"flex items-center gap-2 sm:gap-3 min-w-0 text-sm",
|
||||||
className="cursor-pointer"
|
dmSansClassName(),
|
||||||
>
|
|
||||||
<Logo className="h-7" />
|
|
||||||
</button>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{user && (
|
|
||||||
<Avatar className="border border-border h-8 w-8 md:h-10 md:w-10">
|
|
||||||
<AvatarImage src={user?.image ?? ""} />
|
|
||||||
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
aria-label="Breadcrumb"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center min-w-0 rounded-lg py-1 pr-2 -ml-1 pl-1",
|
||||||
|
"hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors cursor-pointer text-left",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Logo className="h-7 shrink-0" />
|
||||||
|
{headerDisplayName ? (
|
||||||
|
<div className="flex flex-col items-start justify-center ml-2 min-w-0">
|
||||||
|
<p className="text-[#8B8B8B] text-[11px] leading-tight">
|
||||||
|
{headerPossessive}
|
||||||
|
</p>
|
||||||
|
<p className="text-white font-bold text-xl leading-none -mt-1">
|
||||||
|
supermemory
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="ml-2 font-medium text-white/90">
|
||||||
|
supermemory
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="text-white/35 shrink-0" aria-hidden>
|
||||||
|
/
|
||||||
|
</span>
|
||||||
|
<span className="text-white/50 font-medium shrink-0">Settings</span>
|
||||||
|
</nav>
|
||||||
|
<UserProfileMenu />
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 min-h-0 overflow-y-auto md:overflow-hidden">
|
<main className="flex-1 min-h-0 overflow-y-auto md:overflow-hidden">
|
||||||
<div className="flex flex-col md:flex-row md:justify-center gap-4 md:gap-8 lg:gap-12 px-4 md:px-6 pt-4 pb-6 md:h-full">
|
<div className="flex flex-col md:flex-row md:justify-center gap-4 md:gap-8 lg:gap-12 px-4 md:px-6 pt-4 pb-6 md:h-full">
|
||||||
<div className="w-full md:w-auto md:min-w-[280px] shrink-0">
|
<div className="md:w-auto md:max-w-[380px] shrink-0">
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { useAuth } from "@lib/auth-context"
|
||||||
import { TextEditor } from "./text-editor"
|
import { TextEditor } from "./text-editor"
|
||||||
import { useProject } from "@/stores"
|
import { useProject } from "@/stores"
|
||||||
import { useQuickNoteDraft } from "@/stores/quick-note-draft"
|
import { useQuickNoteDraft } from "@/stores/quick-note-draft"
|
||||||
|
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
|
||||||
|
|
||||||
interface FullscreenNoteModalProps {
|
interface FullscreenNoteModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
|
@ -27,6 +28,7 @@ export function FullscreenNoteModal({
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
}: FullscreenNoteModalProps) {
|
}: FullscreenNoteModalProps) {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
const localStorageUsername = useLocalStorageUsername()
|
||||||
const { selectedProject } = useProject()
|
const { selectedProject } = useProject()
|
||||||
const { setDraft } = useQuickNoteDraft(selectedProject)
|
const { setDraft } = useQuickNoteDraft(selectedProject)
|
||||||
const [content, setContent] = useState(initialContent)
|
const [content, setContent] = useState(initialContent)
|
||||||
|
|
@ -43,10 +45,7 @@ export function FullscreenNoteModal({
|
||||||
}, [isOpen, initialContent])
|
}, [isOpen, initialContent])
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
user?.displayUsername ||
|
user?.displayUsername || localStorageUsername || user?.name || ""
|
||||||
(typeof window !== "undefined" && localStorage.getItem("username")) ||
|
|
||||||
(typeof window !== "undefined" && localStorage.getItem("userName")) ||
|
|
||||||
""
|
|
||||||
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
|
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Logo } from "@ui/assets/Logo"
|
import { Logo } from "@ui/assets/Logo"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
|
||||||
import { useAuth } from "@lib/auth-context"
|
import { useAuth } from "@lib/auth-context"
|
||||||
import {
|
import {
|
||||||
LayoutGridIcon,
|
LayoutGridIcon,
|
||||||
Plus,
|
Plus,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
LogOut,
|
|
||||||
Settings,
|
Settings,
|
||||||
Home,
|
Home,
|
||||||
Code2,
|
Code2,
|
||||||
Sun,
|
Sun,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
HelpCircle,
|
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
MessageCircleIcon,
|
MessageCircleIcon,
|
||||||
RotateCcw,
|
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { Button } from "@ui/components/button"
|
import { Button } from "@ui/components/button"
|
||||||
import { cn } from "@lib/utils"
|
import { cn } from "@lib/utils"
|
||||||
|
|
@ -30,13 +26,13 @@ import {
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@ui/components/dropdown-menu"
|
} from "@ui/components/dropdown-menu"
|
||||||
import { authClient } from "@lib/auth"
|
|
||||||
import { useProject } from "@/stores"
|
import { useProject } from "@/stores"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { SpaceSelector } from "./space-selector"
|
import { SpaceSelector } from "./space-selector"
|
||||||
import { useIsMobile } from "@hooks/use-mobile"
|
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 { FeedbackModal } from "./feedback-modal"
|
||||||
import { useViewMode } from "@/lib/view-mode-context"
|
import { useViewMode } from "@/lib/view-mode-context"
|
||||||
import { useQueryState } from "nuqs"
|
import { useQueryState } from "nuqs"
|
||||||
|
|
@ -53,7 +49,6 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
|
||||||
const { selectedProjects, setSelectedProjects } = useProject()
|
const { selectedProjects, setSelectedProjects } = useProject()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const { resetOrgOnboarded } = useOrgOnboarding()
|
|
||||||
const [feedbackOpen, setFeedbackOpen] = useQueryState(
|
const [feedbackOpen, setFeedbackOpen] = useQueryState(
|
||||||
"feedback",
|
"feedback",
|
||||||
feedbackParam,
|
feedbackParam,
|
||||||
|
|
@ -61,18 +56,11 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
|
||||||
const isFeedbackOpen = feedbackOpen ?? false
|
const isFeedbackOpen = feedbackOpen ?? false
|
||||||
const { viewMode, setViewMode } = useViewMode()
|
const { viewMode, setViewMode } = useViewMode()
|
||||||
|
|
||||||
const handleTryOnboarding = () => {
|
|
||||||
resetOrgOnboarded()
|
|
||||||
router.push("/onboarding?step=input&flow=welcome")
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFeedback = () => setFeedbackOpen(true)
|
const handleFeedback = () => setFeedbackOpen(true)
|
||||||
|
|
||||||
|
const localStorageUsername = useLocalStorageUsername()
|
||||||
const displayName =
|
const displayName =
|
||||||
user?.displayUsername ||
|
user?.displayUsername || localStorageUsername || user?.name || ""
|
||||||
(typeof window !== "undefined" && localStorage.getItem("username")) ||
|
|
||||||
(typeof window !== "undefined" && localStorage.getItem("userName")) ||
|
|
||||||
""
|
|
||||||
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
|
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
|
||||||
return (
|
return (
|
||||||
<div className="flex p-3 md:p-4 justify-between items-center gap-2">
|
<div className="flex p-3 md:p-4 justify-between items-center gap-2">
|
||||||
|
|
@ -316,96 +304,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{user && (
|
<UserProfileMenu />
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded-full cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-transform hover:scale-105"
|
|
||||||
>
|
|
||||||
<Avatar className="border border-[#2E3033] h-8 w-8 md:h-10 md:w-10">
|
|
||||||
<AvatarImage src={user?.image ?? ""} />
|
|
||||||
<AvatarFallback className="bg-[#0D121A] text-white">
|
|
||||||
{user?.name?.charAt(0)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="end"
|
|
||||||
className={cn(
|
|
||||||
"min-w-[220px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
|
||||||
dmSansClassName(),
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div id="user-info" className="px-3 py-2.5">
|
|
||||||
<p className="text-white text-sm font-medium truncate">
|
|
||||||
{user?.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-[#737373] text-xs truncate">{user?.email}</p>
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => 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 className="h-4 w-4 text-[#737373]" />
|
|
||||||
Settings
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={handleTryOnboarding}
|
|
||||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
|
||||||
>
|
|
||||||
<RotateCcw className="h-4 w-4 text-[#737373]" />
|
|
||||||
Restart Onboarding
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
|
||||||
<DropdownMenuItem
|
|
||||||
asChild
|
|
||||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
|
||||||
>
|
|
||||||
<a href="mailto:support@supermemory.com">
|
|
||||||
<HelpCircle className="h-4 w-4 text-[#737373]" />
|
|
||||||
Help & Support
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
asChild
|
|
||||||
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://supermemory.link/discord"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 text-[#737373]"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<title>Discord</title>
|
|
||||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
|
||||||
</svg>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
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 className="h-4 w-4 text-[#737373]" />
|
|
||||||
Logout
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<FeedbackModal
|
<FeedbackModal
|
||||||
isOpen={isFeedbackOpen}
|
isOpen={isFeedbackOpen}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,66 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
import { motion } from "motion/react"
|
import { motion } from "motion/react"
|
||||||
import { Logo } from "@ui/assets/Logo"
|
import { Logo } from "@ui/assets/Logo"
|
||||||
import { useAuth } from "@lib/auth-context"
|
import { useAuth } from "@lib/auth-context"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
import { UserProfileMenu } from "@/components/user-profile-menu"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { cn } from "@lib/utils"
|
||||||
|
import { dmSansClassName } from "@/lib/fonts"
|
||||||
|
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
|
||||||
|
|
||||||
export function SetupHeader() {
|
export function SetupHeader() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
const localStorageUsername = useLocalStorageUsername()
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
user?.displayUsername ||
|
user?.displayUsername || localStorageUsername || user?.name || ""
|
||||||
(typeof window !== "undefined" && localStorage.getItem("username")) ||
|
|
||||||
(typeof window !== "undefined" && localStorage.getItem("userName")) ||
|
|
||||||
""
|
|
||||||
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
|
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex p-6 justify-between items-center"
|
className="relative z-20 flex p-6 justify-between items-center"
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center z-10!">
|
<nav
|
||||||
<Logo className="h-7" />
|
className={cn(
|
||||||
{displayName && (
|
"flex items-center gap-2 sm:gap-3 min-w-0 z-10! text-sm",
|
||||||
<div className="flex flex-col items-start justify-center ml-2">
|
dmSansClassName(),
|
||||||
<p className="text-[#8B8B8B] text-[11px] leading-tight">
|
|
||||||
{userName}
|
|
||||||
</p>
|
|
||||||
<p className="text-white font-bold text-xl leading-none -mt-1">
|
|
||||||
supermemory
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
aria-label="Breadcrumb"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push("/")}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center min-w-0 rounded-lg py-1 pr-2 -ml-1 pl-1",
|
||||||
|
"hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors cursor-pointer text-left",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Logo className="h-7 shrink-0" />
|
||||||
|
{displayName ? (
|
||||||
|
<div className="flex flex-col items-start justify-center ml-2 min-w-0">
|
||||||
|
<p className="text-[#8B8B8B] text-[11px] leading-tight">
|
||||||
|
{userName}
|
||||||
|
</p>
|
||||||
|
<p className="text-white font-bold text-xl leading-none -mt-1">
|
||||||
|
supermemory
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="ml-2 font-medium text-white/90">supermemory</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="text-white/35 shrink-0" aria-hidden>
|
||||||
|
/
|
||||||
|
</span>
|
||||||
|
<span className="text-white/50 font-medium shrink-0">Setup</span>
|
||||||
|
</nav>
|
||||||
{user && (
|
{user && (
|
||||||
<Avatar className="border border-border h-8 w-8 md:h-10 md:w-10 z-10!">
|
<UserProfileMenu className="z-10" avatarClassName="border-border" />
|
||||||
<AvatarImage src={user?.image ?? ""} />
|
|
||||||
<AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import { XIcon, Download, Copy, Check } from "lucide-react"
|
import { XIcon, Download, Copy, Check } from "lucide-react"
|
||||||
import { GradientLogo } from "@ui/assets/Logo"
|
import { GradientLogo } from "@ui/assets/Logo"
|
||||||
import { useAuth } from "@lib/auth-context"
|
import { useAuth } from "@lib/auth-context"
|
||||||
|
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import * as htmlToImage from "html-to-image"
|
import * as htmlToImage from "html-to-image"
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ const XIcon2 = ({ className }: { className?: string }) => (
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M12.6 0.75H15.054L9.694 6.89L16 15.25H11.063L7.196 10.176L2.771 15.25H0.316L6.05 8.682L0 0.75H5.063L8.558 5.391L12.6 0.75ZM11.74 13.77H13.1L4.324 2.145H2.865L11.74 13.77Z"
|
d="M12.6 0.75H15.054L9.694 6.89L16 15.25H11.063L7.196 10.176L2.771 15.25H0.316L6.05 8.682L0 0.75H5.063L8.558 5.391L12.6 0.75ZM11.74 13.77H13.1L4.324 2.145H2.865L11.74 13.77Z"
|
||||||
|
|
@ -47,6 +49,7 @@ const LinkedInIcon = ({ className }: { className?: string }) => (
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M3.58065 4.89474H0V16H3.58065V4.89474ZM1.79032 0C0.801613 0 0 0.801613 0 1.79032C0 2.77903 0.801613 3.58065 1.79032 3.58065C2.77903 3.58065 3.58065 2.77903 3.58065 1.79032C3.58065 0.801613 2.77903 0 1.79032 0ZM8.71613 4.89474H5.33871V16H8.71613V10.0645C8.71613 8.46774 9.11774 6.93548 11.1613 6.93548C13.1774 6.93548 13.2097 8.75806 13.2097 10.1613V16H16V9.48387C16 6.74194 15.3871 4.64516 12.1935 4.64516C10.6452 4.64516 9.59677 5.48387 9.16129 6.27419H9.12903V4.89474H8.71613Z"
|
d="M3.58065 4.89474H0V16H3.58065V4.89474ZM1.79032 0C0.801613 0 0 0.801613 0 1.79032C0 2.77903 0.801613 3.58065 1.79032 3.58065C2.77903 3.58065 3.58065 2.77903 3.58065 1.79032C3.58065 0.801613 2.77903 0 1.79032 0ZM8.71613 4.89474H5.33871V16H8.71613V10.0645C8.71613 8.46774 9.11774 6.93548 11.1613 6.93548C13.1774 6.93548 13.2097 8.75806 13.2097 10.1613V16H16V9.48387C16 6.74194 15.3871 4.64516 12.1935 4.64516C10.6452 4.64516 9.59677 5.48387 9.16129 6.27419H9.12903V4.89474H8.71613Z"
|
||||||
|
|
@ -62,6 +65,7 @@ const InstagramIcon = ({ className }: { className?: string }) => (
|
||||||
viewBox="0 0 13 13"
|
viewBox="0 0 13 13"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
x="1.08333"
|
x="1.08333"
|
||||||
|
|
@ -284,11 +288,9 @@ export function ShareModal({
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const previewRef = useRef<HTMLDivElement>(null)
|
const previewRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const localStorageUsername = useLocalStorageUsername()
|
||||||
const displayName =
|
const displayName =
|
||||||
user?.displayUsername ||
|
user?.displayUsername || localStorageUsername || user?.name || ""
|
||||||
(typeof window !== "undefined" ? localStorage.getItem("username") : null) ||
|
|
||||||
(typeof window !== "undefined" ? localStorage.getItem("userName") : null) ||
|
|
||||||
""
|
|
||||||
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "Your"
|
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "Your"
|
||||||
|
|
||||||
const capturePreview = useCallback(async (): Promise<Blob | null> => {
|
const capturePreview = useCallback(async (): Promise<Blob | null> => {
|
||||||
|
|
|
||||||
141
apps/web/components/user-profile-menu.tsx
Normal file
141
apps/web/components/user-profile-menu.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"rounded-full cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-transform hover:scale-105",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
className={cn(
|
||||||
|
"border border-[#2E3033] h-8 w-8 md:h-10 md:w-10",
|
||||||
|
avatarClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AvatarImage src={user.image ?? ""} />
|
||||||
|
<AvatarFallback className="bg-[#0D121A] text-white">
|
||||||
|
{user.name?.charAt(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className={cn(
|
||||||
|
"min-w-[220px] p-1.5 rounded-xl border border-[#2E3033] shadow-[0px_1.5px_20px_0px_rgba(0,0,0,0.65)]",
|
||||||
|
dmSansClassName(),
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div id="user-info" className="px-3 py-2.5">
|
||||||
|
<p className="text-white text-sm font-medium truncate">{user.name}</p>
|
||||||
|
<p className="text-[#737373] text-xs truncate">{user.email}</p>
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => 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 className="h-4 w-4 text-[#737373]" />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleTryOnboarding}
|
||||||
|
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4 text-[#737373]" />
|
||||||
|
Restart Onboarding
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
||||||
|
<DropdownMenuItem
|
||||||
|
asChild
|
||||||
|
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||||
|
>
|
||||||
|
<a href="mailto:support@supermemory.com">
|
||||||
|
<HelpCircle className="h-4 w-4 text-[#737373]" />
|
||||||
|
Help & Support
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
asChild
|
||||||
|
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://supermemory.link/discord"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 text-[#737373]"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
|
</svg>
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator className="bg-[#2E3033]" />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={handleSignOut}
|
||||||
|
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 text-[#737373]" />
|
||||||
|
Logout
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -10,19 +10,47 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-placeholder-onboarding: #525966;
|
--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-thin {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #293952 transparent;
|
scrollbar-color: var(--sm-scrollbar-thumb) transparent;
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm-tweet-theme .react-tweet-theme {
|
.sm-tweet-theme .react-tweet-theme {
|
||||||
|
|
|
||||||
10
biome.json
10
biome.json
|
|
@ -100,6 +100,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["**/globals.css"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"complexity": {
|
||||||
|
"noImportantStyles": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"vcs": {
|
"vcs": {
|
||||||
|
|
|
||||||
19
packages/hooks/use-local-storage-username.ts
Normal file
19
packages/hooks/use-local-storage-username.ts
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue