From b074145c2e9d167c4adef57d5d62fcd9fa31b978 Mon Sep 17 00:00:00 2001 From: Robby P <101137670+rbbydotdev@@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:54:37 +0700 Subject: [PATCH 1/7] add 'you are here' timezone top of list remove current tz from dropdown list --- frontend/package-lock.json | 24 +++++ frontend/package.json | 1 + frontend/src/components/timezone-dropdown.tsx | 89 ++++++++++++++----- frontend/src/components/ui/separator.tsx | 29 ++++++ 4 files changed, 121 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/ui/separator.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4da80bc..f30b8d5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -1396,6 +1397,29 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", + "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8634f27..10a2d95 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/frontend/src/components/timezone-dropdown.tsx b/frontend/src/components/timezone-dropdown.tsx index fe48a67..3400a96 100644 --- a/frontend/src/components/timezone-dropdown.tsx +++ b/frontend/src/components/timezone-dropdown.tsx @@ -17,12 +17,19 @@ import { PopoverTrigger, } from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; +import { MapPin } from "lucide-react"; import { useEffect } from "react"; const tzlocal = Intl.DateTimeFormat().resolvedOptions().timeZone; -const timezones = [ - { value: tzlocal, label: tzlocal }, + +type TZ = { value: string; label: string }; +const mytz = { + value: tzlocal, + label: tzlocal ? tzlocal.split("/").pop()?.replace("_", " ") || "" : "", +}; +const timezones: TZ[] = [ { value: "America/New_York", label: "New York" }, { value: "America/Los_Angeles", label: "Los Angeles" }, { value: "America/Chicago", label: "Chicago" }, @@ -33,9 +40,9 @@ const timezones = [ { value: "Asia/Dubai", label: "Dubai" }, { value: "Australia/Sydney", label: "Sydney" }, { value: "Asia/Bangkok", label: "Bangkok" }, -]; +].filter((tz) => tz.value !== tzlocal); -const defaultValue = timezones[0].value; +const defaultValue = mytz.value; export function useTimezoneDropdown() { const [open, setOpen] = React.useState(false); const [value, setValue] = React.useState(defaultValue); @@ -70,6 +77,13 @@ export function TimezoneDropdown({ value: string; setValue: (value: string) => void; }) { + const onSelect = React.useCallback( + (currentValue: string) => { + setValue(currentValue === value ? "" : currentValue); + setOpen(false); + }, + [setOpen, setValue, value] + ); return ( @@ -80,7 +94,9 @@ export function TimezoneDropdown({ className="w-[180px] justify-between" > {value - ? timezones.find((timezone) => timezone.value === value)?.label + ? value === mytz.value + ? mytz.label + : timezones.find((timezone) => timezone.value === value)?.label : "Select timezone..."} @@ -91,24 +107,25 @@ export function TimezoneDropdown({ No timezone found. + {mytz && ( + <> + + + + + + )} {timezones.map((timezone) => ( - { - setValue(currentValue === value ? "" : currentValue); - setOpen(false); - }} - className="cursor-pointer" - > - {timezone.label} - - + ))} @@ -117,3 +134,31 @@ export function TimezoneDropdown({ ); } + +const TimezoneCommandItem = ({ + timezone, + onSelect, + current, + children, +}: { + timezone: TZ; + onSelect: (cv: string) => void; + current: string; + children?: React.ReactNode; +}) => ( + + {children} + {timezone.label} + + +); diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx new file mode 100644 index 0000000..6d7f122 --- /dev/null +++ b/frontend/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } From 8b92e334bd619294521f4f4d77720563f003d612 Mon Sep 17 00:00:00 2001 From: robby <101137670+rbbydotdev@users.noreply.github.com> Date: Wed, 4 Dec 2024 01:38:03 +0700 Subject: [PATCH 2/7] Update README.md --- .gitignore | 1 + README.md | 4 ++-- backend/src/app.ts | 4 ++-- config.example.js | 3 +++ index.html | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 config.example.js create mode 100644 index.html diff --git a/.gitignore b/.gitignore index a1b621c..928e041 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.sw? .clasp.json +config.js diff --git a/README.md b/README.md index e051ac0..23f320a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Someday -**Free to host and open-source cal.com / calendly alternative built on [Google-Apps-Script](https://developers.google.com/apps-script) for Gmail users.** +**Free to host calendar availability picker - open-source cal.com / calendly alternative built on [Google-Apps-Script](https://developers.google.com/apps-script) for Gmail users.** @@ -15,7 +15,7 @@ Someday is a simple, open-source scheduling tool designed specifically for Gmail ### Key Features -- **Free and Open Source**: Enjoy all the premium scheduling features without any cost. Someday is completely free to use and open for contributions. +- **Free and Open Source**: Someday is completely free to use and open for contributions. - **Effortless Integration**: Designed as a Google Apps Script, Someday integrates seamlessly with your Gmail, making it easy to manage your schedule directly from your inbox. - **Developer-Friendly**: Built with modern, developer-preferred technologies, Someday is easy to customize and extend to meet your specific needs. - **Customizable Work Hours**: Set your availability with precision, allowing others to book time slots that fit your schedule perfectly. diff --git a/backend/src/app.ts b/backend/src/app.ts index a6cd5f1..7730d5e 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,5 +1,5 @@ const CALENDAR = "primary"; -const TIME_ZONE = "America/New_York"; +const TIME_ZONE = "America/Los_Angeles"; // America/Los_Angeles // America/Denver // America/Chicago @@ -8,7 +8,7 @@ const TIME_ZONE = "America/New_York"; const WORKDAYS = [1, 2, 3, 4, 5]; const WORKHOURS = { start: 9, - end: 13, + end: 16, }; const DAYS_IN_ADVANCE = 28; //high numbered days in advance cause significant loading time slow down diff --git a/config.example.js b/config.example.js new file mode 100644 index 0000000..bd6b93f --- /dev/null +++ b/config.example.js @@ -0,0 +1,3 @@ +const CONFIG = { + SCRIPT_URL: "YOUR-SCRIPT-URL-HERE" +}; \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..7e72f28 --- /dev/null +++ b/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + \ No newline at end of file From f7f54a54afb12a265c805268898a16a391eff63a Mon Sep 17 00:00:00 2001 From: Olivia McGoffin Date: Thu, 27 Feb 2025 16:16:57 -0800 Subject: [PATCH 3/7] Add public url to index --- .gitignore | 1 - config.example.js | 3 --- index.html | 7 ++----- 3 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 config.example.js diff --git a/.gitignore b/.gitignore index 928e041..a1b621c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,3 @@ dist-ssr *.sw? .clasp.json -config.js diff --git a/config.example.js b/config.example.js deleted file mode 100644 index bd6b93f..0000000 --- a/config.example.js +++ /dev/null @@ -1,3 +0,0 @@ -const CONFIG = { - SCRIPT_URL: "YOUR-SCRIPT-URL-HERE" -}; \ No newline at end of file diff --git a/index.html b/index.html index 7e72f28..8f05484 100644 --- a/index.html +++ b/index.html @@ -23,11 +23,8 @@ - - - + \ No newline at end of file From 80d62bd252e96f217497516010172a36c6af4dd1 Mon Sep 17 00:00:00 2001 From: Olivia McGoffin Date: Thu, 27 Feb 2025 16:45:59 -0800 Subject: [PATCH 4/7] Modify app to take in multiple calendars and store calendars in script properties --- backend/src/app.ts | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 7730d5e..ea02796 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,4 +1,4 @@ -const CALENDAR = "primary"; +const CALENDARS: string[] = JSON.parse(PropertiesService.getScriptProperties().getProperty('CALENDARS') ?? '["primary"]'); const TIME_ZONE = "America/Los_Angeles"; // America/Los_Angeles // America/Denver @@ -29,7 +29,6 @@ function fetchAvailability(): { const nearestTimeslot = new Date( Math.floor(new Date().getTime() / TSDURMS) * TSDURMS ); - const calendarId = CALENDAR; const now = nearestTimeslot; const end = new Date( Date.UTC( @@ -42,15 +41,18 @@ function fetchAvailability(): { const response = Calendar.Freebusy!.query({ timeMin: now.toISOString(), timeMax: end.toISOString(), - items: [{ id: calendarId }], + items: CALENDARS.map((id: string) => ({ id })), }); - const events = ( - (response as any).calendars[calendarId].busy as { + const events = CALENDARS.map((calendarId: string) => + ((response as any).calendars[calendarId].busy as { start: string; end: string; - }[] - ).map(({ start, end }) => ({ start: new Date(start), end: new Date(end) })); + }[]).map(({ start, end }) => ({ + start: new Date(start), + end: new Date(end) + })) + ).reduce((acc, curr) => acc.concat(curr), []); //get all timeslots between now and end date const timeslots = []; for ( @@ -66,7 +68,7 @@ function fetchAvailability(): { if (startTZ.getHours() < WORKHOURS.start) continue; if (startTZ.getHours() >= WORKHOURS.end) continue; if (WORKDAYS.indexOf(startTZ.getDay()) < 0) continue; - if (events.some((event) => event.start < end && event.end > start)) { + if (events.some((event: { start: Date; end: Date }) => event.start < end && event.end > start)) { continue; } timeslots.push(start.toISOString()); @@ -82,7 +84,7 @@ function bookTimeslot( note: string ): string { Logger.log(`Booking timeslot: ${timeslot} for ${name}`); - const calendarId = CALENDAR; + const calendarId = CALENDARS[0]; const startTime = new Date(timeslot); if (isNaN(startTime.getTime())) { throw new Error("Invalid start time"); @@ -96,18 +98,14 @@ function bookTimeslot( const possibleEvents = Calendar.Freebusy!.query({ timeMin: startTime.toISOString(), timeMax: endTime.toISOString(), - items: [{ id: calendarId }], + items: CALENDARS.map((id: string) => ({ id })), }); - const busy = (possibleEvents as any).calendars[calendarId].busy; + const hasConflict = CALENDARS.some((calId: string) => + (possibleEvents as any).calendars[calId].busy.length > 0 + ); - if ( - busy.some((event: { start: Date; end: Date }) => { - const eventStart = new Date(event.start.toString()); - const eventEnd = new Date(event.end.toString()); - return eventStart <= endTime && eventEnd >= startTime; - }) - ) { + if (hasConflict) { throw new Error("Timeslot not available"); } From ad1bba9b6be2260a6c1e3b80b4e99e5ec5021825 Mon Sep 17 00:00:00 2001 From: Olivia McGoffin Date: Sun, 2 Mar 2025 17:18:58 -0800 Subject: [PATCH 5/7] Safely handles cases where the CALENDARS property might be malformed JSON Returns [primary] as a fallback if there's any issue Update README.md --- README.md | 10 ++++++++-- backend/src/app.ts | 31 ++++++++++++++++++------------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 23f320a..4af2673 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # Someday **Free to host calendar availability picker - open-source cal.com / calendly alternative built on [Google-Apps-Script](https://developers.google.com/apps-script) for Gmail users.** @@ -15,7 +14,8 @@ Someday is a simple, open-source scheduling tool designed specifically for Gmail ### Key Features -- **Free and Open Source**: Someday is completely free to use and open for contributions. +- **Free to Host**: Using Google Apps Script, hosting is free via your google account. +- **Open Source**: Someday is completely free to use and open for contributions. - **Effortless Integration**: Designed as a Google Apps Script, Someday integrates seamlessly with your Gmail, making it easy to manage your schedule directly from your inbox. - **Developer-Friendly**: Built with modern, developer-preferred technologies, Someday is easy to customize and extend to meet your specific needs. - **Customizable Work Hours**: Set your availability with precision, allowing others to book time slots that fit your schedule perfectly. @@ -111,6 +111,12 @@ __you may need to sign out of all accounts, and only into your target account__ - Authorization modal will pop up, 'Review permissions', select your account, you will see a warning, go to advanced, then Go to (unsafe) then click Allow - if it worked, refresh the page/editor then run the function again and it should complete without issue. +5. **Calendar Access:** + - By default, the script uses your primary calendar + - To use other calendars, make sure they are added to your Google Calendar with appropriate permissions + - You can change which calendar to use by modifying the `CALENDAR` variable in `backend/src/app.ts` + - Note: The script needs at least read access to the calendar you specify + ## Cheat Sheet - `npm run deploy` - build and delpoy diff --git a/backend/src/app.ts b/backend/src/app.ts index ea02796..470428d 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,4 +1,14 @@ -const CALENDARS: string[] = JSON.parse(PropertiesService.getScriptProperties().getProperty('CALENDARS') ?? '["primary"]'); +const CALENDARS: string[] = (() => { + const calendarsProp = PropertiesService.getScriptProperties().getProperty('CALENDARS'); + try { + if (!calendarsProp) return ["primary"]; + const parsed = JSON.parse(calendarsProp); + return Array.isArray(parsed) ? parsed : ["primary"]; + } catch (e) { + Logger.log(`Error parsing CALENDARS property: ${e}`); + return ["primary"]; + } +})(); const TIME_ZONE = "America/Los_Angeles"; // America/Los_Angeles // America/Denver @@ -44,15 +54,15 @@ function fetchAvailability(): { items: CALENDARS.map((id: string) => ({ id })), }); - const events = CALENDARS.map((calendarId: string) => - ((response as any).calendars[calendarId].busy as { - start: string; - end: string; - }[]).map(({ start, end }) => ({ + const events = CALENDARS.map((calendarId: string) => { + const busyTimes = (response as any).calendars[calendarId].busy; + Logger.log(`Busy times for ${calendarId}: ${JSON.stringify(busyTimes)}`); + return busyTimes.map(({ start, end }: { start: string; end: string }) => ({ start: new Date(start), end: new Date(end) - })) - ).reduce((acc, curr) => acc.concat(curr), []); + })); + }).reduce((acc, curr) => acc.concat(curr), []); + //get all timeslots between now and end date const timeslots = []; for ( @@ -83,7 +93,6 @@ function bookTimeslot( phone: string, note: string ): string { - Logger.log(`Booking timeslot: ${timeslot} for ${name}`); const calendarId = CALENDARS[0]; const startTime = new Date(timeslot); if (isNaN(startTime.getTime())) { @@ -92,8 +101,6 @@ function bookTimeslot( const endTime = new Date(startTime.getTime()); endTime.setUTCMinutes(startTime.getUTCMinutes() + TIMESLOT_DURATION); - Logger.log(`Timeslot start: ${startTime}, end: ${endTime}`); - try { const possibleEvents = Calendar.Freebusy!.query({ timeMin: startTime.toISOString(), @@ -120,11 +127,9 @@ function bookTimeslot( status: "confirmed", } ); - Logger.log(`Event created: ${event.getId()}`); return `Timeslot booked successfully`; } catch (e) { const error = e as Error; - Logger.log(`Failed to create event: ${error.message}`); throw new Error(`Failed to create event: ${error.message}`); } } From 5afac4be197daf8631819c35fe3c9e10aa99f020 Mon Sep 17 00:00:00 2001 From: Olivia McGoffin Date: Sun, 2 Mar 2025 18:19:47 -0800 Subject: [PATCH 6/7] Remove index.html from tracking --- index.html | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 index.html diff --git a/index.html b/index.html deleted file mode 100644 index 8f05484..0000000 --- a/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file From 9b2008289f3b47dbc33c2efc2010f7a1aa62336d Mon Sep 17 00:00:00 2001 From: Olivia McGoffin Date: Sun, 2 Mar 2025 18:35:39 -0800 Subject: [PATCH 7/7] Update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 4af2673..cb5e551 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,11 @@ __you may need to sign out of all accounts, and only into your target account__ - To use other calendars, make sure they are added to your Google Calendar with appropriate permissions - You can change which calendar to use by modifying the `CALENDAR` variable in `backend/src/app.ts` - Note: The script needs at least read access to the calendar you specify + - To use multiple calendars, you'll need to add them as Script Properties in the Apps Script editor: + 1. Open the script editor with `clasp open` + 2. Go to Project Settings (⚙️ icon) + 3. Under "Script Properties", click "Add Script Property" + 4. Add a property named "CALENDARS" with a comma-separated list of calendar IDs ## Cheat Sheet