One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links ๐Ÿ“… calendar.xyehr.cn
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #246 from EvanTechDev/feature/add-ellipsis-to-month-view-schedule-titles

Add category reorder actions and dropdown menu in sidebar; implement moveCategory in context

authored by

Evan Huang and committed by
GitHub
e481d907 ddf9722b

+96 -22
+65 -19
components/app/sidebar/sidebar.tsx
··· 22 22 import { Button } from '@/components/ui/button' 23 23 import { Input } from '@/components/ui/input' 24 24 import { Label } from '@/components/ui/label' 25 - import { Plus, X, Edit2 } from 'lucide-react' 25 + import { 26 + ArrowDown, 27 + ArrowUp, 28 + Edit2, 29 + MoreHorizontal, 30 + Plus, 31 + X, 32 + } from 'lucide-react' 26 33 import { useEffect, useState, type CSSProperties } from 'react' 27 34 import { cn } from '@/lib/utils' 28 35 import { toast } from 'sonner' 29 36 import Image from 'next/image' 37 + import { 38 + DropdownMenu, 39 + DropdownMenuContent, 40 + DropdownMenuItem, 41 + DropdownMenuTrigger, 42 + } from '@/components/ui/dropdown-menu' 30 43 31 44 interface SidebarProps { 32 45 onCreateEvent: () => void ··· 81 94 addCategory: addCategoryToContext, 82 95 removeCategory: removeCategoryFromContext, 83 96 updateCategory: updateCategoryInContext, 97 + moveCategory: moveCategoryInContext, 84 98 } = useCalendar() 85 99 86 100 const [newCategoryName, setNewCategoryName] = useState('') ··· 125 139 } 126 140 const deleteCategoryEventsLabel = 127 141 t.deleteCategoryEvents || 'ๅŒๆ—ถๅˆ ้™คๆญคๅˆ†็ฑปไธ‹็š„ๆ‰€ๆœ‰ๆ—ฅ็จ‹' 142 + const moveUpText = t['moveUp'] || 'Move up' 143 + const moveDownText = t['moveDown'] || 'Move down' 128 144 129 145 useEffect(() => { 130 146 if (selectedDate) { ··· 182 198 toast(t.categoryUpdated || 'ๅˆ†็ฑปๅทฒๆ›ดๆ–ฐ') 183 199 } 184 200 201 + const handleMoveCategory = (id: string, direction: 'up' | 'down') => { 202 + moveCategoryInContext(id, direction) 203 + toast(direction === 'up' ? moveUpText : moveDownText, { 204 + description: 205 + direction === 'up' 206 + ? t['categoryMovedUp'] || 'Category moved up' 207 + : t['categoryMovedDown'] || 'Category moved down', 208 + }) 209 + } 210 + 185 211 const confirmDelete = () => { 186 212 if (categoryToDelete) { 187 213 if (deleteCategoryEvents) { ··· 257 283 <span className="text-sm font-medium">{t.myCalendars}</span> 258 284 </div> 259 285 {calendars.map((calendar) => ( 260 - <div 261 - key={calendar.id} 262 - className="flex items-center justify-between" 263 - > 286 + <div key={calendar.id} className="flex items-center justify-between"> 264 287 <div className="flex items-center space-x-2"> 265 288 <Checkbox 266 289 checked={selectedCategoryFilters.includes(calendar.id)} ··· 276 299 <span className="text-sm">{calendar.name}</span> 277 300 </div> 278 301 <div className="flex items-center"> 279 - <Button 280 - variant="ghost" 281 - size="sm" 282 - onClick={() => handleEditClick(calendar.id)} 283 - > 284 - <Edit2 className="h-4 w-4" /> 285 - </Button> 286 - <Button 287 - variant="ghost" 288 - size="sm" 289 - onClick={() => handleDeleteClick(calendar.id)} 290 - > 291 - <X className="h-4 w-4" /> 292 - </Button> 302 + <DropdownMenu> 303 + <DropdownMenuTrigger asChild> 304 + <Button variant="ghost" size="icon" className="h-7 w-7"> 305 + <MoreHorizontal className="h-4 w-4" /> 306 + </Button> 307 + </DropdownMenuTrigger> 308 + <DropdownMenuContent align="end"> 309 + <DropdownMenuItem onClick={() => handleEditClick(calendar.id)}> 310 + <Edit2 className="mr-2 h-4 w-4" /> 311 + {t.edit} 312 + </DropdownMenuItem> 313 + <DropdownMenuItem 314 + onClick={() => handleDeleteClick(calendar.id)} 315 + className="text-destructive focus:text-destructive" 316 + > 317 + <X className="mr-2 h-4 w-4" /> 318 + {t.delete} 319 + </DropdownMenuItem> 320 + <DropdownMenuItem 321 + onClick={() => handleMoveCategory(calendar.id, 'up')} 322 + disabled={calendars.findIndex((item) => item.id === calendar.id) === 0} 323 + > 324 + <ArrowUp className="mr-2 h-4 w-4" /> 325 + {moveUpText} 326 + </DropdownMenuItem> 327 + <DropdownMenuItem 328 + onClick={() => handleMoveCategory(calendar.id, 'down')} 329 + disabled={ 330 + calendars.findIndex((item) => item.id === calendar.id) === 331 + calendars.length - 1 332 + } 333 + > 334 + <ArrowDown className="mr-2 h-4 w-4" /> 335 + {moveDownText} 336 + </DropdownMenuItem> 337 + </DropdownMenuContent> 338 + </DropdownMenu> 293 339 </div> 294 340 </div> 295 341 ))}
+1 -1
components/app/views/month-view.tsx
··· 143 143 }} 144 144 /> 145 145 <div 146 - className="pl-1.5" 146 + className="pl-1.5 truncate" 147 147 style={{ color: getDarkerColorClass(event.color) }} 148 148 > 149 149 {event.title}
+21
components/providers/calendar-context.tsx
··· 39 39 addCategory: (category: CalendarCategory) => void 40 40 removeCategory: (id: string) => void 41 41 updateCategory: (id: string, category: Partial<CalendarCategory>) => void 42 + moveCategory: (id: string, direction: 'up' | 'down') => void 42 43 addEvent: (newEvent: CalendarEvent) => void 43 44 } 44 45 ··· 86 87 ) 87 88 } 88 89 90 + const moveCategory = (id: string, direction: 'up' | 'down') => { 91 + setCalendars((prevCalendars) => { 92 + const currentIndex = prevCalendars.findIndex((cal) => cal.id === id) 93 + if (currentIndex === -1) return prevCalendars 94 + 95 + const targetIndex = 96 + direction === 'up' ? currentIndex - 1 : currentIndex + 1 97 + 98 + if (targetIndex < 0 || targetIndex >= prevCalendars.length) { 99 + return prevCalendars 100 + } 101 + 102 + const nextCalendars = [...prevCalendars] 103 + const [movedCalendar] = nextCalendars.splice(currentIndex, 1) 104 + nextCalendars.splice(targetIndex, 0, movedCalendar) 105 + return nextCalendars 106 + }) 107 + } 108 + 89 109 const addEvent = (newEvent: CalendarEvent) => { 90 110 setEvents((prevEvents) => { 91 111 const eventExists = prevEvents.some((event) => event.id === newEvent.id) ··· 110 130 addCategory, 111 131 removeCategory, 112 132 updateCategory, 133 + moveCategory, 113 134 addEvent, 114 135 }} 115 136 >
+4 -1
lib/i18n.ts
··· 97 97 ) 98 98 } 99 99 100 - export const isZhLanguage = (language: Language) => language.startsWith('zh') 100 + const zhLanguages: Language[] = ['zh-CN', 'zh-HK', 'zh-TW'] 101 + 102 + export const isZhLanguage = (language: Language) => 103 + zhLanguages.includes(language) 101 104 102 105 export const getStoredLanguage = async (): Promise<Language> => { 103 106 const storedLanguage = await readEncryptedLocalStorage<string | null>(
+4
locales/en.json
··· 332 332 "categoryDeletedDescription": "Category has been deleted successfully", 333 333 "noMatchingEvents": "No matching events found", 334 334 "edit": "Edit", 335 + "moveUp": "Move up", 336 + "moveDown": "Move down", 337 + "categoryMovedUp": "Category moved up", 338 + "categoryMovedDown": "Category moved down", 335 339 "dragToNewPosition": "Drag to new position", 336 340 "categoryAdded": "Category added", 337 341 "categoryAddedDesc": "Successfully added",
+1 -1
package.json
··· 1 1 { 2 2 "name": "one-calendar", 3 - "version": "2.2.13", 3 + "version": "2.2.14", 4 4 "private": true, 5 5 "packageManager": "bun@1.3.8", 6 6 "scripts": {