diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx new file mode 100644 index 000000000..c45eca45c --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ApiKeyContent.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { Check, Copy, Shield } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { useTranslations } from "next-intl"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { useApiKey } from "@/hooks/use-api-key"; + +export function ApiKeyContent() { + const t = useTranslations("userSettings"); + const { apiKey, isLoading, copied, copyToClipboard } = useApiKey(); + + return ( + + + + + {t("api_key_warning_title")} + {t("api_key_warning_description")} + + +
+

{t("your_api_key")}

+ {isLoading ? ( +
+ ) : apiKey ? ( +
+
+ {apiKey} +
+ + + + + + {copied ? t("copied") : t("copy")} + + +
+ ) : ( +

{t("no_api_key")}

+ )} +
+ +
+

{t("usage_title")}

+

{t("usage_description")}

+
+						Authorization: Bearer {apiKey || "YOUR_API_KEY"}
+					
+
+ + + ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx new file mode 100644 index 000000000..a6cb11ecf --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/ProfileContent.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useAtomValue } from "jotai"; +import { AnimatePresence, motion } from "motion/react"; +import { useTranslations } from "next-intl"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { updateUserMutationAtom } from "@/atoms/user/user-mutation.atoms"; +import { currentUserAtom } from "@/atoms/user/user-query.atoms"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Spinner } from "@/components/ui/spinner"; + +function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) { + const [hasError, setHasError] = useState(false); + + useEffect(() => { + setHasError(false); + }, [url]); + + if (url && !hasError) { + return ( + Avatar setHasError(true)} + /> + ); + } + + return ( +
+ {fallback} +
+ ); +} + +export function ProfileContent() { + const t = useTranslations("userSettings"); + const { data: user, isLoading: isUserLoading } = useAtomValue(currentUserAtom); + const { mutateAsync: updateUser, isPending } = useAtomValue(updateUserMutationAtom); + + const [displayName, setDisplayName] = useState(""); + + useEffect(() => { + if (user) { + setDisplayName(user.display_name || ""); + } + }, [user]); + + const getInitials = (email: string) => { + const name = email.split("@")[0]; + return name.slice(0, 2).toUpperCase(); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + await updateUser({ + display_name: displayName || null, + }); + toast.success(t("profile_saved")); + } catch { + toast.error(t("profile_save_error")); + } + }; + + const hasChanges = displayName !== (user?.display_name || ""); + + return ( + + + {isUserLoading ? ( +
+ +
+ ) : ( +
+
+
+
+ + +
+ +
+ + setDisplayName(e.target.value)} + /> +

+ {t("profile_display_name_hint")} +

+
+ +
+ + +
+
+
+ +
+ +
+
+ )} +
+
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/page.tsx new file mode 100644 index 000000000..019684a95 --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/page.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { UserKey, User } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/animated-tabs"; +import { ApiKeyContent } from "./components/ApiKeyContent"; +import { ProfileContent } from "./components/ProfileContent"; + +export default function UserSettingsPage() { + const t = useTranslations("userSettings"); + + return ( +
+
+ + + + + {t("profile_nav_label")} + + + + {t("api_key_nav_label")} + + + + + + + + + +
+
+ ); +} diff --git a/surfsense_web/app/dashboard/user/settings/components/ApiKeyContent.tsx b/surfsense_web/app/dashboard/user/settings/components/ApiKeyContent.tsx deleted file mode 100644 index 6bf10a78f..000000000 --- a/surfsense_web/app/dashboard/user/settings/components/ApiKeyContent.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { Check, Copy, Key, Menu, Shield } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import { useTranslations } from "next-intl"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Button } from "@/components/ui/button"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { useApiKey } from "@/hooks/use-api-key"; - -interface ApiKeyContentProps { - onMenuClick: () => void; -} - -export function ApiKeyContent({ onMenuClick }: ApiKeyContentProps) { - const t = useTranslations("userSettings"); - const { apiKey, isLoading, copied, copyToClipboard } = useApiKey(); - - return ( - -
-
- - -
- - - - -
-

- {t("api_key_title")} -

-

{t("api_key_description")}

-
-
-
-
- - - - - - {t("api_key_warning_title")} - {t("api_key_warning_description")} - - -
-

{t("your_api_key")}

- {isLoading ? ( -
- ) : apiKey ? ( -
-
- {apiKey} -
- - - - - - {copied ? t("copied") : t("copy")} - - -
- ) : ( -

{t("no_api_key")}

- )} -
- -
-

{t("usage_title")}

-

{t("usage_description")}

-
-									Authorization: Bearer {apiKey || "YOUR_API_KEY"}
-								
-
- - -
-
- - ); -} diff --git a/surfsense_web/app/dashboard/user/settings/components/ProfileContent.tsx b/surfsense_web/app/dashboard/user/settings/components/ProfileContent.tsx deleted file mode 100644 index a1ff4d781..000000000 --- a/surfsense_web/app/dashboard/user/settings/components/ProfileContent.tsx +++ /dev/null @@ -1,182 +0,0 @@ -"use client"; - -import { useAtomValue } from "jotai"; -import { Menu, User } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import { useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { updateUserMutationAtom } from "@/atoms/user/user-mutation.atoms"; -import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Spinner } from "@/components/ui/spinner"; - -interface ProfileContentProps { - onMenuClick: () => void; -} - -function AvatarDisplay({ url, fallback }: { url?: string; fallback: string }) { - const [hasError, setHasError] = useState(false); - - useEffect(() => { - setHasError(false); - }, [url]); - - if (url && !hasError) { - return ( - Avatar setHasError(true)} - /> - ); - } - - return ( -
- {fallback} -
- ); -} - -export function ProfileContent({ onMenuClick }: ProfileContentProps) { - const t = useTranslations("userSettings"); - const { data: user, isLoading: isUserLoading } = useAtomValue(currentUserAtom); - const { mutateAsync: updateUser, isPending } = useAtomValue(updateUserMutationAtom); - - const [displayName, setDisplayName] = useState(""); - - useEffect(() => { - if (user) { - setDisplayName(user.display_name || ""); - } - }, [user]); - - const getInitials = (email: string) => { - const name = email.split("@")[0]; - return name.slice(0, 2).toUpperCase(); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - try { - await updateUser({ - display_name: displayName || null, - }); - toast.success(t("profile_saved")); - } catch { - toast.error(t("profile_save_error")); - } - }; - - const hasChanges = displayName !== (user?.display_name || ""); - - return ( - -
-
- - -
- - - - -
-

- {t("profile_title")} -

-

{t("profile_description")}

-
-
-
-
- - - - {isUserLoading ? ( -
- -
- ) : ( -
-
-
-
- - -
- -
- - setDisplayName(e.target.value)} - /> -

- {t("profile_display_name_hint")} -

-
- -
- - -
-
-
- -
- -
-
- )} -
-
-
-
-
- ); -} diff --git a/surfsense_web/app/dashboard/user/settings/components/UserSettingsSidebar.tsx b/surfsense_web/app/dashboard/user/settings/components/UserSettingsSidebar.tsx deleted file mode 100644 index 3424113a9..000000000 --- a/surfsense_web/app/dashboard/user/settings/components/UserSettingsSidebar.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import type { LucideIcon } from "lucide-react"; -import { ArrowLeft, ChevronRight, X } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import { useTranslations } from "next-intl"; -import { Button } from "@/components/ui/button"; -import { APP_VERSION } from "@/lib/env-config"; -import { cn } from "@/lib/utils"; - -export interface SettingsNavItem { - id: string; - label: string; - description: string; - icon: LucideIcon; -} - -interface UserSettingsSidebarProps { - activeSection: string; - onSectionChange: (section: string) => void; - onBackToApp: () => void; - isOpen: boolean; - onClose: () => void; - navItems: SettingsNavItem[]; -} - -export function UserSettingsSidebar({ - activeSection, - onSectionChange, - onBackToApp, - isOpen, - onClose, - navItems, -}: UserSettingsSidebarProps) { - const t = useTranslations("userSettings"); - - const handleNavClick = (sectionId: string) => { - onSectionChange(sectionId); - onClose(); - }; - - return ( - <> - - {isOpen && ( - - )} - - - - - ); -} diff --git a/surfsense_web/app/dashboard/user/settings/page.tsx b/surfsense_web/app/dashboard/user/settings/page.tsx deleted file mode 100644 index 8e04ce37a..000000000 --- a/surfsense_web/app/dashboard/user/settings/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import { Key, User } from "lucide-react"; -import { motion } from "motion/react"; -import { useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useState } from "react"; -import { ApiKeyContent } from "./components/ApiKeyContent"; -import { ProfileContent } from "./components/ProfileContent"; -import { type SettingsNavItem, UserSettingsSidebar } from "./components/UserSettingsSidebar"; - -export default function UserSettingsPage() { - const t = useTranslations("userSettings"); - const router = useRouter(); - const [activeSection, setActiveSection] = useState("profile"); - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - - const navItems: SettingsNavItem[] = [ - { - id: "profile", - label: t("profile_nav_label"), - description: t("profile_nav_description"), - icon: User, - }, - { - id: "api-key", - label: t("api_key_nav_label"), - description: t("api_key_nav_description"), - icon: Key, - }, - ]; - - const handleBackToApp = useCallback(() => { - router.back(); - }, [router]); - - return ( - -
-
- setIsSidebarOpen(false)} - navItems={navItems} - /> - {activeSection === "profile" && ( - setIsSidebarOpen(true)} /> - )} - {activeSection === "api-key" && ( - setIsSidebarOpen(true)} /> - )} -
-
-
- ); -} diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index c288aacb3..b0ff496f6 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -304,8 +304,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid }, []); const handleUserSettings = useCallback(() => { - router.push("/dashboard/user/settings"); - }, [router]); + router.push(`/dashboard/${searchSpaceId}/user-settings`); + }, [router, searchSpaceId]); const handleSearchSpaceSettings = useCallback( (space: SearchSpace) => { diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 29925a555..4c3615086 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -303,7 +303,7 @@ export function DocumentUploadTab({ {!isFileCountLimitReached && (