mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-01 18:19:08 +00:00
Merge pull request #195 from MODSetter/dev
feat(FRONTEND): Moved User Dropdown to Search Space Dashboard
This commit is contained in:
commit
5cc8373205
7 changed files with 169 additions and 98 deletions
|
@ -1,14 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { motion } from 'framer-motion'
|
||||
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 { Spotlight } from '@/components/ui/spotlight'
|
||||
import { Logo } from '@/components/Logo';
|
||||
import { ThemeTogglerComponent } from '@/components/theme/theme-toggle';
|
||||
import { UserDropdown } from '@/components/UserDropdown';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertDialog,
|
||||
|
@ -28,8 +29,17 @@ import {
|
|||
} from "@/components/ui/alert";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useSearchSpaces } from '@/hooks/use-search-spaces';
|
||||
import { apiClient } from '@/lib/api';
|
||||
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
|
||||
* @param dateString - The date string to format
|
||||
|
@ -147,17 +157,47 @@ const DashboardPage = () => {
|
|||
|
||||
const router = useRouter();
|
||||
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 (error) return <ErrorScreen message={error} />;
|
||||
|
||||
const handleLogout = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem('surfsense_bearer_token');
|
||||
router.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSearchSpace = async (id: number) => {
|
||||
// Send DELETE request to the API
|
||||
try {
|
||||
|
@ -201,18 +241,10 @@ const DashboardPage = () => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleLogout}
|
||||
className="h-9 w-9 rounded-full"
|
||||
aria-label="Logout"
|
||||
>
|
||||
<LogOut className="h-5 w-5" />
|
||||
</Button>
|
||||
<ThemeTogglerComponent />
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<UserDropdown user={customUser} />
|
||||
<ThemeTogglerComponent />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-6 mt-6">
|
||||
|
|
92
surfsense_web/components/UserDropdown.tsx
Normal file
92
surfsense_web/components/UserDropdown.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -31,14 +31,6 @@ interface SearchSpace {
|
|||
user_id: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
is_verified: boolean;
|
||||
}
|
||||
|
||||
interface AppSidebarProviderProps {
|
||||
searchSpaceId: string;
|
||||
navSecondary: {
|
||||
|
@ -58,20 +50,17 @@ interface AppSidebarProviderProps {
|
|||
}[];
|
||||
}
|
||||
|
||||
export function AppSidebarProvider({
|
||||
searchSpaceId,
|
||||
navSecondary,
|
||||
navMain
|
||||
export function AppSidebarProvider({
|
||||
searchSpaceId,
|
||||
navSecondary,
|
||||
navMain
|
||||
}: 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 [searchSpace, setSearchSpace] = useState<SearchSpace | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoadingChats, setIsLoadingChats] = useState(true);
|
||||
const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true);
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(true);
|
||||
const [chatError, setChatError] = useState<string | null>(null);
|
||||
const [searchSpaceError, setSearchSpaceError] = useState<string | null>(null);
|
||||
const [userError, setUserError] = useState<string | null>(null);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [chatToDelete, setChatToDelete] = useState<{ id: number, name: string } | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
@ -82,33 +71,6 @@ export function AppSidebarProvider({
|
|||
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
|
||||
useEffect(() => {
|
||||
const fetchRecentChats = async () => {
|
||||
|
@ -119,9 +81,9 @@ export function AppSidebarProvider({
|
|||
try {
|
||||
// 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}`);
|
||||
|
||||
|
||||
// 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()
|
||||
);
|
||||
// console.log("sortedChats", sortedChats);
|
||||
|
@ -171,7 +133,7 @@ export function AppSidebarProvider({
|
|||
|
||||
// Set up a refresh interval (every 5 minutes)
|
||||
const intervalId = setInterval(fetchRecentChats, 5 * 60 * 1000);
|
||||
|
||||
|
||||
// Clean up interval on component unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}, [searchSpaceId]);
|
||||
|
@ -179,16 +141,16 @@ export function AppSidebarProvider({
|
|||
// Handle delete chat
|
||||
const handleDeleteChat = async () => {
|
||||
if (!chatToDelete) return;
|
||||
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
|
||||
|
||||
// Use the API client instead of direct fetch
|
||||
await apiClient.delete(`api/v1/chats/${chatToDelete.id}`);
|
||||
|
||||
|
||||
// Close dialog and refresh chats
|
||||
setRecentChats(recentChats.filter(chat => chat.id !== chatToDelete.id));
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting chat:', error);
|
||||
} finally {
|
||||
|
@ -226,15 +188,15 @@ export function AppSidebarProvider({
|
|||
}, [searchSpaceId]);
|
||||
|
||||
// Create a fallback chat if there's an error or no chats
|
||||
const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
|
||||
? [{
|
||||
name: chatError ? "Error loading chats" : "No recent chats",
|
||||
url: "#",
|
||||
icon: chatError ? "AlertCircle" : "MessageCircleMore",
|
||||
id: 0,
|
||||
search_space_id: Number(searchSpaceId),
|
||||
actions: []
|
||||
}]
|
||||
const fallbackChats = chatError || (!isLoadingChats && recentChats.length === 0)
|
||||
? [{
|
||||
name: chatError ? "Error loading chats" : "No recent chats",
|
||||
url: "#",
|
||||
icon: chatError ? "AlertCircle" : "MessageCircleMore",
|
||||
id: 0,
|
||||
search_space_id: Number(searchSpaceId),
|
||||
actions: []
|
||||
}]
|
||||
: [];
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<AppSidebar
|
||||
user={customUser}
|
||||
navSecondary={updatedNavSecondary}
|
||||
navMain={navMain}
|
||||
RecentChats={isClient ? displayChats : []}
|
||||
/>
|
||||
|
||||
|
||||
{/* Delete Confirmation Dialog - Only render on client */}
|
||||
{isClient && (
|
||||
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
|
|
|
@ -23,7 +23,6 @@ import { Logo } from "@/components/Logo";
|
|||
import { NavMain } from "@/components/sidebar/nav-main"
|
||||
import { NavProjects } from "@/components/sidebar/nav-projects"
|
||||
import { NavSecondary } from "@/components/sidebar/nav-secondary"
|
||||
import { NavUser } from "@/components/sidebar/nav-user"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
@ -143,11 +142,6 @@ const defaultData = {
|
|||
}
|
||||
|
||||
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
||||
user?: {
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
}
|
||||
navMain?: {
|
||||
title: string
|
||||
url: string
|
||||
|
@ -178,7 +172,6 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|||
}
|
||||
|
||||
export function AppSidebar({
|
||||
user = defaultData.user,
|
||||
navMain = defaultData.navMain,
|
||||
navSecondary = defaultData.navSecondary,
|
||||
RecentChats = defaultData.RecentChats,
|
||||
|
@ -232,9 +225,9 @@ export function AppSidebar({
|
|||
{processedRecentChats.length > 0 && <NavProjects chats={processedRecentChats} />}
|
||||
<NavSecondary items={processedNavSecondary} className="mt-auto" />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
{/* <SidebarFooter>
|
||||
footer
|
||||
</SidebarFooter> */}
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue