diff --git a/frontend/public/notify_icon.png b/frontend/public/notify_icon.png new file mode 100644 index 0000000..367898c Binary files /dev/null and b/frontend/public/notify_icon.png differ diff --git a/frontend/src/app/apple-icon.png b/frontend/src/app/apple-icon.png new file mode 100644 index 0000000..25bf013 Binary files /dev/null and b/frontend/src/app/apple-icon.png differ diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico new file mode 100644 index 0000000..77600b9 Binary files /dev/null and b/frontend/src/app/favicon.ico differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 51ff795..1b8bbcd 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -6,7 +6,7 @@ @layer base { :root { - --background: 220 20% 97%; + --background: 220 14% 96%; --foreground: 220 20% 10%; --card: 0 0% 100%; --card-foreground: 220 20% 10%; @@ -17,27 +17,27 @@ --secondary: 220 15% 94%; --secondary-foreground: 220 20% 20%; --muted: 220 15% 94%; - --muted-foreground: 220 10% 50%; + --muted-foreground: 220 10% 46%; --accent: 220 15% 94%; --accent-foreground: 220 20% 20%; --destructive: 0 72% 55%; --destructive-foreground: 0 0% 100%; - --border: 220 15% 92%; - --input: 220 15% 90%; + --border: 220 15% 90%; + --input: 220 15% 88%; --ring: 220 80% 55%; - --radius: 0.5rem; + --radius: 0.75rem; --sidebar: 0 0% 100%; --sidebar-foreground: 220 20% 20%; --sidebar-primary: 220 80% 55%; --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 220 15% 96%; --sidebar-accent-foreground: 220 20% 20%; - --sidebar-border: 220 15% 94%; + --sidebar-border: 220 15% 93%; --sidebar-ring: 220 80% 55%; } html { - font-size: 28px; + font-size: 16px; } * { @@ -47,6 +47,9 @@ body { background-color: hsl(var(--background)); color: hsl(var(--foreground)); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } } @@ -92,38 +95,38 @@ } :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); + --radius: 0.75rem; + --background: hsl(220 14% 96%); + --foreground: hsl(220 20% 10%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(220 20% 10%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(220 20% 10%); + --primary: hsl(220 80% 55%); + --primary-foreground: hsl(0 0% 100%); + --secondary: hsl(220 15% 94%); + --secondary-foreground: hsl(220 20% 20%); + --muted: hsl(220 15% 94%); + --muted-foreground: hsl(220 10% 46%); + --accent: hsl(220 15% 94%); + --accent-foreground: hsl(220 20% 20%); + --destructive: hsl(0 72% 55%); + --border: hsl(220 15% 90%); + --input: hsl(220 15% 88%); + --ring: hsl(220 80% 55%); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar: hsl(0 0% 100%); + --sidebar-foreground: hsl(220 20% 20%); + --sidebar-primary: hsl(220 80% 55%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(220 15% 96%); + --sidebar-accent-foreground: hsl(220 20% 20%); + --sidebar-border: hsl(220 15% 93%); + --sidebar-ring: hsl(220 80% 55%); } .dark { diff --git a/frontend/src/app/icon.png b/frontend/src/app/icon.png new file mode 100644 index 0000000..a73b722 Binary files /dev/null and b/frontend/src/app/icon.png differ diff --git a/frontend/src/app/invites/page.tsx b/frontend/src/app/invites/page.tsx index d272c5e..b754380 100644 --- a/frontend/src/app/invites/page.tsx +++ b/frontend/src/app/invites/page.tsx @@ -115,7 +115,7 @@ const InvitesPage = () => { return { key: "statusRevoked", color: "bg-red-100 text-red-700" }; } if (new Date(invite.expires_at) < new Date()) { - return { key: "statusExpired", color: "bg-slate-100 text-slate-700" }; + return { key: "statusExpired", color: "bg-muted text-muted-foreground" }; } if (invite.used_count >= invite.max_uses) { return { key: "statusExhausted", color: "bg-amber-100 text-amber-700" }; @@ -125,8 +125,12 @@ const InvitesPage = () => { return ( +
+

{t("navInvites")}

+

{t("createInviteDesc")}

+
- + {t("createInvite")} {t("createInviteDesc")} @@ -160,7 +164,7 @@ const InvitesPage = () => { - + {t("myInvites")} {t("myInvitesDesc")} @@ -173,17 +177,17 @@ const InvitesPage = () => { return (
- + {invite.code}
-
+
{invite.used_count}/{invite.max_uses} @@ -209,7 +213,7 @@ const InvitesPage = () => { variant="ghost" size="sm" onClick={() => viewDetails(invite.id)} - className="text-slate-400 hover:text-blue-500" + className="text-muted-foreground hover:text-blue-500" > @@ -219,7 +223,7 @@ const InvitesPage = () => { @@ -248,7 +252,7 @@ const InvitesPage = () => { ); })} {invites.length === 0 && ( -
+
{t("noInvites")}
)} @@ -262,7 +266,7 @@ const InvitesPage = () => { - + {selectedInvite?.code} @@ -275,19 +279,19 @@ const InvitesPage = () => { selectedInvite.registeredUsers.map((user) => (
-
{user.username}
-
+
{user.username}
+
{formatDateTime(user.createdAt)}
)) ) : ( -
+
{t("noRegisteredUsers")}
)} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index a01e259..621e796 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -5,6 +5,10 @@ import { I18nProvider } from "@/lib/i18n"; export const metadata = { title: "Notify", description: "简洁提醒应用", + icons: { + icon: "/icon.png", + apple: "/apple-icon.png", + }, }; const RootLayout = ({ children }: { children: React.ReactNode }) => { diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index aa35b39..751b01b 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -31,11 +31,11 @@ const LoginPage = () => { }; return ( -
+
- + {t("login")} {t("loginWelcome")} @@ -66,7 +66,7 @@ const LoginPage = () => { {t("login")} {t("noAccount")} diff --git a/frontend/src/app/notifications/page.tsx b/frontend/src/app/notifications/page.tsx index fdc6d16..1dc6cdc 100644 --- a/frontend/src/app/notifications/page.tsx +++ b/frontend/src/app/notifications/page.tsx @@ -46,10 +46,10 @@ const NotificationsPage = () => { return ( - +
- {t("notifications")} + {t("notifications")} {t("notificationsDesc")}
@@ -66,13 +66,13 @@ const NotificationsPage = () => { {notifications.map((item) => (
-
+
{t("triggerTime")}:{new Date(item.triggerAt).toLocaleString()}
-
{t("channel")}:{item.channel}
+
{t("channel")}:{item.channel}
))} {notifications.length === 0 && ( -
+
{t("noNotification")}
)} diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx index b2068bb..5771c51 100644 --- a/frontend/src/app/register/page.tsx +++ b/frontend/src/app/register/page.tsx @@ -32,11 +32,11 @@ const RegisterPage = () => { }; return ( -
+
- + {t("registerTitle")} {t("registerDesc")} @@ -76,7 +76,7 @@ const RegisterPage = () => { {t("register")} {t("hasAccount")} diff --git a/frontend/src/app/reminders/page.tsx b/frontend/src/app/reminders/page.tsx index b863ee5..bd7e1cb 100644 --- a/frontend/src/app/reminders/page.tsx +++ b/frontend/src/app/reminders/page.tsx @@ -160,8 +160,12 @@ const RemindersPage = () => { return ( +
+

{t("navReminder")}

+

{t("createReminderDesc")}

+
- + {t("createReminder")} {t("createReminderDesc")} @@ -279,14 +283,14 @@ const RemindersPage = () => { return ( {user.username} @@ -307,8 +311,8 @@ const RemindersPage = () => {
{showBarkSettings && ( -
-

{t("barkSettingsDesc")}

+
+

{t("barkSettingsDesc")}

@@ -393,7 +397,7 @@ const RemindersPage = () => { - + {t("reminderList")} {t("reminderListDesc")} @@ -403,29 +407,29 @@ const RemindersPage = () => { {tasks.map((task) => (
- {task.title} + {task.title} {task.recurrenceRule && ( {getRecurrenceLabel(task.recurrenceRule)} )}
-
+
{formatDateTime(task.dueAt)}
- {t("reminder")} + {t("reminder")} @@ -452,7 +456,7 @@ const RemindersPage = () => {
))} {tasks.length === 0 && ( -
+
{t("noReminder")}
)} diff --git a/frontend/src/app/todos/page.tsx b/frontend/src/app/todos/page.tsx index 8bdcc5c..9e34f21 100644 --- a/frontend/src/app/todos/page.tsx +++ b/frontend/src/app/todos/page.tsx @@ -32,6 +32,7 @@ import { Label } from "@/components/ui/label"; import { RecurrencePicker, type RecurrenceRule as RecurrenceRuleInput } from "@/components/ui/recurrence-picker"; import { api } from "@/lib/api"; import { useTranslation } from "@/lib/i18n"; +import { Bell, CalendarCheck, CheckCircle2, ListTodo, Plus, Repeat, Trash2 } from "lucide-react"; type RecurrenceType = "hourly" | "daily" | "weekly" | "monthly" | "yearly"; @@ -54,9 +55,16 @@ type Todo = { checkInAt?: string | null; }; +type ReminderTask = { + id: string; + title: string; + dueAt: string; +}; + const TodosPage = () => { const t = useTranslation(); const [todos, setTodos] = useState([]); + const [reminders, setReminders] = useState([]); const [title, setTitle] = useState(""); const [dueAt, setDueAt] = useState(undefined); const [offsetMinutes, setOffsetMinutes] = useState(10); @@ -70,8 +78,14 @@ const TodosPage = () => { setTodos(data); }; + const loadReminders = async () => { + const data = (await api.getReminderTasks()) as ReminderTask[]; + setReminders(data); + }; + useEffect(() => { loadTodos().catch(() => null); + loadReminders().catch(() => null); }, []); const resetForm = () => { @@ -118,8 +132,6 @@ const TodosPage = () => { } }; - const colorDots = ["bg-emerald-500", "bg-amber-500", "bg-blue-500", "bg-rose-500"]; - const getRecurrenceLabel = (rule: RecurrenceRule): string => { const interval = rule.interval || 1; if (rule.type === "weekly" && interval === 2) { @@ -134,166 +146,307 @@ const TodosPage = () => { } }; + const formatDueDate = (dateStr: string): string => { + const d = new Date(dateStr); + const now = new Date(); + const isToday = d.toDateString() === now.toDateString(); + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + const isTomorrow = d.toDateString() === tomorrow.toDateString(); + const pad = (n: number) => n.toString().padStart(2, "0"); + const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`; + + if (isToday) return `Today ${timeStr}`; + if (isTomorrow) return `Tomorrow ${timeStr}`; + return `${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${timeStr}`; + }; + + const getDueStatus = (dateStr: string, isCheckedIn: boolean): { label: string; className: string } => { + if (isCheckedIn) { + return { label: t("checkInSuccess"), className: "bg-emerald-50 text-emerald-700 border-emerald-200" }; + } + const d = new Date(dateStr); + const now = new Date(); + if (d < now) { + return { label: "Overdue", className: "bg-red-50 text-red-700 border-red-200" }; + } + const hoursUntil = (d.getTime() - now.getTime()) / (1000 * 60 * 60); + if (hoursUntil <= 24) { + return { label: "Due Soon", className: "bg-amber-50 text-amber-700 border-amber-200" }; + } + return { label: "Upcoming", className: "bg-blue-50 text-blue-700 border-blue-200" }; + }; + + // Stats + const totalTasks = todos.length; + const checkedInCount = todos.filter((t) => t.isCheckedIn).length; + const recurringCount = todos.filter((t) => t.recurrenceRule).length; + + // Upcoming reminders (next 3, sorted by dueAt) + const upcomingReminders = reminders + .filter((r) => new Date(r.dueAt) >= new Date()) + .sort((a, b) => new Date(a.dueAt).getTime() - new Date(b.dueAt).getTime()) + .slice(0, 3); + return ( - - -
- {t("myTodoList")} - {t("myTodoListDesc")} -
- - - - - -
- - {t("createTask")} - {t("createTaskDesc")} - -
-
- - setTitle(event.target.value)} - /> -
-
- - -
-
- - setOffsetMinutes(Number(event.target.value))} - /> -
-
-
- setIsRecurring(checked === true)} - /> - -
- {isRecurring && ( -
- -
- )} -
-
- - - -
-
-
-
- -
- {todos.map((todo, index) => ( -
- -
-
- {todo.title} - {todo.recurrenceRule && ( - - {getRecurrenceLabel(todo.recurrenceRule)} - - )} - {todo.checkInCount > 0 && ( - - {todo.checkInCount}x - - )} -
-
- {t("due")} {new Date(todo.dueAt).toLocaleString()} -
-
- - - - - - - - {t("confirmDelete")} - - {t("confirmDeleteDesc", { title: todo.title })} - - - - {t("cancel")} - deleteTodo(todo.id)} + + + + + + + {todos.length > 0 ? ( +
+ {/* Table header */} +
+
+
{t("title")}
+
{t("due")}
+
{t("status")}
+
+
+ {todos.map((todo) => { + const status = getDueStatus(todo.dueAt, todo.isCheckedIn); + return ( +
+ {/* Checkbox / Check-in */} + + + {/* Title + badges */} +
+
+ + {todo.title} + + {todo.recurrenceRule && ( + + + {getRecurrenceLabel(todo.recurrenceRule)} + + )} + {todo.checkInCount > 0 && ( + + {todo.checkInCount}x + + )} +
+
+ + {/* Due date */} +
+ {formatDueDate(todo.dueAt)} +
+ + {/* Status pill */} + + {status.label} + + + {/* Delete */} +
+ + + + + + + {t("confirmDelete")} + + {t("confirmDeleteDesc", { title: todo.title })} + + + + {t("cancel")} + deleteTodo(todo.id)} + > + {t("delete")} + + + + +
+
+ ); + })}
- ))} - {todos.length === 0 && ( -
- {t("noTodo")} + ) : ( + /* Empty state */ +
+
+ +
+

{t("todoEmptyTitle")}

+

{t("todoEmptyDesc")}

+
)} + + + + {/* Right column - Stats & Upcoming */} +
+ {/* Stats cards */} +
+ +
+
+ +
+
+
{totalTasks}
+
{t("totalTasks")}
+
+
+
+ +
+
+ +
+
+
{checkedInCount}
+
{t("checkedInToday")}
+
+
+
+ +
+
+ +
+
+
{recurringCount}
+
{t("recurringTasks")}
+
+
+
- - + + {/* Upcoming reminders */} + + + {t("upcomingReminders")} + + + {upcomingReminders.length > 0 ? ( +
+ {upcomingReminders.map((reminder) => ( +
+
+ +
+
+
{reminder.title}
+
{formatDueDate(reminder.dueAt)}
+
+
+ ))} +
+ ) : ( +

{t("noUpcomingReminders")}

+ )} +
+
+
+
); }; diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 735b271..b2f1ad0 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -2,38 +2,18 @@ import type { ReactNode } from "react"; -import { Button } from "@/components/ui/button"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import AppSidebar from "@/components/AppSidebar"; -import LanguageSwitcher from "@/components/LanguageSwitcher"; -import { clearToken } from "@/lib/auth"; -import { useTranslation } from "@/lib/i18n"; import { NotificationProvider } from "@/lib/notification-context"; import { UserProvider } from "@/lib/user-context"; const AppShellContent = ({ children }: { children: ReactNode }) => { - const t = useTranslation(); - return ( -
+
-
-
-
-
-

Notify

-

{t("appDesc")}

-
-
- - -
-
-
+
{children}
diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx index e3eebd1..1008ee9 100644 --- a/frontend/src/components/AppSidebar.tsx +++ b/frontend/src/components/AppSidebar.tsx @@ -1,8 +1,9 @@ "use client"; +import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Bell, BellDot, ListTodo, Settings, UserPlus } from "lucide-react"; +import { Bell, BellDot, ListTodo, LogOut, Settings, UserPlus } from "lucide-react"; import { Sidebar, @@ -10,7 +11,6 @@ import { SidebarFooter, SidebarGroup, SidebarGroupContent, - SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, @@ -18,9 +18,11 @@ import { SidebarSeparator, } from "@/components/ui/sidebar"; import Avatar from "@/components/ui/avatar"; +import LanguageSwitcher from "@/components/LanguageSwitcher"; import { useTranslation, type TranslationKey } from "@/lib/i18n"; import { useNotification } from "@/lib/notification-context"; import { useUser } from "@/lib/user-context"; +import { clearToken } from "@/lib/auth"; const navItems: { href: string; labelKey: TranslationKey; icon: typeof ListTodo }[] = [ { href: "/todos", labelKey: "navTodo", icon: ListTodo }, @@ -40,19 +42,14 @@ const AppSidebar = () => {
- - - - + Notify + notify
- - {t("navigation")} - {navItems.map((item) => { @@ -67,7 +64,7 @@ const AppSidebar = () => { > - + {isNotifications && unreadCount > 0 && ( )} @@ -90,12 +87,25 @@ const AppSidebar = () => { - -
- - - {user?.username} - + +
+
+ + + {user?.username} + +
+
+ + +
diff --git a/frontend/src/components/LanguageSwitcher.tsx b/frontend/src/components/LanguageSwitcher.tsx index 7181a28..d2541f1 100644 --- a/frontend/src/components/LanguageSwitcher.tsx +++ b/frontend/src/components/LanguageSwitcher.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { Globe } from "lucide-react"; -import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, @@ -17,28 +16,28 @@ const languages: { value: Locale; label: string }[] = [ ]; const LanguageSwitcher = () => { - const { locale, setLocale, t } = useI18n(); + const { locale, setLocale } = useI18n(); const [open, setOpen] = useState(false); return ( - + -
+
{languages.map((lang) => ( )} -

{t("avatarHint")}

+

{t("avatarHint")}

@@ -201,11 +201,11 @@ const SettingsPanel = () => {
-
+
-
{t("webNotifications")}
-
{t("webNotificationsDesc")}
+
{t("webNotifications")}
+
{t("webNotificationsDesc")}
{ } />
-
+
-
{t("barkAlerts")}
-
{t("barkAlertsDesc")}
+
{t("barkAlerts")}
+
{t("barkAlertsDesc")}
{
{/* Change Password Section */} -
-

{t("changePassword")}

+
+

{t("changePassword")}

diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index 64e912f..9d43abd 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( (({ className, ...props }, ref) => ( )); @@ -76,7 +76,7 @@ const AlertDialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 65d4fcd..f3bc63d 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -5,16 +5,16 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", + "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-md", destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + "border border-border/80 bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", @@ -22,8 +22,8 @@ const buttonVariants = cva( }, size: { default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", + sm: "h-8 rounded-lg px-3 text-xs", + lg: "h-10 rounded-lg px-8", icon: "h-9 w-9", }, }, diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx index 2b88a48..a194dba 100644 --- a/frontend/src/components/ui/card.tsx +++ b/frontend/src/components/ui/card.tsx @@ -6,7 +6,7 @@ const Card = React.forwardRef (
) @@ -15,14 +15,14 @@ Card.displayName = "Card"; const CardHeader = React.forwardRef>( ({ className, ...props }, ref) => ( -
+
) ); CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef>( ({ className, ...props }, ref) => ( -

+

) ); CardTitle.displayName = "CardTitle"; @@ -31,7 +31,7 @@ const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

+

)); CardDescription.displayName = "CardDescription"; diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx index 1e674a4..d170faf 100644 --- a/frontend/src/components/ui/checkbox.tsx +++ b/frontend/src/components/ui/checkbox.tsx @@ -11,13 +11,13 @@ const Checkbox = React.forwardRef< - + )); diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index 54497b9..619d0a0 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef< {children} - + Close @@ -72,7 +72,7 @@ const DialogTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )); @@ -84,7 +84,7 @@ const DialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 012a9fe..865df9f 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef (

) @@ -145,12 +145,12 @@ const SidebarMenuItem = React.forwardRef>( ({ className, ...props }, ref) => ( -
+
) ); SidebarSeparator.displayName = "SidebarSeparator"; diff --git a/frontend/src/lib/i18n/translations.ts b/frontend/src/lib/i18n/translations.ts index 47f730c..ef8038b 100644 --- a/frontend/src/lib/i18n/translations.ts +++ b/frontend/src/lib/i18n/translations.ts @@ -51,6 +51,13 @@ export const translations = { addTask: "+ 添加任务", due: "截止", noTodo: "暂无 Todo", + todoEmptyTitle: "还没有任何任务", + todoEmptyDesc: "创建你的第一个任务,开始高效管理时间", + totalTasks: "总任务", + checkedInToday: "今日打卡", + recurringTasks: "周期任务", + upcomingReminders: "即将到来的提醒", + noUpcomingReminders: "暂无即将到来的提醒", confirmDelete: "确认删除", confirmDeleteDesc: "确定要删除「{title}」吗?此操作无法撤销。", checkIn: "打卡", @@ -275,6 +282,13 @@ export const translations = { addTask: "+ Add Task", due: "Due", noTodo: "No Todo items", + todoEmptyTitle: "No tasks yet", + todoEmptyDesc: "Create your first task and start managing your time efficiently", + totalTasks: "Total Tasks", + checkedInToday: "Checked In", + recurringTasks: "Recurring", + upcomingReminders: "Upcoming Reminders", + noUpcomingReminders: "No upcoming reminders", confirmDelete: "Confirm Delete", confirmDeleteDesc: "Are you sure you want to delete \"{title}\"? This action cannot be undone.", checkIn: "Check In", diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index a505ff6..0ad0770 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -56,9 +56,11 @@ const config: Config = { }, }, borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", + "2xl": "calc(var(--radius) + 4px)", + xl: "var(--radius)", + lg: "calc(var(--radius) - 2px)", + md: "calc(var(--radius) - 4px)", + sm: "calc(var(--radius) - 6px)", }, }, },