[READ-ONLY] 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
nextjs
0

Configure Feed

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

fix: run lint command and improve some code

+766 -250
+2
.gitignore
··· 42 42 43 43 # turbo 44 44 .turbo 45 + 46 + package-lock.json
+11 -4
app/(app)/app/page.tsx
··· 12 12 let active = true 13 13 const run = async () => { 14 14 try { 15 - const response = await fetch('/api/auth/get-session', { cache: 'no-store' }) 15 + const response = await fetch('/api/auth/get-session', { 16 + cache: 'no-store', 17 + }) 16 18 const data = response.ok ? await response.json() : null 17 19 if (!active) return 18 - const signedIn = data !== null && typeof data === 'object' && 'session' in data 20 + const signedIn = 21 + data !== null && typeof data === 'object' && 'session' in data 19 22 setIsSignedIn(signedIn) 20 23 } catch { 21 24 if (!active) return ··· 25 28 } 26 29 } 27 30 void run() 28 - return () => { active = false } 31 + return () => { 32 + active = false 33 + } 29 34 }, []) 30 35 31 36 useEffect(() => { ··· 47 52 } 48 53 } 49 54 void checkDb() 50 - return () => { active = false } 55 + return () => { 56 + active = false 57 + } 51 58 }, [sessionChecked, isSignedIn]) 52 59 53 60 const shouldShowAuthWait = useMemo(() => {
+3 -1
app/api/blob/check/route.ts
··· 27 27 ORDER BY table_name ASC 28 28 ` 29 29 30 - return NextResponse.json({ tables: tables.map((row: { table_name: string }) => row.table_name) }) 30 + return NextResponse.json({ 31 + tables: tables.map((row: { table_name: string }) => row.table_name), 32 + }) 31 33 } catch (error) { 32 34 return NextResponse.json( 33 35 {
+11 -8
app/api/blob/route.ts
··· 7 7 8 8 const NO_STORE_HEADERS = { 9 9 'Cache-Control': 'no-store, no-cache, must-revalidate', 10 - 'Pragma': 'no-cache', 11 - 'Expires': '0', 10 + Pragma: 'no-cache', 11 + Expires: '0', 12 12 } 13 13 14 14 function jsonNoStore(body: unknown, init?: ResponseInit) { 15 15 return NextResponse.json(body, { 16 16 ...init, 17 - headers: { ...NO_STORE_HEADERS, ...(init?.headers ?? {}) }, 17 + headers: { ...NO_STORE_HEADERS, ...init?.headers }, 18 18 }) 19 19 } 20 20 ··· 35 35 await prisma.calendarBackup.upsert({ 36 36 where: { userId }, 37 37 update: { encryptedData: encrypted_data, iv, timestamp: new Date() }, 38 - create: { userId, encryptedData: encrypted_data, iv, timestamp: new Date() }, 38 + create: { 39 + userId, 40 + encryptedData: encrypted_data, 41 + iv, 42 + timestamp: new Date(), 43 + }, 39 44 }) 40 45 41 46 return NextResponse.json({ success: true, backend: 'postgres' }) ··· 50 55 const session = await getServerSession() 51 56 const userId = session?.user?.id 52 57 53 - if (!userId) 54 - return jsonNoStore({ error: 'Unauthorized' }, { status: 401 }) 58 + if (!userId) return jsonNoStore({ error: 'Unauthorized' }, { status: 401 }) 55 59 56 60 const result = await prisma.calendarBackup.findUnique({ 57 61 where: { userId }, 58 62 select: { encryptedData: true, iv: true, timestamp: true }, 59 63 }) 60 - if (!result) 61 - return jsonNoStore({ error: 'Not found' }, { status: 404 }) 64 + if (!result) return jsonNoStore({ error: 'Not found' }, { status: 404 }) 62 65 63 66 return jsonNoStore({ 64 67 ciphertext: result.encryptedData,
+36 -6
app/api/share/list/route.ts
··· 11 11 return crypto.createHash('sha256').update(shareId, 'utf8').digest() 12 12 } 13 13 14 - function decryptWithKey(encryptedData: string, iv: string, authTag: string, key: Buffer) { 15 - const decipher = crypto.createDecipheriv(ALGORITHM, key, Buffer.from(iv, 'hex')) 14 + function decryptWithKey( 15 + encryptedData: string, 16 + iv: string, 17 + authTag: string, 18 + key: Buffer, 19 + ) { 20 + const decipher = crypto.createDecipheriv( 21 + ALGORITHM, 22 + key, 23 + Buffer.from(iv, 'hex'), 24 + ) 16 25 decipher.setAuthTag(Buffer.from(authTag, 'hex')) 17 26 let decrypted = decipher.update(encryptedData, 'hex', 'utf8') 18 27 decrypted += decipher.final('utf8') ··· 22 31 export async function GET() { 23 32 const session = await getServerSession() 24 33 const user = session?.user 25 - if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 34 + if (!user) 35 + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 26 36 27 37 const result = await prisma.share.findMany({ 28 38 where: { userId: user.id }, 29 39 orderBy: { timestamp: 'desc' }, 30 - select: { shareId: true, encryptedData: true, iv: true, authTag: true, timestamp: true, isProtected: true }, 40 + select: { 41 + shareId: true, 42 + encryptedData: true, 43 + iv: true, 44 + authTag: true, 45 + timestamp: true, 46 + isProtected: true, 47 + }, 31 48 }) 32 49 33 50 const shares = result.map((row: any) => { ··· 35 52 let eventTitle = '' 36 53 if (!row.isProtected) { 37 54 try { 38 - const decrypted = decryptWithKey(row.encryptedData, row.iv, row.authTag, keyV2Unprotected(row.shareId)) 55 + const decrypted = decryptWithKey( 56 + row.encryptedData, 57 + row.iv, 58 + row.authTag, 59 + keyV2Unprotected(row.shareId), 60 + ) 39 61 const dataObj = JSON.parse(decrypted) 40 62 eventId = dataObj.id ?? '' 41 63 eventTitle = dataObj.title ?? '' ··· 44 66 eventId = 'ๅ—ไฟๆŠค' 45 67 eventTitle = 'ๅ—ไฟๆŠค' 46 68 } 47 - return { id: row.shareId, eventId, eventTitle, sharedBy: user.id, shareDate: row.timestamp.toISOString(), shareLink: `/share/${row.shareId}`, isProtected: row.isProtected } 69 + return { 70 + id: row.shareId, 71 + eventId, 72 + eventTitle, 73 + sharedBy: user.id, 74 + shareDate: row.timestamp.toISOString(), 75 + shareLink: `/share/${row.shareId}`, 76 + isProtected: row.isProtected, 77 + } 48 78 }) 49 79 50 80 return NextResponse.json({ shares })
+135 -23
app/api/share/route.ts
··· 24 24 let encrypted = cipher.update(data, 'utf8', 'hex') 25 25 encrypted += cipher.final('hex') 26 26 const authTag = cipher.getAuthTag() 27 - return { encryptedData: encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') } 27 + return { 28 + encryptedData: encrypted, 29 + iv: iv.toString('hex'), 30 + authTag: authTag.toString('hex'), 31 + } 28 32 } 29 33 30 - function decryptWithKey(encryptedData: string, iv: string, authTag: string, key: Buffer): string { 34 + function decryptWithKey( 35 + encryptedData: string, 36 + iv: string, 37 + authTag: string, 38 + key: Buffer, 39 + ): string { 31 40 const ivBuffer = Buffer.from(iv, 'hex') 32 41 const authTagBuffer = Buffer.from(authTag, 'hex') 33 42 const decipher = crypto.createDecipheriv(ALGORITHM, key, ivBuffer) ··· 40 49 export async function POST(request: NextRequest) { 41 50 try { 42 51 const body = await request.json() 43 - const { id, data, password, burnAfterRead } = body as { id?: string; data?: unknown; password?: string; burnAfterRead?: boolean } 44 - if (!id || data === undefined || data === null) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }) 52 + const { id, data, password, burnAfterRead } = body as { 53 + id?: string 54 + data?: unknown 55 + password?: string 56 + burnAfterRead?: boolean 57 + } 58 + if (!id || data === undefined || data === null) 59 + return NextResponse.json( 60 + { error: 'Missing required fields' }, 61 + { status: 400 }, 62 + ) 45 63 46 64 const session = await getServerSession() 47 65 const user = session?.user 48 - if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 66 + if (!user) 67 + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 49 68 50 69 const hasPassword = typeof password === 'string' && password.length > 0 51 70 const burn = !!burnAfterRead 52 71 const dataString = typeof data === 'string' ? data : JSON.stringify(data) 53 - const key = hasPassword ? keyV3Password(password as string, id) : keyV2Unprotected(id) 72 + const key = hasPassword 73 + ? keyV3Password(password as string, id) 74 + : keyV2Unprotected(id) 54 75 const { encryptedData, iv, authTag } = encryptWithKey(dataString, key) 55 76 const now = new Date() 56 77 57 78 await prisma.share.upsert({ 58 79 where: { shareId: id }, 59 - update: { userId: user.id, encryptedData, iv, authTag, timestamp: now, isProtected: hasPassword, isBurn: burn, encVersion: hasPassword ? 3 : 2 }, 60 - create: { userId: user.id, shareId: id, encryptedData, iv, authTag, timestamp: now, isProtected: hasPassword, isBurn: burn, encVersion: hasPassword ? 3 : 2 }, 80 + update: { 81 + userId: user.id, 82 + encryptedData, 83 + iv, 84 + authTag, 85 + timestamp: now, 86 + isProtected: hasPassword, 87 + isBurn: burn, 88 + encVersion: hasPassword ? 3 : 2, 89 + }, 90 + create: { 91 + userId: user.id, 92 + shareId: id, 93 + encryptedData, 94 + iv, 95 + authTag, 96 + timestamp: now, 97 + isProtected: hasPassword, 98 + isBurn: burn, 99 + encVersion: hasPassword ? 3 : 2, 100 + }, 61 101 }) 62 102 63 - return NextResponse.json({ success: true, id, protected: hasPassword, burnAfterRead: burn, shareLink: `/share/${id}` }) 103 + return NextResponse.json({ 104 + success: true, 105 + id, 106 + protected: hasPassword, 107 + burnAfterRead: burn, 108 + shareLink: `/share/${id}`, 109 + }) 64 110 } catch (error) { 65 - return NextResponse.json({ error: error instanceof Error ? error.message : 'Unknown error occurred' }, { status: 500 }) 111 + return NextResponse.json( 112 + { 113 + error: 114 + error instanceof Error ? error.message : 'Unknown error occurred', 115 + }, 116 + { status: 500 }, 117 + ) 66 118 } 67 119 } 68 120 69 121 export async function GET(request: NextRequest) { 70 122 const id = request.nextUrl.searchParams.get('id') 71 123 const password = request.nextUrl.searchParams.get('password') ?? '' 72 - if (!id) return NextResponse.json({ error: 'Missing share ID' }, { status: 400 }) 124 + if (!id) 125 + return NextResponse.json({ error: 'Missing share ID' }, { status: 400 }) 73 126 74 127 try { 75 128 const result = await prisma.$transaction(async (tx: any) => { 76 - const share = await tx.share.findUnique({ where: { shareId: id }, select: { encryptedData: true, iv: true, authTag: true, timestamp: true, isProtected: true, isBurn: true } }) 129 + const share = await tx.share.findUnique({ 130 + where: { shareId: id }, 131 + select: { 132 + encryptedData: true, 133 + iv: true, 134 + authTag: true, 135 + timestamp: true, 136 + isProtected: true, 137 + isBurn: true, 138 + }, 139 + }) 77 140 if (!share) return { status: 404 as const } 78 - if (share.isProtected && !password) return { status: 401 as const, burnAfterRead: share.isBurn } 141 + if (share.isProtected && !password) 142 + return { status: 401 as const, burnAfterRead: share.isBurn } 79 143 80 - const key = share.isProtected ? keyV3Password(password, id) : keyV2Unprotected(id) 144 + const key = share.isProtected 145 + ? keyV3Password(password, id) 146 + : keyV2Unprotected(id) 81 147 let decryptedData: string 82 - try { decryptedData = decryptWithKey(share.encryptedData, share.iv, share.authTag, key) } catch { return { status: 403 as const, protected: share.isProtected } } 148 + try { 149 + decryptedData = decryptWithKey( 150 + share.encryptedData, 151 + share.iv, 152 + share.authTag, 153 + key, 154 + ) 155 + } catch { 156 + return { status: 403 as const, protected: share.isProtected } 157 + } 83 158 if (share.isBurn) await tx.share.delete({ where: { shareId: id } }) 84 159 85 - return { status: 200 as const, data: decryptedData, timestamp: share.timestamp.toISOString(), protected: share.isProtected, burnAfterRead: share.isBurn } 160 + return { 161 + status: 200 as const, 162 + data: decryptedData, 163 + timestamp: share.timestamp.toISOString(), 164 + protected: share.isProtected, 165 + burnAfterRead: share.isBurn, 166 + } 86 167 }) 87 168 88 - if (result.status === 404) return NextResponse.json({ error: 'Share not found' }, { status: 404 }) 89 - if (result.status === 401) return NextResponse.json({ error: 'Password required', requiresPassword: true, burnAfterRead: result.burnAfterRead }, { status: 401 }) 90 - if (result.status === 403) return NextResponse.json({ error: result.protected ? 'Invalid password' : 'Failed to decrypt share data.' }, { status: 403 }) 169 + if (result.status === 404) 170 + return NextResponse.json({ error: 'Share not found' }, { status: 404 }) 171 + if (result.status === 401) 172 + return NextResponse.json( 173 + { 174 + error: 'Password required', 175 + requiresPassword: true, 176 + burnAfterRead: result.burnAfterRead, 177 + }, 178 + { status: 401 }, 179 + ) 180 + if (result.status === 403) 181 + return NextResponse.json( 182 + { 183 + error: result.protected 184 + ? 'Invalid password' 185 + : 'Failed to decrypt share data.', 186 + }, 187 + { status: 403 }, 188 + ) 91 189 92 - return NextResponse.json({ success: true, data: result.data, timestamp: result.timestamp, protected: result.protected, burnAfterRead: result.burnAfterRead }) 190 + return NextResponse.json({ 191 + success: true, 192 + data: result.data, 193 + timestamp: result.timestamp, 194 + protected: result.protected, 195 + burnAfterRead: result.burnAfterRead, 196 + }) 93 197 } catch (error) { 94 - return NextResponse.json({ error: error instanceof Error ? error.message : 'Unknown error occurred' }, { status: 500 }) 198 + return NextResponse.json( 199 + { 200 + error: 201 + error instanceof Error ? error.message : 'Unknown error occurred', 202 + }, 203 + { status: 500 }, 204 + ) 95 205 } 96 206 } 97 207 98 208 export async function DELETE(request: NextRequest) { 99 209 const body = await request.json() 100 210 const { id } = body as { id?: string } 101 - if (!id) return NextResponse.json({ error: 'Missing share ID' }, { status: 400 }) 211 + if (!id) 212 + return NextResponse.json({ error: 'Missing share ID' }, { status: 400 }) 102 213 103 214 const session = await getServerSession() 104 215 const user = session?.user 105 - if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 216 + if (!user) 217 + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 106 218 107 219 await prisma.share.deleteMany({ where: { shareId: id, userId: user.id } }) 108 220 return NextResponse.json({ success: true })
+19 -4
components/app/analytics/analytics-utils.ts
··· 15 15 16 16 export type AnalyticsRangePreset = 'week' | 'month' | 'quarter' 17 17 18 - const COLOR_ORDER = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'] 18 + const COLOR_ORDER = [ 19 + '#3b82f6', 20 + '#10b981', 21 + '#f59e0b', 22 + '#ef4444', 23 + '#8b5cf6', 24 + '#ec4899', 25 + '#14b8a6', 26 + ] 19 27 20 28 const COLOR_MAP: Record<string, string> = { 21 29 'bg-blue-500': '#3b82f6', ··· 80 88 return events 81 89 .map((event) => { 82 90 const raw = event as CalendarEvent & { createdAt?: string | Date } 83 - const start = raw.startDate instanceof Date ? raw.startDate : parseISO(String(raw.startDate)) 84 - const end = raw.endDate instanceof Date ? raw.endDate : parseISO(String(raw.endDate)) 91 + const start = 92 + raw.startDate instanceof Date 93 + ? raw.startDate 94 + : parseISO(String(raw.startDate)) 95 + const end = 96 + raw.endDate instanceof Date 97 + ? raw.endDate 98 + : parseISO(String(raw.endDate)) 85 99 const createdAtRaw = raw.createdAt 86 100 const createdAt = createdAtRaw 87 101 ? createdAtRaw instanceof Date ··· 130 144 131 145 export const groupDayKey = (date: Date): string => format(date, 'yyyy-MM-dd') 132 146 133 - export const groupMonthKey = (date: Date): string => format(startOfMonth(date), 'yyyy-MM') 147 + export const groupMonthKey = (date: Date): string => 148 + format(startOfMonth(date), 'yyyy-MM') 134 149 135 150 export const getMonthDays = (year: number): Date[] => { 136 151 return eachDayOfInterval({
+5 -1
components/app/analytics/analytics-view.tsx
··· 42 42 <div className="flex justify-between items-center"> 43 43 <h1 className="text-2xl font-bold">{t.analytics}</h1> 44 44 </div> 45 - <TimeAnalyticsComponent events={events} calendars={calendars} key={`time-analytics-${language}-${forceUpdate}`} /> 45 + <TimeAnalyticsComponent 46 + events={events} 47 + calendars={calendars} 48 + key={`time-analytics-${language}-${forceUpdate}`} 49 + /> 46 50 </div> 47 51 ) 48 52 }
+8 -2
components/app/analytics/charts/category-average-duration-chart.tsx
··· 39 39 </CardHeader> 40 40 <CardContent> 41 41 {data.length === 0 ? ( 42 - <div className="flex h-[320px] items-center justify-center text-sm text-muted-foreground">{t.noData}</div> 42 + <div className="flex h-[320px] items-center justify-center text-sm text-muted-foreground"> 43 + {t.noData} 44 + </div> 43 45 ) : ( 44 46 <ChartContainer config={chartConfig} className="h-[320px] w-full"> 45 47 <BarChart ··· 51 53 <XAxis type="number" unit="h" /> 52 54 <YAxis type="category" dataKey="category" width={96} /> 53 55 <ChartTooltip 54 - content={<ChartTooltipContent formatter={(value) => `${value} ${t.analyticsHourUnit}`} />} 56 + content={ 57 + <ChartTooltipContent 58 + formatter={(value) => `${value} ${t.analyticsHourUnit}`} 59 + /> 60 + } 55 61 /> 56 62 <Bar dataKey="hours" radius={[0, 8, 8, 0]}> 57 63 {data.map((entry) => (
+15 -4
components/app/analytics/charts/category-donut-chart.tsx
··· 39 39 </CardHeader> 40 40 <CardContent> 41 41 {data.length === 0 ? ( 42 - <div className="flex h-[280px] items-center justify-center text-sm text-muted-foreground">{t.noData}</div> 42 + <div className="flex h-[280px] items-center justify-center text-sm text-muted-foreground"> 43 + {t.noData} 44 + </div> 43 45 ) : ( 44 46 <div className="grid gap-4 md:grid-cols-[280px_1fr]"> 45 47 <ChartContainer config={chartConfig} className="h-[280px] w-full"> ··· 56 58 ))} 57 59 </Pie> 58 60 <ChartTooltip 59 - content={<ChartTooltipContent formatter={(value) => `${value} ${t.analyticsCountUnit}`} />} 61 + content={ 62 + <ChartTooltipContent 63 + formatter={(value) => `${value} ${t.analyticsCountUnit}`} 64 + /> 65 + } 60 66 /> 61 67 </PieChart> 62 68 </ChartContainer> 63 69 <div className="space-y-3"> 64 70 {data.map((item) => ( 65 - <div key={item.category} className="flex items-center justify-between text-sm"> 71 + <div 72 + key={item.category} 73 + className="flex items-center justify-between text-sm" 74 + > 66 75 <div className="flex items-center gap-2"> 67 76 <span 68 77 className="h-2.5 w-2.5 rounded-full" ··· 70 79 /> 71 80 <span>{item.category}</span> 72 81 </div> 73 - <span className="font-medium">{item.percent.toFixed(1)}%</span> 82 + <span className="font-medium"> 83 + {item.percent.toFixed(1)}% 84 + </span> 74 85 </div> 75 86 ))} 76 87 </div>
+11 -4
components/app/analytics/charts/daily-monthly-count-chart.tsx
··· 68 68 <Card> 69 69 <CardHeader className="flex flex-row items-center justify-between space-y-0"> 70 70 <CardTitle>{t.analyticsDailyMonthlyCountTitle}</CardTitle> 71 - <Tabs value={mode} onValueChange={(value) => onModeChange(value as 'day' | 'month')}> 71 + <Tabs 72 + value={mode} 73 + onValueChange={(value) => onModeChange(value as 'day' | 'month')} 74 + > 72 75 <TabsList> 73 76 <TabsTrigger value="day">{t.analyticsByDay}</TabsTrigger> 74 77 <TabsTrigger value="month">{t.analyticsByMonth}</TabsTrigger> ··· 77 80 </CardHeader> 78 81 <CardContent> 79 82 {activeData.length === 0 || series.length === 0 ? ( 80 - <div className="flex h-[320px] items-center justify-center text-sm text-muted-foreground">{t.noData}</div> 83 + <div className="flex h-[320px] items-center justify-center text-sm text-muted-foreground"> 84 + {t.noData} 85 + </div> 81 86 ) : ( 82 87 <ChartContainer config={chartConfig} className="h-[320px] w-full"> 83 88 <BarChart data={activeData} margin={{ left: 8, right: 8, top: 8 }}> ··· 100 105 style={{ backgroundColor: item.color }} 101 106 /> 102 107 <span className="text-muted-foreground"> 103 - {seriesLabelMap.get(String(item.dataKey ?? '')) ?? name} 108 + {seriesLabelMap.get(String(item.dataKey ?? '')) ?? 109 + name} 104 110 </span> 105 111 </div> 106 112 <span className="font-mono font-medium text-foreground tabular-nums"> ··· 120 126 fill={item.color} 121 127 > 122 128 {activeData.map((datum) => { 123 - const isTopSegment = topDataKeyByLabel.get(datum.label) === item.key 129 + const isTopSegment = 130 + topDataKeyByLabel.get(datum.label) === item.key 124 131 return ( 125 132 <Cell 126 133 key={`${item.key}-${datum.label}`}
+5 -2
components/app/analytics/charts/weekday-stacked-duration-chart.tsx
··· 61 61 </CardHeader> 62 62 <CardContent> 63 63 {data.length === 0 || series.length === 0 ? ( 64 - <div className="flex h-[320px] items-center justify-center text-sm text-muted-foreground">{t.noData}</div> 64 + <div className="flex h-[320px] items-center justify-center text-sm text-muted-foreground"> 65 + {t.noData} 66 + </div> 65 67 ) : ( 66 68 <ChartContainer config={chartConfig} className="h-[320px] w-full"> 67 69 <BarChart data={data} margin={{ left: 8, right: 16, top: 8 }}> ··· 97 99 fill={item.color} 98 100 > 99 101 {data.map((datum) => { 100 - const isTopSegment = topDataKeyByDay.get(datum.day) === item.key 102 + const isTopSegment = 103 + topDataKeyByDay.get(datum.day) === item.key 101 104 return ( 102 105 <Cell 103 106 key={`${item.key}-${datum.day}`}
+19 -5
components/app/analytics/charts/year-heatmap-chart.tsx
··· 36 36 const firstDayOfYear = startOfYear(new Date(selectedYear, 0, 1)) 37 37 const lastDayOfYear = endOfYear(new Date(selectedYear, 11, 31)) 38 38 const startDay = startOfWeek(firstDayOfYear, { weekStartsOn: 1 }) 39 - const endDay = addDays(lastDayOfYear, 7 - ((getDay(lastDayOfYear) + 6) % 7) - 1) 39 + const endDay = addDays( 40 + lastDayOfYear, 41 + 7 - ((getDay(lastDayOfYear) + 6) % 7) - 1, 42 + ) 40 43 const totalDays = differenceInDays(endDay, startDay) + 1 41 44 const totalWeeks = Math.ceil(totalDays / 7) 42 - const countMap = new Map(data.map((item) => [format(item.date, 'yyyy-MM-dd'), item.count])) 45 + const countMap = new Map( 46 + data.map((item) => [format(item.date, 'yyyy-MM-dd'), item.count]), 47 + ) 43 48 const maxCount = data.reduce((max, item) => Math.max(max, item.count), 0) 44 49 45 50 const weekdayLabels = [ ··· 82 87 <div className="overflow-x-auto pb-2"> 83 88 <div 84 89 className="relative" 85 - style={{ minWidth: `${Math.max(totalWeeks * 16 + 72, 760)}px`, paddingTop: '20px' }} 90 + style={{ 91 + minWidth: `${Math.max(totalWeeks * 16 + 72, 760)}px`, 92 + paddingTop: '20px', 93 + }} 86 94 > 87 95 <div className="absolute left-12 right-0 top-0"> 88 96 {monthLabels.map((month) => ( ··· 120 128 <div 121 129 key={key} 122 130 className={`h-3 w-3 rounded-sm transition-colors hover:ring-1 hover:ring-ring ${ 123 - isCurrentYear ? intensityClasses[getLevel(count)] : 'bg-transparent' 131 + isCurrentYear 132 + ? intensityClasses[getLevel(count)] 133 + : 'bg-transparent' 124 134 }`} 125 - title={isCurrentYear ? `${key}: ${count} ${t.analyticsScheduleUnit}` : ''} 135 + title={ 136 + isCurrentYear 137 + ? `${key}: ${count} ${t.analyticsScheduleUnit}` 138 + : '' 139 + } 126 140 /> 127 141 ) 128 142 })}
+76 -32
components/app/analytics/import-export.tsx
··· 13 13 DialogHeader, 14 14 DialogTitle, 15 15 } from '@/components/ui/dialog' 16 - import { 17 - Download, 18 - Upload, 19 - CalendarIcon, 20 - ExternalLink, 21 - AlertCircle, 22 - } from 'lucide-react' 16 + import { Download, Upload, AlertCircle } from 'lucide-react' 23 17 import { 24 18 decryptPayload, 25 19 encryptPayload, ··· 187 181 SETTINGS_KEYS.timezone, 188 182 undefined, 189 183 ), 190 - notificationSound: await readEncryptedLocalStorage<string | undefined>( 191 - SETTINGS_KEYS.notificationSound, 192 - undefined, 193 - ), 184 + notificationSound: await readEncryptedLocalStorage< 185 + string | undefined 186 + >(SETTINGS_KEYS.notificationSound, undefined), 194 187 defaultView: await readEncryptedLocalStorage<string | undefined>( 195 188 SETTINGS_KEYS.defaultView, 196 189 undefined, ··· 199 192 SETTINGS_KEYS.enableShortcuts, 200 193 undefined, 201 194 ), 202 - timeFormat: await readEncryptedLocalStorage<'24h' | '12h' | undefined>( 203 - SETTINGS_KEYS.timeFormat, 204 - undefined, 205 - ), 195 + timeFormat: await readEncryptedLocalStorage< 196 + '24h' | '12h' | undefined 197 + >(SETTINGS_KEYS.timeFormat, undefined), 206 198 toastPosition: await readEncryptedLocalStorage< 207 199 'bottom-left' | 'bottom-center' | 'bottom-right' | undefined 208 200 >(SETTINGS_KEYS.toastPosition, undefined), ··· 401 393 } 402 394 } 403 395 404 - const normalizeImportedEvent = (input: Partial<CalendarEvent>): CalendarEvent => { 396 + const normalizeImportedEvent = ( 397 + input: Partial<CalendarEvent>, 398 + ): CalendarEvent => { 405 399 const start = input.startDate ? new Date(input.startDate) : new Date() 406 - const parsedEnd = input.endDate ? new Date(input.endDate) : new Date(start.getTime() + 60 * 60 * 1000) 407 - const end = parsedEnd < start ? new Date(start.getTime() + 60 * 60 * 1000) : parsedEnd 400 + const parsedEnd = input.endDate 401 + ? new Date(input.endDate) 402 + : new Date(start.getTime() + 60 * 60 * 1000) 403 + const end = 404 + parsedEnd < start ? new Date(start.getTime() + 60 * 60 * 1000) : parsedEnd 408 405 409 406 return { 410 407 id: input.id || `${Date.now()}${Math.random().toString(36).slice(2, 9)}`, ··· 415 412 recurrence: input.recurrence || 'none', 416 413 location: input.location, 417 414 participants: Array.isArray(input.participants) ? input.participants : [], 418 - notification: typeof input.notification === 'number' ? input.notification : 0, 415 + notification: 416 + typeof input.notification === 'number' ? input.notification : 0, 419 417 description: input.description, 420 418 color: input.color || 'bg-[#E6F6FD]', 421 419 calendarId: input.calendarId || '', 422 420 } 423 421 } 424 422 425 - const mergeCategoriesFromBackup = (importedCategories: ImportedCategory[]) => { 423 + const mergeCategoriesFromBackup = ( 424 + importedCategories: ImportedCategory[], 425 + ) => { 426 426 if (importedCategories.length === 0) return 427 427 428 428 const merged = [...calendars] ··· 448 448 449 449 const parseJsonEvents = async ( 450 450 rawContent: string, 451 - ): Promise<{ events: CalendarEvent[]; shouldApplyImportCategory: boolean }> => { 451 + ): Promise<{ 452 + events: CalendarEvent[] 453 + shouldApplyImportCategory: boolean 454 + }> => { 452 455 const parsed = JSON.parse(rawContent) 453 456 454 457 if (isEncryptedPayload(parsed) || parsed?.encrypted) { ··· 511 514 } 512 515 }) 513 516 514 - const autoCategories: ImportedCategory[] = Array.from(extraCategoryIds).map((id) => ({ 517 + const autoCategories: ImportedCategory[] = Array.from( 518 + extraCategoryIds, 519 + ).map((id) => ({ 515 520 id, 516 521 name: `Imported ${id.slice(0, 6)}`, 517 522 color: 'bg-blue-500', ··· 520 525 mergeCategoriesFromBackup([...importedCategories, ...autoCategories]) 521 526 522 527 if (Array.isArray(payload.data.bookmarks)) { 523 - await writeEncryptedLocalStorage('bookmarked-events', payload.data.bookmarks) 528 + await writeEncryptedLocalStorage( 529 + 'bookmarked-events', 530 + payload.data.bookmarks, 531 + ) 524 532 } 525 533 if (Array.isArray(payload.data.countdowns)) { 526 534 await writeEncryptedLocalStorage('countdowns', payload.data.countdowns) ··· 529 537 const settings = payload.data.settings || {} 530 538 const settingWrites: Promise<void>[] = [] 531 539 if (settings.language !== undefined) { 532 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.language, settings.language)) 540 + settingWrites.push( 541 + writeEncryptedLocalStorage(SETTINGS_KEYS.language, settings.language), 542 + ) 533 543 } 534 544 if (settings.firstDayOfWeek !== undefined) { 535 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.firstDayOfWeek, settings.firstDayOfWeek)) 545 + settingWrites.push( 546 + writeEncryptedLocalStorage( 547 + SETTINGS_KEYS.firstDayOfWeek, 548 + settings.firstDayOfWeek, 549 + ), 550 + ) 536 551 } 537 552 if (settings.timezone !== undefined) { 538 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.timezone, settings.timezone)) 553 + settingWrites.push( 554 + writeEncryptedLocalStorage(SETTINGS_KEYS.timezone, settings.timezone), 555 + ) 539 556 } 540 557 if (settings.notificationSound !== undefined) { 541 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.notificationSound, settings.notificationSound)) 558 + settingWrites.push( 559 + writeEncryptedLocalStorage( 560 + SETTINGS_KEYS.notificationSound, 561 + settings.notificationSound, 562 + ), 563 + ) 542 564 } 543 565 if (settings.defaultView !== undefined) { 544 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.defaultView, settings.defaultView)) 566 + settingWrites.push( 567 + writeEncryptedLocalStorage( 568 + SETTINGS_KEYS.defaultView, 569 + settings.defaultView, 570 + ), 571 + ) 545 572 } 546 573 if (settings.enableShortcuts !== undefined) { 547 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.enableShortcuts, settings.enableShortcuts)) 574 + settingWrites.push( 575 + writeEncryptedLocalStorage( 576 + SETTINGS_KEYS.enableShortcuts, 577 + settings.enableShortcuts, 578 + ), 579 + ) 548 580 } 549 581 if (settings.timeFormat !== undefined) { 550 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.timeFormat, settings.timeFormat)) 582 + settingWrites.push( 583 + writeEncryptedLocalStorage( 584 + SETTINGS_KEYS.timeFormat, 585 + settings.timeFormat, 586 + ), 587 + ) 551 588 } 552 589 if (settings.toastPosition !== undefined) { 553 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.toastPosition, settings.toastPosition)) 590 + settingWrites.push( 591 + writeEncryptedLocalStorage( 592 + SETTINGS_KEYS.toastPosition, 593 + settings.toastPosition, 594 + ), 595 + ) 554 596 } 555 597 if (settings.theme !== undefined) { 556 - settingWrites.push(writeEncryptedLocalStorage(SETTINGS_KEYS.theme, settings.theme)) 598 + settingWrites.push( 599 + writeEncryptedLocalStorage(SETTINGS_KEYS.theme, settings.theme), 600 + ) 557 601 } 558 602 await Promise.all(settingWrites) 559 603
+7 -1
components/app/analytics/metrics/analytics-metrics-grid.tsx
··· 1 1 'use client' 2 2 3 - import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' 3 + import { 4 + Card, 5 + CardContent, 6 + CardDescription, 7 + CardHeader, 8 + CardTitle, 9 + } from '@/components/ui/card' 4 10 5 11 interface MetricItem { 6 12 title: string
+108 -30
components/app/analytics/time-analytics.tsx
··· 19 19 resolveDateRange, 20 20 type AnalyticsRangePreset, 21 21 } from './analytics-utils' 22 - import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' 22 + import { 23 + Select, 24 + SelectContent, 25 + SelectItem, 26 + SelectTrigger, 27 + SelectValue, 28 + } from '@/components/ui/select' 23 29 import { DailyMonthlyCountChart } from './charts/daily-monthly-count-chart' 24 30 import { YearHeatmapChart } from './charts/year-heatmap-chart' 25 31 import { CategoryDonutChart } from './charts/category-donut-chart' ··· 34 40 key?: string 35 41 } 36 42 37 - export default function TimeAnalyticsComponent({ events, calendars = [] }: TimeAnalyticsProps) { 43 + export default function TimeAnalyticsComponent({ 44 + events, 45 + calendars = [], 46 + }: TimeAnalyticsProps) { 38 47 const [language] = useLanguage() 39 48 const t = translations[language] 40 49 const weekdayLabels = [ ··· 59 68 const futureDateRange = useMemo( 60 69 () => ({ 61 70 start: now, 62 - end: new Date(now.getTime() + (dateRange.end.getTime() - dateRange.start.getTime())), 71 + end: new Date( 72 + now.getTime() + (dateRange.end.getTime() - dateRange.start.getTime()), 73 + ), 63 74 }), 64 75 [dateRange.end, dateRange.start, now], 65 76 ) 66 77 67 - const normalizedEvents = useMemo(() => mapEventsToAnalyticsEvents(events), [events]) 68 - const rangeEvents = useMemo(() => filterEventsInRange(normalizedEvents, dateRange), [normalizedEvents, dateRange]) 78 + const normalizedEvents = useMemo( 79 + () => mapEventsToAnalyticsEvents(events), 80 + [events], 81 + ) 82 + const rangeEvents = useMemo( 83 + () => filterEventsInRange(normalizedEvents, dateRange), 84 + [normalizedEvents, dateRange], 85 + ) 69 86 const futureRangeEvents = useMemo( 70 87 () => filterEventsInRange(normalizedEvents, futureDateRange), 71 88 [futureDateRange, normalizedEvents], ··· 88 105 return categoryMeta.get(categoryId)?.name ?? categoryId 89 106 } 90 107 91 - const resolveCategoryColor = (categoryId: string, fallbackColor: string): string => { 108 + const resolveCategoryColor = ( 109 + categoryId: string, 110 + fallbackColor: string, 111 + ): string => { 92 112 if (categoryId === 'uncategorized') return '#64748b' 93 113 return categoryMeta.get(categoryId)?.color ?? fallbackColor 94 114 } ··· 107 127 return color 108 128 } 109 129 110 - 111 130 const countChart = useMemo(() => { 112 - const seriesMeta = new Map<string, { label: string; color: string; totalCount: number }>() 131 + const seriesMeta = new Map< 132 + string, 133 + { label: string; color: string; totalCount: number } 134 + >() 113 135 const dailyBuckets = new Map<string, Record<string, number>>() 114 136 const monthlyBuckets = new Map<string, Record<string, number>>() 115 137 ··· 138 160 139 161 const series = Array.from(seriesMeta.entries()) 140 162 .sort((a, b) => { 141 - const colorOrder = getChartColorOrderIndex(a[1].color) - getChartColorOrderIndex(b[1].color) 163 + const colorOrder = 164 + getChartColorOrderIndex(a[1].color) - 165 + getChartColorOrderIndex(b[1].color) 142 166 if (colorOrder !== 0) return colorOrder 143 167 return b[1].totalCount - a[1].totalCount 144 168 }) ··· 201 225 }, [categoryMeta, rangeEvents, resolveCategoryLabel]) 202 226 203 227 const categoryAvgDurationData = useMemo(() => { 204 - const categoryDuration = new Map<string, { total: number; count: number; color: string }>() 228 + const categoryDuration = new Map< 229 + string, 230 + { total: number; count: number; color: string } 231 + >() 205 232 rangeEvents.forEach((event) => { 206 233 const duration = calculateDaySpanInHours(event.start, event.end) 207 234 const label = resolveCategoryLabel(event.category) ··· 223 250 }, [categoryMeta, rangeEvents, resolveCategoryLabel]) 224 251 225 252 const weekdayStacked = useMemo(() => { 226 - const buckets: Record<string, Record<string, { hours: number; color: string }>> = {} 253 + const buckets: Record< 254 + string, 255 + Record<string, { hours: number; color: string }> 256 + > = {} 227 257 228 258 rangeEvents.forEach((event) => { 229 259 const label = dayName(event.start) ··· 252 282 }) 253 283 254 284 const series = Array.from(categoryColors.entries()) 255 - .sort((a, b) => getChartColorOrderIndex(a[1]) - getChartColorOrderIndex(b[1])) 285 + .sort( 286 + (a, b) => getChartColorOrderIndex(a[1]) - getChartColorOrderIndex(b[1]), 287 + ) 256 288 .map(([key, color]) => ({ 257 289 key, 258 290 color, ··· 263 295 264 296 const metrics = useMemo(() => { 265 297 if (rangeEvents.length === 0) { 266 - const futureLeadTimes = futureRangeEvents.map( 267 - (event) => Math.max((event.start.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), 0), 298 + const futureLeadTimes = futureRangeEvents.map((event) => 299 + Math.max( 300 + (event.start.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), 301 + 0, 302 + ), 268 303 ) 269 304 const futureAvgLead = 270 305 futureLeadTimes.length === 0 271 306 ? 0 272 - : futureLeadTimes.reduce((sum, value) => sum + value, 0) / futureLeadTimes.length 307 + : futureLeadTimes.reduce((sum, value) => sum + value, 0) / 308 + futureLeadTimes.length 273 309 return [ 274 - { title: t.analyticsMetricLongestStreak, value: `0 ${t.analyticsDayUnit}`, subtitle: t.analyticsNoScheduleInRange }, 275 - { title: t.analyticsMetricBusiestWeekday, value: t.analyticsNone, subtitle: t.analyticsNoScheduleInRange }, 276 - { title: t.analyticsMetricAvgLeadDays, value: `${futureAvgLead.toFixed(1)} ${t.analyticsDayUnit}`, subtitle: t.analyticsLeadDaysHint }, 277 - { title: t.analyticsMetricPeakTimeWindow, value: t.analyticsNone, subtitle: t.analyticsNoScheduleInRange }, 310 + { 311 + title: t.analyticsMetricLongestStreak, 312 + value: `0 ${t.analyticsDayUnit}`, 313 + subtitle: t.analyticsNoScheduleInRange, 314 + }, 315 + { 316 + title: t.analyticsMetricBusiestWeekday, 317 + value: t.analyticsNone, 318 + subtitle: t.analyticsNoScheduleInRange, 319 + }, 320 + { 321 + title: t.analyticsMetricAvgLeadDays, 322 + value: `${futureAvgLead.toFixed(1)} ${t.analyticsDayUnit}`, 323 + subtitle: t.analyticsLeadDaysHint, 324 + }, 325 + { 326 + title: t.analyticsMetricPeakTimeWindow, 327 + value: t.analyticsNone, 328 + subtitle: t.analyticsNoScheduleInRange, 329 + }, 278 330 ] 279 331 } 280 332 281 333 const days = generateRangeDays(dateRange) 282 - const activeSet = new Set(rangeEvents.map((event) => groupDayKey(event.start))) 334 + const activeSet = new Set( 335 + rangeEvents.map((event) => groupDayKey(event.start)), 336 + ) 283 337 284 338 let bestStreak = 0 285 339 let currentStreak = 0 ··· 308 362 weekdayMap.set(label, (weekdayMap.get(label) ?? 0) + 1) 309 363 }) 310 364 311 - const totalWeeks = Math.max((differenceInCalendarDays(dateRange.end, dateRange.start) + 1) / 7, 1) 365 + const totalWeeks = Math.max( 366 + (differenceInCalendarDays(dateRange.end, dateRange.start) + 1) / 7, 367 + 1, 368 + ) 312 369 const weekdayAverages = weekdayLabels.map((label) => ({ 313 370 day: label, 314 371 avg: (weekdayMap.get(label) ?? 0) / totalWeeks, 315 372 })) 316 373 317 - const busiestDay = weekdayAverages.reduce((max, current) => (current.avg > max.avg ? current : max), weekdayAverages[0]) 374 + const busiestDay = weekdayAverages.reduce( 375 + (max, current) => (current.avg > max.avg ? current : max), 376 + weekdayAverages[0], 377 + ) 318 378 const overallAvg = rangeEvents.length / 7 319 - const uplift = overallAvg === 0 ? 0 : ((busiestDay.avg - overallAvg) / overallAvg) * 100 379 + const uplift = 380 + overallAvg === 0 ? 0 : ((busiestDay.avg - overallAvg) / overallAvg) * 100 320 381 321 - const leadTimes = futureRangeEvents.map( 322 - (event) => Math.max((event.start.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), 0), 382 + const leadTimes = futureRangeEvents.map((event) => 383 + Math.max( 384 + (event.start.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), 385 + 0, 386 + ), 323 387 ) 324 388 const avgLead = 325 - leadTimes.length === 0 ? 0 : leadTimes.reduce((sum, value) => sum + value, 0) / leadTimes.length 389 + leadTimes.length === 0 390 + ? 0 391 + : leadTimes.reduce((sum, value) => sum + value, 0) / leadTimes.length 326 392 327 393 const hourCounts = Array.from({ length: 24 }).map(() => 0) 328 394 rangeEvents.forEach((event) => { ··· 339 405 } 340 406 } 341 407 342 - const concentrationRatio = rangeEvents.length === 0 ? 0 : (bestWindowCount / rangeEvents.length) * 100 408 + const concentrationRatio = 409 + rangeEvents.length === 0 410 + ? 0 411 + : (bestWindowCount / rangeEvents.length) * 100 343 412 344 413 return [ 345 414 { ··· 363 432 { 364 433 title: t.analyticsMetricPeakTimeWindow, 365 434 value: formatHourRange(bestWindowHour), 366 - subtitle: t.analyticsPeakWindowSubtitle.replace('{pct}', concentrationRatio.toFixed(1)), 435 + subtitle: t.analyticsPeakWindowSubtitle.replace( 436 + '{pct}', 437 + concentrationRatio.toFixed(1), 438 + ), 367 439 }, 368 440 ] 369 441 }, [dateRange, futureRangeEvents, now, rangeEvents, t, weekdayLabels]) ··· 372 444 <div className="space-y-6 rounded-lg border p-4"> 373 445 <div className="flex items-center justify-between"> 374 446 <h2 className="text-base font-semibold">{t.analyticsOverviewTitle}</h2> 375 - <Select value={preset} onValueChange={(value) => setPreset(value as AnalyticsRangePreset)}> 447 + <Select 448 + value={preset} 449 + onValueChange={(value) => setPreset(value as AnalyticsRangePreset)} 450 + > 376 451 <SelectTrigger className="w-[160px]"> 377 452 <SelectValue /> 378 453 </SelectTrigger> ··· 399 474 400 475 <div className="grid grid-cols-1 gap-4 xl:grid-cols-2"> 401 476 <CategoryAverageDurationChart data={categoryAvgDurationData} /> 402 - <WeekdayStackedDurationChart data={weekdayStacked.data} series={weekdayStacked.series} /> 477 + <WeekdayStackedDurationChart 478 + data={weekdayStacked.data} 479 + series={weekdayStacked.series} 480 + /> 403 481 </div> 404 482 405 483 <YearHeatmapChart data={heatmapData} />
-1
components/app/calendar.tsx
··· 47 47 import Sidebar from '@/components/app/sidebar/sidebar' 48 48 import { translations, useLanguage } from '@/lib/i18n' 49 49 import { Button } from '@/components/ui/button' 50 - import { cn } from '@/lib/utils' 51 50 import { APP_CONFIG } from '@/lib/config' 52 51 import { toast } from 'sonner' 53 52 import {
-1
components/app/event/event-preview.tsx
··· 124 124 } 125 125 }, [open, modal]) 126 126 127 - 128 127 useEffect(() => { 129 128 let active = true 130 129 const loadBookmarks = () =>
-1
components/app/profile/shared-event.tsx
··· 15 15 Loader2, 16 16 Clock, 17 17 CalendarPlus, 18 - ExternalLink, 19 18 Copy, 20 19 AlertCircle, 21 20 Home,
+1 -1
components/app/profile/user-profile-button.tsx
··· 1 1 'use client' 2 2 3 - import { useEffect, useMemo, useRef, useState } from 'react' 3 + import { useEffect, useRef, useState } from 'react' 4 4 import { 5 5 User, 6 6 LogOut,
+1 -4
components/app/sidebar/right-sidebar.tsx
··· 1 - import { 2 - Calendar, 3 - Bookmark, 4 - } from 'lucide-react' 1 + import { Calendar, Bookmark } from 'lucide-react' 5 2 import { ClockDashed } from '@/components/icons/clock-dashed' 6 3 import MiniCalendarSheet from './mini-calendar-sheet' 7 4 import { Button } from '@/components/ui/button'
+15 -4
components/app/sidebar/sidebar.tsx
··· 283 283 <span className="text-sm font-medium">{t.myCalendars}</span> 284 284 </div> 285 285 {calendars.map((calendar) => ( 286 - <div key={calendar.id} className="flex items-center justify-between"> 286 + <div 287 + key={calendar.id} 288 + className="flex items-center justify-between" 289 + > 287 290 <div className="flex items-center space-x-2"> 288 291 <Checkbox 289 292 checked={selectedCategoryFilters.includes(calendar.id)} ··· 306 309 </Button> 307 310 </DropdownMenuTrigger> 308 311 <DropdownMenuContent align="end"> 309 - <DropdownMenuItem onClick={() => handleEditClick(calendar.id)}> 312 + <DropdownMenuItem 313 + onClick={() => handleEditClick(calendar.id)} 314 + > 310 315 <Edit2 className="mr-2 h-4 w-4" /> 311 316 {t.edit} 312 317 </DropdownMenuItem> ··· 319 324 </DropdownMenuItem> 320 325 <DropdownMenuItem 321 326 onClick={() => handleMoveCategory(calendar.id, 'up')} 322 - disabled={calendars.findIndex((item) => item.id === calendar.id) === 0} 327 + disabled={ 328 + calendars.findIndex( 329 + (item) => item.id === calendar.id, 330 + ) === 0 331 + } 323 332 > 324 333 <ArrowUp className="mr-2 h-4 w-4" /> 325 334 {moveUpText} ··· 327 336 <DropdownMenuItem 328 337 onClick={() => handleMoveCategory(calendar.id, 'down')} 329 338 disabled={ 330 - calendars.findIndex((item) => item.id === calendar.id) === 339 + calendars.findIndex( 340 + (item) => item.id === calendar.id, 341 + ) === 331 342 calendars.length - 1 332 343 } 333 344 >
+1 -8
components/app/views/day-view.tsx
··· 3 3 import { useEffect, useRef, useState } from 'react' 4 4 import type React from 'react' 5 5 import { Edit3, Share2, Bookmark, Trash2 } from 'lucide-react' 6 - import { 7 - format, 8 - isSameDay, 9 - isWithinInterval, 10 - endOfDay, 11 - startOfDay, 12 - add, 13 - } from 'date-fns' 6 + import { format, isSameDay, isWithinInterval, add } from 'date-fns' 14 7 import { cn } from '@/lib/utils' 15 8 import type { CalendarEvent } from '../calendar' 16 9 import { translations, type Language } from '@/lib/i18n'
+75 -20
components/auth/login-form.tsx
··· 5 5 import { useState } from 'react' 6 6 7 7 import { Button } from '@/components/ui/button' 8 - import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' 8 + import { 9 + Card, 10 + CardContent, 11 + CardDescription, 12 + CardHeader, 13 + CardTitle, 14 + } from '@/components/ui/card' 9 15 import { Input } from '@/components/ui/input' 10 16 import { InputOTP } from '@/components/ui/input-otp' 11 17 import { Label } from '@/components/ui/label' 12 18 import { authClient } from '@/lib/auth-client' 13 19 import { cn } from '@/lib/utils' 14 20 15 - export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { 21 + export function LoginForm({ 22 + className, 23 + ...props 24 + }: React.ComponentPropsWithoutRef<'div'>) { 16 25 const [email, setEmail] = useState('') 17 26 const [password, setPassword] = useState('') 18 27 const [totp, setTotp] = useState('') ··· 20 29 const [isLoading, setIsLoading] = useState(false) 21 30 const [isVerifyingTotp, setIsVerifyingTotp] = useState(false) 22 31 const [error, setError] = useState('') 23 - const [isCaptchaCompleted, setIsCaptchaCompleted] = useState(process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true) 32 + const [isCaptchaCompleted, setIsCaptchaCompleted] = useState( 33 + process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true, 34 + ) 24 35 const router = useRouter() 25 36 26 37 const handleEmailLogin = async (e: React.FormEvent) => { 27 38 e.preventDefault() 28 - if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) return setError('Please complete the CAPTCHA verification.') 39 + if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) 40 + return setError('Please complete the CAPTCHA verification.') 29 41 setIsLoading(true) 30 42 setError('') 31 43 32 44 const res = await authClient.signIn.email({ email, password }) 33 45 if (res.error) { 34 46 const message = res.error.message || '' 35 - if (message.toLowerCase().includes('two-factor') || message.toLowerCase().includes('totp')) { 47 + if ( 48 + message.toLowerCase().includes('two-factor') || 49 + message.toLowerCase().includes('totp') 50 + ) { 36 51 setNeedsTwoFactor(true) 37 52 } else { 38 53 setError(message || 'Login failed. Please try again.') ··· 41 56 return 42 57 } 43 58 44 - if ((res as any).data?.twoFactorRedirect || (res as any).data?.requiresTwoFactor) { 59 + if ( 60 + (res as any).data?.twoFactorRedirect || 61 + (res as any).data?.requiresTwoFactor 62 + ) { 45 63 setNeedsTwoFactor(true) 46 64 setIsLoading(false) 47 65 return ··· 55 73 if (totp.length < 6) return 56 74 setIsVerifyingTotp(true) 57 75 setError('') 58 - const res = await authClient.twoFactor.verifyTotp({ code: totp, trustDevice: true }) 76 + const res = await authClient.twoFactor.verifyTotp({ 77 + code: totp, 78 + trustDevice: true, 79 + }) 59 80 if (res.error) { 60 81 setError(res.error.message || 'Invalid verification code.') 61 82 setIsVerifyingTotp(false) ··· 70 91 <CardHeader className="text-center"> 71 92 <CardTitle className="text-xl">Welcome back</CardTitle> 72 93 <CardDescription> 73 - {needsTwoFactor ? 'Enter your authenticator app code' : 'Login with your email and password'} 94 + {needsTwoFactor 95 + ? 'Enter your authenticator app code' 96 + : 'Login with your email and password'} 74 97 </CardDescription> 75 98 </CardHeader> 76 99 <CardContent> ··· 78 101 <div className="grid gap-6"> 79 102 <div className="grid gap-2"> 80 103 <Label>Two-factor code</Label> 81 - <InputOTP value={totp} onChange={(value) => setTotp(value.replace(/\D/g, '').slice(0, 6))} maxLength={6} /> 104 + <InputOTP 105 + value={totp} 106 + onChange={(value) => 107 + setTotp(value.replace(/\D/g, '').slice(0, 6)) 108 + } 109 + maxLength={6} 110 + /> 82 111 </div> 83 112 {error && <div className="text-sm text-red-500">{error}</div>} 84 - <Button className="w-full bg-[#0066ff] text-white hover:bg-[#0047cc]" onClick={handleVerifyTotp} disabled={isVerifyingTotp || totp.length < 6}> 113 + <Button 114 + className="w-full bg-[#0066ff] text-white hover:bg-[#0047cc]" 115 + onClick={handleVerifyTotp} 116 + disabled={isVerifyingTotp || totp.length < 6} 117 + > 85 118 {isVerifyingTotp ? 'Verifying...' : 'Verify and sign in'} 86 119 </Button> 87 120 </div> ··· 90 123 <div className="grid gap-6"> 91 124 <div className="grid gap-2"> 92 125 <Label htmlFor="email">Email</Label> 93 - <Input id="email" type="email" placeholder="m@example.com" required value={email} onChange={(e) => setEmail(e.target.value)} /> 126 + <Input 127 + id="email" 128 + type="email" 129 + placeholder="m@example.com" 130 + required 131 + value={email} 132 + onChange={(e) => setEmail(e.target.value)} 133 + /> 94 134 </div> 95 135 <div className="grid gap-2"> 96 136 <div className="flex items-center"> 97 137 <Label htmlFor="password">Password</Label> 98 - <a href="/reset-password" className="ml-auto text-sm underline-offset-4 hover:underline"> 138 + <a 139 + href="/reset-password" 140 + className="ml-auto text-sm underline-offset-4 hover:underline" 141 + > 99 142 Forgot your password? 100 143 </a> 101 144 </div> 102 - <Input id="password" type="password" required value={password} onChange={(e) => setPassword(e.target.value)} /> 145 + <Input 146 + id="password" 147 + type="password" 148 + required 149 + value={password} 150 + onChange={(e) => setPassword(e.target.value)} 151 + /> 103 152 </div> 104 153 {process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && ( 105 154 <Turnstile ··· 109 158 onExpire={() => setIsCaptchaCompleted(false)} 110 159 onError={() => { 111 160 setIsCaptchaCompleted(false) 112 - setError('CAPTCHA initialization failed. Please try again.') 161 + setError( 162 + 'CAPTCHA initialization failed. Please try again.', 163 + ) 113 164 }} 114 165 /> 115 166 )} 116 167 {error && <div className="text-sm text-red-500">{error}</div>} 117 - <Button type="submit" className="w-full bg-[#0066ff] text-white hover:bg-[#0047cc]" disabled={isLoading || !isCaptchaCompleted}> 168 + <Button 169 + type="submit" 170 + className="w-full bg-[#0066ff] text-white hover:bg-[#0047cc]" 171 + disabled={isLoading || !isCaptchaCompleted} 172 + > 118 173 {isLoading ? 'Signing in...' : 'Sign in'} 119 174 </Button> 120 175 <div className="text-center text-sm"> ··· 129 184 </CardContent> 130 185 </Card> 131 186 132 - <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 133 - By clicking continue, you agree to our{' '} 134 - <a href="/terms">Terms of Service</a> and{' '} 135 - <a href="/privacy">Privacy Policy</a>. 136 - </div> 187 + <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 188 + By clicking continue, you agree to our{' '} 189 + <a href="/terms">Terms of Service</a> and{' '} 190 + <a href="/privacy">Privacy Policy</a>. 191 + </div> 137 192 </div> 138 193 ) 139 194 }
+75 -22
components/auth/reset-form.tsx
··· 6 6 import type React from 'react' 7 7 8 8 import { Button } from '@/components/ui/button' 9 - import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' 9 + import { 10 + Card, 11 + CardContent, 12 + CardDescription, 13 + CardHeader, 14 + CardTitle, 15 + } from '@/components/ui/card' 10 16 import { Input } from '@/components/ui/input' 11 17 import { Label } from '@/components/ui/label' 12 18 import { authClient } from '@/lib/auth-client' 13 19 import { cn } from '@/lib/utils' 14 20 15 - export function ResetPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { 21 + export function ResetPasswordForm({ 22 + className, 23 + ...props 24 + }: React.ComponentPropsWithoutRef<'div'>) { 16 25 const searchParams = useSearchParams() 17 26 const token = searchParams.get('token') 18 27 const [email, setEmail] = useState('') ··· 22 31 const [error, setError] = useState('') 23 32 const [done, setDone] = useState(false) 24 33 const [notice, setNotice] = useState('') 25 - const [isCaptchaCompleted, setIsCaptchaCompleted] = useState(process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true) 34 + const [isCaptchaCompleted, setIsCaptchaCompleted] = useState( 35 + process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true, 36 + ) 26 37 27 38 const handleSubmit = async (e: React.FormEvent) => { 28 39 e.preventDefault() 29 - if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) return setError('Please complete the CAPTCHA verification.') 40 + if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) 41 + return setError('Please complete the CAPTCHA verification.') 30 42 setIsLoading(true) 31 43 setError('') 32 44 setNotice('') 33 - const res = await authClient.forgetPassword({ email, redirectTo: '/reset-password' } as never) 45 + const res = await authClient.forgetPassword({ 46 + email, 47 + redirectTo: '/reset-password', 48 + } as never) 34 49 if (!res.error) { 35 50 setDone(true) 36 51 setNotice('Reset email sent. Please check your inbox.') 37 52 setIsLoading(false) 38 53 return 39 54 } 40 - const fallbackBody = JSON.stringify({ email, redirectTo: '/reset-password' }) 41 - const fallbackEndpoints = ['/api/auth/forget-password', '/api/auth/forgot-password', '/api/auth/request-password-reset'] 55 + const fallbackBody = JSON.stringify({ 56 + email, 57 + redirectTo: '/reset-password', 58 + }) 59 + const fallbackEndpoints = [ 60 + '/api/auth/forget-password', 61 + '/api/auth/forgot-password', 62 + '/api/auth/request-password-reset', 63 + ] 42 64 let fallbackSucceeded = false 43 65 for (const endpoint of fallbackEndpoints) { 44 66 try { ··· 53 75 } 54 76 } catch {} 55 77 } 56 - if (fallbackSucceeded) { setDone(true); setNotice('Reset email sent. Please check your inbox.') } 57 - else setError(res.error.message || 'An error occurred. Please try again.') 78 + if (fallbackSucceeded) { 79 + setDone(true) 80 + setNotice('Reset email sent. Please check your inbox.') 81 + } else setError(res.error.message || 'An error occurred. Please try again.') 58 82 setIsLoading(false) 59 83 } 60 84 61 85 const handleResetPassword = async (e: React.FormEvent) => { 62 86 e.preventDefault() 63 - if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) return setError('Please complete the CAPTCHA verification.') 87 + if (process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && !isCaptchaCompleted) 88 + return setError('Please complete the CAPTCHA verification.') 64 89 if (password !== confirmPassword) return setError('Passwords do not match.') 65 90 setIsLoading(true) 66 91 setError('') 67 - const { error } = await authClient.resetPassword({ newPassword: password, token } as never) 68 - if (error) setError(error.message || 'Failed to reset password. Please try again.') 92 + const { error } = await authClient.resetPassword({ 93 + newPassword: password, 94 + token, 95 + } as never) 96 + if (error) 97 + setError(error.message || 'Failed to reset password. Please try again.') 69 98 else setDone(true) 70 99 setIsLoading(false) 71 100 } ··· 94 123 <> 95 124 <div className="grid gap-2"> 96 125 <Label htmlFor="password">New password</Label> 97 - <Input id="password" type="password" required value={password} onChange={(e) => setPassword(e.target.value)} /> 126 + <Input 127 + id="password" 128 + type="password" 129 + required 130 + value={password} 131 + onChange={(e) => setPassword(e.target.value)} 132 + /> 98 133 </div> 99 134 <div className="grid gap-2"> 100 135 <Label htmlFor="confirmPassword">Confirm password</Label> ··· 110 145 ) : ( 111 146 <div className="grid gap-2"> 112 147 <Label htmlFor="email">Email</Label> 113 - <Input id="email" type="email" required value={email} onChange={(e) => setEmail(e.target.value)} /> 148 + <Input 149 + id="email" 150 + type="email" 151 + required 152 + value={email} 153 + onChange={(e) => setEmail(e.target.value)} 154 + /> 114 155 </div> 115 156 )} 116 157 {process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && ( ··· 125 166 }} 126 167 /> 127 168 )} 128 - {!isTokenFlow && notice ? <div className="text-sm text-emerald-600">{notice}</div> : null} 169 + {!isTokenFlow && notice ? ( 170 + <div className="text-sm text-emerald-600">{notice}</div> 171 + ) : null} 129 172 {error && <div className="text-sm text-red-500">{error}</div>} 130 - <Button type="submit" className="w-full bg-[#0066ff] text-white hover:bg-[#0047cc]" disabled={isLoading || !isCaptchaCompleted}> 131 - {isLoading ? (isTokenFlow ? 'Updating...' : 'Sending...') : isTokenFlow ? 'Update password' : 'Send reset email'} 173 + <Button 174 + type="submit" 175 + className="w-full bg-[#0066ff] text-white hover:bg-[#0047cc]" 176 + disabled={isLoading || !isCaptchaCompleted} 177 + > 178 + {isLoading 179 + ? isTokenFlow 180 + ? 'Updating...' 181 + : 'Sending...' 182 + : isTokenFlow 183 + ? 'Update password' 184 + : 'Send reset email'} 132 185 </Button> 133 186 </div> 134 187 </form> 135 188 </CardContent> 136 189 </Card> 137 190 138 - <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 139 - By clicking continue, you agree to our{' '} 140 - <a href="/terms">Terms of Service</a> and{' '} 141 - <a href="/privacy">Privacy Policy</a>. 142 - </div> 191 + <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 192 + By clicking continue, you agree to our{' '} 193 + <a href="/terms">Terms of Service</a> and{' '} 194 + <a href="/privacy">Privacy Policy</a>. 195 + </div> 143 196 </div> 144 197 ) 145 198 }
+72 -21
components/auth/sign-up-form.tsx
··· 5 5 import { useState } from 'react' 6 6 7 7 import { Button } from '@/components/ui/button' 8 - import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' 8 + import { 9 + Card, 10 + CardContent, 11 + CardDescription, 12 + CardHeader, 13 + CardTitle, 14 + } from '@/components/ui/card' 9 15 import { Input } from '@/components/ui/input' 10 16 import { InputOTP } from '@/components/ui/input-otp' 11 17 import { Label } from '@/components/ui/label' 12 18 import { authClient } from '@/lib/auth-client' 13 19 import { cn } from '@/lib/utils' 14 20 15 - export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) { 21 + export function SignUpForm({ 22 + className, 23 + ...props 24 + }: React.ComponentPropsWithoutRef<'div'>) { 16 25 const router = useRouter() 17 - const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', password: '' }) 26 + const [formData, setFormData] = useState({ 27 + firstName: '', 28 + lastName: '', 29 + email: '', 30 + password: '', 31 + }) 18 32 const [otp, setOtp] = useState('') 19 33 const [isLoading, setIsLoading] = useState(false) 20 34 const [isVerifying, setIsVerifying] = useState(false) 21 35 const [isResending, setIsResending] = useState(false) 22 36 const [error, setError] = useState('') 23 37 const [sent, setSent] = useState(false) 24 - const [isCaptchaCompleted, setIsCaptchaCompleted] = useState(process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true) 38 + const [isCaptchaCompleted, setIsCaptchaCompleted] = useState( 39 + process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ? false : true, 40 + ) 25 41 26 42 const sendVerificationOtp = async (withResendLoading: boolean) => { 27 43 if (withResendLoading) setIsResending(true) ··· 55 71 }) 56 72 57 73 if (signUpRes.error) { 58 - setError(signUpRes.error.message || 'An error occurred. Please try again.') 74 + setError( 75 + signUpRes.error.message || 'An error occurred. Please try again.', 76 + ) 59 77 setIsLoading(false) 60 78 return 61 79 } ··· 69 87 if (!otp.trim()) return 70 88 setIsVerifying(true) 71 89 setError('') 72 - const verifyRes = await authClient.emailOtp.verifyEmail({ email: formData.email, otp: otp.trim() }) 90 + const verifyRes = await authClient.emailOtp.verifyEmail({ 91 + email: formData.email, 92 + otp: otp.trim(), 93 + }) 73 94 if (verifyRes.error) { 74 - setError(verifyRes.error.message || 'Invalid verification code. Please try again.') 95 + setError( 96 + verifyRes.error.message || 97 + 'Invalid verification code. Please try again.', 98 + ) 75 99 setIsVerifying(false) 76 100 return 77 101 } ··· 91 115 <CardHeader className="text-center"> 92 116 <CardTitle className="text-xl">Create your account</CardTitle> 93 117 <CardDescription> 94 - {sent ? `Verification code sent to ${formData.email}` : 'Sign up with your email and password'} 118 + {sent 119 + ? `Verification code sent to ${formData.email}` 120 + : 'Sign up with your email and password'} 95 121 </CardDescription> 96 122 </CardHeader> 97 123 <CardContent> ··· 101 127 <div className="text-sm font-medium">Step 2 of 2</div> 102 128 <div className="mt-2 text-sm text-muted-foreground"> 103 129 A verification code has been sent to{' '} 104 - <span className="font-medium text-foreground">{formData.email}</span>. Enter the code below to activate your account. 130 + <span className="font-medium text-foreground"> 131 + {formData.email} 132 + </span> 133 + . Enter the code below to activate your account. 105 134 </div> 106 135 </div> 107 136 <div className="grid gap-2"> 108 137 <Label htmlFor="otp">Verification code</Label> 109 - <InputOTP value={otp} onChange={(value) => setOtp(value.replace(/\D/g, '').slice(0, 6))} maxLength={6} /> 138 + <InputOTP 139 + value={otp} 140 + onChange={(value) => 141 + setOtp(value.replace(/\D/g, '').slice(0, 6)) 142 + } 143 + maxLength={6} 144 + /> 110 145 </div> 111 146 {error && <div className="text-sm text-red-500">{error}</div>} 112 147 <Button ··· 117 152 > 118 153 {isVerifying ? 'Verifying...' : 'Verify code'} 119 154 </Button> 120 - <Button type="button" variant="outline" className="w-full" onClick={handleResend} disabled={isResending || isVerifying}> 155 + <Button 156 + type="button" 157 + variant="outline" 158 + className="w-full" 159 + onClick={handleResend} 160 + disabled={isResending || isVerifying} 161 + > 121 162 {isResending ? 'Resending...' : 'Resend code'} 122 163 </Button> 123 164 </div> ··· 132 173 name="firstName" 133 174 required 134 175 value={formData.firstName} 135 - onChange={(e) => setFormData({ ...formData, firstName: e.target.value })} 176 + onChange={(e) => 177 + setFormData({ ...formData, firstName: e.target.value }) 178 + } 136 179 /> 137 180 </div> 138 181 <div className="grid gap-2"> ··· 142 185 name="lastName" 143 186 required 144 187 value={formData.lastName} 145 - onChange={(e) => setFormData({ ...formData, lastName: e.target.value })} 188 + onChange={(e) => 189 + setFormData({ ...formData, lastName: e.target.value }) 190 + } 146 191 /> 147 192 </div> 148 193 </div> ··· 153 198 type="email" 154 199 required 155 200 value={formData.email} 156 - onChange={(e) => setFormData({ ...formData, email: e.target.value })} 201 + onChange={(e) => 202 + setFormData({ ...formData, email: e.target.value }) 203 + } 157 204 /> 158 205 </div> 159 206 <div className="grid gap-2"> ··· 163 210 type="password" 164 211 required 165 212 value={formData.password} 166 - onChange={(e) => setFormData({ ...formData, password: e.target.value })} 213 + onChange={(e) => 214 + setFormData({ ...formData, password: e.target.value }) 215 + } 167 216 /> 168 217 </div> 169 218 {process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && ( ··· 174 223 onExpire={() => setIsCaptchaCompleted(false)} 175 224 onError={() => { 176 225 setIsCaptchaCompleted(false) 177 - setError('CAPTCHA initialization failed. Please try again.') 226 + setError( 227 + 'CAPTCHA initialization failed. Please try again.', 228 + ) 178 229 }} 179 230 /> 180 231 )} ··· 198 249 </CardContent> 199 250 </Card> 200 251 201 - <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 202 - By clicking continue, you agree to our{' '} 203 - <a href="/terms">Terms of Service</a> and{' '} 204 - <a href="/privacy">Privacy Policy</a>. 205 - </div> 252 + <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> 253 + By clicking continue, you agree to our{' '} 254 + <a href="/terms">Terms of Service</a> and{' '} 255 + <a href="/privacy">Privacy Policy</a>. 256 + </div> 206 257 </div> 207 258 ) 208 259 }
+5 -5
components/providers/calendar-context.tsx
··· 83 83 const currentIndex = state.calendars.findIndex((cal) => cal.id === id) 84 84 if (currentIndex === -1) return { calendars: state.calendars } 85 85 86 - const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1 86 + const targetIndex = 87 + direction === 'up' ? currentIndex - 1 : currentIndex + 1 87 88 88 89 if (targetIndex < 0 || targetIndex >= state.calendars.length) { 89 90 return { calendars: state.calendars } ··· 120 121 121 122 useEffect(() => { 122 123 const hydrate = async () => { 123 - const storedCalendars = await readEncryptedLocalStorage<CalendarCategory[]>( 124 - 'calendar-categories', 125 - [], 126 - ) 124 + const storedCalendars = await readEncryptedLocalStorage< 125 + CalendarCategory[] 126 + >('calendar-categories', []) 127 127 const storedEvents = await readEncryptedLocalStorage<CalendarEvent[]>( 128 128 'calendar-events', 129 129 [],
+4
eslint.config.ts
··· 1 1 import js from '@eslint/js' 2 2 import nextPlugin from '@next/eslint-plugin-next' 3 + import unusedImports from 'eslint-plugin-unused-imports' 3 4 import tseslint, { type ConfigArray } from 'typescript-eslint' 4 5 import oxlint from 'eslint-plugin-oxlint' 5 6 ··· 9 10 { 10 11 plugins: { 11 12 '@next/next': nextPlugin, 13 + 'unused-imports': unusedImports, 12 14 }, 13 15 rules: { 14 16 ...nextPlugin.configs.recommended.rules, ··· 29 31 'no-case-declarations': 'warn', 30 32 'no-console': 'warn', 31 33 'prefer-const': 'error', 34 + 'unused-imports/no-unused-imports': 'error', 35 + 'unused-imports/no-unused-vars': 'off', 32 36 }, 33 37 }, 34 38 {
+5 -1
lib/auth-client.ts
··· 6 6 7 7 export const authClient = createAuthClient({ 8 8 baseURL: process.env.NEXT_PUBLIC_APP_URL, 9 - plugins: [emailOTPClient(), twoFactorClient(), sentinelClient({ autoSolveChallenge: true })], 9 + plugins: [ 10 + emailOTPClient(), 11 + twoFactorClient(), 12 + sentinelClient({ autoSolveChallenge: true }), 13 + ], 10 14 })
+7 -6
lib/auth.ts
··· 26 26 html: payload.html, 27 27 }) 28 28 if (result.error) throw new Error(result.error.message) 29 - if (!result.data?.id) throw new Error('Email provider did not return a message id') 29 + if (!result.data?.id) 30 + throw new Error('Email provider did not return a message id') 30 31 } 31 32 32 33 export const auth = betterAuth({ ··· 48 49 body: 'We received a request to reset your password. Use the button below to continue.', 49 50 actionLabel: 'Reset password', 50 51 actionUrl: url, 51 - secondary: 'If you did not request this, you can safely ignore this email.', 52 + secondary: 53 + 'If you did not request this, you can safely ignore this email.', 52 54 }), 53 55 }) 54 56 }, ··· 85 87 sendVerificationOTP: async ({ email, otp, type }) => { 86 88 await sendAuthEmail({ 87 89 to: email, 88 - subject: type === 'forget-password' ? 'Reset code' : 'Verification code', 90 + subject: 91 + type === 'forget-password' ? 'Reset code' : 'Verification code', 89 92 html: await renderAuthEmailTemplate({ 90 93 preview: 91 94 type === 'forget-password' 92 95 ? 'Your One Calendar reset code' 93 96 : 'Your One Calendar verification code', 94 97 title: 95 - type === 'forget-password' 96 - ? 'Reset code' 97 - : 'Verification code', 98 + type === 'forget-password' ? 'Reset code' : 'Verification code', 98 99 body: `Use this code to continue: ${otp}`, 99 100 secondary: 'This code will expire shortly for your security.', 100 101 }),
+6 -1
lib/crypto.ts
··· 24 24 ['deriveKey'], 25 25 ) 26 26 return crypto.subtle.deriveKey( 27 - { name: 'PBKDF2', salt: salt as Uint8Array, iterations: 250000, hash: 'SHA-256' }, 27 + { 28 + name: 'PBKDF2', 29 + salt: salt as Uint8Array, 30 + iterations: 250000, 31 + hash: 'SHA-256', 32 + }, 28 33 k, 29 34 { name: 'AES-GCM', length: 256 }, 30 35 false,
+1 -1
lib/fetch-json.ts
··· 15 15 ...init, 16 16 headers: { 17 17 Accept: 'application/json', 18 - ...(init?.headers ?? {}), 18 + ...init?.headers, 19 19 }, 20 20 }).then(async (response) => { 21 21 if (!response.ok) {
+27 -26
package.json
··· 14 14 "generate:locales": "bun lib/gen-locales.mjs" 15 15 }, 16 16 "dependencies": { 17 + "@better-auth/infra": "latest", 18 + "@better-auth/prisma-adapter": "latest", 17 19 "@marsidev/react-turnstile": "latest", 20 + "@prisma/adapter-pg": "latest", 21 + "@prisma/client": "latest", 22 + "@prisma/extension-accelerate": "latest", 18 23 "@radix-ui/react-avatar": "^1.1.2", 19 24 "@radix-ui/react-dialog": "latest", 20 25 "@radix-ui/react-slot": "^1.2.0", 21 26 "@radix-ui/react-toast": "latest", 27 + "@react-email/components": "latest", 28 + "@react-email/render": "latest", 29 + "bcryptjs": "latest", 30 + "better-auth": "latest", 22 31 "class-variance-authority": "^0.7.1", 23 32 "clsx": "^2.1.1", 33 + "csv-parse": "latest", 24 34 "date-fns": "4.1.0", 25 35 "framer-motion": "^12.7.4", 26 36 "geist": "latest", ··· 33 43 "react": "^18", 34 44 "react-day-picker": "9.14.0", 35 45 "react-dom": "^18", 46 + "react-icons": "5.6.0", 36 47 "recharts": "3.8.1", 48 + "resend": "latest", 37 49 "sonner": "2.0.7", 38 50 "tailwind-merge": "3.5.0", 39 51 "tailwindcss-animate": "^1.0.7", 40 - "react-icons": "5.6.0", 41 - "@prisma/client": "latest", 42 - "zustand": "latest", 43 - "@prisma/adapter-pg": "latest", 44 - "@prisma/extension-accelerate": "latest", 45 - "better-auth": "latest", 46 - "@better-auth/prisma-adapter": "latest", 47 - "bcryptjs": "latest", 48 - "resend": "latest", 49 - "csv-parse": "latest", 50 - "@react-email/components": "latest", 51 - "@react-email/render": "latest", 52 - "@better-auth/infra": "latest" 52 + "zustand": "latest" 53 53 }, 54 54 "lint-staged": { 55 55 "*.{ts,tsx,js,jsx}": [ ··· 62 62 ] 63 63 }, 64 64 "devDependencies": { 65 + "@commitlint/cli": "20.5.0", 66 + "@commitlint/config-conventional": "20.5.0", 67 + "@commitlint/types": "20.5.0", 68 + "@eslint/js": "^9.0.0", 65 69 "@tailwindcss/postcss": "4.2.4", 66 70 "@types/node": "25.6.0", 67 71 "@types/react": "^18", 68 72 "@types/react-dom": "^18", 69 - "postcss": "8.5.10", 70 - "tailwindcss": "4.2.4", 71 - "typescript": "6.0.3", 73 + "eslint": "^9.39.4", 74 + "eslint-config-next": "^16.2.3", 75 + "eslint-plugin-oxlint": "^1.60.0", 76 + "eslint-plugin-unused-imports": "^4.4.1", 72 77 "husky": "9.1.7", 73 - "@commitlint/cli": "20.5.0", 74 - "@commitlint/config-conventional": "20.5.0", 75 - "@commitlint/types": "20.5.0", 76 - "eslint": "^9.0.0", 77 - "oxlint": "1.60.0", 78 - "eslint-plugin-oxlint": "1.60.0", 79 - "@eslint/js": "10.0.1", 80 - "typescript-eslint": "8.58.2", 81 - "eslint-config-next": "16.2.3", 82 78 "lint-staged": "16.4.0", 79 + "oxlint": "^1.60.0", 80 + "postcss": "8.5.10", 83 81 "prettier": "3.8.3", 84 - "prisma": "latest" 82 + "prisma": "latest", 83 + "tailwindcss": "4.2.4", 84 + "typescript": "^6.0.3", 85 + "typescript-eslint": "^8.58.2" 85 86 } 86 87 }