Files
lemonspace_app/components/canvas/canvas-app-menu.tsx
Matthias Meister 79d9092d43 Implement internationalization support across components
- Integrated `next-intl` for toast messages and locale handling in various components, including `Providers`, `CanvasUserMenu`, and `CreditOverview`.
- Replaced hardcoded strings with translation keys to enhance localization capabilities.
- Updated `RootLayout` to dynamically set the language attribute based on the user's locale.
- Ensured consistent user feedback through localized toast messages in actions such as sign-out, canvas operations, and billing notifications.
2026-04-01 18:16:52 +02:00

230 lines
6.9 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useMutation } from "convex/react";
import { useTheme } from "next-themes";
import { useTranslations } from "next-intl";
import {
Monitor,
Moon,
Pencil,
Sun,
Trash2,
Menu,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import { toast } from "@/lib/toast";
import { useAuthQuery } from "@/hooks/use-auth-query";
type CanvasAppMenuProps = {
canvasId: Id<"canvases">;
};
export function CanvasAppMenu({ canvasId }: CanvasAppMenuProps) {
const t = useTranslations('toasts');
const router = useRouter();
const canvas = useAuthQuery(api.canvases.get, { canvasId });
const removeCanvas = useMutation(api.canvases.remove);
const renameCanvas = useMutation(api.canvases.update);
const { theme = "system", setTheme } = useTheme();
const [renameOpen, setRenameOpen] = useState(false);
const [renameValue, setRenameValue] = useState("");
const [renameSaving, setRenameSaving] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleteBusy, setDeleteBusy] = useState(false);
useEffect(() => {
if (renameOpen && canvas?.name !== undefined) {
setRenameValue(canvas.name);
}
}, [renameOpen, canvas?.name]);
const handleRename = async () => {
const trimmed = renameValue.trim();
if (!trimmed) {
toast.error(t('dashboard.renameEmptyTitle'), t('dashboard.renameEmptyDesc'));
return;
}
if (trimmed === canvas?.name) {
setRenameOpen(false);
return;
}
setRenameSaving(true);
try {
await renameCanvas({ canvasId, name: trimmed });
toast.success(t('dashboard.renameSuccess'));
setRenameOpen(false);
} catch {
toast.error(t('dashboard.renameFailed'));
} finally {
setRenameSaving(false);
}
};
const handleDelete = async () => {
setDeleteBusy(true);
try {
await removeCanvas({ canvasId });
toast.success("Projekt gelöscht");
setDeleteOpen(false);
router.replace("/dashboard");
router.refresh();
} catch {
toast.error("Löschen fehlgeschlagen");
} finally {
setDeleteBusy(false);
}
};
return (
<>
<div className="absolute top-4 right-4 z-20">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
size="icon"
variant="outline"
className="size-10 rounded-lg border-border/80 bg-card/95 shadow-md backdrop-blur-sm"
aria-label="Canvas-Menü"
title="Canvas-Menü"
>
<Menu className="size-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem
onSelect={() => {
setRenameOpen(true);
}}
>
<Pencil className="size-4" />
Projekt umbenennen
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => setDeleteOpen(true)}
>
<Trash2 className="size-4" />
Projekt löschen
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Sun className="size-4" />
Erscheinungsbild
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onSelect={() => setTheme("light")}>
<Sun className="size-4" />
Hell
{theme === "light" ? " ✓" : ""}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setTheme("dark")}>
<Moon className="size-4" />
Dunkel
{theme === "dark" ? " ✓" : ""}
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setTheme("system")}>
<Monitor className="size-4" />
System
{theme === "system" ? " ✓" : ""}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Dialog open={renameOpen} onOpenChange={setRenameOpen}>
<DialogContent className="sm:max-w-md" showCloseButton>
<DialogHeader>
<DialogTitle>Projekt umbenennen</DialogTitle>
<DialogDescription>
Gib einen neuen Namen für dein Projekt ein.
</DialogDescription>
</DialogHeader>
<Input
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") void handleRename();
}}
placeholder="Name"
autoFocus
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setRenameOpen(false)}
>
Abbrechen
</Button>
<Button
type="button"
onClick={() => void handleRename()}
disabled={renameSaving}
>
Speichern
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogContent className="sm:max-w-md" showCloseButton>
<DialogHeader>
<DialogTitle>Projekt löschen?</DialogTitle>
<DialogDescription>
&ldquo;{canvas?.name ?? "dieses Projekt"}&rdquo; und alle Knoten werden dauerhaft
gelöscht. Das lässt sich nicht rückgängig machen.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setDeleteOpen(false)}
>
Abbrechen
</Button>
<Button
type="button"
variant="destructive"
onClick={() => void handleDelete()}
disabled={deleteBusy}
>
Löschen
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}