feat: Implement internationalization with react-i18next, add translation files, and integrate language switching across components.
This commit is contained in:
@@ -25,15 +25,16 @@ import {
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "sonner"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: "Notes",
|
||||
titleKey: "Notes",
|
||||
url: "/",
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
title: "Archive",
|
||||
titleKey: "Archive",
|
||||
url: "/archive",
|
||||
icon: Archive,
|
||||
},
|
||||
@@ -50,12 +51,13 @@ function TagItem({ tag, isActive }: TagItemProps) {
|
||||
const { mutate: deleteTag } = useDeleteTag();
|
||||
const { mutate: renameTag } = useRenameTag();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm(`Delete tag "${tag.name}"? Notes will keep their content.`)) {
|
||||
if (confirm(t("Delete tag \"{{name}}\"? Notes will keep their content.", { name: tag.name }))) {
|
||||
deleteTag(tag.id, {
|
||||
onSuccess: () => {
|
||||
toast.success("Tag deleted");
|
||||
toast.success(t("Tag deleted"));
|
||||
navigate("/");
|
||||
},
|
||||
onError: (err: any) => toast.error(err.message)
|
||||
@@ -67,7 +69,7 @@ function TagItem({ tag, isActive }: TagItemProps) {
|
||||
if (editName.trim() && editName.trim() !== tag.name) {
|
||||
renameTag({ id: tag.id, name: editName.trim() }, {
|
||||
onSuccess: () => {
|
||||
toast.success("Tag renamed");
|
||||
toast.success(t("Tag renamed"));
|
||||
setIsEditing(false);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
@@ -130,11 +132,11 @@ function TagItem({ tag, isActive }: TagItemProps) {
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem onClick={(e) => { e.preventDefault(); setIsEditing(true); }}>
|
||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||||
Rename
|
||||
{t("Rename")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={(e) => { e.preventDefault(); handleDelete(); }} className="text-destructive focus:text-destructive">
|
||||
<Trash2 className="mr-2 h-3.5 w-3.5" />
|
||||
Delete
|
||||
{t("Delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -149,6 +151,7 @@ export function AppSidebar() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [tagsOpen, setTagsOpen] = useState(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: tags } = useTags();
|
||||
const activeTag = searchParams.get("tag");
|
||||
@@ -158,24 +161,24 @@ export function AppSidebar() {
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>K-Notes</SidebarGroupLabel>
|
||||
<SidebarGroupLabel>{t("K-Notes")}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild isActive={location.pathname === item.url && !activeTag} tooltip={item.title}>
|
||||
<SidebarMenuItem key={item.titleKey}>
|
||||
<SidebarMenuButton asChild isActive={location.pathname === item.url && !activeTag} tooltip={t(item.titleKey)}>
|
||||
<Link to={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
<span>{t(item.titleKey)}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton onClick={() => setSettingsOpen(true)} tooltip="Settings">
|
||||
<SidebarMenuButton onClick={() => setSettingsOpen(true)} tooltip={t("Settings")}>
|
||||
<Settings />
|
||||
<span>Settings</span>
|
||||
<span>{t("Settings")}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
@@ -189,7 +192,7 @@ export function AppSidebar() {
|
||||
<CollapsibleTrigger className="flex items-center justify-between w-full cursor-pointer group/collapsible">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Tag className="h-3.5 w-3.5" />
|
||||
<span>Tags</span>
|
||||
<span>{t("Tags")}</span>
|
||||
</div>
|
||||
<ChevronRight className="h-3.5 w-3.5 transition-transform group-data-[state=open]/collapsible:rotate-90" />
|
||||
</CollapsibleTrigger>
|
||||
@@ -208,7 +211,7 @@ export function AppSidebar() {
|
||||
))
|
||||
) : (
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
No tags yet
|
||||
{t("No tags yet")}
|
||||
</div>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
|
||||
@@ -3,11 +3,13 @@ import { useDeleteNote, useUpdateNote } from "@/hooks/use-notes";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Archive, Trash2, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function BulkActionsBar() {
|
||||
const { selectedIds, clearSelection, isBulkMode } = useBulkSelection();
|
||||
const { mutate: deleteNote } = useDeleteNote();
|
||||
const { mutate: updateNote } = useUpdateNote();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isBulkMode) return null;
|
||||
|
||||
@@ -16,25 +18,25 @@ export function BulkActionsBar() {
|
||||
ids.forEach((id) => {
|
||||
updateNote({ id, is_archived: true });
|
||||
});
|
||||
toast.success(`Archived ${ids.length} note${ids.length > 1 ? "s" : ""}`);
|
||||
toast.success(t("Archived {{count}} note", { count: ids.length, defaultValue_other: "Archived {{count}} notes" }));
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const handleDeleteAll = () => {
|
||||
if (!confirm(`Are you sure you want to delete ${selectedIds.size} note(s)?`)) return;
|
||||
if (!confirm(t("Are you sure you want to delete {{count}} note?", { count: selectedIds.size, defaultValue_other: "Are you sure you want to delete {{count}} notes?" }))) return;
|
||||
|
||||
const ids = Array.from(selectedIds);
|
||||
ids.forEach((id) => {
|
||||
deleteNote(id);
|
||||
});
|
||||
toast.success(`Deleted ${ids.length} note${ids.length > 1 ? "s" : ""}`);
|
||||
toast.success(t("Deleted {{count}} note", { count: ids.length, defaultValue_other: "Deleted {{count}} notes" }));
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-background border rounded-full px-4 py-2 shadow-lg animate-in slide-in-from-bottom-4 duration-200">
|
||||
<span className="text-sm font-medium">
|
||||
{selectedIds.size} selected
|
||||
{t("{{count}} selected", { count: selectedIds.size })}
|
||||
</span>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
@@ -46,7 +48,7 @@ export function BulkActionsBar() {
|
||||
className="gap-2"
|
||||
>
|
||||
<Archive className="h-4 w-4" />
|
||||
Archive
|
||||
{t("Archive")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -56,7 +58,7 @@ export function BulkActionsBar() {
|
||||
className="gap-2 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Delete
|
||||
{t("Delete")}
|
||||
</Button>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
|
||||
import { NoteForm } from "./note-form";
|
||||
import { toast } from "sonner";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface CreateNoteDialogProps {
|
||||
trigger?: React.ReactNode;
|
||||
@@ -15,6 +16,7 @@ interface CreateNoteDialogProps {
|
||||
export function CreateNoteDialog({ trigger, open: controlledOpen, onOpenChange }: CreateNoteDialogProps) {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const { mutate: createNote, isPending } = useCreateNote();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Support both controlled and uncontrolled modes
|
||||
const isControlled = controlledOpen !== undefined;
|
||||
@@ -29,7 +31,7 @@ export function CreateNoteDialog({ trigger, open: controlledOpen, onOpenChange }
|
||||
|
||||
createNote({ ...data, tags }, {
|
||||
onSuccess: () => {
|
||||
toast.success("Note created");
|
||||
toast.success(t("Note created"));
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
@@ -41,7 +43,7 @@ export function CreateNoteDialog({ trigger, open: controlledOpen, onOpenChange }
|
||||
const defaultTrigger = (
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Note
|
||||
{t("New Note")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -59,12 +61,12 @@ export function CreateNoteDialog({ trigger, open: controlledOpen, onOpenChange }
|
||||
)}
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Note</DialogTitle>
|
||||
<DialogTitle>{t("Create Note")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new note to your collection.
|
||||
{t("Add a new note to your collection.")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<NoteForm onSubmit={onSubmit} isLoading={isPending} submitLabel="Create" />
|
||||
<NoteForm onSubmit={onSubmit} isLoading={isPending} submitLabel={t("Create")} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
40
k-notes-frontend/src/components/language-switcher.tsx
Normal file
40
k-notes-frontend/src/components/language-switcher.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Languages } from "lucide-react";
|
||||
|
||||
const LANGUAGES = [
|
||||
{ code: "en", label: "English" },
|
||||
{ code: "pl", label: "Polski" },
|
||||
];
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const changeLanguage = (languageCode: string) => {
|
||||
i18n.changeLanguage(languageCode);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="language" className="text-right flex items-center gap-2">
|
||||
<Languages className="h-4 w-4" />
|
||||
{t("Language")}
|
||||
</Label>
|
||||
<div className="col-span-3 flex gap-2">
|
||||
{LANGUAGES.map((lang) => (
|
||||
<Button
|
||||
key={lang.code}
|
||||
variant={i18n.language === lang.code ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
>
|
||||
{lang.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { VersionHistoryDialog } from "./version-history-dialog";
|
||||
import { NoteViewDialog } from "./note-view-dialog";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useBulkSelection } from "@/components/bulk-selection-context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NoteCardProps {
|
||||
note: Note;
|
||||
@@ -27,6 +28,7 @@ export function NoteCard({ note }: NoteCardProps) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [viewOpen, setViewOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Bulk selection
|
||||
const { isSelected, toggleSelection, isBulkMode } = useBulkSelection();
|
||||
@@ -57,7 +59,7 @@ export function NoteCard({ note }: NoteCardProps) {
|
||||
|
||||
const handleDelete = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (confirm("Are you sure?")) {
|
||||
if (confirm(t("Are you sure?"))) {
|
||||
deleteNote(note.id);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +76,7 @@ export function NoteCard({ note }: NoteCardProps) {
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
setEditing(false);
|
||||
toast.success("Note updated");
|
||||
toast.success(t("Note updated"));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -129,7 +131,7 @@ export function NoteCard({ note }: NoteCardProps) {
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end w-full gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={(e) => { e.stopPropagation(); setHistoryOpen(true); }} title="History">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={(e) => { e.stopPropagation(); setHistoryOpen(true); }} title={t("History")}>
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-black/5 dark:hover:bg-white/10" onClick={(e) => { e.stopPropagation(); setEditing(true); }}>
|
||||
@@ -151,7 +153,7 @@ export function NoteCard({ note }: NoteCardProps) {
|
||||
<Dialog open={editing} onOpenChange={setEditing}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Note</DialogTitle>
|
||||
<DialogTitle>{t("Edit Note")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<NoteForm
|
||||
defaultValues={{
|
||||
@@ -162,7 +164,7 @@ export function NoteCard({ note }: NoteCardProps) {
|
||||
tags: note.tags.map(t => t.name).join(", "),
|
||||
}}
|
||||
onSubmit={handleEdit}
|
||||
submitLabel="Update"
|
||||
submitLabel={t("Update")}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -8,16 +8,17 @@ import { Input } from "@/components/ui/input";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Editor } from "@/components/editor/editor";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const noteSchema = z.object({
|
||||
title: z.string().min(1, "Title is required").max(200, "Title too long"),
|
||||
const noteSchema = (t: any) => z.object({
|
||||
title: z.string().min(1, t("Title is required")).max(200, t("Title too long")),
|
||||
content: z.string().optional(),
|
||||
is_pinned: z.boolean().default(false),
|
||||
tags: z.string().optional(), // Comma separated for now
|
||||
color: z.string().default("DEFAULT"),
|
||||
});
|
||||
|
||||
type NoteFormValues = z.infer<typeof noteSchema>;
|
||||
type NoteFormValues = z.infer<ReturnType<typeof noteSchema>>;
|
||||
|
||||
interface NoteFormProps {
|
||||
defaultValues?: Partial<NoteFormValues>;
|
||||
@@ -27,8 +28,9 @@ interface NoteFormProps {
|
||||
}
|
||||
|
||||
export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Save" }: NoteFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm<NoteFormValues>({
|
||||
resolver: zodResolver(noteSchema) as any,
|
||||
resolver: zodResolver(noteSchema(t)) as any,
|
||||
defaultValues: {
|
||||
title: "",
|
||||
content: "",
|
||||
@@ -47,9 +49,9 @@ export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Sa
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<FormLabel>{t("Title")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Note title" {...field} />
|
||||
<Input placeholder={t("Note title")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -60,10 +62,10 @@ export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Sa
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormLabel>{t("Content")}</FormLabel>
|
||||
<FormControl>
|
||||
<Editor
|
||||
placeholder="Note content... Type / for commands"
|
||||
placeholder={t("Note content... Type / for commands")}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
@@ -77,9 +79,9 @@ export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Sa
|
||||
name="tags"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tags (comma separated)</FormLabel>
|
||||
<FormLabel>{t("Tags (comma separated)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="work, todo, ideas" {...field} />
|
||||
<Input placeholder={t("work, todo, ideas")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -91,7 +93,7 @@ export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Sa
|
||||
name="color"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Color</FormLabel>
|
||||
<FormLabel>{t("Color")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{NOTE_COLORS.map((color) => (
|
||||
@@ -125,13 +127,13 @@ export function NoteForm({ defaultValues, onSubmit, isLoading, submitLabel = "Sa
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>Pin this note</FormLabel>
|
||||
<FormLabel>{t("Pin this note")}</FormLabel>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={isLoading} className="w-full">
|
||||
{isLoading ? "Saving..." : submitLabel}
|
||||
{isLoading ? t("Saving...") : submitLabel}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label";
|
||||
import { toast } from "sonner";
|
||||
import { api } from "@/lib/api";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
|
||||
interface SettingsDialogProps {
|
||||
open: boolean;
|
||||
@@ -15,6 +17,7 @@ interface SettingsDialogProps {
|
||||
|
||||
export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: SettingsDialogProps) {
|
||||
const [url, setUrl] = useState("http://localhost:3000");
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem("k_notes_api_url");
|
||||
@@ -30,11 +33,11 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
||||
// Remove trailing slash if present
|
||||
const cleanUrl = url.replace(/\/$/, "");
|
||||
localStorage.setItem("k_notes_api_url", cleanUrl);
|
||||
toast.success("Settings saved. Please refresh the page.");
|
||||
toast.success(t("Settings saved. Please refresh the page."));
|
||||
onOpenChange(false);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
toast.error("Invalid URL");
|
||||
toast.error(t("Invalid URL"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,9 +54,9 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success("Export successful");
|
||||
toast.success(t("Export successful"));
|
||||
} catch (e) {
|
||||
toast.error("Export failed");
|
||||
toast.error(t("Export failed"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,12 +68,12 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
await api.importData(data);
|
||||
toast.success("Import successful. Reloading...");
|
||||
toast.success(t("Import successful. Reloading..."));
|
||||
onOpenChange(false);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Import failed");
|
||||
toast.error(t("Import failed"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,15 +81,15 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogTitle>{t("Settings")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure the application settings.
|
||||
{t("Configure the application settings.")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="url" className="text-right">
|
||||
Backend URL
|
||||
{t("Backend URL")}
|
||||
</Label>
|
||||
<Input
|
||||
id="url"
|
||||
@@ -98,22 +101,25 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
<LanguageSwitcher />
|
||||
|
||||
{dataManagementEnabled && <>
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="py-4 space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<h4 className="font-medium leading-none">Data Management</h4>
|
||||
<h4 className="font-medium leading-none">{t("Data Management")}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Export your notes for backup or import from a JSON file.
|
||||
{t("Export your notes for backup or import from a JSON file.")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" onClick={handleExport}>
|
||||
Export Data
|
||||
{t("Export Data")}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
|
||||
Import Data
|
||||
{t("Import Data")}
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
@@ -127,7 +133,7 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
||||
</>}
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleSave}>Save changes</Button>
|
||||
<Button onClick={handleSave}>{t("Save changes")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import '../i18n';
|
||||
import App from './App.tsx'
|
||||
import { Providers } from "@/components/providers";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Providers>
|
||||
|
||||
@@ -11,6 +11,7 @@ import Masonry from "react-masonry-css";
|
||||
import { NoteCardSkeletonGrid } from "@/components/note-card-skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Masonry breakpoint columns configuration
|
||||
const masonryBreakpoints = {
|
||||
@@ -26,6 +27,7 @@ export default function DashboardPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const isArchive = location.pathname === "/archive";
|
||||
const activeTag = searchParams.get("tag");
|
||||
const { t } = useTranslation();
|
||||
|
||||
// View mode state
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||
@@ -99,7 +101,7 @@ export default function DashboardPage() {
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
id="search-input"
|
||||
placeholder="Search your notes..."
|
||||
placeholder={t("Search your notes...")}
|
||||
className="pl-9 w-full bg-background"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
@@ -113,7 +115,7 @@ export default function DashboardPage() {
|
||||
size="icon"
|
||||
className={clsx("h-8 w-8", viewMode === "grid" && "bg-background shadow-sm")}
|
||||
onClick={() => setViewMode("grid")}
|
||||
title="Grid View"
|
||||
title={t("Grid View")}
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -122,7 +124,7 @@ export default function DashboardPage() {
|
||||
size="icon"
|
||||
className={clsx("h-8 w-8", viewMode === "list" && "bg-background shadow-sm")}
|
||||
onClick={() => setViewMode("list")}
|
||||
title="List View"
|
||||
title={t("List View")}
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -140,7 +142,7 @@ export default function DashboardPage() {
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-sm text-muted-foreground">Filtering by:</span>
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
{activeTag}
|
||||
{t(activeTag)}
|
||||
<Link to="/" className="ml-1 hover:text-destructive">
|
||||
<X className="h-3 w-3" />
|
||||
</Link>
|
||||
@@ -164,12 +166,12 @@ export default function DashboardPage() {
|
||||
<div className="text-center py-20 bg-background rounded-lg border border-dashed">
|
||||
<div className="text-muted-foreground">
|
||||
{searchQuery
|
||||
? "No matching notes found"
|
||||
? t("No matching notes found")
|
||||
: activeTag
|
||||
? `No notes with tag "${activeTag}"`
|
||||
? t("No notes with tag \"${activeTag}\"")
|
||||
: isArchive
|
||||
? "No archived notes yet"
|
||||
: "Your notes will appear here. Click + to create one."
|
||||
? t("No archived notes yet")
|
||||
: t("Your notes will appear here. Click + to create one.")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +182,7 @@ export default function DashboardPage() {
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3 text-muted-foreground">
|
||||
<Pin className="h-4 w-4 rotate-45" />
|
||||
<span className="text-sm font-medium uppercase tracking-wide">Pinned</span>
|
||||
<span className="text-sm font-medium uppercase tracking-wide">{t("Pinned")}</span>
|
||||
</div>
|
||||
{renderNotes(pinnedNotes)}
|
||||
</div>
|
||||
@@ -191,7 +193,7 @@ export default function DashboardPage() {
|
||||
<div>
|
||||
{pinnedNotes.length > 0 && (
|
||||
<div className="flex items-center gap-2 mb-3 text-muted-foreground border-t pt-4">
|
||||
<span className="text-sm font-medium uppercase tracking-wide">Others</span>
|
||||
<span className="text-sm font-medium uppercase tracking-wide">{t("Others")}</span>
|
||||
</div>
|
||||
)}
|
||||
{renderNotes(unpinnedNotes)}
|
||||
|
||||
Reference in New Issue
Block a user