mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-21 18:55:16 +02:00
merge: upstream/dev with migration renumbering
This commit is contained in:
commit
a7145b2c63
176 changed files with 8791 additions and 3608 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 (
|
||||
|
|
|
|||
14
surfsense_web/app/dashboard/loading.tsx
Normal file
14
surfsense_web/app/dashboard/loading.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue