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 #187 from EvanTechDev/feature/run-eslint-and-clean-up-code

Codebase: style normalization, UI primitives cleanup and small bug fixes

authored by

Evan Huang and committed by
GitHub
e98a4a80 b5ee5fb5

+3320 -1904
+29 -25
components/app/analytics/analytics-view.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import TimeAnalyticsComponent from "@/components/app/analytics/time-analytics" 4 - import EventsCalendar from "@/components/app/analytics/events-calendar" 5 - import { useCalendar } from "@/components/providers/calendar-context" 6 - import type { CalendarEvent } from "@/components/app/calendar" 7 - import { translations, useLanguage } from "@/lib/i18n" 8 - import { useState, useEffect } from "react" 3 + import TimeAnalyticsComponent from "@/components/app/analytics/time-analytics"; 4 + import EventsCalendar from "@/components/app/analytics/events-calendar"; 5 + import { useCalendar } from "@/components/providers/calendar-context"; 6 + import type { CalendarEvent } from "@/components/app/calendar"; 7 + import { translations, useLanguage } from "@/lib/i18n"; 8 + import { useState, useEffect } from "react"; 9 9 10 10 interface AnalyticsViewProps { 11 - events: CalendarEvent[] 12 - onCreateEvent: (startDate: Date, endDate: Date) => void 11 + events: CalendarEvent[]; 12 + onCreateEvent: (startDate: Date, endDate: Date) => void; 13 13 } 14 14 15 15 export default function AnalyticsView({ events }: AnalyticsViewProps) { 16 - const { calendars } = useCalendar() 17 - const [language] = useLanguage() 18 - const t = translations[language] 19 - const [forceUpdate, setForceUpdate] = useState(0) 16 + const { calendars } = useCalendar(); 17 + const [language] = useLanguage(); 18 + const t = translations[language]; 19 + const [forceUpdate, setForceUpdate] = useState(0); 20 20 21 21 useEffect(() => { 22 22 const handleStorageChange = (e: StorageEvent) => { 23 23 if (e.key === "preferred-language") { 24 - setForceUpdate((prev) => prev + 1) 24 + setForceUpdate((prev) => prev + 1); 25 25 } 26 - } 26 + }; 27 27 28 28 const handleLanguageChange = () => { 29 - setForceUpdate((prev) => prev + 1) 30 - } 29 + setForceUpdate((prev) => prev + 1); 30 + }; 31 31 32 - window.addEventListener("storage", handleStorageChange) 33 - window.addEventListener("languagechange", handleLanguageChange) 32 + window.addEventListener("storage", handleStorageChange); 33 + window.addEventListener("languagechange", handleLanguageChange); 34 34 35 35 return () => { 36 - window.removeEventListener("storage", handleStorageChange) 37 - window.removeEventListener("languagechange", handleLanguageChange) 38 - } 39 - }, []) 36 + window.removeEventListener("storage", handleStorageChange); 37 + window.removeEventListener("languagechange", handleLanguageChange); 38 + }; 39 + }, []); 40 40 41 41 return ( 42 42 <div className="space-y-8 p-4"> 43 43 <div className="flex justify-between items-center"> 44 44 <h1 className="text-2xl font-bold">{t.analytics}</h1> 45 45 </div> 46 - <TimeAnalyticsComponent events={events} calendars={calendars} key={`time-analytics-${language}-${forceUpdate}`} /> 46 + <TimeAnalyticsComponent 47 + events={events} 48 + calendars={calendars} 49 + key={`time-analytics-${language}-${forceUpdate}`} 50 + /> 47 51 <EventsCalendar /> 48 52 </div> 49 - ) 53 + ); 50 54 }
+31 -28
components/app/analytics/build-info-card.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 4 - import { translations, type Language } from "@/lib/i18n" 5 - import { useEffect, useMemo, useState } from "react" 3 + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 4 + import { translations, type Language } from "@/lib/i18n"; 5 + import { useEffect, useMemo, useState } from "react"; 6 6 7 - const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION ?? "unknown" 8 - const COMMIT_HASH = process.env.NEXT_PUBLIC_GIT_COMMIT ?? "unknown" 9 - const DEPLOYED_AT = process.env.NEXT_PUBLIC_BUILD_TIME ?? "" 7 + const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION ?? "unknown"; 8 + const COMMIT_HASH = process.env.NEXT_PUBLIC_GIT_COMMIT ?? "unknown"; 9 + const DEPLOYED_AT = process.env.NEXT_PUBLIC_BUILD_TIME ?? ""; 10 10 11 11 const formatTimeAgo = (language: Language, deployedAt: string) => { 12 - const t = translations[language] 13 - const deployedDate = new Date(deployedAt) 12 + const t = translations[language]; 13 + const deployedDate = new Date(deployedAt); 14 14 if (Number.isNaN(deployedDate.getTime())) { 15 - return t.buildInfoUnknown 15 + return t.buildInfoUnknown; 16 16 } 17 17 18 - const diffMs = Date.now() - deployedDate.getTime() 19 - const diffMinutes = Math.max(1, Math.floor(diffMs / 60000)) 18 + const diffMs = Date.now() - deployedDate.getTime(); 19 + const diffMinutes = Math.max(1, Math.floor(diffMs / 60000)); 20 20 21 21 if (diffMinutes < 60) { 22 - return `${diffMinutes} ${t.buildInfoMinutes}` 22 + return `${diffMinutes} ${t.buildInfoMinutes}`; 23 23 } 24 24 25 - const diffHours = Math.floor(diffMinutes / 60) 25 + const diffHours = Math.floor(diffMinutes / 60); 26 26 if (diffHours < 24) { 27 - return `${diffHours} ${t.buildInfoHours}` 27 + return `${diffHours} ${t.buildInfoHours}`; 28 28 } 29 29 30 - const diffDays = Math.floor(diffHours / 24) 31 - return `${diffDays} ${t.buildInfoDays}` 32 - } 30 + const diffDays = Math.floor(diffHours / 24); 31 + return `${diffDays} ${t.buildInfoDays}`; 32 + }; 33 33 34 34 interface BuildInfoCardProps { 35 - language: Language 35 + language: Language; 36 36 } 37 37 38 38 export default function BuildInfoCard({ language }: BuildInfoCardProps) { 39 - const [tick, setTick] = useState(0) 39 + const [tick, setTick] = useState(0); 40 40 41 41 useEffect(() => { 42 42 const timer = window.setInterval(() => { 43 - setTick((value) => value + 1) 44 - }, 60000) 43 + setTick((value) => value + 1); 44 + }, 60000); 45 45 46 46 return () => { 47 - window.clearInterval(timer) 48 - } 49 - }, []) 47 + window.clearInterval(timer); 48 + }; 49 + }, []); 50 50 51 - const t = translations[language] 52 - const deployedAgo = useMemo(() => formatTimeAgo(language, DEPLOYED_AT), [language, tick]) 51 + const t = translations[language]; 52 + const deployedAgo = useMemo( 53 + () => formatTimeAgo(language, DEPLOYED_AT), 54 + [language, tick], 55 + ); 53 56 54 57 return ( 55 58 <Card> ··· 71 74 </div> 72 75 </CardContent> 73 76 </Card> 74 - ) 77 + ); 75 78 }
+17 -8
components/app/analytics/import-export.tsx
··· 71 71 const [language] = useLanguage(); 72 72 const t = translations[language]; 73 73 const { calendars } = useCalendar(); 74 - const [importCalendarId, setImportCalendarId] = useState<string>("__uncategorized__"); 74 + const [importCalendarId, setImportCalendarId] = 75 + useState<string>("__uncategorized__"); 75 76 76 77 const [forceUpdate, setForceUpdate] = useState(0); 77 78 ··· 174 175 } 175 176 }; 176 177 177 - 178 178 const mapCalendarColorToEventColor = (calendarColor?: string) => { 179 179 const mapping: Record<string, string> = { 180 180 "bg-blue-500": "bg-[#E6F6FD]", ··· 189 189 }; 190 190 191 191 const applyImportCategory = (eventsToImport: CalendarEvent[]) => { 192 - const targetCategory = calendars.find((calendar) => calendar.id === importCalendarId); 193 - const categoryId = importCalendarId === "__uncategorized__" ? "" : importCalendarId; 192 + const targetCategory = calendars.find( 193 + (calendar) => calendar.id === importCalendarId, 194 + ); 195 + const categoryId = 196 + importCalendarId === "__uncategorized__" ? "" : importCalendarId; 194 197 const color = mapCalendarColorToEventColor(targetCategory?.color); 195 198 196 199 return eventsToImport.map((event) => ({ ··· 808 811 </p> 809 812 </div> 810 813 811 - 812 814 <div className="space-y-2"> 813 - <Label htmlFor="import-calendar-category">{t.importToCalendarCategory}</Label> 814 - <Select value={importCalendarId} onValueChange={setImportCalendarId}> 815 + <Label htmlFor="import-calendar-category"> 816 + {t.importToCalendarCategory} 817 + </Label> 818 + <Select 819 + value={importCalendarId} 820 + onValueChange={setImportCalendarId} 821 + > 815 822 <SelectTrigger id="import-calendar-category"> 816 823 <SelectValue /> 817 824 </SelectTrigger> 818 825 <SelectContent> 819 - <SelectItem value="__uncategorized__">{t.uncategorized}</SelectItem> 826 + <SelectItem value="__uncategorized__"> 827 + {t.uncategorized} 828 + </SelectItem> 820 829 {calendars.map((calendar) => ( 821 830 <SelectItem key={calendar.id} value={calendar.id}> 822 831 {calendar.name}
+129 -92
components/app/analytics/share-management.tsx
··· 1 - import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from "@/components/ui/alert-dialog"; 2 - import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; 1 + import { 2 + AlertDialog, 3 + AlertDialogContent, 4 + AlertDialogHeader, 5 + AlertDialogTitle, 6 + AlertDialogDescription, 7 + AlertDialogFooter, 8 + AlertDialogCancel, 9 + AlertDialogAction, 10 + } from "@/components/ui/alert-dialog"; 11 + import { 12 + Card, 13 + CardHeader, 14 + CardTitle, 15 + CardDescription, 16 + CardContent, 17 + } from "@/components/ui/card"; 3 18 import { Copy, ExternalLink, Lock, Trash2 } from "lucide-react"; 4 - import { translations, useLanguage } from "@/lib/i18n" 19 + import { translations, useLanguage } from "@/lib/i18n"; 5 20 import { Button } from "@/components/ui/button"; 6 - import { Input } from "@/components/ui/input" 21 + import { Input } from "@/components/ui/input"; 7 22 import { useEffect, useState } from "react"; 8 23 import { format } from "date-fns"; 9 24 import { toast } from "sonner"; 10 - import { fetchJson } from "@/lib/fetch-json" 25 + import { fetchJson } from "@/lib/fetch-json"; 11 26 12 27 interface SharedEvent { 13 28 id: string; ··· 21 36 22 37 export default function ShareManagement() { 23 38 const [language] = useLanguage(); 24 - const t = translations[language] 39 + const t = translations[language]; 25 40 const [sharedEvents, setSharedEvents] = useState<SharedEvent[]>([]); 26 41 const [selectedShare, setSelectedShare] = useState<SharedEvent | null>(null); 27 42 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); 28 43 const [isDeleting, setIsDeleting] = useState(false); 29 44 const [loadingDecrypt, setLoadingDecrypt] = useState(false); 30 - const [passwordDialogOpen, setPasswordDialogOpen] = useState(false) 31 - const [passwordInput, setPasswordInput] = useState("") 32 - const [decryptingShare, setDecryptingShare] = useState<SharedEvent | null>(null) 33 - const [isDecrypting, setIsDecrypting] = useState(false) 34 - 45 + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); 46 + const [passwordInput, setPasswordInput] = useState(""); 47 + const [decryptingShare, setDecryptingShare] = useState<SharedEvent | null>( 48 + null, 49 + ); 50 + const [isDecrypting, setIsDecrypting] = useState(false); 35 51 36 52 useEffect(() => { 37 53 async function fetchSharedEvents() { 38 54 try { 39 - const data = await fetchJson<{ shares?: SharedEvent[] }>("/api/share/list") 40 - setSharedEvents(data.shares || []) 55 + const data = await fetchJson<{ shares?: SharedEvent[] }>( 56 + "/api/share/list", 57 + ); 58 + setSharedEvents(data.shares || []); 41 59 } catch (error) { 42 60 console.error("Error fetching shared events:", error); 43 61 toast.error(t.shareManagementLoadFailed, { ··· 87 105 }; 88 106 89 107 const handleDecrypt = async () => { 90 - if (!decryptingShare || !passwordInput) return 108 + if (!decryptingShare || !passwordInput) return; 91 109 92 - try { 93 - setIsDecrypting(true) 110 + try { 111 + setIsDecrypting(true); 94 112 95 - const data = await fetchJson<{ success: boolean; data: string }>( 96 - `/api/share?id=${decryptingShare.id}&password=${encodeURIComponent(passwordInput)}` 97 - ) 113 + const data = await fetchJson<{ success: boolean; data: string }>( 114 + `/api/share?id=${decryptingShare.id}&password=${encodeURIComponent(passwordInput)}`, 115 + ); 98 116 99 - if (!data.success) { 100 - toast.error(t.invalidPassword) 101 - return 102 - } 117 + if (!data.success) { 118 + toast.error(t.invalidPassword); 119 + return; 120 + } 103 121 104 - const parsed = JSON.parse(data.data) 122 + const parsed = JSON.parse(data.data); 105 123 106 - setSharedEvents((prev) => 107 - prev.map((s) => 108 - s.id === decryptingShare.id 109 - ? { ...s, eventId: parsed.id, eventTitle: parsed.title, isProtected: false } 110 - : s 111 - ) 112 - ) 124 + setSharedEvents((prev) => 125 + prev.map((s) => 126 + s.id === decryptingShare.id 127 + ? { 128 + ...s, 129 + eventId: parsed.id, 130 + eventTitle: parsed.title, 131 + isProtected: false, 132 + } 133 + : s, 134 + ), 135 + ); 113 136 114 - toast.success(t.decrypted) 115 - setPasswordDialogOpen(false) 116 - setDecryptingShare(null) 117 - setPasswordInput("") 118 - } catch (error) { 119 - toast.error(t.decryptFailed) 120 - } finally { 121 - setIsDecrypting(false) 122 - } 123 - } 124 - 137 + toast.success(t.decrypted); 138 + setPasswordDialogOpen(false); 139 + setDecryptingShare(null); 140 + setPasswordInput(""); 141 + } catch (error) { 142 + toast.error(t.decryptFailed); 143 + } finally { 144 + setIsDecrypting(false); 145 + } 146 + }; 125 147 126 148 return ( 127 149 <Card className="w-full"> ··· 129 151 <div className="flex justify-between items-center"> 130 152 <div> 131 153 <CardTitle>{t.shareManagementTitle}</CardTitle> 132 - <CardDescription> 133 - {t.shareManagementDescription} 134 - </CardDescription> 154 + <CardDescription>{t.shareManagementDescription}</CardDescription> 135 155 </div> 136 156 </div> 137 157 </CardHeader> ··· 155 175 </p> 156 176 </div> 157 177 <div className="flex space-x-2"> 158 - <Button variant="outline" size="icon" onClick={() => copyShareLink(share.shareLink)}> 178 + <Button 179 + variant="outline" 180 + size="icon" 181 + onClick={() => copyShareLink(share.shareLink)} 182 + > 159 183 <Copy className="h-4 w-4" /> 160 184 </Button> 161 - <Button variant="outline" size="icon" onClick={() => openShareLink(share.shareLink)}> 185 + <Button 186 + variant="outline" 187 + size="icon" 188 + onClick={() => openShareLink(share.shareLink)} 189 + > 162 190 <ExternalLink className="h-4 w-4" /> 163 191 </Button> 164 192 {share.isProtected && ( ··· 166 194 variant="outline" 167 195 size="icon" 168 196 onClick={() => { 169 - setDecryptingShare(share) 170 - setPasswordInput("") 171 - setPasswordDialogOpen(true) 172 - }} 197 + setDecryptingShare(share); 198 + setPasswordInput(""); 199 + setPasswordDialogOpen(true); 200 + }} 173 201 disabled={loadingDecrypt} 174 202 > 175 203 <Lock className="h-4 w-4" /> ··· 203 231 </AlertDialogHeader> 204 232 <AlertDialogFooter> 205 233 <AlertDialogCancel>{t.cancel}</AlertDialogCancel> 206 - <AlertDialogAction onClick={deleteShare} disabled={isDeleting} variant="destructive"> 234 + <AlertDialogAction 235 + onClick={deleteShare} 236 + disabled={isDeleting} 237 + variant="destructive" 238 + > 207 239 {isDeleting ? ( 208 240 <span className="flex items-center"> 209 241 <svg ··· 212 244 fill="none" 213 245 viewBox="0 0 24 24" 214 246 > 215 - <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> 247 + <circle 248 + className="opacity-25" 249 + cx="12" 250 + cy="12" 251 + r="10" 252 + stroke="currentColor" 253 + strokeWidth="4" 254 + ></circle> 216 255 <path 217 256 className="opacity-75" 218 257 fill="currentColor" ··· 229 268 </AlertDialogContent> 230 269 </AlertDialog> 231 270 232 - <AlertDialog open={passwordDialogOpen} onOpenChange={setPasswordDialogOpen}> 233 - <AlertDialogContent> 234 - <AlertDialogHeader> 235 - <AlertDialogTitle> 236 - {t.enterSharePassword} 237 - </AlertDialogTitle> 238 - <AlertDialogDescription> 239 - {t.sharePasswordDescription} 240 - </AlertDialogDescription> 241 - </AlertDialogHeader> 242 - 243 - <div className="py-2"> 244 - <Input 245 - type="password" 246 - value={passwordInput} 247 - onChange={(e) => setPasswordInput(e.target.value)} 248 - placeholder={t.enterPassword} 249 - onKeyDown={(e) => { 250 - if (e.key === "Enter") handleDecrypt() 251 - }} 252 - /> 253 - </div> 271 + <AlertDialog 272 + open={passwordDialogOpen} 273 + onOpenChange={setPasswordDialogOpen} 274 + > 275 + <AlertDialogContent> 276 + <AlertDialogHeader> 277 + <AlertDialogTitle>{t.enterSharePassword}</AlertDialogTitle> 278 + <AlertDialogDescription> 279 + {t.sharePasswordDescription} 280 + </AlertDialogDescription> 281 + </AlertDialogHeader> 254 282 255 - <AlertDialogFooter> 256 - <AlertDialogCancel 257 - onClick={() => { 258 - setDecryptingShare(null) 259 - setPasswordInput("") 260 - }} 261 - > 262 - {t.cancel} 263 - </AlertDialogCancel> 283 + <div className="py-2"> 284 + <Input 285 + type="password" 286 + value={passwordInput} 287 + onChange={(e) => setPasswordInput(e.target.value)} 288 + placeholder={t.enterPassword} 289 + onKeyDown={(e) => { 290 + if (e.key === "Enter") handleDecrypt(); 291 + }} 292 + /> 293 + </div> 264 294 265 - <AlertDialogAction onClick={handleDecrypt} disabled={isDecrypting}> 266 - {isDecrypting 267 - ? t.decrypting 268 - : t.decrypt} 269 - </AlertDialogAction> 270 - </AlertDialogFooter> 271 - </AlertDialogContent> 272 - </AlertDialog> 295 + <AlertDialogFooter> 296 + <AlertDialogCancel 297 + onClick={() => { 298 + setDecryptingShare(null); 299 + setPasswordInput(""); 300 + }} 301 + > 302 + {t.cancel} 303 + </AlertDialogCancel> 273 304 305 + <AlertDialogAction onClick={handleDecrypt} disabled={isDecrypting}> 306 + {isDecrypting ? t.decrypting : t.decrypt} 307 + </AlertDialogAction> 308 + </AlertDialogFooter> 309 + </AlertDialogContent> 310 + </AlertDialog> 274 311 </Card> 275 312 ); 276 313 }
+22 -15
components/app/auth-waiting-loading.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { translations, useLanguage } from "@/lib/i18n" 4 - import { useEffect, useState } from "react" 5 - import Image from "next/image" 3 + import { translations, useLanguage } from "@/lib/i18n"; 4 + import { useEffect, useState } from "react"; 5 + import Image from "next/image"; 6 6 7 7 export default function AuthWaitingLoading() { 8 - const [language] = useLanguage() 9 - const t = translations[language] 10 - const [dotCount, setDotCount] = useState(1) 8 + const [language] = useLanguage(); 9 + const t = translations[language]; 10 + const [dotCount, setDotCount] = useState(1); 11 11 12 12 useEffect(() => { 13 13 const timer = window.setInterval(() => { 14 - setDotCount((prev) => (prev >= 3 ? 1 : prev + 1)) 15 - }, 450) 14 + setDotCount((prev) => (prev >= 3 ? 1 : prev + 1)); 15 + }, 450); 16 16 17 17 return () => { 18 - window.clearInterval(timer) 19 - } 20 - }, []) 18 + window.clearInterval(timer); 19 + }; 20 + }, []); 21 21 22 22 return ( 23 23 <div className="flex min-h-screen items-center justify-center bg-white px-6 dark:bg-black"> 24 24 <div className="flex flex-col items-center gap-5 text-center"> 25 - <Image src="/icon.svg" alt="One Calendar" width={128} height={128} priority /> 25 + <Image 26 + src="/icon.svg" 27 + alt="One Calendar" 28 + width={128} 29 + height={128} 30 + priority 31 + /> 26 32 <p className="text-sm text-slate-700 dark:text-slate-300"> 27 - {t.loadingCalendar}{".".repeat(dotCount)} 33 + {t.loadingCalendar} 34 + {".".repeat(dotCount)} 28 35 </p> 29 36 </div> 30 37 </div> 31 - ) 38 + ); 32 39 }
+35 -18
components/app/calendar.tsx
··· 180 180 "time-format", 181 181 "24h", 182 182 ); 183 - const [toastPosition, setToastPosition] = useLocalStorage<"bottom-left" | "bottom-center" | "bottom-right">( 184 - "toast-position", 185 - "bottom-right", 186 - ); 183 + const [toastPosition, setToastPosition] = useLocalStorage< 184 + "bottom-left" | "bottom-center" | "bottom-right" 185 + >("toast-position", "bottom-right"); 187 186 188 187 useEffect(() => { 189 188 if (view !== defaultView) { ··· 445 444 setDeleteConfirmOpen(true); 446 445 }; 447 446 448 - 449 - 450 447 const cleanupSharesForEvent = async (eventId: string) => { 451 - const storedShares = await readEncryptedLocalStorage<any[]>("shared-events", []); 452 - const relatedShares = storedShares.filter((share: any) => share?.eventId === eventId); 448 + const storedShares = await readEncryptedLocalStorage<any[]>( 449 + "shared-events", 450 + [], 451 + ); 452 + const relatedShares = storedShares.filter( 453 + (share: any) => share?.eventId === eventId, 454 + ); 453 455 if (!relatedShares.length) return; 454 456 455 457 const results = await Promise.allSettled( ··· 495 497 setEvents((prevEvents) => 496 498 prevEvents.filter((event) => event.id !== deletedEvent.id), 497 499 ); 498 - void readEncryptedLocalStorage<any[]>("bookmarked-events", []).then((bookmarks) => 499 - writeEncryptedLocalStorage( 500 - "bookmarked-events", 501 - bookmarks.filter((bookmark) => bookmark.id !== deletedEvent.id), 502 - ), 500 + void readEncryptedLocalStorage<any[]>("bookmarked-events", []).then( 501 + (bookmarks) => 502 + writeEncryptedLocalStorage( 503 + "bookmarked-events", 504 + bookmarks.filter((bookmark) => bookmark.id !== deletedEvent.id), 505 + ), 503 506 ); 504 507 setEventDialogOpen(false); 505 508 setSelectedEvent(null); ··· 813 816 )} 814 817 </div> 815 818 {backupEnabled ? ( 816 - <div className="inline-flex h-8 w-8 items-center justify-center rounded-full border" title="Backup status" aria-label="Backup status"> 819 + <div 820 + className="inline-flex h-8 w-8 items-center justify-center rounded-full border" 821 + title="Backup status" 822 + aria-label="Backup status" 823 + > 817 824 {backupStatusIcon ?? <CloudUpload className="h-4 w-4" />} 818 825 </div> 819 826 ) : null} ··· 829 836 </Button> 830 837 </DropdownMenuTrigger> 831 838 <DropdownMenuContent align="end"> 832 - <DropdownMenuItem onClick={() => window.open("https://calendarstatus.xyehr.cn", "_blank", "noopener,noreferrer")}> 839 + <DropdownMenuItem 840 + onClick={() => 841 + window.open( 842 + "https://calendarstatus.xyehr.cn", 843 + "_blank", 844 + "noopener,noreferrer", 845 + ) 846 + } 847 + > 833 848 <ShieldCheck className="mr-2 h-4 w-4" /> 834 849 {t.status} 835 850 </DropdownMenuItem> 836 - <DropdownMenuItem onClick={() => { 837 - window.location.href = "mailto:evan.huang000@proton.me"; 838 - }}> 851 + <DropdownMenuItem 852 + onClick={() => { 853 + window.location.href = "mailto:evan.huang000@proton.me"; 854 + }} 855 + > 839 856 <MessageSquare className="mr-2 h-4 w-4" /> 840 857 {t.feedback} 841 858 </DropdownMenuItem>
+23 -89
components/app/event/event-dialog.tsx
··· 18 18 PopoverContent, 19 19 PopoverTrigger, 20 20 } from "@/components/ui/popover"; 21 - import { addDays, format, parse, isValid, set, getHours, getMinutes } from "date-fns"; 22 - import { ArrowRight, Calendar as CalendarIcon, Clock } from "lucide-react"; 21 + import { addDays, format, getHours, getMinutes, set } from "date-fns"; 22 + import { Calendar as CalendarIcon, Clock } from "lucide-react"; 23 23 import { isZhLanguage, translations, type Language } from "@/lib/i18n"; 24 24 import { useCalendar } from "@/components/providers/calendar-context"; 25 25 import { Checkbox } from "@/components/ui/checkbox"; ··· 126 126 timezone, 127 127 }: EventDialogProps) { 128 128 const { calendars } = useCalendar(); 129 + const [participants, setParticipants] = useState(""); 130 + const [customNotificationTime, setCustomNotificationTime] = useState("10"); 131 + const [selectedCalendar, setSelectedCalendar] = useState(""); 132 + const [notification, setNotification] = useState("0"); 133 + const [description, setDescription] = useState(""); 134 + const [location, setLocation] = useState(""); 129 135 const [title, setTitle] = useState(""); 136 + const [color, setColor] = useState(colorOptions[0].value); 137 + 130 138 const [isAllDay, setIsAllDay] = useState(false); 139 + const [endTimeError, setEndTimeError] = useState(false); 140 + const [startTimeError, setStartTimeError] = useState(false); 141 + const [endDateOpen, setEndDateOpen] = useState(false); 142 + const [startDateOpen, setStartDateOpen] = useState(false); 143 + const [endTimeOpen, setEndTimeOpen] = useState(false); 144 + const [startTimeOpen, setStartTimeOpen] = useState(false); 131 145 132 146 const [startDate, setStartDate] = useState(initialDate); 133 147 const [endDate, setEndDate] = useState(initialDate); 134 - 135 148 const [startTime, setStartTime] = useState<TimeInput>({ 136 149 hours: "00", 137 150 minutes: "00", ··· 145 158 isCustomInput: false, 146 159 }); 147 160 148 - const [startTimeOpen, setStartTimeOpen] = useState(false); 149 - const [endTimeOpen, setEndTimeOpen] = useState(false); 150 - 151 - const [startDateOpen, setStartDateOpen] = useState(false); 152 - const [endDateOpen, setEndDateOpen] = useState(false); 153 - 154 - const [location, setLocation] = useState(""); 155 - const [participants, setParticipants] = useState(""); 156 - const [notification, setNotification] = useState("0"); 157 - const [customNotificationTime, setCustomNotificationTime] = useState("10"); 158 - const [description, setDescription] = useState(""); 159 - const [color, setColor] = useState(colorOptions[0].value); 160 - const [selectedCalendar, setSelectedCalendar] = useState(""); 161 - const [aiPrompt, setAiPrompt] = useState(""); 162 - const [isAiLoading, setIsAiLoading] = useState(false); 163 - 164 - const [startTimeError, setStartTimeError] = useState(false); 165 - const [endTimeError, setEndTimeError] = useState(false); 166 - 167 - const t = translations[language]; 168 - const isZh = isZhLanguage(language); 169 161 const calendarSelectValue = 170 162 selectedCalendar || (calendars.length > 0 ? "__uncategorized__" : ""); 163 + const isZh = isZhLanguage(language); 164 + const t = translations[language]; 165 + 171 166 const getEventColorByCalendarId = (calendarId: string) => { 172 167 const calendar = calendars.find((item) => item.id === calendarId); 173 168 if (!calendar) return colorOptions[0].value; ··· 391 386 }); 392 387 393 388 const newStartDate = set(new Date(startDate), { 394 - hours: parseInt(hours), 395 - minutes: parseInt(minutes), 389 + hours: parseInt(hours, 10), 390 + minutes: parseInt(minutes, 10), 396 391 seconds: 0, 397 392 milliseconds: 0, 398 393 }); ··· 498 493 onOpenChange(false); 499 494 }; 500 495 501 - const handleAiSubmit = async () => { 502 - if (!aiPrompt.trim()) return; 503 - setIsAiLoading(true); 504 - 505 - try { 506 - const response = await fetch("/api/chat/schedule", { 507 - method: "POST", 508 - headers: { 509 - "Content-Type": "application/json", 510 - }, 511 - body: JSON.stringify({ 512 - prompt: aiPrompt, 513 - currentValues: { 514 - title, 515 - startDate: format(getFullStartDate(), "yyyy-MM-dd'T'HH:mm"), 516 - endDate: format(getFullEndDate(), "yyyy-MM-dd'T'HH:mm"), 517 - location, 518 - participants, 519 - description, 520 - }, 521 - }), 522 - }); 523 - 524 - if (!response.ok) throw new Error("AI่ฏทๆฑ‚ๅคฑ่ดฅ"); 525 - 526 - const result = await response.json(); 527 - if (result.data) { 528 - const { 529 - title: newTitle, 530 - startDate: newStart, 531 - endDate: newEnd, 532 - location: newLocation, 533 - participants: newParticipants, 534 - description: newDescription, 535 - } = result.data; 536 - 537 - if (newTitle) setTitle(newTitle); 538 - 539 - if (newStart) { 540 - const startDateObj = new Date(newStart); 541 - setStartDate(startDateObj); 542 - setStartTime(extractTimeFromDate(startDateObj)); 543 - } 544 - 545 - if (newEnd) { 546 - const endDateObj = new Date(newEnd); 547 - setEndDate(endDateObj); 548 - setEndTime(extractTimeFromDate(endDateObj)); 549 - } 550 - 551 - if (newLocation) setLocation(newLocation); 552 - if (newParticipants) setParticipants(newParticipants); 553 - if (newDescription) setDescription(newDescription); 554 - } 555 - } catch (error) { 556 - console.error("AI้”™่ฏฏ:", error); 557 - } finally { 558 - setIsAiLoading(false); 559 - } 560 - }; 561 - 562 496 const renderTimeSelector = ( 563 497 value: TimeInput, 564 498 onChange: (hours: string, minutes: string) => void, ··· 790 724 791 725 const fullStartDate = getFullStartDate(); 792 726 const possibleEndDate = set(new Date(endDate), { 793 - hours: parseInt(hours), 794 - minutes: parseInt(minutes), 727 + hours: parseInt(hours, 10), 728 + minutes: parseInt(minutes, 10), 795 729 seconds: 0, 796 730 }); 797 731
+118 -43
components/app/event/event-preview.tsx
··· 1 1 "use client"; 2 2 3 - import { getEncryptionState, readEncryptedLocalStorage, subscribeEncryptionState, writeEncryptedLocalStorage } from "@/hooks/useLocalStorage"; 3 + import { 4 + getEncryptionState, 5 + readEncryptedLocalStorage, 6 + subscribeEncryptionState, 7 + writeEncryptedLocalStorage, 8 + } from "@/hooks/useLocalStorage"; 4 9 import React, { useState, useRef, useEffect } from "react"; 5 10 import { 6 11 Edit2, ··· 84 89 const [sharePassword, setSharePassword] = useState(""); 85 90 const [burnAfterRead, setBurnAfterRead] = useState(false); 86 91 const colorMapping: Record<string, string> = { 87 - 'bg-[#E6F6FD]': '#3B82F6', 88 - 'bg-[#E7F8F2]': '#10B981', 89 - 'bg-[#FEF5E6]': '#F59E0B', 90 - 'bg-[#FFE4E6]': '#EF4444', 91 - 'bg-[#F3EEFE]': '#8B5CF6', 92 - 'bg-[#FCE7F3]': '#EC4899', 93 - 'bg-[#EEF2FF]': '#6366F1', 94 - 'bg-[#FFF0E5]': '#FB923C', 95 - 'bg-[#E6FAF7]': '#14B8A6', 96 - } 92 + "bg-[#E6F6FD]": "#3B82F6", 93 + "bg-[#E7F8F2]": "#10B981", 94 + "bg-[#FEF5E6]": "#F59E0B", 95 + "bg-[#FFE4E6]": "#EF4444", 96 + "bg-[#F3EEFE]": "#8B5CF6", 97 + "bg-[#FCE7F3]": "#EC4899", 98 + "bg-[#EEF2FF]": "#6366F1", 99 + "bg-[#FFF0E5]": "#FB923C", 100 + "bg-[#E6FAF7]": "#14B8A6", 101 + }; 97 102 98 - 99 103 useEffect(() => { 100 104 if (open && openShareImmediately) { 101 105 if (!isSignedIn && !atprotoSignedIn) { ··· 107 111 } 108 112 } 109 113 }, [open, openShareImmediately, isSignedIn, atprotoSignedIn, language]); 110 - 111 114 112 115 useEffect(() => { 113 116 fetch("/api/atproto/session") ··· 121 124 useEffect(() => { 122 125 let active = true; 123 126 const loadBookmarks = () => 124 - readEncryptedLocalStorage<any[]>("bookmarked-events", []).then((stored) => { 125 - if (active) { 126 - setBookmarks(stored); 127 - } 128 - }); 127 + readEncryptedLocalStorage<any[]>("bookmarked-events", []).then( 128 + (stored) => { 129 + if (active) { 130 + setBookmarks(stored); 131 + } 132 + }, 133 + ); 129 134 130 135 loadBookmarks(); 131 136 const unsubscribe = subscribeEncryptionState(() => { ··· 149 154 150 155 useEffect(() => { 151 156 if (event) { 152 - const isCurrentEventBookmarked = bookmarks.some((bookmark: any) => bookmark.id === event.id); 157 + const isCurrentEventBookmarked = bookmarks.some( 158 + (bookmark: any) => bookmark.id === event.id, 159 + ); 153 160 setIsBookmarked(isCurrentEventBookmarked); 154 161 } 155 162 }, [event, bookmarks]); ··· 172 179 }; 173 180 174 181 const formatNotificationTime = () => { 175 - if (event.notification === 0) return isZh ? "ไบ‹ไปถๅผ€ๅง‹ๆ—ถ" : "At time of event"; 176 - return isZh ? `${event.notification} ๅˆ†้’Ÿๅ‰` : `${event.notification} minutes before`; 182 + if (event.notification === 0) 183 + return isZh ? "ไบ‹ไปถๅผ€ๅง‹ๆ—ถ" : "At time of event"; 184 + return isZh 185 + ? `${event.notification} ๅˆ†้’Ÿๅ‰` 186 + : `${event.notification} minutes before`; 177 187 }; 178 188 179 189 const getInitials = (name: string) => name.charAt(0).toUpperCase(); ··· 230 240 const toggleBookmark = async () => { 231 241 if (!event) return; 232 242 if (isBookmarked) { 233 - const updatedBookmarks = bookmarks.filter((bookmark: any) => bookmark.id !== event.id); 243 + const updatedBookmarks = bookmarks.filter( 244 + (bookmark: any) => bookmark.id !== event.id, 245 + ); 234 246 await writeEncryptedLocalStorage("bookmarked-events", updatedBookmarks); 235 247 setBookmarks(updatedBookmarks); 236 248 setIsBookmarked(false); 237 249 toast(isZh ? "ๅทฒๅ–ๆถˆๆ”ถ่—" : "Removed from bookmarks", { 238 - description: isZh ? "ไบ‹ไปถๅทฒไปŽๆ”ถ่—ๅคนไธญ็งป้™ค" : "Event has been removed from your bookmarks", 250 + description: isZh 251 + ? "ไบ‹ไปถๅทฒไปŽๆ”ถ่—ๅคนไธญ็งป้™ค" 252 + : "Event has been removed from your bookmarks", 239 253 }); 240 254 } else { 241 255 const bookmarkData = { ··· 252 266 setBookmarks(updatedBookmarks); 253 267 setIsBookmarked(true); 254 268 toast(isZh ? "ๅทฒๆ”ถ่—" : "Bookmarked", { 255 - description: isZh ? "ไบ‹ไปถๅทฒๆทปๅŠ ๅˆฐๆ”ถ่—ๅคน" : "Event has been added to your bookmarks", 269 + description: isZh 270 + ? "ไบ‹ไปถๅทฒๆทปๅŠ ๅˆฐๆ”ถ่—ๅคน" 271 + : "Event has been added to your bookmarks", 256 272 }); 257 273 } 258 274 }; ··· 280 296 281 297 try { 282 298 setIsSharing(true); 283 - const shareId = Date.now().toString() + Math.random().toString(36).substring(2, 9); 284 - const clerkUsername = (user?.username || user?.firstName || atprotoHandle || "Anonymous"); 299 + const shareId = 300 + Date.now().toString() + Math.random().toString(36).substring(2, 9); 301 + const clerkUsername = 302 + user?.username || user?.firstName || atprotoHandle || "Anonymous"; 285 303 const sharedEvent = { ...event, sharedBy: clerkUsername }; 286 304 287 305 const payload: any = { id: shareId, data: sharedEvent }; ··· 302 320 const result = await response.json(); 303 321 304 322 if (result.success) { 305 - const link = result?.shareLink ? `${window.location.origin}${result.shareLink}` : `${window.location.origin}/share/${shareId}`; 323 + const link = result?.shareLink 324 + ? `${window.location.origin}${result.shareLink}` 325 + : `${window.location.origin}/share/${shareId}`; 306 326 setShareLink(link); 307 327 308 328 try { ··· 342 362 } 343 363 } catch {} 344 364 345 - const storedShares = await readEncryptedLocalStorage<any[]>("shared-events", []); 365 + const storedShares = await readEncryptedLocalStorage<any[]>( 366 + "shared-events", 367 + [], 368 + ); 346 369 storedShares.push({ 347 370 id: shareId, 348 371 eventId: event.id, ··· 434 457 <div className="flex justify-between items-center p-5"> 435 458 <div className="w-24" /> 436 459 <div className="flex space-x-2 ml-auto"> 437 - <Button variant="ghost" size="icon" onClick={() => onEdit()} className="h-8 w-8"> 460 + <Button 461 + variant="ghost" 462 + size="icon" 463 + onClick={() => onEdit()} 464 + className="h-8 w-8" 465 + > 438 466 <Edit2 className="h-5 w-5" /> 439 467 </Button> 440 468 <Button ··· 454 482 > 455 483 <Share2 className="h-5 w-5" /> 456 484 </Button> 457 - <Button variant="ghost" size="icon" onClick={toggleBookmark} className="h-8 w-8"> 458 - <Bookmark className={cn("h-5 w-5", isBookmarked ? "fill-blue-500 text-blue-500" : "")} /> 485 + <Button 486 + variant="ghost" 487 + size="icon" 488 + onClick={toggleBookmark} 489 + className="h-8 w-8" 490 + > 491 + <Bookmark 492 + className={cn( 493 + "h-5 w-5", 494 + isBookmarked ? "fill-blue-500 text-blue-500" : "", 495 + )} 496 + /> 459 497 </Button> 460 - <Button variant="ghost" size="icon" onClick={handleDeleteClick} className="h-8 w-8"> 498 + <Button 499 + variant="ghost" 500 + size="icon" 501 + onClick={handleDeleteClick} 502 + className="h-8 w-8" 503 + > 461 504 <Trash2 className="h-5 w-5" /> 462 505 </Button> 463 - <Button variant="ghost" size="icon" onClick={() => onOpenChange(false)} className="h-8 w-8 ml-2"> 506 + <Button 507 + variant="ghost" 508 + size="icon" 509 + onClick={() => onOpenChange(false)} 510 + className="h-8 w-8 ml-2" 511 + > 464 512 <X className="h-5 w-5" /> 465 513 </Button> 466 514 </div> 467 515 </div> 468 516 469 517 <div className="px-5 pb-5 flex"> 470 - <div className="w-2 self-stretch rounded-full mr-4" style={{ backgroundColor: colorMapping[event.color] }} /> 518 + <div 519 + className="w-2 self-stretch rounded-full mr-4" 520 + style={{ backgroundColor: colorMapping[event.color] }} 521 + /> 471 522 472 523 <div className="flex-1"> 473 524 <h2 ··· 498 549 <div className="flex items-start"> 499 550 <Users className="h-5 w-5 mr-3 mt-0.5 text-muted-foreground" /> 500 551 <div className="flex-1"> 501 - <div className="flex items-center justify-between cursor-pointer" onClick={toggleParticipants}> 552 + <div 553 + className="flex items-center justify-between cursor-pointer" 554 + onClick={toggleParticipants} 555 + > 502 556 <p> 503 - {event.participants.filter((p) => p.trim() !== "").length}{" "} 557 + { 558 + event.participants.filter((p) => p.trim() !== "") 559 + .length 560 + }{" "} 504 561 {isZh ? "ๅ‚ไธŽ่€…" : "participants"} 505 562 </p> 506 563 <ChevronDown ··· 517 574 .map((participant, index) => ( 518 575 <div key={index} className="flex items-center"> 519 576 <div className="bg-gray-200 rounded-full h-8 w-8 flex items-center justify-center mr-2"> 520 - <span className="font-medium">{getInitials(participant)}</span> 577 + <span className="font-medium"> 578 + {getInitials(participant)} 579 + </span> 521 580 </div> 522 581 <p>{participant}</p> 523 582 </div> ··· 580 639 if (!nextOpen && shareOnlyMode) onOpenChange(false); 581 640 }} 582 641 > 583 - <DialogContent className="sm:max-w-md" ref={dialogContentRef} onClick={handleDialogClick}> 642 + <DialogContent 643 + className="sm:max-w-md" 644 + ref={dialogContentRef} 645 + onClick={handleDialogClick} 646 + > 584 647 <DialogHeader> 585 648 <DialogTitle>{t.shareEvent}</DialogTitle> 586 649 </DialogHeader> ··· 596 659 597 660 <div className="space-y-3"> 598 661 <div className="flex items-center justify-between"> 599 - <Label htmlFor="enable-password">{t.shareEnablePasswordProtection}</Label> 662 + <Label htmlFor="enable-password"> 663 + {t.shareEnablePasswordProtection} 664 + </Label> 600 665 <Checkbox 601 666 id="enable-password" 602 667 checked={passwordEnabled} ··· 613 678 614 679 {passwordEnabled && ( 615 680 <div className="space-y-2"> 616 - <Label htmlFor="share-password">{t.sharePasswordLabel}</Label> 681 + <Label htmlFor="share-password"> 682 + {t.sharePasswordLabel} 683 + </Label> 617 684 <Input 618 685 id="share-password" 619 686 type="password" ··· 626 693 </p> 627 694 628 695 <div className="flex items-center justify-between pt-2"> 629 - <Label htmlFor="burn-after-read">{t.shareBurnAfterRead}</Label> 696 + <Label htmlFor="burn-after-read"> 697 + {t.shareBurnAfterRead} 698 + </Label> 630 699 <Checkbox 631 700 id="burn-after-read" 632 701 checked={burnAfterRead} 633 - onCheckedChange={(checked) => setBurnAfterRead(checked === true)} 702 + onCheckedChange={(checked) => 703 + setBurnAfterRead(checked === true) 704 + } 634 705 /> 635 706 </div> 636 707 <p className="text-xs text-muted-foreground"> ··· 704 775 <div className="mt-4 flex flex-col items-center"> 705 776 <Label className="mb-2">{t.qrCode}</Label> 706 777 <div className="border p-3 rounded bg-white mb-2"> 707 - <img src={qrCodeDataURL || "/placeholder.svg"} alt="QR Code" className="w-full max-w-[200px] mx-auto" /> 778 + <img 779 + src={qrCodeDataURL || "/placeholder.svg"} 780 + alt="QR Code" 781 + className="w-full max-w-[200px] mx-auto" 782 + /> 708 783 </div> 709 784 <Button 710 785 variant="outline"
+28 -23
components/app/profile/daily-toast.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { readEncryptedLocalStorage, writeEncryptedLocalStorage } from "@/hooks/useLocalStorage" 4 - import { translations, useLanguage } from "@/lib/i18n" 5 - import { useEffect, useState } from "react" 6 - import { toast } from "sonner" 3 + import { 4 + readEncryptedLocalStorage, 5 + writeEncryptedLocalStorage, 6 + } from "@/hooks/useLocalStorage"; 7 + import { translations, useLanguage } from "@/lib/i18n"; 8 + import { useEffect, useState } from "react"; 9 + import { toast } from "sonner"; 7 10 8 11 export default function DailyToast() { 9 - const [ready, setReady] = useState(false) 10 - const [language] = useLanguage() 11 - const t = translations[language] 12 + const [ready, setReady] = useState(false); 13 + const [language] = useLanguage(); 14 + const t = translations[language]; 12 15 13 16 useEffect(() => { 14 - const timer = setTimeout(() => setReady(true), 0) 15 - return () => clearTimeout(timer) 16 - }, []) 17 + const timer = setTimeout(() => setReady(true), 0); 18 + return () => clearTimeout(timer); 19 + }, []); 17 20 18 21 useEffect(() => { 19 - if (!ready) return 22 + if (!ready) return; 20 23 21 - const today = new Date().toISOString().split("T")[0] 22 - readEncryptedLocalStorage<string | null>("today-toast", null).then((toastShown) => { 23 - if (toastShown !== today) { 24 - toast(t.welcomeBackTitle, { 25 - description: t.welcomeBackDescription, 26 - }) 24 + const today = new Date().toISOString().split("T")[0]; 25 + readEncryptedLocalStorage<string | null>("today-toast", null).then( 26 + (toastShown) => { 27 + if (toastShown !== today) { 28 + toast(t.welcomeBackTitle, { 29 + description: t.welcomeBackDescription, 30 + }); 27 31 28 - void writeEncryptedLocalStorage("today-toast", today) 29 - } 30 - }) 31 - }, [ready]) 32 + void writeEncryptedLocalStorage("today-toast", today); 33 + } 34 + }, 35 + ); 36 + }, [ready]); 32 37 33 - return null 38 + return null; 34 39 }
+144 -77
components/app/profile/settings.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import UserProfileButton, { type UserProfileSection } from "@/components/app/profile/user-profile-button" 4 - import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 5 - import { getLanguageAutonym, supportedLanguages, translations, type Language } from "@/lib/i18n" 6 - import ShareManagement from "@/components/app/analytics/share-management" 7 - import BuildInfoCard from "@/components/app/analytics/build-info-card" 8 - import ImportExport from "@/components/app/analytics/import-export" 9 - import type { NOTIFICATION_SOUNDS } from "@/utils/notifications" 10 - import type { CalendarEvent } from "@/components/app/calendar" 11 - import { Switch } from "@/components/ui/switch" 12 - import { Label } from "@/components/ui/label" 13 - import { Kbd } from "@/components/ui/kbd" 14 - import { useTheme } from "next-themes" 3 + import UserProfileButton, { 4 + type UserProfileSection, 5 + } from "@/components/app/profile/user-profile-button"; 6 + import { 7 + Select, 8 + SelectContent, 9 + SelectItem, 10 + SelectTrigger, 11 + SelectValue, 12 + } from "@/components/ui/select"; 13 + import { 14 + getLanguageAutonym, 15 + supportedLanguages, 16 + translations, 17 + type Language, 18 + } from "@/lib/i18n"; 19 + import ShareManagement from "@/components/app/analytics/share-management"; 20 + import BuildInfoCard from "@/components/app/analytics/build-info-card"; 21 + import ImportExport from "@/components/app/analytics/import-export"; 22 + import type { NOTIFICATION_SOUNDS } from "@/utils/notifications"; 23 + import type { CalendarEvent } from "@/components/app/calendar"; 24 + import { Switch } from "@/components/ui/switch"; 25 + import { Label } from "@/components/ui/label"; 26 + import { Kbd } from "@/components/ui/kbd"; 27 + import { useTheme } from "next-themes"; 15 28 16 29 interface SettingsProps { 17 - language: Language 18 - setLanguage: (lang: Language) => void 19 - firstDayOfWeek: number 20 - setFirstDayOfWeek: (day: number) => void 21 - timezone: string 22 - setTimezone: (timezone: string) => void 23 - notificationSound: keyof typeof NOTIFICATION_SOUNDS 24 - setNotificationSound: (sound: keyof typeof NOTIFICATION_SOUNDS) => void 25 - defaultView: string 26 - setDefaultView: (view: string) => void 27 - enableShortcuts: boolean 28 - setEnableShortcuts: (enable: boolean) => void 29 - timeFormat: "24h" | "12h" 30 - setTimeFormat: (format: "24h" | "12h") => void 31 - events: CalendarEvent[] 32 - onImportEvents: (events: CalendarEvent[]) => void 33 - focusUserProfileSection?: UserProfileSection | null 34 - toastPosition: "bottom-left" | "bottom-center" | "bottom-right" 35 - setToastPosition: (position: "bottom-left" | "bottom-center" | "bottom-right") => void 30 + language: Language; 31 + setLanguage: (lang: Language) => void; 32 + firstDayOfWeek: number; 33 + setFirstDayOfWeek: (day: number) => void; 34 + timezone: string; 35 + setTimezone: (timezone: string) => void; 36 + notificationSound: keyof typeof NOTIFICATION_SOUNDS; 37 + setNotificationSound: (sound: keyof typeof NOTIFICATION_SOUNDS) => void; 38 + defaultView: string; 39 + setDefaultView: (view: string) => void; 40 + enableShortcuts: boolean; 41 + setEnableShortcuts: (enable: boolean) => void; 42 + timeFormat: "24h" | "12h"; 43 + setTimeFormat: (format: "24h" | "12h") => void; 44 + events: CalendarEvent[]; 45 + onImportEvents: (events: CalendarEvent[]) => void; 46 + focusUserProfileSection?: UserProfileSection | null; 47 + toastPosition: "bottom-left" | "bottom-center" | "bottom-right"; 48 + setToastPosition: ( 49 + position: "bottom-left" | "bottom-center" | "bottom-right", 50 + ) => void; 36 51 } 37 52 38 53 export default function Settings({ ··· 56 71 toastPosition, 57 72 setToastPosition, 58 73 }: SettingsProps) { 59 - const { theme, setTheme } = useTheme() 60 - const t = translations[language] 74 + const { theme, setTheme } = useTheme(); 75 + const t = translations[language]; 61 76 62 77 const getGMTTimezones = () => { 63 - const timezones = Intl.supportedValuesOf("timeZone") 78 + const timezones = Intl.supportedValuesOf("timeZone"); 64 79 65 80 const getUTCOffset = (timeZone: string) => { 66 81 const parts = new Intl.DateTimeFormat("en-US", { 67 82 timeZone, 68 83 timeZoneName: "shortOffset", 69 - }).formatToParts(new Date()) 84 + }).formatToParts(new Date()); 70 85 71 - const timeZoneName = parts.find((part) => part.type === "timeZoneName")?.value ?? "" 86 + const timeZoneName = 87 + parts.find((part) => part.type === "timeZoneName")?.value ?? ""; 72 88 73 89 if (timeZoneName === "GMT" || timeZoneName === "UTC") { 74 - return { offsetString: "UTC+00:00", offsetMinutes: 0 } 90 + return { offsetString: "UTC+00:00", offsetMinutes: 0 }; 75 91 } 76 92 77 - const match = timeZoneName.match(/(?:GMT|UTC)([+-])(\d{1,2})(?::?(\d{2}))?/) 93 + const match = timeZoneName.match( 94 + /(?:GMT|UTC)([+-])(\d{1,2})(?::?(\d{2}))?/, 95 + ); 78 96 if (!match) { 79 - return { offsetString: "UTC+00:00", offsetMinutes: 0 } 97 + return { offsetString: "UTC+00:00", offsetMinutes: 0 }; 80 98 } 81 99 82 - const [, sign, hours, minutes = "00"] = match 83 - const parsedHours = Number.parseInt(hours, 10) 84 - const parsedMinutes = Number.parseInt(minutes, 10) 85 - const totalMinutes = parsedHours * 60 + parsedMinutes 86 - const offsetMinutes = sign === "-" ? -totalMinutes : totalMinutes 100 + const [, sign, hours, minutes = "00"] = match; 101 + const parsedHours = Number.parseInt(hours, 10); 102 + const parsedMinutes = Number.parseInt(minutes, 10); 103 + const totalMinutes = parsedHours * 60 + parsedMinutes; 104 + const offsetMinutes = sign === "-" ? -totalMinutes : totalMinutes; 87 105 88 106 return { 89 107 offsetString: `UTC${sign}${hours.padStart(2, "0")}:${minutes}`, 90 108 offsetMinutes, 91 - } 92 - } 109 + }; 110 + }; 93 111 94 112 return timezones 95 113 .map((tz) => { 96 114 try { 97 - const { offsetString, offsetMinutes } = getUTCOffset(tz) 115 + const { offsetString, offsetMinutes } = getUTCOffset(tz); 98 116 99 117 return { 100 118 value: tz, 101 119 label: `${offsetString} ยท ${tz}`, 102 120 offsetMinutes, 103 - } 121 + }; 104 122 } catch { 105 123 return { 106 124 value: tz, 107 125 label: `UTC+00:00 ยท ${tz}`, 108 126 offsetMinutes: 0, 109 - } 127 + }; 110 128 } 111 129 }) 112 - .sort((a, b) => a.offsetMinutes - b.offsetMinutes || a.value.localeCompare(b.value)) 113 - } 130 + .sort( 131 + (a, b) => 132 + a.offsetMinutes - b.offsetMinutes || a.value.localeCompare(b.value), 133 + ); 134 + }; 114 135 115 - const gmtTimezones = getGMTTimezones() 136 + const gmtTimezones = getGMTTimezones(); 116 137 117 138 const handleLanguageChange = (newLang: Language) => { 118 - setLanguage(newLang) 119 - window.dispatchEvent(new CustomEvent("languagechange", { detail: { language: newLang } })) 120 - } 139 + setLanguage(newLang); 140 + window.dispatchEvent( 141 + new CustomEvent("languagechange", { detail: { language: newLang } }), 142 + ); 143 + }; 121 144 122 145 const handleThemeChange = (newTheme: string) => { 123 - setTheme(newTheme) 124 - } 146 + setTheme(newTheme); 147 + }; 125 148 126 149 return ( 127 150 <div className="space-y-8 p-4"> ··· 149 172 150 173 <div className="space-y-2"> 151 174 <Label htmlFor="language">{t.language}</Label> 152 - <Select value={language} onValueChange={(value: Language) => handleLanguageChange(value)}> 175 + <Select 176 + value={language} 177 + onValueChange={(value: Language) => handleLanguageChange(value)} 178 + > 153 179 <SelectTrigger id="language"> 154 180 <SelectValue /> 155 181 </SelectTrigger> ··· 213 239 214 240 <div className="space-y-2"> 215 241 <Label htmlFor="time-format">{t.timeFormat}</Label> 216 - <Select value={timeFormat} onValueChange={(value: "24h" | "12h") => setTimeFormat(value)}> 242 + <Select 243 + value={timeFormat} 244 + onValueChange={(value: "24h" | "12h") => setTimeFormat(value)} 245 + > 217 246 <SelectTrigger id="time-format"> 218 247 <SelectValue /> 219 248 </SelectTrigger> ··· 226 255 227 256 <div className="space-y-2"> 228 257 <Label htmlFor="toast-position">{t.toastPosition}</Label> 229 - <Select value={toastPosition} onValueChange={(value: "bottom-left" | "bottom-center" | "bottom-right") => setToastPosition(value)}> 258 + <Select 259 + value={toastPosition} 260 + onValueChange={( 261 + value: "bottom-left" | "bottom-center" | "bottom-right", 262 + ) => setToastPosition(value)} 263 + > 230 264 <SelectTrigger id="toast-position"> 231 265 <SelectValue /> 232 266 </SelectTrigger> 233 267 <SelectContent> 234 - <SelectItem value="bottom-left">{t.toastPositionBottomLeft}</SelectItem> 235 - <SelectItem value="bottom-center">{t.toastPositionBottomCenter}</SelectItem> 236 - <SelectItem value="bottom-right">{t.toastPositionBottomRight}</SelectItem> 268 + <SelectItem value="bottom-left"> 269 + {t.toastPositionBottomLeft} 270 + </SelectItem> 271 + <SelectItem value="bottom-center"> 272 + {t.toastPositionBottomCenter} 273 + </SelectItem> 274 + <SelectItem value="bottom-right"> 275 + {t.toastPositionBottomRight} 276 + </SelectItem> 237 277 </SelectContent> 238 278 </Select> 239 279 </div> 240 280 241 281 <div className="flex items-center space-x-2"> 242 - <Switch id="enable-shortcuts" checked={enableShortcuts} onCheckedChange={setEnableShortcuts} /> 282 + <Switch 283 + id="enable-shortcuts" 284 + checked={enableShortcuts} 285 + onCheckedChange={setEnableShortcuts} 286 + /> 243 287 <Label htmlFor="enable-shortcuts">{t.enableShortcuts}</Label> 244 288 </div> 245 289 ··· 247 291 <div className="rounded-md border p-4"> 248 292 <h3 className="mb-2 font-medium">{t.availableShortcuts}</h3> 249 293 <div className="grid grid-cols-2 gap-2 text-sm"> 250 - <div><Kbd>N</Kbd> - {t.newEvent}</div> 251 - <div><Kbd>/</Kbd> - {t.searchEvents}</div> 252 - <div><Kbd>T</Kbd> - {t.today}</div> 253 - <div><Kbd>1</Kbd> - {t.dayView}</div> 254 - <div><Kbd>2</Kbd> - {t.weekView}</div> 255 - <div><Kbd>3</Kbd> - {t.monthView}</div> 256 - <div><Kbd>4</Kbd> - {t.yearView}</div> 257 - <div><Kbd>5</Kbd> - {t.fourDayView}</div> 258 - <div><Kbd>โ†’</Kbd> - {t.nextPeriod}</div> 259 - <div><Kbd>โ†</Kbd> - {t.previousPeriod}</div> 294 + <div> 295 + <Kbd>N</Kbd> - {t.newEvent} 296 + </div> 297 + <div> 298 + <Kbd>/</Kbd> - {t.searchEvents} 299 + </div> 300 + <div> 301 + <Kbd>T</Kbd> - {t.today} 302 + </div> 303 + <div> 304 + <Kbd>1</Kbd> - {t.dayView} 305 + </div> 306 + <div> 307 + <Kbd>2</Kbd> - {t.weekView} 308 + </div> 309 + <div> 310 + <Kbd>3</Kbd> - {t.monthView} 311 + </div> 312 + <div> 313 + <Kbd>4</Kbd> - {t.yearView} 314 + </div> 315 + <div> 316 + <Kbd>5</Kbd> - {t.fourDayView} 317 + </div> 318 + <div> 319 + <Kbd>โ†’</Kbd> - {t.nextPeriod} 320 + </div> 321 + <div> 322 + <Kbd>โ†</Kbd> - {t.previousPeriod} 323 + </div> 260 324 </div> 261 325 </div> 262 326 )} ··· 264 328 265 329 <div className="space-y-3"> 266 330 <h2 className="text-lg font-semibold">{t.account}</h2> 267 - <UserProfileButton mode="settings" focusSection={focusUserProfileSection} /> 331 + <UserProfileButton 332 + mode="settings" 333 + focusSection={focusUserProfileSection} 334 + /> 268 335 </div> 269 336 270 337 <ShareManagement /> 271 338 <ImportExport events={events} onImportEvents={onImportEvents} /> 272 339 <BuildInfoCard language={language} /> 273 340 </div> 274 - ) 341 + ); 275 342 }
+179 -57
components/app/profile/shared-event.tsx
··· 29 29 import { useCalendar } from "@/components/providers/calendar-context"; 30 30 import { motion } from "framer-motion"; 31 31 import { Badge } from "@/components/ui/badge"; 32 - import { Card, CardContent, CardDescription, CardTitle } from "@/components/ui/card"; 32 + import { 33 + Card, 34 + CardContent, 35 + CardDescription, 36 + CardTitle, 37 + } from "@/components/ui/card"; 33 38 import { Input } from "@/components/ui/input"; 34 39 import { Label } from "@/components/ui/label"; 35 40 ··· 55 60 56 61 function getDarkerColorClass(color: string) { 57 62 const colorMapping: Record<string, string> = { 58 - 'bg-[#E6F6FD]': '#3B82F6', 59 - 'bg-[#E7F8F2]': '#10B981', 60 - 'bg-[#FEF5E6]': '#F59E0B', 61 - 'bg-[#FFE4E6]': '#EF4444', 62 - 'bg-[#F3EEFE]': '#8B5CF6', 63 - 'bg-[#FCE7F3]': '#EC4899', 64 - 'bg-[#EEF2FF]': '#6366F1', 65 - 'bg-[#FFF0E5]': '#FB923C', 66 - 'bg-[#E6FAF7]': '#14B8A6', 67 - } 68 - 69 - return colorMapping[color] || '#3A3A3A'; 63 + "bg-[#E6F6FD]": "#3B82F6", 64 + "bg-[#E7F8F2]": "#10B981", 65 + "bg-[#FEF5E6]": "#F59E0B", 66 + "bg-[#FFE4E6]": "#EF4444", 67 + "bg-[#F3EEFE]": "#8B5CF6", 68 + "bg-[#FCE7F3]": "#EC4899", 69 + "bg-[#EEF2FF]": "#6366F1", 70 + "bg-[#FFF0E5]": "#FB923C", 71 + "bg-[#E6FAF7]": "#14B8A6", 72 + }; 73 + 74 + return colorMapping[color] || "#3A3A3A"; 70 75 } 71 76 72 - export default function SharedEventView({ shareId, handle }: SharedEventViewProps) { 77 + export default function SharedEventView({ 78 + shareId, 79 + handle, 80 + }: SharedEventViewProps) { 73 81 const router = useRouter(); 74 82 const { toast } = useToast(); 75 83 const [language] = useLanguage(); ··· 106 114 return; 107 115 } 108 116 109 - const response = await fetch(handle 110 - ? `/api/share/public?handle=${encodeURIComponent(handle)}&id=${encodeURIComponent(shareId)}` 111 - : `/api/share?id=${encodeURIComponent(shareId)}`); 117 + const response = await fetch( 118 + handle 119 + ? `/api/share/public?handle=${encodeURIComponent(handle)}&id=${encodeURIComponent(shareId)}` 120 + : `/api/share?id=${encodeURIComponent(shareId)}`, 121 + ); 112 122 113 123 if (!response.ok) { 114 124 if (response.status === 401) { ··· 119 129 return; 120 130 } 121 131 } 122 - setError(response.status === 404 ? "Shared event not found" : "Failed to load shared event"); 132 + setError( 133 + response.status === 404 134 + ? "Shared event not found" 135 + : "Failed to load shared event", 136 + ); 123 137 return; 124 138 } 125 139 ··· 129 143 return; 130 144 } 131 145 132 - const eventData = typeof result.data === "object" ? result.data : JSON.parse(result.data); 146 + const eventData = 147 + typeof result.data === "object" 148 + ? result.data 149 + : JSON.parse(result.data); 133 150 setEvent(eventData); 134 151 setBurnAfterRead(!!result?.burnAfterRead); 135 152 } catch { ··· 174 191 return; 175 192 } 176 193 if (response.status === 404) { 177 - setPasswordError(isZh ? "ๅˆ†ไบซไธๅญ˜ๅœจๆˆ–ๅทฒ่ขซ้”€ๆฏ" : "Share not found or already destroyed"); 194 + setPasswordError( 195 + isZh 196 + ? "ๅˆ†ไบซไธๅญ˜ๅœจๆˆ–ๅทฒ่ขซ้”€ๆฏ" 197 + : "Share not found or already destroyed", 198 + ); 178 199 setRequiresPassword(true); 179 200 return; 180 201 } ··· 190 211 return; 191 212 } 192 213 193 - const eventData = typeof result.data === "object" ? result.data : JSON.parse(result.data); 214 + const eventData = 215 + typeof result.data === "object" ? result.data : JSON.parse(result.data); 194 216 setEvent(eventData); 195 217 setBurnAfterRead(!!result?.burnAfterRead); 196 218 setRequiresPassword(false); ··· 199 221 if (result?.burnAfterRead) { 200 222 toast({ 201 223 title: isZh ? "้˜…ๅŽๅณ็„š" : "Burn after read", 202 - description: isZh ? "ๅทฒๆˆๅŠŸๆŸฅ็œ‹๏ผŒ่ฏฅๅˆ†ไบซๅทฒไปŽๆœๅŠกๅ™จๅˆ ้™คใ€‚" : "Viewed successfully. This share has been deleted from the server.", 224 + description: isZh 225 + ? "ๅทฒๆˆๅŠŸๆŸฅ็œ‹๏ผŒ่ฏฅๅˆ†ไบซๅทฒไปŽๆœๅŠกๅ™จๅˆ ้™คใ€‚" 226 + : "Viewed successfully. This share has been deleted from the server.", 203 227 }); 204 228 } 205 229 } catch { ··· 223 247 setIsAdding(true); 224 248 225 249 let targetCalendarId = event.calendarId; 226 - const calendarExists = calendars.some((cal) => cal.id === targetCalendarId); 250 + const calendarExists = calendars.some( 251 + (cal) => cal.id === targetCalendarId, 252 + ); 227 253 if (!calendarExists) targetCalendarId = calendars[0]?.id ?? "default"; 228 254 229 255 const newEvent = { ··· 238 264 239 265 toast({ 240 266 title: isZh ? "ๆทปๅŠ ๆˆๅŠŸ" : "Added Successfully", 241 - description: isZh ? "ไบ‹ไปถๅทฒๆทปๅŠ ๅˆฐๆ‚จ็š„ๆ—ฅๅކ" : "Event has been added to your calendar", 267 + description: isZh 268 + ? "ไบ‹ไปถๅทฒๆทปๅŠ ๅˆฐๆ‚จ็š„ๆ—ฅๅކ" 269 + : "Event has been added to your calendar", 242 270 }); 243 271 244 272 setTimeout(() => router.push("/"), 1500); 245 273 } catch (e) { 246 274 toast({ 247 275 title: isZh ? "ๆทปๅŠ ๅคฑ่ดฅ" : "Add Failed", 248 - description: e instanceof Error ? e.message : isZh ? "ๆœช็Ÿฅ้”™่ฏฏ" : "Unknown error", 276 + description: 277 + e instanceof Error ? e.message : isZh ? "ๆœช็Ÿฅ้”™่ฏฏ" : "Unknown error", 249 278 variant: "destructive", 250 279 }); 251 280 } finally { ··· 258 287 setCopied(true); 259 288 toast({ 260 289 title: isZh ? "้“พๆŽฅๅทฒๅคๅˆถ" : "Link Copied", 261 - description: isZh ? "ๅˆ†ไบซ้“พๆŽฅๅทฒๅคๅˆถๅˆฐๅ‰ช่ดดๆฟ" : "Share link copied to clipboard", 290 + description: isZh 291 + ? "ๅˆ†ไบซ้“พๆŽฅๅทฒๅคๅˆถๅˆฐๅ‰ช่ดดๆฟ" 292 + : "Share link copied to clipboard", 262 293 }); 263 294 setTimeout(() => setCopied(false), 2000); 264 295 }; ··· 315 346 </div> 316 347 317 348 <div className="relative z-10 flex w-full max-w-sm flex-col gap-6"> 318 - <a href="/" className="flex items-center gap-2 self-center font-medium"> 319 - <Image src="/icon.svg" alt="One Calendar" width={16} height={16} className="size-4" /> 349 + <a 350 + href="/" 351 + className="flex items-center gap-2 self-center font-medium" 352 + > 353 + <Image 354 + src="/icon.svg" 355 + alt="One Calendar" 356 + width={16} 357 + height={16} 358 + className="size-4" 359 + /> 320 360 One Calendar 321 361 </a> 322 362 323 - <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}> 363 + <motion.div 364 + initial={{ opacity: 0, y: 20 }} 365 + animate={{ opacity: 1, y: 0 }} 366 + transition={{ duration: 0.5 }} 367 + > 324 368 <Card className="max-w-md w-full overflow-hidden"> 325 369 <CardContent className="p-6"> 326 370 <div className="flex flex-col items-center text-center gap-3 mb-6"> ··· 350 394 351 395 <div className="space-y-3"> 352 396 <div className="space-y-2"> 353 - <Label htmlFor="share-password">{isZh ? "ๅฏ†็ " : "Password"}</Label> 397 + <Label htmlFor="share-password"> 398 + {isZh ? "ๅฏ†็ " : "Password"} 399 + </Label> 354 400 <Input 355 401 id="share-password" 356 402 type="password" ··· 359 405 onKeyDown={(e) => { 360 406 if (e.key === "Enter") tryDecryptWithPassword(); 361 407 }} 362 - placeholder={isZh ? "่พ“ๅ…ฅๅˆ†ไบซๅฏ†็ " : "Enter share password"} 408 + placeholder={ 409 + isZh ? "่พ“ๅ…ฅๅˆ†ไบซๅฏ†็ " : "Enter share password" 410 + } 363 411 /> 364 412 {passwordError ? ( 365 413 <p className="text-sm text-red-500">{passwordError}</p> 366 414 ) : ( 367 415 <p className="text-xs text-muted-foreground"> 368 - {isZh ? "ๅฏ†็ ้”™่ฏฏๅฐ†ๆ— ๆณ•่งฃๅฏ†ใ€‚" : "Wrong password cannot be decrypted."} 416 + {isZh 417 + ? "ๅฏ†็ ้”™่ฏฏๅฐ†ๆ— ๆณ•่งฃๅฏ†ใ€‚" 418 + : "Wrong password cannot be decrypted."} 369 419 </p> 370 420 )} 371 421 </div> 372 422 373 - <Button className="w-full" onClick={tryDecryptWithPassword} disabled={passwordSubmitting}> 423 + <Button 424 + className="w-full" 425 + onClick={tryDecryptWithPassword} 426 + disabled={passwordSubmitting} 427 + > 374 428 {passwordSubmitting ? ( 375 429 <span className="flex items-center justify-center"> 376 430 <Loader2 className="mr-2 h-4 w-4 animate-spin" /> ··· 383 437 )} 384 438 </Button> 385 439 386 - <Button variant="outline" className="w-full" onClick={() => router.push("/")}> 440 + <Button 441 + variant="outline" 442 + className="w-full" 443 + onClick={() => router.push("/")} 444 + > 387 445 <Home className="mr-2 h-4 w-4" /> 388 446 {isZh ? "่ฟ”ๅ›žไธป้กต" : "Return to Home"} 389 447 </Button> ··· 419 477 </div> 420 478 421 479 <div className="relative z-10 flex w-full max-w-sm flex-col gap-6"> 422 - <a href="/" className="flex items-center gap-2 self-center font-medium"> 423 - <Image src="/icon.svg" alt="One Calendar" width={16} height={16} className="size-4" /> 480 + <a 481 + href="/" 482 + className="flex items-center gap-2 self-center font-medium" 483 + > 484 + <Image 485 + src="/icon.svg" 486 + alt="One Calendar" 487 + width={16} 488 + height={16} 489 + className="size-4" 490 + /> 424 491 One Calendar 425 492 </a> 426 493 427 - <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}> 494 + <motion.div 495 + initial={{ opacity: 0, y: 20 }} 496 + animate={{ opacity: 1, y: 0 }} 497 + transition={{ duration: 0.5 }} 498 + > 428 499 <Card className="max-w-md w-full overflow-hidden"> 429 500 <CardContent className="p-6 text-center"> 430 501 <div className="bg-red-50 dark:bg-red-900/20 rounded-full p-4 mx-auto w-fit"> ··· 454 525 const endDate = new Date(event.endDate); 455 526 const durationMs = endDate.getTime() - startDate.getTime(); 456 527 const normalizedDurationMs = event.isAllDay 457 - ? Math.max(1, Math.ceil(durationMs / (1000 * 60 * 60 * 24))) * 24 * 60 * 60 * 1000 528 + ? Math.max(1, Math.ceil(durationMs / (1000 * 60 * 60 * 24))) * 529 + 24 * 530 + 60 * 531 + 60 * 532 + 1000 458 533 : durationMs; 459 534 const durationHours = Math.floor(normalizedDurationMs / (1000 * 60 * 60)); 460 - const durationMinutes = Math.floor((normalizedDurationMs % (1000 * 60 * 60)) / (1000 * 60)); 461 - const durationText = 462 - isZh 463 - ? `${durationHours > 0 ? `${durationHours}ๅฐๆ—ถ` : ""}${durationMinutes > 0 ? ` ${durationMinutes}ๅˆ†้’Ÿ` : ""}` 464 - : `${durationHours > 0 ? `${durationHours}h` : ""}${durationMinutes > 0 ? ` ${durationMinutes}m` : ""}`; 535 + const durationMinutes = Math.floor( 536 + (normalizedDurationMs % (1000 * 60 * 60)) / (1000 * 60), 537 + ); 538 + const durationText = isZh 539 + ? `${durationHours > 0 ? `${durationHours}ๅฐๆ—ถ` : ""}${durationMinutes > 0 ? ` ${durationMinutes}ๅˆ†้’Ÿ` : ""}` 540 + : `${durationHours > 0 ? `${durationHours}h` : ""}${durationMinutes > 0 ? ` ${durationMinutes}m` : ""}`; 465 541 466 542 return ( 467 543 <div className="relative flex flex-col items-center justify-center min-h-screen p-4"> ··· 486 562 487 563 <div className="relative z-10 flex w-full max-w-sm flex-col gap-6"> 488 564 <a href="/" className="flex items-center gap-2 self-center font-medium"> 489 - <Image src="/icon.svg" alt="One Calendar" width={16} height={16} className="size-4" /> 490 - One Calendar 491 - </a> 565 + <Image 566 + src="/icon.svg" 567 + alt="One Calendar" 568 + width={16} 569 + height={16} 570 + className="size-4" 571 + /> 572 + One Calendar 573 + </a> 492 574 493 - <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}> 575 + <motion.div 576 + initial={{ opacity: 0, y: 20 }} 577 + animate={{ opacity: 1, y: 0 }} 578 + transition={{ duration: 0.5 }} 579 + > 494 580 <Card className="relative max-w-md w-full overflow-hidden"> 495 - <div className={cn("absolute inset-y-0 left-0 w-1")} style={{ backgroundColor: getDarkerColorClass(event.color) }}/> 581 + <div 582 + className={cn("absolute inset-y-0 left-0 w-1")} 583 + style={{ backgroundColor: getDarkerColorClass(event.color) }} 584 + /> 496 585 <div> 497 586 <CardContent className="p-6"> 498 587 <div className="flex justify-between items-start mb-4"> 499 588 <div> 500 - <CardTitle className="text-2xl font-bold mb-1">{event.title}</CardTitle> 589 + <CardTitle className="text-2xl font-bold mb-1"> 590 + {event.title} 591 + </CardTitle> 501 592 <CardDescription> 502 593 {isZh ? "ๅˆ†ไบซ่€…๏ผš" : "Shared by: "} 503 - <a className="font-medium underline underline-offset-4" href={`https://bsky.app/profile/${String(event.sharedBy || "").replace(/^@/, "")}`} target="_blank" rel="noreferrer">{event.sharedBy}</a> 594 + <a 595 + className="font-medium underline underline-offset-4" 596 + href={`https://bsky.app/profile/${String(event.sharedBy || "").replace(/^@/, "")}`} 597 + target="_blank" 598 + rel="noreferrer" 599 + > 600 + {event.sharedBy} 601 + </a> 504 602 </CardDescription> 505 603 {burnAfterRead && ( 506 604 <div className="mt-2 inline-flex items-center gap-2 text-sm text-red-500"> 507 605 <Flame className="h-4 w-4" /> 508 - {isZh ? "ๆญคๅˆ†ไบซไธบ้˜…ๅŽๅณ็„š๏ผˆๅทฒๅœจๆœๅŠกๅ™จๅˆ ้™ค๏ผ‰" : "Burn-after-read (deleted from server)"} 606 + {isZh 607 + ? "ๆญคๅˆ†ไบซไธบ้˜…ๅŽๅณ็„š๏ผˆๅทฒๅœจๆœๅŠกๅ™จๅˆ ้™ค๏ผ‰" 608 + : "Burn-after-read (deleted from server)"} 509 609 </div> 510 610 )} 511 611 </div> ··· 519 619 <CardContent className="p-4 flex items-start"> 520 620 <Calendar className="h-5 w-5 mr-3 mt-0.5 text-primary" /> 521 621 <div> 522 - <p className="font-medium">{formatDateWithTimezone(event.startDate)}</p> 622 + <p className="font-medium"> 623 + {formatDateWithTimezone(event.startDate)} 624 + </p> 523 625 <p className="text-muted-foreground"> 524 - {isZh ? "่‡ณ" : "to"} {formatDateWithTimezone(event.endDate)} 626 + {isZh ? "่‡ณ" : "to"}{" "} 627 + {formatDateWithTimezone(event.endDate)} 525 628 </p> 526 629 </div> 527 630 </CardContent> ··· 560 663 className="flex items-start" 561 664 > 562 665 <Bell className="h-5 w-5 mr-3 mt-0.5 text-muted-foreground" /> 563 - <p>{isZh ? `ๆๅ‰ ${event.notification} ๅˆ†้’Ÿๆ้†’` : `${event.notification} minutes before`}</p> 666 + <p> 667 + {isZh 668 + ? `ๆๅ‰ ${event.notification} ๅˆ†้’Ÿๆ้†’` 669 + : `${event.notification} minutes before`} 670 + </p> 564 671 </motion.div> 565 672 )} 566 673 ··· 578 685 </div> 579 686 580 687 <div className="mt-8 space-y-3"> 581 - <Button className="w-full" variant="default" onClick={handleAddToCalendar} disabled={isAdding}> 688 + <Button 689 + className="w-full" 690 + variant="default" 691 + onClick={handleAddToCalendar} 692 + disabled={isAdding} 693 + > 582 694 {isAdding ? ( 583 695 <span className="flex items-center justify-center"> 584 696 <Loader2 className="mr-2 h-4 w-4 animate-spin" /> ··· 592 704 )} 593 705 </Button> 594 706 595 - <Button variant="outline" className="w-full" onClick={copyLink}> 707 + <Button 708 + variant="outline" 709 + className="w-full" 710 + onClick={copyLink} 711 + > 596 712 <Copy className="mr-2 h-4 w-4" /> 597 - {copied ? (isZh ? "ๅทฒๅคๅˆถ!" : "Copied!") : isZh ? "ๅคๅˆถๅˆ†ไบซ้“พๆŽฅ" : "Copy Share Link"} 713 + {copied 714 + ? isZh 715 + ? "ๅทฒๅคๅˆถ!" 716 + : "Copied!" 717 + : isZh 718 + ? "ๅคๅˆถๅˆ†ไบซ้“พๆŽฅ" 719 + : "Copy Share Link"} 598 720 </Button> 599 721 </div> 600 722 </CardContent>
+601 -353
components/app/profile/user-profile-button.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { useEffect, useMemo, useRef, useState } from "react" 3 + import { useEffect, useMemo, useRef, useState } from "react"; 4 4 import { 5 5 User, 6 6 LogOut, ··· 14 14 Camera, 15 15 BarChart2, 16 16 Settings, 17 - } from "lucide-react" 18 - import { Button } from "@/components/ui/button" 17 + } from "lucide-react"; 18 + import { Button } from "@/components/ui/button"; 19 19 import { 20 20 Dialog, 21 21 DialogContent, ··· 23 23 DialogFooter, 24 24 DialogHeader, 25 25 DialogTitle, 26 - } from "@/components/ui/dialog" 26 + } from "@/components/ui/dialog"; 27 27 import { 28 28 AlertDialog, 29 29 AlertDialogAction, ··· 33 33 AlertDialogFooter, 34 34 AlertDialogHeader, 35 35 AlertDialogTitle, 36 - } from "@/components/ui/alert-dialog" 36 + } from "@/components/ui/alert-dialog"; 37 37 import { 38 38 DropdownMenu, 39 39 DropdownMenuContent, 40 40 DropdownMenuItem, 41 41 DropdownMenuTrigger, 42 - } from "@/components/ui/dropdown-menu" 43 - import { Input } from "@/components/ui/input" 44 - import { Label } from "@/components/ui/label" 45 - import { ScrollArea } from "@/components/ui/scroll-area" 46 - import { Spinner } from "@/components/ui/spinner" 47 - import { toast } from "sonner" 48 - import { useCalendar } from "@/components/providers/calendar-context" 49 - import { translations, useLanguage } from "@/lib/i18n" 50 - import { useUser, SignOutButton } from "@clerk/nextjs" 51 - import { useRouter } from "next/navigation" 52 - import { decryptPayload, encryptPayload, isEncryptedPayload } from "@/lib/crypto" 42 + } from "@/components/ui/dropdown-menu"; 43 + import { Input } from "@/components/ui/input"; 44 + import { Label } from "@/components/ui/label"; 45 + import { ScrollArea } from "@/components/ui/scroll-area"; 46 + import { Spinner } from "@/components/ui/spinner"; 47 + import { toast } from "sonner"; 48 + import { useCalendar } from "@/components/providers/calendar-context"; 49 + import { translations, useLanguage } from "@/lib/i18n"; 50 + import { useUser, SignOutButton } from "@clerk/nextjs"; 51 + import { useRouter } from "next/navigation"; 52 + import { 53 + decryptPayload, 54 + encryptPayload, 55 + isEncryptedPayload, 56 + } from "@/lib/crypto"; 53 57 import { 54 58 readInMemoryStorage, 55 59 clearEncryptionPassword, ··· 57 61 readEncryptedLocalStorage, 58 62 setEncryptionPassword, 59 63 writeInMemoryStorage, 60 - } from "@/hooks/useLocalStorage" 64 + } from "@/hooks/useLocalStorage"; 61 65 62 - const AUTO_KEY = "auto-backup-enabled" 63 - const BACKUP_STATUS_KEY = "auto-backup-sync-status" 64 - const BACKUP_VERSION = 1 66 + const AUTO_KEY = "auto-backup-enabled"; 67 + const BACKUP_STATUS_KEY = "auto-backup-sync-status"; 68 + const BACKUP_VERSION = 1; 65 69 const BACKUP_KEYS = [ 66 70 "calendar-events", 67 71 "calendar-categories", ··· 77 81 "skip-landing", 78 82 "today-toast", 79 83 "toast-position", 80 - ] 84 + ]; 81 85 82 86 async function apiGet() { 83 - const r = await fetch("/api/blob") 84 - if (r.status === 404) return null 85 - if (!r.ok) throw new Error() 86 - return r.json() 87 + const r = await fetch("/api/blob"); 88 + if (r.status === 404) return null; 89 + if (!r.ok) throw new Error(); 90 + return r.json(); 87 91 } 88 92 89 93 async function apiPost(body: any) { ··· 91 95 method: "POST", 92 96 headers: { "Content-Type": "application/json" }, 93 97 body: JSON.stringify(body), 94 - }) 95 - if (!r.ok) throw new Error() 98 + }); 99 + if (!r.ok) throw new Error(); 96 100 } 97 101 98 102 async function apiDelete() { 99 - const r = await fetch("/api/blob", { method: "DELETE" }) 100 - if (!r.ok) throw new Error() 103 + const r = await fetch("/api/blob", { method: "DELETE" }); 104 + if (!r.ok) throw new Error(); 101 105 } 102 106 103 107 function collectLocalStorage() { 104 - const storage: Record<string, string> = {} 108 + const storage: Record<string, string> = {}; 105 109 BACKUP_KEYS.forEach((key) => { 106 - const value = readInMemoryStorage(key) ?? localStorage.getItem(key) 107 - if (value !== null) storage[key] = value 108 - }) 109 - return storage 110 + const value = readInMemoryStorage(key) ?? localStorage.getItem(key); 111 + if (value !== null) storage[key] = value; 112 + }); 113 + return storage; 110 114 } 111 115 112 - async function applyCloudStorageToMemory(storage: Record<string, string>, password: string) { 116 + async function applyCloudStorageToMemory( 117 + storage: Record<string, string>, 118 + password: string, 119 + ) { 113 120 await Promise.all( 114 121 Object.entries(storage).map(async ([key, value]) => { 115 - let normalized = value 122 + let normalized = value; 116 123 try { 117 - const parsed = JSON.parse(value) 124 + const parsed = JSON.parse(value); 118 125 if (isEncryptedPayload(parsed)) { 119 - normalized = await decryptPayload(password, parsed.ciphertext, parsed.iv) 126 + normalized = await decryptPayload( 127 + password, 128 + parsed.ciphertext, 129 + parsed.iv, 130 + ); 120 131 } 121 132 } catch { 122 - normalized = value 133 + normalized = value; 123 134 } 124 - writeInMemoryStorage(key, normalized) 125 - markEncryptedSnapshot(key, normalized) 135 + writeInMemoryStorage(key, normalized); 136 + markEncryptedSnapshot(key, normalized); 126 137 }), 127 - ) 138 + ); 128 139 } 129 140 130 - export type UserProfileSection = "profile" | "backup" | "key" | "delete" | "signout" 141 + export type UserProfileSection = 142 + | "profile" 143 + | "backup" 144 + | "key" 145 + | "delete" 146 + | "signout"; 131 147 132 148 type UserProfileButtonProps = { 133 - variant?: React.ComponentProps<typeof Button>["variant"] 134 - className?: string 135 - mode?: "dropdown" | "settings" 136 - onNavigateToSettings?: (section: UserProfileSection) => void 137 - onNavigateToView?: (view: "analytics" | "settings") => void 138 - focusSection?: UserProfileSection | null 139 - } 149 + variant?: React.ComponentProps<typeof Button>["variant"]; 150 + className?: string; 151 + mode?: "dropdown" | "settings"; 152 + onNavigateToSettings?: (section: UserProfileSection) => void; 153 + onNavigateToView?: (view: "analytics" | "settings") => void; 154 + focusSection?: UserProfileSection | null; 155 + }; 140 156 141 157 export default function UserProfileButton({ 142 158 variant = "ghost", ··· 146 162 onNavigateToView, 147 163 focusSection = null, 148 164 }: UserProfileButtonProps) { 149 - const [language] = useLanguage() 150 - const t = translations[language] 151 - const { events, calendars, setEvents, setCalendars } = useCalendar() 152 - const { user, isSignedIn } = useUser() 153 - const router = useRouter() 154 - const [atprotoHandle, setAtprotoHandle] = useState("") 155 - const [atprotoDisplayName, setAtprotoDisplayName] = useState("") 156 - const [atprotoAvatar, setAtprotoAvatar] = useState("") 157 - const [atprotoSignedIn, setAtprotoSignedIn] = useState(false) 158 - const isAnySignedIn = isSignedIn || atprotoSignedIn 165 + const [language] = useLanguage(); 166 + const t = translations[language]; 167 + const { events, calendars, setEvents, setCalendars } = useCalendar(); 168 + const { user, isSignedIn } = useUser(); 169 + const router = useRouter(); 170 + const [atprotoHandle, setAtprotoHandle] = useState(""); 171 + const [atprotoDisplayName, setAtprotoDisplayName] = useState(""); 172 + const [atprotoAvatar, setAtprotoAvatar] = useState(""); 173 + const [atprotoSignedIn, setAtprotoSignedIn] = useState(false); 174 + const isAnySignedIn = isSignedIn || atprotoSignedIn; 159 175 160 - const [enabled, setEnabled] = useState(false) 161 - const [profileOpen, setProfileOpen] = useState(false) 162 - const [backupOpen, setBackupOpen] = useState(false) 163 - const [setPwdOpen, setSetPwdOpen] = useState(false) 164 - const [unlockOpen, setUnlockOpen] = useState(false) 165 - const [rotateOpen, setRotateOpen] = useState(false) 166 - const [deleteAccountOpen, setDeleteAccountOpen] = useState(false) 167 - const [deleteCloudOpen, setDeleteCloudOpen] = useState(false) 168 - const [isDeletingAccount, setIsDeletingAccount] = useState(false) 169 - const [isUnlocking, setIsUnlocking] = useState(false) 170 - const [deleteAccountConfirmText, setDeleteAccountConfirmText] = useState("") 171 - const [deleteCloudConfirmText, setDeleteCloudConfirmText] = useState("") 172 - const [profileSection, setProfileSection] = useState<"basic" | "emails" | "oauth">("basic") 176 + const [enabled, setEnabled] = useState(false); 177 + const [profileOpen, setProfileOpen] = useState(false); 178 + const [backupOpen, setBackupOpen] = useState(false); 179 + const [setPwdOpen, setSetPwdOpen] = useState(false); 180 + const [unlockOpen, setUnlockOpen] = useState(false); 181 + const [rotateOpen, setRotateOpen] = useState(false); 182 + const [deleteAccountOpen, setDeleteAccountOpen] = useState(false); 183 + const [deleteCloudOpen, setDeleteCloudOpen] = useState(false); 184 + const [isDeletingAccount, setIsDeletingAccount] = useState(false); 185 + const [isUnlocking, setIsUnlocking] = useState(false); 186 + const [deleteAccountConfirmText, setDeleteAccountConfirmText] = useState(""); 187 + const [deleteCloudConfirmText, setDeleteCloudConfirmText] = useState(""); 188 + const [profileSection, setProfileSection] = useState< 189 + "basic" | "emails" | "oauth" 190 + >("basic"); 173 191 174 - const [password, setPassword] = useState("") 175 - const [confirm, setConfirm] = useState("") 176 - const [oldPassword, setOldPassword] = useState("") 177 - const [error, setError] = useState("") 192 + const [password, setPassword] = useState(""); 193 + const [confirm, setConfirm] = useState(""); 194 + const [oldPassword, setOldPassword] = useState(""); 195 + const [error, setError] = useState(""); 178 196 179 - const [firstName, setFirstName] = useState("") 180 - const [lastName, setLastName] = useState("") 181 - const [newEmail, setNewEmail] = useState("") 182 - const [profileSaving, setProfileSaving] = useState(false) 183 - const [avatarUploading, setAvatarUploading] = useState(false) 197 + const [firstName, setFirstName] = useState(""); 198 + const [lastName, setLastName] = useState(""); 199 + const [newEmail, setNewEmail] = useState(""); 200 + const [profileSaving, setProfileSaving] = useState(false); 201 + const [avatarUploading, setAvatarUploading] = useState(false); 184 202 185 - const keyRef = useRef<string | null>(null) 186 - const restoredRef = useRef(false) 187 - const timerRef = useRef<any>(null) 188 - const [backupTick, setBackupTick] = useState(0) 203 + const keyRef = useRef<string | null>(null); 204 + const restoredRef = useRef(false); 205 + const timerRef = useRef<any>(null); 206 + const [backupTick, setBackupTick] = useState(0); 189 207 190 208 const broadcastBackupStatus = (status: "uploading" | "failed" | "done") => { 191 - localStorage.setItem(BACKUP_STATUS_KEY, status) 192 - window.dispatchEvent(new CustomEvent("backup-status-change", { detail: { status } })) 193 - } 209 + localStorage.setItem(BACKUP_STATUS_KEY, status); 210 + window.dispatchEvent( 211 + new CustomEvent("backup-status-change", { detail: { status } }), 212 + ); 213 + }; 194 214 195 215 useEffect(() => { 196 216 fetch("/api/atproto/session") 197 217 .then((r) => r.json()) 198 - .then((data: { signedIn?: boolean; handle?: string; displayName?: string; avatar?: string }) => { 199 - setAtprotoSignedIn(!!data.signedIn) 200 - setAtprotoHandle(data.handle || "") 201 - setAtprotoDisplayName(data.displayName || "") 202 - setAtprotoAvatar(data.avatar || "") 203 - }) 204 - .catch(() => undefined) 205 - }, []) 218 + .then( 219 + (data: { 220 + signedIn?: boolean; 221 + handle?: string; 222 + displayName?: string; 223 + avatar?: string; 224 + }) => { 225 + setAtprotoSignedIn(!!data.signedIn); 226 + setAtprotoHandle(data.handle || ""); 227 + setAtprotoDisplayName(data.displayName || ""); 228 + setAtprotoAvatar(data.avatar || ""); 229 + }, 230 + ) 231 + .catch(() => undefined); 232 + }, []); 206 233 207 234 useEffect(() => { 208 - if (mode !== "settings" || !focusSection) return 209 - const target = document.getElementById(`settings-account-${focusSection}`) 210 - target?.scrollIntoView({ behavior: "smooth", block: "center" }) 211 - }, [focusSection, mode]) 235 + if (mode !== "settings" || !focusSection) return; 236 + const target = document.getElementById(`settings-account-${focusSection}`); 237 + target?.scrollIntoView({ behavior: "smooth", block: "center" }); 238 + }, [focusSection, mode]); 212 239 213 240 useEffect(() => { 214 241 if (!deleteAccountOpen) { 215 - setDeleteAccountConfirmText("") 242 + setDeleteAccountConfirmText(""); 216 243 } 217 - }, [deleteAccountOpen]) 244 + }, [deleteAccountOpen]); 218 245 219 246 useEffect(() => { 220 - setEnabled(localStorage.getItem(AUTO_KEY) === "true") 221 - }, []) 247 + setEnabled(localStorage.getItem(AUTO_KEY) === "true"); 248 + }, []); 222 249 223 250 useEffect(() => { 224 - if (!user) return 225 - setFirstName(user.firstName || "") 226 - setLastName(user.lastName || "") 227 - }, [user]) 251 + if (!user) return; 252 + setFirstName(user.firstName || ""); 253 + setLastName(user.lastName || ""); 254 + }, [user]); 228 255 229 256 useEffect(() => { 230 - if (mode === "settings") return 231 - if (!isAnySignedIn || keyRef.current || restoredRef.current) return 257 + if (mode === "settings") return; 258 + if (!isAnySignedIn || keyRef.current || restoredRef.current) return; 232 259 apiGet().then((cloud) => { 233 - if (cloud) setUnlockOpen(true) 234 - }) 235 - }, [isAnySignedIn, mode]) 260 + if (cloud) setUnlockOpen(true); 261 + }); 262 + }, [isAnySignedIn, mode]); 236 263 237 264 useEffect(() => { 238 - const watchKeys = new Set(BACKUP_KEYS) 265 + const watchKeys = new Set(BACKUP_KEYS); 239 266 const handleLocalWrite = (event: Event) => { 240 - const customEvent = event as CustomEvent<{ key?: string }> 267 + const customEvent = event as CustomEvent<{ key?: string }>; 241 268 if (!customEvent.detail?.key || watchKeys.has(customEvent.detail.key)) { 242 - setBackupTick((prev) => prev + 1) 269 + setBackupTick((prev) => prev + 1); 243 270 } 244 - } 271 + }; 245 272 246 - window.addEventListener("local-storage-written", handleLocalWrite) 247 - const handleLanguageChange = () => setBackupTick((prev) => prev + 1) 248 - window.addEventListener("languagechange", handleLanguageChange) 273 + window.addEventListener("local-storage-written", handleLocalWrite); 274 + const handleLanguageChange = () => setBackupTick((prev) => prev + 1); 275 + window.addEventListener("languagechange", handleLanguageChange); 249 276 return () => { 250 - window.removeEventListener("local-storage-written", handleLocalWrite) 251 - window.removeEventListener("languagechange", handleLanguageChange) 252 - } 253 - }, []) 277 + window.removeEventListener("local-storage-written", handleLocalWrite); 278 + window.removeEventListener("languagechange", handleLanguageChange); 279 + }; 280 + }, []); 254 281 255 282 useEffect(() => { 256 - if (!enabled || !keyRef.current || !restoredRef.current) return 257 - if (timerRef.current) clearTimeout(timerRef.current) 283 + if (!enabled || !keyRef.current || !restoredRef.current) return; 284 + if (timerRef.current) clearTimeout(timerRef.current); 258 285 259 286 timerRef.current = setTimeout(async () => { 260 287 try { 261 - broadcastBackupStatus("uploading") 288 + broadcastBackupStatus("uploading"); 262 289 const payload = await encryptPayload( 263 290 keyRef.current!, 264 291 JSON.stringify({ v: BACKUP_VERSION, storage: collectLocalStorage() }), 265 - ) 266 - await apiPost(payload) 267 - broadcastBackupStatus("done") 292 + ); 293 + await apiPost(payload); 294 + broadcastBackupStatus("done"); 268 295 } catch { 269 - broadcastBackupStatus("failed") 296 + broadcastBackupStatus("failed"); 270 297 } finally { 271 - timerRef.current = null 298 + timerRef.current = null; 272 299 } 273 - }, 800) 274 - }, [events, calendars, enabled, backupTick]) 300 + }, 800); 301 + }, [events, calendars, enabled, backupTick]); 275 302 276 303 async function saveProfile() { 277 - if (!user) return 304 + if (!user) return; 278 305 try { 279 - setProfileSaving(true) 306 + setProfileSaving(true); 280 307 await user.update({ 281 308 firstName: firstName || null, 282 309 lastName: lastName || null, 283 - }) 284 - toast(t.profileUpdated) 310 + }); 311 + toast(t.profileUpdated); 285 312 } catch (e: any) { 286 313 toast(t.profileUpdateFailed, { 287 314 description: e?.errors?.[0]?.longMessage || e?.message || "", 288 - }) 315 + }); 289 316 } finally { 290 - setProfileSaving(false) 317 + setProfileSaving(false); 291 318 } 292 319 } 293 320 294 321 async function updateAvatar(file?: File | null) { 295 - if (!user || !file) return 322 + if (!user || !file) return; 296 323 try { 297 - setAvatarUploading(true) 298 - await user.setProfileImage({ file }) 299 - await user.reload() 300 - toast(t.avatarUpdated) 324 + setAvatarUploading(true); 325 + await user.setProfileImage({ file }); 326 + await user.reload(); 327 + toast(t.avatarUpdated); 301 328 } catch (e: any) { 302 329 toast(t.avatarUpdateFailed, { 303 330 description: e?.errors?.[0]?.longMessage || e?.message || "", 304 - }) 331 + }); 305 332 } finally { 306 - setAvatarUploading(false) 333 + setAvatarUploading(false); 307 334 } 308 335 } 309 336 310 337 async function addEmailAddress() { 311 - if (!user || !newEmail) return 338 + if (!user || !newEmail) return; 312 339 try { 313 - const email = await user.createEmailAddress({ email: newEmail }) 314 - await email.prepareVerification({ strategy: "email_code" }) 315 - setNewEmail("") 316 - toast(t.emailAddedCheckInbox) 317 - await user.reload() 340 + const email = await user.createEmailAddress({ email: newEmail }); 341 + await email.prepareVerification({ strategy: "email_code" }); 342 + setNewEmail(""); 343 + toast(t.emailAddedCheckInbox); 344 + await user.reload(); 318 345 } catch (e: any) { 319 346 toast(t.addEmailFailed, { 320 347 description: e?.errors?.[0]?.longMessage || e?.message || "", 321 - }) 348 + }); 322 349 } 323 350 } 324 351 325 352 async function setPrimaryEmail(emailId: string) { 326 - if (!user) return 353 + if (!user) return; 327 354 try { 328 - await user.update({ primaryEmailAddressId: emailId }) 329 - toast(t.primaryEmailUpdated) 330 - await user.reload() 355 + await user.update({ primaryEmailAddressId: emailId }); 356 + toast(t.primaryEmailUpdated); 357 + await user.reload(); 331 358 } catch (e: any) { 332 359 toast(t.primaryEmailUpdateFailed, { 333 360 description: e?.errors?.[0]?.longMessage || e?.message || "", 334 - }) 361 + }); 335 362 } 336 363 } 337 364 338 365 async function unlinkOAuth(accountId: string) { 339 - if (!user) return 366 + if (!user) return; 340 367 try { 341 - const target = user.externalAccounts.find((acc) => acc.id === accountId) 342 - if (!target) return 343 - await target.destroy() 344 - toast(t.oauthDisconnected) 345 - await user.reload() 368 + const target = user.externalAccounts.find((acc) => acc.id === accountId); 369 + if (!target) return; 370 + await target.destroy(); 371 + toast(t.oauthDisconnected); 372 + await user.reload(); 346 373 } catch (e: any) { 347 374 toast(t.disconnectFailed, { 348 375 description: e?.errors?.[0]?.longMessage || e?.message || "", 349 - }) 376 + }); 350 377 } 351 378 } 352 379 353 - 354 - 355 380 const hydrateEvent = (raw: any): CalendarEvent => { 356 - const startDate = raw?.startDate ? new Date(raw.startDate) : new Date() 357 - const endDate = raw?.endDate ? new Date(raw.endDate) : new Date(startDate.getTime() + 60 * 60 * 1000) 381 + const startDate = raw?.startDate ? new Date(raw.startDate) : new Date(); 382 + const endDate = raw?.endDate 383 + ? new Date(raw.endDate) 384 + : new Date(startDate.getTime() + 60 * 60 * 1000); 358 385 359 386 return { 360 387 id: raw?.id || `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, 361 388 title: raw?.title || t.unnamedEvent, 362 389 startDate, 363 - endDate: endDate < startDate ? new Date(startDate.getTime() + 60 * 60 * 1000) : endDate, 390 + endDate: 391 + endDate < startDate 392 + ? new Date(startDate.getTime() + 60 * 60 * 1000) 393 + : endDate, 364 394 isAllDay: Boolean(raw?.isAllDay), 365 - recurrence: ["none", "daily", "weekly", "monthly", "yearly"].includes(raw?.recurrence) 395 + recurrence: ["none", "daily", "weekly", "monthly", "yearly"].includes( 396 + raw?.recurrence, 397 + ) 366 398 ? raw.recurrence 367 399 : "none", 368 400 location: raw?.location, 369 401 participants: Array.isArray(raw?.participants) ? raw.participants : [], 370 - notification: typeof raw?.notification === "number" ? raw.notification : 0, 402 + notification: 403 + typeof raw?.notification === "number" ? raw.notification : 0, 371 404 description: raw?.description, 372 405 color: raw?.color || "bg-blue-500", 373 406 calendarId: raw?.calendarId || "1", 374 - } 375 - } 407 + }; 408 + }; 376 409 377 410 async function unlock() { 378 - if (!password) return 411 + if (!password) return; 379 412 380 413 try { 381 - setIsUnlocking(true) 382 - const cloud = await apiGet() 383 - if (!cloud) return 414 + setIsUnlocking(true); 415 + const cloud = await apiGet(); 416 + if (!cloud) return; 384 417 385 - let plain 418 + let plain; 386 419 try { 387 - plain = await decryptPayload(password, cloud.ciphertext, cloud.iv) 420 + plain = await decryptPayload(password, cloud.ciphertext, cloud.iv); 388 421 } catch { 389 - toast(t.incorrectPassword) 390 - return 422 + toast(t.incorrectPassword); 423 + return; 391 424 } 392 425 393 426 try { 394 - const data = JSON.parse(plain) 427 + const data = JSON.parse(plain); 395 428 if (data?.storage) { 396 - await applyCloudStorageToMemory(data.storage, password) 429 + await applyCloudStorageToMemory(data.storage, password); 397 430 } else if (data?.events || data?.calendars) { 398 - const fallbackStorage: Record<string, string> = {} 399 - if (data?.events) fallbackStorage["calendar-events"] = JSON.stringify(data.events) 400 - if (data?.calendars) fallbackStorage["calendar-categories"] = JSON.stringify(data.calendars) 401 - await applyCloudStorageToMemory(fallbackStorage, password) 431 + const fallbackStorage: Record<string, string> = {}; 432 + if (data?.events) 433 + fallbackStorage["calendar-events"] = JSON.stringify(data.events); 434 + if (data?.calendars) 435 + fallbackStorage["calendar-categories"] = JSON.stringify( 436 + data.calendars, 437 + ); 438 + await applyCloudStorageToMemory(fallbackStorage, password); 402 439 } 403 - await setEncryptionPassword(password) 404 - const restoredEvents = await readEncryptedLocalStorage("calendar-events", []) 405 - const restoredCalendars = await readEncryptedLocalStorage("calendar-categories", []) 406 - const restoredLanguage = await readEncryptedLocalStorage<string | null>("preferred-language", null) 407 - setEvents(restoredEvents) 408 - setCalendars(restoredCalendars) 440 + await setEncryptionPassword(password); 441 + const restoredEvents = await readEncryptedLocalStorage( 442 + "calendar-events", 443 + [], 444 + ); 445 + const restoredCalendars = await readEncryptedLocalStorage( 446 + "calendar-categories", 447 + [], 448 + ); 449 + const restoredLanguage = await readEncryptedLocalStorage<string | null>( 450 + "preferred-language", 451 + null, 452 + ); 453 + setEvents(restoredEvents); 454 + setCalendars(restoredCalendars); 409 455 if (restoredLanguage) { 410 456 window.dispatchEvent( 411 - new CustomEvent("languagechange", { detail: { language: restoredLanguage } }), 412 - ) 457 + new CustomEvent("languagechange", { 458 + detail: { language: restoredLanguage }, 459 + }), 460 + ); 413 461 } 414 462 } catch {} 415 463 416 - keyRef.current = password 417 - restoredRef.current = true 418 - localStorage.setItem(AUTO_KEY, "true") 419 - setEnabled(true) 420 - broadcastBackupStatus("done") 464 + keyRef.current = password; 465 + restoredRef.current = true; 466 + localStorage.setItem(AUTO_KEY, "true"); 467 + setEnabled(true); 468 + broadcastBackupStatus("done"); 421 469 422 - setPassword("") 423 - setUnlockOpen(false) 424 - toast(t.dataRestoredAutoBackupEnabled) 470 + setPassword(""); 471 + setUnlockOpen(false); 472 + toast(t.dataRestoredAutoBackupEnabled); 425 473 } finally { 426 - setIsUnlocking(false) 474 + setIsUnlocking(false); 427 475 } 428 476 } 429 477 430 478 async function enable() { 431 479 if (password !== confirm) { 432 - setError(t.passwordsDoNotMatch) 433 - return 480 + setError(t.passwordsDoNotMatch); 481 + return; 434 482 } 435 - await setEncryptionPassword(password) 436 - const payload = await encryptPayload(password, JSON.stringify({ v: BACKUP_VERSION, storage: collectLocalStorage() })) 437 - await apiPost(payload) 438 - localStorage.setItem(AUTO_KEY, "true") 439 - keyRef.current = password 440 - restoredRef.current = true 441 - setEnabled(true) 442 - broadcastBackupStatus("done") 443 - setPassword("") 444 - setConfirm("") 445 - setSetPwdOpen(false) 446 - toast(t.autoBackupEnabled) 483 + await setEncryptionPassword(password); 484 + const payload = await encryptPayload( 485 + password, 486 + JSON.stringify({ v: BACKUP_VERSION, storage: collectLocalStorage() }), 487 + ); 488 + await apiPost(payload); 489 + localStorage.setItem(AUTO_KEY, "true"); 490 + keyRef.current = password; 491 + restoredRef.current = true; 492 + setEnabled(true); 493 + broadcastBackupStatus("done"); 494 + setPassword(""); 495 + setConfirm(""); 496 + setSetPwdOpen(false); 497 + toast(t.autoBackupEnabled); 447 498 } 448 499 449 500 async function rotate() { 450 501 if (password !== confirm) { 451 - setError(t.passwordsDoNotMatch) 452 - return 502 + setError(t.passwordsDoNotMatch); 503 + return; 453 504 } 454 - const cloud = await apiGet() 455 - if (!cloud) return 505 + const cloud = await apiGet(); 506 + if (!cloud) return; 456 507 457 508 try { 458 - await decryptPayload(oldPassword, cloud.ciphertext, cloud.iv) 509 + await decryptPayload(oldPassword, cloud.ciphertext, cloud.iv); 459 510 } catch { 460 - toast(t.incorrectOldPassword) 461 - return 511 + toast(t.incorrectOldPassword); 512 + return; 462 513 } 463 514 464 - const next = await encryptPayload(password, JSON.stringify({ v: BACKUP_VERSION, storage: collectLocalStorage() })) 465 - await apiPost(next) 466 - await setEncryptionPassword(password) 467 - keyRef.current = password 468 - setRotateOpen(false) 469 - setOldPassword("") 470 - setPassword("") 471 - setConfirm("") 472 - toast(t.encryptionKeyUpdated) 515 + const next = await encryptPayload( 516 + password, 517 + JSON.stringify({ v: BACKUP_VERSION, storage: collectLocalStorage() }), 518 + ); 519 + await apiPost(next); 520 + await setEncryptionPassword(password); 521 + keyRef.current = password; 522 + setRotateOpen(false); 523 + setOldPassword(""); 524 + setPassword(""); 525 + setConfirm(""); 526 + toast(t.encryptionKeyUpdated); 473 527 } 474 528 475 529 function disableAutoBackup() { 476 - localStorage.removeItem(AUTO_KEY) 477 - localStorage.removeItem(BACKUP_STATUS_KEY) 478 - keyRef.current = null 479 - restoredRef.current = false 480 - setEnabled(false) 481 - clearEncryptionPassword() 482 - toast(t.autoBackupDisabled) 530 + localStorage.removeItem(AUTO_KEY); 531 + localStorage.removeItem(BACKUP_STATUS_KEY); 532 + keyRef.current = null; 533 + restoredRef.current = false; 534 + setEnabled(false); 535 + clearEncryptionPassword(); 536 + toast(t.autoBackupDisabled); 483 537 } 484 538 485 539 async function destroy() { 486 - await apiDelete() 487 - localStorage.removeItem(AUTO_KEY) 488 - localStorage.removeItem(BACKUP_STATUS_KEY) 489 - keyRef.current = null 490 - restoredRef.current = false 491 - setEnabled(false) 492 - toast(t.cloudDataDeleted) 540 + await apiDelete(); 541 + localStorage.removeItem(AUTO_KEY); 542 + localStorage.removeItem(BACKUP_STATUS_KEY); 543 + keyRef.current = null; 544 + restoredRef.current = false; 545 + setEnabled(false); 546 + toast(t.cloudDataDeleted); 493 547 } 494 548 495 549 const openProfileSection = (section: "basic" | "emails" | "oauth") => { 496 - setProfileSection(section) 497 - setProfileOpen(true) 498 - } 550 + setProfileSection(section); 551 + setProfileOpen(true); 552 + }; 499 553 500 554 async function deleteAccount() { 501 - if (!user || deleteAccountConfirmText !== "DELETE MY ACCOUNT") return 555 + if (!user || deleteAccountConfirmText !== "DELETE MY ACCOUNT") return; 502 556 try { 503 - setIsDeletingAccount(true) 504 - const response = await fetch("/api/account", { method: "DELETE" }) 557 + setIsDeletingAccount(true); 558 + const response = await fetch("/api/account", { method: "DELETE" }); 505 559 if (!response.ok) { 506 - const data = await response.json().catch(() => ({})) 507 - throw new Error(data?.error || "Failed to delete account data") 560 + const data = await response.json().catch(() => ({})); 561 + throw new Error(data?.error || "Failed to delete account data"); 508 562 } 509 563 510 - await user.delete() 564 + await user.delete(); 511 565 512 - toast(t.accountDeleted) 513 - router.replace("/") 566 + toast(t.accountDeleted); 567 + router.replace("/"); 514 568 } catch (e: any) { 515 569 toast(t.deleteAccountFailed, { 516 570 description: e?.message || "", 517 - }) 571 + }); 518 572 } finally { 519 - setIsDeletingAccount(false) 520 - setDeleteAccountOpen(false) 573 + setIsDeletingAccount(false); 574 + setDeleteAccountOpen(false); 521 575 } 522 576 } 523 577 ··· 526 580 {mode === "dropdown" ? ( 527 581 <DropdownMenu> 528 582 <DropdownMenuTrigger asChild> 529 - {(isSignedIn && user?.imageUrl) || (!isSignedIn && atprotoSignedIn && atprotoAvatar) ? ( 530 - <Button variant="ghost" size="icon" className="rounded-full overflow-hidden h-8 w-8 p-0"> 583 + {(isSignedIn && user?.imageUrl) || 584 + (!isSignedIn && atprotoSignedIn && atprotoAvatar) ? ( 585 + <Button 586 + variant="ghost" 587 + size="icon" 588 + className="rounded-full overflow-hidden h-8 w-8 p-0" 589 + > 531 590 <img 532 591 src={isSignedIn ? user.imageUrl : atprotoAvatar} 533 592 alt="avatar" ··· 539 598 /> 540 599 </Button> 541 600 ) : ( 542 - <Button variant={variant} size="icon" className={`rounded-full h-10 w-10 ${className}`}> 601 + <Button 602 + variant={variant} 603 + size="icon" 604 + className={`rounded-full h-10 w-10 ${className}`} 605 + > 543 606 <User className="h-5 w-5" /> 544 607 </Button> 545 608 )} ··· 548 611 <DropdownMenuContent align="end"> 549 612 {!isAnySignedIn ? ( 550 613 <> 551 - <DropdownMenuItem onClick={() => router.push("/sign-in")}>{t.signIn}</DropdownMenuItem> 552 - <DropdownMenuItem onClick={() => router.push("/sign-up")}>{t.signUp}</DropdownMenuItem> 614 + <DropdownMenuItem onClick={() => router.push("/sign-in")}> 615 + {t.signIn} 616 + </DropdownMenuItem> 617 + <DropdownMenuItem onClick={() => router.push("/sign-up")}> 618 + {t.signUp} 619 + </DropdownMenuItem> 553 620 </> 554 621 ) : null} 555 622 <DropdownMenuItem onClick={() => onNavigateToView?.("settings")}> ··· 577 644 referrerPolicy="no-referrer" 578 645 /> 579 646 <div className="min-w-0"> 580 - <p className="font-medium truncate">{[user?.firstName, user?.lastName].filter(Boolean).join(" ") || user?.username || atprotoDisplayName || atprotoHandle || "User"}</p> 581 - <p className="text-sm text-muted-foreground truncate">{user?.primaryEmailAddress?.emailAddress || (atprotoHandle ? `@${atprotoHandle}` : "")}</p> 647 + <p className="font-medium truncate"> 648 + {[user?.firstName, user?.lastName] 649 + .filter(Boolean) 650 + .join(" ") || 651 + user?.username || 652 + atprotoDisplayName || 653 + atprotoHandle || 654 + "User"} 655 + </p> 656 + <p className="text-sm text-muted-foreground truncate"> 657 + {user?.primaryEmailAddress?.emailAddress || 658 + (atprotoHandle ? `@${atprotoHandle}` : "")} 659 + </p> 582 660 </div> 583 661 </div> 584 662 <div className="space-y-4"> ··· 586 664 <> 587 665 <div className="space-y-3 rounded-md border p-3"> 588 666 <p className="text-sm font-semibold">{t.basicInfo}</p> 589 - <p className="text-xs text-muted-foreground">{t.editProfileDescription}</p> 590 - <Button id="settings-account-profile" variant="outline" onClick={() => openProfileSection("basic")}><CircleUser className="h-4 w-4 mr-2" />{t.openBasicInfo}</Button> 667 + <p className="text-xs text-muted-foreground"> 668 + {t.editProfileDescription} 669 + </p> 670 + <Button 671 + id="settings-account-profile" 672 + variant="outline" 673 + onClick={() => openProfileSection("basic")} 674 + > 675 + <CircleUser className="h-4 w-4 mr-2" /> 676 + {t.openBasicInfo} 677 + </Button> 591 678 </div> 592 679 593 680 <div className="space-y-3 rounded-md border p-3"> 594 - <p className="text-sm font-semibold">{t.emailManagement}</p> 595 - <p className="text-xs text-muted-foreground">{t.manageEmailAddressesDescription}</p> 596 - <Button variant="outline" onClick={() => openProfileSection("emails")}><Mail className="h-4 w-4 mr-2" />{t.openEmailSettings}</Button> 681 + <p className="text-sm font-semibold"> 682 + {t.emailManagement} 683 + </p> 684 + <p className="text-xs text-muted-foreground"> 685 + {t.manageEmailAddressesDescription} 686 + </p> 687 + <Button 688 + variant="outline" 689 + onClick={() => openProfileSection("emails")} 690 + > 691 + <Mail className="h-4 w-4 mr-2" /> 692 + {t.openEmailSettings} 693 + </Button> 597 694 </div> 598 695 599 696 <div className="space-y-3 rounded-md border p-3"> 600 697 <p className="text-sm font-semibold">OAuth</p> 601 - <p className="text-xs text-muted-foreground">{t.manageOauthDescription}</p> 602 - <Button variant="outline" onClick={() => openProfileSection("oauth")}><LinkIcon className="h-4 w-4 mr-2" />{t.openOauthSettings}</Button> 698 + <p className="text-xs text-muted-foreground"> 699 + {t.manageOauthDescription} 700 + </p> 701 + <Button 702 + variant="outline" 703 + onClick={() => openProfileSection("oauth")} 704 + > 705 + <LinkIcon className="h-4 w-4 mr-2" /> 706 + {t.openOauthSettings} 707 + </Button> 603 708 </div> 604 709 </> 605 710 ) : null} 606 711 607 712 <div className="space-y-3 rounded-md border p-3"> 608 713 <p className="text-sm font-semibold">{t.autoBackup}</p> 609 - <p className="text-xs text-muted-foreground">{t.autoBackupHelp}</p> 610 - <Button id="settings-account-backup" variant="outline" onClick={() => setBackupOpen(true)}><CloudUpload className="h-4 w-4 mr-2" />{t.openBackupSettings}</Button> 714 + <p className="text-xs text-muted-foreground"> 715 + {t.autoBackupHelp} 716 + </p> 717 + <Button 718 + id="settings-account-backup" 719 + variant="outline" 720 + onClick={() => setBackupOpen(true)} 721 + > 722 + <CloudUpload className="h-4 w-4 mr-2" /> 723 + {t.openBackupSettings} 724 + </Button> 611 725 </div> 612 726 613 727 <div className="space-y-3 rounded-md border p-3"> 614 728 <p className="text-sm font-semibold">{t.changeKey}</p> 615 - <p className="text-xs text-muted-foreground">{t.rotateKeyHelp}</p> 616 - <Button id="settings-account-key" variant="outline" onClick={() => setRotateOpen(true)}><KeyRound className="h-4 w-4 mr-2" />{t.changeEncryptionKeyAction}</Button> 729 + <p className="text-xs text-muted-foreground"> 730 + {t.rotateKeyHelp} 731 + </p> 732 + <Button 733 + id="settings-account-key" 734 + variant="outline" 735 + onClick={() => setRotateOpen(true)} 736 + > 737 + <KeyRound className="h-4 w-4 mr-2" /> 738 + {t.changeEncryptionKeyAction} 739 + </Button> 617 740 </div> 618 741 619 742 <div className="space-y-3 rounded-md border p-3"> 620 743 <p className="text-sm font-semibold">{t.signOut}</p> 621 - <p className="text-xs text-muted-foreground">{t.signOutHelp}</p> 744 + <p className="text-xs text-muted-foreground"> 745 + {t.signOutHelp} 746 + </p> 622 747 {isSignedIn ? ( 623 - <SignOutButton> 624 - <Button id="settings-account-signout" variant="outline"><LogOut className="h-4 w-4 mr-2" />{t.signOut}</Button> 625 - </SignOutButton> 748 + <SignOutButton> 749 + <Button id="settings-account-signout" variant="outline"> 750 + <LogOut className="h-4 w-4 mr-2" /> 751 + {t.signOut} 752 + </Button> 753 + </SignOutButton> 626 754 ) : ( 627 - <Button id="settings-account-signout" variant="outline" onClick={async () => { 628 - await fetch("/api/atproto/logout", { method: "POST" }) 629 - setAtprotoSignedIn(false) 630 - setAtprotoHandle("") 631 - setAtprotoDisplayName("") 632 - setAtprotoAvatar("") 633 - router.refresh() 634 - }}><LogOut className="h-4 w-4 mr-2" />{t.signOut}</Button> 755 + <Button 756 + id="settings-account-signout" 757 + variant="outline" 758 + onClick={async () => { 759 + await fetch("/api/atproto/logout", { method: "POST" }); 760 + setAtprotoSignedIn(false); 761 + setAtprotoHandle(""); 762 + setAtprotoDisplayName(""); 763 + setAtprotoAvatar(""); 764 + router.refresh(); 765 + }} 766 + > 767 + <LogOut className="h-4 w-4 mr-2" /> 768 + {t.signOut} 769 + </Button> 635 770 )} 636 771 </div> 637 772 638 773 <div className="rounded-md border border-destructive/70 p-3 space-y-3 bg-destructive/5"> 639 - <p className="text-sm font-semibold text-destructive">{t.dangerZone}</p> 774 + <p className="text-sm font-semibold text-destructive"> 775 + {t.dangerZone} 776 + </p> 640 777 <div className="space-y-3 rounded-md border border-destructive/50 p-3"> 641 - <p className="text-sm font-semibold text-destructive">{t.deleteData}</p> 642 - <p className="text-xs text-muted-foreground">{t.deleteAccountDataHelp}</p> 643 - <Button id="settings-account-delete" variant="destructive" onClick={() => setDeleteCloudOpen(true)}><Trash2 className="h-4 w-4 mr-2" />{t.deleteData}</Button> 644 - </div> 645 - {isSignedIn ? ( 646 - <div className="space-y-3 rounded-md border border-destructive/50 p-3"> 647 - <p className="text-sm font-semibold text-destructive">{t.deleteAccount}</p> 648 - <p className="text-xs text-muted-foreground">{t.deleteAccountPermanentHelp}</p> 649 - <Button variant="destructive" onClick={() => setDeleteAccountOpen(true)}> 778 + <p className="text-sm font-semibold text-destructive"> 779 + {t.deleteData} 780 + </p> 781 + <p className="text-xs text-muted-foreground"> 782 + {t.deleteAccountDataHelp} 783 + </p> 784 + <Button 785 + id="settings-account-delete" 786 + variant="destructive" 787 + onClick={() => setDeleteCloudOpen(true)} 788 + > 650 789 <Trash2 className="h-4 w-4 mr-2" /> 651 - {t.deleteAccount} 790 + {t.deleteData} 652 791 </Button> 653 792 </div> 793 + {isSignedIn ? ( 794 + <div className="space-y-3 rounded-md border border-destructive/50 p-3"> 795 + <p className="text-sm font-semibold text-destructive"> 796 + {t.deleteAccount} 797 + </p> 798 + <p className="text-xs text-muted-foreground"> 799 + {t.deleteAccountPermanentHelp} 800 + </p> 801 + <Button 802 + variant="destructive" 803 + onClick={() => setDeleteAccountOpen(true)} 804 + > 805 + <Trash2 className="h-4 w-4 mr-2" /> 806 + {t.deleteAccount} 807 + </Button> 808 + </div> 654 809 ) : null} 655 810 </div> 656 811 </div> 657 812 </> 658 813 ) : ( 659 814 <div className="grid gap-2 sm:grid-cols-2"> 660 - <Button variant="outline" onClick={() => router.push("/sign-in")}>{t.signIn}</Button> 661 - <Button onClick={() => router.push("/sign-up")}>{t.signUp}</Button> 815 + <Button variant="outline" onClick={() => router.push("/sign-in")}> 816 + {t.signIn} 817 + </Button> 818 + <Button onClick={() => router.push("/sign-up")}> 819 + {t.signUp} 820 + </Button> 662 821 </div> 663 822 )} 664 823 </div> ··· 668 827 <DialogContent className="sm:max-w-2xl"> 669 828 <DialogHeader> 670 829 <DialogTitle>{t.profile}</DialogTitle> 671 - <DialogDescription> 672 - {t.manageProfileDescription} 673 - </DialogDescription> 830 + <DialogDescription>{t.manageProfileDescription}</DialogDescription> 674 831 </DialogHeader> 675 832 676 833 <ScrollArea className="max-h-[70vh] pr-4"> 677 834 <div className="space-y-6 py-1"> 678 - <section className="space-y-3 rounded-lg border p-4" hidden={profileSection !== "basic"}> 835 + <section 836 + className="space-y-3 rounded-lg border p-4" 837 + hidden={profileSection !== "basic"} 838 + > 679 839 <h3 className="font-medium">{t.basicInfo}</h3> 680 840 <div className="space-y-2"> 681 841 <Label>{t.avatar}</Label> 682 842 <div className="flex items-center gap-3"> 683 843 <img 684 - src={user?.imageUrl || atprotoAvatar || "/placeholder.svg"} 844 + src={ 845 + user?.imageUrl || atprotoAvatar || "/placeholder.svg" 846 + } 685 847 alt="avatar" 686 848 width={52} 687 849 height={52} ··· 703 865 className="hidden" 704 866 disabled={avatarUploading} 705 867 onChange={(e) => { 706 - void updateAvatar(e.target.files?.[0]) 707 - e.currentTarget.value = "" 868 + void updateAvatar(e.target.files?.[0]); 869 + e.currentTarget.value = ""; 708 870 }} 709 871 /> 710 872 </div> ··· 712 874 <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> 713 875 <div className="space-y-2"> 714 876 <Label>{t.firstName}</Label> 715 - <Input value={firstName} onChange={(e) => setFirstName(e.target.value)} /> 877 + <Input 878 + value={firstName} 879 + onChange={(e) => setFirstName(e.target.value)} 880 + /> 716 881 </div> 717 882 <div className="space-y-2"> 718 883 <Label>{t.lastName}</Label> 719 - <Input value={lastName} onChange={(e) => setLastName(e.target.value)} /> 884 + <Input 885 + value={lastName} 886 + onChange={(e) => setLastName(e.target.value)} 887 + /> 720 888 </div> 721 889 </div> 722 890 <Button onClick={saveProfile} disabled={profileSaving}> ··· 724 892 </Button> 725 893 </section> 726 894 727 - <section className="space-y-3 rounded-lg border p-4" hidden={profileSection !== "emails"}> 728 - <h3 className="font-medium flex items-center gap-2"><Mail className="h-4 w-4" />{t.emails}</h3> 895 + <section 896 + className="space-y-3 rounded-lg border p-4" 897 + hidden={profileSection !== "emails"} 898 + > 899 + <h3 className="font-medium flex items-center gap-2"> 900 + <Mail className="h-4 w-4" /> 901 + {t.emails} 902 + </h3> 729 903 <div className="space-y-2"> 730 904 {(user?.emailAddresses || []).map((email) => ( 731 - <div key={email.id} className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"> 905 + <div 906 + key={email.id} 907 + className="flex items-center justify-between rounded-md border px-3 py-2 text-sm" 908 + > 732 909 <div> 733 910 <p className="font-medium">{email.emailAddress}</p> 734 911 <p className="text-muted-foreground text-xs"> ··· 741 918 </p> 742 919 </div> 743 920 {user?.primaryEmailAddressId !== email.id && ( 744 - <Button variant="outline" size="sm" onClick={() => setPrimaryEmail(email.id)}> 921 + <Button 922 + variant="outline" 923 + size="sm" 924 + onClick={() => setPrimaryEmail(email.id)} 925 + > 745 926 {t.setPrimary} 746 927 </Button> 747 928 )} ··· 759 940 </div> 760 941 </section> 761 942 762 - <section className="space-y-3 rounded-lg border p-4" hidden={profileSection !== "oauth"}> 763 - <h3 className="font-medium flex items-center gap-2"><LinkIcon className="h-4 w-4" />OAuth</h3> 943 + <section 944 + className="space-y-3 rounded-lg border p-4" 945 + hidden={profileSection !== "oauth"} 946 + > 947 + <h3 className="font-medium flex items-center gap-2"> 948 + <LinkIcon className="h-4 w-4" /> 949 + OAuth 950 + </h3> 764 951 {(user?.externalAccounts || []).length === 0 ? ( 765 952 <p className="text-sm text-muted-foreground"> 766 953 {t.noConnectedOauthAccounts} ··· 768 955 ) : ( 769 956 <div className="space-y-2"> 770 957 {(user?.externalAccounts || []).map((account) => ( 771 - <div key={account.id} className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"> 958 + <div 959 + key={account.id} 960 + className="flex items-center justify-between rounded-md border px-3 py-2 text-sm" 961 + > 772 962 <div> 773 963 <p className="font-medium">{account.provider}</p> 774 - <p className="text-muted-foreground text-xs">{account.emailAddress || account.username || "-"}</p> 964 + <p className="text-muted-foreground text-xs"> 965 + {account.emailAddress || account.username || "-"} 966 + </p> 775 967 </div> 776 - <Button variant="outline" size="sm" onClick={() => unlinkOAuth(account.id)}> 968 + <Button 969 + variant="outline" 970 + size="sm" 971 + onClick={() => unlinkOAuth(account.id)} 972 + > 777 973 {t.disconnect} 778 974 </Button> 779 975 </div> ··· 799 995 </AlertDialogDescription> 800 996 </AlertDialogHeader> 801 997 <div className="space-y-2"> 802 - <Label htmlFor="delete-account-confirm-input">DELETE MY ACCOUNT</Label> 998 + <Label htmlFor="delete-account-confirm-input"> 999 + DELETE MY ACCOUNT 1000 + </Label> 803 1001 <Input 804 1002 id="delete-account-confirm-input" 805 1003 value={deleteAccountConfirmText} ··· 813 1011 <AlertDialogAction 814 1012 className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 815 1013 onClick={(e) => { 816 - e.preventDefault() 817 - void deleteAccount() 1014 + e.preventDefault(); 1015 + void deleteAccount(); 818 1016 }} 819 - disabled={isDeletingAccount || deleteAccountConfirmText !== "DELETE MY ACCOUNT"} 1017 + disabled={ 1018 + isDeletingAccount || 1019 + deleteAccountConfirmText !== "DELETE MY ACCOUNT" 1020 + } 820 1021 > 821 1022 {isDeletingAccount ? t.deleting : t.confirmDeleteAccount} 822 1023 </AlertDialogAction> ··· 824 1025 </AlertDialogContent> 825 1026 </AlertDialog> 826 1027 827 - 828 1028 <AlertDialog open={deleteCloudOpen} onOpenChange={setDeleteCloudOpen}> 829 1029 <AlertDialogContent> 830 1030 <AlertDialogHeader> 831 1031 <AlertDialogTitle>{t.deleteCloudConfirmTitle}</AlertDialogTitle> 832 - <AlertDialogDescription>{t.deleteCloudConfirmDescription}</AlertDialogDescription> 1032 + <AlertDialogDescription> 1033 + {t.deleteCloudConfirmDescription} 1034 + </AlertDialogDescription> 833 1035 </AlertDialogHeader> 834 1036 <div className="space-y-2"> 835 - <Label htmlFor="delete-cloud-confirm-input">DELETE CLOUD DATA</Label> 1037 + <Label htmlFor="delete-cloud-confirm-input"> 1038 + DELETE CLOUD DATA 1039 + </Label> 836 1040 <Input 837 1041 id="delete-cloud-confirm-input" 838 1042 value={deleteCloudConfirmText} ··· 846 1050 <AlertDialogAction 847 1051 className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 848 1052 onClick={(e) => { 849 - e.preventDefault() 850 - if (deleteCloudConfirmText !== "DELETE CLOUD DATA") return 1053 + e.preventDefault(); 1054 + if (deleteCloudConfirmText !== "DELETE CLOUD DATA") return; 851 1055 void destroy().finally(() => { 852 - setDeleteCloudOpen(false) 853 - setDeleteCloudConfirmText("") 854 - }) 1056 + setDeleteCloudOpen(false); 1057 + setDeleteCloudConfirmText(""); 1058 + }); 855 1059 }} 856 1060 disabled={deleteCloudConfirmText !== "DELETE CLOUD DATA"} 857 1061 > ··· 864 1068 <DialogContent> 865 1069 <DialogHeader> 866 1070 <DialogTitle>{t.autoBackup}</DialogTitle> 867 - <DialogDescription>{enabled ? t.autoBackupStatusEnabled : t.autoBackupStatusDisabled}</DialogDescription> 1071 + <DialogDescription> 1072 + {enabled ? t.autoBackupStatusEnabled : t.autoBackupStatusDisabled} 1073 + </DialogDescription> 868 1074 </DialogHeader> 869 1075 <DialogFooter> 870 1076 {enabled ? ( 871 - <Button variant="destructive" onClick={disableAutoBackup}>{t.disable}</Button> 1077 + <Button variant="destructive" onClick={disableAutoBackup}> 1078 + {t.disable} 1079 + </Button> 872 1080 ) : ( 873 - <Button onClick={() => { setBackupOpen(false); setSetPwdOpen(true) }}>{t.enable}</Button> 1081 + <Button 1082 + onClick={() => { 1083 + setBackupOpen(false); 1084 + setSetPwdOpen(true); 1085 + }} 1086 + > 1087 + {t.enable} 1088 + </Button> 874 1089 )} 875 1090 </DialogFooter> 876 1091 </DialogContent> ··· 880 1095 <DialogContent> 881 1096 <DialogHeader> 882 1097 <DialogTitle>{t.setEncryptionPassword}</DialogTitle> 883 - <DialogDescription>{t.setEncryptionPasswordDescription}</DialogDescription> 1098 + <DialogDescription> 1099 + {t.setEncryptionPasswordDescription} 1100 + </DialogDescription> 884 1101 </DialogHeader> 885 1102 <Label>{t.password}</Label> 886 - <Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> 1103 + <Input 1104 + type="password" 1105 + value={password} 1106 + onChange={(e) => setPassword(e.target.value)} 1107 + /> 887 1108 <Label>{t.confirmPassword}</Label> 888 - <Input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} /> 1109 + <Input 1110 + type="password" 1111 + value={confirm} 1112 + onChange={(e) => setConfirm(e.target.value)} 1113 + /> 889 1114 {error && <p className="text-sm text-red-500">{error}</p>} 890 1115 <DialogFooter> 891 1116 <Button onClick={enable}>{t.confirm}</Button> ··· 893 1118 </DialogContent> 894 1119 </Dialog> 895 1120 896 - <Dialog open={unlockOpen} onOpenChange={(open) => { if (open) setUnlockOpen(true) }}> 897 - <DialogContent onInteractOutside={(e) => e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()}> 1121 + <Dialog 1122 + open={unlockOpen} 1123 + onOpenChange={(open) => { 1124 + if (open) setUnlockOpen(true); 1125 + }} 1126 + > 1127 + <DialogContent 1128 + onInteractOutside={(e) => e.preventDefault()} 1129 + onEscapeKeyDown={(e) => e.preventDefault()} 1130 + > 898 1131 <DialogHeader> 899 1132 <DialogTitle>{t.enterPasswordTitle}</DialogTitle> 900 1133 <DialogDescription>{t.enterPasswordDescription}</DialogDescription> 901 1134 </DialogHeader> 902 - <Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> 1135 + <Input 1136 + type="password" 1137 + value={password} 1138 + onChange={(e) => setPassword(e.target.value)} 1139 + /> 903 1140 <DialogFooter> 904 1141 <Button onClick={unlock} disabled={isUnlocking}> 905 1142 {isUnlocking ? ( ··· 915 1152 </DialogContent> 916 1153 </Dialog> 917 1154 918 - 919 1155 <Dialog open={rotateOpen} onOpenChange={setRotateOpen}> 920 1156 <DialogContent> 921 1157 <DialogHeader> 922 1158 <DialogTitle>{t.changeEncryptionKey}</DialogTitle> 923 1159 </DialogHeader> 924 1160 <Label>{t.oldPassword}</Label> 925 - <Input type="password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} /> 1161 + <Input 1162 + type="password" 1163 + value={oldPassword} 1164 + onChange={(e) => setOldPassword(e.target.value)} 1165 + /> 926 1166 <Label>{t.newPassword}</Label> 927 - <Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> 1167 + <Input 1168 + type="password" 1169 + value={password} 1170 + onChange={(e) => setPassword(e.target.value)} 1171 + /> 928 1172 <Label>{t.confirmNewPassword}</Label> 929 - <Input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} /> 1173 + <Input 1174 + type="password" 1175 + value={confirm} 1176 + onChange={(e) => setConfirm(e.target.value)} 1177 + /> 930 1178 {error && <p className="text-sm text-red-500">{error}</p>} 931 1179 <DialogFooter> 932 1180 <Button onClick={rotate}>{t.confirmChange}</Button> ··· 934 1182 </DialogContent> 935 1183 </Dialog> 936 1184 </> 937 - ) 1185 + ); 938 1186 }
+7 -1
components/app/sidebar/bookmark-panel.tsx
··· 17 17 import { zhCN, enUS } from "date-fns/locale"; 18 18 import { toast } from "sonner"; 19 19 import { cn } from "@/lib/utils"; 20 - import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; 20 + import { 21 + Empty, 22 + EmptyDescription, 23 + EmptyHeader, 24 + EmptyMedia, 25 + EmptyTitle, 26 + } from "@/components/ui/empty"; 21 27 import { isZhLanguage, translations, useLanguage } from "@/lib/i18n"; 22 28 import { 23 29 getEncryptionState,
+73 -19
components/app/sidebar/countdown.tsx
··· 43 43 import { isZhLanguage, translations, useLanguage } from "@/lib/i18n"; 44 44 import { toast } from "sonner"; 45 45 import { ClockDashed } from "@/components/icons/clock-dashed"; 46 - import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; 46 + import { 47 + Empty, 48 + EmptyDescription, 49 + EmptyHeader, 50 + EmptyMedia, 51 + EmptyTitle, 52 + } from "@/components/ui/empty"; 47 53 48 54 interface Countdown { 49 55 id: string; ··· 78 84 return new Date(year, month - 1, day); 79 85 }; 80 86 81 - 82 87 const TEXT_COLOR_MAP: Record<string, string> = { 83 88 "bg-red-500": "#ef4444", 84 89 "bg-blue-500": "#3b82f6", ··· 90 95 "bg-orange-500": "#f97316", 91 96 }; 92 97 93 - const allIconNames = Object.keys(lucideIcons).sort((a, b) => a.localeCompare(b)); 98 + const allIconNames = Object.keys(lucideIcons).sort((a, b) => 99 + a.localeCompare(b), 100 + ); 94 101 95 102 const toDateString = (date: Date) => { 96 103 const year = date.getFullYear(); ··· 130 137 return target.slice(0, 200); 131 138 }, [iconSearch]); 132 139 133 - const renderCountdownIcon = (iconName: string | undefined, colorClass: string, size = 20, withBackground = false) => { 140 + const renderCountdownIcon = ( 141 + iconName: string | undefined, 142 + colorClass: string, 143 + size = 20, 144 + withBackground = false, 145 + ) => { 134 146 const iconColor = TEXT_COLOR_MAP[colorClass] ?? "#3b82f6"; 135 - const IconComponent = lucideIcons[(iconName || "Clock") as keyof typeof lucideIcons] ?? lucideIcons.Clock; 147 + const IconComponent = 148 + lucideIcons[(iconName || "Clock") as keyof typeof lucideIcons] ?? 149 + lucideIcons.Clock; 136 150 if (withBackground) { 137 151 return ( 138 152 <div className="h-10 w-10 rounded-full bg-muted/70 dark:bg-muted/40 flex items-center justify-center"> ··· 173 187 const daysInMonth = (year: number, monthIndex: number) => 174 188 new Date(year, monthIndex + 1, 0).getDate(); 175 189 176 - const buildClampedDate = (year: number, monthIndex: number, day: number) => { 190 + const buildClampedDate = ( 191 + year: number, 192 + monthIndex: number, 193 + day: number, 194 + ) => { 177 195 const clampedDay = Math.min(day, daysInMonth(year, monthIndex)); 178 196 return new Date(year, monthIndex, clampedDay); 179 197 }; ··· 187 205 nextDate = new Date(today); 188 206 nextDate.setDate(today.getDate() + daysToAdd); 189 207 } else if (repeat === "monthly") { 190 - nextDate = buildClampedDate(today.getFullYear(), today.getMonth(), targetDate.getDate()); 208 + nextDate = buildClampedDate( 209 + today.getFullYear(), 210 + today.getMonth(), 211 + targetDate.getDate(), 212 + ); 191 213 if (nextDate < today) { 192 214 const nextMonth = today.getMonth() + 1; 193 215 const year = today.getFullYear() + Math.floor(nextMonth / 12); ··· 195 217 nextDate = buildClampedDate(year, month, targetDate.getDate()); 196 218 } 197 219 } else if (repeat === "yearly") { 198 - nextDate = buildClampedDate(today.getFullYear(), targetDate.getMonth(), targetDate.getDate()); 220 + nextDate = buildClampedDate( 221 + today.getFullYear(), 222 + targetDate.getMonth(), 223 + targetDate.getDate(), 224 + ); 199 225 if (nextDate < today) { 200 - nextDate = buildClampedDate(today.getFullYear() + 1, targetDate.getMonth(), targetDate.getDate()); 226 + nextDate = buildClampedDate( 227 + today.getFullYear() + 1, 228 + targetDate.getMonth(), 229 + targetDate.getDate(), 230 + ); 201 231 } 202 232 } 203 233 ··· 358 388 > 359 389 <Avatar className="h-12 w-12 mr-3"> 360 390 <AvatarFallback className="bg-transparent"> 361 - {renderCountdownIcon(countdown.icon, countdown.color, 20, true)} 391 + {renderCountdownIcon( 392 + countdown.icon, 393 + countdown.color, 394 + 20, 395 + true, 396 + )} 362 397 </AvatarFallback> 363 398 </Avatar> 364 399 <div className="flex-1"> ··· 375 410 {Math.abs(daysLeft)} 376 411 </div> 377 412 <div className="text-xs text-muted-foreground"> 378 - {daysLeft < 0 ? t.countdownDaysAgo : t.countdownDaysLeft} 413 + {daysLeft < 0 414 + ? t.countdownDaysAgo 415 + : t.countdownDaysLeft} 379 416 </div> 380 417 </div> 381 418 </div> ··· 415 452 <div className="flex items-center mb-6"> 416 453 <Avatar className="h-16 w-16 mr-4"> 417 454 <AvatarFallback className="bg-transparent"> 418 - {renderCountdownIcon(selectedCountdown.icon, selectedCountdown.color, 26)} 455 + {renderCountdownIcon( 456 + selectedCountdown.icon, 457 + selectedCountdown.color, 458 + 26, 459 + )} 419 460 </AvatarFallback> 420 461 </Avatar> 421 462 <div> ··· 423 464 <div 424 465 className={`text-2xl font-bold mt-1 ${daysLeft < 0 ? "text-red-500" : "text-primary"}`} 425 466 > 426 - {Math.abs(daysLeft)} {daysLeft < 0 ? t.countdownDaysAgo : t.countdownDaysLeft} 467 + {Math.abs(daysLeft)}{" "} 468 + {daysLeft < 0 ? t.countdownDaysAgo : t.countdownDaysLeft} 427 469 </div> 428 470 </div> 429 471 </div> ··· 545 587 </SelectContent> 546 588 </Select> 547 589 </div> 548 - 549 590 550 591 <div className="space-y-2"> 551 592 <Label>{t.countdownIcon}</Label> 552 593 <Popover> 553 594 <PopoverTrigger asChild> 554 - <Button variant="outline" className="w-full justify-start gap-2"> 555 - {renderCountdownIcon(newCountdown.icon, newCountdown.color || "bg-blue-500")} 595 + <Button 596 + variant="outline" 597 + className="w-full justify-start gap-2" 598 + > 599 + {renderCountdownIcon( 600 + newCountdown.icon, 601 + newCountdown.color || "bg-blue-500", 602 + )} 556 603 <span>{newCountdown.icon || "Clock"}</span> 557 604 </Button> 558 605 </PopoverTrigger> ··· 570 617 key={iconName} 571 618 className={cn( 572 619 "h-11 w-11 flex items-center justify-center rounded-md cursor-pointer hover:bg-accent", 573 - newCountdown.icon === iconName && "ring-2 ring-primary bg-accent/60", 620 + newCountdown.icon === iconName && 621 + "ring-2 ring-primary bg-accent/60", 574 622 )} 575 - onClick={() => setNewCountdown({ ...newCountdown, icon: iconName })} 623 + onClick={() => 624 + setNewCountdown({ ...newCountdown, icon: iconName }) 625 + } 576 626 > 577 - {renderCountdownIcon(iconName, newCountdown.color || "bg-blue-500", 18)} 627 + {renderCountdownIcon( 628 + iconName, 629 + newCountdown.color || "bg-blue-500", 630 + 18, 631 + )} 578 632 </div> 579 633 ))} 580 634 </div>
+7 -1
components/app/sidebar/mini-calendar-sheet.tsx
··· 22 22 import { Button } from "@/components/ui/button"; 23 23 import { useState, useEffect } from "react"; 24 24 import { cn } from "@/lib/utils"; 25 - import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; 25 + import { 26 + Empty, 27 + EmptyDescription, 28 + EmptyHeader, 29 + EmptyMedia, 30 + EmptyTitle, 31 + } from "@/components/ui/empty"; 26 32 27 33 const getWeekStartsOn = (locale: string): 0 | 1 => { 28 34 try {
+205 -123
components/app/sidebar/sidebar.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog" 4 - import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" 5 - import { useCalendar } from "@/components/providers/calendar-context" 6 - import { translations, type Language } from "@/lib/i18n" 7 - import { Calendar } from "@/components/ui/calendar" 8 - import { Checkbox } from "@/components/ui/checkbox" 9 - import { Button } from "@/components/ui/button" 10 - import { Input } from "@/components/ui/input" 11 - import { Label } from "@/components/ui/label" 12 - import { Plus, X, Edit2 } from "lucide-react" 13 - import { useEffect, useState, type CSSProperties } from "react" 14 - import { cn } from "@/lib/utils" 15 - import { toast } from "sonner" 16 - import Image from "next/image" 3 + import { 4 + Dialog, 5 + DialogContent, 6 + DialogHeader, 7 + DialogTitle, 8 + DialogFooter, 9 + DialogDescription, 10 + } from "@/components/ui/dialog"; 11 + import { 12 + Select, 13 + SelectContent, 14 + SelectItem, 15 + SelectTrigger, 16 + SelectValue, 17 + } from "@/components/ui/select"; 18 + import { useCalendar } from "@/components/providers/calendar-context"; 19 + import { translations, type Language } from "@/lib/i18n"; 20 + import { Calendar } from "@/components/ui/calendar"; 21 + import { Checkbox } from "@/components/ui/checkbox"; 22 + import { Button } from "@/components/ui/button"; 23 + import { Input } from "@/components/ui/input"; 24 + import { Label } from "@/components/ui/label"; 25 + import { Plus, X, Edit2 } from "lucide-react"; 26 + import { useEffect, useState, type CSSProperties } from "react"; 27 + import { cn } from "@/lib/utils"; 28 + import { toast } from "sonner"; 29 + import Image from "next/image"; 17 30 18 31 interface SidebarProps { 19 - onCreateEvent: () => void 20 - onDateSelect: (date: Date) => void 21 - onViewChange?: (view: string) => void 22 - language?: Language 23 - selectedDate?: Date 24 - isCollapsed?: boolean 25 - onToggleCollapse?: () => void 26 - selectedCategoryFilters?: string[] 27 - onCategoryFilterChange?: (categoryId: string, checked: boolean) => void 28 - onCollapseTransitionEnd?: () => void 32 + onCreateEvent: () => void; 33 + onDateSelect: (date: Date) => void; 34 + onViewChange?: (view: string) => void; 35 + language?: Language; 36 + selectedDate?: Date; 37 + isCollapsed?: boolean; 38 + onToggleCollapse?: () => void; 39 + selectedCategoryFilters?: string[]; 40 + onCategoryFilterChange?: (categoryId: string, checked: boolean) => void; 41 + onCollapseTransitionEnd?: () => void; 29 42 } 30 43 31 44 export interface CalendarCategory { 32 - id: string 33 - name: string 34 - color: string 35 - keywords?: string[] 45 + id: string; 46 + name: string; 47 + color: string; 48 + keywords?: string[]; 36 49 } 37 50 38 51 const CALENDAR_COLOR_OPTIONS = [ ··· 43 56 { value: "bg-purple-500", hex: "#8b5cf6", labelKey: "colorPurple" }, 44 57 { value: "bg-pink-500", hex: "#ec4899", labelKey: "colorPink" }, 45 58 { value: "bg-teal-500", hex: "#14b8a6", labelKey: "colorTeal" }, 46 - ] as const 59 + ] as const; 47 60 48 - const CALENDAR_COLOR_MAP = Object.fromEntries(CALENDAR_COLOR_OPTIONS.map((option) => [option.value, option.hex])) 61 + const CALENDAR_COLOR_MAP = Object.fromEntries( 62 + CALENDAR_COLOR_OPTIONS.map((option) => [option.value, option.hex]), 63 + ); 49 64 50 65 export default function Sidebar({ 51 66 onCreateEvent, ··· 59 74 onCategoryFilterChange, 60 75 onCollapseTransitionEnd, 61 76 }: SidebarProps) { 62 - 63 77 const { 64 78 calendars, 65 79 events, ··· 67 81 addCategory: addCategoryToContext, 68 82 removeCategory: removeCategoryFromContext, 69 83 updateCategory: updateCategoryInContext, 70 - } = useCalendar() 84 + } = useCalendar(); 71 85 72 - const [newCategoryName, setNewCategoryName] = useState("") 73 - const [newCategoryColor, setNewCategoryColor] = useState("bg-blue-500") 74 - const [showAddCategory, setShowAddCategory] = useState(false) 75 - const [localSelectedDate, setLocalSelectedDate] = useState<Date | undefined>(selectedDate || new Date()) 76 - const [manageCategoriesOpen, setManageCategoriesOpen] = useState(false) 77 - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) 78 - const [categoryToDelete, setCategoryToDelete] = useState<string | null>(null) 79 - const [deleteCategoryEvents, setDeleteCategoryEvents] = useState(false) 80 - const [editDialogOpen, setEditDialogOpen] = useState(false) 81 - const [editingCategoryId, setEditingCategoryId] = useState<string | null>(null) 82 - const [editingCategoryName, setEditingCategoryName] = useState("") 83 - const [editingCategoryColor, setEditingCategoryColor] = useState("bg-blue-500") 84 - const t = translations[language || "zh-CN"] 85 - const weekdayNames = t.sidebarCalendarWeekdaysShort 86 - const monthNames = t.sidebarCalendarMonthsLong 87 - const monthYearTemplate = t.sidebarCalendarMonthYearFormat 86 + const [newCategoryName, setNewCategoryName] = useState(""); 87 + const [newCategoryColor, setNewCategoryColor] = useState("bg-blue-500"); 88 + const [showAddCategory, setShowAddCategory] = useState(false); 89 + const [localSelectedDate, setLocalSelectedDate] = useState<Date | undefined>( 90 + selectedDate || new Date(), 91 + ); 92 + const [manageCategoriesOpen, setManageCategoriesOpen] = useState(false); 93 + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); 94 + const [categoryToDelete, setCategoryToDelete] = useState<string | null>(null); 95 + const [deleteCategoryEvents, setDeleteCategoryEvents] = useState(false); 96 + const [editDialogOpen, setEditDialogOpen] = useState(false); 97 + const [editingCategoryId, setEditingCategoryId] = useState<string | null>( 98 + null, 99 + ); 100 + const [editingCategoryName, setEditingCategoryName] = useState(""); 101 + const [editingCategoryColor, setEditingCategoryColor] = 102 + useState("bg-blue-500"); 103 + const t = translations[language || "zh-CN"]; 104 + const weekdayNames = t.sidebarCalendarWeekdaysShort; 105 + const monthNames = t.sidebarCalendarMonthsLong; 106 + const monthYearTemplate = t.sidebarCalendarMonthYearFormat; 88 107 89 108 const formatCalendarCaption = (date: Date) => { 90 - const month = monthNames[date.getMonth()] 91 - const year = new Intl.NumberFormat(language, { useGrouping: false }).format(date.getFullYear()) 92 - return monthYearTemplate.replace("{{month}}", month).replace("{{year}}", year) 93 - } 109 + const month = monthNames[date.getMonth()]; 110 + const year = new Intl.NumberFormat(language, { useGrouping: false }).format( 111 + date.getFullYear(), 112 + ); 113 + return monthYearTemplate 114 + .replace("{{month}}", month) 115 + .replace("{{year}}", year); 116 + }; 94 117 95 118 const deleteText = { 96 119 title: t.deleteConfirmationTitle, ··· 99 122 delete: t.delete, 100 123 toastSuccess: t.categoryDeleted, 101 124 toastDescription: t.categoryDeletedDescription, 102 - } 103 - const deleteCategoryEventsLabel = t.deleteCategoryEvents || "ๅŒๆ—ถๅˆ ้™คๆญคๅˆ†็ฑปไธ‹็š„ๆ‰€ๆœ‰ๆ—ฅ็จ‹" 104 - 125 + }; 126 + const deleteCategoryEventsLabel = 127 + t.deleteCategoryEvents || "ๅŒๆ—ถๅˆ ้™คๆญคๅˆ†็ฑปไธ‹็š„ๆ‰€ๆœ‰ๆ—ฅ็จ‹"; 128 + 105 129 useEffect(() => { 106 130 if (selectedDate) { 107 131 setLocalSelectedDate((prev) => { 108 132 if (!prev || prev.getTime() !== selectedDate.getTime()) { 109 - return selectedDate 133 + return selectedDate; 110 134 } 111 - return prev 112 - }) 135 + return prev; 136 + }); 113 137 } 114 - }, [selectedDate]) 138 + }, [selectedDate]); 115 139 116 140 const addCategory = () => { 117 141 if (newCategoryName.trim()) { ··· 120 144 name: newCategoryName.trim(), 121 145 color: newCategoryColor, 122 146 keywords: [], 123 - } 124 - addCategoryToContext(newCategory) 125 - setNewCategoryName("") 126 - setNewCategoryColor("bg-blue-500") 127 - setShowAddCategory(false) 128 - setManageCategoriesOpen(false) 147 + }; 148 + addCategoryToContext(newCategory); 149 + setNewCategoryName(""); 150 + setNewCategoryColor("bg-blue-500"); 151 + setShowAddCategory(false); 152 + setManageCategoriesOpen(false); 129 153 toast(t.categoryAdded || "ๅˆ†็ฑปๅทฒๆทปๅŠ ", { 130 154 description: `${t.categoryAddedDesc || "ๅทฒๆˆๅŠŸๆทปๅŠ "} "${newCategoryName}" ${t.category || "ๅˆ†็ฑป"}`, 131 - }) 155 + }); 132 156 } 133 - } 157 + }; 134 158 135 159 const handleDeleteClick = (id: string) => { 136 - setCategoryToDelete(id) 137 - setDeleteCategoryEvents(false) 138 - setDeleteDialogOpen(true) 139 - } 160 + setCategoryToDelete(id); 161 + setDeleteCategoryEvents(false); 162 + setDeleteDialogOpen(true); 163 + }; 140 164 141 165 const handleEditClick = (id: string) => { 142 - const category = calendars.find((calendar) => calendar.id === id) 143 - if (!category) return 144 - setEditingCategoryId(id) 145 - setEditingCategoryName(category.name) 146 - setEditingCategoryColor(category.color) 147 - setEditDialogOpen(true) 148 - } 166 + const category = calendars.find((calendar) => calendar.id === id); 167 + if (!category) return; 168 + setEditingCategoryId(id); 169 + setEditingCategoryName(category.name); 170 + setEditingCategoryColor(category.color); 171 + setEditDialogOpen(true); 172 + }; 149 173 150 174 const saveCategoryEdit = () => { 151 - if (!editingCategoryId || !editingCategoryName.trim()) return 175 + if (!editingCategoryId || !editingCategoryName.trim()) return; 152 176 updateCategoryInContext(editingCategoryId, { 153 177 name: editingCategoryName.trim(), 154 178 color: editingCategoryColor, 155 - }) 156 - setEditDialogOpen(false) 157 - setEditingCategoryId(null) 158 - toast(t.categoryUpdated || "ๅˆ†็ฑปๅทฒๆ›ดๆ–ฐ") 159 - } 179 + }); 180 + setEditDialogOpen(false); 181 + setEditingCategoryId(null); 182 + toast(t.categoryUpdated || "ๅˆ†็ฑปๅทฒๆ›ดๆ–ฐ"); 183 + }; 160 184 161 185 const confirmDelete = () => { 162 - if (categoryToDelete) { 163 - if (deleteCategoryEvents) { 164 - setEvents(events.filter((event) => event.calendarId !== categoryToDelete)) 186 + if (categoryToDelete) { 187 + if (deleteCategoryEvents) { 188 + setEvents( 189 + events.filter((event) => event.calendarId !== categoryToDelete), 190 + ); 191 + } 192 + removeCategoryFromContext(categoryToDelete); 193 + toast(deleteText.toastSuccess, { 194 + description: deleteCategoryEvents 195 + ? t.categoryDeletedWithEvents 196 + : deleteText.toastDescription, 197 + }); 165 198 } 166 - removeCategoryFromContext(categoryToDelete) 167 - toast(deleteText.toastSuccess, { 168 - description: deleteCategoryEvents ? t.categoryDeletedWithEvents : deleteText.toastDescription, 169 - }) 170 - } 171 - setDeleteDialogOpen(false) 172 - setCategoryToDelete(null) 173 - setDeleteCategoryEvents(false) 174 - } 199 + setDeleteDialogOpen(false); 200 + setCategoryToDelete(null); 201 + setDeleteCategoryEvents(false); 202 + }; 175 203 176 204 return ( 177 205 <div 178 206 style={{ "--sidebar-calendar-width": "17rem" } as CSSProperties} 179 207 className={cn( 180 208 "border-r bg-background overflow-y-auto transition-all duration-300 ease-in-out", 181 - isCollapsed 182 - ? "w-0 opacity-0 overflow-hidden" 183 - : "w-60 opacity-100", 209 + isCollapsed ? "w-0 opacity-0 overflow-hidden" : "w-60 opacity-100", 184 210 )} 185 211 onTransitionEnd={(event) => { 186 - if (event.target === event.currentTarget && event.propertyName === "width") { 187 - onCollapseTransitionEnd?.() 212 + if ( 213 + event.target === event.currentTarget && 214 + event.propertyName === "width" 215 + ) { 216 + onCollapseTransitionEnd?.(); 188 217 } 189 218 }} 190 219 > ··· 216 245 formatWeekdayName: (date) => weekdayNames[date.getDay()], 217 246 }} 218 247 onSelect={(date) => { 219 - setLocalSelectedDate(date) 220 - date && onDateSelect(date) 248 + setLocalSelectedDate(date); 249 + date && onDateSelect(date); 221 250 }} 222 251 className="rounded-lg border" 223 252 /> ··· 228 257 <span className="text-sm font-medium">{t.myCalendars}</span> 229 258 </div> 230 259 {calendars.map((calendar) => ( 231 - <div key={calendar.id} className="flex items-center justify-between"> 260 + <div 261 + key={calendar.id} 262 + className="flex items-center justify-between" 263 + > 232 264 <div className="flex items-center space-x-2"> 233 265 <Checkbox 234 266 checked={selectedCategoryFilters.includes(calendar.id)} 235 - onCheckedChange={(checked) => onCategoryFilterChange?.(calendar.id, checked === true)} 267 + onCheckedChange={(checked) => 268 + onCategoryFilterChange?.(calendar.id, checked === true) 269 + } 236 270 className="h-4 w-4 rounded-md border-0 data-[state=checked]:text-white" 237 271 style={{ 238 - backgroundColor: CALENDAR_COLOR_MAP[calendar.color] ?? "#3b82f6", 272 + backgroundColor: 273 + CALENDAR_COLOR_MAP[calendar.color] ?? "#3b82f6", 239 274 }} 240 275 /> 241 276 <span className="text-sm">{calendar.name}</span> 242 277 </div> 243 278 <div className="flex items-center"> 244 - <Button variant="ghost" size="sm" onClick={() => handleEditClick(calendar.id)}> 279 + <Button 280 + variant="ghost" 281 + size="sm" 282 + onClick={() => handleEditClick(calendar.id)} 283 + > 245 284 <Edit2 className="h-4 w-4" /> 246 285 </Button> 247 - <Button variant="ghost" size="sm" onClick={() => handleDeleteClick(calendar.id)}> 286 + <Button 287 + variant="ghost" 288 + size="sm" 289 + onClick={() => handleDeleteClick(calendar.id)} 290 + > 248 291 <X className="h-4 w-4" /> 249 292 </Button> 250 293 </div> ··· 254 297 <div className="flex items-center space-x-2"> 255 298 <Checkbox 256 299 checked={selectedCategoryFilters.includes("__uncategorized__")} 257 - onCheckedChange={(checked) => onCategoryFilterChange?.("__uncategorized__", checked === true)} 300 + onCheckedChange={(checked) => 301 + onCategoryFilterChange?.( 302 + "__uncategorized__", 303 + checked === true, 304 + ) 305 + } 258 306 className="h-4 w-4 rounded-md border border-muted-foreground/60" 259 307 /> 260 - <span className="text-sm text-muted-foreground">{t.uncategorized}</span> 308 + <span className="text-sm text-muted-foreground"> 309 + {t.uncategorized} 310 + </span> 261 311 </div> 262 312 )} 263 313 {showAddCategory ? ( ··· 296 346 <Checkbox 297 347 id="delete-category-events" 298 348 checked={deleteCategoryEvents} 299 - onCheckedChange={(checked) => setDeleteCategoryEvents(checked === true)} 349 + onCheckedChange={(checked) => 350 + setDeleteCategoryEvents(checked === true) 351 + } 300 352 /> 301 - <Label htmlFor="delete-category-events">{deleteCategoryEventsLabel}</Label> 353 + <Label htmlFor="delete-category-events"> 354 + {deleteCategoryEventsLabel} 355 + </Label> 302 356 </div> 303 357 </DialogDescription> 304 358 </DialogHeader> 305 359 <DialogFooter> 306 - <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}> 360 + <Button 361 + variant="outline" 362 + onClick={() => setDeleteDialogOpen(false)} 363 + > 307 364 {deleteText.cancel} 308 365 </Button> 309 366 <Button variant="destructive" onClick={confirmDelete}> ··· 313 370 </DialogContent> 314 371 </Dialog> 315 372 316 - <Dialog open={manageCategoriesOpen} onOpenChange={setManageCategoriesOpen}> 373 + <Dialog 374 + open={manageCategoriesOpen} 375 + onOpenChange={setManageCategoriesOpen} 376 + > 317 377 <DialogContent className="max-w-md max-h-[90vh] overflow-y-auto"> 318 378 <DialogHeader> 319 379 <DialogTitle>{t.createCategories}</DialogTitle> ··· 330 390 </div> 331 391 <div className="space-y-2"> 332 392 <Label htmlFor="category-color">{t.color}</Label> 333 - <Select value={newCategoryColor} onValueChange={setNewCategoryColor}> 393 + <Select 394 + value={newCategoryColor} 395 + onValueChange={setNewCategoryColor} 396 + > 334 397 <SelectTrigger id="category-color"> 335 398 <SelectValue placeholder={t.selectColor} /> 336 399 </SelectTrigger> ··· 340 403 <div className="flex items-center"> 341 404 <div 342 405 className="w-4 h-4 rounded-full mr-2" 343 - style={{ backgroundColor: CALENDAR_COLOR_MAP[option.value] }} 406 + style={{ 407 + backgroundColor: CALENDAR_COLOR_MAP[option.value], 408 + }} 344 409 /> 345 410 {t[option.labelKey]} 346 411 </div> ··· 351 416 </div> 352 417 </div> 353 418 <DialogFooter className="justify-end"> 354 - <Button variant="outline" onClick={() => setManageCategoriesOpen(false)}>{t.cancel}</Button> 419 + <Button 420 + variant="outline" 421 + onClick={() => setManageCategoriesOpen(false)} 422 + > 423 + {t.cancel} 424 + </Button> 355 425 <Button onClick={addCategory} disabled={!newCategoryName}> 356 426 <Plus className="mr-2 h-4 w-4" /> 357 427 {t.addCategory} ··· 377 447 </div> 378 448 <div className="space-y-2"> 379 449 <Label htmlFor="edit-category-color">{t.color}</Label> 380 - <Select value={editingCategoryColor} onValueChange={setEditingCategoryColor}> 450 + <Select 451 + value={editingCategoryColor} 452 + onValueChange={setEditingCategoryColor} 453 + > 381 454 <SelectTrigger id="edit-category-color"> 382 455 <SelectValue placeholder={t.selectColor} /> 383 456 </SelectTrigger> ··· 387 460 <div className="flex items-center"> 388 461 <div 389 462 className="w-4 h-4 rounded-full mr-2" 390 - style={{ backgroundColor: CALENDAR_COLOR_MAP[option.value] }} 463 + style={{ 464 + backgroundColor: CALENDAR_COLOR_MAP[option.value], 465 + }} 391 466 /> 392 467 {t[option.labelKey]} 393 468 </div> ··· 398 473 </div> 399 474 </div> 400 475 <DialogFooter> 401 - <Button variant="outline" onClick={() => setEditDialogOpen(false)}>{t.cancel}</Button> 402 - <Button onClick={saveCategoryEdit} disabled={!editingCategoryName.trim()}>{t.save}</Button> 476 + <Button variant="outline" onClick={() => setEditDialogOpen(false)}> 477 + {t.cancel} 478 + </Button> 479 + <Button 480 + onClick={saveCategoryEdit} 481 + disabled={!editingCategoryName.trim()} 482 + > 483 + {t.save} 484 + </Button> 403 485 </DialogFooter> 404 486 </DialogContent> 405 487 </Dialog> 406 488 </div> 407 - ) 489 + ); 408 490 }
+76 -20
components/app/views/day-view.tsx
··· 15 15 import type { CalendarEvent } from "../calendar"; 16 16 import { translations, type Language } from "@/lib/i18n"; 17 17 18 - const ContextMenu = ({ children }: { children: React.ReactNode }) => <>{children}</>; 19 - const ContextMenuTrigger = ({ children }: { children: React.ReactNode; asChild?: boolean }) => <>{children}</>; 18 + const ContextMenu = ({ children }: { children: React.ReactNode }) => ( 19 + <>{children}</> 20 + ); 21 + const ContextMenuTrigger = ({ 22 + children, 23 + }: { 24 + children: React.ReactNode; 25 + asChild?: boolean; 26 + }) => <>{children}</>; 20 27 const ContextMenuContent = () => null; 21 28 const ContextMenuItem = (_props: any) => null; 22 29 ··· 84 91 ignoreNextEventClickRef.current = false; 85 92 }, 0); 86 93 }; 87 - 88 94 89 95 const [createSelection, setCreateSelection] = useState<{ 90 96 startMinute: number; ··· 301 307 302 308 useEffect(() => { 303 309 const handleMouseMove = (event: MouseEvent) => { 304 - if (!isCreatingRef.current || createStartMinuteRef.current === null) return; 310 + if (!isCreatingRef.current || createStartMinuteRef.current === null) 311 + return; 305 312 const endMinute = getMinutesFromMousePosition(event.clientY); 306 313 setCreateSelection({ 307 314 startMinute: createStartMinuteRef.current, ··· 310 317 }; 311 318 312 319 const handleMouseUp = () => { 313 - if (!isCreatingRef.current || createStartMinuteRef.current === null) return; 320 + if (!isCreatingRef.current || createStartMinuteRef.current === null) 321 + return; 314 322 315 323 const startMinute = Math.min( 316 324 createStartMinuteRef.current, ··· 324 332 const startDate = new Date(date); 325 333 startDate.setHours(0, startMinute, 0, 0); 326 334 327 - const effectiveEndMinute = endMinute === startMinute ? startMinute + 30 : endMinute; 335 + const effectiveEndMinute = 336 + endMinute === startMinute ? startMinute + 30 : endMinute; 328 337 const endDate = new Date(date); 329 338 endDate.setHours(0, Math.min(effectiveEndMinute, 24 * 60), 0, 0); 330 339 ··· 530 539 if (!isDraggingRef.current) { 531 540 onEventClick(event); 532 541 } 533 - 534 542 }} 535 543 > 536 544 <div ··· 547 555 </ContextMenuTrigger> 548 556 549 557 <ContextMenuContent className="w-40"> 550 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onEditEvent?.(event); }}> 558 + <ContextMenuItem 559 + onSelect={(e) => { 560 + e.preventDefault(); 561 + e.stopPropagation(); 562 + queueIgnoreEventClick(); 563 + onEditEvent?.(event); 564 + }} 565 + > 551 566 <Edit3 className="mr-2 h-4 w-4" /> 552 567 {menuLabels.edit} 553 568 </ContextMenuItem> 554 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onShareEvent?.(event); }}> 569 + <ContextMenuItem 570 + onSelect={(e) => { 571 + e.preventDefault(); 572 + e.stopPropagation(); 573 + queueIgnoreEventClick(); 574 + onShareEvent?.(event); 575 + }} 576 + > 555 577 <Share2 className="mr-2 h-4 w-4" /> 556 578 {menuLabels.share} 557 579 </ContextMenuItem> 558 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onBookmarkEvent?.(event); }}> 580 + <ContextMenuItem 581 + onSelect={(e) => { 582 + e.preventDefault(); 583 + e.stopPropagation(); 584 + queueIgnoreEventClick(); 585 + onBookmarkEvent?.(event); 586 + }} 587 + > 559 588 <Bookmark className="mr-2 h-4 w-4" /> 560 589 {menuLabels.bookmark} 561 590 </ContextMenuItem> 562 591 <ContextMenuItem 563 - onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onDeleteEvent?.(event); }} 592 + onSelect={(e) => { 593 + e.preventDefault(); 594 + e.stopPropagation(); 595 + queueIgnoreEventClick(); 596 + onDeleteEvent?.(event); 597 + }} 564 598 className="text-red-600" 565 599 > 566 600 <Trash2 className="mr-2 h-4 w-4" /> ··· 696 730 697 731 <div className="relative border-l" onMouseDown={handleGridMouseDown}> 698 732 {hours.map((hour) => ( 699 - <div 700 - key={hour} 701 - className="h-[60px] border-t" 702 - /> 733 + <div key={hour} className="h-[60px] border-t" /> 703 734 ))} 704 735 705 736 {eventLayouts.map(({ event, column, totalColumns }) => { ··· 748 779 if (!isDraggingRef.current) { 749 780 onEventClick(event); 750 781 } 751 - 752 782 }} 753 783 > 754 784 <div ··· 790 820 </ContextMenuTrigger> 791 821 792 822 <ContextMenuContent className="w-40"> 793 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onEditEvent?.(event); }}> 823 + <ContextMenuItem 824 + onSelect={(e) => { 825 + e.preventDefault(); 826 + e.stopPropagation(); 827 + queueIgnoreEventClick(); 828 + onEditEvent?.(event); 829 + }} 830 + > 794 831 <Edit3 className="mr-2 h-4 w-4" /> 795 832 {menuLabels.edit} 796 833 </ContextMenuItem> 797 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onShareEvent?.(event); }}> 834 + <ContextMenuItem 835 + onSelect={(e) => { 836 + e.preventDefault(); 837 + e.stopPropagation(); 838 + queueIgnoreEventClick(); 839 + onShareEvent?.(event); 840 + }} 841 + > 798 842 <Share2 className="mr-2 h-4 w-4" /> 799 843 {menuLabels.share} 800 844 </ContextMenuItem> 801 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onBookmarkEvent?.(event); }}> 845 + <ContextMenuItem 846 + onSelect={(e) => { 847 + e.preventDefault(); 848 + e.stopPropagation(); 849 + queueIgnoreEventClick(); 850 + onBookmarkEvent?.(event); 851 + }} 852 + > 802 853 <Bookmark className="mr-2 h-4 w-4" /> 803 854 {menuLabels.bookmark} 804 855 </ContextMenuItem> 805 856 <ContextMenuItem 806 - onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onDeleteEvent?.(event); }} 857 + onSelect={(e) => { 858 + e.preventDefault(); 859 + e.stopPropagation(); 860 + queueIgnoreEventClick(); 861 + onDeleteEvent?.(event); 862 + }} 807 863 className="text-red-600" 808 864 > 809 865 <Trash2 className="mr-2 h-4 w-4" />
+95 -56
components/app/views/month-view.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, subDays } from "date-fns" 4 - import { translations, type Language } from "@/lib/i18n" 5 - import type { CalendarEvent } from "../calendar" 6 - import { cn } from "@/lib/utils" 3 + import { 4 + format, 5 + startOfMonth, 6 + endOfMonth, 7 + eachDayOfInterval, 8 + isSameMonth, 9 + isSameDay, 10 + subDays, 11 + } from "date-fns"; 12 + import { translations, type Language } from "@/lib/i18n"; 13 + import type { CalendarEvent } from "../calendar"; 14 + import { cn } from "@/lib/utils"; 7 15 8 16 interface MonthViewProps { 9 - date: Date 10 - events: CalendarEvent[] 11 - onEventClick: (event: CalendarEvent) => void 12 - language: Language 13 - firstDayOfWeek: number 14 - timezone: string 17 + date: Date; 18 + events: CalendarEvent[]; 19 + onEventClick: (event: CalendarEvent) => void; 20 + language: Language; 21 + firstDayOfWeek: number; 22 + timezone: string; 15 23 } 16 24 17 25 function getDarkerColorClass(color: string) { 18 26 const colorMapping: Record<string, string> = { 19 - 'bg-[#E6F6FD]': '#3B82F6', 20 - 'bg-[#E7F8F2]': '#10B981', 21 - 'bg-[#FEF5E6]': '#F59E0B', 22 - 'bg-[#FFE4E6]': '#EF4444', 23 - 'bg-[#F3EEFE]': '#8B5CF6', 24 - 'bg-[#FCE7F3]': '#EC4899', 25 - 'bg-[#EEF2FF]': '#6366F1', 26 - 'bg-[#FFF0E5]': '#FB923C', 27 - 'bg-[#E6FAF7]': '#14B8A6', 28 - } 27 + "bg-[#E6F6FD]": "#3B82F6", 28 + "bg-[#E7F8F2]": "#10B981", 29 + "bg-[#FEF5E6]": "#F59E0B", 30 + "bg-[#FFE4E6]": "#EF4444", 31 + "bg-[#F3EEFE]": "#8B5CF6", 32 + "bg-[#FCE7F3]": "#EC4899", 33 + "bg-[#EEF2FF]": "#6366F1", 34 + "bg-[#FFF0E5]": "#FB923C", 35 + "bg-[#E6FAF7]": "#14B8A6", 36 + }; 29 37 30 - 31 - return colorMapping[color] || '#3A3A3A'; 38 + return colorMapping[color] || "#3A3A3A"; 32 39 } 33 40 34 41 function getDarkModeEventBackgroundColor(color: string) { 35 42 const darkModeColorMapping: Record<string, string> = { 36 - 'bg-[#E6F6FD]': '#2F4655', 37 - 'bg-[#E7F8F2]': '#2D4935', 38 - 'bg-[#FEF5E6]': '#4F3F1B', 39 - 'bg-[#FFE4E6]': '#6C2920', 40 - 'bg-[#F3EEFE]': '#483A63', 41 - 'bg-[#FCE7F3]': '#5A334A', 42 - 'bg-[#E6FAF7]': '#1F4A47', 43 - } 43 + "bg-[#E6F6FD]": "#2F4655", 44 + "bg-[#E7F8F2]": "#2D4935", 45 + "bg-[#FEF5E6]": "#4F3F1B", 46 + "bg-[#FFE4E6]": "#6C2920", 47 + "bg-[#F3EEFE]": "#483A63", 48 + "bg-[#FCE7F3]": "#5A334A", 49 + "bg-[#E6FAF7]": "#1F4A47", 50 + }; 44 51 45 - return darkModeColorMapping[color] 52 + return darkModeColorMapping[color]; 46 53 } 47 54 48 - export default function MonthView({ date, events, onEventClick, language, firstDayOfWeek, timezone }: MonthViewProps) { 49 - const t = translations[language] 50 - const monthStart = startOfMonth(date) 51 - const monthEnd = endOfMonth(date) 52 - const monthDays = eachDayOfInterval({ start: monthStart, end: monthEnd }) 53 - const today = new Date() 55 + export default function MonthView({ 56 + date, 57 + events, 58 + onEventClick, 59 + language, 60 + firstDayOfWeek, 61 + timezone, 62 + }: MonthViewProps) { 63 + const t = translations[language]; 64 + const monthStart = startOfMonth(date); 65 + const monthEnd = endOfMonth(date); 66 + const monthDays = eachDayOfInterval({ start: monthStart, end: monthEnd }); 67 + const today = new Date(); 54 68 const isDark = 55 69 typeof document !== "undefined" && 56 - document.documentElement.classList.contains("dark") 70 + document.documentElement.classList.contains("dark"); 57 71 58 - const startWeekDay = monthStart.getDay() 59 - const leadingEmptyDays = (7 + (startWeekDay - firstDayOfWeek)) % 7 72 + const startWeekDay = monthStart.getDay(); 73 + const leadingEmptyDays = (7 + (startWeekDay - firstDayOfWeek)) % 7; 60 74 61 - const prevMonthDays: Date[] = [] 75 + const prevMonthDays: Date[] = []; 62 76 for (let i = leadingEmptyDays; i > 0; i--) { 63 - prevMonthDays.push(subDays(monthStart, i)) 77 + prevMonthDays.push(subDays(monthStart, i)); 64 78 } 65 79 66 - const totalDays = [...prevMonthDays, ...monthDays] 80 + const totalDays = [...prevMonthDays, ...monthDays]; 67 81 68 82 return ( 69 83 <div className="grid grid-cols-7 gap-1 p-4"> 70 84 {(() => { 71 - const orderedDays = [...t.weekdays.slice(firstDayOfWeek), ...t.weekdays.slice(0, firstDayOfWeek)] 85 + const orderedDays = [ 86 + ...t.weekdays.slice(firstDayOfWeek), 87 + ...t.weekdays.slice(0, firstDayOfWeek), 88 + ]; 72 89 return orderedDays.map((day) => ( 73 90 <div key={day} className="text-center font-medium text-sm py-2"> 74 91 {day} 75 92 </div> 76 - )) 93 + )); 77 94 })()} 78 95 79 96 {totalDays.map((day) => { 80 - const dayEvents = events.filter((event) => isSameDay(new Date(event.startDate), day)) 81 - const visibleEvents = dayEvents.slice(0, 3) 82 - const remainingCount = dayEvents.length - visibleEvents.length 97 + const dayEvents = events.filter((event) => 98 + isSameDay(new Date(event.startDate), day), 99 + ); 100 + const visibleEvents = dayEvents.slice(0, 3); 101 + const remainingCount = dayEvents.length - visibleEvents.length; 83 102 84 103 return ( 85 104 <div ··· 101 120 {visibleEvents.map((event) => ( 102 121 <div 103 122 key={event.id} 104 - className={cn("relative text-xs truncate rounded-md p-1 cursor-pointer text-white", event.color)} 123 + className={cn( 124 + "relative text-xs truncate rounded-md p-1 cursor-pointer text-white", 125 + event.color, 126 + )} 105 127 onClick={() => onEventClick(event)} 106 128 style={{ 107 129 opacity: 1, 108 - backgroundColor: isDark ? getDarkModeEventBackgroundColor(event.color) : undefined, 130 + backgroundColor: isDark 131 + ? getDarkModeEventBackgroundColor(event.color) 132 + : undefined, 109 133 }} 110 134 > 111 - <div className={cn("absolute left-0 top-0 w-1 h-full rounded-l-md")} style={{ backgroundColor: getDarkerColorClass(event.color) }} /> 112 - <div className="pl-1.5" style={{ color: getDarkerColorClass(event.color) }}>{event.title}</div> 135 + <div 136 + className={cn( 137 + "absolute left-0 top-0 w-1 h-full rounded-l-md", 138 + )} 139 + style={{ 140 + backgroundColor: getDarkerColorClass(event.color), 141 + }} 142 + /> 143 + <div 144 + className="pl-1.5" 145 + style={{ color: getDarkerColorClass(event.color) }} 146 + > 147 + {event.title} 148 + </div> 113 149 </div> 114 150 ))} 115 151 {remainingCount > 0 && ( 116 152 <div className="text-xs text-muted-foreground"> 117 - {(remainingCount === 1 ? t.moreEvents : t.moreEventsPlural).replace("{count}", remainingCount.toString())} 153 + {(remainingCount === 1 154 + ? t.moreEvents 155 + : t.moreEventsPlural 156 + ).replace("{count}", remainingCount.toString())} 118 157 </div> 119 158 )} 120 159 </div> 121 160 </div> 122 - ) 161 + ); 123 162 })} 124 163 </div> 125 - ) 164 + ); 126 165 }
+77 -19
components/app/views/week-view.tsx
··· 17 17 import { cn } from "@/lib/utils"; 18 18 import { translations, type Language } from "@/lib/i18n"; 19 19 20 - const ContextMenu = ({ children }: { children: React.ReactNode }) => <>{children}</>; 21 - const ContextMenuTrigger = ({ children }: { children: React.ReactNode; asChild?: boolean }) => <>{children}</>; 20 + const ContextMenu = ({ children }: { children: React.ReactNode }) => ( 21 + <>{children}</> 22 + ); 23 + const ContextMenuTrigger = ({ 24 + children, 25 + }: { 26 + children: React.ReactNode; 27 + asChild?: boolean; 28 + }) => <>{children}</>; 22 29 const ContextMenuContent = () => null; 23 30 const ContextMenuItem = (_props: any) => null; 24 31 ··· 113 120 }, 0); 114 121 }; 115 122 116 - 117 123 const [createSelection, setCreateSelection] = useState<{ 118 124 dayIndex: number; 119 125 startMinute: number; 120 126 endMinute: number; 121 127 } | null>(null); 122 - const createStartRef = useRef<{ dayIndex: number; minute: number } | null>(null); 128 + const createStartRef = useRef<{ dayIndex: number; minute: number } | null>( 129 + null, 130 + ); 123 131 const isCreatingRef = useRef(false); 124 132 const isDark = 125 133 typeof document !== "undefined" && ··· 293 301 if (!isCreatingRef.current || !createStartRef.current) return; 294 302 295 303 const { dayIndex, minute } = createStartRef.current; 296 - const startMinute = Math.min(minute, createSelection?.endMinute ?? minute); 304 + const startMinute = Math.min( 305 + minute, 306 + createSelection?.endMinute ?? minute, 307 + ); 297 308 const endMinute = Math.max(minute, createSelection?.endMinute ?? minute); 298 309 const day = weekDays[dayIndex]; 299 310 ··· 301 312 const startDate = new Date(day); 302 313 startDate.setHours(0, startMinute, 0, 0); 303 314 304 - const effectiveEndMinute = endMinute === startMinute ? startMinute + 30 : endMinute; 315 + const effectiveEndMinute = 316 + endMinute === startMinute ? startMinute + 30 : endMinute; 305 317 const endDate = new Date(day); 306 318 endDate.setHours(0, Math.min(effectiveEndMinute, 24 * 60), 0, 0); 307 319 ··· 638 650 if (!isDraggingRef.current) { 639 651 onEventClick(event); 640 652 } 641 - 642 653 }} 643 654 > 644 655 <div ··· 655 666 </ContextMenuTrigger> 656 667 657 668 <ContextMenuContent className="w-40"> 658 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onEditEvent?.(event); }}> 669 + <ContextMenuItem 670 + onSelect={(e) => { 671 + e.preventDefault(); 672 + e.stopPropagation(); 673 + queueIgnoreEventClick(); 674 + onEditEvent?.(event); 675 + }} 676 + > 659 677 <Edit3 className="mr-2 h-4 w-4" /> 660 678 {menuLabels.edit} 661 679 </ContextMenuItem> 662 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onShareEvent?.(event); }}> 680 + <ContextMenuItem 681 + onSelect={(e) => { 682 + e.preventDefault(); 683 + e.stopPropagation(); 684 + queueIgnoreEventClick(); 685 + onShareEvent?.(event); 686 + }} 687 + > 663 688 <Share2 className="mr-2 h-4 w-4" /> 664 689 {menuLabels.share} 665 690 </ContextMenuItem> 666 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onBookmarkEvent?.(event); }}> 691 + <ContextMenuItem 692 + onSelect={(e) => { 693 + e.preventDefault(); 694 + e.stopPropagation(); 695 + queueIgnoreEventClick(); 696 + onBookmarkEvent?.(event); 697 + }} 698 + > 667 699 <Bookmark className="mr-2 h-4 w-4" /> 668 700 {menuLabels.bookmark} 669 701 </ContextMenuItem> 670 702 <ContextMenuItem 671 - onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onDeleteEvent?.(event); }} 703 + onSelect={(e) => { 704 + e.preventDefault(); 705 + e.stopPropagation(); 706 + queueIgnoreEventClick(); 707 + onDeleteEvent?.(event); 708 + }} 672 709 className="text-red-600" 673 710 > 674 711 <Trash2 className="mr-2 h-4 w-4" /> ··· 817 854 onMouseDown={(event) => handleGridMouseDown(dayIndex, event)} 818 855 > 819 856 {hours.map((hour) => ( 820 - <div 821 - key={hour} 822 - className="h-[60px] border-t" 823 - /> 857 + <div key={hour} className="h-[60px] border-t" /> 824 858 ))} 825 859 826 860 {eventLayouts.map( ··· 908 942 </ContextMenuTrigger> 909 943 910 944 <ContextMenuContent className="w-40"> 911 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onEditEvent?.(event); }}> 945 + <ContextMenuItem 946 + onSelect={(e) => { 947 + e.preventDefault(); 948 + e.stopPropagation(); 949 + queueIgnoreEventClick(); 950 + onEditEvent?.(event); 951 + }} 952 + > 912 953 <Edit3 className="mr-2 h-4 w-4" /> 913 954 {menuLabels.edit} 914 955 </ContextMenuItem> 915 - <ContextMenuItem onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onShareEvent?.(event); }}> 956 + <ContextMenuItem 957 + onSelect={(e) => { 958 + e.preventDefault(); 959 + e.stopPropagation(); 960 + queueIgnoreEventClick(); 961 + onShareEvent?.(event); 962 + }} 963 + > 916 964 <Share2 className="mr-2 h-4 w-4" /> 917 965 {menuLabels.share} 918 966 </ContextMenuItem> 919 967 <ContextMenuItem 920 - onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onBookmarkEvent?.(event); }} 968 + onSelect={(e) => { 969 + e.preventDefault(); 970 + e.stopPropagation(); 971 + queueIgnoreEventClick(); 972 + onBookmarkEvent?.(event); 973 + }} 921 974 > 922 975 <Bookmark className="mr-2 h-4 w-4" /> 923 976 {menuLabels.bookmark} 924 977 </ContextMenuItem> 925 978 <ContextMenuItem 926 - onSelect={(e) => { e.preventDefault(); e.stopPropagation(); queueIgnoreEventClick(); onDeleteEvent?.(event); }} 979 + onSelect={(e) => { 980 + e.preventDefault(); 981 + e.stopPropagation(); 982 + queueIgnoreEventClick(); 983 + onDeleteEvent?.(event); 984 + }} 927 985 className="text-red-600" 928 986 > 929 987 <Trash2 className="mr-2 h-4 w-4" />
+124 -66
components/app/views/year-view.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { eachDayOfInterval, endOfMonth, format, isSameDay, isSameMonth, startOfWeek } from "date-fns" 4 - import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" 5 - import { isZhLanguage, translations, type Language } from "@/lib/i18n" 6 - import type { CalendarEvent } from "../calendar" 7 - import { useMemo, useState } from "react" 8 - import { cn } from "@/lib/utils" 3 + import { 4 + eachDayOfInterval, 5 + endOfMonth, 6 + format, 7 + isSameDay, 8 + isSameMonth, 9 + startOfWeek, 10 + } from "date-fns"; 11 + import { 12 + Popover, 13 + PopoverContent, 14 + PopoverTrigger, 15 + } from "@/components/ui/popover"; 16 + import { isZhLanguage, translations, type Language } from "@/lib/i18n"; 17 + import type { CalendarEvent } from "../calendar"; 18 + import { useMemo, useState } from "react"; 19 + import { cn } from "@/lib/utils"; 9 20 10 21 interface YearViewProps { 11 - date: Date 12 - events: CalendarEvent[] 13 - onEventClick: (event: CalendarEvent) => void 14 - language: Language 15 - firstDayOfWeek: number 16 - isSidebarCollapsed?: boolean 17 - isSidebarExpanding?: boolean 22 + date: Date; 23 + events: CalendarEvent[]; 24 + onEventClick: (event: CalendarEvent) => void; 25 + language: Language; 26 + firstDayOfWeek: number; 27 + isSidebarCollapsed?: boolean; 28 + isSidebarExpanding?: boolean; 18 29 } 19 30 20 31 function getDarkerColorClass(color: string) { ··· 28 39 "bg-[#EEF2FF]": "#6366F1", 29 40 "bg-[#FFF0E5]": "#FB923C", 30 41 "bg-[#E6FAF7]": "#14B8A6", 31 - } 42 + }; 32 43 33 - return colorMapping[color] || "#3A3A3A" 44 + return colorMapping[color] || "#3A3A3A"; 34 45 } 35 46 36 47 function getDarkModeEventBackgroundColor(color: string) { ··· 42 53 "bg-[#F3EEFE]": "#483A63", 43 54 "bg-[#FCE7F3]": "#5A334A", 44 55 "bg-[#E6FAF7]": "#1F4A47", 45 - } 56 + }; 46 57 47 - return darkModeColorMapping[color] 58 + return darkModeColorMapping[color]; 48 59 } 49 60 50 61 export default function YearView({ ··· 56 67 isSidebarCollapsed = false, 57 68 isSidebarExpanding = false, 58 69 }: YearViewProps) { 59 - const t = translations[language] 60 - const currentYear = date.getFullYear() 61 - const today = new Date() 62 - const [openDayKey, setOpenDayKey] = useState<string | null>(null) 63 - const isDark = typeof document !== "undefined" && document.documentElement.classList.contains("dark") 70 + const t = translations[language]; 71 + const currentYear = date.getFullYear(); 72 + const today = new Date(); 73 + const [openDayKey, setOpenDayKey] = useState<string | null>(null); 74 + const isDark = 75 + typeof document !== "undefined" && 76 + document.documentElement.classList.contains("dark"); 64 77 65 78 const weekdayLabels = useMemo( 66 - () => [...t.weekdays.slice(firstDayOfWeek), ...t.weekdays.slice(0, firstDayOfWeek)], 79 + () => [ 80 + ...t.weekdays.slice(firstDayOfWeek), 81 + ...t.weekdays.slice(0, firstDayOfWeek), 82 + ], 67 83 [firstDayOfWeek, t.weekdays], 68 - ) 84 + ); 69 85 70 86 const eventsByDayKey = useMemo(() => { 71 - const grouped = new Map<string, CalendarEvent[]>() 87 + const grouped = new Map<string, CalendarEvent[]>(); 72 88 events.forEach((event) => { 73 - const eventDate = new Date(event.startDate) 74 - const key = format(eventDate, "yyyy-MM-dd") 75 - const existing = grouped.get(key) ?? [] 76 - existing.push(event) 77 - grouped.set(key, existing) 78 - }) 89 + const eventDate = new Date(event.startDate); 90 + const key = format(eventDate, "yyyy-MM-dd"); 91 + const existing = grouped.get(key) ?? []; 92 + existing.push(event); 93 + grouped.set(key, existing); 94 + }); 79 95 80 96 grouped.forEach((dayEvents) => { 81 - dayEvents.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()) 82 - }) 97 + dayEvents.sort( 98 + (a, b) => 99 + new Date(a.startDate).getTime() - new Date(b.startDate).getTime(), 100 + ); 101 + }); 83 102 84 - return grouped 85 - }, [events]) 103 + return grouped; 104 + }, [events]); 86 105 87 106 const months = useMemo( 88 107 () => 89 108 Array.from({ length: 12 }, (_, monthIndex) => { 90 - const monthStart = new Date(currentYear, monthIndex, 1) 91 - const monthEnd = endOfMonth(monthStart) 109 + const monthStart = new Date(currentYear, monthIndex, 1); 110 + const monthEnd = endOfMonth(monthStart); 92 111 const gridStart = startOfWeek(monthStart, { 93 112 weekStartsOn: firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6, 94 - }) 95 - const monthDays = eachDayOfInterval({ start: gridStart, end: monthEnd }) 113 + }); 114 + const monthDays = eachDayOfInterval({ 115 + start: gridStart, 116 + end: monthEnd, 117 + }); 96 118 97 119 while (monthDays.length < 42) { 98 - const lastDay = monthDays[monthDays.length - 1] 120 + const lastDay = monthDays[monthDays.length - 1]; 99 121 monthDays.push( 100 - new Date(lastDay.getFullYear(), lastDay.getMonth(), lastDay.getDate() + 1), 101 - ) 122 + new Date( 123 + lastDay.getFullYear(), 124 + lastDay.getMonth(), 125 + lastDay.getDate() + 1, 126 + ), 127 + ); 102 128 } 103 129 104 130 return { 105 131 monthIndex, 106 132 label: t.months[monthIndex] ?? format(monthStart, "LLLL"), 107 133 days: monthDays, 108 - } 134 + }; 109 135 }), 110 136 [currentYear, firstDayOfWeek, t.months], 111 - ) 137 + ); 112 138 113 139 return ( 114 140 <div className="p-3 md:p-4"> ··· 122 148 > 123 149 {months.map((month) => ( 124 150 <section key={month.label} className="space-y-1"> 125 - <h2 className="text-lg font-semibold tracking-tight">{month.label}</h2> 151 + <h2 className="text-lg font-semibold tracking-tight"> 152 + {month.label} 153 + </h2> 126 154 <div className="grid grid-cols-7 gap-y-1 text-center"> 127 155 {weekdayLabels.map((weekday) => ( 128 - <div key={`${month.label}-${weekday}`} className="text-xs text-muted-foreground"> 156 + <div 157 + key={`${month.label}-${weekday}`} 158 + className="text-xs text-muted-foreground" 159 + > 129 160 {weekday} 130 161 </div> 131 162 ))} 132 163 133 164 {month.days.map((day) => { 134 - const dayKey = format(day, "yyyy-MM-dd") 135 - const popoverKey = `${month.monthIndex}-${dayKey}` 136 - const isToday = isSameDay(day, today) 137 - const isCurrentMonth = isSameMonth(day, new Date(currentYear, month.monthIndex, 1)) 138 - const dayEvents = eventsByDayKey.get(dayKey) ?? [] 165 + const dayKey = format(day, "yyyy-MM-dd"); 166 + const popoverKey = `${month.monthIndex}-${dayKey}`; 167 + const isToday = isSameDay(day, today); 168 + const isCurrentMonth = isSameMonth( 169 + day, 170 + new Date(currentYear, month.monthIndex, 1), 171 + ); 172 + const dayEvents = eventsByDayKey.get(dayKey) ?? []; 139 173 140 174 return ( 141 175 <Popover 142 176 key={`${month.label}-${day.toISOString()}`} 143 177 open={openDayKey === popoverKey} 144 - onOpenChange={(open) => setOpenDayKey(open ? popoverKey : null)} 178 + onOpenChange={(open) => 179 + setOpenDayKey(open ? popoverKey : null) 180 + } 145 181 > 146 182 <PopoverTrigger asChild> 147 183 <button ··· 150 186 "mx-auto flex h-6 w-6 items-center justify-center rounded-full text-xs transition-colors hover:bg-accent", 151 187 !isCurrentMonth && "text-muted-foreground", 152 188 dayEvents.length > 0 && "font-semibold", 153 - isToday && isCurrentMonth && 189 + isToday && 190 + isCurrentMonth && 154 191 "bg-[#0052CC] text-white hover:bg-[#0047B3] data-[state=open]:bg-[#0047B3] green:bg-[#24a854] green:hover:bg-[#1f9249] green:data-[state=open]:bg-[#1f9249] orange:bg-[#e26912] orange:hover:bg-[#c85a0f] orange:data-[state=open]:bg-[#c85a0f] azalea:bg-[#CD2F7B] azalea:hover:bg-[#b2266b] azalea:data-[state=open]:bg-[#b2266b]", 155 192 )} 156 193 > ··· 160 197 <PopoverContent side="right" align="start" className="w-72"> 161 198 <div className="space-y-2"> 162 199 <div className="text-sm font-medium"> 163 - {day.toLocaleDateString(isZhLanguage(language) ? "zh-CN" : "en-US", { 164 - year: "numeric", 165 - month: "long", 166 - day: "numeric", 167 - })} 200 + {day.toLocaleDateString( 201 + isZhLanguage(language) ? "zh-CN" : "en-US", 202 + { 203 + year: "numeric", 204 + month: "long", 205 + day: "numeric", 206 + }, 207 + )} 168 208 </div> 169 209 170 210 {dayEvents.length > 0 ? ( ··· 177 217 "relative w-full cursor-pointer truncate rounded-md p-1.5 pl-3 text-left text-xs", 178 218 event.color, 179 219 )} 180 - onClick={() => { setOpenDayKey(null); onEventClick(event); }} 220 + onClick={() => { 221 + setOpenDayKey(null); 222 + onEventClick(event); 223 + }} 181 224 style={{ 182 - backgroundColor: isDark ? getDarkModeEventBackgroundColor(event.color) : undefined, 225 + backgroundColor: isDark 226 + ? getDarkModeEventBackgroundColor( 227 + event.color, 228 + ) 229 + : undefined, 183 230 }} 184 231 > 185 232 <div 186 233 className="absolute left-0 top-0 h-full w-1 rounded-l-md" 187 - style={{ backgroundColor: getDarkerColorClass(event.color) }} 234 + style={{ 235 + backgroundColor: getDarkerColorClass( 236 + event.color, 237 + ), 238 + }} 188 239 /> 189 - <div style={{ color: getDarkerColorClass(event.color) }} className="truncate"> 240 + <div 241 + style={{ 242 + color: getDarkerColorClass(event.color), 243 + }} 244 + className="truncate" 245 + > 190 246 {event.title || t.unnamedEvent} 191 247 </div> 192 248 </button> 193 249 ))} 194 250 </div> 195 251 ) : ( 196 - <div className="text-xs text-muted-foreground">{t.noEventsFound}</div> 252 + <div className="text-xs text-muted-foreground"> 253 + {t.noEventsFound} 254 + </div> 197 255 )} 198 256 </div> 199 257 </PopoverContent> 200 258 </Popover> 201 - ) 259 + ); 202 260 })} 203 261 </div> 204 262 </section> 205 263 ))} 206 264 </div> 207 265 </div> 208 - ) 266 + ); 209 267 }
+71 -36
components/auth/atproto-login-form.tsx
··· 2 2 3 3 import { Suspense, useState } from "react"; 4 4 import { useSearchParams } from "next/navigation"; 5 - import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 5 + import { 6 + Card, 7 + CardContent, 8 + CardDescription, 9 + CardHeader, 10 + CardTitle, 11 + } from "@/components/ui/card"; 6 12 import { Input } from "@/components/ui/input"; 7 13 import { Button } from "@/components/ui/button"; 8 14 ··· 23 29 body: JSON.stringify({ handle }), 24 30 }); 25 31 26 - const data = (await res.json()) as { authorizeUrl?: string; error?: string }; 32 + const data = (await res.json()) as { 33 + authorizeUrl?: string; 34 + error?: string; 35 + }; 27 36 if (!res.ok || !data.authorizeUrl) { 28 37 throw new Error(data.error || "Failed to start OAuth login"); 29 38 } ··· 36 45 } 37 46 }; 38 47 39 - const queryError = searchParams.get("reason") || searchParams.get("error") || ""; 48 + const queryError = 49 + searchParams.get("reason") || searchParams.get("error") || ""; 40 50 41 51 const startRegister = async () => { 42 52 setRegisterLoading(true); ··· 47 57 headers: { "Content-Type": "application/json" }, 48 58 }); 49 59 50 - const data = (await res.json()) as { authorizeUrl?: string; error?: string }; 60 + const data = (await res.json()) as { 61 + authorizeUrl?: string; 62 + error?: string; 63 + }; 51 64 if (!res.ok || !data.authorizeUrl) { 52 - throw new Error(data.error || "Failed to create registration OAuth URL"); 65 + throw new Error( 66 + data.error || "Failed to create registration OAuth URL", 67 + ); 53 68 } 54 69 55 70 window.location.href = data.authorizeUrl; ··· 60 75 } 61 76 }; 62 77 63 - 64 78 return ( 65 79 <div className="space-y-4"> 66 80 <Card> 67 - <CardHeader className="text-center"> 68 - <CardTitle className="text-xl">Sign in with Atmosphere</CardTitle> 69 - <CardDescription>Enter your Atmosphere handle to continue with OAuth</CardDescription> 70 - </CardHeader> 71 - <CardContent className="space-y-4"> 72 - <Input 73 - value={handle} 74 - onChange={(e) => setHandle(e.target.value)} 75 - placeholder="alice.bsky.social" 76 - autoComplete="username" 77 - /> 78 - <Button className="w-full bg-[#0066ff] hover:bg-[#0052cc] text-white" onClick={startLogin} disabled={!handle || loading}> 79 - {loading ? "Redirecting..." : "Continue with Atmosphere OAuth"} 80 - </Button> 81 - {error || queryError ? <p className="text-sm text-red-500">{error || queryError}</p> : null} 82 - <Button 83 - variant="outline" 84 - className="w-full" 85 - type="button" 86 - onClick={startRegister} 87 - disabled={registerLoading} 88 - > 89 - {registerLoading ? "Preparing..." : "Create an Atmosphere account"} 90 - </Button> 91 - <p className="pt-1 text-center text-xs text-muted-foreground"> 92 - Not an Atmosphere user? Return to normal <a href="/sign-in" className="underline underline-offset-4 hover:text-primary">sign in</a> 93 - </p> 94 - </CardContent> 81 + <CardHeader className="text-center"> 82 + <CardTitle className="text-xl">Sign in with Atmosphere</CardTitle> 83 + <CardDescription> 84 + Enter your Atmosphere handle to continue with OAuth 85 + </CardDescription> 86 + </CardHeader> 87 + <CardContent className="space-y-4"> 88 + <Input 89 + value={handle} 90 + onChange={(e) => setHandle(e.target.value)} 91 + placeholder="alice.bsky.social" 92 + autoComplete="username" 93 + /> 94 + <Button 95 + className="w-full bg-[#0066ff] hover:bg-[#0052cc] text-white" 96 + onClick={startLogin} 97 + disabled={!handle || loading} 98 + > 99 + {loading ? "Redirecting..." : "Continue with Atmosphere OAuth"} 100 + </Button> 101 + {error || queryError ? ( 102 + <p className="text-sm text-red-500">{error || queryError}</p> 103 + ) : null} 104 + <Button 105 + variant="outline" 106 + className="w-full" 107 + type="button" 108 + onClick={startRegister} 109 + disabled={registerLoading} 110 + > 111 + {registerLoading ? "Preparing..." : "Create an Atmosphere account"} 112 + </Button> 113 + <p className="pt-1 text-center text-xs text-muted-foreground"> 114 + Not an Atmosphere user? Return to normal{" "} 115 + <a 116 + href="/sign-in" 117 + className="underline underline-offset-4 hover:text-primary" 118 + > 119 + sign in 120 + </a> 121 + </p> 122 + </CardContent> 95 123 </Card> 96 124 <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 97 - By continuing, you agree to our <a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a>. 125 + By continuing, you agree to our <a href="/terms">Terms of Service</a>{" "} 126 + and <a href="/privacy">Privacy Policy</a>. 98 127 </div> 99 128 </div> 100 129 ); ··· 102 131 103 132 export function AtprotoLoginForm() { 104 133 return ( 105 - <Suspense fallback={<div className="text-sm text-muted-foreground text-center">Loading...</div>}> 134 + <Suspense 135 + fallback={ 136 + <div className="text-sm text-muted-foreground text-center"> 137 + Loading... 138 + </div> 139 + } 140 + > 106 141 <AtprotoLoginContent /> 107 142 </Suspense> 108 143 );
+13 -4
components/auth/auth-brand.tsx
··· 1 - import Image from "next/image" 1 + import Image from "next/image"; 2 2 3 3 export function AuthBrand() { 4 4 return ( 5 - <a href="https://xyehr.cn" className="flex items-center gap-2 self-center font-medium text-foreground"> 5 + <a 6 + href="https://xyehr.cn" 7 + className="flex items-center gap-2 self-center font-medium text-foreground" 8 + > 6 9 <span className="flex size-6 items-center justify-center rounded-md bg-primary/15 text-primary"> 7 - <Image src="/icon.svg" alt="One Calendar" width={16} height={16} className="size-4" /> 10 + <Image 11 + src="/icon.svg" 12 + alt="One Calendar" 13 + width={16} 14 + height={16} 15 + className="size-4" 16 + /> 8 17 </span> 9 18 <span>One Calendar</span> 10 19 </a> 11 - ) 20 + ); 12 21 }
+75 -40
components/auth/login-form.tsx
··· 2 2 3 3 import { Button } from "@/components/ui/button"; 4 4 import { cn } from "@/lib/utils"; 5 - import { 6 - Card, 7 - CardContent, 8 - CardHeader, 9 - CardTitle, 10 - } from "@/components/ui/card"; 5 + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 11 6 import { Input } from "@/components/ui/input"; 12 7 import { Label } from "@/components/ui/label"; 13 8 import { useSignIn } from "@clerk/nextjs"; ··· 25 20 const [isLoading, setIsLoading] = useState(false); 26 21 const [error, setError] = useState(""); 27 22 const [isCaptchaCompleted, setIsCaptchaCompleted] = useState( 28 - process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true 23 + process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true, 29 24 ); 30 25 const turnstileRef = useRef<any>(null); 31 26 const router = useRouter(); ··· 46 41 setError(""); 47 42 } else { 48 43 setIsCaptchaCompleted(false); 49 - setError(`CAPTCHA verification failed: ${data.details?.join(", ") || "Unknown error"}`); 44 + setError( 45 + `CAPTCHA verification failed: ${data.details?.join(", ") || "Unknown error"}`, 46 + ); 50 47 if (turnstileRef.current) { 51 48 turnstileRef.current.reset(); 52 49 } ··· 83 80 router.push("/app"); 84 81 } 85 82 } catch (err: any) { 86 - setError(err.errors?.[0]?.longMessage || "Login failed. Please try again."); 83 + setError( 84 + err.errors?.[0]?.longMessage || "Login failed. Please try again.", 85 + ); 87 86 if (siteKey) { 88 87 setIsCaptchaCompleted(false); 89 88 if (turnstileRef.current) { ··· 95 94 } 96 95 }; 97 96 98 - const handleOAuthLogin = (strategy: "oauth_google" | "oauth_microsoft" | "oauth_github") => { 97 + const handleOAuthLogin = ( 98 + strategy: "oauth_google" | "oauth_microsoft" | "oauth_github", 99 + ) => { 99 100 const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; 100 101 if (siteKey && !isCaptchaCompleted) { 101 102 setError("Please complete the CAPTCHA verification."); ··· 126 127 type="button" 127 128 onClick={() => handleOAuthLogin("oauth_microsoft")} 128 129 > 129 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23" width="20" height="20"> 130 - <path fill="#f25022" d="M1 1h10v10H1z"/> 131 - <path fill="#00a4ef" d="M12 1h10v10H12z"/> 132 - <path fill="#7fba00" d="M1 12h10v10H1z"/> 133 - <path fill="#ffb900" d="M12 12h10v10H12z"/> 130 + <svg 131 + xmlns="http://www.w3.org/2000/svg" 132 + viewBox="0 0 23 23" 133 + width="20" 134 + height="20" 135 + > 136 + <path fill="#f25022" d="M1 1h10v10H1z" /> 137 + <path fill="#00a4ef" d="M12 1h10v10H12z" /> 138 + <path fill="#7fba00" d="M1 12h10v10H1z" /> 139 + <path fill="#ffb900" d="M12 12h10v10H12z" /> 134 140 </svg> 135 141 <span className="ml-2">Login with Microsoft</span> 136 142 </Button> ··· 140 146 type="button" 141 147 onClick={() => handleOAuthLogin("oauth_google")} 142 148 > 143 - <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"> 144 - <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/> 145 - <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/> 146 - <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/> 147 - <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/> 148 - <path d="M1 1h22v22H1z" fill="none"/> 149 + <svg 150 + xmlns="http://www.w3.org/2000/svg" 151 + height="24" 152 + viewBox="0 0 24 24" 153 + width="24" 154 + > 155 + <path 156 + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" 157 + fill="#4285F4" 158 + /> 159 + <path 160 + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" 161 + fill="#34A853" 162 + /> 163 + <path 164 + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" 165 + fill="#FBBC05" 166 + /> 167 + <path 168 + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" 169 + fill="#EA4335" 170 + /> 171 + <path d="M1 1h22v22H1z" fill="none" /> 149 172 </svg> 150 173 <span className="ml-2">Login with Google</span> 151 174 </Button> ··· 155 178 type="button" 156 179 onClick={() => handleOAuthLogin("oauth_github")} 157 180 > 158 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"> 181 + <svg 182 + xmlns="http://www.w3.org/2000/svg" 183 + viewBox="0 0 24 24" 184 + width="20" 185 + height="20" 186 + > 159 187 <path 160 188 d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" 161 189 fill="currentColor" ··· 200 228 /> 201 229 </div> 202 230 {siteKey && ( 203 - <div className="turnstile-container"> 204 - <Turnstile 205 - ref={turnstileRef} 206 - siteKey={siteKey} 207 - onSuccess={handleTurnstileSuccess} 208 - onError={() => { 209 - console.error("Turnstile widget error"); 210 - setIsCaptchaCompleted(false); 211 - setError("CAPTCHA initialization failed. Please try again."); 212 - }} 213 - options={{ theme: "auto", action: "login", cData: "login-page", refreshExpired: "auto", size: "flexible" }} 214 - /> 215 - </div> 216 - )} 217 - {error && ( 218 - <div className="text-sm text-red-500">{error}</div> 231 + <div className="turnstile-container"> 232 + <Turnstile 233 + ref={turnstileRef} 234 + siteKey={siteKey} 235 + onSuccess={handleTurnstileSuccess} 236 + onError={() => { 237 + console.error("Turnstile widget error"); 238 + setIsCaptchaCompleted(false); 239 + setError( 240 + "CAPTCHA initialization failed. Please try again.", 241 + ); 242 + }} 243 + options={{ 244 + theme: "auto", 245 + action: "login", 246 + cData: "login-page", 247 + refreshExpired: "auto", 248 + size: "flexible", 249 + }} 250 + /> 251 + </div> 219 252 )} 253 + {error && <div className="text-sm text-red-500">{error}</div>} 220 254 <Button 221 255 type="submit" 222 256 className="w-full bg-[#0066ff] hover:bg-[#0047cc] text-white" ··· 236 270 </CardContent> 237 271 </Card> 238 272 <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 239 - By clicking continue, you agree to our <a href="/terms">Terms of Service</a>{" "} 240 - and <a href="/privacy">Privacy Policy</a>. 273 + By clicking continue, you agree to our{" "} 274 + <a href="/terms">Terms of Service</a> and{" "} 275 + <a href="/privacy">Privacy Policy</a>. 241 276 </div> 242 277 </div> 243 278 );
+88 -64
components/auth/reset-form.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" 4 - import { Turnstile } from "@marsidev/react-turnstile" 5 - import { Button } from "@/components/ui/button" 6 - import { Input } from "@/components/ui/input" 7 - import { Label } from "@/components/ui/label" 8 - import { useRouter } from "next/navigation" 9 - import { useSignIn } from "@clerk/nextjs" 10 - import { useState, useRef } from "react" 11 - import { cn } from "@/lib/utils" 12 - import type React from "react" 3 + import { 4 + Card, 5 + CardContent, 6 + CardDescription, 7 + CardHeader, 8 + CardTitle, 9 + } from "@/components/ui/card"; 10 + import { Turnstile } from "@marsidev/react-turnstile"; 11 + import { Button } from "@/components/ui/button"; 12 + import { Input } from "@/components/ui/input"; 13 + import { Label } from "@/components/ui/label"; 14 + import { useRouter } from "next/navigation"; 15 + import { useSignIn } from "@clerk/nextjs"; 16 + import { useState, useRef } from "react"; 17 + import { cn } from "@/lib/utils"; 18 + import type React from "react"; 13 19 14 - export function ResetPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<"div">) { 15 - const { signIn } = useSignIn() 16 - const router = useRouter() 17 - const [step, setStep] = useState<"email" | "code" | "password">("email") 20 + export function ResetPasswordForm({ 21 + className, 22 + ...props 23 + }: React.ComponentPropsWithoutRef<"div">) { 24 + const { signIn } = useSignIn(); 25 + const router = useRouter(); 26 + const [step, setStep] = useState<"email" | "code" | "password">("email"); 18 27 const [formData, setFormData] = useState({ 19 28 email: "", 20 29 code: "", 21 30 password: "", 22 - }) 23 - const [isLoading, setIsLoading] = useState(false) 24 - const [error, setError] = useState("") 31 + }); 32 + const [isLoading, setIsLoading] = useState(false); 33 + const [error, setError] = useState(""); 25 34 const [isCaptchaCompleted, setIsCaptchaCompleted] = useState( 26 35 process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true, 27 - ) 28 - const turnstileRef = useRef<any>(null) 36 + ); 37 + const turnstileRef = useRef<any>(null); 29 38 30 39 const handleTurnstileSuccess = async (token: string) => { 31 40 try { ··· 33 42 method: "POST", 34 43 headers: { "Content-Type": "application/json" }, 35 44 body: JSON.stringify({ token, action: "reset-password" }), 36 - }) 45 + }); 37 46 38 47 if (!response.ok) { 39 - throw new Error(`HTTP error! Status: ${response.status}`) 48 + throw new Error(`HTTP error! Status: ${response.status}`); 40 49 } 41 50 42 - const data = await response.json() 51 + const data = await response.json(); 43 52 44 53 if (data.success) { 45 - setIsCaptchaCompleted(true) 46 - setError("") 54 + setIsCaptchaCompleted(true); 55 + setError(""); 47 56 } else { 48 - setIsCaptchaCompleted(false) 49 - setError(`CAPTCHA verification failed: ${data.details?.join(", ") || "Unknown error"}`) 57 + setIsCaptchaCompleted(false); 58 + setError( 59 + `CAPTCHA verification failed: ${data.details?.join(", ") || "Unknown error"}`, 60 + ); 50 61 if (turnstileRef.current) { 51 - turnstileRef.current.reset() 62 + turnstileRef.current.reset(); 52 63 } 53 64 } 54 65 } catch (err) { 55 - setIsCaptchaCompleted(false) 56 - setError("Error verifying CAPTCHA. Please try again.") 66 + setIsCaptchaCompleted(false); 67 + setError("Error verifying CAPTCHA. Please try again."); 57 68 if (turnstileRef.current) { 58 - turnstileRef.current.reset() 69 + turnstileRef.current.reset(); 59 70 } 60 71 } 61 - } 72 + }; 62 73 63 74 const handleSubmit = async (e: React.FormEvent) => { 64 - e.preventDefault() 65 - const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY 75 + e.preventDefault(); 76 + const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; 66 77 if (siteKey && !isCaptchaCompleted && step === "email") { 67 - setError("Please complete the CAPTCHA verification.") 68 - return 78 + setError("Please complete the CAPTCHA verification."); 79 + return; 69 80 } 70 - setIsLoading(true) 71 - setError("") 81 + setIsLoading(true); 82 + setError(""); 72 83 73 84 try { 74 85 if (step === "email") { 75 86 await signIn?.create({ 76 87 strategy: "reset_password_email_code", 77 88 identifier: formData.email, 78 - }) 79 - setStep("code") 89 + }); 90 + setStep("code"); 80 91 } else if (step === "code") { 81 92 const result = await signIn?.attemptFirstFactor({ 82 93 strategy: "reset_password_email_code", 83 94 code: formData.code, 84 - }) 95 + }); 85 96 if (result?.status === "needs_new_password") { 86 - setStep("password") 97 + setStep("password"); 87 98 } 88 99 } else { 89 100 const result = await signIn?.resetPassword({ 90 101 password: formData.password, 91 - }) 102 + }); 92 103 if (result?.status === "complete") { 93 - router.push("/app") 104 + router.push("/app"); 94 105 } 95 106 } 96 107 } catch (err: any) { 97 - setError(err.errors?.[0]?.longMessage || "An error occurred. Please try again.") 108 + setError( 109 + err.errors?.[0]?.longMessage || "An error occurred. Please try again.", 110 + ); 98 111 if (siteKey && err.errors && step === "email") { 99 - setIsCaptchaCompleted(false) 112 + setIsCaptchaCompleted(false); 100 113 if (turnstileRef.current) { 101 - turnstileRef.current.reset() 114 + turnstileRef.current.reset(); 102 115 } 103 116 } 104 117 } finally { 105 - setIsLoading(false) 118 + setIsLoading(false); 106 119 } 107 - } 120 + }; 108 121 109 122 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { 110 123 setFormData({ 111 124 ...formData, 112 125 [e.target.name]: e.target.value, 113 - }) 114 - } 126 + }); 127 + }; 115 128 116 129 const getStepContent = () => { 117 130 switch (step) { 118 131 case "email": 119 132 return { 120 133 title: "Reset your password", 121 - description: "Enter your email address and we'll send you a verification code", 134 + description: 135 + "Enter your email address and we'll send you a verification code", 122 136 fields: ( 123 137 <div className="grid gap-2"> 124 138 <Label htmlFor="email">Email</Label> ··· 135 149 </div> 136 150 ), 137 151 buttonText: "Send verification code", 138 - } 152 + }; 139 153 case "code": 140 154 return { 141 155 title: "Enter verification code", ··· 155 169 </div> 156 170 ), 157 171 buttonText: "Verify code", 158 - } 172 + }; 159 173 case "password": 160 174 return { 161 175 title: "Set new password", ··· 175 189 </div> 176 190 ), 177 191 buttonText: "Reset password", 178 - } 192 + }; 179 193 } 180 - } 194 + }; 181 195 182 - const stepContent = getStepContent() 183 - const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY 196 + const stepContent = getStepContent(); 197 + const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; 184 198 185 199 return ( 186 200 <div className={cn("flex flex-col gap-6", className)} {...props}> ··· 201 215 siteKey={siteKey} 202 216 onSuccess={handleTurnstileSuccess} 203 217 onError={() => { 204 - setIsCaptchaCompleted(false) 205 - setError("CAPTCHA initialization failed. Please try again.") 218 + setIsCaptchaCompleted(false); 219 + setError( 220 + "CAPTCHA initialization failed. Please try again.", 221 + ); 206 222 }} 207 223 options={{ 208 224 theme: "auto", ··· 220 236 <Button 221 237 type="submit" 222 238 className="w-full bg-[#0066ff] hover:bg-[#0047cc] text-white" 223 - disabled={siteKey && step === "email" ? !isCaptchaCompleted || isLoading : isLoading} 239 + disabled={ 240 + siteKey && step === "email" 241 + ? !isCaptchaCompleted || isLoading 242 + : isLoading 243 + } 224 244 > 225 245 {isLoading ? "Processing..." : stepContent.buttonText} 226 246 </Button> 227 247 228 248 <div className="text-center text-sm"> 229 249 Remember your password?{" "} 230 - <a href="/sign-in" className="underline underline-offset-4 hover:text-primary"> 250 + <a 251 + href="/sign-in" 252 + className="underline underline-offset-4 hover:text-primary" 253 + > 231 254 Sign in 232 255 </a> 233 256 </div> ··· 237 260 </Card> 238 261 239 262 <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 240 - By continuing, you agree to our <a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a>. 263 + By continuing, you agree to our <a href="/terms">Terms of Service</a>{" "} 264 + and <a href="/privacy">Privacy Policy</a>. 241 265 </div> 242 266 </div> 243 - ) 267 + ); 244 268 }
+108 -34
components/auth/sign-up-form.tsx
··· 2 2 3 3 import { Button } from "@/components/ui/button"; 4 4 import { cn } from "@/lib/utils"; 5 - import { 6 - Card, 7 - CardContent, 8 - CardHeader, 9 - CardTitle, 10 - } from "@/components/ui/card"; 5 + import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 11 6 import { Input } from "@/components/ui/input"; 12 7 import { Label } from "@/components/ui/label"; 13 8 import { useSignUp } from "@clerk/nextjs"; ··· 32 27 const [isLoading, setIsLoading] = useState(false); 33 28 const [error, setError] = useState(""); 34 29 const [isCaptchaCompleted, setIsCaptchaCompleted] = useState( 35 - process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true 30 + process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true, 36 31 ); 37 32 const turnstileRef = useRef<any>(null); 38 33 ··· 56 51 throw new Error("Invalid JSON response from server"); 57 52 } 58 53 59 - 60 54 if (data.success) { 61 55 setIsCaptchaCompleted(true); 62 56 setError(""); 63 57 } else { 64 58 setIsCaptchaCompleted(false); 65 - setError(`CAPTCHA verification failed: ${data.details?.join(", ") || "Unknown error"}`); 59 + setError( 60 + `CAPTCHA verification failed: ${data.details?.join(", ") || "Unknown error"}`, 61 + ); 66 62 if (turnstileRef.current) { 67 63 turnstileRef.current.reset(); 68 64 } ··· 77 73 }; 78 74 79 75 const allowedEmailDomains = [ 80 - "@qq.com", "@163.com", "@aliyun.com", "@dingtalk.com", "@email.cn", "@foxmail.com", 81 - "@gmail.com", "@gmx.com", "@gmx.de", "@hotmail.com", "@live.cn", "@live.com", "@mail.com", 82 - "@mail.retiehe.com", "@mail.ru", "@me.com", "@msn.cn", "@msn.com", "@my.com", "@net-c.com", 83 - "@outlook.com", "@outlook.jp", "@petalmail.com", "@retinbox.com", "@sina.cn", "@sina.com", 84 - "@sohu.com", "@tom.com", "@tutanota.com", "@vip.qq.com", "@vip.163.com", "@wo.cn", 85 - "@yahoo.co.jp", "@yahoo.com", "@yahoo.com.hk", "@yahoo.com.tw", "@yandex.com", "@yandex.ru", 86 - "@yeah.net", "@111.com", "@126.com", "@139.com", "@proton.me", "@pm.me", 87 - "@protonmail.com", "@protonmail.ch" 76 + "@qq.com", 77 + "@163.com", 78 + "@aliyun.com", 79 + "@dingtalk.com", 80 + "@email.cn", 81 + "@foxmail.com", 82 + "@gmail.com", 83 + "@gmx.com", 84 + "@gmx.de", 85 + "@hotmail.com", 86 + "@live.cn", 87 + "@live.com", 88 + "@mail.com", 89 + "@mail.retiehe.com", 90 + "@mail.ru", 91 + "@me.com", 92 + "@msn.cn", 93 + "@msn.com", 94 + "@my.com", 95 + "@net-c.com", 96 + "@outlook.com", 97 + "@outlook.jp", 98 + "@petalmail.com", 99 + "@retinbox.com", 100 + "@sina.cn", 101 + "@sina.com", 102 + "@sohu.com", 103 + "@tom.com", 104 + "@tutanota.com", 105 + "@vip.qq.com", 106 + "@vip.163.com", 107 + "@wo.cn", 108 + "@yahoo.co.jp", 109 + "@yahoo.com", 110 + "@yahoo.com.hk", 111 + "@yahoo.com.tw", 112 + "@yandex.com", 113 + "@yandex.ru", 114 + "@yeah.net", 115 + "@111.com", 116 + "@126.com", 117 + "@139.com", 118 + "@proton.me", 119 + "@pm.me", 120 + "@protonmail.com", 121 + "@protonmail.ch", 88 122 ]; 89 123 90 124 const isEmailDomainAllowed = (email: string) => { 91 - return allowedEmailDomains.some(domain => email.toLowerCase().endsWith(domain)); 125 + return allowedEmailDomains.some((domain) => 126 + email.toLowerCase().endsWith(domain), 127 + ); 92 128 }; 93 129 94 - const handleOAuthSignUp = (strategy: "oauth_google" | "oauth_microsoft" | "oauth_github") => { 130 + const handleOAuthSignUp = ( 131 + strategy: "oauth_google" | "oauth_microsoft" | "oauth_github", 132 + ) => { 95 133 const siteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY; 96 134 if (siteKey && !isCaptchaCompleted) { 97 135 setError("Please complete the CAPTCHA verification."); ··· 117 155 try { 118 156 if (step === "initial") { 119 157 if (!isEmailDomainAllowed(formData.email)) { 120 - setError("Email domain is not allowed. Please use a supported email provider."); 158 + setError( 159 + "Email domain is not allowed. Please use a supported email provider.", 160 + ); 121 161 setIsLoading(false); 122 162 return; 123 163 } ··· 140 180 } 141 181 } 142 182 } catch (err: any) { 143 - setError(err.errors?.[0]?.longMessage || "An error occurred. Please try again."); 183 + setError( 184 + err.errors?.[0]?.longMessage || "An error occurred. Please try again.", 185 + ); 144 186 if (siteKey && err.errors) { 145 187 setIsCaptchaCompleted(false); 146 188 if (turnstileRef.current) { ··· 185 227 /> 186 228 </div> 187 229 {error && <div className="text-sm text-red-500">{error}</div>} 188 - <Button type="submit" className="w-full bg-[#0066ff] hover:bg-[#0047cc] text-white" disabled={isLoading}> 230 + <Button 231 + type="submit" 232 + className="w-full bg-[#0066ff] hover:bg-[#0047cc] text-white" 233 + disabled={isLoading} 234 + > 189 235 {isLoading ? "Verifying..." : "Verify Email"} 190 236 </Button> 191 237 <div className="text-center text-sm"> ··· 231 277 type="button" 232 278 onClick={() => handleOAuthSignUp("oauth_microsoft")} 233 279 > 234 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23" width="20" height="20"> 280 + <svg 281 + xmlns="http://www.w3.org/2000/svg" 282 + viewBox="0 0 23 23" 283 + width="20" 284 + height="20" 285 + > 235 286 <path fill="#f25022" d="M1 1h10v10H1z" /> 236 287 <path fill="#00a4ef" d="M12 1h10v10H12z" /> 237 288 <path fill="#7fba00" d="M1 12h10v10H1z" /> ··· 245 296 type="button" 246 297 onClick={() => handleOAuthSignUp("oauth_google")} 247 298 > 248 - <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"> 249 - <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" /> 250 - <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" /> 251 - <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" /> 252 - <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" /> 299 + <svg 300 + xmlns="http://www.w3.org/2000/svg" 301 + height="24" 302 + viewBox="0 0 24 24" 303 + width="24" 304 + > 305 + <path 306 + d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" 307 + fill="#4285F4" 308 + /> 309 + <path 310 + d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" 311 + fill="#34A853" 312 + /> 313 + <path 314 + d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" 315 + fill="#FBBC05" 316 + /> 317 + <path 318 + d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" 319 + fill="#EA4335" 320 + /> 253 321 <path d="M1 1h22v22H1z" fill="none" /> 254 322 </svg> 255 323 <span className="ml-2">Continue with Google</span> ··· 260 328 type="button" 261 329 onClick={() => handleOAuthSignUp("oauth_github")} 262 330 > 263 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"> 331 + <svg 332 + xmlns="http://www.w3.org/2000/svg" 333 + viewBox="0 0 24 24" 334 + width="20" 335 + height="20" 336 + > 264 337 <path 265 338 d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" 266 339 fill="currentColor" ··· 334 407 onError={() => { 335 408 console.error("Turnstile widget error"); 336 409 setIsCaptchaCompleted(false); 337 - setError("CAPTCHA initialization failed. Please try again."); 410 + setError( 411 + "CAPTCHA initialization failed. Please try again.", 412 + ); 338 413 }} 339 414 options={{ 340 415 theme: "auto", ··· 375 450 </Card> 376 451 377 452 <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 378 - By continuing, you agree to our{" "} 379 - <a href="/terms">Terms of Service</a> and{" "} 380 - <a href="/privacy">Privacy Policy</a>. 453 + By continuing, you agree to our <a href="/terms">Terms of Service</a>{" "} 454 + and <a href="/privacy">Privacy Policy</a>. 381 455 </div> 382 456 </div> 383 457 ); 384 - } 458 + }
+4 -4
components/icons/clock-dashed.tsx
··· 1 - 'use client' 1 + "use client"; 2 2 3 - import React from 'react' 3 + import React from "react"; 4 4 5 5 export function ClockDashed(props: React.SVGProps<SVGSVGElement>) { 6 6 return ( ··· 10 10 height={16} 11 11 viewBox="0 0 16 16" 12 12 strokeLinejoin="round" 13 - style={{ color: 'currentColor' }} 13 + style={{ color: "currentColor" }} 14 14 {...props} 15 15 > 16 16 <path ··· 20 20 fill="currentColor" 21 21 /> 22 22 </svg> 23 - ) 23 + ); 24 24 }
+17 -3
components/icons/share.tsx
··· 1 - 'use client'; 2 - import React from 'react'; 1 + "use client"; 2 + import React from "react"; 3 3 4 4 export function Share(props: React.SVGProps<SVGSVGElement>) { 5 5 return ( 6 - <svg data-testid="geist-icon" height="16" stroke-linejoin="round" viewBox="0 0 16 16" width="16" style={{ color: "currentcolor" }}><path fill-rule="evenodd" clip-rule="evenodd" d="M7.29289 1.39644C7.68342 1.00592 8.31658 1.00592 8.70711 1.39644L11.7803 4.46966L12.3107 4.99999L11.25 6.06065L10.7197 5.53032L8.75 3.56065V10.25V11H7.25V10.25V3.56065L5.28033 5.53032L4.75 6.06065L3.68934 4.99999L4.21967 4.46966L7.29289 1.39644ZM13.5 9.24999V13.5H2.5V9.24999V8.49999H1V9.24999V14C1 14.5523 1.44771 15 2 15H14C14.5523 15 15 14.5523 15 14V9.24999V8.49999H13.5V9.24999Z" fill="currentColor"></path></svg> 6 + <svg 7 + data-testid="geist-icon" 8 + height="16" 9 + stroke-linejoin="round" 10 + viewBox="0 0 16 16" 11 + width="16" 12 + style={{ color: "currentcolor" }} 13 + > 14 + <path 15 + fill-rule="evenodd" 16 + clip-rule="evenodd" 17 + d="M7.29289 1.39644C7.68342 1.00592 8.31658 1.00592 8.70711 1.39644L11.7803 4.46966L12.3107 4.99999L11.25 6.06065L10.7197 5.53032L8.75 3.56065V10.25V11H7.25V10.25V3.56065L5.28033 5.53032L4.75 6.06065L3.68934 4.99999L4.21967 4.46966L7.29289 1.39644ZM13.5 9.24999V13.5H2.5V9.24999V8.49999H1V9.24999V14C1 14.5523 1.44771 15 2 15H14C14.5523 15 15 14.5523 15 14V9.24999V8.49999H13.5V9.24999Z" 18 + fill="currentColor" 19 + ></path> 20 + </svg> 7 21 ); 8 22 }
+37 -11
components/landing/comparison.tsx
··· 1 1 import { LandingTitle } from "./title"; 2 2 3 3 const rows = [ 4 - { label: "End-to-end encryption (E2EE)", one: "โœ…", google: "โŒ", proton: "โœ…" }, 4 + { 5 + label: "End-to-end encryption (E2EE)", 6 + one: "โœ…", 7 + google: "โŒ", 8 + proton: "โœ…", 9 + }, 5 10 { label: "No analytics by default", one: "โœ…", google: "โŒ", proton: "โœ…" }, 6 11 { label: "ICS import / export", one: "โœ…", google: "โœ…", proton: "โœ…" }, 7 12 { label: "Keyboard shortcuts", one: "โœ…", google: "โœ…", proton: "โœ…" }, ··· 12 17 return ( 13 18 <section className="border-b border-white/10 py-24 md:py-28"> 14 19 <div className="grid gap-10 lg:grid-cols-[1fr_1fr]"> 15 - <LandingTitle as="h2" className="text-3xl font-semibold text-white md:text-5xl"> 20 + <LandingTitle 21 + as="h2" 22 + className="text-3xl font-semibold text-white md:text-5xl" 23 + > 16 24 Privacy at a glance. 17 25 </LandingTitle> 18 26 <p className="max-w-xl text-base text-[var(--landing-muted)] md:text-lg"> 19 - A quick snapshot from the repository comparison table, focused on encryption, tracking defaults, and data portability. 27 + A quick snapshot from the repository comparison table, focused on 28 + encryption, tracking defaults, and data portability. 20 29 </p> 21 30 </div> 22 31 23 32 <div className="mt-10 overflow-hidden rounded-2xl border border-white/10"> 24 33 <div className="grid border-b border-white/10 bg-white/[0.02] text-xs uppercase tracking-[0.18em] text-[var(--landing-subtle)] md:grid-cols-[1.6fr_0.6fr_0.6fr_0.6fr]"> 25 34 <div className="p-4">Feature</div> 26 - <div className="border-t border-white/10 p-4 md:border-l md:border-t-0">One Calendar</div> 27 - <div className="border-t border-white/10 p-4 md:border-l md:border-t-0">Google</div> 28 - <div className="border-t border-white/10 p-4 md:border-l md:border-t-0">Proton</div> 35 + <div className="border-t border-white/10 p-4 md:border-l md:border-t-0"> 36 + One Calendar 37 + </div> 38 + <div className="border-t border-white/10 p-4 md:border-l md:border-t-0"> 39 + Google 40 + </div> 41 + <div className="border-t border-white/10 p-4 md:border-l md:border-t-0"> 42 + Proton 43 + </div> 29 44 </div> 30 45 31 46 {rows.map((row) => ( 32 - <div key={row.label} className="grid text-sm md:grid-cols-[1.6fr_0.6fr_0.6fr_0.6fr] md:text-base"> 33 - <div className="border-b border-white/10 p-4 text-white">{row.label}</div> 34 - <div className="border-b border-white/10 p-4 text-white md:border-l">{row.one}</div> 35 - <div className="border-b border-white/10 p-4 text-[var(--landing-muted)] md:border-l">{row.google}</div> 36 - <div className="border-b border-white/10 p-4 text-[var(--landing-muted)] md:border-l">{row.proton}</div> 47 + <div 48 + key={row.label} 49 + className="grid text-sm md:grid-cols-[1.6fr_0.6fr_0.6fr_0.6fr] md:text-base" 50 + > 51 + <div className="border-b border-white/10 p-4 text-white"> 52 + {row.label} 53 + </div> 54 + <div className="border-b border-white/10 p-4 text-white md:border-l"> 55 + {row.one} 56 + </div> 57 + <div className="border-b border-white/10 p-4 text-[var(--landing-muted)] md:border-l"> 58 + {row.google} 59 + </div> 60 + <div className="border-b border-white/10 p-4 text-[var(--landing-muted)] md:border-l"> 61 + {row.proton} 62 + </div> 37 63 </div> 38 64 ))} 39 65 </div>
+5 -2
components/landing/cta.tsx
··· 1 1 export function LandingCta() { 2 2 return ( 3 3 <section className="py-24 text-center md:py-28"> 4 - <p className="text-xs uppercase tracking-[0.28em] text-[var(--landing-subtle)]">Ready to simplify planning</p> 4 + <p className="text-xs uppercase tracking-[0.28em] text-[var(--landing-subtle)]"> 5 + Ready to simplify planning 6 + </p> 5 7 <h2 className="mx-auto mt-4 max-w-3xl text-4xl font-semibold leading-tight text-white md:text-6xl"> 6 8 Your time. Your data. Yours. 7 9 </h2> 8 10 <p className="mx-auto mt-4 max-w-2xl text-sm text-[var(--landing-muted)] md:text-base"> 9 - Keep your schedule clear with privacy-first defaults, portable formats, and dependable sync. 11 + Keep your schedule clear with privacy-first defaults, portable formats, 12 + and dependable sync. 10 13 </p> 11 14 <div className="mt-8 flex flex-wrap justify-center gap-3"> 12 15 <a
+22 -7
components/landing/data-showcase.tsx
··· 16 16 export function LandingDataShowcase() { 17 17 return ( 18 18 <section id="data" className="border-b border-white/10 py-24 md:py-28"> 19 - <LandingTitle as="h2" className="text-3xl font-semibold leading-tight text-white md:text-5xl"> 19 + <LandingTitle 20 + as="h2" 21 + className="text-3xl font-semibold leading-tight text-white md:text-5xl" 22 + > 20 23 Trusted data. 21 24 <br /> 22 25 Clear architecture. 23 26 </LandingTitle> 24 27 <p className="mt-4 max-w-3xl text-base text-[var(--landing-muted)] md:text-lg"> 25 - Practical metrics and straightforward infrastructure choices, without black-box behavior. 28 + Practical metrics and straightforward infrastructure choices, without 29 + black-box behavior. 26 30 </p> 27 31 28 32 <div className="mt-12 grid gap-10 lg:grid-cols-[1.2fr_1fr]"> 29 33 <div className="grid gap-6 md:grid-cols-3"> 30 34 {metrics.map((metric) => ( 31 35 <div key={metric.label} className="border-b border-white/15 pb-4"> 32 - <p className="text-4xl font-semibold text-white">{metric.value}</p> 33 - <p className="mt-2 text-sm uppercase tracking-[0.12em] text-[var(--landing-subtle)]">{metric.label}</p> 36 + <p className="text-4xl font-semibold text-white"> 37 + {metric.value} 38 + </p> 39 + <p className="mt-2 text-sm uppercase tracking-[0.12em] text-[var(--landing-subtle)]"> 40 + {metric.label} 41 + </p> 34 42 </div> 35 43 ))} 36 44 </div> 37 45 38 46 <div className="space-y-4"> 39 47 {stack.map((item, idx) => ( 40 - <div key={item} className="flex items-start gap-3 border-l border-white/15 pl-4"> 41 - <span className="mt-0.5 text-xs text-[var(--landing-subtle)]">0{idx + 1}</span> 42 - <p className="text-sm text-[var(--landing-muted)] md:text-base">{item}</p> 48 + <div 49 + key={item} 50 + className="flex items-start gap-3 border-l border-white/15 pl-4" 51 + > 52 + <span className="mt-0.5 text-xs text-[var(--landing-subtle)]"> 53 + 0{idx + 1} 54 + </span> 55 + <p className="text-sm text-[var(--landing-muted)] md:text-base"> 56 + {item} 57 + </p> 43 58 </div> 44 59 ))} 45 60 </div>
+46 -14
components/landing/deep-dive.tsx
··· 48 48 const principles: Item[] = [ 49 49 { 50 50 title: "Simplicity by default", 51 - detail: "Keep first-view information clear and calm so users focus on decisions, not interface noise.", 51 + detail: 52 + "Keep first-view information clear and calm so users focus on decisions, not interface noise.", 52 53 icon: "M6 9h20M6 16h20M6 23h12", 53 54 }, 54 55 { 55 56 title: "Portability by default", 56 - detail: "Preserve user freedom with reliable import/export and avoid system-level lock-in.", 57 + detail: 58 + "Preserve user freedom with reliable import/export and avoid system-level lock-in.", 57 59 icon: "M16 5v18m0 0-6-6m6 6 6-6M6 27h20", 58 60 }, 59 61 { 60 62 title: "Auditability by default", 61 - detail: "Use an open implementation that can be inspected, discussed, and improved by the community.", 63 + detail: 64 + "Use an open implementation that can be inspected, discussed, and improved by the community.", 62 65 icon: "M5 8h22v16H5zM11 14h10M11 19h7", 63 66 }, 64 67 { 65 68 title: "Privacy by default", 66 - detail: "Treat privacy as baseline product behavior, not as a premium feature toggle.", 69 + detail: 70 + "Treat privacy as baseline product behavior, not as a premium feature toggle.", 67 71 icon: "M16 4 7 8v7c0 6 4 9 9 12 5-3 9-6 9-12V8l-9-4Zm0 8v5", 68 72 }, 69 73 { 70 74 title: "Speed by default", 71 - detail: "Enable high-frequency workflows with keyboard-first interaction and low-friction editing.", 75 + detail: 76 + "Enable high-frequency workflows with keyboard-first interaction and low-friction editing.", 72 77 icon: "M4 10h24v12H4zM8 14h3m3 0h10m-13 4h9", 73 78 }, 74 79 { 75 80 title: "Scalability by default", 76 - detail: "Support smooth growth from personal planning routines to structured team coordination.", 81 + detail: 82 + "Support smooth growth from personal planning routines to structured team coordination.", 77 83 icon: "M7 24h18M10 24V8m6 16V5m6 19v-9", 78 84 }, 79 85 ]; ··· 82 88 return ( 83 89 <article className="flex items-start gap-4 border-b border-white/10 py-5 last:border-b-0"> 84 90 <span className="rounded-md border border-white/15 bg-white/[0.03] p-2"> 85 - <svg viewBox="0 0 32 32" aria-hidden="true" className="h-5 w-5 stroke-white" fill="none" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"> 91 + <svg 92 + viewBox="0 0 32 32" 93 + aria-hidden="true" 94 + className="h-5 w-5 stroke-white" 95 + fill="none" 96 + strokeWidth="1.7" 97 + strokeLinecap="round" 98 + strokeLinejoin="round" 99 + > 86 100 <path d={item.icon} /> 87 101 </svg> 88 102 </span> 89 103 <div> 90 - <LandingTitle as="h3" className="text-lg font-medium text-white">{item.title}</LandingTitle> 91 - <p className="mt-2 text-sm leading-relaxed text-[var(--landing-muted)] md:text-base">{item.detail}</p> 104 + <LandingTitle as="h3" className="text-lg font-medium text-white"> 105 + {item.title} 106 + </LandingTitle> 107 + <p className="mt-2 text-sm leading-relaxed text-[var(--landing-muted)] md:text-base"> 108 + {item.detail} 109 + </p> 92 110 </div> 93 111 </article> 94 112 ); ··· 97 115 export function LandingDeepDive() { 98 116 return ( 99 117 <section id="deep-dive" className="border-b border-white/10 py-24 md:py-28"> 100 - <LandingTitle as="h2" className="text-3xl font-semibold leading-tight text-white md:text-5xl"> 118 + <LandingTitle 119 + as="h2" 120 + className="text-3xl font-semibold leading-tight text-white md:text-5xl" 121 + > 101 122 More complete real-world scenarios 102 123 <br /> 103 124 and product direction 104 125 </LandingTitle> 105 126 <p className="mt-4 max-w-3xl text-base text-[var(--landing-muted)] md:text-lg"> 106 - One Calendar is more than event storage. It is a practical scheduling system for sustained focus, 107 - coordinated execution, and consistent long-term planning. 127 + One Calendar is more than event storage. It is a practical scheduling 128 + system for sustained focus, coordinated execution, and consistent 129 + long-term planning. 108 130 </p> 109 131 110 132 <div className="mt-12 grid gap-12 lg:grid-cols-2"> 111 133 <div> 112 - <LandingTitle as="h3" className="text-2xl font-semibold text-white md:text-3xl">Real-world scenarios</LandingTitle> 134 + <LandingTitle 135 + as="h3" 136 + className="text-2xl font-semibold text-white md:text-3xl" 137 + > 138 + Real-world scenarios 139 + </LandingTitle> 113 140 <div className="mt-4"> 114 141 {useCases.map((item) => ( 115 142 <RowItem key={item.title} item={item} /> ··· 118 145 </div> 119 146 120 147 <div> 121 - <LandingTitle as="h3" className="text-2xl font-semibold text-white md:text-3xl">Product principles</LandingTitle> 148 + <LandingTitle 149 + as="h3" 150 + className="text-2xl font-semibold text-white md:text-3xl" 151 + > 152 + Product principles 153 + </LandingTitle> 122 154 <div className="mt-4"> 123 155 {principles.map((item) => ( 124 156 <RowItem key={item.title} item={item} />
+11 -2
components/landing/faq.tsx
··· 53 53 return ( 54 54 <section id="faq" className="border-b border-white/10 py-24 md:py-28"> 55 55 <div className="grid w-full gap-8 md:grid-cols-[300px_1fr]"> 56 - <LandingTitle as="h2" className="text-3xl font-semibold text-white md:text-5xl">FAQ</LandingTitle> 56 + <LandingTitle 57 + as="h2" 58 + className="text-3xl font-semibold text-white md:text-5xl" 59 + > 60 + FAQ 61 + </LandingTitle> 57 62 <div className="px-0 md:px-2"> 58 63 <Accordion type="single" collapsible className="w-full"> 59 64 {faqItems.map((item, idx) => ( 60 - <AccordionItem key={item.q} value={`faq-${idx}`} className="border-white/10"> 65 + <AccordionItem 66 + key={item.q} 67 + value={`faq-${idx}`} 68 + className="border-white/10" 69 + > 61 70 <AccordionTrigger className="py-5 text-left text-base font-medium text-white hover:no-underline"> 62 71 {item.q} 63 72 </AccordionTrigger>
+43 -14
components/landing/features.tsx
··· 3 3 const features = [ 4 4 { 5 5 title: "Fast planning", 6 - description: "Drag, resize, and edit events inline without modal-heavy flow.", 6 + description: 7 + "Drag, resize, and edit events inline without modal-heavy flow.", 7 8 icon: <path d="M4 10h24M8 4v8m16-8v8M5 18h22M5 24h12" />, 8 9 }, 9 10 { 10 11 title: "Privacy by default", 11 - description: "No analytics scripts by default with optional end-to-end encryption.", 12 - icon: <path d="M16 4 6 9v7c0 6 4 9 10 12 6-3 10-6 10-12V9L16 4Zm0 8v7m-3-4h6" />, 12 + description: 13 + "No analytics scripts by default with optional end-to-end encryption.", 14 + icon: ( 15 + <path d="M16 4 6 9v7c0 6 4 9 10 12 6-3 10-6 10-12V9L16 4Zm0 8v7m-3-4h6" /> 16 + ), 13 17 }, 14 18 { 15 19 title: "Open and portable", 16 - description: "Import/export with .ics, .json, and .csv while keeping full control.", 20 + description: 21 + "Import/export with .ics, .json, and .csv while keeping full control.", 17 22 icon: <path d="M16 4v16m0 0-5-5m5 5 5-5M5 26h22" />, 18 23 }, 19 24 { 20 25 title: "Multi-view workflow", 21 - description: "Switch between day, week, month, and year perspectives without losing context.", 26 + description: 27 + "Switch between day, week, month, and year perspectives without losing context.", 22 28 icon: <path d="M5 8h22M5 16h22M5 24h22M10 4v24M22 4v24" />, 23 29 }, 24 30 { 25 31 title: "Keyboard-first speed", 26 - description: "Navigate and edit quickly with shortcuts designed for high-frequency planning.", 32 + description: 33 + "Navigate and edit quickly with shortcuts designed for high-frequency planning.", 27 34 icon: <path d="M4 9h24v14H4zM8 13h4m4 0h8m-14 4h12" />, 28 35 }, 29 36 { 30 37 title: "Cross-device consistency", 31 - description: "A consistent experience across desktop and mobile layouts for daily reliability.", 32 - icon: <path d="M10 5h12a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Zm4 15h4" />, 38 + description: 39 + "A consistent experience across desktop and mobile layouts for daily reliability.", 40 + icon: ( 41 + <path d="M10 5h12a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Zm4 15h4" /> 42 + ), 33 43 }, 34 44 ]; 35 45 ··· 37 47 return ( 38 48 <section id="features" className="border-y border-white/10 py-24 md:py-28"> 39 49 <div className="grid gap-10 lg:grid-cols-[1fr_1fr] lg:items-start"> 40 - <LandingTitle as="h2" className="text-3xl font-semibold leading-tight text-white md:text-5xl"> 50 + <LandingTitle 51 + as="h2" 52 + className="text-3xl font-semibold leading-tight text-white md:text-5xl" 53 + > 41 54 Fast planning. 42 55 <br /> 43 56 Less noise. 44 57 </LandingTitle> 45 58 <p className="max-w-xl text-base text-[var(--landing-muted)] md:text-lg"> 46 - Every interaction is designed to keep flow intact: fast edits, secure defaults, and formats that remain portable across tools. 59 + Every interaction is designed to keep flow intact: fast edits, secure 60 + defaults, and formats that remain portable across tools. 47 61 </p> 48 62 </div> 49 63 50 64 <div className="mt-14 grid gap-6 md:grid-cols-2 xl:grid-cols-3"> 51 65 {features.map((feature) => ( 52 - <article key={feature.title} className="rounded-xl border border-white/10 p-6"> 53 - <svg viewBox="0 0 32 32" aria-hidden="true" className="mb-5 h-9 w-9 stroke-white" fill="none" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"> 66 + <article 67 + key={feature.title} 68 + className="rounded-xl border border-white/10 p-6" 69 + > 70 + <svg 71 + viewBox="0 0 32 32" 72 + aria-hidden="true" 73 + className="mb-5 h-9 w-9 stroke-white" 74 + fill="none" 75 + strokeWidth="1.5" 76 + strokeLinecap="round" 77 + strokeLinejoin="round" 78 + > 54 79 {feature.icon} 55 80 </svg> 56 - <LandingTitle as="h3" className="text-xl font-medium text-white">{feature.title}</LandingTitle> 57 - <p className="mt-3 text-sm leading-relaxed text-[var(--landing-subtle)]">{feature.description}</p> 81 + <LandingTitle as="h3" className="text-xl font-medium text-white"> 82 + {feature.title} 83 + </LandingTitle> 84 + <p className="mt-3 text-sm leading-relaxed text-[var(--landing-subtle)]"> 85 + {feature.description} 86 + </p> 58 87 </article> 59 88 ))} 60 89 </div>
+26 -6
components/landing/footer.tsx
··· 1 1 import { LandingLogoIcon } from "./logo-icon"; 2 2 3 3 const footerColumns = [ 4 - { title: "Product", links: [{ label: "Overview", href: "#features" }, { label: "About", href: "/about" }] }, 4 + { 5 + title: "Product", 6 + links: [ 7 + { label: "Overview", href: "#features" }, 8 + { label: "About", href: "/about" }, 9 + ], 10 + }, 5 11 { 6 12 title: "Resources", 7 13 links: [ 8 - { label: "Documentation", href: "https://docs.xyehr.cn/docs/one-calendar" }, 14 + { 15 + label: "Documentation", 16 + href: "https://docs.xyehr.cn/docs/one-calendar", 17 + }, 9 18 { label: "Status", href: "https://calendarstatus.xyehr.cn" }, 10 19 { label: "Support", href: "mailto:evan.huang000@proton.me" }, 11 20 ], ··· 27 36 <div className="mx-auto max-w-7xl border-t border-white/10 pt-10"> 28 37 <div className="grid gap-10 md:grid-cols-4"> 29 38 <div> 30 - <a href="#top" aria-label="One Calendar home" className="inline-flex items-center gap-2 text-white transition hover:brightness-110"> 39 + <a 40 + href="#top" 41 + aria-label="One Calendar home" 42 + className="inline-flex items-center gap-2 text-white transition hover:brightness-110" 43 + > 31 44 <LandingLogoIcon className="h-5 w-5" /> 32 45 <span className="text-sm font-medium">One Calendar</span> 33 46 </a> ··· 39 52 <ul className="mt-4 space-y-3 text-sm text-[var(--landing-muted)]"> 40 53 {column.links.map((link) => ( 41 54 <li key={link.label}> 42 - <a href={link.href} className="transition duration-200 hover:-translate-y-0.5 hover:text-white"> 55 + <a 56 + href={link.href} 57 + className="transition duration-200 hover:-translate-y-0.5 hover:text-white" 58 + > 43 59 {link.label} 44 60 </a> 45 61 </li> ··· 50 66 </div> 51 67 52 68 <div className="mt-10 flex flex-wrap items-center gap-6 border-t border-white/10 pt-5 text-xs text-[var(--landing-subtle)]"> 53 - <a href="#" className="transition hover:text-white">Privacy</a> 54 - <a href="#" className="transition hover:text-white">Terms</a> 69 + <a href="#" className="transition hover:text-white"> 70 + Privacy 71 + </a> 72 + <a href="#" className="transition hover:text-white"> 73 + Terms 74 + </a> 55 75 <p>ยฉ {new Date().getFullYear()} One Calendar. All rights reserved.</p> 56 76 </div> 57 77 </div>
+18 -4
components/landing/header.tsx
··· 13 13 return ( 14 14 <header className="sticky top-0 z-30 border-b border-white/10 bg-[var(--landing-bg)]/90 py-4 backdrop-blur"> 15 15 <nav className="flex items-center justify-between gap-6"> 16 - <a href="#top" aria-label="One Calendar home" className="flex items-center gap-2 text-white transition hover:brightness-110"> 16 + <a 17 + href="#top" 18 + aria-label="One Calendar home" 19 + className="flex items-center gap-2 text-white transition hover:brightness-110" 20 + > 17 21 <LandingLogoIcon className="h-5 w-5" /> 18 - <span className="text-lg font-semibold tracking-tight">One Calendar</span> 22 + <span className="text-lg font-semibold tracking-tight"> 23 + One Calendar 24 + </span> 19 25 </a> 20 26 21 27 <div className="hidden items-center gap-7 text-sm text-[var(--landing-muted)] md:flex"> 22 28 {navLinks.map((link) => ( 23 - <a key={link.label} href={link.href} className="transition duration-200 hover:-translate-y-0.5 hover:text-white"> 29 + <a 30 + key={link.label} 31 + href={link.href} 32 + className="transition duration-200 hover:-translate-y-0.5 hover:text-white" 33 + > 24 34 {link.label} 25 35 </a> 26 36 ))} 27 37 </div> 28 38 29 39 <div className="flex items-center gap-3"> 30 - <a href="/sign-in" aria-label="Log in" className="text-sm text-[var(--landing-muted)] transition duration-200 hover:-translate-y-0.5 hover:text-white"> 40 + <a 41 + href="/sign-in" 42 + aria-label="Log in" 43 + className="text-sm text-[var(--landing-muted)] transition duration-200 hover:-translate-y-0.5 hover:text-white" 44 + > 31 45 Log in 32 46 </a> 33 47 <a
+66 -11
components/landing/hero-demo.tsx
··· 13 13 }; 14 14 15 15 const demoEvents: DemoEvent[] = [ 16 - { id: "1", title: "Design review", start: "10:00", end: "10:45", tone: "bg-[#1a2430]", accent: "#6ea8ff" }, 17 - { id: "2", title: "Roadmap sync", start: "13:30", end: "14:15", tone: "bg-[#1a2a22]", accent: "#5bcf9a" }, 18 - { id: "3", title: "Focus block", start: "15:00", end: "17:00", tone: "bg-[#261f33]", accent: "#b18cff" }, 19 - { id: "4", title: "Release review", start: "17:30", end: "18:00", tone: "bg-[#31201f]", accent: "#ffab8a" }, 16 + { 17 + id: "1", 18 + title: "Design review", 19 + start: "10:00", 20 + end: "10:45", 21 + tone: "bg-[#1a2430]", 22 + accent: "#6ea8ff", 23 + }, 24 + { 25 + id: "2", 26 + title: "Roadmap sync", 27 + start: "13:30", 28 + end: "14:15", 29 + tone: "bg-[#1a2a22]", 30 + accent: "#5bcf9a", 31 + }, 32 + { 33 + id: "3", 34 + title: "Focus block", 35 + start: "15:00", 36 + end: "17:00", 37 + tone: "bg-[#261f33]", 38 + accent: "#b18cff", 39 + }, 40 + { 41 + id: "4", 42 + title: "Release review", 43 + start: "17:30", 44 + end: "18:00", 45 + tone: "bg-[#31201f]", 46 + accent: "#ffab8a", 47 + }, 20 48 ]; 21 49 22 50 const encryptedRows = [ ··· 32 60 33 61 function WeekViewEventBlock({ event }: { event: DemoEvent }) { 34 62 return ( 35 - <div className={cn("relative overflow-hidden rounded-lg p-2 text-sm", event.tone)}> 36 - <div className="absolute left-0 top-0 h-full w-1 rounded-l-md" style={{ backgroundColor: event.accent }} /> 63 + <div 64 + className={cn( 65 + "relative overflow-hidden rounded-lg p-2 text-sm", 66 + event.tone, 67 + )} 68 + > 69 + <div 70 + className="absolute left-0 top-0 h-full w-1 rounded-l-md" 71 + style={{ backgroundColor: event.accent }} 72 + /> 37 73 <div className="pl-1"> 38 - <p className="font-medium leading-tight" style={{ color: event.accent }}>{event.title}</p> 39 - <p className="text-xs" style={{ color: event.accent }}>{event.start} - {event.end}</p> 74 + <p 75 + className="font-medium leading-tight" 76 + style={{ color: event.accent }} 77 + > 78 + {event.title} 79 + </p> 80 + <p className="text-xs" style={{ color: event.accent }}> 81 + {event.start} - {event.end} 82 + </p> 40 83 </div> 41 84 </div> 42 85 ); ··· 46 89 return ( 47 90 <div className="mt-8 grid gap-4 lg:grid-cols-[1.2fr_1fr]"> 48 91 <div className="rounded-xl border border-white/10 bg-[var(--landing-panel)] p-4"> 49 - <LandingTitle as="p" className="mb-3 text-xs uppercase tracking-[0.14em] text-[var(--landing-subtle)]">Week View</LandingTitle> 92 + <LandingTitle 93 + as="p" 94 + className="mb-3 text-xs uppercase tracking-[0.14em] text-[var(--landing-subtle)]" 95 + > 96 + Week View 97 + </LandingTitle> 50 98 <div className="space-y-2"> 51 - {demoEvents.map((event) => <WeekViewEventBlock key={event.id} event={event} />)} 99 + {demoEvents.map((event) => ( 100 + <WeekViewEventBlock key={event.id} event={event} /> 101 + ))} 52 102 </div> 53 103 </div> 54 104 55 105 <div className="rounded-xl border border-white/10 bg-[var(--landing-panel)] p-4"> 56 - <LandingTitle as="p" className="mb-3 text-xs uppercase tracking-[0.14em] text-[var(--landing-subtle)]">Encrypted Stream</LandingTitle> 106 + <LandingTitle 107 + as="p" 108 + className="mb-3 text-xs uppercase tracking-[0.14em] text-[var(--landing-subtle)]" 109 + > 110 + Encrypted Stream 111 + </LandingTitle> 57 112 <div className="space-y-2 font-mono text-[12px] text-white/75"> 58 113 {encryptedRows.map((line) => ( 59 114 <p key={line}>{line}</p>
+14 -3
components/landing/hero.tsx
··· 7 7 return ( 8 8 <section id="top" className="py-16 md:py-24"> 9 9 <div className="mx-auto max-w-4xl text-center"> 10 - <LandingTitle as="h1" className="text-4xl font-semibold leading-tight tracking-tight text-white md:text-[56px]"> 10 + <LandingTitle 11 + as="h1" 12 + className="text-4xl font-semibold leading-tight tracking-tight text-white md:text-[56px]" 13 + > 11 14 The calendar that keeps 12 15 <br /> 13 16 your life private ··· 16 19 Secure by design. Powerful by default. 17 20 </p> 18 21 <div className="mt-8 flex justify-center gap-3"> 19 - <a href="/sign-up" aria-label="Get started" className="rounded-md bg-white px-5 py-2.5 text-sm font-medium text-black transition duration-200 hover:-translate-y-0.5 hover:brightness-110"> 22 + <a 23 + href="/sign-up" 24 + aria-label="Get started" 25 + className="rounded-md bg-white px-5 py-2.5 text-sm font-medium text-black transition duration-200 hover:-translate-y-0.5 hover:brightness-110" 26 + > 20 27 Get started 21 28 </a> 22 - <a href="#features" aria-label="View product features" className="rounded-md border border-white/15 px-5 py-2.5 text-sm text-[var(--landing-muted)] transition duration-200 hover:-translate-y-0.5 hover:border-white/30 hover:text-white"> 29 + <a 30 + href="#features" 31 + aria-label="View product features" 32 + className="rounded-md border border-white/15 px-5 py-2.5 text-sm text-[var(--landing-muted)] transition duration-200 hover:-translate-y-0.5 hover:border-white/30 hover:text-white" 33 + > 23 34 View features 24 35 </a> 25 36 </div>
+29 -18
components/landing/testimonials.tsx
··· 2 2 3 3 const highlights = [ 4 4 { 5 - icon: ( 6 - <path d="M4 16h24M4 22h18M4 10h24M4 4h14" /> 7 - ), 5 + icon: <path d="M4 16h24M4 22h18M4 10h24M4 4h14" />, 8 6 title: "Clarity over complexity", 9 - detail: "A calm planning flow instead of overloaded automation and noisy dashboards.", 7 + detail: 8 + "A calm planning flow instead of overloaded automation and noisy dashboards.", 10 9 }, 11 10 { 12 11 icon: ( 13 12 <path d="M16 4 6 9v7c0 6 4 9 10 12 6-3 10-6 10-12V9L16 4Zm-3 12 2 2 4-5" /> 14 13 ), 15 14 title: "Privacy-first defaults", 16 - detail: "No analytics by default, with optional end-to-end encryption for sensitive data.", 15 + detail: 16 + "No analytics by default, with optional end-to-end encryption for sensitive data.", 17 17 }, 18 18 { 19 - icon: ( 20 - <path d="M6 24h20M10 20V8m6 12V4m6 16v-9" /> 21 - ), 19 + icon: <path d="M6 24h20M10 20V8m6 12V4m6 16v-9" />, 22 20 title: "Portable workflows", 23 - detail: "Import/export support keeps your data usable across ecosystems without lock-in.", 21 + detail: 22 + "Import/export support keeps your data usable across ecosystems without lock-in.", 24 23 }, 25 24 { 26 - icon: ( 27 - <path d="M5 11h22M5 17h22M8 5v18m14-18v18" /> 28 - ), 25 + icon: <path d="M5 11h22M5 17h22M8 5v18m14-18v18" />, 29 26 title: "Fast team coordination", 30 - detail: "Weekly scheduling, quick edits, and structure that scales from solo to team use.", 27 + detail: 28 + "Weekly scheduling, quick edits, and structure that scales from solo to team use.", 31 29 }, 32 30 ]; 33 31 ··· 41 39 export function LandingTestimonials() { 42 40 return ( 43 41 <section className="border-b border-white/10 py-24 md:py-28"> 44 - <LandingTitle as="h2" className="text-center text-3xl font-semibold text-white md:text-5xl"> 42 + <LandingTitle 43 + as="h2" 44 + className="text-center text-3xl font-semibold text-white md:text-5xl" 45 + > 45 46 Why One Calendar 46 47 </LandingTitle> 47 48 ··· 49 50 {stats.map((item) => ( 50 51 <div key={item.label} className="text-center md:text-left"> 51 52 <p className="text-3xl font-semibold text-white">{item.value}</p> 52 - <p className="mt-1 text-xs uppercase tracking-[0.12em] text-[var(--landing-subtle)]">{item.label}</p> 53 + <p className="mt-1 text-xs uppercase tracking-[0.12em] text-[var(--landing-subtle)]"> 54 + {item.label} 55 + </p> 53 56 </div> 54 57 ))} 55 58 </div> 56 59 57 60 <div className="mt-10 grid gap-4 md:grid-cols-2"> 58 61 {highlights.map((item) => ( 59 - <article key={item.title} className="rounded-xl border border-white/10 p-5"> 62 + <article 63 + key={item.title} 64 + className="rounded-xl border border-white/10 p-5" 65 + > 60 66 <svg 61 67 viewBox="0 0 32 32" 62 68 aria-hidden="true" ··· 68 74 > 69 75 {item.icon} 70 76 </svg> 71 - <LandingTitle as="h3" className="text-lg font-medium text-white md:text-xl"> 77 + <LandingTitle 78 + as="h3" 79 + className="text-lg font-medium text-white md:text-xl" 80 + > 72 81 {item.title} 73 82 </LandingTitle> 74 - <p className="mt-2 text-sm leading-relaxed text-[var(--landing-muted)] md:text-base">{item.detail}</p> 83 + <p className="mt-2 text-sm leading-relaxed text-[var(--landing-muted)] md:text-base"> 84 + {item.detail} 85 + </p> 75 86 </article> 76 87 ))} 77 88 </div>
+15 -2
components/landing/title.tsx
··· 1 1 "use client"; 2 2 3 3 import { cn } from "@/lib/utils"; 4 - import { type ElementType, type ReactNode, useEffect, useRef, useState } from "react"; 4 + import { 5 + type ElementType, 6 + type ReactNode, 7 + useEffect, 8 + useRef, 9 + useState, 10 + } from "react"; 5 11 6 12 type LandingTitleProps<T extends ElementType> = { 7 13 as?: T; ··· 36 42 }, []); 37 43 38 44 return ( 39 - <Tag ref={ref} className={cn("landing-title", visible && "landing-title-visible", className)}> 45 + <Tag 46 + ref={ref} 47 + className={cn( 48 + "landing-title", 49 + visible && "landing-title-visible", 50 + className, 51 + )} 52 + > 40 53 {children} 41 54 </Tag> 42 55 );
+6 -1
components/providers/calendar-context.tsx
··· 1 1 "use client"; 2 2 3 3 import { useLocalStorage } from "@/hooks/useLocalStorage"; 4 - import { createContext, useContext, type Dispatch, type SetStateAction } from "react"; 4 + import { 5 + createContext, 6 + useContext, 7 + type Dispatch, 8 + type SetStateAction, 9 + } from "react"; 5 10 import type React from "react"; 6 11 7 12 export interface CalendarCategory {
+4 -4
components/providers/theme-provider.tsx
··· 1 - 'use client'; 1 + "use client"; 2 2 3 - import { ThemeProvider as NextThemesProvider } from 'next-themes'; 4 - import * as React from 'react'; 3 + import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 + import * as React from "react"; 5 5 6 6 export function ThemeProvider({ 7 7 children, 8 - themes = ['light', 'dark', 'green', 'orange', 'azalea'], 8 + themes = ["light", "dark", "green", "orange", "azalea"], 9 9 ...props 10 10 }: React.ComponentProps<typeof NextThemesProvider> & { 11 11 themes?: string[];
+11 -11
components/ui/accordion.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as AccordionPrimitive from "@radix-ui/react-accordion" 4 - import { ChevronDownIcon } from "lucide-react" 5 - import * as React from "react" 3 + import * as AccordionPrimitive from "@radix-ui/react-accordion"; 4 + import { ChevronDownIcon } from "lucide-react"; 5 + import * as React from "react"; 6 6 7 - import { cn } from "@/lib/utils" 7 + import { cn } from "@/lib/utils"; 8 8 9 9 function Accordion({ 10 10 ...props 11 11 }: React.ComponentProps<typeof AccordionPrimitive.Root>) { 12 - return <AccordionPrimitive.Root data-slot="accordion" {...props} /> 12 + return <AccordionPrimitive.Root data-slot="accordion" {...props} />; 13 13 } 14 14 15 15 function AccordionItem({ ··· 22 22 className={cn("border-b last:border-b-0", className)} 23 23 {...props} 24 24 /> 25 - ) 25 + ); 26 26 } 27 27 28 28 function AccordionTrigger({ ··· 36 36 data-slot="accordion-trigger" 37 37 className={cn( 38 38 "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", 39 - className 39 + className, 40 40 )} 41 41 {...props} 42 42 > ··· 44 44 <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" /> 45 45 </AccordionPrimitive.Trigger> 46 46 </AccordionPrimitive.Header> 47 - ) 47 + ); 48 48 } 49 49 50 50 function AccordionContent({ ··· 60 60 > 61 61 <div className={cn("pt-0 pb-4", className)}>{children}</div> 62 62 </AccordionPrimitive.Content> 63 - ) 63 + ); 64 64 } 65 65 66 - export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 66 + export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
+41 -26
components/ui/alert-dialog.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { AlertDialog as AlertDialogPrimitive } from "radix-ui" 4 - import * as React from "react" 3 + import { AlertDialog as AlertDialogPrimitive } from "radix-ui"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 7 - import { Button } from "@/components/ui/button" 6 + import { cn } from "@/lib/utils"; 7 + import { Button } from "@/components/ui/button"; 8 8 9 9 function AlertDialog({ 10 10 ...props 11 11 }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { 12 - return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} /> 12 + return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />; 13 13 } 14 14 15 15 function AlertDialogTrigger({ ··· 17 17 }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { 18 18 return ( 19 19 <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> 20 - ) 20 + ); 21 21 } 22 22 23 23 function AlertDialogPortal({ ··· 25 25 }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { 26 26 return ( 27 27 <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> 28 - ) 28 + ); 29 29 } 30 30 31 31 function AlertDialogOverlay({ ··· 35 35 return ( 36 36 <AlertDialogPrimitive.Overlay 37 37 data-slot="alert-dialog-overlay" 38 - className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)} 38 + className={cn( 39 + "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", 40 + className, 41 + )} 39 42 {...props} 40 43 /> 41 - ) 44 + ); 42 45 } 43 46 44 47 function AlertDialogContent({ ··· 46 49 size = "default", 47 50 ...props 48 51 }: React.ComponentProps<typeof AlertDialogPrimitive.Content> & { 49 - size?: "default" | "sm" 52 + size?: "default" | "sm"; 50 53 }) { 51 54 return ( 52 55 <AlertDialogPortal> ··· 56 59 data-size={size} 57 60 className={cn( 58 61 "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none", 59 - className 62 + className, 60 63 )} 61 64 {...props} 62 65 /> 63 66 </AlertDialogPortal> 64 - ) 67 + ); 65 68 } 66 69 67 70 function AlertDialogHeader({ ··· 71 74 return ( 72 75 <div 73 76 data-slot="alert-dialog-header" 74 - className={cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)} 77 + className={cn( 78 + "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", 79 + className, 80 + )} 75 81 {...props} 76 82 /> 77 - ) 83 + ); 78 84 } 79 85 80 86 function AlertDialogFooter({ ··· 86 92 data-slot="alert-dialog-footer" 87 93 className={cn( 88 94 "bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end", 89 - className 95 + className, 90 96 )} 91 97 {...props} 92 98 /> 93 - ) 99 + ); 94 100 } 95 101 96 102 function AlertDialogMedia({ ··· 100 106 return ( 101 107 <div 102 108 data-slot="alert-dialog-media" 103 - className={cn("bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6", className)} 109 + className={cn( 110 + "bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6", 111 + className, 112 + )} 104 113 {...props} 105 114 /> 106 - ) 115 + ); 107 116 } 108 117 109 118 function AlertDialogTitle({ ··· 113 122 return ( 114 123 <AlertDialogPrimitive.Title 115 124 data-slot="alert-dialog-title" 116 - className={cn("text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className)} 125 + className={cn( 126 + "text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", 127 + className, 128 + )} 117 129 {...props} 118 130 /> 119 - ) 131 + ); 120 132 } 121 133 122 134 function AlertDialogDescription({ ··· 126 138 return ( 127 139 <AlertDialogPrimitive.Description 128 140 data-slot="alert-dialog-description" 129 - className={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)} 141 + className={cn( 142 + "text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", 143 + className, 144 + )} 130 145 {...props} 131 146 /> 132 - ) 147 + ); 133 148 } 134 149 135 150 function AlertDialogAction({ ··· 147 162 {...props} 148 163 /> 149 164 </Button> 150 - ) 165 + ); 151 166 } 152 167 153 168 function AlertDialogCancel({ ··· 165 180 {...props} 166 181 /> 167 182 </Button> 168 - ) 183 + ); 169 184 } 170 185 171 186 export { ··· 181 196 AlertDialogPortal, 182 197 AlertDialogTitle, 183 198 AlertDialogTrigger, 184 - } 199 + };
+11 -11
components/ui/alert.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority" 2 - import * as React from "react" 1 + import { cva, type VariantProps } from "class-variance-authority"; 2 + import * as React from "react"; 3 3 4 - import { cn } from "@/lib/utils" 4 + import { cn } from "@/lib/utils"; 5 5 6 6 const alertVariants = cva( 7 7 "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", ··· 16 16 defaultVariants: { 17 17 variant: "default", 18 18 }, 19 - } 20 - ) 19 + }, 20 + ); 21 21 22 22 function Alert({ 23 23 className, ··· 31 31 className={cn(alertVariants({ variant }), className)} 32 32 {...props} 33 33 /> 34 - ) 34 + ); 35 35 } 36 36 37 37 function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { ··· 40 40 data-slot="alert-title" 41 41 className={cn( 42 42 "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", 43 - className 43 + className, 44 44 )} 45 45 {...props} 46 46 /> 47 - ) 47 + ); 48 48 } 49 49 50 50 function AlertDescription({ ··· 56 56 data-slot="alert-description" 57 57 className={cn( 58 58 "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", 59 - className 59 + className, 60 60 )} 61 61 {...props} 62 62 /> 63 - ) 63 + ); 64 64 } 65 65 66 - export { Alert, AlertTitle, AlertDescription } 66 + export { Alert, AlertTitle, AlertDescription };
+10 -10
components/ui/avatar.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as AvatarPrimitive from "@radix-ui/react-avatar" 4 - import * as React from "react" 3 + import * as AvatarPrimitive from "@radix-ui/react-avatar"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 6 + import { cn } from "@/lib/utils"; 7 7 8 8 function Avatar({ 9 9 className, ··· 14 14 data-slot="avatar" 15 15 className={cn( 16 16 "relative flex size-8 shrink-0 overflow-hidden rounded-full", 17 - className 17 + className, 18 18 )} 19 19 {...props} 20 20 /> 21 - ) 21 + ); 22 22 } 23 23 24 24 function AvatarImage({ ··· 31 31 className={cn("aspect-square size-full", className)} 32 32 {...props} 33 33 /> 34 - ) 34 + ); 35 35 } 36 36 37 37 function AvatarFallback({ ··· 43 43 data-slot="avatar-fallback" 44 44 className={cn( 45 45 "bg-muted flex size-full items-center justify-center rounded-full", 46 - className 46 + className, 47 47 )} 48 48 {...props} 49 49 /> 50 - ) 50 + ); 51 51 } 52 52 53 - export { Avatar, AvatarImage, AvatarFallback } 53 + export { Avatar, AvatarImage, AvatarFallback };
+9 -9
components/ui/badge.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority" 2 - import { Slot } from "@radix-ui/react-slot" 3 - import * as React from "react" 1 + import { cva, type VariantProps } from "class-variance-authority"; 2 + import { Slot } from "@radix-ui/react-slot"; 3 + import * as React from "react"; 4 4 5 - import { cn } from "@/lib/utils" 5 + import { cn } from "@/lib/utils"; 6 6 7 7 const badgeVariants = cva( 8 8 "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", ··· 22 22 defaultVariants: { 23 23 variant: "default", 24 24 }, 25 - } 26 - ) 25 + }, 26 + ); 27 27 28 28 function Badge({ 29 29 className, ··· 32 32 ...props 33 33 }: React.ComponentProps<"span"> & 34 34 VariantProps<typeof badgeVariants> & { asChild?: boolean }) { 35 - const Comp = asChild ? Slot : "span" 35 + const Comp = asChild ? Slot : "span"; 36 36 37 37 return ( 38 38 <Comp ··· 40 40 className={cn(badgeVariants({ variant }), className)} 41 41 {...props} 42 42 /> 43 - ) 43 + ); 44 44 } 45 45 46 - export { Badge, badgeVariants } 46 + export { Badge, badgeVariants };
+21 -17
components/ui/button.tsx
··· 1 - import { cva, type VariantProps } from "class-variance-authority" 2 - import { Slot } from "@radix-ui/react-slot" 3 - import * as React from "react" 1 + import { cva, type VariantProps } from "class-variance-authority"; 2 + import { Slot } from "@radix-ui/react-slot"; 3 + import * as React from "react"; 4 4 5 - import { cn } from "@/lib/utils" 5 + import { cn } from "@/lib/utils"; 6 6 7 7 const buttonVariants = cva( 8 8 "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", ··· 10 10 variants: { 11 11 variant: { 12 12 default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", 13 - outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", 14 - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", 15 - ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", 13 + outline: 14 + "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", 15 + secondary: 16 + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", 17 + ghost: 18 + "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", 16 19 destructive: 17 20 "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 18 21 link: "text-primary underline-offset-4 hover:underline", ··· 28 31 variant: "default", 29 32 size: "default", 30 33 }, 31 - } 32 - ) 34 + }, 35 + ); 33 36 34 37 export interface ButtonProps 35 - extends React.ButtonHTMLAttributes<HTMLButtonElement>, 38 + extends 39 + React.ButtonHTMLAttributes<HTMLButtonElement>, 36 40 VariantProps<typeof buttonVariants> { 37 - asChild?: boolean 41 + asChild?: boolean; 38 42 } 39 43 40 44 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 41 45 ({ className, variant, size, asChild = false, ...props }, ref) => { 42 - const Comp = asChild ? Slot : "button" 46 + const Comp = asChild ? Slot : "button"; 43 47 return ( 44 48 <Comp 45 49 className={cn(buttonVariants({ variant, size, className }))} 46 50 ref={ref} 47 51 {...props} 48 52 /> 49 - ) 50 - } 51 - ) 52 - Button.displayName = "Button" 53 + ); 54 + }, 55 + ); 56 + Button.displayName = "Button"; 53 57 54 - export { Button, buttonVariants } 58 + export { Button, buttonVariants };
+52 -42
components/ui/calendar.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as React from "react" 3 + import * as React from "react"; 4 4 import { 5 5 DayPicker, 6 6 getDefaultClassNames, 7 7 type DayButton, 8 8 type Locale, 9 - } from "react-day-picker" 9 + } from "react-day-picker"; 10 10 11 - import { cn } from "@/lib/utils" 12 - import { Button, buttonVariants } from "@/components/ui/button" 13 - import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from "lucide-react" 11 + import { cn } from "@/lib/utils"; 12 + import { Button, buttonVariants } from "@/components/ui/button"; 13 + import { 14 + ChevronLeftIcon, 15 + ChevronRightIcon, 16 + ChevronDownIcon, 17 + } from "lucide-react"; 14 18 15 19 function Calendar({ 16 20 className, ··· 23 27 components, 24 28 ...props 25 29 }: React.ComponentProps<typeof DayPicker> & { 26 - buttonVariant?: React.ComponentProps<typeof Button>["variant"] 30 + buttonVariant?: React.ComponentProps<typeof Button>["variant"]; 27 31 }) { 28 - const defaultClassNames = getDefaultClassNames() 32 + const defaultClassNames = getDefaultClassNames(); 29 33 30 34 return ( 31 35 <DayPicker ··· 34 38 "p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", 35 39 String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, 36 40 String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, 37 - className 41 + className, 38 42 )} 39 43 captionLayout={captionLayout} 40 44 locale={locale} ··· 47 51 root: cn("w-fit", defaultClassNames.root), 48 52 months: cn( 49 53 "relative flex flex-col gap-4 md:flex-row", 50 - defaultClassNames.months 54 + defaultClassNames.months, 51 55 ), 52 56 month: cn("flex w-full flex-col gap-4", defaultClassNames.month), 53 57 nav: cn( 54 58 "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", 55 - defaultClassNames.nav 59 + defaultClassNames.nav, 56 60 ), 57 61 button_previous: cn( 58 62 buttonVariants({ variant: buttonVariant }), 59 63 "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", 60 - defaultClassNames.button_previous 64 + defaultClassNames.button_previous, 61 65 ), 62 66 button_next: cn( 63 67 buttonVariants({ variant: buttonVariant }), 64 68 "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", 65 - defaultClassNames.button_next 69 + defaultClassNames.button_next, 66 70 ), 67 71 month_caption: cn( 68 72 "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", 69 - defaultClassNames.month_caption 73 + defaultClassNames.month_caption, 70 74 ), 71 75 dropdowns: cn( 72 76 "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", 73 - defaultClassNames.dropdowns 77 + defaultClassNames.dropdowns, 74 78 ), 75 79 dropdown_root: cn( 76 80 "cn-calendar-dropdown-root relative rounded-(--cell-radius)", 77 - defaultClassNames.dropdown_root 81 + defaultClassNames.dropdown_root, 78 82 ), 79 83 dropdown: cn( 80 84 "absolute inset-0 bg-popover opacity-0", 81 - defaultClassNames.dropdown 85 + defaultClassNames.dropdown, 82 86 ), 83 87 caption_label: cn( 84 88 "font-medium select-none", 85 89 captionLayout === "label" 86 90 ? "text-sm" 87 91 : "cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", 88 - defaultClassNames.caption_label 92 + defaultClassNames.caption_label, 89 93 ), 90 94 table: "w-full border-collapse", 91 95 weekdays: cn("flex", defaultClassNames.weekdays), 92 96 weekday: cn( 93 97 "flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none", 94 - defaultClassNames.weekday 98 + defaultClassNames.weekday, 95 99 ), 96 100 week: cn("mt-2 flex w-full", defaultClassNames.week), 97 101 week_number_header: cn( 98 102 "w-(--cell-size) select-none", 99 - defaultClassNames.week_number_header 103 + defaultClassNames.week_number_header, 100 104 ), 101 105 week_number: cn( 102 106 "text-[0.8rem] text-muted-foreground select-none", 103 - defaultClassNames.week_number 107 + defaultClassNames.week_number, 104 108 ), 105 109 day: cn( 106 110 "group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", 107 111 props.showWeekNumber 108 112 ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" 109 113 : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", 110 - defaultClassNames.day 114 + defaultClassNames.day, 111 115 ), 112 116 range_start: cn( 113 117 "relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted", 114 - defaultClassNames.range_start 118 + defaultClassNames.range_start, 115 119 ), 116 120 range_middle: cn("rounded-none", defaultClassNames.range_middle), 117 121 range_end: cn( 118 122 "relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted", 119 - defaultClassNames.range_end 123 + defaultClassNames.range_end, 120 124 ), 121 125 today: cn( 122 126 "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none", 123 - defaultClassNames.today 127 + defaultClassNames.today, 124 128 ), 125 129 outside: cn( 126 130 "text-muted-foreground aria-selected:text-muted-foreground", 127 - defaultClassNames.outside 131 + defaultClassNames.outside, 128 132 ), 129 133 disabled: cn( 130 134 "text-muted-foreground opacity-50", 131 - defaultClassNames.disabled 135 + defaultClassNames.disabled, 132 136 ), 133 137 hidden: cn("invisible", defaultClassNames.hidden), 134 138 ...classNames, ··· 142 146 className={cn(className)} 143 147 {...props} 144 148 /> 145 - ) 149 + ); 146 150 }, 147 151 Chevron: ({ className, orientation, ...props }) => { 148 152 if (orientation === "left") { 149 153 return ( 150 - <ChevronLeftIcon className={cn("cn-rtl-flip size-4", className)} {...props} /> 151 - ) 154 + <ChevronLeftIcon 155 + className={cn("cn-rtl-flip size-4", className)} 156 + {...props} 157 + /> 158 + ); 152 159 } 153 160 154 161 if (orientation === "right") { 155 162 return ( 156 - <ChevronRightIcon className={cn("cn-rtl-flip size-4", className)} {...props} /> 157 - ) 163 + <ChevronRightIcon 164 + className={cn("cn-rtl-flip size-4", className)} 165 + {...props} 166 + /> 167 + ); 158 168 } 159 169 160 170 return ( 161 171 <ChevronDownIcon className={cn("size-4", className)} {...props} /> 162 - ) 172 + ); 163 173 }, 164 174 DayButton: ({ ...props }) => ( 165 175 <CalendarDayButton locale={locale} {...props} /> ··· 171 181 {children} 172 182 </div> 173 183 </td> 174 - ) 184 + ); 175 185 }, 176 186 ...components, 177 187 }} 178 188 {...props} 179 189 /> 180 - ) 190 + ); 181 191 } 182 192 183 193 function CalendarDayButton({ ··· 187 197 locale, 188 198 ...props 189 199 }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) { 190 - const defaultClassNames = getDefaultClassNames() 200 + const defaultClassNames = getDefaultClassNames(); 191 201 192 - const ref = React.useRef<HTMLButtonElement>(null) 202 + const ref = React.useRef<HTMLButtonElement>(null); 193 203 React.useEffect(() => { 194 - if (modifiers.focused) ref.current?.focus() 195 - }, [modifiers.focused]) 204 + if (modifiers.focused) ref.current?.focus(); 205 + }, [modifiers.focused]); 196 206 197 207 return ( 198 208 <Button ··· 212 222 className={cn( 213 223 "relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70", 214 224 defaultClassNames.day, 215 - className 225 + className, 216 226 )} 217 227 {...props} 218 228 /> 219 - ) 229 + ); 220 230 } 221 231 222 - export { Calendar, CalendarDayButton } 232 + export { Calendar, CalendarDayButton };
+24 -15
components/ui/card.tsx
··· 1 - import * as React from "react" 1 + import * as React from "react"; 2 2 3 - import { cn } from "@/lib/utils" 3 + import { cn } from "@/lib/utils"; 4 4 5 5 function Card({ 6 6 className, ··· 11 11 <div 12 12 data-slot="card" 13 13 data-size={size} 14 - className={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)} 14 + className={cn( 15 + "ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", 16 + className, 17 + )} 15 18 {...props} 16 19 /> 17 - ) 20 + ); 18 21 } 19 22 20 23 function CardHeader({ className, ...props }: React.ComponentProps<"div">) { ··· 23 26 data-slot="card-header" 24 27 className={cn( 25 28 "gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", 26 - className 29 + className, 27 30 )} 28 31 {...props} 29 32 /> 30 - ) 33 + ); 31 34 } 32 35 33 36 function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 34 37 return ( 35 38 <div 36 39 data-slot="card-title" 37 - className={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)} 40 + className={cn( 41 + "text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", 42 + className, 43 + )} 38 44 {...props} 39 45 /> 40 - ) 46 + ); 41 47 } 42 48 43 49 function CardDescription({ className, ...props }: React.ComponentProps<"div">) { ··· 47 53 className={cn("text-muted-foreground text-sm", className)} 48 54 {...props} 49 55 /> 50 - ) 56 + ); 51 57 } 52 58 53 59 function CardAction({ className, ...props }: React.ComponentProps<"div">) { ··· 56 62 data-slot="card-action" 57 63 className={cn( 58 64 "col-start-2 row-span-2 row-start-1 self-start justify-self-end", 59 - className 65 + className, 60 66 )} 61 67 {...props} 62 68 /> 63 - ) 69 + ); 64 70 } 65 71 66 72 function CardContent({ className, ...props }: React.ComponentProps<"div">) { ··· 70 76 className={cn("px-4 group-data-[size=sm]/card:px-3", className)} 71 77 {...props} 72 78 /> 73 - ) 79 + ); 74 80 } 75 81 76 82 function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 77 83 return ( 78 84 <div 79 85 data-slot="card-footer" 80 - className={cn("bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", className)} 86 + className={cn( 87 + "bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", 88 + className, 89 + )} 81 90 {...props} 82 91 /> 83 - ) 92 + ); 84 93 } 85 94 86 95 export { ··· 91 100 CardAction, 92 101 CardDescription, 93 102 CardContent, 94 - } 103 + };
+9 -10
components/ui/checkbox.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { Checkbox as CheckboxPrimitive } from "radix-ui" 4 - import * as React from "react" 3 + import { Checkbox as CheckboxPrimitive } from "radix-ui"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 7 - import { CheckIcon } from "lucide-react" 6 + import { cn } from "@/lib/utils"; 7 + import { CheckIcon } from "lucide-react"; 8 8 9 9 function Checkbox({ 10 10 className, ··· 15 15 data-slot="checkbox" 16 16 className={cn( 17 17 "border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-[3px] aria-invalid:ring-[3px] peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50", 18 - className 18 + className, 19 19 )} 20 20 {...props} 21 21 > ··· 23 23 data-slot="checkbox-indicator" 24 24 className="[&>svg]:size-3.5 grid place-content-center text-current transition-none" 25 25 > 26 - <CheckIcon 27 - /> 26 + <CheckIcon /> 28 27 </CheckboxPrimitive.Indicator> 29 28 </CheckboxPrimitive.Root> 30 - ) 29 + ); 31 30 } 32 31 33 - export { Checkbox } 32 + export { Checkbox };
+6 -7
components/ui/collapsible.tsx
··· 1 - "use client" 2 - import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 1 + "use client"; 2 + import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 3 3 4 - const Collapsible = CollapsiblePrimitive.Root 4 + const Collapsible = CollapsiblePrimitive.Root; 5 5 6 - const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 7 - 8 - const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 6 + const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 9 7 10 - export { Collapsible, CollapsibleTrigger, CollapsibleContent } 8 + const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 11 9 10 + export { Collapsible, CollapsibleTrigger, CollapsibleContent };
+42 -42
components/ui/context-menu.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" 4 - import { Check, ChevronRight, Circle } from "lucide-react" 5 - import * as React from "react" 3 + import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; 4 + import { Check, ChevronRight, Circle } from "lucide-react"; 5 + import * as React from "react"; 6 6 7 - import { cn } from "@/lib/utils" 7 + import { cn } from "@/lib/utils"; 8 8 9 - const ContextMenu = ContextMenuPrimitive.Root 9 + const ContextMenu = ContextMenuPrimitive.Root; 10 10 11 - const ContextMenuTrigger = ContextMenuPrimitive.Trigger 11 + const ContextMenuTrigger = ContextMenuPrimitive.Trigger; 12 12 13 - const ContextMenuGroup = ContextMenuPrimitive.Group 13 + const ContextMenuGroup = ContextMenuPrimitive.Group; 14 14 15 - const ContextMenuPortal = ContextMenuPrimitive.Portal 15 + const ContextMenuPortal = ContextMenuPrimitive.Portal; 16 16 17 - const ContextMenuSub = ContextMenuPrimitive.Sub 17 + const ContextMenuSub = ContextMenuPrimitive.Sub; 18 18 19 - const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup 19 + const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; 20 20 21 21 const ContextMenuSubTrigger = React.forwardRef< 22 22 React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, 23 23 React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { 24 - inset?: boolean 24 + inset?: boolean; 25 25 } 26 26 >(({ className, inset, children, ...props }, ref) => ( 27 27 <ContextMenuPrimitive.SubTrigger ··· 29 29 className={cn( 30 30 "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", 31 31 inset && "pl-8", 32 - className 32 + className, 33 33 )} 34 34 {...props} 35 35 > 36 36 {children} 37 37 <ChevronRight className="ml-auto h-4 w-4" /> 38 38 </ContextMenuPrimitive.SubTrigger> 39 - )) 40 - ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName 39 + )); 40 + ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; 41 41 42 42 const ContextMenuSubContent = React.forwardRef< 43 43 React.ElementRef<typeof ContextMenuPrimitive.SubContent>, ··· 47 47 ref={ref} 48 48 className={cn( 49 49 "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]", 50 - className 50 + className, 51 51 )} 52 52 {...props} 53 53 /> 54 - )) 55 - ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName 54 + )); 55 + ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; 56 56 57 57 const ContextMenuContent = React.forwardRef< 58 58 React.ElementRef<typeof ContextMenuPrimitive.Content>, ··· 63 63 ref={ref} 64 64 className={cn( 65 65 "z-50 max-h-[--radix-context-menu-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-context-menu-content-transform-origin]", 66 - className 66 + className, 67 67 )} 68 68 {...props} 69 69 /> 70 70 </ContextMenuPrimitive.Portal> 71 - )) 72 - ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName 71 + )); 72 + ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; 73 73 74 74 const ContextMenuItem = React.forwardRef< 75 75 React.ElementRef<typeof ContextMenuPrimitive.Item>, 76 76 React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { 77 - inset?: boolean 77 + inset?: boolean; 78 78 } 79 79 >(({ className, inset, ...props }, ref) => ( 80 80 <ContextMenuPrimitive.Item ··· 82 82 className={cn( 83 83 "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 84 84 inset && "pl-8", 85 - className 85 + className, 86 86 )} 87 87 {...props} 88 88 /> 89 - )) 90 - ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName 89 + )); 90 + ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; 91 91 92 92 const ContextMenuCheckboxItem = React.forwardRef< 93 93 React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>, ··· 97 97 ref={ref} 98 98 className={cn( 99 99 "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 100 - className 100 + className, 101 101 )} 102 102 checked={checked} 103 103 {...props} ··· 109 109 </span> 110 110 {children} 111 111 </ContextMenuPrimitive.CheckboxItem> 112 - )) 112 + )); 113 113 ContextMenuCheckboxItem.displayName = 114 - ContextMenuPrimitive.CheckboxItem.displayName 114 + ContextMenuPrimitive.CheckboxItem.displayName; 115 115 116 116 const ContextMenuRadioItem = React.forwardRef< 117 117 React.ElementRef<typeof ContextMenuPrimitive.RadioItem>, ··· 121 121 ref={ref} 122 122 className={cn( 123 123 "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 124 - className 124 + className, 125 125 )} 126 126 {...props} 127 127 > ··· 132 132 </span> 133 133 {children} 134 134 </ContextMenuPrimitive.RadioItem> 135 - )) 136 - ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName 135 + )); 136 + ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; 137 137 138 138 const ContextMenuLabel = React.forwardRef< 139 139 React.ElementRef<typeof ContextMenuPrimitive.Label>, 140 140 React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { 141 - inset?: boolean 141 + inset?: boolean; 142 142 } 143 143 >(({ className, inset, ...props }, ref) => ( 144 144 <ContextMenuPrimitive.Label ··· 146 146 className={cn( 147 147 "px-2 py-1.5 text-sm font-semibold text-foreground", 148 148 inset && "pl-8", 149 - className 149 + className, 150 150 )} 151 151 {...props} 152 152 /> 153 - )) 154 - ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName 153 + )); 154 + ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; 155 155 156 156 const ContextMenuSeparator = React.forwardRef< 157 157 React.ElementRef<typeof ContextMenuPrimitive.Separator>, ··· 162 162 className={cn("-mx-1 my-1 h-px bg-border", className)} 163 163 {...props} 164 164 /> 165 - )) 166 - ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName 165 + )); 166 + ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; 167 167 168 168 const ContextMenuShortcut = ({ 169 169 className, ··· 173 173 <span 174 174 className={cn( 175 175 "ml-auto text-xs tracking-widest text-muted-foreground", 176 - className 176 + className, 177 177 )} 178 178 {...props} 179 179 /> 180 - ) 181 - } 182 - ContextMenuShortcut.displayName = "ContextMenuShortcut" 180 + ); 181 + }; 182 + ContextMenuShortcut.displayName = "ContextMenuShortcut"; 183 183 184 184 export { 185 185 ContextMenu, ··· 197 197 ContextMenuSubContent, 198 198 ContextMenuSubTrigger, 199 199 ContextMenuRadioGroup, 200 - } 200 + };
+35 -26
components/ui/dialog.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { Dialog as DialogPrimitive } from "radix-ui" 4 - import * as React from "react" 3 + import { Dialog as DialogPrimitive } from "radix-ui"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 7 - import { Button } from "@/components/ui/button" 8 - import { XIcon } from "lucide-react" 6 + import { cn } from "@/lib/utils"; 7 + import { Button } from "@/components/ui/button"; 8 + import { XIcon } from "lucide-react"; 9 9 10 10 function Dialog({ 11 11 ...props 12 12 }: React.ComponentProps<typeof DialogPrimitive.Root>) { 13 - return <DialogPrimitive.Root data-slot="dialog" {...props} /> 13 + return <DialogPrimitive.Root data-slot="dialog" {...props} />; 14 14 } 15 15 16 16 function DialogTrigger({ 17 17 ...props 18 18 }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { 19 - return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> 19 + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; 20 20 } 21 21 22 22 function DialogPortal({ 23 23 ...props 24 24 }: React.ComponentProps<typeof DialogPrimitive.Portal>) { 25 - return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> 25 + return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; 26 26 } 27 27 28 28 function DialogClose({ 29 29 ...props 30 30 }: React.ComponentProps<typeof DialogPrimitive.Close>) { 31 - return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> 31 + return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; 32 32 } 33 33 34 34 function DialogOverlay({ ··· 38 38 return ( 39 39 <DialogPrimitive.Overlay 40 40 data-slot="dialog-overlay" 41 - className={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)} 41 + className={cn( 42 + "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", 43 + className, 44 + )} 42 45 {...props} 43 46 /> 44 - ) 47 + ); 45 48 } 46 49 47 50 function DialogContent({ ··· 50 53 showCloseButton = true, 51 54 ...props 52 55 }: React.ComponentProps<typeof DialogPrimitive.Content> & { 53 - showCloseButton?: boolean 56 + showCloseButton?: boolean; 54 57 }) { 55 58 return ( 56 59 <DialogPortal> ··· 59 62 data-slot="dialog-content" 60 63 className={cn( 61 64 "bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none", 62 - className 65 + className, 63 66 )} 64 67 {...props} 65 68 > 66 69 {children} 67 70 {showCloseButton && ( 68 71 <DialogPrimitive.Close data-slot="dialog-close" asChild> 69 - <Button variant="ghost" className="absolute top-2 right-2" size="icon-sm"> 70 - <XIcon 71 - /> 72 + <Button 73 + variant="ghost" 74 + className="absolute top-2 right-2" 75 + size="icon-sm" 76 + > 77 + <XIcon /> 72 78 <span className="sr-only">Close</span> 73 79 </Button> 74 80 </DialogPrimitive.Close> 75 81 )} 76 82 </DialogPrimitive.Content> 77 83 </DialogPortal> 78 - ) 84 + ); 79 85 } 80 86 81 87 function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { ··· 85 91 className={cn("gap-2 flex flex-col", className)} 86 92 {...props} 87 93 /> 88 - ) 94 + ); 89 95 } 90 96 91 97 function DialogFooter({ ··· 94 100 children, 95 101 ...props 96 102 }: React.ComponentProps<"div"> & { 97 - showCloseButton?: boolean 103 + showCloseButton?: boolean; 98 104 }) { 99 105 return ( 100 106 <div 101 107 data-slot="dialog-footer" 102 108 className={cn( 103 109 "bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", 104 - className 110 + className, 105 111 )} 106 112 {...props} 107 113 > ··· 112 118 </DialogPrimitive.Close> 113 119 )} 114 120 </div> 115 - ) 121 + ); 116 122 } 117 123 118 124 function DialogTitle({ ··· 125 131 className={cn("text-base leading-none font-medium", className)} 126 132 {...props} 127 133 /> 128 - ) 134 + ); 129 135 } 130 136 131 137 function DialogDescription({ ··· 135 141 return ( 136 142 <DialogPrimitive.Description 137 143 data-slot="dialog-description" 138 - className={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)} 144 + className={cn( 145 + "text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", 146 + className, 147 + )} 139 148 {...props} 140 149 /> 141 - ) 150 + ); 142 151 } 143 152 144 153 export { ··· 152 161 DialogPortal, 153 162 DialogTitle, 154 163 DialogTrigger, 155 - } 164 + };
+33 -33
components/ui/dropdown-menu.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 4 - import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" 5 - import * as React from "react" 3 + import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 4 + import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; 5 + import * as React from "react"; 6 6 7 - import { cn } from "@/lib/utils" 7 + import { cn } from "@/lib/utils"; 8 8 9 9 function DropdownMenu({ 10 10 ...props 11 11 }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { 12 - return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> 12 + return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />; 13 13 } 14 14 15 15 function DropdownMenuPortal({ ··· 17 17 }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { 18 18 return ( 19 19 <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> 20 - ) 20 + ); 21 21 } 22 22 23 23 function DropdownMenuTrigger({ ··· 28 28 data-slot="dropdown-menu-trigger" 29 29 {...props} 30 30 /> 31 - ) 31 + ); 32 32 } 33 33 34 34 function DropdownMenuContent({ ··· 43 43 sideOffset={sideOffset} 44 44 className={cn( 45 45 "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", 46 - className 46 + className, 47 47 )} 48 48 {...props} 49 49 /> 50 50 </DropdownMenuPrimitive.Portal> 51 - ) 51 + ); 52 52 } 53 53 54 54 function DropdownMenuGroup({ ··· 56 56 }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { 57 57 return ( 58 58 <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> 59 - ) 59 + ); 60 60 } 61 61 62 62 function DropdownMenuItem({ ··· 65 65 variant = "default", 66 66 ...props 67 67 }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { 68 - inset?: boolean 69 - variant?: "default" | "destructive" 68 + inset?: boolean; 69 + variant?: "default" | "destructive"; 70 70 }) { 71 71 return ( 72 72 <DropdownMenuPrimitive.Item ··· 75 75 data-variant={variant} 76 76 className={cn( 77 77 "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 78 - className 78 + className, 79 79 )} 80 80 {...props} 81 81 /> 82 - ) 82 + ); 83 83 } 84 84 85 85 function DropdownMenuCheckboxItem({ ··· 93 93 data-slot="dropdown-menu-checkbox-item" 94 94 className={cn( 95 95 "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 96 - className 96 + className, 97 97 )} 98 98 checked={checked} 99 99 {...props} ··· 105 105 </span> 106 106 {children} 107 107 </DropdownMenuPrimitive.CheckboxItem> 108 - ) 108 + ); 109 109 } 110 110 111 111 function DropdownMenuRadioGroup({ ··· 116 116 data-slot="dropdown-menu-radio-group" 117 117 {...props} 118 118 /> 119 - ) 119 + ); 120 120 } 121 121 122 122 function DropdownMenuRadioItem({ ··· 129 129 data-slot="dropdown-menu-radio-item" 130 130 className={cn( 131 131 "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 132 - className 132 + className, 133 133 )} 134 134 {...props} 135 135 > ··· 140 140 </span> 141 141 {children} 142 142 </DropdownMenuPrimitive.RadioItem> 143 - ) 143 + ); 144 144 } 145 145 146 146 function DropdownMenuLabel({ ··· 148 148 inset, 149 149 ...props 150 150 }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { 151 - inset?: boolean 151 + inset?: boolean; 152 152 }) { 153 153 return ( 154 154 <DropdownMenuPrimitive.Label ··· 156 156 data-inset={inset} 157 157 className={cn( 158 158 "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", 159 - className 159 + className, 160 160 )} 161 161 {...props} 162 162 /> 163 - ) 163 + ); 164 164 } 165 165 166 166 function DropdownMenuSeparator({ ··· 173 173 className={cn("bg-border -mx-1 my-1 h-px", className)} 174 174 {...props} 175 175 /> 176 - ) 176 + ); 177 177 } 178 178 179 179 function DropdownMenuShortcut({ ··· 185 185 data-slot="dropdown-menu-shortcut" 186 186 className={cn( 187 187 "text-muted-foreground ml-auto text-xs tracking-widest", 188 - className 188 + className, 189 189 )} 190 190 {...props} 191 191 /> 192 - ) 192 + ); 193 193 } 194 194 195 195 function DropdownMenuSub({ 196 196 ...props 197 197 }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { 198 - return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> 198 + return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />; 199 199 } 200 200 201 201 function DropdownMenuSubTrigger({ ··· 204 204 children, 205 205 ...props 206 206 }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { 207 - inset?: boolean 207 + inset?: boolean; 208 208 }) { 209 209 return ( 210 210 <DropdownMenuPrimitive.SubTrigger ··· 212 212 data-inset={inset} 213 213 className={cn( 214 214 "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8", 215 - className 215 + className, 216 216 )} 217 217 {...props} 218 218 > 219 219 {children} 220 220 <ChevronRightIcon className="ml-auto size-4" /> 221 221 </DropdownMenuPrimitive.SubTrigger> 222 - ) 222 + ); 223 223 } 224 224 225 225 function DropdownMenuSubContent({ ··· 231 231 data-slot="dropdown-menu-sub-content" 232 232 className={cn( 233 233 "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", 234 - className 234 + className, 235 235 )} 236 236 {...props} 237 237 /> 238 - ) 238 + ); 239 239 } 240 240 241 241 export { ··· 254 254 DropdownMenuSub, 255 255 DropdownMenuSubTrigger, 256 256 DropdownMenuSubContent, 257 - } 257 + };
+16 -19
components/ui/empty.tsx
··· 1 - import type React from "react" 2 - import { cva, type VariantProps } from "class-variance-authority" 1 + import type React from "react"; 2 + import { cva, type VariantProps } from "class-variance-authority"; 3 3 4 - import { cn } from "@/lib/utils" 4 + import { cn } from "@/lib/utils"; 5 5 6 6 function Empty({ className, ...props }: React.ComponentProps<"div">) { 7 7 return ( ··· 9 9 data-slot="empty" 10 10 className={cn( 11 11 "gap-4 rounded-xl border-dashed p-6 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance", 12 - className 12 + className, 13 13 )} 14 14 {...props} 15 15 /> 16 - ) 16 + ); 17 17 } 18 18 19 19 function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { 20 20 return ( 21 21 <div 22 22 data-slot="empty-header" 23 - className={cn( 24 - "gap-2 flex max-w-sm flex-col items-center", 25 - className 26 - )} 23 + className={cn("gap-2 flex max-w-sm flex-col items-center", className)} 27 24 {...props} 28 25 /> 29 - ) 26 + ); 30 27 } 31 28 32 29 const emptyMediaVariants = cva( ··· 41 38 defaultVariants: { 42 39 variant: "default", 43 40 }, 44 - } 45 - ) 41 + }, 42 + ); 46 43 47 44 function EmptyMedia({ 48 45 className, ··· 56 53 className={cn(emptyMediaVariants({ variant, className }))} 57 54 {...props} 58 55 /> 59 - ) 56 + ); 60 57 } 61 58 62 59 function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { ··· 66 63 className={cn("text-sm font-medium tracking-tight", className)} 67 64 {...props} 68 65 /> 69 - ) 66 + ); 70 67 } 71 68 72 69 function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { ··· 75 72 data-slot="empty-description" 76 73 className={cn( 77 74 "text-sm/relaxed text-muted-foreground [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", 78 - className 75 + className, 79 76 )} 80 77 {...props} 81 78 /> 82 - ) 79 + ); 83 80 } 84 81 85 82 function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { ··· 88 85 data-slot="empty-content" 89 86 className={cn( 90 87 "gap-2.5 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance", 91 - className 88 + className, 92 89 )} 93 90 {...props} 94 91 /> 95 - ) 92 + ); 96 93 } 97 94 98 95 export { ··· 102 99 EmptyDescription, 103 100 EmptyContent, 104 101 EmptyMedia, 105 - } 102 + };
+5 -5
components/ui/input.tsx
··· 1 - import * as React from "react" 1 + import * as React from "react"; 2 2 3 - import { cn } from "@/lib/utils" 3 + import { cn } from "@/lib/utils"; 4 4 5 5 function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 6 return ( ··· 9 9 data-slot="input" 10 10 className={cn( 11 11 "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", 12 - className 12 + className, 13 13 )} 14 14 {...props} 15 15 /> 16 - ) 16 + ); 17 17 } 18 18 19 - export { Input } 19 + export { Input };
+5 -5
components/ui/kbd.tsx
··· 1 - import { cn } from "@/lib/utils" 1 + import { cn } from "@/lib/utils"; 2 2 3 3 function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { 4 4 return ( ··· 8 8 "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none", 9 9 "[&_svg:not([class*='size-'])]:size-3", 10 10 "[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10", 11 - className 11 + className, 12 12 )} 13 13 {...props} 14 14 /> 15 - ) 15 + ); 16 16 } 17 17 18 18 function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { ··· 22 22 className={cn("inline-flex items-center gap-1", className)} 23 23 {...props} 24 24 /> 25 - ) 25 + ); 26 26 } 27 27 28 - export { Kbd, KbdGroup } 28 + export { Kbd, KbdGroup };
+10 -10
components/ui/label.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { cva, type VariantProps } from "class-variance-authority" 4 - import * as LabelPrimitive from "@radix-ui/react-label" 5 - import * as React from "react" 3 + import { cva, type VariantProps } from "class-variance-authority"; 4 + import * as LabelPrimitive from "@radix-ui/react-label"; 5 + import * as React from "react"; 6 6 7 - import { cn } from "@/lib/utils" 7 + import { cn } from "@/lib/utils"; 8 8 9 9 const labelVariants = cva( 10 - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 - ) 10 + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 + ); 12 12 13 13 const Label = React.forwardRef< 14 14 React.ElementRef<typeof LabelPrimitive.Root>, ··· 20 20 className={cn(labelVariants(), className)} 21 21 {...props} 22 22 /> 23 - )) 24 - Label.displayName = LabelPrimitive.Root.displayName 23 + )); 24 + Label.displayName = LabelPrimitive.Root.displayName; 25 25 26 - export { Label } 26 + export { Label };
+13 -13
components/ui/popover.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { Popover as PopoverPrimitive } from "radix-ui" 4 - import * as React from "react" 3 + import { Popover as PopoverPrimitive } from "radix-ui"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 6 + import { cn } from "@/lib/utils"; 7 7 8 8 function Popover({ 9 9 ...props 10 10 }: React.ComponentProps<typeof PopoverPrimitive.Root>) { 11 - return <PopoverPrimitive.Root data-slot="popover" {...props} /> 11 + return <PopoverPrimitive.Root data-slot="popover" {...props} />; 12 12 } 13 13 14 14 function PopoverTrigger({ 15 15 ...props 16 16 }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { 17 - return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} /> 17 + return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; 18 18 } 19 19 20 20 function PopoverContent({ ··· 31 31 sideOffset={sideOffset} 32 32 className={cn( 33 33 "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 z-50 w-72 origin-(--radix-popover-content-transform-origin) outline-hidden", 34 - className 34 + className, 35 35 )} 36 36 {...props} 37 37 /> 38 38 </PopoverPrimitive.Portal> 39 - ) 39 + ); 40 40 } 41 41 42 42 function PopoverAnchor({ 43 43 ...props 44 44 }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { 45 - return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} /> 45 + return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; 46 46 } 47 47 48 48 function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { ··· 52 52 className={cn("flex flex-col gap-0.5 text-sm", className)} 53 53 {...props} 54 54 /> 55 - ) 55 + ); 56 56 } 57 57 58 58 function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) { ··· 62 62 className={cn("font-medium", className)} 63 63 {...props} 64 64 /> 65 - ) 65 + ); 66 66 } 67 67 68 68 function PopoverDescription({ ··· 75 75 className={cn("text-muted-foreground", className)} 76 76 {...props} 77 77 /> 78 - ) 78 + ); 79 79 } 80 80 81 81 export { ··· 86 86 PopoverHeader, 87 87 PopoverTitle, 88 88 PopoverTrigger, 89 - } 89 + };
+8 -8
components/ui/scroll-area.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" 4 - import * as React from "react" 3 + import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 6 + import { cn } from "@/lib/utils"; 7 7 8 8 function ScrollArea({ 9 9 className, ··· 25 25 <ScrollBar className="pointer-events-none opacity-0" /> 26 26 <ScrollAreaPrimitive.Corner /> 27 27 </ScrollAreaPrimitive.Root> 28 - ) 28 + ); 29 29 } 30 30 31 31 function ScrollBar({ ··· 40 40 orientation={orientation} 41 41 className={cn( 42 42 "data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none", 43 - className 43 + className, 44 44 )} 45 45 {...props} 46 46 > ··· 49 49 className="rounded-full bg-border relative flex-1" 50 50 /> 51 51 </ScrollAreaPrimitive.ScrollAreaScrollbar> 52 - ) 52 + ); 53 53 } 54 54 55 - export { ScrollArea, ScrollBar } 55 + export { ScrollArea, ScrollBar };
+36 -27
components/ui/select.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { Select as SelectPrimitive } from "radix-ui" 4 - import * as React from "react" 3 + import { Select as SelectPrimitive } from "radix-ui"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 7 - import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" 6 + import { cn } from "@/lib/utils"; 7 + import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"; 8 8 9 9 function Select({ 10 10 ...props 11 11 }: React.ComponentProps<typeof SelectPrimitive.Root>) { 12 - return <SelectPrimitive.Root data-slot="select" {...props} /> 12 + return <SelectPrimitive.Root data-slot="select" {...props} />; 13 13 } 14 14 15 15 function SelectGroup({ ··· 22 22 className={cn("scroll-my-1 p-1", className)} 23 23 {...props} 24 24 /> 25 - ) 25 + ); 26 26 } 27 27 28 28 function SelectValue({ 29 29 ...props 30 30 }: React.ComponentProps<typeof SelectPrimitive.Value>) { 31 - return <SelectPrimitive.Value data-slot="select-value" {...props} /> 31 + return <SelectPrimitive.Value data-slot="select-value" {...props} />; 32 32 } 33 33 34 34 function SelectTrigger({ ··· 37 37 children, 38 38 ...props 39 39 }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { 40 - size?: "sm" | "default" 40 + size?: "sm" | "default"; 41 41 }) { 42 42 return ( 43 43 <SelectPrimitive.Trigger ··· 45 45 data-size={size} 46 46 className={cn( 47 47 "border-input data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors select-none focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0", 48 - className 48 + className, 49 49 )} 50 50 {...props} 51 51 > ··· 54 54 <ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" /> 55 55 </SelectPrimitive.Icon> 56 56 </SelectPrimitive.Trigger> 57 - ) 57 + ); 58 58 } 59 59 60 60 function SelectContent({ ··· 69 69 <SelectPrimitive.Content 70 70 data-slot="select-content" 71 71 data-align-trigger={position === "item-aligned"} 72 - className={cn("bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 relative z-50 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )} 72 + className={cn( 73 + "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 relative z-50 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", 74 + position === "popper" && 75 + "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", 76 + className, 77 + )} 73 78 position={position} 74 79 align={align} 75 80 {...props} ··· 79 84 data-position={position} 80 85 className={cn( 81 86 "data-[position=popper]:h-[var(--radix-select-trigger-height)] data-[position=popper]:w-full data-[position=popper]:min-w-[var(--radix-select-trigger-width)]", 82 - position === "popper" && "" 87 + position === "popper" && "", 83 88 )} 84 89 > 85 90 {children} ··· 87 92 <SelectScrollDownButton /> 88 93 </SelectPrimitive.Content> 89 94 </SelectPrimitive.Portal> 90 - ) 95 + ); 91 96 } 92 97 93 98 function SelectLabel({ ··· 100 105 className={cn("text-muted-foreground px-1.5 py-1 text-xs", className)} 101 106 {...props} 102 107 /> 103 - ) 108 + ); 104 109 } 105 110 106 111 function SelectItem({ ··· 113 118 data-slot="select-item" 114 119 className={cn( 115 120 "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", 116 - className 121 + className, 117 122 )} 118 123 {...props} 119 124 > ··· 124 129 </span> 125 130 <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> 126 131 </SelectPrimitive.Item> 127 - ) 132 + ); 128 133 } 129 134 130 135 function SelectSeparator({ ··· 137 142 className={cn("bg-border -mx-1 my-1 h-px pointer-events-none", className)} 138 143 {...props} 139 144 /> 140 - ) 145 + ); 141 146 } 142 147 143 148 function SelectScrollUpButton({ ··· 147 152 return ( 148 153 <SelectPrimitive.ScrollUpButton 149 154 data-slot="select-scroll-up-button" 150 - className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", className)} 155 + className={cn( 156 + "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", 157 + className, 158 + )} 151 159 {...props} 152 160 > 153 - <ChevronUpIcon 154 - /> 161 + <ChevronUpIcon /> 155 162 </SelectPrimitive.ScrollUpButton> 156 - ) 163 + ); 157 164 } 158 165 159 166 function SelectScrollDownButton({ ··· 163 170 return ( 164 171 <SelectPrimitive.ScrollDownButton 165 172 data-slot="select-scroll-down-button" 166 - className={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", className)} 173 + className={cn( 174 + "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4", 175 + className, 176 + )} 167 177 {...props} 168 178 > 169 - <ChevronDownIcon 170 - /> 179 + <ChevronDownIcon /> 171 180 </SelectPrimitive.ScrollDownButton> 172 - ) 181 + ); 173 182 } 174 183 175 184 export { ··· 183 192 SelectSeparator, 184 193 SelectTrigger, 185 194 SelectValue, 186 - } 195 + };
+7 -7
components/ui/separator.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as SeparatorPrimitive from "@radix-ui/react-separator" 4 - import * as React from "react" 3 + import * as SeparatorPrimitive from "@radix-ui/react-separator"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 6 + import { cn } from "@/lib/utils"; 7 7 8 8 function Separator({ 9 9 className, ··· 18 18 orientation={orientation} 19 19 className={cn( 20 20 "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", 21 - className 21 + className, 22 22 )} 23 23 {...props} 24 24 /> 25 - ) 25 + ); 26 26 } 27 27 28 - export { Separator } 28 + export { Separator };
+90 -46
components/ui/sheet.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { cva, type VariantProps } from "class-variance-authority" 4 - import * as SheetPrimitive from "@radix-ui/react-dialog" 5 - import { X } from "lucide-react" 6 - import * as React from "react" 3 + import { cva, type VariantProps } from "class-variance-authority"; 4 + import * as SheetPrimitive from "@radix-ui/react-dialog"; 5 + import { X } from "lucide-react"; 6 + import * as React from "react"; 7 7 8 - import { cn } from "@/lib/utils" 8 + import { cn } from "@/lib/utils"; 9 9 10 - const Sheet = SheetPrimitive.Root 10 + const Sheet = SheetPrimitive.Root; 11 11 12 - const SheetTrigger = SheetPrimitive.Trigger 12 + const SheetTrigger = SheetPrimitive.Trigger; 13 13 14 - const SheetClose = SheetPrimitive.Close 14 + const SheetClose = SheetPrimitive.Close; 15 15 16 - const SheetPortal = ({ className, ...props }: SheetPrimitive.DialogPortalProps) => ( 16 + const SheetPortal = ({ 17 + className, 18 + ...props 19 + }: SheetPrimitive.DialogPortalProps) => ( 17 20 <SheetPrimitive.Portal className={cn(className)} {...props} /> 18 - ) 19 - SheetPortal.displayName = SheetPrimitive.Portal.displayName 21 + ); 22 + SheetPortal.displayName = SheetPrimitive.Portal.displayName; 20 23 21 24 const SheetOverlay = React.forwardRef< 22 25 React.ElementRef<typeof SheetPrimitive.Overlay>, ··· 30 33 {...props} 31 34 ref={ref} 32 35 /> 33 - )) 34 - SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 36 + )); 37 + SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; 35 38 36 39 const sheetVariants = cva( 37 40 "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", ··· 50 53 side: "right", 51 54 }, 52 55 }, 53 - ) 56 + ); 54 57 55 58 interface SheetContentProps 56 - extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, 59 + extends 60 + React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, 57 61 VariantProps<typeof sheetVariants> {} 58 62 59 - const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>( 60 - ({ side = "right", className, children, ...props }, ref) => ( 61 - <SheetPortal> 62 - <SheetOverlay /> 63 - <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}> 64 - {children} 65 - <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> 66 - <X className="h-4 w-4" /> 67 - <span className="sr-only">Close</span> 68 - </SheetPrimitive.Close> 69 - </SheetPrimitive.Content> 70 - </SheetPortal> 71 - ), 72 - ) 73 - SheetContent.displayName = SheetPrimitive.Content.displayName 63 + const SheetContent = React.forwardRef< 64 + React.ElementRef<typeof SheetPrimitive.Content>, 65 + SheetContentProps 66 + >(({ side = "right", className, children, ...props }, ref) => ( 67 + <SheetPortal> 68 + <SheetOverlay /> 69 + <SheetPrimitive.Content 70 + ref={ref} 71 + className={cn(sheetVariants({ side }), className)} 72 + {...props} 73 + > 74 + {children} 75 + <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> 76 + <X className="h-4 w-4" /> 77 + <span className="sr-only">Close</span> 78 + </SheetPrimitive.Close> 79 + </SheetPrimitive.Content> 80 + </SheetPortal> 81 + )); 82 + SheetContent.displayName = SheetPrimitive.Content.displayName; 74 83 75 - const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( 76 - <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} /> 77 - ) 78 - SheetHeader.displayName = "SheetHeader" 84 + const SheetHeader = ({ 85 + className, 86 + ...props 87 + }: React.HTMLAttributes<HTMLDivElement>) => ( 88 + <div 89 + className={cn( 90 + "flex flex-col space-y-2 text-center sm:text-left", 91 + className, 92 + )} 93 + {...props} 94 + /> 95 + ); 96 + SheetHeader.displayName = "SheetHeader"; 79 97 80 - const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( 81 - <div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} /> 82 - ) 83 - SheetFooter.displayName = "SheetFooter" 98 + const SheetFooter = ({ 99 + className, 100 + ...props 101 + }: React.HTMLAttributes<HTMLDivElement>) => ( 102 + <div 103 + className={cn( 104 + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", 105 + className, 106 + )} 107 + {...props} 108 + /> 109 + ); 110 + SheetFooter.displayName = "SheetFooter"; 84 111 85 112 const SheetTitle = React.forwardRef< 86 113 React.ElementRef<typeof SheetPrimitive.Title>, 87 114 React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> 88 115 >(({ className, ...props }, ref) => ( 89 - <SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} /> 90 - )) 91 - SheetTitle.displayName = SheetPrimitive.Title.displayName 116 + <SheetPrimitive.Title 117 + ref={ref} 118 + className={cn("text-lg font-semibold text-foreground", className)} 119 + {...props} 120 + /> 121 + )); 122 + SheetTitle.displayName = SheetPrimitive.Title.displayName; 92 123 93 124 const SheetDescription = React.forwardRef< 94 125 React.ElementRef<typeof SheetPrimitive.Description>, 95 126 React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> 96 127 >(({ className, ...props }, ref) => ( 97 - <SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> 98 - )) 99 - SheetDescription.displayName = SheetPrimitive.Description.displayName 128 + <SheetPrimitive.Description 129 + ref={ref} 130 + className={cn("text-sm text-muted-foreground", className)} 131 + {...props} 132 + /> 133 + )); 134 + SheetDescription.displayName = SheetPrimitive.Description.displayName; 100 135 101 - export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } 136 + export { 137 + Sheet, 138 + SheetTrigger, 139 + SheetClose, 140 + SheetContent, 141 + SheetHeader, 142 + SheetFooter, 143 + SheetTitle, 144 + SheetDescription, 145 + };
+23 -25
components/ui/sonner.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react" 4 - import { Toaster as Sonner, type ToasterProps } from "sonner" 5 - import { useTheme } from "next-themes" 6 - import { useLocalStorage } from "@/hooks/useLocalStorage" 3 + import { 4 + CircleCheckIcon, 5 + InfoIcon, 6 + TriangleAlertIcon, 7 + OctagonXIcon, 8 + Loader2Icon, 9 + } from "lucide-react"; 10 + import { Toaster as Sonner, type ToasterProps } from "sonner"; 11 + import { useTheme } from "next-themes"; 12 + import { useLocalStorage } from "@/hooks/useLocalStorage"; 7 13 8 14 const Toaster = ({ ...props }: ToasterProps) => { 9 - const { theme = "system" } = useTheme() 10 - const [toastPosition] = useLocalStorage<"bottom-left" | "bottom-center" | "bottom-right">("toast-position", "bottom-right") 15 + const { theme = "system" } = useTheme(); 16 + const [toastPosition] = useLocalStorage< 17 + "bottom-left" | "bottom-center" | "bottom-right" 18 + >("toast-position", "bottom-right"); 11 19 12 20 return ( 13 21 <Sonner 14 22 theme={theme as ToasterProps["theme"]} 15 23 className="toaster group" 16 24 icons={{ 17 - success: ( 18 - <CircleCheckIcon className="size-4" /> 19 - ), 20 - info: ( 21 - <InfoIcon className="size-4" /> 22 - ), 23 - warning: ( 24 - <TriangleAlertIcon className="size-4" /> 25 - ), 26 - error: ( 27 - <OctagonXIcon className="size-4" /> 28 - ), 29 - loading: ( 30 - <Loader2Icon className="size-4 animate-spin" /> 31 - ), 25 + success: <CircleCheckIcon className="size-4" />, 26 + info: <InfoIcon className="size-4" />, 27 + warning: <TriangleAlertIcon className="size-4" />, 28 + error: <OctagonXIcon className="size-4" />, 29 + loading: <Loader2Icon className="size-4 animate-spin" />, 32 30 }} 33 31 style={ 34 32 { ··· 46 44 }} 47 45 {...props} 48 46 /> 49 - ) 50 - } 47 + ); 48 + }; 51 49 52 - export { Toaster } 50 + export { Toaster };
+4 -4
components/ui/spinner.tsx
··· 1 - import { LoaderIcon } from "lucide-react" 1 + import { LoaderIcon } from "lucide-react"; 2 2 3 - import { cn } from "@/lib/utils" 3 + import { cn } from "@/lib/utils"; 4 4 5 5 function Spinner({ className, ...props }: React.ComponentProps<"svg">) { 6 6 return ( ··· 10 10 className={cn("size-4 animate-spin", className)} 11 11 {...props} 12 12 /> 13 - ) 13 + ); 14 14 } 15 15 16 - export { Spinner } 16 + export { Spinner };
+8 -8
components/ui/switch.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as SwitchPrimitive from "@radix-ui/react-switch" 4 - import * as React from "react" 3 + import * as SwitchPrimitive from "@radix-ui/react-switch"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 6 + import { cn } from "@/lib/utils"; 7 7 8 8 function Switch({ 9 9 className, ··· 14 14 data-slot="switch" 15 15 className={cn( 16 16 "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", 17 - className 17 + className, 18 18 )} 19 19 {...props} 20 20 > 21 21 <SwitchPrimitive.Thumb 22 22 data-slot="switch-thumb" 23 23 className={cn( 24 - "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0" 24 + "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0", 25 25 )} 26 26 /> 27 27 </SwitchPrimitive.Root> 28 - ) 28 + ); 29 29 } 30 30 31 - export { Switch } 31 + export { Switch };
+11 -11
components/ui/tabs.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import * as TabsPrimitive from "@radix-ui/react-tabs" 4 - import * as React from "react" 3 + import * as TabsPrimitive from "@radix-ui/react-tabs"; 4 + import * as React from "react"; 5 5 6 - import { cn } from "@/lib/utils" 6 + import { cn } from "@/lib/utils"; 7 7 8 8 function Tabs({ 9 9 className, ··· 15 15 className={cn("flex flex-col gap-2", className)} 16 16 {...props} 17 17 /> 18 - ) 18 + ); 19 19 } 20 20 21 21 function TabsList({ ··· 27 27 data-slot="tabs-list" 28 28 className={cn( 29 29 "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", 30 - className 30 + className, 31 31 )} 32 32 {...props} 33 33 /> 34 - ) 34 + ); 35 35 } 36 36 37 37 function TabsTrigger({ ··· 43 43 data-slot="tabs-trigger" 44 44 className={cn( 45 45 "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 46 - className 46 + className, 47 47 )} 48 48 {...props} 49 49 /> 50 - ) 50 + ); 51 51 } 52 52 53 53 function TabsContent({ ··· 60 60 className={cn("flex-1 outline-none", className)} 61 61 {...props} 62 62 /> 63 - ) 63 + ); 64 64 } 65 65 66 - export { Tabs, TabsList, TabsTrigger, TabsContent } 66 + export { Tabs, TabsList, TabsTrigger, TabsContent };
+5 -5
components/ui/textarea.tsx
··· 1 - import * as React from "react" 1 + import * as React from "react"; 2 2 3 - import { cn } from "@/lib/utils" 3 + import { cn } from "@/lib/utils"; 4 4 5 5 function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 6 return ( ··· 8 8 data-slot="textarea" 9 9 className={cn( 10 10 "border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 rounded-lg border bg-transparent px-2.5 py-2 text-base transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50", 11 - className 11 + className, 12 12 )} 13 13 {...props} 14 14 /> 15 - ) 15 + ); 16 16 } 17 17 18 - export { Textarea } 18 + export { Textarea };
+28 -28
components/ui/toast.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 - import { cva, type VariantProps } from "class-variance-authority" 4 - import * as ToastPrimitives from "@radix-ui/react-toast" 5 - import { X } from "lucide-react" 6 - import * as React from "react" 3 + import { cva, type VariantProps } from "class-variance-authority"; 4 + import * as ToastPrimitives from "@radix-ui/react-toast"; 5 + import { X } from "lucide-react"; 6 + import * as React from "react"; 7 7 8 - import { cn } from "@/lib/utils" 8 + import { cn } from "@/lib/utils"; 9 9 10 - const ToastProvider = ToastPrimitives.Provider 10 + const ToastProvider = ToastPrimitives.Provider; 11 11 12 12 const ToastViewport = React.forwardRef< 13 13 React.ElementRef<typeof ToastPrimitives.Viewport>, ··· 17 17 ref={ref} 18 18 className={cn( 19 19 "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", 20 - className 20 + className, 21 21 )} 22 22 {...props} 23 23 /> 24 - )) 25 - ToastViewport.displayName = ToastPrimitives.Viewport.displayName 24 + )); 25 + ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 26 26 27 27 const toastVariants = cva( 28 28 "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", ··· 37 37 defaultVariants: { 38 38 variant: "default", 39 39 }, 40 - } 41 - ) 40 + }, 41 + ); 42 42 43 43 const Toast = React.forwardRef< 44 44 React.ElementRef<typeof ToastPrimitives.Root>, ··· 51 51 className={cn(toastVariants({ variant }), className)} 52 52 {...props} 53 53 /> 54 - ) 55 - }) 56 - Toast.displayName = ToastPrimitives.Root.displayName 54 + ); 55 + }); 56 + Toast.displayName = ToastPrimitives.Root.displayName; 57 57 58 58 const ToastAction = React.forwardRef< 59 59 React.ElementRef<typeof ToastPrimitives.Action>, ··· 63 63 ref={ref} 64 64 className={cn( 65 65 "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", 66 - className 66 + className, 67 67 )} 68 68 {...props} 69 69 /> 70 - )) 71 - ToastAction.displayName = ToastPrimitives.Action.displayName 70 + )); 71 + ToastAction.displayName = ToastPrimitives.Action.displayName; 72 72 73 73 const ToastClose = React.forwardRef< 74 74 React.ElementRef<typeof ToastPrimitives.Close>, ··· 78 78 ref={ref} 79 79 className={cn( 80 80 "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", 81 - className 81 + className, 82 82 )} 83 83 toast-close="" 84 84 {...props} 85 85 > 86 86 <X className="h-4 w-4" /> 87 87 </ToastPrimitives.Close> 88 - )) 89 - ToastClose.displayName = ToastPrimitives.Close.displayName 88 + )); 89 + ToastClose.displayName = ToastPrimitives.Close.displayName; 90 90 91 91 const ToastTitle = React.forwardRef< 92 92 React.ElementRef<typeof ToastPrimitives.Title>, ··· 97 97 className={cn("text-sm font-semibold [&+div]:text-xs", className)} 98 98 {...props} 99 99 /> 100 - )) 101 - ToastTitle.displayName = ToastPrimitives.Title.displayName 100 + )); 101 + ToastTitle.displayName = ToastPrimitives.Title.displayName; 102 102 103 103 const ToastDescription = React.forwardRef< 104 104 React.ElementRef<typeof ToastPrimitives.Description>, ··· 109 109 className={cn("text-sm opacity-90", className)} 110 110 {...props} 111 111 /> 112 - )) 113 - ToastDescription.displayName = ToastPrimitives.Description.displayName 112 + )); 113 + ToastDescription.displayName = ToastPrimitives.Description.displayName; 114 114 115 - type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> 115 + type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>; 116 116 117 - type ToastActionElement = React.ReactElement<typeof ToastAction> 117 + type ToastActionElement = React.ReactElement<typeof ToastAction>; 118 118 119 119 export { 120 120 type ToastProps, ··· 126 126 ToastDescription, 127 127 ToastClose, 128 128 ToastAction, 129 - } 129 + };
+9 -7
components/ui/toaster.tsx
··· 1 - "use client" 1 + "use client"; 2 2 3 3 import { 4 4 Toast, ··· 7 7 ToastProvider, 8 8 ToastTitle, 9 9 ToastViewport, 10 - } from "@/components/ui/toast" 11 - import { useToast } from "@/components/ui/use-toast" 10 + } from "@/components/ui/toast"; 11 + import { useToast } from "@/components/ui/use-toast"; 12 12 13 13 export function Toaster() { 14 - const { toasts } = useToast() 14 + const { toasts } = useToast(); 15 15 16 16 return ( 17 17 <ToastProvider> ··· 20 20 <Toast key={id} {...props}> 21 21 <div className="grid gap-1"> 22 22 {title && <ToastTitle>{title}</ToastTitle>} 23 - {description && <ToastDescription>{description}</ToastDescription>} 23 + {description && ( 24 + <ToastDescription>{description}</ToastDescription> 25 + )} 24 26 </div> 25 27 {action} 26 28 <ToastClose /> 27 29 </Toast> 28 - ) 30 + ); 29 31 })} 30 32 <ToastViewport /> 31 33 </ToastProvider> 32 - ) 34 + ); 33 35 }