[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
4.9 kB
163 lines
1'use client'
2
3import type { Dispatch, SetStateAction } from 'react'
4import type React from 'react'
5import { useEffect, useRef } from 'react'
6import { create } from 'zustand'
7import {
8 readEncryptedLocalStorage,
9 writeEncryptedLocalStorage,
10} from '@/hooks/useLocalStorage'
11
12export interface CalendarCategory {
13 id: string
14 name: string
15 color: string
16 keywords?: string[]
17}
18
19export interface CalendarEvent {
20 id: string
21 title: string
22 startDate: Date
23 endDate: Date
24 isAllDay: boolean
25 recurrence: 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly'
26 location?: string
27 participants: string[]
28 notification: number
29 description?: string
30 color: string
31 calendarId: string
32}
33
34interface CalendarContextType {
35 calendars: CalendarCategory[]
36 setCalendars: Dispatch<SetStateAction<CalendarCategory[]>>
37 events: CalendarEvent[]
38 setEvents: Dispatch<SetStateAction<CalendarEvent[]>>
39 addCategory: (category: CalendarCategory) => void
40 removeCategory: (id: string) => void
41 updateCategory: (id: string, category: Partial<CalendarCategory>) => void
42 moveCategory: (id: string, direction: 'up' | 'down') => void
43 addEvent: (newEvent: CalendarEvent) => void
44}
45
46interface CalendarState {
47 calendars: CalendarCategory[]
48 events: CalendarEvent[]
49 setCalendars: (value: SetStateAction<CalendarCategory[]>) => void
50 setEvents: (value: SetStateAction<CalendarEvent[]>) => void
51 addCategory: (category: CalendarCategory) => void
52 removeCategory: (id: string) => void
53 updateCategory: (id: string, category: Partial<CalendarCategory>) => void
54 moveCategory: (id: string, direction: 'up' | 'down') => void
55 addEvent: (newEvent: CalendarEvent) => void
56}
57
58const useCalendarStore = create<CalendarState>()((set) => ({
59 calendars: [],
60 events: [],
61 setCalendars: (value) =>
62 set((state) => ({
63 calendars: typeof value === 'function' ? value(state.calendars) : value,
64 })),
65 setEvents: (value) =>
66 set((state) => ({
67 events: typeof value === 'function' ? value(state.events) : value,
68 })),
69 addCategory: (category) =>
70 set((state) => ({ calendars: [...state.calendars, category] })),
71 removeCategory: (id) =>
72 set((state) => ({
73 calendars: state.calendars.filter((cal) => cal.id !== id),
74 })),
75 updateCategory: (id, category) =>
76 set((state) => ({
77 calendars: state.calendars.map((cal) =>
78 cal.id === id ? { ...cal, ...category } : cal,
79 ),
80 })),
81 moveCategory: (id, direction) =>
82 set((state) => {
83 const currentIndex = state.calendars.findIndex((cal) => cal.id === id)
84 if (currentIndex === -1) return { calendars: state.calendars }
85
86 const targetIndex =
87 direction === 'up' ? currentIndex - 1 : currentIndex + 1
88
89 if (targetIndex < 0 || targetIndex >= state.calendars.length) {
90 return { calendars: state.calendars }
91 }
92
93 const nextCalendars = [...state.calendars]
94 const [movedCalendar] = nextCalendars.splice(currentIndex, 1)
95 nextCalendars.splice(targetIndex, 0, movedCalendar)
96
97 return { calendars: nextCalendars }
98 }),
99 addEvent: (newEvent) =>
100 set((state) => {
101 const eventExists = state.events.some((event) => event.id === newEvent.id)
102
103 if (eventExists) {
104 return {
105 events: state.events.map((event) =>
106 event.id === newEvent.id ? newEvent : event,
107 ),
108 }
109 }
110
111 return { events: [...state.events, newEvent] }
112 }),
113}))
114
115export function CalendarProvider({ children }: { children: React.ReactNode }) {
116 const calendars = useCalendarStore((state) => state.calendars)
117 const events = useCalendarStore((state) => state.events)
118 const setCalendars = useCalendarStore((state) => state.setCalendars)
119 const setEvents = useCalendarStore((state) => state.setEvents)
120 const hydratedRef = useRef(false)
121
122 useEffect(() => {
123 const hydrate = async () => {
124 const storedCalendars = await readEncryptedLocalStorage<
125 CalendarCategory[]
126 >('calendar-categories', [])
127 const storedEvents = await readEncryptedLocalStorage<CalendarEvent[]>(
128 'calendar-events',
129 [],
130 )
131
132 setCalendars(storedCalendars)
133 setEvents(
134 storedEvents.map((event) => ({
135 ...event,
136 startDate: new Date(event.startDate),
137 endDate: new Date(event.endDate),
138 })),
139 )
140 hydratedRef.current = true
141 }
142
143 void hydrate()
144 }, [setCalendars, setEvents])
145
146 useEffect(() => {
147 if (!hydratedRef.current) return
148 void writeEncryptedLocalStorage('calendar-categories', calendars)
149 }, [calendars])
150
151 useEffect(() => {
152 if (!hydratedRef.current) return
153 void writeEncryptedLocalStorage('calendar-events', events)
154 }, [events])
155
156 return children
157}
158
159export function useCalendar(): CalendarContextType {
160 return useCalendarStore()
161}
162
163export const useCalendarContext = useCalendar