This commit is contained in:
marko-kraemer 2025-04-15 19:14:58 +01:00
parent f0d7392d3b
commit 39ba527ba9
16 changed files with 915 additions and 446 deletions

View File

@ -1,67 +0,0 @@
"use client"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { AppSidebar } from "@/components/dashboard/sidebar/app-sidebar"
import { NavActions } from "@/components/dashboard/sidebar/nav-actions"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
export default function Page() {
const router = useRouter()
const [currentTeamId, setCurrentTeamId] = useState<string>()
const handleTeamSelected = (team: any) => {
setCurrentTeamId(team.account_id)
// Navigate to the team dashboard if it has a slug
if (team.slug) {
router.push(`/dashboard/${team.slug}`)
}
}
return (
<SidebarProvider>
<AppSidebar
teamId={currentTeamId}
onTeamSelected={handleTeamSelected}
/>
<SidebarInset>
<header className="flex h-14 shrink-0 items-center gap-2">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<Separator
orientation="vertical"
className="mr-2 data-[orientation=vertical]:h-4"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">
Project Management & Task Tracking
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="ml-auto px-3">
<NavActions />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 px-4 py-10">
<div className="bg-muted/50 mx-auto h-24 w-full max-w-3xl rounded-xl" />
<div className="bg-muted/50 mx-auto h-full w-full max-w-3xl rounded-xl" />
</div>
</SidebarInset>
</SidebarProvider>
)
}

View File

@ -222,7 +222,8 @@ function MessageContent({ content }: { content: string }) {
}
export default function AgentPage({ params }: AgentPageProps) {
const { threadId } = params;
const resolvedParams = React.use(params as any) as { threadId: string };
const { threadId } = resolvedParams;
const router = useRouter();
const searchParams = useSearchParams();
const initialMessage = searchParams.get('message');

View File

@ -1,28 +1,25 @@
import DashboardLayout from "@/components/dashboard/DashboardLayout";
import { createClient } from "@/lib/supabase/server";
interface DashboardRootLayoutProps {
children: React.ReactNode;
import { SidebarLeft } from "@/components/dashboard/sidebar/sidebar-left"
import { SidebarRight } from "@/components/dashboard/sidebar/sidebar-right"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
interface DashboardLayoutProps {
children: React.ReactNode
}
export default async function DashboardRootLayout({
export default async function DashboardLayout({
children,
}: DashboardRootLayoutProps) {
// Get the current user via Supabase
const supabaseClient = await createClient();
const { data: { user } } = await supabaseClient.auth.getUser();
// Get the personal account details
const { data: personalAccount } = await supabaseClient.rpc('get_personal_account');
}: DashboardLayoutProps) {
return (
<DashboardLayout
accountId={personalAccount?.account_id || user.id}
userName={user?.user_metadata?.name || user.email?.split('@')[0] || 'User'}
userEmail={user.email}
rightPanelTitle="Suna's Computer"
>
{children}
</DashboardLayout>
);
<SidebarProvider>
<SidebarLeft />
<SidebarInset>
{children}
</SidebarInset>
{/* <SidebarRight /> */}
</SidebarProvider>
)
}

View File

@ -0,0 +1,28 @@
import DashboardLayout from "@/components/dashboard/DashboardLayout";
import { createClient } from "@/lib/supabase/server";
interface DashboardRootLayoutProps {
children: React.ReactNode;
}
export default async function DashboardRootLayout({
children,
}: DashboardRootLayoutProps) {
// Get the current user via Supabase
const supabaseClient = await createClient();
const { data: { user } } = await supabaseClient.auth.getUser();
// Get the personal account details
const { data: personalAccount } = await supabaseClient.rpc('get_personal_account');
return (
<DashboardLayout
accountId={personalAccount?.account_id || user.id}
userName={user?.user_metadata?.name || user.email?.split('@')[0] || 'User'}
userEmail={user.email}
rightPanelTitle="Suna's Computer"
>
{children}
</DashboardLayout>
);
}

View File

@ -1,85 +1,37 @@
"use client";
import React, { useState, Suspense } from 'react';
import { Skeleton } from "@/components/ui/skeleton";
import { useRouter } from 'next/navigation';
import { ChatInput } from '@/components/chat/chat-input';
import { createProject, addUserMessage, startAgent, createThread } from "@/lib/api";
function DashboardContent() {
const [inputValue, setInputValue] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const handleSubmit = async (message: string) => {
if (!message.trim() || isSubmitting) return;
setIsSubmitting(true);
try {
// 1. Create a new project with the message as the name
const newAgent = await createProject({
name: message.trim().length > 50
? message.trim().substring(0, 47) + "..."
: message.trim(),
description: "",
});
// 2. Create a new thread for this project
const thread = await createThread(newAgent.id);
// 3. Add the user message to the thread
await addUserMessage(thread.thread_id, message.trim());
// 4. Start the agent with the thread ID
const agentRun = await startAgent(thread.thread_id);
// 5. Navigate to the new agent's thread page
router.push(`/dashboard/agents/${thread.thread_id}`);
} catch (error) {
console.error("Error creating agent:", error);
setIsSubmitting(false);
}
};
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import { SidebarTrigger } from "@/components/ui/sidebar"
export default function Page() {
return (
<div className="flex flex-col items-center justify-center h-full w-full">
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="text-center mb-10">
<h1 className="text-4xl font-medium text-foreground mb-2">Hello.</h1>
<h2 className="text-2xl text-muted-foreground">What can I help with?</h2>
<>
<header className="bg-background sticky top-0 flex h-14 shrink-0 items-center gap-2">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<Separator
orientation="vertical"
className="mr-2 data-[orientation=vertical]:h-4"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">
Project Management & Task Tracking
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<ChatInput
onSubmit={handleSubmit}
loading={isSubmitting}
placeholder="Ask anything..."
value={inputValue}
onChange={setInputValue}
/>
</header>
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="bg-muted/50 mx-auto h-24 w-full max-w-3xl rounded-xl" />
<div className="bg-muted/50 mx-auto h-[100vh] w-full max-w-3xl rounded-xl" />
</div>
</div>
);
</>
)
}
export default function DashboardPage() {
return (
<Suspense fallback={
<div className="flex flex-col items-center justify-center h-full w-full">
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="flex flex-col items-center text-center mb-10">
<Skeleton className="h-10 w-40 mb-2" />
<Skeleton className="h-7 w-56" />
</div>
<Skeleton className="w-full h-[100px] rounded-xl" />
<div className="flex justify-center mt-3">
<Skeleton className="h-5 w-16" />
</div>
</div>
</div>
}>
<DashboardContent />
</Suspense>
);
}

View File

@ -0,0 +1,85 @@
"use client";
import React, { useState, Suspense } from 'react';
import { Skeleton } from "@/components/ui/skeleton";
import { useRouter } from 'next/navigation';
import { ChatInput } from '@/components/chat/chat-input';
import { createProject, addUserMessage, startAgent, createThread } from "@/lib/api";
function DashboardContent() {
const [inputValue, setInputValue] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const handleSubmit = async (message: string) => {
if (!message.trim() || isSubmitting) return;
setIsSubmitting(true);
try {
// 1. Create a new project with the message as the name
const newAgent = await createProject({
name: message.trim().length > 50
? message.trim().substring(0, 47) + "..."
: message.trim(),
description: "",
});
// 2. Create a new thread for this project
const thread = await createThread(newAgent.id);
// 3. Add the user message to the thread
await addUserMessage(thread.thread_id, message.trim());
// 4. Start the agent with the thread ID
const agentRun = await startAgent(thread.thread_id);
// 5. Navigate to the new agent's thread page
router.push(`/dashboard/agents/${thread.thread_id}`);
} catch (error) {
console.error("Error creating agent:", error);
setIsSubmitting(false);
}
};
return (
<div className="flex flex-col items-center justify-center h-full w-full">
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="text-center mb-10">
<h1 className="text-4xl font-medium text-foreground mb-2">Hello.</h1>
<h2 className="text-2xl text-muted-foreground">What can I help with?</h2>
</div>
<ChatInput
onSubmit={handleSubmit}
loading={isSubmitting}
placeholder="Ask anything..."
value={inputValue}
onChange={setInputValue}
/>
</div>
</div>
);
}
export default function DashboardPage() {
return (
<Suspense fallback={
<div className="flex flex-col items-center justify-center h-full w-full">
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[560px] max-w-[90%]">
<div className="flex flex-col items-center text-center mb-10">
<Skeleton className="h-10 w-40 mb-2" />
<Skeleton className="h-7 w-56" />
</div>
<Skeleton className="w-full h-[100px] rounded-xl" />
<div className="flex justify-center mt-3">
<Skeleton className="h-5 w-16" />
</div>
</div>
</div>
}>
<DashboardContent />
</Suspense>
);
}

View File

@ -1,276 +0,0 @@
"use client"
import * as React from "react"
import {
AudioWaveform,
Blocks,
Calendar,
Command,
Home,
Inbox,
MessageCircleQuestion,
Search,
Settings2,
Sparkles,
Trash2,
} from "lucide-react"
import { NavFavorites } from "@/components/nav-favorites"
import { NavMain } from "@/components/nav-main"
import { NavSecondary } from "@/components/nav-secondary"
import { NavWorkspaces } from "@/components/nav-workspaces"
import { TeamSwitcher } from "@/components/dashboard/sidebar/team-switcher"
import {
Sidebar,
SidebarContent,
SidebarHeader,
SidebarRail,
} from "@/components/ui/sidebar"
// This is sample data.
const data = {
teams: [
{
name: "Acme Inc",
logo: Command,
plan: "Enterprise",
},
{
name: "Acme Corp.",
logo: AudioWaveform,
plan: "Startup",
},
{
name: "Evil Corp.",
logo: Command,
plan: "Free",
},
],
navMain: [
{
title: "Search",
url: "#",
icon: Search,
},
{
title: "Ask AI",
url: "#",
icon: Sparkles,
},
{
title: "Home",
url: "#",
icon: Home,
isActive: true,
},
{
title: "Inbox",
url: "#",
icon: Inbox,
badge: "10",
},
],
navSecondary: [
{
title: "Calendar",
url: "#",
icon: Calendar,
},
{
title: "Settings",
url: "#",
icon: Settings2,
},
{
title: "Templates",
url: "#",
icon: Blocks,
},
{
title: "Trash",
url: "#",
icon: Trash2,
},
{
title: "Help",
url: "#",
icon: MessageCircleQuestion,
},
],
favorites: [
{
name: "Project Management & Task Tracking",
url: "#",
emoji: "📊",
},
{
name: "Family Recipe Collection & Meal Planning",
url: "#",
emoji: "🍳",
},
{
name: "Fitness Tracker & Workout Routines",
url: "#",
emoji: "💪",
},
{
name: "Book Notes & Reading List",
url: "#",
emoji: "📚",
},
{
name: "Sustainable Gardening Tips & Plant Care",
url: "#",
emoji: "🌱",
},
{
name: "Language Learning Progress & Resources",
url: "#",
emoji: "🗣️",
},
{
name: "Home Renovation Ideas & Budget Tracker",
url: "#",
emoji: "🏠",
},
{
name: "Personal Finance & Investment Portfolio",
url: "#",
emoji: "💰",
},
{
name: "Movie & TV Show Watchlist with Reviews",
url: "#",
emoji: "🎬",
},
{
name: "Daily Habit Tracker & Goal Setting",
url: "#",
emoji: "✅",
},
],
workspaces: [
{
name: "Personal Life Management",
emoji: "🏠",
pages: [
{
name: "Daily Journal & Reflection",
url: "#",
emoji: "📔",
},
{
name: "Health & Wellness Tracker",
url: "#",
emoji: "🍏",
},
{
name: "Personal Growth & Learning Goals",
url: "#",
emoji: "🌟",
},
],
},
{
name: "Professional Development",
emoji: "💼",
pages: [
{
name: "Career Objectives & Milestones",
url: "#",
emoji: "🎯",
},
{
name: "Skill Acquisition & Training Log",
url: "#",
emoji: "🧠",
},
{
name: "Networking Contacts & Events",
url: "#",
emoji: "🤝",
},
],
},
{
name: "Creative Projects",
emoji: "🎨",
pages: [
{
name: "Writing Ideas & Story Outlines",
url: "#",
emoji: "✍️",
},
{
name: "Art & Design Portfolio",
url: "#",
emoji: "🖼️",
},
{
name: "Music Composition & Practice Log",
url: "#",
emoji: "🎵",
},
],
},
{
name: "Home Management",
emoji: "🏡",
pages: [
{
name: "Household Budget & Expense Tracking",
url: "#",
emoji: "💰",
},
{
name: "Home Maintenance Schedule & Tasks",
url: "#",
emoji: "🔧",
},
{
name: "Family Calendar & Event Planning",
url: "#",
emoji: "📅",
},
],
},
{
name: "Travel & Adventure",
emoji: "🧳",
pages: [
{
name: "Trip Planning & Itineraries",
url: "#",
emoji: "🗺️",
},
{
name: "Travel Bucket List & Inspiration",
url: "#",
emoji: "🌎",
},
{
name: "Travel Journal & Photo Gallery",
url: "#",
emoji: "📸",
},
],
},
],
}
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar className="border-r-0" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
<NavMain items={data.navMain} />
</SidebarHeader>
<SidebarContent>
<NavFavorites favorites={data.favorites} />
<NavWorkspaces workspaces={data.workspaces} />
<NavSecondary items={data.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarRail />
</Sidebar>
)
}

View File

@ -0,0 +1,172 @@
"use client"
import { useEffect, useState } from "react"
import {
ArrowUpRight,
Link as LinkIcon,
MoreHorizontal,
Trash2,
StarOff,
} from "lucide-react"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import { getProjects, getThreads } from "@/lib/api"
import Link from "next/link"
export function NavAgents() {
const { isMobile } = useSidebar()
const [agents, setAgents] = useState<{name: string, url: string, emoji: string}[]>([])
const [isLoading, setIsLoading] = useState(true)
// Load agents dynamically from the API
useEffect(() => {
async function loadAgents() {
try {
const projectsData = await getProjects()
const agentsList = []
for (const project of projectsData) {
const threads = await getThreads(project.id)
if (threads && threads.length > 0) {
// For each thread in the project, create an agent entry
for (const thread of threads) {
// Generate a simple emoji based on the project name hash
const emoji = getEmojiFromName(project.name)
agentsList.push({
name: project.name,
url: `/dashboard/agents/${thread.thread_id}`,
emoji: emoji
})
}
}
}
setAgents(agentsList)
} catch (err) {
console.error("Error loading agents for sidebar:", err)
} finally {
setIsLoading(false)
}
}
loadAgents()
}, [])
// Function to generate emoji from name
const getEmojiFromName = (name: string) => {
const emojis = ["📊", "📝", "💼", "🔍", "✅", "📈", "💡", "🎯", "🗂️", "🤖", "💬", "📚"]
// Simple hash function to pick a consistent emoji for the same name
let hash = 0
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash)
}
return emojis[Math.abs(hash) % emojis.length]
}
// Get only the latest 20 agents for the sidebar
const recentAgents = agents.slice(0, 20)
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<div className="flex justify-between items-center">
<SidebarGroupLabel>Agents</SidebarGroupLabel>
</div>
<SidebarMenu>
{isLoading ? (
// Show skeleton loaders while loading
Array.from({length: 3}).map((_, index) => (
<SidebarMenuItem key={`skeleton-${index}`}>
<SidebarMenuButton>
<div className="h-4 w-4 bg-sidebar-foreground/10 rounded-md animate-pulse"></div>
<div className="h-3 bg-sidebar-foreground/10 rounded w-3/4 animate-pulse"></div>
</SidebarMenuButton>
</SidebarMenuItem>
))
) : recentAgents.length > 0 ? (
// Show agents
<>
{recentAgents.map((item, index) => (
<SidebarMenuItem key={`agent-${index}`}>
<SidebarMenuButton asChild>
<Link href={item.url} title={item.name}>
<span>{item.emoji}</span>
<span>{item.name}</span>
</Link>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<StarOff className="text-muted-foreground" />
<span>Remove from agents</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LinkIcon className="text-muted-foreground" />
<span>Copy Link</span>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a href={item.url} target="_blank" rel="noopener noreferrer">
<ArrowUpRight className="text-muted-foreground" />
<span>Open in New Tab</span>
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
{agents.length > 20 && (
<SidebarMenuItem>
<SidebarMenuButton asChild className="text-sidebar-foreground/70">
<Link href="/dashboard/agents">
<MoreHorizontal />
<span>See all agents</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</>
) : (
// Empty state
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<span>No agents yet</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</SidebarMenu>
</SidebarGroup>
)
}

View File

@ -0,0 +1,35 @@
"use client"
import { type LucideIcon } from "lucide-react"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
export function NavMain({
items,
}: {
items: {
title: string
url: string
icon: LucideIcon
isActive?: boolean
}[]
}) {
return (
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={item.isActive}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
)
}

View File

@ -0,0 +1,43 @@
import React from "react"
import { type LucideIcon } from "lucide-react"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
export function NavSecondary({
items,
...props
}: {
items: {
title: string
url: string
icon: LucideIcon
badge?: React.ReactNode
}[]
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
return (
<SidebarGroup {...props}>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
{item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}

View File

@ -0,0 +1,126 @@
"use client"
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Settings,
User,
} 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 {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { createClient } from "@/lib/supabase/client"
export function NavUser({
user,
}: {
user: {
name: string
email: string
avatar: string
}
}) {
const { isMobile } = useSidebar()
const router = useRouter()
const handleLogout = async () => {
const supabase = createClient()
await supabase.auth.signOut()
router.push("/auth")
}
const getInitials = (name: string) => {
return name.split(' ')
.map(part => part.charAt(0))
.join('')
.toUpperCase()
.substring(0, 2)
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">{getInitials(user.name)}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="start"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">{getInitials(user.name)}</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href="/dashboard/settings/billing">
<CreditCard className="mr-2 h-4 w-4" />
Billing
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/dashboard/settings">
<Settings className="mr-2 h-4 w-4" />
Settings
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@ -0,0 +1,90 @@
"use client"
import * as React from "react"
import {
BookOpen,
CalendarClock,
HelpCircle,
MessageCircleQuestion,
} from "lucide-react"
import { NavAgents } from "@/components/dashboard/sidebar/nav-agents"
import { NavUser } from "@/components/dashboard/sidebar/nav-user"
import { NavSecondary } from "@/components/dashboard/sidebar/nav-secondary"
import { TeamSwitcher } from "@/components/dashboard/sidebar/team-switcher"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarRail,
} from "@/components/ui/sidebar"
import { useEffect, useState } from "react"
import { createClient } from "@/lib/supabase/client"
// Only keep necessary data
const navSecondaryItems = [
{
title: "Help",
url: "#",
icon: HelpCircle,
},
{
title: "Careers",
url: "#",
icon: BookOpen,
},
{
title: "Book Demo",
url: "#",
icon: CalendarClock,
},
]
export function SidebarLeft({
...props
}: React.ComponentProps<typeof Sidebar>) {
const [user, setUser] = useState<{
name: string;
email: string;
avatar: string;
}>({
name: "Loading...",
email: "loading@example.com",
avatar: ""
})
// Fetch user data
useEffect(() => {
const fetchUserData = async () => {
const supabase = createClient()
const { data } = await supabase.auth.getUser()
if (data.user) {
setUser({
name: data.user.user_metadata?.name || data.user.email?.split('@')[0] || 'User',
email: data.user.email || '',
avatar: data.user.user_metadata?.avatar_url || ''
})
}
}
fetchUserData()
}, [])
return (
<Sidebar className="border-r-0" {...props}>
<SidebarHeader>
<TeamSwitcher />
</SidebarHeader>
<SidebarContent>
<NavAgents />
<NavSecondary items={navSecondaryItems} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}

View File

@ -0,0 +1,71 @@
import * as React from "react"
import { Plus } from "lucide-react"
import { Calendars } from "@/components/dashboard/sidebar/calendars"
import { DatePicker } from "@/components/dashboard/sidebar/date-picker"
import { NavUser } from "@/components/dashboard/sidebar/nav-user"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarSeparator,
} from "@/components/ui/sidebar"
// This is sample data.
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
calendars: [
{
name: "My Calendars",
items: ["Personal", "Work", "Family"],
},
{
name: "Favorites",
items: ["Holidays", "Birthdays"],
},
{
name: "Other",
items: ["Travel", "Reminders", "Deadlines"],
},
],
}
export function SidebarRight({
...props
}: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar
collapsible="none"
className="sticky top-0 hidden h-svh border-l lg:flex"
{...props}
>
<SidebarHeader className="border-sidebar-border h-16 border-b">
<NavUser user={data.user} />
</SidebarHeader>
<SidebarContent>
<DatePicker />
<SidebarSeparator className="mx-0" />
<Calendars calendars={data.calendars} />
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<Plus />
<span>New Calendar</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
)
}

View File

@ -0,0 +1,212 @@
"use client"
import * as React from "react"
import { useRouter } from "next/navigation"
import { ChevronDown, Plus, Command, AudioWaveform } from "lucide-react"
import { useAccounts } from "@/hooks/use-accounts"
import NewTeamForm from "@/components/basejump/new-team-form"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export function TeamSwitcher() {
const router = useRouter()
const { data: accounts } = useAccounts()
const [showNewTeamDialog, setShowNewTeamDialog] = React.useState(false)
// Prepare personal account and team accounts
const personalAccount = React.useMemo(() => accounts?.find(account => account.personal_account), [accounts])
const teamAccounts = React.useMemo(() => accounts?.filter(account => !account.personal_account), [accounts])
// Create a default list of teams with logos for the UI (will show until real data loads)
const defaultTeams = [
{
name: personalAccount?.name || "Personal Account",
logo: Command,
plan: "Personal",
account_id: personalAccount?.account_id,
slug: personalAccount?.slug,
personal_account: true
},
...(teamAccounts?.map(team => ({
name: team.name,
logo: AudioWaveform,
plan: "Team",
account_id: team.account_id,
slug: team.slug,
personal_account: false
})) || [])
]
// Use the first team or first entry in defaultTeams as activeTeam
const [activeTeam, setActiveTeam] = React.useState(defaultTeams[0])
// Update active team when accounts load
React.useEffect(() => {
if (accounts?.length) {
const currentTeam = accounts.find(account => account.account_id === activeTeam.account_id)
if (currentTeam) {
setActiveTeam({
name: currentTeam.name,
logo: currentTeam.personal_account ? Command : AudioWaveform,
plan: currentTeam.personal_account ? "Personal" : "Team",
account_id: currentTeam.account_id,
slug: currentTeam.slug,
personal_account: currentTeam.personal_account
})
} else {
// If current team not found, set first available account as active
const firstAccount = accounts[0]
setActiveTeam({
name: firstAccount.name,
logo: firstAccount.personal_account ? Command : AudioWaveform,
plan: firstAccount.personal_account ? "Personal" : "Team",
account_id: firstAccount.account_id,
slug: firstAccount.slug,
personal_account: firstAccount.personal_account
})
}
}
}, [accounts, activeTeam.account_id])
// Handle team selection
const handleTeamSelect = (team) => {
setActiveTeam(team)
// Navigate to the appropriate dashboard
if (team.personal_account) {
router.push('/dashboard')
} else {
router.push(`/dashboard/${team.slug}`)
}
}
if (!activeTeam) {
return null
}
return (
<Dialog open={showNewTeamDialog} onOpenChange={setShowNewTeamDialog}>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton className="w-fit px-1.5">
<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-5 items-center justify-center rounded-md">
<activeTeam.logo className="size-3" />
</div>
<span className="truncate font-medium">{activeTeam.name}</span>
<ChevronDown className="opacity-50" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-64 rounded-lg"
align="start"
side="bottom"
sideOffset={4}
>
{personalAccount && (
<>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Personal Account
</DropdownMenuLabel>
<DropdownMenuItem
key={personalAccount.account_id}
onClick={() => handleTeamSelect({
name: personalAccount.name,
logo: Command,
plan: "Personal",
account_id: personalAccount.account_id,
slug: personalAccount.slug,
personal_account: true
})}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-xs border">
<Command className="size-4 shrink-0" />
</div>
{personalAccount.name}
<DropdownMenuShortcut>1</DropdownMenuShortcut>
</DropdownMenuItem>
</>
)}
{teamAccounts?.length > 0 && (
<>
<DropdownMenuLabel className="text-muted-foreground text-xs mt-2">
Teams
</DropdownMenuLabel>
{teamAccounts.map((team, index) => (
<DropdownMenuItem
key={team.account_id}
onClick={() => handleTeamSelect({
name: team.name,
logo: AudioWaveform,
plan: "Team",
account_id: team.account_id,
slug: team.slug,
personal_account: false
})}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-xs border">
<AudioWaveform className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 2}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
</>
)}
<DropdownMenuSeparator />
<DialogTrigger asChild>
<DropdownMenuItem
className="gap-2 p-2"
onClick={() => {
setShowNewTeamDialog(true)
}}
>
<div className="bg-background flex size-6 items-center justify-center rounded-md border">
<Plus className="size-4" />
</div>
<div className="text-muted-foreground font-medium">Add team</div>
</DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
<DialogContent className="sm:max-w-[425px] border-subtle dark:border-white/10 bg-card-bg dark:bg-background-secondary rounded-2xl shadow-custom">
<DialogHeader>
<DialogTitle className="text-foreground">Create a new team</DialogTitle>
<DialogDescription className="text-foreground/70">
Create a team to collaborate with others.
</DialogDescription>
</DialogHeader>
<NewTeamForm />
</DialogContent>
</Dialog>
)
}