This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-28 15:54:10 -08:00
commit 35904ba0c8
43 changed files with 1019 additions and 663 deletions

View file

@ -40,8 +40,8 @@ export function GoogleLoginButton() {
return (
<div className="relative w-full overflow-hidden">
<AmbientBackground />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
<Logo className="rounded-full my-8" />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center px-6 md:px-0">
<Logo className="h-16 w-16 md:h-32 md:w-32 rounded-full my-4 md:my-8 transition-all" />
{/* <h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
Login
</h1> */}
@ -93,7 +93,7 @@ export function GoogleLoginButton() {
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="group/btn relative flex w-full items-center justify-center space-x-2 rounded-lg bg-white px-6 py-4 text-neutral-700 shadow-lg transition-all duration-200 hover:shadow-xl dark:bg-neutral-800 dark:text-neutral-200"
className="group/btn relative flex w-full items-center justify-center space-x-2 rounded-lg bg-white px-6 py-3 md:py-4 text-neutral-700 shadow-lg transition-all duration-200 hover:shadow-xl dark:bg-neutral-800 dark:text-neutral-200"
onClick={handleGoogleLogin}
>
<div className="absolute inset-0 h-full w-full transform opacity-0 transition duration-200 group-hover/btn:opacity-100">

View file

@ -118,8 +118,8 @@ export function LocalLoginForm() {
};
return (
<div className="w-full max-w-md">
<form onSubmit={handleSubmit} className="space-y-4">
<div className="w-full max-w-md px-6 md:px-0">
<form onSubmit={handleSubmit} className="space-y-3 md:space-y-4">
{/* Error Display */}
<AnimatePresence>
{error && error.title && (
@ -194,7 +194,7 @@ export function LocalLoginForm() {
required
value={username}
onChange={(e) => setUsername(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -217,7 +217,7 @@ export function LocalLoginForm() {
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border pr-10 px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
className={`mt-1 block w-full rounded-md border pr-10 px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -238,7 +238,7 @@ export function LocalLoginForm() {
<button
type="submit"
disabled={isLoggingIn}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
className="w-full rounded-md bg-blue-600 px-4 py-1.5 md:py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base"
>
{isLoggingIn ? tCommon("loading") : t("sign_in")}
</button>

View file

@ -85,7 +85,7 @@ function LoginContent() {
// Get the auth type from environment variables
setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE");
setIsLoading(false);
}, [searchParams]);
}, [searchParams, t, tCommon]);
// Show loading state while determining auth type
if (isLoading) {
@ -111,8 +111,8 @@ function LoginContent() {
<div className="relative w-full overflow-hidden">
<AmbientBackground />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
<Logo className="rounded-md" />
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
<Logo className="h-16 w-16 md:h-32 md:w-32 rounded-md transition-all" />
<h1 className="mt-4 mb-6 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:mt-8 md:mb-8 md:text-3xl lg:text-4xl transition-all">
{t("sign_in")}
</h1>

View file

@ -157,17 +157,17 @@ export default function RegisterPage() {
return (
<div className="relative w-full overflow-hidden">
<AmbientBackground />
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
<Logo className="rounded-md" />
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center px-6 md:px-0">
<Logo className="h-16 w-16 md:h-32 md:w-32 rounded-md transition-all" />
<h1 className="mt-4 mb-6 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:mt-8 md:mb-8 md:text-3xl lg:text-4xl transition-all">
{t("create_account")}
</h1>
<div className="w-full max-w-md">
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-3 md:space-y-4">
{/* Enhanced Error Display */}
<AnimatePresence>
{error && error.title && (
{error?.title && (
<motion.div
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
@ -239,7 +239,7 @@ export default function RegisterPage() {
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -261,7 +261,7 @@ export default function RegisterPage() {
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -283,7 +283,7 @@ export default function RegisterPage() {
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${
className={`mt-1 block w-full rounded-md border px-3 py-1.5 md:py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-all ${
error.title
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
@ -295,7 +295,7 @@ export default function RegisterPage() {
<button
type="submit"
disabled={isRegistering}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
className="w-full rounded-md bg-blue-600 px-4 py-1.5 md:py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base"
>
{isRegistering ? t("creating_account_btn") : t("register")}
</button>

View file

@ -241,7 +241,7 @@ export function DashboardClientLayout({
return (
<SidebarProvider
className="h-full bg-red-600 overflow-hidden"
className="h-full overflow-hidden"
open={open}
onOpenChange={setOpen}
>
@ -257,8 +257,10 @@ export function DashboardClientLayout({
<div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
<div className="hidden md:flex items-center gap-2">
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
</div>
</div>
<div className="flex items-center gap-2">
<LanguageSwitcher />

View file

@ -278,14 +278,17 @@ export default function ConnectorsPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-8 flex items-center justify-between"
className="mb-8 flex items-center justify-between gap-2"
>
<div>
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
<p className="text-muted-foreground mt-2">{t("subtitle")}</p>
<h1 className="text-xl md:text-3xl font-bold tracking-tight">{t("title")}</h1>
<p className="text-xs md:text-base text-muted-foreground mt-2">{t("subtitle")}</p>
</div>
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}>
<Plus className="mr-2 h-4 w-4" />
<Button
className="h-8 text-xs px-3 md:h-10 md:text-sm md:px-4"
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
>
<Plus className="mr-2 h-3 w-3 md:h-4 md:w-4" />
{t("add_connector")}
</Button>
</motion.div>

View file

@ -75,14 +75,14 @@ export function DocumentsFilters({
return (
<motion.div
className="flex flex-wrap items-center justify-between gap-3"
className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.1 }}
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
<motion.div
className="relative"
className="relative w-full sm:w-auto"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
@ -90,7 +90,7 @@ export function DocumentsFilters({
<Input
id={`${id}-input`}
ref={inputRef}
className="peer min-w-60 ps-9"
className="peer w-full sm:min-w-60 ps-9"
value={searchValue}
onChange={(e) => onSearch(e.target.value)}
placeholder={t("filter_placeholder")}
@ -231,11 +231,11 @@ export function DocumentsFilters({
</DropdownMenu>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
{selectedIds.size > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="ml-auto" variant="outline">
<Button className="w-full sm:w-auto" variant="outline">
<Trash
className="-ms-1 me-2 opacity-60"
size={16}

View file

@ -83,8 +83,14 @@ export function DocumentsTableShell({
const toggleAll = (checked: boolean) => {
const next = new Set(selectedIds);
if (checked) sorted.forEach((d) => next.add(d.id));
else sorted.forEach((d) => next.delete(d.id));
if (checked)
sorted.forEach((d) => {
next.add(d.id);
});
else
sorted.forEach((d) => {
next.delete(d.id);
});
setSelectedIds(next);
};
@ -323,26 +329,16 @@ export function DocumentsTableShell({
const icon = getDocumentTypeIcon(doc.document_type);
return (
<div key={doc.id} className="p-3">
<div className="flex items-start gap-3">
<div className="flex items-center gap-3">
<Checkbox
checked={selectedIds.has(doc.id)}
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
aria-label="Select row"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<span className="text-muted-foreground shrink-0">{icon}</span>
<div className="font-medium truncate">{doc.title}</div>
</div>
<RowActions
document={doc}
deleteDocument={deleteDocument}
refreshDocuments={async () => {
await onRefresh();
}}
searchSpaceId={searchSpaceId as string}
/>
<div className="flex items-center gap-2 min-w-0">
<span className="text-muted-foreground shrink-0">{icon}</span>
<div className="font-medium truncate">{doc.title}</div>
</div>
<div className="mt-1 flex flex-wrap items-center gap-2">
<DocumentTypeChip type={doc.document_type} />
@ -371,6 +367,14 @@ export function DocumentsTableShell({
</div>
)}
</div>
<RowActions
document={doc}
deleteDocument={deleteDocument}
refreshDocuments={async () => {
await onRefresh();
}}
searchSpaceId={searchSpaceId as string}
/>
</div>
</div>
);

View file

@ -6,13 +6,14 @@ import { useTranslations } from "next-intl";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
interface ProcessingIndicatorProps {
activeTasksCount: number;
documentProcessorTasksCount: number;
}
export function ProcessingIndicator({ activeTasksCount }: ProcessingIndicatorProps) {
export function ProcessingIndicator({ documentProcessorTasksCount }: ProcessingIndicatorProps) {
const t = useTranslations("documents");
if (activeTasksCount === 0) return null;
// Only show when there are document_processor tasks (uploads), not connector_indexing_task (periodic reindexing)
if (documentProcessorTasksCount === 0) return null;
return (
<AnimatePresence>
@ -32,7 +33,7 @@ export function ProcessingIndicator({ activeTasksCount }: ProcessingIndicatorPro
{t("processing_documents")}
</AlertTitle>
<AlertDescription className="text-muted-foreground">
{t("active_tasks_count", { count: activeTasksCount })}
{t("active_tasks_count", { count: documentProcessorTasksCount })}
</AlertDescription>
</div>
</div>

View file

@ -1,6 +1,6 @@
"use client";
import { FileText, Pencil, Trash2 } from "lucide-react";
import { FileText, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
@ -16,6 +16,12 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import type { Document } from "./types";
@ -57,53 +63,108 @@ export function RowActions({
return (
<div className="flex items-center justify-end gap-1">
{/* Edit Button */}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={handleEdit}
{/* Desktop Actions */}
<div className="hidden md:flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit Document</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Edit Document</p>
</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={handleEdit}
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit Document</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Edit Document</p>
</TooltipContent>
</Tooltip>
{/* View Metadata Button */}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={() => setIsMetadataOpen(true)}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<FileText className="h-4 w-4" />
<span className="sr-only">View Metadata</span>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-muted/80"
onClick={() => setIsMetadataOpen(true)}
>
<FileText className="h-4 w-4" />
<span className="sr-only">View Metadata</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>View Metadata</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setIsDeleteOpen(true)}
disabled={isDeleting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Delete</p>
</TooltipContent>
</Tooltip>
</div>
{/* Mobile Actions Dropdown */}
<div className="flex md:hidden">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>View Metadata</p>
</TooltipContent>
</Tooltip>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={handleEdit}>
<Pencil className="mr-2 h-4 w-4" />
<span>Edit</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setIsMetadataOpen(true)}>
<FileText className="mr-2 h-4 w-4" />
<span>Metadata</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setIsDeleteOpen(true)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<JsonMetadataViewer
title={document.title}
metadata={document.document_metadata}
@ -111,30 +172,6 @@ export function RowActions({
onOpenChange={setIsMetadataOpen}
/>
{/* Delete Button */}
<Tooltip>
<TooltipTrigger asChild>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setIsDeleteOpen(true)}
disabled={isDeleting}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Delete</span>
</Button>
</motion.div>
</TooltipTrigger>
<TooltipContent side="top">
<p>Delete</p>
</TooltipContent>
</Tooltip>
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
<AlertDialogContent>
<AlertDialogHeader>

View file

@ -139,6 +139,14 @@ export default function DocumentsTable() {
enablePolling: true,
refetchInterval: 5000, // Poll every 5 seconds when tasks are active
});
// Filter active tasks to only include document_processor tasks (uploads via "add sources")
// Exclude connector_indexing_task tasks (periodic reindexing)
const documentProcessorTasks =
summary?.active_tasks.filter((task) => task.source === "document_processor") || [];
const documentProcessorTasksCount = documentProcessorTasks.length;
const activeTasksCount = summary?.active_tasks.length || 0;
const prevActiveTasksCount = useRef(activeTasksCount);
@ -220,8 +228,8 @@ export default function DocumentsTable() {
transition={{ delay: 0.1 }}
>
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-muted-foreground">{t("subtitle")}</p>
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
</div>
<Button onClick={refreshCurrentView} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
@ -229,7 +237,7 @@ export default function DocumentsTable() {
</Button>
</motion.div>
<ProcessingIndicator activeTasksCount={activeTasksCount} />
<ProcessingIndicator documentProcessorTasksCount={documentProcessorTasksCount} />
<DocumentsFilters
typeCounts={typeCounts ?? {}}

View file

@ -21,7 +21,6 @@ import {
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { notesApiService } from "@/lib/apis/notes-api.service";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
@ -130,7 +129,7 @@ export default function EditorPage() {
setError(null);
setHasUnsavedChanges(false);
setLoading(true);
}, [documentId]);
}, []);
// Fetch document content - DIRECT CALL TO FASTAPI
// Skip fetching if this is a new note
@ -427,30 +426,43 @@ export default function EditorPage() {
className="flex flex-col min-h-screen w-full"
>
{/* Toolbar */}
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-4 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-6">
<div className="flex items-center gap-3 flex-1 min-w-0">
<FileText className="h-5 w-5 text-muted-foreground shrink-0" />
<div className="sticky top-0 z-40 flex h-14 md:h-16 shrink-0 items-center gap-2 md:gap-4 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-3 md:px-6">
<div className="flex items-center gap-2 md:gap-3 flex-1 min-w-0">
<FileText className="h-4 w-4 md:h-5 md:w-5 text-muted-foreground shrink-0" />
<div className="flex flex-col min-w-0">
<h1 className="text-lg font-semibold truncate">{displayTitle}</h1>
{hasUnsavedChanges && <p className="text-xs text-muted-foreground">Unsaved changes</p>}
<h1 className="text-base md:text-lg font-semibold truncate">{displayTitle}</h1>
{hasUnsavedChanges && (
<p className="text-[10px] md:text-xs text-muted-foreground">Unsaved changes</p>
)}
</div>
</div>
<Separator orientation="vertical" className="h-6" />
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleBack} disabled={saving} className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back
<Button
variant="outline"
onClick={handleBack}
disabled={saving}
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
>
<ArrowLeft className="h-3.5 w-3.5 md:h-4 md:w-4" />
<span className="text-xs md:text-sm">Back</span>
</Button>
<Button onClick={handleSave} disabled={saving} className="gap-2">
<Button
onClick={handleSave}
disabled={saving}
className="gap-1 md:gap-2 px-2 md:px-4 h-8 md:h-10"
>
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{isNewNote ? "Creating..." : "Saving..."}
<Loader2 className="h-3.5 w-3.5 md:h-4 md:w-4 animate-spin" />
<span className="text-xs md:text-sm">
{isNewNote ? "Creating..." : "Saving..."}
</span>
</>
) : (
<>
<Save className="h-4 w-4" />
Save
<Save className="h-3.5 w-3.5 md:h-4 md:w-4" />
<span className="text-xs md:text-sm">Save</span>
</>
)}
</Button>
@ -459,7 +471,7 @@ export default function EditorPage() {
{/* Editor Container */}
<div className="flex-1 min-h-0 overflow-hidden relative">
<div className="h-full w-full overflow-auto p-6">
<div className="h-full w-full overflow-auto p-3 md:p-6">
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}

View file

@ -506,8 +506,8 @@ export default function LogsManagePage() {
transition={{ delay: 0.1 }}
>
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-muted-foreground">{t("subtitle")}</p>
<h2 className="text-xl md:text-2xl font-bold tracking-tight">{t("title")}</h2>
<p className="text-xs md:text-sm text-muted-foreground">{t("subtitle")}</p>
</div>
<Button onClick={handleRefresh} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
@ -521,48 +521,10 @@ export default function LogsManagePage() {
uniqueLevels={uniqueLevels}
uniqueStatuses={uniqueStatuses}
inputRef={inputRef}
onBulkDelete={handleDeleteRows}
id={id}
/>
{/* Delete Button */}
{table.getSelectedRowModel().rows.length > 0 && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex justify-end"
>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
{t("delete_selected")}
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
{table.getSelectedRowModel().rows.length}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
</div>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
</AlertDialogDescription>
</AlertDialogHeader>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteRows}>{t("delete")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</motion.div>
)}
{/* Logs Table */}
<LogsTable
table={table}
@ -713,29 +675,31 @@ function LogsFilters({
uniqueLevels,
uniqueStatuses,
inputRef,
onBulkDelete,
id,
}: {
table: any;
uniqueLevels: string[];
uniqueStatuses: string[];
inputRef: React.RefObject<HTMLInputElement | null>;
onBulkDelete: () => Promise<void>;
id: string;
}) {
const t = useTranslations("logs");
return (
<motion.div
className="flex flex-wrap items-center justify-between gap-3"
className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
{/* Search Input */}
<motion.div className="relative" variants={fadeInScale}>
<motion.div className="relative w-full sm:w-auto" variants={fadeInScale}>
<Input
ref={inputRef}
className={cn(
"peer min-w-60 ps-9",
"peer w-full sm:min-w-60 ps-9",
Boolean(table.getColumn("message")?.getFilterValue()) && "pe-9"
)}
value={(table.getColumn("message")?.getFilterValue() ?? "") as string}
@ -806,6 +770,39 @@ function LogsFilters({
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-3 w-full sm:w-auto sm:ml-auto">
{table.getSelectedRowModel().rows.length > 0 && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full sm:w-auto" variant="outline">
<Trash className="-ms-1 me-2 opacity-60" size={16} strokeWidth={2} />
{t("delete_selected")}
<span className="-me-1 ms-3 inline-flex h-5 max-h-full items-center rounded border border-border bg-background px-1 font-[inherit] text-[0.625rem] font-medium text-muted-foreground/70">
{table.getSelectedRowModel().rows.length}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-full border border-border">
<CircleAlert className="opacity-80" size={16} strokeWidth={2} />
</div>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirm_title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirm_delete_desc", { count: table.getSelectedRowModel().rows.length })}
</AlertDialogDescription>
</AlertDialogHeader>
</div>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={onBulkDelete}>{t("delete")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</motion.div>
);
}
@ -973,6 +970,7 @@ function LogsTable({
style={{ width: `${header.getSize()}px` }}
className={cn(
"h-12 px-4 py-3",
header.column.id === "select" ? "ps-4 pe-0" : "",
// keep Created At header from wrapping and align it
header.column.id === "created_at" ? "whitespace-nowrap text-right" : ""
)}
@ -1030,7 +1028,8 @@ function LogsTable({
<TableCell
key={cell.id}
className={cn(
"px-4 py-3 align-middle overflow-hidden",
"px-4 py-3 align-middle",
cell.column.id === "select" ? "ps-4 pe-0" : "overflow-hidden",
isCreatedAt
? "whitespace-nowrap text-xs text-muted-foreground text-right"
: "",

View file

@ -87,14 +87,14 @@ function SettingsSidebar({
<aside
className={cn(
"fixed md:relative left-0 top-0 z-50 md:z-auto",
"w-72 shrink-0 border-r border-border bg-background md:bg-muted/20 h-full flex flex-col",
"w-72 shrink-0 bg-background md:bg-muted/20 h-full flex flex-col",
"transition-transform duration-300 ease-out",
"md:translate-x-0",
isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"
)}
>
{/* Header with back button */}
<div className="p-4 border-b border-border flex items-center justify-between">
<div className="p-4 flex items-center justify-between">
<Button
variant="ghost"
onClick={onBackToApp}
@ -176,7 +176,7 @@ function SettingsSidebar({
</nav>
{/* Footer */}
<div className="p-4 border-t border-border">
<div className="p-4">
<p className="text-xs text-muted-foreground text-center">SurfSense Settings</p>
</div>
</aside>
@ -229,12 +229,12 @@ function SettingsContent({
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.1, duration: 0.3 }}
className="flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-xl md:rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/10 shadow-sm shrink-0"
className="flex items-center justify-center w-10 h-10 md:w-14 md:h-14 rounded-lg md:rounded-2xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/10 shadow-sm shrink-0"
>
<Icon className="h-6 w-6 md:h-7 md:w-7 text-primary" />
<Icon className="h-5 w-5 md:h-7 md:w-7 text-primary" />
</motion.div>
<div className="min-w-0">
<h1 className="text-xl md:text-2xl font-bold tracking-tight truncate">
<h1 className="text-lg md:text-2xl font-bold tracking-tight truncate">
{activeItem?.label}
</h1>
</div>

View file

@ -51,11 +51,13 @@ export default function AddSourcesPage() {
>
{/* Header */}
<div className="text-center space-y-2">
<h1 className="text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
<Database className="h-8 w-8" />
<h1 className="text-2xl sm:text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
<Database className="h-6 w-6 sm:h-8 sm:w-8" />
Add Sources
</h1>
<p className="text-muted-foreground text-lg">Add your sources to your search space</p>
<p className="text-muted-foreground text-sm sm:text-lg">
Add your sources to your search space
</p>
</div>
{/* Tabs */}

View file

@ -1,29 +1,15 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import {
type ColumnDef,
type ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import { useAtomValue } from "jotai";
import {
ArrowLeft,
Calendar,
Check,
ChevronDown,
ChevronUp,
Clock,
Copy,
Crown,
Edit2,
ExternalLink,
Hash,
Link2,
LinkIcon,
@ -32,7 +18,6 @@ import {
Plus,
RefreshCw,
Search,
Settings,
Shield,
ShieldCheck,
Trash2,
@ -40,7 +25,6 @@ import {
UserMinus,
UserPlus,
Users,
X,
} from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
@ -90,7 +74,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
@ -105,7 +88,6 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import {
Table,
TableBody,
@ -295,7 +277,7 @@ export default function TeamManagementPage() {
staleTime: 5 * 60 * 1000,
});
const { data: permissionsData, isLoading: permissionsLoading } = useAtomValue(permissionsAtom);
const { data: permissionsData } = useAtomValue(permissionsAtom);
const permissions = permissionsData?.permissions || [];
const groupedPermissions = useMemo(() => {
const groups: Record<string, typeof permissions> = {};
@ -308,8 +290,6 @@ export default function TeamManagementPage() {
return groups;
}, [permissions]);
const canManageMembers = hasPermission("members:view");
const canManageRoles = hasPermission("roles:read");
const canInvite = hasPermission("members:invite");
const handleRefresh = useCallback(async () => {
@ -339,40 +319,44 @@ export default function TeamManagementPage() {
variants={staggerContainer}
className="min-h-screen bg-background"
>
<div className="container max-w-7xl mx-auto p-6 lg:p-8">
<div className="container max-w-7xl mx-auto p-4 md:p-6 lg:p-8">
<motion.div variants={fadeInUp} className="space-y-8">
{/* Header */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-start space-x-3 md:items-center md:space-x-4">
<button
onClick={() => router.push(`/dashboard/${searchSpaceId}`)}
className="flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors"
className="flex items-center justify-center h-9 w-9 md:h-10 md:w-10 rounded-lg bg-primary/10 hover:bg-primary/20 transition-colors shrink-0"
aria-label="Back to Dashboard"
type="button"
>
<ArrowLeft className="h-5 w-5 text-primary" />
<ArrowLeft className="h-4 w-4 md:h-5 md:w-5 text-primary" />
</button>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 ring-1 ring-primary/10">
<Users className="h-6 w-6 text-primary" />
<div className="flex h-10 w-10 md:h-12 md:w-12 items-center justify-center rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 ring-1 ring-primary/10 shrink-0">
<Users className="h-5 w-5 md:h-6 md:w-6 text-primary" />
</div>
<div className="space-y-1">
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
<div className="space-y-1 min-w-0">
<h1 className="text-2xl md:text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text">
Team Management
</h1>
<p className="text-muted-foreground">
<p className="text-xs md:text-sm text-muted-foreground">
Manage members, roles, and invite links for your search space
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button onClick={handleRefresh} variant="outline" size="sm" className="gap-2">
<Button
onClick={handleRefresh}
variant="outline"
size="sm"
className="gap-2 w-full md:w-auto"
>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
</div>
<Separator className="bg-gradient-to-r from-border via-border/50 to-transparent" />
</div>
{/* Summary Cards */}
@ -435,42 +419,55 @@ export default function TeamManagementPage() {
{/* Tabs Content */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<div className="flex items-center justify-between">
<TabsList className="bg-muted/50 p-1">
<TabsTrigger value="members" className="gap-2 data-[state=active]:bg-background">
<Users className="h-4 w-4" />
<span>Members</span>
<Badge variant="secondary" className="ml-1 text-xs">
{members.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="roles" className="gap-2 data-[state=active]:bg-background">
<Shield className="h-4 w-4" />
<span>Roles</span>
<Badge variant="secondary" className="ml-1 text-xs">
{roles.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="invites" className="gap-2 data-[state=active]:bg-background">
<LinkIcon className="h-4 w-4" />
<span>Invites</span>
<Badge variant="secondary" className="ml-1 text-xs">
{invites.filter((i) => i.is_active).length}
</Badge>
</TabsTrigger>
</TabsList>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="overflow-x-auto pb-1 md:pb-0">
<TabsList className="bg-muted/50 p-1 w-full md:w-fit grid grid-cols-3 md:flex">
<TabsTrigger
value="members"
className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
>
<Users className="h-4 w-4 hidden md:block" />
<span>Members</span>
<Badge variant="secondary" className="ml-1 text-xs">
{members.length}
</Badge>
</TabsTrigger>
<TabsTrigger
value="roles"
className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
>
<Shield className="h-4 w-4 hidden md:block" />
<span>Roles</span>
<Badge variant="secondary" className="ml-1 text-xs">
{roles.length}
</Badge>
</TabsTrigger>
<TabsTrigger
value="invites"
className="gap-1.5 md:gap-2 data-[state=active]:bg-background whitespace-nowrap w-full text-xs md:text-sm flex-1"
>
<LinkIcon className="h-4 w-4 hidden md:block" />
<span>Invites</span>
<Badge variant="secondary" className="ml-1 text-xs">
{invites.filter((i) => i.is_active).length}
</Badge>
</TabsTrigger>
</TabsList>
</div>
{activeTab === "invites" && canInvite && (
<CreateInviteDialog
roles={roles}
onCreateInvite={handleCreateInvite}
searchSpaceId={searchSpaceId}
className="w-full md:w-auto"
/>
)}
{activeTab === "roles" && hasPermission("roles:create") && (
<CreateRoleDialog
groupedPermissions={groupedPermissions}
onCreateRole={handleCreateRole}
className="w-full md:w-auto"
/>
)}
</div>
@ -533,8 +530,6 @@ function MembersTab({
canManageRoles: boolean;
canRemove: boolean;
}) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [searchQuery, setSearchQuery] = useState("");
const filteredMembers = useMemo(() => {
@ -575,13 +570,13 @@ function MembersTab({
</div>
{/* Members List */}
<div className="rounded-lg border bg-card overflow-hidden">
<div className="rounded-lg border bg-card overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="w-[300px]">Member</TableHead>
<TableHead>Role</TableHead>
<TableHead>Joined</TableHead>
<TableHead className="w-auto md:w-[300px] px-2 md:px-4">Member</TableHead>
<TableHead className="px-2 md:px-4">Role</TableHead>
<TableHead className="hidden md:table-cell">Joined</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
@ -604,11 +599,11 @@ function MembersTab({
transition={{ delay: index * 0.05 }}
className="group border-b transition-colors hover:bg-muted/50"
>
<TableCell>
<div className="flex items-center gap-3">
<TableCell className="py-2 px-2 md:py-4 md:px-4 align-middle">
<div className="flex items-center gap-1.5 md:gap-3">
<div className="relative">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center ring-2 ring-background">
<User className="h-5 w-5 text-primary" />
<div className="h-8 w-8 md:h-10 md:w-10 rounded-full bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center ring-2 ring-background">
<User className="h-4 w-4 md:h-5 md:w-5 text-primary" />
</div>
{member.is_owner && (
<div className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-amber-500 flex items-center justify-center ring-2 ring-background">
@ -616,12 +611,14 @@ function MembersTab({
</div>
)}
</div>
<div>
<p className="font-medium">{member.user_email || "Unknown"}</p>
<div className="min-w-0">
<p className="font-medium text-xs md:text-sm truncate">
{member.user_email || "Unknown"}
</p>
{member.is_owner && (
<Badge
variant="outline"
className="text-xs mt-1 bg-amber-500/10 text-amber-600 border-amber-500/20"
className="text-[10px] md:text-xs mt-0.5 md:mt-1 bg-amber-500/10 text-amber-600 border-amber-500/20 hidden md:inline-flex"
>
Owner
</Badge>
@ -629,7 +626,7 @@ function MembersTab({
</div>
</div>
</TableCell>
<TableCell>
<TableCell className="py-2 px-2 md:py-4 md:px-4 align-middle">
{canManageRoles && !member.is_owner ? (
<Select
value={member.role_id?.toString() || "none"}
@ -637,7 +634,7 @@ function MembersTab({
onUpdateRole(member.id, value === "none" ? null : Number(value))
}
>
<SelectTrigger className="w-[180px]">
<SelectTrigger className="w-full md:w-[180px] h-8 md:h-10 text-xs md:text-sm">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
@ -653,19 +650,22 @@ function MembersTab({
</SelectContent>
</Select>
) : (
<Badge variant="secondary" className="gap-1">
<Shield className="h-3 w-3" />
<Badge
variant="secondary"
className="gap-1 text-[10px] md:text-xs py-0 md:py-0.5"
>
<Shield className="h-2.5 w-2.5 md:h-3 md:w-3" />
{member.role?.name || "No role"}
</Badge>
)}
</TableCell>
<TableCell>
<TableCell className="hidden md:table-cell">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" />
{new Date(member.joined_at).toLocaleDateString()}
</div>
</TableCell>
<TableCell className="text-right">
<TableCell className="text-right py-2 px-2 md:py-4 md:px-4 align-middle">
{canRemove && !member.is_owner && (
<AlertDialog>
<AlertDialogTrigger asChild>
@ -962,11 +962,11 @@ function InvitesTab({
className={cn("relative overflow-hidden transition-all", isInactive && "opacity-60")}
>
<CardContent className="p-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4 flex-1 min-w-0">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-start md:items-center gap-4 flex-1 min-w-0">
<div
className={cn(
"h-12 w-12 rounded-xl flex items-center justify-center shrink-0",
"h-10 w-10 md:h-12 md:w-12 rounded-xl flex items-center justify-center shrink-0",
invite.is_active && !isExpired && !isMaxedOut
? "bg-emerald-500/20"
: "bg-muted"
@ -974,7 +974,7 @@ function InvitesTab({
>
<Link2
className={cn(
"h-6 w-6",
"h-5 w-5 md:h-6 md:w-6",
invite.is_active && !isExpired && !isMaxedOut
? "text-emerald-600"
: "text-muted-foreground"
@ -991,7 +991,7 @@ function InvitesTab({
)}
{isMaxedOut && (
<Badge variant="secondary" className="text-xs">
Max uses reached
Maxed
</Badge>
)}
{!invite.is_active && !isExpired && !isMaxedOut && (
@ -1000,44 +1000,44 @@ function InvitesTab({
</Badge>
)}
</div>
<div className="flex items-center gap-4 mt-1 text-sm text-muted-foreground flex-wrap">
<div className="flex flex-col md:flex-row md:items-center gap-2 md:gap-4 mt-1 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Shield className="h-3 w-3" />
{invite.role?.name || "Default role"}
</span>
<span className="flex items-center gap-1">
<Hash className="h-3 w-3" />
{invite.uses_count} uses
{invite.max_uses && ` / ${invite.max_uses}`}
{invite.uses_count}
{invite.max_uses ? ` / ${invite.max_uses} uses` : " uses"}
</span>
{invite.expires_at && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{isExpired
? "Expired"
: `Expires ${new Date(invite.expires_at).toLocaleDateString()}`}
: `Exp: ${new Date(invite.expires_at).toLocaleDateString()}`}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="flex items-center gap-2 shrink-0 self-end md:self-auto">
<Button
variant="outline"
size="sm"
className="gap-2"
className="gap-2 flex-1 md:flex-none"
onClick={() => copyInviteLink(invite)}
disabled={Boolean(isInactive)}
>
{copiedId === invite.id ? (
<>
<Check className="h-4 w-4 text-emerald-500" />
Copied!
<span className="md:inline">Copied!</span>
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy Link
<span className="md:inline">Copy</span>
</>
)}
</Button>
@ -1088,11 +1088,11 @@ function InvitesTab({
function CreateInviteDialog({
roles,
onCreateInvite,
searchSpaceId,
className,
}: {
roles: Role[];
onCreateInvite: (data: CreateInviteRequest["data"]) => Promise<Invite>;
searchSpaceId: number;
className?: string;
}) {
const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false);
@ -1142,12 +1142,12 @@ function CreateInviteDialog({
return (
<Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}>
<DialogTrigger asChild>
<Button className="gap-2">
<Button className={cn("gap-2", className)}>
<UserPlus className="h-4 w-4" />
Create Invite
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6">
{createdInvite ? (
<>
<DialogHeader>
@ -1159,7 +1159,7 @@ function CreateInviteDialog({
Share this link to invite people to your search space.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-3 py-2 md:py-4">
<div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
<code className="flex-1 min-w-0 text-sm break-all">
{window.location.origin}/invite/{createdInvite.invite_code}
@ -1203,7 +1203,7 @@ function CreateInviteDialog({
Create a link to invite people to this search space.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-3 py-2 md:py-4">
<div className="space-y-2">
<Label htmlFor="invite-name">Name (optional)</Label>
<Input
@ -1234,7 +1234,7 @@ function CreateInviteDialog({
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col md:grid md:grid-cols-2 gap-3 md:gap-4">
<div className="space-y-2">
<Label htmlFor="max-uses">Max uses (optional)</Label>
<Input
@ -1301,9 +1301,11 @@ function CreateInviteDialog({
function CreateRoleDialog({
groupedPermissions,
onCreateRole,
className,
}: {
groupedPermissions: Record<string, { value: string; name: string; category: string }[]>;
onCreateRole: (data: CreateRoleRequest["data"]) => Promise<Role>;
className?: string;
}) {
const [open, setOpen] = useState(false);
const [creating, setCreating] = useState(false);
@ -1358,20 +1360,20 @@ function CreateRoleDialog({
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<Button className={cn("gap-2", className)}>
<Plus className="h-4 w-4" />
Create Role
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-xl p-4 md:p-6">
<DialogHeader>
<DialogTitle>Create Custom Role</DialogTitle>
<DialogDescription>
<DialogDescription className="text-xs md:text-sm">
Define a new role with specific permissions for this search space.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-3 py-2 md:py-4">
<div className="flex flex-col md:grid md:grid-cols-2 gap-3 md:gap-4">
<div className="space-y-2">
<Label htmlFor="role-name">Role Name *</Label>
<Input

View file

@ -7,7 +7,6 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
@ -38,7 +37,6 @@ import {
} from "@/components/ui/card";
import { Spotlight } from "@/components/ui/spotlight";
import { Tilt } from "@/components/ui/tilt";
import { authenticatedFetch } from "@/lib/auth-utils";
/**
* Formats a date string into a readable format
@ -65,7 +63,7 @@ const LoadingScreen = () => {
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<Card className="w-[350px] bg-background/60 backdrop-blur-sm">
<Card className="w-full max-w-[350px] bg-background/60 backdrop-blur-sm">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium">{t("loading")}</CardTitle>
<CardDescription>{t("fetching_spaces")}</CardDescription>
@ -101,7 +99,7 @@ const ErrorScreen = ({ message }: { message: string }) => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Card className="w-[400px] bg-background/60 backdrop-blur-sm border-destructive/20">
<Card className="w-full max-w-[400px] bg-background/60 backdrop-blur-sm border-destructive/20">
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" />
@ -185,21 +183,21 @@ const DashboardPage = () => {
return (
<motion.div
className="container mx-auto py-10"
className="container mx-auto py-6 md:py-10 px-4"
initial="hidden"
animate="visible"
variants={containerVariants}
>
<motion.div className="flex flex-col space-y-6" variants={itemVariants}>
<div className="flex flex-row space-x-4 justify-between">
<div className="flex flex-row space-x-4">
<Logo className="w-10 h-10 rounded-md" />
<div className="flex flex-col space-y-2">
<h1 className="text-4xl font-bold">{t("surfsense_dashboard")}</h1>
<p className="text-muted-foreground">{t("welcome_message")}</p>
<motion.div className="flex flex-col space-y-4 md:space-y-6" variants={itemVariants}>
<div className="flex flex-row items-center justify-between gap-2">
<div className="flex flex-row items-center md:space-x-4">
<Logo className="w-8 h-8 md:w-10 md:h-10 rounded-md shrink-0 hidden md:block" />
<div className="flex flex-col space-y-0.5 md:space-y-2">
<h1 className="text-xl md:text-4xl font-bold">{t("surfsense_dashboard")}</h1>
<p className="text-sm md:text-base text-muted-foreground">{t("welcome_message")}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 md:space-x-3 shrink-0">
<UserDropdown user={customUser} />
<ThemeTogglerComponent />
</div>
@ -207,18 +205,18 @@ const DashboardPage = () => {
<div className="flex flex-col space-y-6 mt-6">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-semibold">{t("your_search_spaces")}</h2>
<h2 className="text-lg md:text-2xl font-semibold">{t("your_search_spaces")}</h2>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Link href="/dashboard/searchspaces">
<Button className="h-10">
<Plus className="mr-2 h-4 w-4" />
<Button className="h-8 md:h-10 text-[11px] md:text-sm px-3 md:px-4">
<Plus className="mr-1 md:mr-2 h-3 w-3 md:h-4 md:w-4" />
{t("create_search_space")}
</Button>
</Link>
</motion.div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{searchSpaces &&
searchSpaces.length > 0 &&
searchSpaces.map((space) => (
@ -295,14 +293,17 @@ const DashboardPage = () => {
<div className="flex flex-1 flex-col justify-between p-1">
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium text-lg">{space.name}</h3>
<h3 className="font-medium text-base md:text-lg">{space.name}</h3>
{!space.is_owner && (
<Badge variant="secondary" className="text-xs font-normal">
<Badge
variant="secondary"
className="text-[10px] md:text-xs font-normal"
>
{t("shared")}
</Badge>
)}
</div>
<p className="mt-1 text-sm text-muted-foreground">
<p className="mt-1 text-xs md:text-sm text-muted-foreground">
{space.description}
</p>
</div>
@ -334,8 +335,10 @@ const DashboardPage = () => {
<div className="rounded-full bg-muted/50 p-4 mb-4">
<Search className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium mb-2">{t("no_spaces_found")}</h3>
<p className="text-muted-foreground mb-6">{t("create_first_space")}</p>
<h3 className="text-base md:text-lg font-medium mb-2">{t("no_spaces_found")}</h3>
<p className="text-xs md:text-sm text-muted-foreground mb-6">
{t("create_first_space")}
</p>
<Link href="/dashboard/searchspaces">
<Button>
<Plus className="mr-2 h-4 w-4" />
@ -359,8 +362,10 @@ const DashboardPage = () => {
>
<Link href="/dashboard/searchspaces" className="flex h-full">
<div className="flex flex-col items-center justify-center h-full w-full rounded-xl border border-dashed bg-muted/10 hover:border-primary/50 transition-colors">
<Plus className="h-10 w-10 mb-3 text-muted-foreground" />
<span className="text-sm font-medium">{t("add_new_search_space")}</span>
<Plus className="h-8 w-8 md:h-10 md:w-10 mb-2 md:mb-3 text-muted-foreground" />
<span className="text-xs md:text-sm font-medium">
{t("add_new_search_space")}
</span>
</div>
</Link>
</Tilt>

View file

@ -159,3 +159,4 @@ button {
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}';
@source '../node_modules/streamdown/dist/*.js';