Merge pull request #195 from MODSetter/dev

feat(FRONTEND): Moved User Dropdown to Search Space Dashboard
This commit is contained in:
Rohan Verma 2025-07-17 16:46:36 +05:30 committed by GitHub
commit 5cc8373205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 169 additions and 98 deletions

View file

@ -1,14 +1,15 @@
"use client"; "use client";
import React from 'react' import React, { useEffect, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Plus, Search, Trash2, AlertCircle, Loader2, LogOut } from 'lucide-react' import { Plus, Search, Trash2, AlertCircle, Loader2 } from 'lucide-react'
import { Tilt } from '@/components/ui/tilt' import { Tilt } from '@/components/ui/tilt'
import { Spotlight } from '@/components/ui/spotlight' import { Spotlight } from '@/components/ui/spotlight'
import { Logo } from '@/components/Logo'; import { Logo } from '@/components/Logo';
import { ThemeTogglerComponent } from '@/components/theme/theme-toggle'; import { ThemeTogglerComponent } from '@/components/theme/theme-toggle';
import { UserDropdown } from '@/components/UserDropdown';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
AlertDialog, AlertDialog,
@ -28,8 +29,17 @@ import {
} from "@/components/ui/alert"; } from "@/components/ui/alert";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { useSearchSpaces } from '@/hooks/use-search-spaces'; import { useSearchSpaces } from '@/hooks/use-search-spaces';
import { apiClient } from '@/lib/api';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
interface User {
id: string;
email: string;
is_active: boolean;
is_superuser: boolean;
is_verified: boolean;
}
/** /**
* Formats a date string into a readable format * Formats a date string into a readable format
* @param dateString - The date string to format * @param dateString - The date string to format
@ -147,17 +157,47 @@ const DashboardPage = () => {
const router = useRouter(); const router = useRouter();
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces(); const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
// User state management
const [user, setUser] = useState<User | null>(null);
const [isLoadingUser, setIsLoadingUser] = useState(true);
const [userError, setUserError] = useState<string | null>(null);
// Fetch user details
useEffect(() => {
const fetchUser = async () => {
try {
if (typeof window === 'undefined') return;
try {
const userData = await apiClient.get<User>('users/me');
setUser(userData);
setUserError(null);
} catch (error) {
console.error('Error fetching user:', error);
setUserError(error instanceof Error ? error.message : 'Unknown error occurred');
} finally {
setIsLoadingUser(false);
}
} catch (error) {
console.error('Error in fetchUser:', error);
setIsLoadingUser(false);
}
};
fetchUser();
}, []);
// Create user object for UserDropdown
const customUser = {
name: user?.email ? user.email.split('@')[0] : 'User',
email: user?.email || (isLoadingUser ? 'Loading...' : userError ? 'Error loading user' : 'Unknown User'),
avatar: '/icon-128.png', // Default avatar
};
if (loading) return <LoadingScreen />; if (loading) return <LoadingScreen />;
if (error) return <ErrorScreen message={error} />; if (error) return <ErrorScreen message={error} />;
const handleLogout = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('surfsense_bearer_token');
router.push('/');
}
};
const handleDeleteSearchSpace = async (id: number) => { const handleDeleteSearchSpace = async (id: number) => {
// Send DELETE request to the API // Send DELETE request to the API
try { try {
@ -201,18 +241,10 @@ const DashboardPage = () => {
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<Button <UserDropdown user={customUser} />
variant="ghost" <ThemeTogglerComponent />
size="icon" </div>
onClick={handleLogout}
className="h-9 w-9 rounded-full"
aria-label="Logout"
>
<LogOut className="h-5 w-5" />
</Button>
<ThemeTogglerComponent />
</div>
</div> </div>
<div className="flex flex-col space-y-6 mt-6"> <div className="flex flex-col space-y-6 mt-6">

View file

@ -0,0 +1,92 @@
"use client"
import {
BadgeCheck,
ChevronsUpDown,
LogOut,
Settings,
} from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
import { useRouter, useParams } from "next/navigation"
export function UserDropdown({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const router = useRouter()
const handleLogout = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('surfsense_bearer_token');
router.push('/');
}
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="relative h-10 w-10 rounded-full"
>
<Avatar className="h-8 w-8">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>{user.name.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56"
align="end"
forceMount
>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{user.name}</p>
<p className="text-xs leading-none text-muted-foreground">
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => router.push(`/dashboard/api-key`)}>
<BadgeCheck className="mr-2 h-4 w-4" />
API Key
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push(`/settings`)}>
<Settings className="mr-2 h-4 w-4" />
Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View file

@ -31,14 +31,6 @@ interface SearchSpace {
user_id: string; user_id: string;
} }
interface User {
id: string;
email: string;
is_active: boolean;
is_superuser: boolean;
is_verified: boolean;
}
interface AppSidebarProviderProps { interface AppSidebarProviderProps {
searchSpaceId: string; searchSpaceId: string;
navSecondary: { navSecondary: {
@ -58,20 +50,17 @@ interface AppSidebarProviderProps {
}[]; }[];
} }
export function AppSidebarProvider({ export function AppSidebarProvider({
searchSpaceId, searchSpaceId,
navSecondary, navSecondary,
navMain navMain
}: AppSidebarProviderProps) { }: AppSidebarProviderProps) {
const [recentChats, setRecentChats] = useState<{ name: string; url: string; icon: string; id: number; search_space_id: number; actions: { name: string; icon: string; onClick: () => void }[] }[]>([]); const [recentChats, setRecentChats] = useState<{ name: string; url: string; icon: string; id: number; search_space_id: number; actions: { name: string; icon: string; onClick: () => void }[] }[]>([]);
const [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null); const [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null);
const [user, setUser] = useState<User | null>(null);
const [isLoadingChats, setIsLoadingChats] = useState(true); const [isLoadingChats, setIsLoadingChats] = useState(true);
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true); const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
const [isLoadingUser, setIsLoadingUser] = useState(true);
const [chatError, setChatError] = useState<string | null>(null); const [chatError, setChatError] = useState<string | null>(null);
const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null); const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null);
const [userError, setUserError] = useState<string | null>(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number, name: string } | null>(null); const [chatToDelete, setChatToDelete] = useState<{ id: number, name: string } | null>(null);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
@ -82,33 +71,6 @@ export function AppSidebarProvider({
setIsClient(true); setIsClient(true);
}, []); }, []);
// Fetch user details
useEffect(() => {
const fetchUser = async () => {
try {
// Only run on client-side
if (typeof window === 'undefined') return;
try {
// Use the API client instead of direct fetch
const userData = await apiClient.get<User>('users/me');
setUser(userData);
setUserError(null);
} catch (error) {
console.error('Error fetching user:', error);
setUserError(error instanceof Error ? error.message : 'Unknown error occurred');
} finally {
setIsLoadingUser(false);
}
} catch (error) {
console.error('Error in fetchUser:', error);
setIsLoadingUser(false);
}
};
fetchUser();
}, []);
// Fetch recent chats // Fetch recent chats
useEffect(() => { useEffect(() => {
const fetchRecentChats = async () => { const fetchRecentChats = async () => {
@ -119,9 +81,9 @@ export function AppSidebarProvider({
try { try {
// Use the API client instead of direct fetch - filter by current search space ID // Use the API client instead of direct fetch - filter by current search space ID
const chats: Chat[] = await apiClient.get<Chat[]>(`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`); const chats: Chat[] = await apiClient.get<Chat[]>(`api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`);
// Sort chats by created_at in descending order (newest first) // Sort chats by created_at in descending order (newest first)
const sortedChats = chats.sort((a, b) => const sortedChats = chats.sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime() new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
); );
// console.log("sortedChats", sortedChats); // console.log("sortedChats", sortedChats);
@ -171,7 +133,7 @@ export function AppSidebarProvider({
// Set up a refresh interval (every 5 minutes) // Set up a refresh interval (every 5 minutes)
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000); const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
// Clean up interval on component unmount // Clean up interval on component unmount
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [searchSpaceId]); }, [searchSpaceId]);
@ -179,16 +141,16 @@ export function AppSidebarProvider({
// Handle delete chat // Handle delete chat
const handleDeleteChat = async () => { const handleDeleteChat = async () => {
if (!chatToDelete) return; if (!chatToDelete) return;
try { try {
setIsDeleting(true); setIsDeleting(true);
// Use the API client instead of direct fetch // Use the API client instead of direct fetch
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`); await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
// Close dialog and refresh chats // Close dialog and refresh chats
setRecentChats(recentChats.filter(chat => chat.id !== chatToDelete.id)); setRecentChats(recentChats.filter(chat => chat.id !== chatToDelete.id));
} catch (error) { } catch (error) {
console.error('Error deleting chat:', error); console.error('Error deleting chat:', error);
} finally { } finally {
@ -226,15 +188,15 @@ export function AppSidebarProvider({
}, [searchSpaceId]); }, [searchSpaceId]);
// Create a fallback chat if there's an error or no chats // Create a fallback chat if there's an error or no chats
const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0) const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
? [{ ? [{
name: chatError ? "Error loading chats" : "No recent chats", name: chatError ? "Error loading chats" : "No recent chats",
url: "#", url: "#",
icon: chatError ? "AlertCircle" : "MessageCircleMore", icon: chatError ? "AlertCircle" : "MessageCircleMore",
id: 0, id: 0,
search_space_id: Number(searchSpaceId), search_space_id: Number(searchSpaceId),
actions: [] actions: []
}] }]
: []; : [];
// Use fallback chats if there's an error or no chats // Use fallback chats if there's an error or no chats
@ -249,22 +211,14 @@ export function AppSidebarProvider({
}; };
} }
// Create user object for AppSidebar
const customUser = {
name: isClient && user?.email ? user.email.split('@')[0] : 'User',
email: isClient ? (user?.email || (isLoadingUser ? 'Loading...' : userError ? 'Error loading user' : 'Unknown User')) : 'Loading...',
avatar: '/icon-128.png', // Default avatar
};
return ( return (
<> <>
<AppSidebar <AppSidebar
user={customUser}
navSecondary={updatedNavSecondary} navSecondary={updatedNavSecondary}
navMain={navMain} navMain={navMain}
RecentChats={isClient ? displayChats : []} RecentChats={isClient ? displayChats : []}
/> />
{/* Delete Confirmation Dialog - Only render on client */} {/* Delete Confirmation Dialog - Only render on client */}
{isClient && ( {isClient && (
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>

View file

@ -23,7 +23,6 @@ import { Logo } from "@/components/Logo";
import { NavMain } from "@/components/sidebar/nav-main" import { NavMain } from "@/components/sidebar/nav-main"
import { NavProjects } from "@/components/sidebar/nav-projects" import { NavProjects } from "@/components/sidebar/nav-projects"
import { NavSecondary } from "@/components/sidebar/nav-secondary" import { NavSecondary } from "@/components/sidebar/nav-secondary"
import { NavUser } from "@/components/sidebar/nav-user"
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@ -143,11 +142,6 @@ const defaultData = {
} }
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> { interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
user?: {
name: string
email: string
avatar: string
}
navMain?: { navMain?: {
title: string title: string
url: string url: string
@ -178,7 +172,6 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
} }
export function AppSidebar({ export function AppSidebar({
user = defaultData.user,
navMain = defaultData.navMain, navMain = defaultData.navMain,
navSecondary = defaultData.navSecondary, navSecondary = defaultData.navSecondary,
RecentChats = defaultData.RecentChats, RecentChats = defaultData.RecentChats,
@ -232,9 +225,9 @@ export function AppSidebar({
{processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />} {processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />}
<NavSecondary items={processedNavSecondary} className="mt-auto" /> <NavSecondary items={processedNavSecondary} className="mt-auto" />
</SidebarContent> </SidebarContent>
<SidebarFooter> {/* <SidebarFooter>
<NavUser user={user} /> footer
</SidebarFooter> </SidebarFooter> */}
</Sidebar> </Sidebar>
) )
} }