mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 09:46:25 +02:00
Merge remote-tracking branch 'upstream/dev' into feat/replace-logs
This commit is contained in:
commit
7a92ecc1ab
56 changed files with 2451 additions and 808 deletions
|
|
@ -0,0 +1,161 @@
|
|||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { Loader2, Plus, Search } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { createSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { trackSearchSpaceCreated } from "@/lib/posthog/events";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface CreateSearchSpaceDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpaceDialogProps) {
|
||||
const t = useTranslations("searchSpace");
|
||||
const tCommon = useTranslations("common");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { mutateAsync: createSearchSpace } = useAtomValue(createSearchSpaceMutationAtom);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const result = await createSearchSpace({
|
||||
name: values.name,
|
||||
description: values.description || "",
|
||||
});
|
||||
|
||||
trackSearchSpaceCreated(result.id, values.name);
|
||||
|
||||
// Hard redirect to ensure fresh state
|
||||
window.location.href = `/dashboard/${result.id}/onboard`;
|
||||
} catch (error) {
|
||||
console.error("Failed to create search space:", error);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (!newOpen) {
|
||||
form.reset();
|
||||
}
|
||||
onOpenChange(newOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Search className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>{t("create_title")}</DialogTitle>
|
||||
<DialogDescription>{t("create_description")}</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="flex flex-col gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("name_label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("name_placeholder")} {...field} autoFocus />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("description_label")}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
({tCommon("optional")})
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("description_placeholder")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="flex gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{tCommon("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t("creating")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("create_button")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
1
surfsense_web/components/layout/ui/dialogs/index.ts
Normal file
1
surfsense_web/components/layout/ui/dialogs/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { CreateSearchSpaceDialog } from "./CreateSearchSpaceDialog";
|
||||
|
|
@ -25,7 +25,7 @@ export function Header({
|
|||
{/* Left side - Mobile menu trigger + Breadcrumb */}
|
||||
<div className="flex flex-1 items-center gap-2 min-w-0">
|
||||
{mobileMenuTrigger}
|
||||
{breadcrumb}
|
||||
<div className="hidden md:block">{breadcrumb}</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Actions */}
|
||||
|
|
|
|||
|
|
@ -5,34 +5,34 @@ import { Button } from "@/components/ui/button";
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Workspace } from "../../types/layout.types";
|
||||
import { WorkspaceAvatar } from "./WorkspaceAvatar";
|
||||
import type { SearchSpace } from "../../types/layout.types";
|
||||
import { SearchSpaceAvatar } from "./SearchSpaceAvatar";
|
||||
|
||||
interface IconRailProps {
|
||||
workspaces: Workspace[];
|
||||
activeWorkspaceId: number | null;
|
||||
onWorkspaceSelect: (id: number) => void;
|
||||
onAddWorkspace: () => void;
|
||||
searchSpaces: SearchSpace[];
|
||||
activeSearchSpaceId: number | null;
|
||||
onSearchSpaceSelect: (id: number) => void;
|
||||
onAddSearchSpace: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function IconRail({
|
||||
workspaces,
|
||||
activeWorkspaceId,
|
||||
onWorkspaceSelect,
|
||||
onAddWorkspace,
|
||||
searchSpaces,
|
||||
activeSearchSpaceId,
|
||||
onSearchSpaceSelect,
|
||||
onAddSearchSpace,
|
||||
className,
|
||||
}: IconRailProps) {
|
||||
return (
|
||||
<div className={cn("flex h-full w-14 flex-col items-center", className)}>
|
||||
<ScrollArea className="w-full">
|
||||
<div className="flex flex-col items-center gap-2 px-1.5 py-3">
|
||||
{workspaces.map((workspace) => (
|
||||
<WorkspaceAvatar
|
||||
key={workspace.id}
|
||||
name={workspace.name}
|
||||
isActive={workspace.id === activeWorkspaceId}
|
||||
onClick={() => onWorkspaceSelect(workspace.id)}
|
||||
{searchSpaces.map((searchSpace) => (
|
||||
<SearchSpaceAvatar
|
||||
key={searchSpace.id}
|
||||
name={searchSpace.name}
|
||||
isActive={searchSpace.id === activeSearchSpaceId}
|
||||
onClick={() => onSearchSpaceSelect(searchSpace.id)}
|
||||
size="md"
|
||||
/>
|
||||
))}
|
||||
|
|
@ -42,15 +42,15 @@ export function IconRail({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onAddWorkspace}
|
||||
onClick={onAddSearchSpace}
|
||||
className="h-10 w-10 rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-muted-foreground/50"
|
||||
>
|
||||
<Plus className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="sr-only">Add workspace</span>
|
||||
<span className="sr-only">Add search space</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Add workspace
|
||||
Add search space
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface WorkspaceAvatarProps {
|
||||
interface SearchSpaceAvatarProps {
|
||||
name: string;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
|
|
@ -11,7 +11,7 @@ interface WorkspaceAvatarProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generates a consistent color based on workspace name
|
||||
* Generates a consistent color based on search space name
|
||||
*/
|
||||
function stringToColor(str: string): string {
|
||||
let hash = 0;
|
||||
|
|
@ -32,7 +32,7 @@ function stringToColor(str: string): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets initials from workspace name (max 2 chars)
|
||||
* Gets initials from search space name (max 2 chars)
|
||||
*/
|
||||
function getInitials(name: string): string {
|
||||
const words = name.trim().split(/\s+/);
|
||||
|
|
@ -42,7 +42,12 @@ function getInitials(name: string): string {
|
|||
return name.slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
export function WorkspaceAvatar({ name, isActive, onClick, size = "md" }: WorkspaceAvatarProps) {
|
||||
export function SearchSpaceAvatar({
|
||||
name,
|
||||
isActive,
|
||||
onClick,
|
||||
size = "md",
|
||||
}: SearchSpaceAvatarProps) {
|
||||
const bgColor = stringToColor(name);
|
||||
const initials = getInitials(name);
|
||||
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export { IconRail } from "./IconRail";
|
||||
export { NavIcon } from "./NavIcon";
|
||||
export { WorkspaceAvatar } from "./WorkspaceAvatar";
|
||||
export { SearchSpaceAvatar } from "./SearchSpaceAvatar";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export { CreateSearchSpaceDialog } from "./dialogs";
|
||||
export { Header } from "./header";
|
||||
export { IconRail, NavIcon, WorkspaceAvatar } from "./icon-rail";
|
||||
export { IconRail, NavIcon, SearchSpaceAvatar } from "./icon-rail";
|
||||
export { AllSearchSpacesSheet } from "./sheets";
|
||||
export { LayoutShell } from "./shell";
|
||||
export {
|
||||
ChatListItem,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,241 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Calendar,
|
||||
MoreHorizontal,
|
||||
Search,
|
||||
Settings,
|
||||
Share2,
|
||||
Trash2,
|
||||
UserCheck,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import type { SearchSpace } from "../../types/layout.types";
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
interface AllSearchSpacesSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaces: SearchSpace[];
|
||||
onSearchSpaceSelect: (id: number) => void;
|
||||
onCreateNew?: () => void;
|
||||
onSettings?: (id: number) => void;
|
||||
onDelete?: (id: number) => void;
|
||||
}
|
||||
|
||||
export function AllSearchSpacesSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
searchSpaces,
|
||||
onSearchSpaceSelect,
|
||||
onCreateNew,
|
||||
onSettings,
|
||||
onDelete,
|
||||
}: AllSearchSpacesSheetProps) {
|
||||
const t = useTranslations("searchSpace");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
const [spaceToDelete, setSpaceToDelete] = useState<SearchSpace | null>(null);
|
||||
|
||||
const handleSelect = (id: number) => {
|
||||
onSearchSpaceSelect(id);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleSettings = (e: React.MouseEvent, space: SearchSpace) => {
|
||||
e.stopPropagation();
|
||||
onOpenChange(false);
|
||||
onSettings?.(space.id);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent, space: SearchSpace) => {
|
||||
e.stopPropagation();
|
||||
setSpaceToDelete(space);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (spaceToDelete) {
|
||||
onDelete?.(spaceToDelete.id);
|
||||
setSpaceToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="right" className="w-full sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Search className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<SheetTitle>{t("all_search_spaces")}</SheetTitle>
|
||||
<SheetDescription>
|
||||
{t("search_spaces_count", { count: searchSpaces.length })}
|
||||
</SheetDescription>
|
||||
</div>
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-3 overflow-y-auto px-4 pb-4">
|
||||
{searchSpaces.length === 0 ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 py-12 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
||||
<Search className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-medium">{t("no_search_spaces")}</p>
|
||||
<p className="text-sm text-muted-foreground">{t("create_first_search_space")}</p>
|
||||
</div>
|
||||
{onCreateNew && (
|
||||
<Button onClick={onCreateNew} className="mt-2">
|
||||
{t("create_button")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
searchSpaces.map((space) => (
|
||||
<button
|
||||
key={space.id}
|
||||
type="button"
|
||||
onClick={() => handleSelect(space.id)}
|
||||
className="flex w-full flex-col gap-2 rounded-lg border p-4 text-left transition-colors hover:bg-accent hover:border-accent-foreground/20"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
<span className="font-medium leading-tight">{space.name}</span>
|
||||
{space.description && (
|
||||
<span className="text-sm text-muted-foreground line-clamp-2">
|
||||
{space.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{space.memberCount > 1 && (
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
<Share2 className="mr-1 h-3 w-3" />
|
||||
{tCommon("shared")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{space.isOwner && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 shrink-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={(e) => handleSettings(e, space)}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{tCommon("settings")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => handleDeleteClick(e, space)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{tCommon("delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
{space.isOwner ? (
|
||||
<UserCheck className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("members_count", { count: space.memberCount })}
|
||||
</span>
|
||||
{space.createdAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
{formatDate(space.createdAt)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{searchSpaces.length > 0 && onCreateNew && (
|
||||
<div className="border-t p-4">
|
||||
<Button onClick={onCreateNew} variant="outline" className="w-full">
|
||||
{t("create_new_search_space")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<AlertDialog open={!!spaceToDelete} onOpenChange={(open) => !open && setSpaceToDelete(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("delete_title")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("delete_confirm", { name: spaceToDelete?.name ?? "" })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{tCommon("delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
surfsense_web/components/layout/ui/sheets/index.ts
Normal file
1
surfsense_web/components/layout/ui/sheets/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { AllSearchSpacesSheet } from "./AllSearchSpacesSheet";
|
||||
|
|
@ -10,19 +10,19 @@ import type {
|
|||
NavItem,
|
||||
NoteItem,
|
||||
PageUsage,
|
||||
SearchSpace,
|
||||
User,
|
||||
Workspace,
|
||||
} from "../../types/layout.types";
|
||||
import { Header } from "../header";
|
||||
import { IconRail } from "../icon-rail";
|
||||
import { MobileSidebar, MobileSidebarTrigger, Sidebar } from "../sidebar";
|
||||
|
||||
interface LayoutShellProps {
|
||||
workspaces: Workspace[];
|
||||
activeWorkspaceId: number | null;
|
||||
onWorkspaceSelect: (id: number) => void;
|
||||
onAddWorkspace: () => void;
|
||||
workspace: Workspace | null;
|
||||
searchSpaces: SearchSpace[];
|
||||
activeSearchSpaceId: number | null;
|
||||
onSearchSpaceSelect: (id: number) => void;
|
||||
onAddSearchSpace: () => void;
|
||||
searchSpace: SearchSpace | null;
|
||||
navItems: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
chats: ChatItem[];
|
||||
|
|
@ -39,8 +39,9 @@ interface LayoutShellProps {
|
|||
onViewAllNotes?: () => void;
|
||||
user: User;
|
||||
onSettings?: () => void;
|
||||
onInviteMembers?: () => void;
|
||||
onSeeAllWorkspaces?: () => void;
|
||||
onManageMembers?: () => void;
|
||||
onSeeAllSearchSpaces?: () => void;
|
||||
onUserSettings?: () => void;
|
||||
onLogout?: () => void;
|
||||
pageUsage?: PageUsage;
|
||||
breadcrumb?: React.ReactNode;
|
||||
|
|
@ -54,11 +55,11 @@ interface LayoutShellProps {
|
|||
}
|
||||
|
||||
export function LayoutShell({
|
||||
workspaces,
|
||||
activeWorkspaceId,
|
||||
onWorkspaceSelect,
|
||||
onAddWorkspace,
|
||||
workspace,
|
||||
searchSpaces,
|
||||
activeSearchSpaceId,
|
||||
onSearchSpaceSelect,
|
||||
onAddSearchSpace,
|
||||
searchSpace,
|
||||
navItems,
|
||||
onNavItemClick,
|
||||
chats,
|
||||
|
|
@ -75,8 +76,9 @@ export function LayoutShell({
|
|||
onViewAllNotes,
|
||||
user,
|
||||
onSettings,
|
||||
onInviteMembers,
|
||||
onSeeAllWorkspaces,
|
||||
onManageMembers,
|
||||
onSeeAllSearchSpaces,
|
||||
onUserSettings,
|
||||
onLogout,
|
||||
pageUsage,
|
||||
breadcrumb,
|
||||
|
|
@ -108,11 +110,11 @@ export function LayoutShell({
|
|||
<MobileSidebar
|
||||
isOpen={mobileMenuOpen}
|
||||
onOpenChange={setMobileMenuOpen}
|
||||
workspaces={workspaces}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
onWorkspaceSelect={onWorkspaceSelect}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
workspace={workspace}
|
||||
searchSpaces={searchSpaces}
|
||||
activeSearchSpaceId={activeSearchSpaceId}
|
||||
onSearchSpaceSelect={onSearchSpaceSelect}
|
||||
onAddSearchSpace={onAddSearchSpace}
|
||||
searchSpace={searchSpace}
|
||||
navItems={navItems}
|
||||
onNavItemClick={onNavItemClick}
|
||||
chats={chats}
|
||||
|
|
@ -129,8 +131,9 @@ export function LayoutShell({
|
|||
onViewAllNotes={onViewAllNotes}
|
||||
user={user}
|
||||
onSettings={onSettings}
|
||||
onInviteMembers={onInviteMembers}
|
||||
onSeeAllWorkspaces={onSeeAllWorkspaces}
|
||||
onManageMembers={onManageMembers}
|
||||
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
||||
onUserSettings={onUserSettings}
|
||||
onLogout={onLogout}
|
||||
pageUsage={pageUsage}
|
||||
/>
|
||||
|
|
@ -149,16 +152,16 @@ export function LayoutShell({
|
|||
<div className={cn("flex h-screen w-full gap-2 p-2 overflow-hidden bg-muted/40", className)}>
|
||||
<div className="hidden md:flex overflow-hidden">
|
||||
<IconRail
|
||||
workspaces={workspaces}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
onWorkspaceSelect={onWorkspaceSelect}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
searchSpaces={searchSpaces}
|
||||
activeSearchSpaceId={activeSearchSpaceId}
|
||||
onSearchSpaceSelect={onSearchSpaceSelect}
|
||||
onAddSearchSpace={onAddSearchSpace}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 rounded-xl border bg-background overflow-hidden">
|
||||
<Sidebar
|
||||
workspace={workspace}
|
||||
searchSpace={searchSpace}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={toggleCollapsed}
|
||||
navItems={navItems}
|
||||
|
|
@ -177,8 +180,9 @@ export function LayoutShell({
|
|||
onViewAllNotes={onViewAllNotes}
|
||||
user={user}
|
||||
onSettings={onSettings}
|
||||
onInviteMembers={onInviteMembers}
|
||||
onSeeAllWorkspaces={onSeeAllWorkspaces}
|
||||
onManageMembers={onManageMembers}
|
||||
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
||||
onUserSettings={onUserSettings}
|
||||
onLogout={onLogout}
|
||||
pageUsage={pageUsage}
|
||||
className="hidden md:flex border-r shrink-0"
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import type {
|
|||
NavItem,
|
||||
NoteItem,
|
||||
PageUsage,
|
||||
SearchSpace,
|
||||
User,
|
||||
Workspace,
|
||||
} from "../../types/layout.types";
|
||||
import { IconRail } from "../icon-rail";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
|
@ -18,11 +18,11 @@ import { Sidebar } from "./Sidebar";
|
|||
interface MobileSidebarProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workspaces: Workspace[];
|
||||
activeWorkspaceId: number | null;
|
||||
onWorkspaceSelect: (id: number) => void;
|
||||
onAddWorkspace: () => void;
|
||||
workspace: Workspace | null;
|
||||
searchSpaces: SearchSpace[];
|
||||
activeSearchSpaceId: number | null;
|
||||
onSearchSpaceSelect: (id: number) => void;
|
||||
onAddSearchSpace: () => void;
|
||||
searchSpace: SearchSpace | null;
|
||||
navItems: NavItem[];
|
||||
onNavItemClick?: (item: NavItem) => void;
|
||||
chats: ChatItem[];
|
||||
|
|
@ -39,8 +39,9 @@ interface MobileSidebarProps {
|
|||
onViewAllNotes?: () => void;
|
||||
user: User;
|
||||
onSettings?: () => void;
|
||||
onInviteMembers?: () => void;
|
||||
onSeeAllWorkspaces?: () => void;
|
||||
onManageMembers?: () => void;
|
||||
onSeeAllSearchSpaces?: () => void;
|
||||
onUserSettings?: () => void;
|
||||
onLogout?: () => void;
|
||||
pageUsage?: PageUsage;
|
||||
}
|
||||
|
|
@ -57,11 +58,11 @@ export function MobileSidebarTrigger({ onClick }: { onClick: () => void }) {
|
|||
export function MobileSidebar({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
workspaces,
|
||||
activeWorkspaceId,
|
||||
onWorkspaceSelect,
|
||||
onAddWorkspace,
|
||||
workspace,
|
||||
searchSpaces,
|
||||
activeSearchSpaceId,
|
||||
onSearchSpaceSelect,
|
||||
onAddSearchSpace,
|
||||
searchSpace,
|
||||
navItems,
|
||||
onNavItemClick,
|
||||
chats,
|
||||
|
|
@ -78,13 +79,14 @@ export function MobileSidebar({
|
|||
onViewAllNotes,
|
||||
user,
|
||||
onSettings,
|
||||
onInviteMembers,
|
||||
onSeeAllWorkspaces,
|
||||
onManageMembers,
|
||||
onSeeAllSearchSpaces,
|
||||
onUserSettings,
|
||||
onLogout,
|
||||
pageUsage,
|
||||
}: MobileSidebarProps) {
|
||||
const handleWorkspaceSelect = (id: number) => {
|
||||
onWorkspaceSelect(id);
|
||||
const handleSearchSpaceSelect = (id: number) => {
|
||||
onSearchSpaceSelect(id);
|
||||
};
|
||||
|
||||
const handleNavItemClick = (item: NavItem) => {
|
||||
|
|
@ -110,17 +112,17 @@ export function MobileSidebar({
|
|||
<div className="shrink-0 border-r bg-muted/40">
|
||||
<ScrollArea className="h-full">
|
||||
<IconRail
|
||||
workspaces={workspaces}
|
||||
activeWorkspaceId={activeWorkspaceId}
|
||||
onWorkspaceSelect={handleWorkspaceSelect}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
searchSpaces={searchSpaces}
|
||||
activeSearchSpaceId={activeSearchSpaceId}
|
||||
onSearchSpaceSelect={handleSearchSpaceSelect}
|
||||
onAddSearchSpace={onAddSearchSpace}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Sidebar
|
||||
workspace={workspace}
|
||||
searchSpace={searchSpace}
|
||||
isCollapsed={false}
|
||||
navItems={navItems}
|
||||
onNavItemClick={handleNavItemClick}
|
||||
|
|
@ -141,8 +143,9 @@ export function MobileSidebar({
|
|||
onViewAllNotes={onViewAllNotes}
|
||||
user={user}
|
||||
onSettings={onSettings}
|
||||
onInviteMembers={onInviteMembers}
|
||||
onSeeAllWorkspaces={onSeeAllWorkspaces}
|
||||
onManageMembers={onManageMembers}
|
||||
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
||||
onUserSettings={onUserSettings}
|
||||
onLogout={onLogout}
|
||||
pageUsage={pageUsage}
|
||||
className="w-full border-none"
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import type {
|
|||
NavItem,
|
||||
NoteItem,
|
||||
PageUsage,
|
||||
SearchSpace,
|
||||
User,
|
||||
Workspace,
|
||||
} from "../../types/layout.types";
|
||||
import { ChatListItem } from "./ChatListItem";
|
||||
import { NavSection } from "./NavSection";
|
||||
|
|
@ -24,7 +24,7 @@ import { SidebarSection } from "./SidebarSection";
|
|||
import { SidebarUserProfile } from "./SidebarUserProfile";
|
||||
|
||||
interface SidebarProps {
|
||||
workspace: Workspace | null;
|
||||
searchSpace: SearchSpace | null;
|
||||
isCollapsed?: boolean;
|
||||
onToggleCollapse?: () => void;
|
||||
navItems: NavItem[];
|
||||
|
|
@ -43,15 +43,16 @@ interface SidebarProps {
|
|||
onViewAllNotes?: () => void;
|
||||
user: User;
|
||||
onSettings?: () => void;
|
||||
onInviteMembers?: () => void;
|
||||
onSeeAllWorkspaces?: () => void;
|
||||
onManageMembers?: () => void;
|
||||
onSeeAllSearchSpaces?: () => void;
|
||||
onUserSettings?: () => void;
|
||||
onLogout?: () => void;
|
||||
pageUsage?: PageUsage;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
workspace,
|
||||
searchSpace,
|
||||
isCollapsed = false,
|
||||
onToggleCollapse,
|
||||
navItems,
|
||||
|
|
@ -70,8 +71,9 @@ export function Sidebar({
|
|||
onViewAllNotes,
|
||||
user,
|
||||
onSettings,
|
||||
onInviteMembers,
|
||||
onSeeAllWorkspaces,
|
||||
onManageMembers,
|
||||
onSeeAllSearchSpaces,
|
||||
onUserSettings,
|
||||
onLogout,
|
||||
pageUsage,
|
||||
className,
|
||||
|
|
@ -86,7 +88,7 @@ export function Sidebar({
|
|||
className
|
||||
)}
|
||||
>
|
||||
{/* Header - workspace name or collapse button when collapsed */}
|
||||
{/* Header - search space name or collapse button when collapsed */}
|
||||
{isCollapsed ? (
|
||||
<div className="flex h-14 shrink-0 items-center justify-center border-b">
|
||||
<SidebarCollapseButton
|
||||
|
|
@ -97,11 +99,11 @@ export function Sidebar({
|
|||
) : (
|
||||
<div className="flex h-14 shrink-0 items-center justify-between px-1 border-b">
|
||||
<SidebarHeader
|
||||
workspace={workspace}
|
||||
searchSpace={searchSpace}
|
||||
isCollapsed={isCollapsed}
|
||||
onSettings={onSettings}
|
||||
onInviteMembers={onInviteMembers}
|
||||
onSeeAllWorkspaces={onSeeAllWorkspaces}
|
||||
onManageMembers={onManageMembers}
|
||||
onSeeAllSearchSpaces={onSeeAllSearchSpaces}
|
||||
/>
|
||||
<div className="">
|
||||
<SidebarCollapseButton
|
||||
|
|
@ -287,7 +289,12 @@ export function Sidebar({
|
|||
<PageUsageDisplay pagesUsed={pageUsage.pagesUsed} pagesLimit={pageUsage.pagesLimit} />
|
||||
)}
|
||||
|
||||
<SidebarUserProfile user={user} onLogout={onLogout} isCollapsed={isCollapsed} />
|
||||
<SidebarUserProfile
|
||||
user={user}
|
||||
onUserSettings={onUserSettings}
|
||||
onLogout={onLogout}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ChevronsUpDown, LayoutGrid, Settings, UserPlus } from "lucide-react";
|
||||
import { ChevronsUpDown, LayoutGrid, Settings, Users } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -11,23 +11,23 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Workspace } from "../../types/layout.types";
|
||||
import type { SearchSpace } from "../../types/layout.types";
|
||||
|
||||
interface SidebarHeaderProps {
|
||||
workspace: Workspace | null;
|
||||
searchSpace: SearchSpace | null;
|
||||
isCollapsed?: boolean;
|
||||
onSettings?: () => void;
|
||||
onInviteMembers?: () => void;
|
||||
onSeeAllWorkspaces?: () => void;
|
||||
onManageMembers?: () => void;
|
||||
onSeeAllSearchSpaces?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SidebarHeader({
|
||||
workspace,
|
||||
searchSpace,
|
||||
isCollapsed,
|
||||
onSettings,
|
||||
onInviteMembers,
|
||||
onSeeAllWorkspaces,
|
||||
onManageMembers,
|
||||
onSeeAllSearchSpaces,
|
||||
className,
|
||||
}: SidebarHeaderProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
|
|
@ -43,24 +43,26 @@ export function SidebarHeader({
|
|||
isCollapsed ? "w-10" : "w-50"
|
||||
)}
|
||||
>
|
||||
<span className="truncate text-base">{workspace?.name ?? t("select_workspace")}</span>
|
||||
<span className="truncate text-base">
|
||||
{searchSpace?.name ?? t("select_search_space")}
|
||||
</span>
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuItem onClick={onInviteMembers}>
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
{t("invite_members")}
|
||||
<DropdownMenuItem onClick={onManageMembers}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
{t("manage_members")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onSettings}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{t("workspace_settings")}
|
||||
{t("search_space_settings")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onSeeAllWorkspaces}>
|
||||
<DropdownMenuItem onClick={onSeeAllSearchSpaces}>
|
||||
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||
{t("see_all_workspaces")}
|
||||
{t("see_all_search_spaces")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ChevronUp, LogOut } from "lucide-react";
|
||||
import { ChevronUp, LogOut, Settings } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -16,6 +16,7 @@ import type { User } from "../../types/layout.types";
|
|||
|
||||
interface SidebarUserProfileProps {
|
||||
user: User;
|
||||
onUserSettings?: () => void;
|
||||
onLogout?: () => void;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
|
@ -62,6 +63,7 @@ function getInitials(email: string): string {
|
|||
|
||||
export function SidebarUserProfile({
|
||||
user,
|
||||
onUserSettings,
|
||||
onLogout,
|
||||
isCollapsed = false,
|
||||
}: SidebarUserProfileProps) {
|
||||
|
|
@ -117,6 +119,13 @@ export function SidebarUserProfile({
|
|||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={onUserSettings}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{t("user_settings")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={onLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
{t("logout")}
|
||||
|
|
@ -177,6 +186,13 @@ export function SidebarUserProfile({
|
|||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={onUserSettings}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
{t("user_settings")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={onLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
{t("logout")}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue