merge: upstream/dev with migration renumbering

This commit is contained in:
CREDO23 2026-01-27 11:22:26 +02:00
commit a7145b2c63
176 changed files with 8791 additions and 3608 deletions

View file

@ -1,14 +1,18 @@
"use client";
import { usePathname } from "next/navigation";
import { FooterNew } from "@/components/homepage/footer-new";
import { Navbar } from "@/components/homepage/navbar";
export default function HomePageLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isAuthPage = pathname === "/login" || pathname === "/register";
return (
<main className="min-h-screen bg-linear-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white overflow-x-hidden">
<Navbar />
{children}
<FooterNew />
{!isAuthPage && <FooterNew />}
</main>
);
}

View file

@ -8,6 +8,7 @@ import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
import { Spinner } from "@/components/ui/spinner";
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
import { AUTH_TYPE } from "@/lib/env-config";
import { ValidationError } from "@/lib/error";
@ -42,9 +43,6 @@ export function LocalLoginForm() {
// Track login attempt
trackLoginAttempt("local");
// Show loading toast
const loadingToast = toast.loading(tCommon("loading"));
try {
const data = await login({
username,
@ -62,8 +60,7 @@ export function LocalLoginForm() {
// Success toast
toast.success(t("login_success"), {
id: loadingToast,
description: "Redirecting to dashboard...",
description: "Redirecting to dashboard",
duration: 2000,
});
@ -76,7 +73,6 @@ export function LocalLoginForm() {
trackLoginFailure("local", err.message);
setError({ title: err.name, message: err.message });
toast.error(err.name, {
id: loadingToast,
description: err.message,
duration: 6000,
});
@ -106,7 +102,6 @@ export function LocalLoginForm() {
// Show error toast with conditional retry action
const toastOptions: any = {
id: loadingToast,
description: errorDetails.description,
duration: 6000,
};
@ -244,9 +239,16 @@ export function LocalLoginForm() {
<button
type="submit"
disabled={isLoggingIn}
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"
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 flex items-center justify-center gap-2"
>
{isLoggingIn ? tCommon("loading") : t("sign_in")}
{isLoggingIn ? (
<>
<Spinner size="sm" className="text-white" />
<span>{t("signing_in")}</span>
</>
) : (
t("sign_in")
)}
</button>
</form>

View file

@ -1,12 +1,12 @@
"use client";
import { Loader2 } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner";
import { Logo } from "@/components/Logo";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors";
import { AUTH_TYPE } from "@/lib/env-config";
import { AmbientBackground } from "./AmbientBackground";
@ -66,7 +66,11 @@ function LoginContent() {
});
// Show toast with conditional retry action
const toastOptions: any = {
const toastOptions: {
description: string;
duration: number;
action?: { label: string; onClick: () => void };
} = {
description: errorDescription,
duration: 6000,
};
@ -95,20 +99,12 @@ function LoginContent() {
setIsLoading(false);
}, [searchParams, t, tCommon]);
// Show loading state while determining auth type
// Use global loading screen for auth type determination - spinner animation won't reset
useGlobalLoadingEffect(isLoading, tCommon("loading"), "login");
// Show nothing while loading - the GlobalLoadingProvider handles the loading UI
if (isLoading) {
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" />
<div className="mt-8 flex items-center space-x-2">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
<span className="text-muted-foreground">{tCommon("loading")}</span>
</div>
</div>
</div>
);
return null;
}
if (authType === "GOOGLE") {
@ -189,23 +185,10 @@ function LoginContent() {
);
}
// Loading fallback for Suspense
const LoadingFallback = () => (
<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" />
<div className="mt-8 flex items-center space-x-2">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
<span className="text-muted-foreground">Loading...</span>
</div>
</div>
</div>
);
export default function LoginPage() {
// Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI
return (
<Suspense fallback={<LoadingFallback />}>
<Suspense fallback={null}>
<LoginContent />
</Suspense>
);

View file

@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
import { toast } from "sonner";
import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
import { Logo } from "@/components/Logo";
import { Spinner } from "@/components/ui/spinner";
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
import { AUTH_TYPE } from "@/lib/env-config";
import { AppError, ValidationError } from "@/lib/error";
@ -60,9 +61,6 @@ export default function RegisterPage() {
// Track registration attempt
trackRegistrationAttempt();
// Show loading toast
const loadingToast = toast.loading(t("creating_account"));
try {
await register({
email,
@ -77,7 +75,6 @@ export default function RegisterPage() {
// Success toast
toast.success(t("register_success"), {
id: loadingToast,
description: t("redirecting_login"),
duration: 2000,
});
@ -95,7 +92,6 @@ export default function RegisterPage() {
trackRegistrationFailure("Registration disabled");
setError({ title: "Registration is disabled", message: friendlyMessage });
toast.error("Registration is disabled", {
id: loadingToast,
description: friendlyMessage,
duration: 6000,
});
@ -109,7 +105,6 @@ export default function RegisterPage() {
trackRegistrationFailure(err.message);
setError({ title: err.name, message: err.message });
toast.error(err.name, {
id: loadingToast,
description: err.message,
duration: 6000,
});
@ -137,7 +132,6 @@ export default function RegisterPage() {
// Show error toast with conditional retry action
const toastOptions: any = {
id: loadingToast,
description: errorDetails.description,
duration: 6000,
};
@ -295,9 +289,16 @@ export default function RegisterPage() {
<button
type="submit"
disabled={isRegistering}
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"
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 flex items-center justify-center gap-2"
>
{isRegistering ? t("creating_account_btn") : t("register")}
{isRegistering ? (
<>
<Spinner size="sm" className="text-white" />
<span>{t("creating_account_btn")}</span>
</>
) : (
t("register")
)}
</button>
</form>

View file

@ -0,0 +1,14 @@
"use client";
import { useTranslations } from "next-intl";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
export default function AuthCallbackLoading() {
const t = useTranslations("auth");
// Use global loading - spinner animation won't reset when page transitions
useGlobalLoadingEffect(true, t("processing_authentication"), "default");
// Return null - the GlobalLoadingProvider handles the loading UI
return null;
}

View file

@ -1,23 +1,18 @@
"use client";
import { Suspense } from "react";
import TokenHandler from "@/components/TokenHandler";
export default function AuthCallbackPage() {
// Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI
// TokenHandler uses useGlobalLoadingEffect to show the loading screen
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Authentication Callback</h1>
<Suspense
fallback={
<div className="flex items-center justify-center min-h-[200px]">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
</div>
}
>
<TokenHandler
redirectPath="/dashboard"
tokenParamName="token"
storageKey="surfsense_bearer_token"
/>
</Suspense>
</div>
<Suspense fallback={null}>
<TokenHandler
redirectPath="/dashboard"
tokenParamName="token"
storageKey="surfsense_bearer_token"
/>
</Suspense>
);
}

View file

@ -1,7 +1,6 @@
"use client";
import { useAtomValue, useSetAtom } from "jotai";
import { Loader2 } from "lucide-react";
import { useParams, usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import type React from "react";
@ -19,6 +18,7 @@ import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb";
import { LayoutDataProvider } from "@/components/layout";
import { OnboardingTour } from "@/components/onboarding-tour";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
export function DashboardClientLayout({
children,
@ -146,31 +146,22 @@ export function DashboardClientLayout({
setActiveSearchSpaceIdState(activeSeacrhSpaceId);
}, [search_space_id, setActiveSearchSpaceIdState]);
if (
// Determine if we should show loading
const shouldShowLoading =
(!hasCheckedOnboarding &&
(loading || accessLoading || globalConfigsLoading) &&
!isOnboardingPage) ||
isAutoConfiguring
) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<Card className="w-[350px] bg-background/60 backdrop-blur-sm">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium">
{isAutoConfiguring ? "Setting up AI..." : t("loading_config")}
</CardTitle>
<CardDescription>
{isAutoConfiguring
? "Auto-configuring with available settings"
: t("checking_llm_prefs")}
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center py-6">
<Loader2 className="h-12 w-12 text-primary animate-spin" />
</CardContent>
</Card>
</div>
);
isAutoConfiguring;
// Use global loading screen - spinner animation won't reset
useGlobalLoadingEffect(
shouldShowLoading,
isAutoConfiguring ? t("setting_up_ai") : t("checking_llm_prefs"),
"default"
);
if (shouldShowLoading) {
return null;
}
if (error && !hasCheckedOnboarding && !isOnboardingPage) {

View file

@ -1,6 +1,6 @@
"use client";
import { ChevronDown, ChevronUp, FileX, Loader2, Plus } from "lucide-react";
import { ChevronDown, ChevronUp, FileX, Plus } from "lucide-react";
import { motion } from "motion/react";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
@ -9,6 +9,7 @@ import { useDocumentUploadDialog } from "@/components/assistant-ui/document-uplo
import { DocumentViewer } from "@/components/document-viewer";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Spinner } from "@/components/ui/spinner";
import {
Table,
TableBody,
@ -114,7 +115,7 @@ export function DocumentsTableShell({
{loading ? (
<div className="flex h-[400px] w-full items-center justify-center">
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<Spinner size="lg" className="text-primary" />
<p className="text-sm text-muted-foreground">{t("loading")}</p>
</div>
</div>

View file

@ -209,7 +209,7 @@ export function RowActions({
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? "Deleting..." : "Delete"}
{isDeleting ? "Deleting" : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View file

@ -1,8 +1,7 @@
"use client";
import { useQueryClient } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { AlertCircle, ArrowLeft, FileText, Loader2, Save } from "lucide-react";
import { AlertCircle, ArrowLeft, FileText, Save } from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
@ -21,6 +20,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
import { notesApiService } from "@/lib/apis/notes-api.service";
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
@ -78,7 +78,6 @@ function extractTitleFromBlockNote(blocknoteDocument: BlockNoteDocument): string
export default function EditorPage() {
const params = useParams();
const router = useRouter();
const queryClient = useQueryClient();
const documentId = params.documentId as string;
const searchSpaceId = Number(params.search_space_id);
const isNewNote = documentId === "new";
@ -349,8 +348,8 @@ export default function EditorPage() {
<div className="flex items-center justify-center min-h-[400px] p-6">
<Card className="w-full max-w-md">
<CardContent className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-12 w-12 text-primary animate-spin mb-4" />
<p className="text-muted-foreground">Loading editor...</p>
<Spinner size="xl" className="text-primary mb-4" />
<p className="text-muted-foreground">Loading editor</p>
</CardContent>
</Card>
</div>
@ -437,7 +436,7 @@ export default function EditorPage() {
>
{saving ? (
<>
<Loader2 className="h-3.5 w-3.5 md:h-4 md:w-4 animate-spin" />
<Spinner size="sm" className="h-3.5 w-3.5 md:h-4 md:w-4" />
<span className="text-xs md:text-sm">{isNewNote ? "Creating" : "Saving"}</span>
</>
) : (

View file

@ -0,0 +1,210 @@
"use client";
import { IconCalendar, IconMailFilled } from "@tabler/icons-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Check, ExternalLink, Gift, Loader2, Mail, Star } from "lucide-react";
import { motion } from "motion/react";
import Link from "next/link";
import { useEffect } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import type { IncentiveTaskInfo } from "@/contracts/types/incentive-tasks.types";
import { incentiveTasksApiService } from "@/lib/apis/incentive-tasks-api.service";
import {
trackIncentiveContactOpened,
trackIncentivePageViewed,
trackIncentiveTaskClicked,
trackIncentiveTaskCompleted,
} from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
export default function MorePagesPage() {
const queryClient = useQueryClient();
// Track page view on mount
useEffect(() => {
trackIncentivePageViewed();
}, []);
// Fetch tasks from API
const { data, isLoading } = useQuery({
queryKey: ["incentive-tasks"],
queryFn: () => incentiveTasksApiService.getTasks(),
});
// Mutation to complete a task
const completeMutation = useMutation({
mutationFn: incentiveTasksApiService.completeTask,
onSuccess: (response, taskType) => {
if (response.success) {
toast.success(response.message);
// Track task completion
const task = data?.tasks.find((t) => t.task_type === taskType);
if (task) {
trackIncentiveTaskCompleted(taskType, task.pages_reward);
}
// Invalidate queries to refresh data
queryClient.invalidateQueries({ queryKey: ["incentive-tasks"] });
queryClient.invalidateQueries({ queryKey: ["user"] });
}
},
onError: () => {
toast.error("Failed to complete task. Please try again.");
},
});
const handleTaskClick = (task: IncentiveTaskInfo) => {
if (!task.completed) {
trackIncentiveTaskClicked(task.task_type);
completeMutation.mutate(task.task_type);
}
};
const allCompleted = data?.tasks.every((t) => t.completed) ?? false;
return (
<div className="flex min-h-[calc(100vh-64px)] items-center justify-center px-4 py-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full max-w-md"
>
{/* Header */}
<div className="mb-6 text-center">
<Gift className="mx-auto mb-3 h-8 w-8 text-primary" />
<h2 className="text-xl font-bold tracking-tight">Get More Pages</h2>
<p className="text-sm text-muted-foreground">Complete tasks to earn additional pages</p>
</div>
{/* Tasks */}
{isLoading ? (
<Card>
<CardContent className="flex items-center gap-3 p-3">
<Skeleton className="h-9 w-9 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-1/4" />
</div>
<Skeleton className="h-8 w-16" />
</CardContent>
</Card>
) : (
<div className="space-y-2">
{data?.tasks.map((task) => (
<Card
key={task.task_type}
className={cn("transition-colors", task.completed && "bg-muted/50")}
>
<CardContent className="flex items-center gap-3 p-3">
<div
className={cn(
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full",
task.completed ? "bg-primary text-primary-foreground" : "bg-muted"
)}
>
{task.completed ? <Check className="h-4 w-4" /> : <Star className="h-4 w-4" />}
</div>
<div className="flex-1 min-w-0">
<p
className={cn(
"text-sm font-medium",
task.completed && "text-muted-foreground line-through"
)}
>
{task.title}
</p>
<p className="text-xs text-muted-foreground">+{task.pages_reward} pages</p>
</div>
<Button
variant={task.completed ? "ghost" : "outline"}
size="sm"
disabled={task.completed || completeMutation.isPending}
onClick={() => handleTaskClick(task)}
asChild={!task.completed}
>
{task.completed ? (
<span>Done</span>
) : (
<a
href={task.action_url}
target="_blank"
rel="noopener noreferrer"
className="gap-1"
>
{completeMutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<>
Go
<ExternalLink className="h-3 w-3" />
</>
)}
</a>
)}
</Button>
</CardContent>
</Card>
))}
</div>
)}
{/* Contact */}
<Separator className="my-6" />
<div className="text-center">
<p className="mb-3 text-sm text-muted-foreground">
{allCompleted ? "Thanks! Need even more pages?" : "Need more pages?"}
</p>
<Dialog onOpenChange={(open) => open && trackIncentiveContactOpened()}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Mail className="h-4 w-4" />
Contact Us
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Contact Us</DialogTitle>
<DialogDescription>Schedule a meeting or send us an email.</DialogDescription>
</DialogHeader>
<div className="flex flex-col items-center gap-4 py-4">
<Link
href="https://calendly.com/eric-surfsense/surfsense-meeting"
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground transition hover:bg-primary/90"
>
<IconCalendar className="h-4 w-4" />
Schedule a Meeting
</Link>
<div className="flex items-center gap-2 text-muted-foreground">
<span className="h-px w-8 bg-border" />
<span className="text-xs">or</span>
<span className="h-px w-8 bg-border" />
</div>
<Link
href="mailto:eric@surfsense.com"
className="flex items-center gap-2 text-sm text-muted-foreground transition hover:text-foreground"
>
<IconMailFilled className="h-4 w-4" />
eric@surfsense.com
</Link>
</div>
</DialogContent>
</Dialog>
</div>
</motion.div>
</div>
);
}

View file

@ -9,6 +9,7 @@ import {
import { useQueryClient } from "@tanstack/react-query";
import { useAtomValue, useSetAtom } from "jotai";
import { useParams, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { z } from "zod";
@ -34,6 +35,7 @@ import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview";
import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage";
import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory";
import { Spinner } from "@/components/ui/spinner";
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
import { useMessagesElectric } from "@/hooks/use-messages-electric";
// import { WriteTodosToolUI } from "@/components/tool-ui/write-todos";
@ -132,6 +134,7 @@ interface ThinkingStepData {
}
export default function NewChatPage() {
const t = useTranslations("dashboard");
const params = useParams();
const queryClient = useQueryClient();
const [isInitializing, setIsInitializing] = useState(true);
@ -1379,8 +1382,9 @@ export default function NewChatPage() {
// Show loading state only when loading an existing thread
if (isInitializing) {
return (
<div className="flex h-[calc(100vh-64px)] items-center justify-center">
<div className="text-muted-foreground">Loading chat...</div>
<div className="flex h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
<Spinner size="lg" />
<div className="text-sm text-muted-foreground">{t("loading_chat")}</div>
</div>
);
}

View file

@ -1,7 +1,6 @@
"use client";
import { useAtomValue } from "jotai";
import { Loader2 } from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
@ -17,6 +16,7 @@ import {
import { Logo } from "@/components/Logo";
import { LLMConfigForm, type LLMConfigFormData } from "@/components/shared/llm-config-form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
export default function OnboardPage() {
@ -156,7 +156,7 @@ export default function OnboardPage() {
<div className="relative">
<div className="absolute inset-0 blur-3xl bg-gradient-to-r from-violet-500/20 to-cyan-500/20 rounded-full" />
<div className="relative flex items-center justify-center w-24 h-24 mx-auto rounded-2xl bg-gradient-to-br from-violet-500 to-purple-600 shadow-2xl shadow-violet-500/25">
<Loader2 className="h-12 w-12 text-white animate-spin" />
<Spinner size="xl" className="text-white" />
</div>
</div>
<div className="space-y-2">

View file

@ -5,6 +5,7 @@ import {
Bot,
Brain,
ChevronRight,
FileText,
type LucideIcon,
Menu,
MessageSquare,
@ -15,6 +16,7 @@ import { AnimatePresence, motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { GeneralSettingsManager } from "@/components/settings/general-settings-manager";
import { LLMRoleManager } from "@/components/settings/llm-role-manager";
import { ModelConfigManager } from "@/components/settings/model-config-manager";
import { PromptConfigManager } from "@/components/settings/prompt-config-manager";
@ -30,6 +32,12 @@ interface SettingsNavItem {
}
const settingsNavItems: SettingsNavItem[] = [
{
id: "general",
labelKey: "nav_general",
descriptionKey: "nav_general_desc",
icon: FileText,
},
{
id: "models",
labelKey: "nav_agent_configs",
@ -262,6 +270,9 @@ function SettingsContent({
ease: [0.4, 0, 0.2, 1],
}}
>
{activeSection === "general" && (
<GeneralSettingsManager searchSpaceId={searchSpaceId} />
)}
{activeSection === "models" && <ModelConfigManager searchSpaceId={searchSpaceId} />}
{activeSection === "roles" && <LLMRoleManager searchSpaceId={searchSpaceId} />}
{activeSection === "prompts" && <PromptConfigManager searchSpaceId={searchSpaceId} />}
@ -277,7 +288,7 @@ export default function SettingsPage() {
const router = useRouter();
const params = useParams();
const searchSpaceId = Number(params.search_space_id);
const [activeSection, setActiveSection] = useState("models");
const [activeSection, setActiveSection] = useState("general");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
// Track settings section view

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
"use client";
import { Loader2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
interface DashboardLayoutProps {
@ -10,8 +10,12 @@ interface DashboardLayoutProps {
}
export default function DashboardLayout({ children }: DashboardLayoutProps) {
const t = useTranslations("dashboard");
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
// Use the global loading screen - spinner animation won't reset
useGlobalLoadingEffect(isCheckingAuth, t("checking_auth"), "default");
useEffect(() => {
// Check if user is authenticated
const token = getBearerToken();
@ -23,21 +27,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
setIsCheckingAuth(false);
}, []);
// Show loading screen while checking authentication
// Return null while loading - the global provider handles the loading UI
if (isCheckingAuth) {
return (
<div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<Card className="w-[350px] bg-background/60 backdrop-blur-sm">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium">Loading Dashboard</CardTitle>
<CardDescription>Checking authentication...</CardDescription>
</CardHeader>
<CardContent className="flex justify-center py-6">
<Loader2 className="h-12 w-12 text-primary animate-spin" />
</CardContent>
</Card>
</div>
);
return null;
}
return (

View file

@ -0,0 +1,14 @@
"use client";
import { useTranslations } from "next-intl";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
export default function DashboardLoading() {
const t = useTranslations("common");
// Use global loading - spinner animation won't reset when page transitions
useGlobalLoadingEffect(true, t("loading"), "default");
// Return null - the GlobalLoadingProvider handles the loading UI
return null;
}

View file

@ -1,7 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
import { AlertCircle, Loader2, Plus, Search } from "lucide-react";
import { AlertCircle, Plus, Search } from "lucide-react";
import { motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
@ -18,37 +18,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
function LoadingScreen() {
const t = useTranslations("dashboard");
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<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>
</CardHeader>
<CardContent className="flex justify-center py-6">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1.5, repeat: Number.POSITIVE_INFINITY, ease: "linear" }}
>
<Loader2 className="h-12 w-12 text-primary" />
</motion.div>
</CardContent>
<CardFooter className="border-t pt-4 text-sm text-muted-foreground">
{t("may_take_moment")}
</CardFooter>
</Card>
</motion.div>
</div>
);
}
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
function ErrorScreen({ message }: { message: string }) {
const t = useTranslations("dashboard");
@ -121,6 +91,7 @@ export default function DashboardPage() {
const router = useRouter();
const [showCreateDialog, setShowCreateDialog] = useState(false);
const t = useTranslations("dashboard");
const { data: searchSpaces = [], isLoading, error } = useAtomValue(searchSpacesAtom);
useEffect(() => {
@ -131,11 +102,16 @@ export default function DashboardPage() {
}
}, [isLoading, searchSpaces, router]);
if (isLoading) return <LoadingScreen />;
// Show loading while fetching or while we have spaces and are about to redirect
const shouldShowLoading = isLoading || searchSpaces.length > 0;
// Use global loading screen - spinner animation won't reset
useGlobalLoadingEffect(shouldShowLoading, t("fetching_spaces"), "default");
if (error) return <ErrorScreen message={error?.message || "Failed to load search spaces"} />;
if (searchSpaces.length > 0) {
return <LoadingScreen />;
if (shouldShowLoading) {
return null;
}
return (

View file

@ -1,7 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
import { Loader2, Menu, User } from "lucide-react";
import { Menu, User } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
@ -11,6 +11,7 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/spinner";
interface ProfileContentProps {
onMenuClick: () => void;
@ -129,7 +130,7 @@ export function ProfileContent({ onMenuClick }: ProfileContentProps) {
>
{isUserLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<Spinner size="md" className="text-muted-foreground" />
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
@ -166,7 +167,7 @@ export function ProfileContent({ onMenuClick }: ProfileContentProps) {
<div className="flex justify-end">
<Button type="submit" disabled={isPending || !hasChanges}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isPending && <Spinner size="sm" className="mr-2" />}
{t("profile_save")}
</Button>
</div>

View file

@ -6,8 +6,6 @@ import {
AlertCircle,
ArrowRight,
CheckCircle2,
Clock,
Loader2,
LogIn,
Shield,
Sparkles,
@ -30,6 +28,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Spinner } from "@/components/ui/spinner";
import type { AcceptInviteResponse } from "@/contracts/types/invites.types";
import { invitesApiService } from "@/lib/apis/invites-api.service";
import { getBearerToken } from "@/lib/auth-utils";
@ -164,7 +163,7 @@ export default function InviteAcceptPage() {
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<Loader2 className="h-12 w-12 text-primary" />
<Spinner size="xl" className="text-primary" />
</motion.div>
<p className="mt-4 text-muted-foreground">Loading invite details...</p>
</CardContent>
@ -353,7 +352,7 @@ export default function InviteAcceptPage() {
<Button className="flex-1 gap-2" onClick={handleAccept} disabled={accepting}>
{accepting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<Spinner size="sm" />
Accepting...
</>
) : (

View file

@ -3,6 +3,7 @@ import "./globals.css";
import { RootProvider } from "fumadocs-ui/provider/next";
import { Roboto } from "next/font/google";
import { ElectricProvider } from "@/components/providers/ElectricProvider";
import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider";
import { I18nProvider } from "@/components/providers/I18nProvider";
import { PostHogProvider } from "@/components/providers/PostHogProvider";
import { ThemeProvider } from "@/components/theme/theme-provider";
@ -104,7 +105,9 @@ export default function RootLayout({
>
<RootProvider>
<ReactQueryClientProvider>
<ElectricProvider>{children}</ElectricProvider>
<ElectricProvider>
<GlobalLoadingProvider>{children}</GlobalLoadingProvider>
</ElectricProvider>
</ReactQueryClientProvider>
<Toaster />
</RootProvider>

View file

@ -175,5 +175,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
changeFrequency: "daily",
priority: 0.8,
},
// How-to documentation
{
url: "https://www.surfsense.com/docs/how-to/electric-sql",
lastModified,
changeFrequency: "daily",
priority: 0.8,
},
];
}