Repository: Dev-Huang1/One-Calendar
Pull request #17: Feat: Add shortcut key feature
Author: login: EvanTechDev
Labels: name: update
description: Update new feature
Created: 2025-03-23T12:45:13Z | Merged: 2025-03-23T12:47:44Z

Whats new?


Shortcuts, time zone updates, default view settings

好的，这是翻译成中文的 pull request 总结：

## Sourcery 总结

为日历添加键盘快捷键，允许用户使用键盘导航和创建事件。它还引入了默认视图的设置，并将时区选择更新为使用 GMT 格式。

新功能：
- 实现用于日历导航和事件创建的键盘快捷键。
- 添加一个设置，允许用户设置日历的默认视图（日、周、月）。

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Adds keyboard shortcuts to the calendar, allowing users to navigate and create events using the keyboard. It also introduces a setting for the default view and updates the timezone selection to use GMT format.

New Features:
- Implements keyboard shortcuts for calendar navigation and event creation.
- Adds a setting to allow users to set the default view for the calendar (day, week, month).

</details>


Diff:
diff --git a/components/Calendar.tsx b/components/Calendar.tsx
index 5b389e09..9e8eaa0f 100644
--- a/components/Calendar.tsx
+++ b/components/Calendar.tsx
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef, Suspense } from "react"
 import { Button } from "@/components/ui/button"
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
 import { Input } from "@/components/ui/input"
-import { CalendarIcon, ChevronLeft, ChevronRight, Search } from "lucide-react"
+import { ChevronLeft, ChevronRight, Search } from "lucide-react"
 import { addDays, subDays } from "date-fns"
 import Sidebar from "./Sidebar"
 import DayView from "./DayView"
@@ -67,6 +67,80 @@ export default function Calendar() {
   // 新增：快速创建事件的初始时间
   const [quickCreateStartTime, setQuickCreateStartTime] = useState<Date | null>(null)
 
+  // Add the new state variables for default view and keyboard shortcuts
+  const [defaultView, setDefaultView] = useLocalStorage<ViewType>("default-view", "week")
+  const [enableShortcuts, setEnableShortcuts] = useLocalStorage<boolean>("enable-shortcuts", true)
+
+  // Add a useEffect to set the initial view based on the default view setting
+  useEffect(() => {
+    // Only set the view on initial load
+    if (view !== defaultView) {
+      setView(defaultView as ViewType)
+    }
+  }, [])
+
+  // Add the keyboard shortcut handler
+  useEffect(() => {
+    if (!enableShortcuts) return
+
+    const handleKeyDown = (e: KeyboardEvent) => {
+      // Skip if user is typing in an input, textarea, or contentEditable element
+      if (
+        document.activeElement instanceof HTMLInputElement ||
+        document.activeElement instanceof HTMLTextAreaElement ||
+        document.activeElement?.getAttribute("contenteditable") === "true"
+      ) {
+        return
+      }
+
+      switch (e.key) {
+        case "n":
+        case "N":
+          e.preventDefault()
+          setEventDialogOpen(true)
+          break
+        case "/":
+          e.preventDefault()
+          // Focus the search input
+          const searchInput = document.querySelector('input[placeholder="' + t.searchEvents + '"]') as HTMLInputElement
+          if (searchInput) {
+            searchInput.focus()
+          }
+          break
+        case "t":
+        case "T":
+          e.preventDefault()
+          handleTodayClick()
+          break
+        case "1":
+          e.preventDefault()
+          setView("day")
+          break
+        case "2":
+          e.preventDefault()
+          setView("week")
+          break
+        case "3":
+          e.preventDefault()
+          setView("month")
+          break
+        case "ArrowRight":
+          e.preventDefault()
+          handleNext()
+          break
+        case "ArrowLeft":
+          e.preventDefault()
+          handlePrevious()
+          break
+      }
+    }
+
+    window.addEventListener("keydown", handleKeyDown)
+    return () => {
+      window.addEventListener("keydown", handleKeyDown)
+    }
+  }, [enableShortcuts, t.searchEvents])
+
   const handleDateSelect = (date: Date) => {
     setDate(date)
     setSidebarDate(date)
@@ -110,8 +184,8 @@ export default function Calendar() {
   }
 
   const handleEventClick = (event: CalendarEvent) => {
-    setSelectedEvent(event)
-    setEventDialogOpen(true)
+    setPreviewEvent(event)
+    setPreviewOpen(true)
   }
 
   const handleEventAdd = (event: CalendarEvent) => {
@@ -229,10 +303,8 @@ export default function Calendar() {
       </div>
 
       <div className="flex-1 flex flex-col">
-        <header className="flex items-center justify-between px-4 h-16 border-b">
+        <header className="flex items-center justify-between px-4 h-16 border-b relative z-40 bg-background">
           <div className="flex items-center space-x-4">
-            <CalendarIcon className="h-8 w-8 text-blue-500" />
-            <h1 className="text-xl font-semibold">{t.calendar}</h1>
             <Button variant="outline" size="sm" onClick={handleTodayClick}>
               {t.today || "今天"}
             </Button>
@@ -255,18 +327,20 @@ export default function Calendar() {
           </div>
 
           <div className="flex items-center space-x-2">
-            <Select value={view} onValueChange={(value: ViewType) => setView(value)}>
-              <SelectTrigger className="w-[120px]">
-                <SelectValue />
-              </SelectTrigger>
-              <SelectContent>
-                <SelectItem value="day">{t.day}</SelectItem>
-                <SelectItem value="week">{t.week}</SelectItem>
-                <SelectItem value="month">{t.month}</SelectItem>
-                <SelectItem value="analytics">{t.analytics}</SelectItem>
-              </SelectContent>
-            </Select>
-            <div className="relative">
+            <div className="relative z-50">
+              <Select value={view} onValueChange={(value: ViewType) => setView(value)}>
+                <SelectTrigger className="w-[120px]">
+                  <SelectValue />
+                </SelectTrigger>
+                <SelectContent>
+                  <SelectItem value="day">{t.day}</SelectItem>
+                  <SelectItem value="week">{t.week}</SelectItem>
+                  <SelectItem value="month">{t.month}</SelectItem>
+                  <SelectItem value="analytics">{t.analytics}</SelectItem>
+                </SelectContent>
+              </Select>
+            </div>
+            <div className="relative z-50">
               <Search className="h-5 w-5 absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400" />
               <Input
                 type="text"
@@ -276,22 +350,30 @@ export default function Calendar() {
                 className="pl-9 pr-4 py-2 w-48"
               />
               {searchTerm && (
-                <div className="absolute z-10 mt-2 w-full bg-white border border-gray-200 rounded-md shadow-lg">
-                  <ScrollArea className="max-h-[300px]">
-                    {filteredEvents.map((event) => (
-                      <div
-                        key={event.id}
-                        className="p-2 hover:bg-gray-100 cursor-pointer"
-                        onClick={() => {
-                          setPreviewEvent(event)
-                          setPreviewOpen(true)
-                          setSearchTerm("")
-                        }}
-                      >
-                        <div className="font-medium">{event.title}</div>
-                        <div className="text-sm text-gray-500">{formatDateDisplay(new Date(event.startDate))}</div>
+                <div className="absolute z-50 mt-2 w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg">
+                  <ScrollArea className="h-[300px] py-2">
+                    {filteredEvents.length > 0 ? (
+                      filteredEvents.map((event) => (
+                        <div
+                          key={event.id}
+                          className="px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
+                          onClick={() => {
+                            setPreviewEvent(event)
+                            setPreviewOpen(true)
+                            setSearchTerm("")
+                          }}
+                        >
+                          <div className="font-medium">{event.title}</div>
+                          <div className="text-sm text-gray-500 dark:text-gray-400">
+                            {formatDateDisplay(new Date(event.startDate))}
+                          </div>
+                        </div>
+                      ))
+                    ) : (
+                      <div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
+                        {language === "zh" ? "没有找到匹配的事件" : "No matching events found"}
                       </div>
-                    ))}
+                    )}
                   </ScrollArea>
                 </div>
               )}
@@ -305,6 +387,10 @@ export default function Calendar() {
               setTimezone={setTimezone}
               notificationSound={notificationSound}
               setNotificationSound={setNotificationSound}
+              defaultView={defaultView}
+              setDefaultView={setDefaultView}
+              enableShortcuts={enableShortcuts}
+              setEnableShortcuts={setEnableShortcuts}
             />
           </div>
         </header>
diff --git a/components/DayView.tsx b/components/DayView.tsx
index 4987edb2..ade3360b 100644
--- a/components/DayView.tsx
+++ b/components/DayView.tsx
@@ -238,7 +238,7 @@ export default function DayView({ date, events, onEventClick, onTimeSlotClick, l
 
   return (
     <div className="flex flex-col h-full">
-      <div className="grid grid-cols-[100px_1fr] border-b">
+      <div className="grid grid-cols-[100px_1fr] border-b relative z-30 bg-background">
         <div className="py-2 text-center">
           <div className="text-sm text-muted-foreground">
             {format(date, "E", { locale: language === "zh" ? zhCN : enUS })}
diff --git a/components/Settings.tsx b/components/Settings.tsx
index 1fa3945b..270e0eda 100644
--- a/components/Settings.tsx
+++ b/components/Settings.tsx
@@ -7,7 +7,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
 import { Label } from "@/components/ui/label"
 import { translations, type Language } from "@/lib/i18n"
 import type { NOTIFICATION_SOUNDS } from "@/utils/notifications"
+import { Switch } from "@/components/ui/switch"
 
+// Update the timezone options to use GMT format and add new settings for default view and keyboard shortcuts
+
+// First, add the defaultView and enableShortcuts props to the SettingsProps interface
 interface SettingsProps {
   language: Language
   setLanguage: (lang: Language) => void
@@ -17,8 +21,13 @@ interface SettingsProps {
   setTimezone: (timezone: string) => void
   notificationSound: keyof typeof NOTIFICATION_SOUNDS
   setNotificationSound: (sound: keyof typeof NOTIFICATION_SOUNDS) => void
+  defaultView: string
+  setDefaultView: (view: string) => void
+  enableShortcuts: boolean
+  setEnableShortcuts: (enable: boolean) => void
 }
 
+// Then update the Settings component to include the new props
 export default function Settings({
   language,
   setLanguage,
@@ -28,14 +37,47 @@ export default function Settings({
   setTimezone,
   notificationSound,
   setNotificationSound,
+  defaultView,
+  setDefaultView,
+  enableShortcuts,
+  setEnableShortcuts,
 }: SettingsProps) {
   const t = translations[language]
-  const timezones = Intl.supportedValuesOf("timeZone")
 
-  // 添加一个处理语言变化的函数
+  // Replace the timezones array with GMT formatted timezones
+  const getGMTTimezones = () => {
+    const timezones = Intl.supportedValuesOf("timeZone")
+    const now = new Date()
+
+    return timezones
+      .map((tz) => {
+        try {
+          // Get the GMT offset for this timezone
+          const offsetMinutes = new Date(now.toLocaleString("en-US", { timeZone: tz })).getTimezoneOffset() * -1
+          const offsetHours = Math.abs(Math.floor(offsetMinutes / 60))
+          const offsetMins = Math.abs(offsetMinutes % 60)
+          const offsetSign = offsetMinutes >= 0 ? "+" : "-"
+          const offsetString = `GMT${offsetSign}${offsetHours.toString().padStart(2, "0")}:${offsetMins.toString().padStart(2, "0")}`
+
+          return {
+            value: tz,
+            label: `${tz} (${offsetString})`,
+          }
+        } catch (e) {
+          return {
+            value: tz,
+            label: tz,
+          }
+        }
+      })
+      .sort((a, b) => a.label.localeCompare(b.label))
+  }
+
+  const gmtTimezones = getGMTTimezones()
+
+  // Add the handleLanguageChange function as before
   const handleLanguageChange = (newLang: Language) => {
     setLanguage(newLang)
-    // 触发一个自定义事件，通知其他组件语言已更改
     window.dispatchEvent(new CustomEvent("languagechange", { detail: { language: newLang } }))
   }
 
@@ -82,6 +124,20 @@ export default function Settings({
             </Select>
           </div>
 
+          <div className="space-y-2">
+            <Label htmlFor="default-view">{language === "zh" ? "默认视图" : "Default View"}</Label>
+            <Select value={defaultView} onValueChange={setDefaultView}>
+              <SelectTrigger id="default-view">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="day">{t.day}</SelectItem>
+                <SelectItem value="week">{t.week}</SelectItem>
+                <SelectItem value="month">{t.month}</SelectItem>
+              </SelectContent>
+            </Select>
+          </div>
+
           <div className="space-y-2">
             <Label htmlFor="timezone">{t.timezone}</Label>
             <Select value={timezone} onValueChange={setTimezone}>
@@ -89,9 +145,9 @@ export default function Settings({
                 <SelectValue />
               </SelectTrigger>
               <SelectContent className="max-h-[200px]">
-                {timezones.map((tz) => (
-                  <SelectItem key={tz} value={tz}>
-                    {tz}
+                {gmtTimezones.map((tz) => (
+                  <SelectItem key={tz.value} value={tz.value}>
+                    {tz.label}
                   </SelectItem>
                 ))}
               </SelectContent>
@@ -115,6 +171,27 @@ export default function Settings({
               </Select>
             </div>
           </div>
+
+          <div className="flex items-center space-x-2">
+            <Switch id="enable-shortcuts" checked={enableShortcuts} onCheckedChange={setEnableShortcuts} />
+            <Label htmlFor="enable-shortcuts">{language === "zh" ? "开启快捷键" : "Enable Keyboard Shortcuts"}</Label>
+          </div>
+
+          {enableShortcuts && (
+            <div className="rounded-md border p-4">
+              <h3 className="mb-2 font-medium">{language === "zh" ? "可用快捷键" : "Available Shortcuts"}</h3>
+              <div className="grid grid-cols-2 gap-2 text-sm">
+                <div>N - {language === "zh" ? "创建新事件" : "New Event"}</div>
+                <div>/ - {language === "zh" ? "搜索事件" : "Search Events"}</div>
+                <div>T - {language === "zh" ? "今天" : "Today"}</div>
+                <div>1 - {language === "zh" ? "日视图" : "Day View"}</div>
+                <div>2 - {language === "zh" ? "周视图" : "Week View"}</div>
+                <div>3 - {language === "zh" ? "月视图" : "Month View"}</div>
+                <div>→ - {language === "zh" ? "下个周期" : "Next Period"}</div>
+                <div>← - {language === "zh" ? "上个周期" : "Previous Period"}</div>
+              </div>
+            </div>
+          )}
         </div>
       </SheetContent>
     </Sheet>
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index be66aaee..9af72538 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -11,6 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
 import { Label } from "@/components/ui/label"
 import { translations } from "@/lib/i18n"
 import { useCalendar } from "@/contexts/CalendarContext"
+import { CalendarIcon } from "lucide-react"
 
 interface SidebarProps {
   onCreateEvent: () => void
@@ -36,7 +37,7 @@ export default function Sidebar({
   language = "zh",
   selectedDate,
 }: SidebarProps) {
-  // 使用 Context 中的日历分类数据
+  // 使�� Context 中的日历分类数据
   const { calendars, addCategory: addCategoryToContext, removeCategory: removeCategoryFromContext } = useCalendar()
 
   const [newCategoryName, setNewCategoryName] = useState("")
@@ -82,11 +83,16 @@ export default function Sidebar({
   return (
     <div className="w-80 border-r bg-background overflow-y-auto">
       <div className="p-4">
+        {/* Add calendar icon and title at the top */}
+        <div className="flex items-center mb-4">
+          <CalendarIcon className="h-6 w-6 text-blue-500 mr-2" />
+          <h1 className="text-lg font-semibold">{t.calendar}</h1>
+        </div>
+
         <Button
-          className="w-full justify-start bg-[#0066FF] text-white hover:bg-[#0052CC] mb-4"
+          className="w-full justify-center bg-[#0066FF] text-white hover:bg-[#0052CC] mb-4 h-10"
           onClick={onCreateEvent}
         >
-          <Plus className="mr-2 h-4 w-4" />
           {t.createEvent}
         </Button>
 
diff --git a/components/WeekView.tsx b/components/WeekView.tsx
index 34dce703..0bfcbe66 100644
--- a/components/WeekView.tsx
+++ b/components/WeekView.tsx
@@ -256,10 +256,10 @@ export default function WeekView({
 
   return (
     <div className="flex flex-col h-full">
-      <div className="grid grid-cols-[100px_repeat(7,1fr)] divide-x">
-        <div />
+      <div className="grid grid-cols-[100px_repeat(7,1fr)] divide-x relative z-30 bg-background">
+        <div className="sticky top-0 z-30 bg-background" />
         {weekDays.map((day) => (
-          <div key={day.toString()} className="sticky top-0 z-10 bg-background p-2 text-center">
+          <div key={day.toString()} className="sticky top-0 z-30 bg-background p-2 text-center">
             <div>{format(day, "E", { locale: language === "zh" ? zhCN : enUS })}</div>
             {/* 如果是今天，使用蓝色高亮显示日期 */}
             <div className={cn(isSameDay(day, today) ? "text-[#0066FF] font-bold" : "")}>{format(day, "d")}</div>
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 00000000..bc69cf2d
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+  React.ElementRef<typeof SwitchPrimitives.Root>,
+  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
+>(({ className, ...props }, ref) => (
+  <SwitchPrimitives.Root
+    className={cn(
+      "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
+      className
+    )}
+    {...props}
+    ref={ref}
+  >
+    <SwitchPrimitives.Thumb
+      className={cn(
+        "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
+      )}
+    />
+  </SwitchPrimitives.Root>
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/lib/i18n.ts b/lib/i18n.ts
index 052dbeae..7bda6055 100644
--- a/lib/i18n.ts
+++ b/lib/i18n.ts
@@ -159,6 +159,17 @@ export const translations = {
     welcomeToAnalytics: "Welcome to Advanced Analytics",
     analyticsDescription: "We've added powerful features to help you better manage your time and schedule",
     unnamedEvent: "Unnamed Event",
+    enableShortcuts: "Enable Keyboard Shortcuts",
+    availableShortcuts: "Available Shortcuts",
+    defaultView: "Default View",
+    newEvent: "New Event",
+    searchEvents: "Search Events",
+    today: "Today",
+    dayView: "Day View",
+    weekView: "Week View",
+    monthView: "Month View",
+    nextPeriod: "Next Period",
+    previousPeriod: "Previous Period",
   },
   zh: {
     // Keep existing Chinese translations
@@ -307,6 +318,17 @@ export const translations = {
     welcomeToAnalytics: "欢迎使用高级分析功能",
     analyticsDescription: "我们添加了强大的功能，帮助您更好地管理时间和日程",
     unnamedEvent: "未命名日程",
+    enableShortcuts: "开启快捷键",
+    availableShortcuts: "可用快捷键",
+    defaultView: "默认视图",
+    newEvent: "创建新事件",
+    searchEvents: "搜索事件",
+    today: "今天",
+    dayView: "日视图",
+    weekView: "周视图",
+    monthView: "月视图",
+    nextPeriod: "下个周期",
+    previousPeriod: "上个周期",
   },
 }
 


Reviews:
user: login: sourcery-ai[bot]
body: Hey @Dev-Huang1 - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a visual indicator in the UI to show whether keyboard shortcuts are enabled or disabled.
- The search functionality could be improved by debouncing the input to reduce the number of API calls.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Review instructions**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 2 issues found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-23T12:46:06Z

Review comments:
diff_hunk: @@ -67,6 +67,80 @@ export default function Calendar() {
   // 新增：快速创建事件的初始时间
   const [quickCreateStartTime, setQuickCreateStartTime] = useState<Date | null>(null)
 
+  // Add the new state variables for default view and keyboard shortcuts
+  const [defaultView, setDefaultView] = useLocalStorage<ViewType>("default-view", "week")
+  const [enableShortcuts, setEnableShortcuts] = useLocalStorage<boolean>("enable-shortcuts", true)
+
+  // Add a useEffect to set the initial view based on the default view setting
+  useEffect(() => {
+    // Only set the view on initial load
+    if (view !== defaultView) {
+      setView(defaultView as ViewType)
+    }
+  }, [])
+
+  // Add the keyboard shortcut handler
+  useEffect(() => {
+    if (!enableShortcuts) return
+
+    const handleKeyDown = (e: KeyboardEvent) => {
+      // Skip if user is typing in an input, textarea, or contentEditable element
+      if (
+        document.activeElement instanceof HTMLInputElement ||
+        document.activeElement instanceof HTMLTextAreaElement ||
+        document.activeElement?.getAttribute("contenteditable") === "true"
+      ) {
+        return
+      }
+
+      switch (e.key) {
+        case "n":
+        case "N":
+          e.preventDefault()
+          setEventDialogOpen(true)
+          break
+        case "/":
+          e.preventDefault()
+          // Focus the search input
+          const searchInput = document.querySelector('input[placeholder="' + t.searchEvents + '"]') as HTMLInputElement
+          if (searchInput) {
+            searchInput.focus()
+          }
+          break
+        case "t":
+        case "T":
+          e.preventDefault()
+          handleTodayClick()
+          break
+        case "1":
+          e.preventDefault()
+          setView("day")
+          break
+        case "2":
+          e.preventDefault()
+          setView("week")
+          break
+        case "3":
+          e.preventDefault()
+          setView("month")
+          break
+        case "ArrowRight":
+          e.preventDefault()
+          handleNext()
+          break
+        case "ArrowLeft":
+          e.preventDefault()
+          handlePrevious()
+          break
+      }
+    }
+
+    window.addEventListener("keydown", handleKeyDown)
+    return () => {
path: components/Calendar.tsx
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** Cleanup function for keyboard shortcuts is misconfigured.

In the cleanup function for the keydown effect, the event listener should be removed rather than added again. Please replace the call to window.addEventListener with window.removeEventListener to avoid attaching duplicate listeners.
created_at: 2025-03-23T12:46:06Z
author_association: NONE
diff_hunk: @@ -36,7 +37,7 @@ export default function Sidebar({
   language = "zh",
   selectedDate,
 }: SidebarProps) {
-  // 使用 Context 中的日历分类数据
+  // 使�� Context 中的日历分类数据
path: components/Sidebar.tsx
user: login: sourcery-ai[bot]
body: **nitpick:** Possible encoding issue in the sidebar comment.

The comment text appears to have an encoding error ("使��"). It would be clearer if the text is corrected to the intended description.
created_at: 2025-03-23T12:46:06Z
author_association: NONE
diff_hunk: @@ -67,6 +67,80 @@ export default function Calendar() {
   // 新增：快速创建事件的初始时间
   const [quickCreateStartTime, setQuickCreateStartTime] = useState<Date | null>(null)
 
+  // Add the new state variables for default view and keyboard shortcuts
+  const [defaultView, setDefaultView] = useLocalStorage<ViewType>("default-view", "week")
+  const [enableShortcuts, setEnableShortcuts] = useLocalStorage<boolean>("enable-shortcuts", true)
+
+  // Add a useEffect to set the initial view based on the default view setting
+  useEffect(() => {
path: components/Calendar.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the keyboard shortcut logic into a custom hook to improve readability and reduce nesting in the component.

Extracting the keyboard shortcut logic into its own custom hook will reduce the nesting and improve readability. For instance:

1. **Create a custom hook:**  
   Create a new file `useKeyboardShortcuts.ts`:

   ```tsx
   import { useEffect } from "react";

   interface KeyHandlers {
     [key: string]: () => void;
   }

   export function useKeyboardShortcuts(enabled: boolean, handlers: KeyHandlers) {
     useEffect(() => {
       if (!enabled) return;

       const handleKeyDown = (e: KeyboardEvent) => {
         // Skip if user is typing in an input, textarea, or contentEditable element
         if (
           document.activeElement instanceof HTMLInputElement ||
           document.activeElement instanceof HTMLTextAreaElement ||
           document.activeElement?.getAttribute("contenteditable") === "true"
         ) {
           return;
         }

         const key = e.key;
         const action = handlers[key] || handlers[key.toLowerCase()];
         if (action) {
           e.preventDefault();
           action();
         }
       };

       window.addEventListener("keydown", handleKeyDown);
       return () => {
         window.removeEventListener("keydown", handleKeyDown);
       };
     }, [enabled, handlers]);
   }
   ```

2. **Use the custom hook in your component:**  
   Replace the current `useEffect` with a concise hook call. For example:

   ```tsx
   import { useKeyboardShortcuts } from "./useKeyboardShortcuts";

   // Inside your component:
   useKeyboardShortcuts(enableShortcuts, {
     n: () => setEventDialogOpen(true),
     "/": () => {
       const searchInput = document.querySelector(`input[placeholder="${t.searchEvents}"]`) as HTMLInputElement;
       if (searchInput) searchInput.focus();
     },
     t: handleTodayClick,
     "1": () => setView("day"),
     "2": () => setView("week"),
     "3": () => setView("month"),
     ArrowRight: handleNext,
     ArrowLeft: handlePrevious,
   });
   ```

This refactoring keeps the functionality intact while simplifying your component logic and reducing nested conditionals.
created_at: 2025-03-23T12:46:06Z
author_association: NONE
diff_hunk: @@ -28,14 +37,47 @@ export default function Settings({
   setTimezone,
   notificationSound,
   setNotificationSound,
+  defaultView,
+  setDefaultView,
+  enableShortcuts,
+  setEnableShortcuts,
 }: SettingsProps) {
   const t = translations[language]
-  const timezones = Intl.supportedValuesOf("timeZone")
 
-  // 添加一个处理语言变化的函数
+  // Replace the timezones array with GMT formatted timezones
+  const getGMTTimezones = () => {
path: components/Settings.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the timezone formatting logic into a separate utility function and memoizing the result using useMemo to avoid recomputation on every render.

Consider extracting the timezone formatting logic into a separate utility and memoizing the result in the component. This will decouple the non-UI work from the UI and prevent re-computation on every render.

For example, create a new module `utils/timezoneUtils.ts`:

```ts
// utils/timezoneUtils.ts
export function getGMTTimezones(): { value: string; label: string }[] {
  const timezones = Intl.supportedValuesOf("timeZone");
  const now = new Date();
  return timezones
    .map((tz) => {
      try {
        const offsetMinutes =
          new Date(now.toLocaleString("en-US", { timeZone: tz })).getTimezoneOffset() * -1;
        const offsetHours = Math.abs(Math.floor(offsetMinutes / 60));
        const offsetMins = Math.abs(offsetMinutes % 60);
        const offsetSign = offsetMinutes >= 0 ? "+" : "-";
        const offsetString = `GMT${offsetSign}${offsetHours.toString().padStart(2, "0")}:${offsetMins
          .toString()
          .padStart(2, "0")}`;
        return {
          value: tz,
          label: `${tz} (${offsetString})`
        };
      } catch (e) {
        return {
          value: tz,
          label: tz
        };
      }
    })
    .sort((a, b) => a.label.localeCompare(b.label));
}
```

Then in your `Settings` component, import and memoize the computed timezones:

```tsx
import { useMemo } from "react";
import { getGMTTimezones } from "@/utils/timezoneUtils";

// ...

export default function Settings({ /* props */ }: SettingsProps) {
  // ...
  const gmtTimezones = useMemo(() => getGMTTimezones(), []);

  // component JSX
}
```

These steps preserve all functionality while reducing inline complexity in your component.
created_at: 2025-03-23T12:46:06Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-03-23T12:45:16Z
body: [vc]: #+xraP5Y1565pL1F4nt12en/4jm1+fOX8qbG2an8KJfA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJvbmUtY2FsZW5kYXIiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGVjaC1hcnQvb25lLWNhbGVuZGFyL0RpODYxb3h2UFY2Rm1zeXRnMkt3dUdteHhUZmMiLCJwcmV2aWV3VXJsIjoib25lLWNhbGVuZGFyLWdpdC1mb3JrLXRlY2hhcnQtc3R1ZGlvLW1haW4tdGVjaC1hcnQudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJvbmUtY2FsZW5kYXItZ2l0LWZvcmstdGVjaGFydC1zdHVkaW8tbWFpbi10ZWNoLWFydC52ZXJjZWwuYXBwIn0sInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **one-calendar** | ✅ Ready ([Inspect](https://vercel.com/tech-art/one-calendar/Di861oxvPV6Fmsytg2KwuGmxxTfc)) | [Visit Preview](https://one-calendar-git-fork-techart-studio-main-tech-art.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/one-calendar-git-fork-techart-studio-main-tech-art.vercel.app?via=pr-comment-feedback-link) | Mar 23, 2025 0:46am |


author_association: CONTRIBUTOR
user: login: sourcery-ai[bot]
created_at: 2025-03-23T12:45:21Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

```html
## Sourcery 评审指南

此 Pull Request 引入了日历导航和事件创建的键盘快捷键，添加了默认日历视图的设置，并更新了设置中的时区选项以显示 GMT 偏移量。它还包括一些小的 UI 增强。

#### 使用键盘快捷键 'n' 创建新事件的顺序图

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses 'n' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: setEventDialogOpen(true)
        Calendar->>Calendar: Open event dialog
    else
        Calendar-->>Window: Do nothing
    end
```

#### 使用键盘快捷键 '/' 聚焦搜索输入的顺序图

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses '/' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: Focus search input
    else
        Calendar-->>Window: Do nothing
    end
```

#### 使用键盘快捷键 '1' 切换到日视图的顺序图

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses '1' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: setView("day")
        Calendar->>Calendar: Update calendar to day view
    else
        Calendar-->>Window: Do nothing
    end
```

#### 使用键盘快捷键 '3' 切换到月视图的顺序图

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses '3' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: setView("month")
        Calendar->>Calendar: Update calendar to month view
    else
        Calendar-->>Window: Do nothing
    end
```

#### 更新后的 SettingsProps 类图

```mermaid
classDiagram
    class SettingsProps {
        language: Language
        setLanguage: (lang: Language) => void
        timezone: string
        setTimezone: (timezone: string) => void
        notificationSound: keyof typeof NOTIFICATION_SOUNDS
        setNotificationSound: (sound: keyof typeof NOTIFICATION_SOUNDS) => void
        defaultView: string
        setDefaultView: (view: string) => void
        enableShortcuts: boolean
        setEnableShortcuts: (enable: boolean) => void
    }
```

### 文件级别变更

| 变更 | 详情 | 文件 |
| ------ | ------- | ----- |
| 实现了日历导航和事件创建的键盘快捷键。 | <ul><li>添加了一个 `useEffect` hook 来监听 keydown 事件。</li><li>定义了诸如 'n' (新建事件), '/' (搜索), 't' (今天), '1' (日视图), '2' (周视图), '3' (月视图) 和箭头键 (导航) 等按键的操作。</li><li>当用户聚焦在 input, textarea, 或 contenteditable 元素上时，跳过快捷键执行。</li><li>添加了一个状态变量来切换键盘快捷键。</li><li>使用 local storage 持久化键盘快捷键的状态。</li><li>在设置中添加了一个开关组件来切换键盘快捷键。</li></ul> | `components/Calendar.tsx`<br/>`components/Settings.tsx`<br/>`lib/i18n.ts` |
| 添加了默认日历视图的设置。 | <ul><li>引入了一个 `defaultView` 状态变量，使用 local storage 来持久化用户偏好的视图。</li><li>基于 `defaultView` 设置，在 `useEffect` hook 中设置初始日历视图。</li><li>在设置中添加了一个选择组件来更改默认视图。</li></ul> | `components/Calendar.tsx`<br/>`components/Settings.tsx`<br/>`lib/i18n.ts` |
| 更新了设置中的时区选项以显示 GMT 偏移量。 | <ul><li>将 `timezones` 数组替换为 `gmtTimezones` 数组，其中包括 GMT 偏移量。</li><li>计算并格式化每个时区的 GMT 偏移量。</li><li>按标签对时区选项进行排序。</li></ul> | `components/Settings.tsx` |
| 通过细微调整增强了日历 UI。 | <ul><li>向侧边栏添加了日历图标和标题。</li><li>调整了 header 的 z-index 和背景颜色，以确保正确的图层和外观。</li><li>修改了创建事件按钮的样式。</li><li>修复了搜索结果的 z-index 问题。</li></ul> | `components/Calendar.tsx`<br/>`components/Sidebar.tsx`<br/>`components/WeekView.tsx`<br/>`components/DayView.tsx` |

---

<details>
<summary>提示和命令</summary>

#### 与 Sourcery 互动

- **触发新的审查：** 在 Pull Request 上评论 `@sourcery-ai review`。
- **继续讨论：** 直接回复 Sourcery 的审查评论。
- **从审查评论生成 GitHub issue：** 通过回复审查评论，要求 Sourcery 从该评论创建一个 issue。您也可以回复审查评论并附上 `@sourcery-ai issue` 来从中创建一个 issue。
- **生成 Pull Request 标题：** 在 Pull Request 标题中的任何位置写入 `@sourcery-ai` 以随时生成标题。您也可以在 Pull Request 上评论 `@sourcery-ai title` 以随时（重新）生成标题。
- **生成 Pull Request 摘要：** 在 Pull Request 正文中的任何位置写入 `@sourcery-ai summary` 以随时在您想要的位置生成 PR 摘要。您也可以在 Pull Request 上评论 `@sourcery-ai summary` 以随时（重新）生成摘要。
- **生成评审指南：** 在 Pull Request 上评论 `@sourcery-ai guide` 以随时（重新）生成评审指南。
- **解决所有 Sourcery 评论：** 在 Pull Request 上评论 `@sourcery-ai resolve` 以解决所有 Sourcery 评论。如果您已经处理了所有评论并且不想再看到它们，这将非常有用。
- **驳回所有 Sourcery 审查：** 在 Pull Request 上评论 `@sourcery-ai dismiss` 以驳回所有现有的 Sourcery 审查。如果您想从新的审查开始，这将特别有用 - 不要忘记评论 `@sourcery-ai review` 以触发新的审查！
- **为 issue 生成行动计划：** 在 issue 上评论 `@sourcery-ai plan` 以为其生成行动计划。

#### 自定义您的体验

访问您的 [仪表板](https://app.sourcery.ai) 以：
- 启用或禁用审查功能，例如 Sourcery 生成的 Pull Request 摘要、评审指南等。
- 更改审查语言。
- 添加、删除或编辑自定义审查说明。
- 调整其他审查设置。

#### 获取帮助

- [联系我们的支持团队](mailto:support@sourcery.ai) 提出问题或反馈。
- 访问我们的 [文档](https://docs.sourcery.ai) 获取详细指南和信息。
- 通过在 [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) 或 [GitHub](https://github.com/sourcery-ai) 上关注我们，与 Sourcery 团队保持联系。

</details>
```

<details>
<summary>Original review guide in English</summary>

## Reviewer's Guide by Sourcery

This pull request introduces keyboard shortcuts for calendar navigation and event creation, adds a setting for the default calendar view, and updates the timezone options in settings to display GMT offsets. It also includes minor UI enhancements.

#### Sequence diagram for keyboard shortcut 'n' to create a new event

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses 'n' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: setEventDialogOpen(true)
        Calendar->>Calendar: Open event dialog
    else
        Calendar-->>Window: Do nothing
    end
```

#### Sequence diagram for keyboard shortcut '/' to focus the search input

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses '/' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: Focus search input
    else
        Calendar-->>Window: Do nothing
    end
```

#### Sequence diagram for keyboard shortcut '1' to switch to day view

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses '1' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: setView("day")
        Calendar->>Calendar: Update calendar to day view
    else
        Calendar-->>Window: Do nothing
    end
```

#### Sequence diagram for keyboard shortcut '3' to switch to month view

```mermaid
sequenceDiagram
    participant User
    participant Window
    participant Calendar

    User->>Window: Presses '3' key
    Window->>Calendar: handleKeyDown(e)
    alt enableShortcuts is true and not typing in input
        Calendar->>Calendar: e.preventDefault()
        Calendar->>Calendar: setView("month")
        Calendar->>Calendar: Update calendar to month view
    else
        Calendar-->>Window: Do nothing
    end
```

#### Updated class diagram for SettingsProps

```mermaid
classDiagram
    class SettingsProps {
        language: Language
        setLanguage: (lang: Language) => void
        timezone: string
        setTimezone: (timezone: string) => void
        notificationSound: keyof typeof NOTIFICATION_SOUNDS
        setNotificationSound: (sound: keyof typeof NOTIFICATION_SOUNDS) => void
        defaultView: string
        setDefaultView: (view: string) => void
        enableShortcuts: boolean
        setEnableShortcuts: (enable: boolean) => void
    }
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implements keyboard shortcuts for calendar navigation and event creation. | <ul><li>Adds a `useEffect` hook to listen for keydown events.</li><li>Defines actions for keys such as 'n' (new event), '/' (search), 't' (today), '1' (day view), '2' (week view), '3' (month view), and arrow keys (navigation).</li><li>Skips shortcut execution when the user is focused on an input, textarea, or contenteditable element.</li><li>Adds a state variable to toggle keyboard shortcuts.</li><li>Persists the state of keyboard shortcuts using local storage.</li><li>Adds a switch component to toggle keyboard shortcuts in the settings.</li></ul> | `components/Calendar.tsx`<br/>`components/Settings.tsx`<br/>`lib/i18n.ts` |
| Adds a setting for the default calendar view. | <ul><li>Introduces a `defaultView` state variable using local storage to persist the user's preferred view.</li><li>Sets the initial calendar view based on the `defaultView` setting in a `useEffect` hook.</li><li>Adds a select component to change the default view in the settings.</li></ul> | `components/Calendar.tsx`<br/>`components/Settings.tsx`<br/>`lib/i18n.ts` |
| Updates the timezone options in settings to display GMT offsets. | <ul><li>Replaces the `timezones` array with a `gmtTimezones` array, which includes GMT offsets.</li><li>Calculates and formats the GMT offset for each timezone.</li><li>Sorts the timezone options by their labels.</li></ul> | `components/Settings.tsx` |
| Enhances the calendar UI with minor adjustments. | <ul><li>Adds a calendar icon and title to the sidebar.</li><li>Adjusts the z-index and background color of the header to ensure proper layering and appearance.</li><li>Modifies the style of the create event button.</li><li>Fixes z-index issues with search results.</li></ul> | `components/Calendar.tsx`<br/>`components/Sidebar.tsx`<br/>`components/WeekView.tsx`<br/>`components/DayView.tsx` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE