1+ import { h } from "@commontools/html" ;
2+ import { cell , derive , handler , NAME , recipe , UI } from "@commontools/builder" ;
3+ import { z } from "zod" ;
4+
5+ // Define a CalendarEvent type
6+ const CalendarEvent = z . object ( {
7+ id : z . string ( ) ,
8+ summary : z . string ( ) . optional ( ) ,
9+ description : z . string ( ) . optional ( ) ,
10+ start : z . string ( ) ,
11+ end : z . string ( ) ,
12+ location : z . string ( ) . optional ( ) ,
13+ eventType : z . string ( ) . optional ( ) ,
14+ } ) ;
15+ type CalendarEvent = z . infer < typeof CalendarEvent > ;
16+
17+ const Auth = z . object ( {
18+ token : z . string ( ) ,
19+ tokenType : z . string ( ) ,
20+ scope : z . array ( z . string ( ) ) ,
21+ expiresIn : z . number ( ) ,
22+ expiresAt : z . number ( ) ,
23+ refreshToken : z . string ( ) ,
24+ user : z . object ( {
25+ email : z . string ( ) ,
26+ name : z . string ( ) ,
27+ picture : z . string ( ) ,
28+ } ) ,
29+ } ) ;
30+ type Auth = z . infer < typeof Auth > ;
31+
32+ // Recipe settings now include calendarId and limit
33+ const Recipe = z
34+ . object ( {
35+ settings : z . object ( {
36+ calendarId : z
37+ . string ( )
38+ . default ( "primary" )
39+ . describe ( "Calendar ID to fetch events from" ) ,
40+ limit : z
41+ . number ( )
42+ . default ( 250 )
43+ . describe ( "number of events to import" ) ,
44+ } ) ,
45+ } )
46+ . describe ( "fake calendar" ) ;
47+
48+ // Updated result schema for calendar events
49+ const ResultSchema = {
50+ type : "object" ,
51+ properties : {
52+ events : {
53+ type : "array" ,
54+ items : {
55+ type : "object" ,
56+ properties : {
57+ id : { type : "string" } ,
58+ summary : { type : "string" } ,
59+ description : { type : "string" } ,
60+ start : { type : "string" } ,
61+ end : { type : "string" } ,
62+ location : { type : "string" } ,
63+ eventType : { type : "string" } ,
64+ } ,
65+ } ,
66+ } ,
67+ googleUpdater : { asCell : true , type : "action" } ,
68+ auth : {
69+ type : "object" ,
70+ properties : {
71+ token : { type : "string" } ,
72+ tokenType : { type : "string" } ,
73+ scope : { type : "array" , items : { type : "string" } } ,
74+ expiresIn : { type : "number" } ,
75+ expiresAt : { type : "number" } ,
76+ refreshToken : { type : "string" } ,
77+ } ,
78+ } ,
79+ } ,
80+ } ;
81+
82+ // Handler to update the limit for events to import
83+ const updateLimit = handler < { detail : { value : string } } , { limit : number } > (
84+ ( { detail } , state ) => {
85+ state . limit = parseInt ( detail ?. value ?? "10" ) || 0 ;
86+ } ,
87+ ) ;
88+
89+ // Handler to update the calendar ID
90+ const updateCalendarId = handler <
91+ { detail : { value : string } } ,
92+ { calendarId : string }
93+ > ( ( { detail } , state ) => {
94+ state . calendarId = detail ?. value ?? "primary" ;
95+ } ) ;
96+
97+ // The updater now fetches calendar events using Fetch
98+ const calendarUpdater = handler <
99+ NonNullable < unknown > ,
100+ { events : CalendarEvent [ ] ; auth : Auth ; settings : { calendarId : string ; limit : number } }
101+ > ( ( _event , state ) => {
102+ console . log ( "calendarUpdater!" ) ;
103+
104+ if ( ! state . auth . token ) {
105+ console . log ( "no token" ) ;
106+ return ;
107+ }
108+ if ( state . auth . expiresAt && state . auth . expiresAt < Date . now ( ) ) {
109+ console . log ( "token expired at " , state . auth . expiresAt ) ;
110+ return ;
111+ }
112+
113+ // Get existing event IDs for lookup
114+ const existingEventIds = new Set ( ( state . events || [ ] ) . map ( ( event ) => event . id ) ) ;
115+ console . log ( "existing event ids" , existingEventIds ) ;
116+
117+ fetchCalendar (
118+ state . auth . token ,
119+ state . settings . limit ,
120+ state . settings . calendarId ,
121+ existingEventIds ,
122+ ) . then ( ( result ) => {
123+ // Filter out any duplicates by ID
124+ const newEvents = result . items . filter ( ( event ) => ! existingEventIds . has ( event . id ) ) ;
125+ if ( newEvents . length > 0 ) {
126+ console . log ( `Adding ${ newEvents . length } new events` ) ;
127+ state . events . push ( ...newEvents ) ;
128+ } else {
129+ console . log ( "No new events found" ) ;
130+ }
131+ } ) ;
132+ } ) ;
133+
134+ // Helper function to fetch calendar events using the Google Calendar API
135+ export async function fetchCalendar (
136+ accessToken : string ,
137+ maxResults : number = 250 ,
138+ calendarId : string = "primary" ,
139+ existingEventIds : Set < string > ,
140+ ) {
141+ // Get current date in ISO format for timeMin parameter
142+ const now = new Date ( ) . toISOString ( ) ;
143+
144+ const listResponse = await fetch (
145+ `https://www.googleapis.com/calendar/v3/calendars/${ encodeURIComponent (
146+ calendarId ,
147+ ) } /events?maxResults=${ maxResults } &timeMin=${ encodeURIComponent ( now ) } &singleEvents=true&orderBy=startTime`,
148+ {
149+ headers : {
150+ Authorization : `Bearer ${ accessToken } ` ,
151+ } ,
152+ } ,
153+ ) ;
154+
155+ const listData = await listResponse . json ( ) ;
156+
157+ if ( ! listData . items || ! Array . isArray ( listData . items ) ) {
158+ return { items : [ ] } ;
159+ }
160+
161+ const events = listData . items
162+ . filter ( ( event : { id : string } ) => ! existingEventIds . has ( event . id ) )
163+ . map ( ( event : any ) => ( {
164+ id : event . id ,
165+ summary : event . summary || "" ,
166+ description : event . description || "" ,
167+ start : event . start
168+ ? event . start . dateTime || event . start . date || ""
169+ : "" ,
170+ end : event . end ? event . end . dateTime || event . end . date || "" : "" ,
171+ location : event . location || "" ,
172+ eventType : event . eventType || "" ,
173+ } ) ) ;
174+
175+ return { items : events } ;
176+ }
177+
178+ // Export the recipe, wiring up state cells, UI and the updater
179+ export default recipe ( Recipe , ResultSchema , ( { settings } ) => {
180+ const auth = cell < Auth > ( {
181+ token : "" ,
182+ tokenType : "" ,
183+ scope : [ ] ,
184+ expiresIn : 0 ,
185+ expiresAt : 0 ,
186+ refreshToken : "" ,
187+ user : {
188+ email : "" ,
189+ name : "" ,
190+ picture : "" ,
191+ } ,
192+ } ) ;
193+
194+ const events = cell < CalendarEvent [ ] > ( [ ] ) ;
195+
196+ derive ( events , ( events ) => {
197+ console . log ( "events" , events . length ) ;
198+ } ) ;
199+
200+ return {
201+ [ NAME ] : "calendar importer" ,
202+ [ UI ] : (
203+ < div >
204+ < h1 > Calendar Importer</ h1 >
205+ < common-hstack >
206+ < label > Import Limit</ label >
207+ < common-input
208+ value = { settings . limit }
209+ placeholder = "count of events to import"
210+ oncommon-input = { updateLimit ( { limit : settings . limit } ) }
211+ />
212+ </ common-hstack >
213+ < common-hstack >
214+ < label > Calendar ID</ label >
215+ < common-input
216+ value = { settings . calendarId }
217+ placeholder = "Calendar ID (e.g. primary)"
218+ oncommon-input = { updateCalendarId ( { calendarId : settings . calendarId } ) }
219+ />
220+ </ common-hstack >
221+ < common-google-oauth $authCell = { auth } auth = { auth } />
222+ < div >
223+ < table >
224+ < thead >
225+ < tr >
226+ < th > Start</ th >
227+ < th > End</ th >
228+ < th > Summary</ th >
229+ < th > Location</ th >
230+ < th > Type</ th >
231+ </ tr >
232+ </ thead >
233+ < tbody >
234+ { events . map ( ( event ) => (
235+ < tr >
236+ < td > { event . start } </ td >
237+ < td > { event . end } </ td >
238+ < td > { event . summary } </ td >
239+ < td > { event . location } </ td >
240+ < td > { event . eventType } </ td >
241+ </ tr >
242+ ) ) }
243+ </ tbody >
244+ </ table >
245+ </ div >
246+ </ div >
247+ ) ,
248+ events,
249+ auth,
250+ googleUpdater : calendarUpdater ( { events, auth, settings } ) ,
251+ } ;
252+ } ) ;
0 commit comments