2025-07-27 10:05:37 -07:00
|
|
|
"use client";
|
2025-04-07 23:47:06 -07:00
|
|
|
|
2025-12-21 16:32:55 -08:00
|
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import { useAtomValue, useSetAtom } from "jotai";
|
2025-08-08 11:17:43 -07:00
|
|
|
import { Trash2 } from "lucide-react";
|
2025-12-16 12:28:30 +05:30
|
|
|
import { useRouter } from "next/navigation";
|
feat(i18n): Add next-intl framework with full bilingual support (EN/ZH)
- Implement next-intl framework for scalable i18n
- Add complete Chinese (Simplified) localization
- Support 400+ translated strings across all pages
- Add language switcher with persistent preference
- Zero breaking changes to existing functionality
Framework additions:
- i18n routing and middleware
- LocaleContext for client-side state
- LanguageSwitcher component
- Translation files (en.json, zh.json)
Translated components:
- Homepage: Hero, features, CTA, navbar
- Auth: Login, register
- Dashboard: Main page, layout
- Connectors: Management, add page (all categories)
- Documents: Upload, manage, filters
- Settings: LLM configs, role assignments
- Onboarding: Add provider, assign roles
- Logs: Task logs viewer
Adding a new language now requires only:
1. Create messages/<locale>.json
2. Add locale to i18n/routing.ts
2025-10-26 14:05:46 +08:00
|
|
|
import { useTranslations } from "next-intl";
|
2025-12-21 16:32:55 -08:00
|
|
|
import { useCallback, useMemo, useState } from "react";
|
2025-12-19 22:59:42 +05:30
|
|
|
import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms";
|
2025-12-17 00:09:43 -08:00
|
|
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
2025-07-27 10:05:37 -07:00
|
|
|
import { AppSidebar } from "@/components/sidebar/app-sidebar";
|
2025-07-27 10:41:15 -07:00
|
|
|
import { Button } from "@/components/ui/button";
|
2025-04-07 23:47:06 -07:00
|
|
|
import {
|
2025-07-27 10:05:37 -07:00
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
2025-04-07 23:47:06 -07:00
|
|
|
} from "@/components/ui/dialog";
|
2025-12-16 12:28:30 +05:30
|
|
|
import { notesApiService } from "@/lib/apis/notes-api.service";
|
2025-12-12 08:59:13 +00:00
|
|
|
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
2025-12-21 16:32:55 -08:00
|
|
|
import { deleteThread, fetchThreads } from "@/lib/chat/thread-persistence";
|
2025-12-12 08:59:13 +00:00
|
|
|
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
2025-04-07 23:47:06 -07:00
|
|
|
|
|
|
|
|
interface AppSidebarProviderProps {
|
2025-07-27 10:05:37 -07:00
|
|
|
searchSpaceId: string;
|
|
|
|
|
navSecondary: {
|
|
|
|
|
title: string;
|
|
|
|
|
url: string;
|
|
|
|
|
icon: string;
|
|
|
|
|
}[];
|
|
|
|
|
navMain: {
|
|
|
|
|
title: string;
|
|
|
|
|
url: string;
|
|
|
|
|
icon: string;
|
|
|
|
|
isActive?: boolean;
|
|
|
|
|
items?: {
|
|
|
|
|
title: string;
|
|
|
|
|
url: string;
|
|
|
|
|
}[];
|
|
|
|
|
}[];
|
2025-04-07 23:47:06 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-17 04:10:24 -07:00
|
|
|
export function AppSidebarProvider({
|
2025-07-27 10:05:37 -07:00
|
|
|
searchSpaceId,
|
|
|
|
|
navSecondary,
|
|
|
|
|
navMain,
|
2025-04-07 23:47:06 -07:00
|
|
|
}: AppSidebarProviderProps) {
|
2025-10-27 20:30:10 -07:00
|
|
|
const t = useTranslations("dashboard");
|
|
|
|
|
const tCommon = useTranslations("common");
|
2025-12-16 12:28:30 +05:30
|
|
|
const router = useRouter();
|
2025-12-21 16:32:55 -08:00
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const [isDeletingThread, setIsDeletingThread] = useState(false);
|
2025-12-19 15:38:39 -08:00
|
|
|
|
2025-12-19 22:59:42 +05:30
|
|
|
// Editor state for handling unsaved changes
|
|
|
|
|
const hasUnsavedEditorChanges = useAtomValue(hasUnsavedEditorChangesAtom);
|
|
|
|
|
const setPendingNavigation = useSetAtom(pendingEditorNavigationAtom);
|
2025-11-03 22:34:37 -08:00
|
|
|
|
2025-12-21 16:32:55 -08:00
|
|
|
// Fetch new chat threads
|
|
|
|
|
const {
|
|
|
|
|
data: threadsData,
|
|
|
|
|
error: threadError,
|
|
|
|
|
isLoading: isLoadingThreads,
|
|
|
|
|
refetch: refetchThreads,
|
|
|
|
|
} = useQuery({
|
|
|
|
|
queryKey: ["threads", searchSpaceId],
|
|
|
|
|
queryFn: () => fetchThreads(Number(searchSpaceId), 4),
|
|
|
|
|
enabled: !!searchSpaceId,
|
|
|
|
|
});
|
2025-11-03 22:34:37 -08:00
|
|
|
|
|
|
|
|
const {
|
2025-12-12 08:59:13 +00:00
|
|
|
data: searchSpace,
|
|
|
|
|
isLoading: isLoadingSearchSpace,
|
2025-11-03 22:34:37 -08:00
|
|
|
error: searchSpaceError,
|
2025-12-12 08:59:13 +00:00
|
|
|
refetch: fetchSearchSpace,
|
|
|
|
|
} = useQuery({
|
|
|
|
|
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId),
|
|
|
|
|
queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }),
|
|
|
|
|
enabled: !!searchSpaceId,
|
|
|
|
|
});
|
2025-11-03 22:34:37 -08:00
|
|
|
|
2025-12-15 12:25:09 +00:00
|
|
|
const { data: user } = useAtomValue(currentUserAtom);
|
2025-11-03 22:34:37 -08:00
|
|
|
|
2025-12-16 12:28:30 +05:30
|
|
|
// Fetch notes
|
|
|
|
|
const {
|
|
|
|
|
data: notesData,
|
|
|
|
|
error: notesError,
|
|
|
|
|
isLoading: isLoadingNotes,
|
|
|
|
|
refetch: refetchNotes,
|
|
|
|
|
} = useQuery({
|
|
|
|
|
queryKey: ["notes", searchSpaceId],
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
notesApiService.getNotes({
|
|
|
|
|
search_space_id: Number(searchSpaceId),
|
2025-12-19 19:36:10 +05:30
|
|
|
page_size: 4, // Get 4 notes for compact sidebar
|
2025-12-16 12:28:30 +05:30
|
|
|
}),
|
|
|
|
|
enabled: !!searchSpaceId,
|
|
|
|
|
});
|
|
|
|
|
|
2025-07-27 10:05:37 -07:00
|
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
2025-12-21 16:32:55 -08:00
|
|
|
const [threadToDelete, setThreadToDelete] = useState<{ id: number; name: string } | null>(null);
|
2025-12-19 21:40:40 +05:30
|
|
|
const [showDeleteNoteDialog, setShowDeleteNoteDialog] = useState(false);
|
2025-12-19 15:38:39 -08:00
|
|
|
const [noteToDelete, setNoteToDelete] = useState<{
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
search_space_id: number;
|
|
|
|
|
} | null>(null);
|
2025-12-19 21:40:40 +05:30
|
|
|
const [isDeletingNote, setIsDeletingNote] = useState(false);
|
2025-04-07 23:47:06 -07:00
|
|
|
|
2025-08-02 21:20:36 -07:00
|
|
|
// Retry function
|
|
|
|
|
const retryFetch = useCallback(() => {
|
|
|
|
|
fetchSearchSpace();
|
2025-11-18 22:12:47 +02:00
|
|
|
}, [fetchSearchSpace]);
|
2025-08-02 21:20:36 -07:00
|
|
|
|
2025-12-21 16:32:55 -08:00
|
|
|
// Transform threads to the format expected by AppSidebar
|
2025-11-03 22:34:37 -08:00
|
|
|
const recentChats = useMemo(() => {
|
2025-12-21 16:32:55 -08:00
|
|
|
if (!threadsData?.threads) return [];
|
2025-12-19 21:40:40 +05:30
|
|
|
|
2025-12-21 16:32:55 -08:00
|
|
|
// Threads are already sorted by updated_at desc from the API
|
|
|
|
|
return threadsData.threads.map((thread) => ({
|
|
|
|
|
name: thread.title || `Chat ${thread.id}`,
|
|
|
|
|
url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`,
|
2025-12-19 21:40:40 +05:30
|
|
|
icon: "MessageCircleMore",
|
2025-12-21 16:32:55 -08:00
|
|
|
id: thread.id,
|
|
|
|
|
search_space_id: Number(searchSpaceId),
|
2025-12-19 21:40:40 +05:30
|
|
|
actions: [
|
|
|
|
|
{
|
|
|
|
|
name: "Delete",
|
|
|
|
|
icon: "Trash2",
|
|
|
|
|
onClick: () => {
|
2025-12-21 16:32:55 -08:00
|
|
|
setThreadToDelete({
|
|
|
|
|
id: thread.id,
|
|
|
|
|
name: thread.title || `Chat ${thread.id}`,
|
|
|
|
|
});
|
2025-12-19 21:40:40 +05:30
|
|
|
setShowDeleteDialog(true);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}));
|
2025-12-21 16:32:55 -08:00
|
|
|
}, [threadsData, searchSpaceId]);
|
2025-08-02 21:20:36 -07:00
|
|
|
|
2025-12-21 16:32:55 -08:00
|
|
|
// Handle delete thread
|
|
|
|
|
const handleDeleteThread = useCallback(async () => {
|
|
|
|
|
if (!threadToDelete) return;
|
2025-07-17 04:10:24 -07:00
|
|
|
|
2025-12-21 16:32:55 -08:00
|
|
|
setIsDeletingThread(true);
|
2025-07-27 10:05:37 -07:00
|
|
|
try {
|
2025-12-21 16:32:55 -08:00
|
|
|
await deleteThread(threadToDelete.id);
|
|
|
|
|
// Invalidate threads query to refresh the list
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
|
2025-07-27 10:05:37 -07:00
|
|
|
} catch (error) {
|
2025-12-21 16:32:55 -08:00
|
|
|
console.error("Error deleting thread:", error);
|
2025-07-27 10:05:37 -07:00
|
|
|
} finally {
|
2025-12-21 16:32:55 -08:00
|
|
|
setIsDeletingThread(false);
|
2025-07-27 10:05:37 -07:00
|
|
|
setShowDeleteDialog(false);
|
2025-12-21 16:32:55 -08:00
|
|
|
setThreadToDelete(null);
|
2025-07-27 10:05:37 -07:00
|
|
|
}
|
2025-12-21 16:32:55 -08:00
|
|
|
}, [threadToDelete, queryClient, searchSpaceId]);
|
2025-07-17 04:10:24 -07:00
|
|
|
|
2025-12-19 21:40:40 +05:30
|
|
|
// Handle delete note with confirmation
|
|
|
|
|
const handleDeleteNote = useCallback(async () => {
|
|
|
|
|
if (!noteToDelete) return;
|
|
|
|
|
|
|
|
|
|
setIsDeletingNote(true);
|
|
|
|
|
try {
|
|
|
|
|
await notesApiService.deleteNote({
|
|
|
|
|
search_space_id: noteToDelete.search_space_id,
|
|
|
|
|
note_id: noteToDelete.id,
|
|
|
|
|
});
|
|
|
|
|
refetchNotes();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error deleting note:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsDeletingNote(false);
|
|
|
|
|
setShowDeleteNoteDialog(false);
|
|
|
|
|
setNoteToDelete(null);
|
|
|
|
|
}
|
|
|
|
|
}, [noteToDelete, refetchNotes]);
|
|
|
|
|
|
2025-08-02 21:20:36 -07:00
|
|
|
// Memoized fallback chats
|
|
|
|
|
const fallbackChats = useMemo(() => {
|
2025-12-21 16:32:55 -08:00
|
|
|
if (threadError) {
|
2025-08-02 21:20:36 -07:00
|
|
|
return [
|
|
|
|
|
{
|
2025-10-27 20:30:10 -07:00
|
|
|
name: t("error_loading_chats"),
|
2025-08-02 21:20:36 -07:00
|
|
|
url: "#",
|
|
|
|
|
icon: "AlertCircle",
|
|
|
|
|
id: 0,
|
|
|
|
|
search_space_id: Number(searchSpaceId),
|
|
|
|
|
actions: [
|
|
|
|
|
{
|
2025-10-27 20:30:10 -07:00
|
|
|
name: tCommon("retry"),
|
2025-08-02 21:20:36 -07:00
|
|
|
icon: "RefreshCw",
|
2025-12-21 16:32:55 -08:00
|
|
|
onClick: () => refetchThreads(),
|
2025-08-02 21:20:36 -07:00
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
2025-04-07 23:47:06 -07:00
|
|
|
|
2025-08-02 21:20:36 -07:00
|
|
|
return [];
|
2025-12-21 16:32:55 -08:00
|
|
|
}, [threadError, searchSpaceId, refetchThreads, t, tCommon]);
|
2025-04-07 23:47:06 -07:00
|
|
|
|
2025-07-27 10:05:37 -07:00
|
|
|
// Use fallback chats if there's an error or no chats
|
|
|
|
|
const displayChats = recentChats.length > 0 ? recentChats : fallbackChats;
|
2025-04-07 23:47:06 -07:00
|
|
|
|
2025-12-16 12:28:30 +05:30
|
|
|
// Transform notes to the format expected by NavNotes
|
|
|
|
|
const recentNotes = useMemo(() => {
|
2025-12-16 20:14:54 +05:30
|
|
|
if (!notesData?.items) return [];
|
|
|
|
|
|
|
|
|
|
// Sort notes by updated_at (most recent first), fallback to created_at if updated_at is null
|
|
|
|
|
const sortedNotes = [...notesData.items].sort((a, b) => {
|
2025-12-16 21:27:31 +05:30
|
|
|
const dateA = a.updated_at
|
|
|
|
|
? new Date(a.updated_at).getTime()
|
|
|
|
|
: new Date(a.created_at).getTime();
|
|
|
|
|
const dateB = b.updated_at
|
|
|
|
|
? new Date(b.updated_at).getTime()
|
|
|
|
|
: new Date(b.created_at).getTime();
|
2025-12-16 20:14:54 +05:30
|
|
|
return dateB - dateA; // Descending order (most recent first)
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-19 19:36:10 +05:30
|
|
|
// Limit to 4 notes for compact sidebar
|
|
|
|
|
return sortedNotes.slice(0, 4).map((note) => ({
|
2025-12-16 20:14:54 +05:30
|
|
|
name: note.title,
|
|
|
|
|
url: `/dashboard/${note.search_space_id}/editor/${note.id}`,
|
|
|
|
|
icon: "FileText",
|
|
|
|
|
id: note.id,
|
|
|
|
|
search_space_id: note.search_space_id,
|
|
|
|
|
actions: [
|
|
|
|
|
{
|
|
|
|
|
name: "Delete",
|
|
|
|
|
icon: "Trash2",
|
2025-12-19 21:40:40 +05:30
|
|
|
onClick: () => {
|
2025-12-19 15:38:39 -08:00
|
|
|
setNoteToDelete({
|
|
|
|
|
id: note.id,
|
|
|
|
|
name: note.title,
|
|
|
|
|
search_space_id: note.search_space_id,
|
|
|
|
|
});
|
2025-12-19 21:40:40 +05:30
|
|
|
setShowDeleteNoteDialog(true);
|
2025-12-16 20:14:54 +05:30
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}));
|
2025-12-19 21:40:40 +05:30
|
|
|
}, [notesData]);
|
2025-12-16 12:28:30 +05:30
|
|
|
|
2025-12-19 22:59:42 +05:30
|
|
|
// Handle add note - check for unsaved changes first
|
2025-12-16 12:28:30 +05:30
|
|
|
const handleAddNote = useCallback(() => {
|
2025-12-19 22:59:42 +05:30
|
|
|
const newNoteUrl = `/dashboard/${searchSpaceId}/editor/new`;
|
2025-12-19 15:38:39 -08:00
|
|
|
|
2025-12-19 22:59:42 +05:30
|
|
|
if (hasUnsavedEditorChanges) {
|
|
|
|
|
// Set pending navigation - the editor will show the unsaved changes dialog
|
|
|
|
|
setPendingNavigation(newNoteUrl);
|
|
|
|
|
} else {
|
|
|
|
|
// No unsaved changes, navigate directly
|
|
|
|
|
router.push(newNoteUrl);
|
|
|
|
|
}
|
|
|
|
|
}, [router, searchSpaceId, hasUnsavedEditorChanges, setPendingNavigation]);
|
2025-12-16 12:28:30 +05:30
|
|
|
|
2025-08-02 21:20:36 -07:00
|
|
|
// Memoized updated navSecondary
|
|
|
|
|
const updatedNavSecondary = useMemo(() => {
|
|
|
|
|
const updated = [...navSecondary];
|
2025-12-21 16:32:55 -08:00
|
|
|
if (updated.length > 0) {
|
2025-08-02 21:20:36 -07:00
|
|
|
updated[0] = {
|
|
|
|
|
...updated[0],
|
|
|
|
|
title:
|
|
|
|
|
searchSpace?.name ||
|
|
|
|
|
(isLoadingSearchSpace
|
2025-10-27 20:30:10 -07:00
|
|
|
? tCommon("loading")
|
2025-08-02 21:20:36 -07:00
|
|
|
: searchSpaceError
|
2025-10-27 20:30:10 -07:00
|
|
|
? t("error_loading_space")
|
|
|
|
|
: t("unknown_search_space")),
|
2025-08-02 21:20:36 -07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return updated;
|
2025-12-21 16:32:55 -08:00
|
|
|
}, [navSecondary, searchSpace?.name, isLoadingSearchSpace, searchSpaceError, t, tCommon]);
|
2025-08-02 21:20:36 -07:00
|
|
|
|
2025-11-03 22:34:37 -08:00
|
|
|
// Prepare page usage data
|
|
|
|
|
const pageUsage = user
|
|
|
|
|
? {
|
|
|
|
|
pagesUsed: user.pages_used,
|
|
|
|
|
pagesLimit: user.pages_limit,
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
2025-07-27 10:05:37 -07:00
|
|
|
return (
|
|
|
|
|
<>
|
2025-11-03 22:34:37 -08:00
|
|
|
<AppSidebar
|
2025-12-13 22:43:38 -08:00
|
|
|
searchSpaceId={searchSpaceId}
|
2025-11-03 22:34:37 -08:00
|
|
|
navSecondary={updatedNavSecondary}
|
|
|
|
|
navMain={navMain}
|
|
|
|
|
RecentChats={displayChats}
|
2025-12-16 12:28:30 +05:30
|
|
|
RecentNotes={recentNotes}
|
|
|
|
|
onAddNote={handleAddNote}
|
2025-11-03 22:34:37 -08:00
|
|
|
pageUsage={pageUsage}
|
|
|
|
|
/>
|
2025-08-02 21:20:36 -07:00
|
|
|
|
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
|
|
|
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
|
|
|
<DialogContent className="sm:max-w-md">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
<Trash2 className="h-5 w-5 text-destructive" />
|
2025-10-27 20:30:10 -07:00
|
|
|
<span>{t("delete_chat")}</span>
|
2025-08-02 21:20:36 -07:00
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
2025-12-21 16:32:55 -08:00
|
|
|
{t("delete_chat_confirm")} <span className="font-medium">{threadToDelete?.name}</span>
|
|
|
|
|
? {t("action_cannot_undone")}
|
2025-08-02 21:20:36 -07:00
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<DialogFooter className="flex gap-2 sm:justify-end">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setShowDeleteDialog(false)}
|
2025-12-21 16:32:55 -08:00
|
|
|
disabled={isDeletingThread}
|
2025-08-02 21:20:36 -07:00
|
|
|
>
|
2025-10-27 20:30:10 -07:00
|
|
|
{tCommon("cancel")}
|
2025-08-02 21:20:36 -07:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
2025-12-21 16:32:55 -08:00
|
|
|
onClick={handleDeleteThread}
|
|
|
|
|
disabled={isDeletingThread}
|
2025-08-02 21:20:36 -07:00
|
|
|
className="gap-2"
|
|
|
|
|
>
|
2025-12-21 16:32:55 -08:00
|
|
|
{isDeletingThread ? (
|
2025-08-02 21:20:36 -07:00
|
|
|
<>
|
|
|
|
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
2025-10-27 20:30:10 -07:00
|
|
|
{t("deleting")}
|
2025-08-02 21:20:36 -07:00
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
2025-10-27 20:30:10 -07:00
|
|
|
{tCommon("delete")}
|
2025-08-02 21:20:36 -07:00
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2025-12-19 21:40:40 +05:30
|
|
|
|
|
|
|
|
{/* Delete Note Confirmation Dialog */}
|
|
|
|
|
<Dialog open={showDeleteNoteDialog} onOpenChange={setShowDeleteNoteDialog}>
|
|
|
|
|
<DialogContent className="sm:max-w-md">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
|
|
<Trash2 className="h-5 w-5 text-destructive" />
|
|
|
|
|
<span>{t("delete_note")}</span>
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
{t("delete_note_confirm")} <span className="font-medium">{noteToDelete?.name}</span>?{" "}
|
|
|
|
|
{t("action_cannot_undone")}
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<DialogFooter className="flex gap-2 sm:justify-end">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setShowDeleteNoteDialog(false)}
|
|
|
|
|
disabled={isDeletingNote}
|
|
|
|
|
>
|
|
|
|
|
{tCommon("cancel")}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={handleDeleteNote}
|
|
|
|
|
disabled={isDeletingNote}
|
|
|
|
|
className="gap-2"
|
|
|
|
|
>
|
|
|
|
|
{isDeletingNote ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
|
|
|
|
{t("deleting")}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
{tCommon("delete")}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2025-07-27 10:05:37 -07:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|