返回 presentation-ai
PresentationMenu.tsx
根目录 / src / components / presentation / controls / PresentationMenu.tsx
1 "use client";
2
3 import {
4 createBlankPresentation,
5 duplicatePresentation,
6 } from "@/app/_actions/notebook/presentation/presentationActions";
7 import {
8 DropdownMenu,
9 DropdownMenuContent,
10 DropdownMenuItem,
11 DropdownMenuSeparator,
12 DropdownMenuTrigger,
13 } from "@/components/ui/dropdown-menu";
14 import { Button } from "@/components/ui/button";
15 import { usePresentationHistoryState } from "@/states/presentation-history-state";
16 import { usePresentationState } from "@/states/presentation-state";
17 import { useMutation } from "@tanstack/react-query";
18 import {
19 Copy,
20 FileEdit,
21 FolderOpen,
22 Menu,
23 Palette,
24 Plus,
25 Redo,
26 Settings,
27 Undo,
28 } from "lucide-react";
29 import { useRouter } from "next/navigation";
30 import { useCallback } from "react";
31 import { toast } from "sonner";
32
33 export function PresentationMenu({
34 readOnly = false,
35 }: {
36 readOnly?: boolean;
37 }) {
38 const currentPresentationId = usePresentationState(
39 (state) => state.currentPresentationId,
40 );
41 const setCurrentPresentation = usePresentationState(
42 (state) => state.setCurrentPresentation,
43 );
44 const setActiveRightPanel = usePresentationState(
45 (state) => state.setActiveRightPanel,
46 );
47 const undo = usePresentationHistoryState((state) => state.undo);
48 const redo = usePresentationHistoryState((state) => state.redo);
49 const canUndo = usePresentationHistoryState((state) => state.canUndo);
50 const canRedo = usePresentationHistoryState((state) => state.canRedo);
51 const router = useRouter();
52
53 const { mutateAsync: duplicatePresentationMutation, isPending: isDuplicating } =
54 useMutation({
55 mutationFn: async () => {
56 if (!currentPresentationId) {
57 toast.error("Current presentation is not available");
58 throw new Error("CURRENT_PRESENTATION_ID_MISSING");
59 }
60
61 return duplicatePresentation(currentPresentationId);
62 },
63 onSuccess: (data) => {
64 if (data.success && data.presentation) {
65 setCurrentPresentation(data.presentation.id, data.presentation.title);
66 router.push(`/presentation/${data.presentation.id}`);
67 return;
68 }
69
70 toast.error(data.message);
71 },
72 onError: () => {
73 toast.error("Failed to duplicate presentation");
74 },
75 });
76
77 const {
78 mutateAsync: createBlankPresentationMutation,
79 isPending: isCreatingBlank,
80 } = useMutation({
81 mutationFn: async () => {
82 const theme = usePresentationState.getState().theme;
83 const language = usePresentationState.getState().language;
84
85 return createBlankPresentation(
86 "Untitled Presentation",
87 theme ??
88 (localStorage.getItem("theme") === "dark" ? "ebony" : "mystique"),
89 language,
90 );
91 },
92 onSuccess: (data) => {
93 if (data.success && data.presentation) {
94 setCurrentPresentation(data.presentation.id, data.presentation.title);
95 router.push(`/presentation/${data.presentation.id}`);
96 return;
97 }
98
99 toast.error(data.message);
100 },
101 onError: () => {
102 toast.error("Failed to create presentation");
103 },
104 });
105
106 const focusTitleInput = useCallback(() => {
107 window.setTimeout(() => {
108 const presentationTitleInput = document.getElementById(
109 "presentation-title-input",
110 );
111 presentationTitleInput?.focus();
112 }, 250);
113 }, []);
114
115 return (
116 <DropdownMenu>
117 <DropdownMenuTrigger asChild>
118 <Button
119 variant="ghost"
120 size="icon"
121 aria-label="Open presentation menu"
122 className="rounded-full"
123 >
124 <Menu className="h-5 w-5" />
125 </Button>
126 </DropdownMenuTrigger>
127 <DropdownMenuContent className="w-64 p-2">
128 <div className="px-2 py-1.5 text-xs font-medium tracking-[0.08em] text-muted-foreground uppercase">
129 File
130 </div>
131 <DropdownMenuItem
132 disabled={isCreatingBlank}
133 onClick={() => void createBlankPresentationMutation()}
134 className="rounded-md"
135 >
136 <Plus className="mr-2 h-4 w-4" />
137 New Presentation
138 </DropdownMenuItem>
139 {!readOnly ? (
140 <DropdownMenuItem onClick={focusTitleInput} className="rounded-md">
141 <FileEdit className="mr-2 h-4 w-4" />
142 Rename
143 </DropdownMenuItem>
144 ) : null}
145 <DropdownMenuItem
146 disabled={isDuplicating}
147 onClick={() => void duplicatePresentationMutation()}
148 className="rounded-md"
149 >
150 <Copy className="mr-2 h-4 w-4" />
151 {readOnly ? "Clone to My Account" : "Duplicate"}
152 </DropdownMenuItem>
153
154 {!readOnly ? (
155 <>
156 <DropdownMenuSeparator />
157 <div className="px-2 py-1.5 text-xs font-medium tracking-[0.08em] text-muted-foreground uppercase">
158 Edit
159 </div>
160 <DropdownMenuItem
161 disabled={!canUndo}
162 onClick={undo}
163 className="rounded-md"
164 >
165 <Undo className="mr-2 h-4 w-4" />
166 <span className="flex-1">Undo</span>
167 <span className="text-xs text-muted-foreground">Ctrl+Z</span>
168 </DropdownMenuItem>
169 <DropdownMenuItem
170 disabled={!canRedo}
171 onClick={redo}
172 className="rounded-md"
173 >
174 <Redo className="mr-2 h-4 w-4" />
175 <span className="flex-1">Redo</span>
176 <span className="text-xs text-muted-foreground">Ctrl+Y</span>
177 </DropdownMenuItem>
178 <DropdownMenuSeparator />
179 <div className="px-2 py-1.5 text-xs font-medium tracking-[0.08em] text-muted-foreground uppercase">
180 Workspace
181 </div>
182 <DropdownMenuItem
183 onClick={() => setActiveRightPanel("globalSettings")}
184 className="rounded-md"
185 >
186 <Settings className="mr-2 h-4 w-4" />
187 Page Setup
188 </DropdownMenuItem>
189 <DropdownMenuItem
190 onClick={() => setActiveRightPanel("theme")}
191 className="rounded-md"
192 >
193 <Palette className="mr-2 h-4 w-4" />
194 Theme Panel
195 </DropdownMenuItem>
196 </>
197 ) : null}
198
199 <DropdownMenuSeparator />
200 <div className="px-2 py-1.5 text-xs font-medium tracking-[0.08em] text-muted-foreground uppercase">
201 View
202 </div>
203 {!readOnly ? (
204 <DropdownMenuItem
205 disabled={!currentPresentationId}
206 onClick={() => {
207 if (currentPresentationId) {
208 router.push(`/presentation/generate/${currentPresentationId}`);
209 }
210 }}
211 className="rounded-md"
212 >
213 <FolderOpen className="mr-2 h-4 w-4" />
214 Back to prompt
215 </DropdownMenuItem>
216 ) : null}
217 <DropdownMenuItem
218 onClick={() => router.push("/presentation")}
219 className="rounded-md"
220 >
221 <FolderOpen className="mr-2 h-4 w-4" />
222 All Presentations
223 </DropdownMenuItem>
224 </DropdownMenuContent>
225 </DropdownMenu>
226 );
227 }
228
228 lines Plain Text