From 496fcdfabc6eee323e41b1e4d37e09a48c144af0 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Mon, 5 Jul 2021 19:50:54 +0000 Subject: [PATCH 01/16] Minimized msgraph calls while event listing by batching --- lib/calendarClient.ts | 54 +++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 4d8d7421..3cebe97c 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -225,28 +225,38 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { ? listCalendars().then((cals) => cals.map((e) => e.externalId)) : Promise.resolve(selectedCalendarIds).then((x) => x) ).then((ids: string[]) => { - const urls = ids.map( - (calendarId) => - "https://graph.microsoft.com/v1.0/me/calendars/" + calendarId + "/events" + filter - ); - return Promise.all( - urls.map((url) => - fetch(url, { - method: "get", - headers: { - Authorization: "Bearer " + accessToken, - Prefer: 'outlook.timezone="Etc/GMT"', - }, - }) - .then(handleErrorsJson) - .then((responseBody) => - responseBody.value.map((evt) => ({ - start: evt.start.dateTime + "Z", - end: evt.end.dateTime + "Z", - })) - ) - ) - ).then((results) => results.reduce((acc, events) => acc.concat(events), [])); + const requests = ids.map((calendarId, id) => ({ + id, + method: "GET", + headers: { + Prefer: 'outlook.timezone="Etc/GMT"', + }, + url: `/me/calendars/${calendarId}/events${filter}`, + })); + + return fetch("https://graph.microsoft.com/v1.0/$batch", { + method: "POST", + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + }, + body: JSON.stringify({ requests }), + }) + .then(handleErrorsJson) + .then((responseBody) => + responseBody.responses.reduce( + (acc, subResponse) => + acc.concat( + subResponse.body.value.map((evt) => { + return { + start: evt.start.dateTime + "Z", + end: evt.end.dateTime + "Z", + }; + }) + ), + [] + ) + ); }); }) .catch((err) => { From e5d94e74a2c4907b63a67249d57f59f6623f0d5f Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 21 Jul 2021 14:01:48 +0200 Subject: [PATCH 02/16] No HTMl in rich event description --- lib/CalEventParser.ts | 9 +++++++++ lib/calendarClient.ts | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index 69724ccc..ef1ff315 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -86,4 +86,13 @@ export default class CalEventParser { eventCopy.description = this.getRichDescriptionHtml(); return eventCopy; } + + /** + * Returns a calendar event with rich description as plain text. + */ + public asRichEventPlain(): CalendarEvent { + const eventCopy: CalendarEvent = { ...this.calEvent }; + eventCopy.description = this.getRichDescription(); + return eventCopy; + } } diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 3891feab..b64e0bf8 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -508,7 +508,7 @@ const listCalendars = (withCredentials) => const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const uid: string = parser.getUid(); - const richEvent: CalendarEvent = parser.asRichEvent(); + const richEvent: CalendarEvent = parser.asRichEventPlain(); const creationResult = credential ? await calendars([credential])[0].createEvent(richEvent) : null; @@ -555,7 +555,7 @@ const updateEvent = async ( ): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const newUid: string = parser.getUid(); - const richEvent: CalendarEvent = parser.asRichEvent(); + const richEvent: CalendarEvent = parser.asRichEventPlain(); const updateResult = credential ? await calendars([credential])[0].updateEvent(uidToUpdate, richEvent) From 39f16d95cb777998241fb382ac780fd29451c8c7 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 21 Jul 2021 14:25:28 +0200 Subject: [PATCH 03/16] Properly replace a link tags --- lib/emails/helpers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/emails/helpers.ts b/lib/emails/helpers.ts index e5218a0a..929a0556 100644 --- a/lib/emails/helpers.ts +++ b/lib/emails/helpers.ts @@ -25,5 +25,9 @@ export function getFormattedMeetingId(videoCallData: VideoCallData): string { } export function stripHtml(html: string): string { - return html.replace("
", "\n").replace(/<[^>]+>/g, ""); + const aLinkRegExp = /"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g; + return html + .replace("
", "\n") + .replace(aLinkRegExp, "$2: $1") + .replace(/<[^>]+>/g, ""); } From 13a6b9b54934b94f9d03c3f480167611209502af Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 21 Jul 2021 18:20:08 +0200 Subject: [PATCH 04/16] Use regex to remove br tag --- lib/emails/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/emails/helpers.ts b/lib/emails/helpers.ts index 929a0556..7c778ce4 100644 --- a/lib/emails/helpers.ts +++ b/lib/emails/helpers.ts @@ -27,7 +27,7 @@ export function getFormattedMeetingId(videoCallData: VideoCallData): string { export function stripHtml(html: string): string { const aLinkRegExp = /"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g; return html - .replace("
", "\n") + .replace(//g, "\n") .replace(aLinkRegExp, "$2: $1") .replace(/<[^>]+>/g, ""); } From 0a60a62910e1d05be4183eb7109a32a322f72737 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 22 Jul 2021 00:46:31 +0200 Subject: [PATCH 05/16] Conditionally use HTML --- lib/calendarClient.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index b64e0bf8..662b02e6 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -508,7 +508,13 @@ const listCalendars = (withCredentials) => const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const uid: string = parser.getUid(); - const richEvent: CalendarEvent = parser.asRichEventPlain(); + /* + * Matching the credential type is a workaround because the office calendar simply strips away newlines (\n and \r). + * We need HTML there. Google Calendar understands newlines and Apple Calendar cannot show HTML, so no HTML should + * be used for Google and Apple Calendar. + */ + const richEvent: CalendarEvent = + credential.type === "office365_calendar" ? parser.asRichEvent() : parser.asRichEventPlain(); const creationResult = credential ? await calendars([credential])[0].createEvent(richEvent) : null; From 936338db3ec4adae08905025a8314fe86ca7d936 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 22 Jul 2021 01:11:25 +0200 Subject: [PATCH 06/16] Added condition when updating event --- lib/calendarClient.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 662b02e6..711cfc14 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -561,7 +561,8 @@ const updateEvent = async ( ): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const newUid: string = parser.getUid(); - const richEvent: CalendarEvent = parser.asRichEventPlain(); + const richEvent: CalendarEvent = + credential.type === "office365_calendar" ? parser.asRichEvent() : parser.asRichEventPlain(); const updateResult = credential ? await calendars([credential])[0].updateEvent(uidToUpdate, richEvent) From 4368ad02894eb18eb7d11ac3795d4f525dba79af Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Thu, 22 Jul 2021 22:52:27 +0000 Subject: [PATCH 07/16] Implement minimum booking notice --- components/booking/AvailableTimes.tsx | 2 ++ components/booking/DatePicker.tsx | 4 ++++ lib/slots.ts | 13 +++++++++---- pages/[user]/[type].tsx | 5 ++++- prisma/schema.prisma | 1 + 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/components/booking/AvailableTimes.tsx b/components/booking/AvailableTimes.tsx index 64cb4dd0..e2d6af3e 100644 --- a/components/booking/AvailableTimes.tsx +++ b/components/booking/AvailableTimes.tsx @@ -7,6 +7,7 @@ const AvailableTimes = ({ date, eventLength, eventTypeId, + minimumBookingNotice, workingHours, timeFormat, user, @@ -20,6 +21,7 @@ const AvailableTimes = ({ eventLength, workingHours, organizerTimeZone, + minimumBookingNotice, }); return ( diff --git a/components/booking/DatePicker.tsx b/components/booking/DatePicker.tsx index b49b887e..c6a49fa0 100644 --- a/components/booking/DatePicker.tsx +++ b/components/booking/DatePicker.tsx @@ -23,6 +23,7 @@ const DatePicker = ({ periodEndDate, periodDays, periodCountCalendarDays, + minimumBookingNotice, }) => { const [calendar, setCalendar] = useState([]); const [selectedMonth, setSelectedMonth] = useState(); @@ -77,6 +78,7 @@ const DatePicker = ({ !getSlots({ inviteeDate: date, frequency: eventLength, + minimumBookingNotice, workingHours, organizerTimeZone, }).length @@ -93,6 +95,7 @@ const DatePicker = ({ !getSlots({ inviteeDate: date, frequency: eventLength, + minimumBookingNotice, workingHours, organizerTimeZone, }).length @@ -106,6 +109,7 @@ const DatePicker = ({ !getSlots({ inviteeDate: date, frequency: eventLength, + minimumBookingNotice, workingHours, organizerTimeZone, }).length diff --git a/lib/slots.ts b/lib/slots.ts index 3c0d45a1..957d084b 100644 --- a/lib/slots.ts +++ b/lib/slots.ts @@ -1,7 +1,6 @@ import dayjs, { Dayjs } from "dayjs"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; - dayjs.extend(utc); dayjs.extend(timezone); @@ -119,9 +118,15 @@ const getSlots = ({ workingHours, organizerTimeZone, }: GetSlots): Dayjs[] => { - const startTime = dayjs().utcOffset(inviteeDate.utcOffset()).isSame(inviteeDate, "day") - ? inviteeDate.hour() * 60 + inviteeDate.minute() + (minimumBookingNotice || 0) - : 0; + // current date in invitee tz + const currentDate = dayjs().utcOffset(inviteeDate.utcOffset()); + const startDate = currentDate.add(minimumBookingNotice, "minutes"); // + minimum notice period + // when the invitee date is not the same as the current date, reset the date to the start of day + if (inviteeDate.date() !== currentDate.date()) { + inviteeDate = inviteeDate.startOf("day"); + } + + const startTime = startDate.isAfter(inviteeDate) ? inviteeDate.hour() * 60 + inviteeDate.minute() : 0; const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency); diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 04cdbd76..222db31e 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -167,14 +167,16 @@ export default function Type(props): Type { organizerTimeZone={props.eventType.timeZone || props.user.timeZone} inviteeTimeZone={timeZone()} eventLength={props.eventType.length} + minimumBookingNotice={props.eventType.minimumBookingNotice} /> {selectedDate && ( @@ -238,6 +240,7 @@ export const getServerSideProps: GetServerSideProps = async (context: GetServerS "periodStartDate", "periodEndDate", "periodCountCalendarDays", + "minimumBookingNotice", ] ); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 83aed724..409c68af 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -31,6 +31,7 @@ model EventType { periodDays Int? periodCountCalendarDays Boolean? requiresConfirmation Boolean @default(false) + minimumBookingNotice Int @default(120) } model Credential { From 00550ac8ce4d1bd74a7ee972759519e328d9da6d Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Thu, 22 Jul 2021 22:55:15 +0000 Subject: [PATCH 08/16] Added migration for minimum booking notice --- .../20210722225431_minimum_booking_notice/migration.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 prisma/migrations/20210722225431_minimum_booking_notice/migration.sql diff --git a/prisma/migrations/20210722225431_minimum_booking_notice/migration.sql b/prisma/migrations/20210722225431_minimum_booking_notice/migration.sql new file mode 100644 index 00000000..75c5a7de --- /dev/null +++ b/prisma/migrations/20210722225431_minimum_booking_notice/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "minimumBookingNotice" INTEGER NOT NULL DEFAULT 120; From 9234f74bec4ce767e39c44bde9f32203f07c4d0f Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Fri, 23 Jul 2021 20:19:23 +0000 Subject: [PATCH 09/16] Added accompanying frontend --- lib/slots.ts | 8 +++++-- pages/api/availability/eventtype.ts | 1 + pages/availability/event/[type].tsx | 35 ++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/slots.ts b/lib/slots.ts index 957d084b..e4fdaa41 100644 --- a/lib/slots.ts +++ b/lib/slots.ts @@ -126,8 +126,12 @@ const getSlots = ({ inviteeDate = inviteeDate.startOf("day"); } - const startTime = startDate.isAfter(inviteeDate) ? inviteeDate.hour() * 60 + inviteeDate.minute() : 0; - + const startTime = startDate.isAfter(inviteeDate) + ? // block out everything when inviteeDate is less than startDate + startDate.date() > inviteeDate.date() + ? 1440 + : startDate.hour() * 60 + startDate.minute() + : 0; const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency); return getOverlaps( diff --git a/pages/api/availability/eventtype.ts b/pages/api/availability/eventtype.ts index 3add13cd..69dbac99 100644 --- a/pages/api/availability/eventtype.ts +++ b/pages/api/availability/eventtype.ts @@ -55,6 +55,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) periodStartDate: req.body.periodStartDate, periodEndDate: req.body.periodEndDate, periodCountCalendarDays: req.body.periodCountCalendarDays, + minimumBookingNotice: req.body.minimumBookingNotice, }; if (req.method == "POST") { diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index ae15a1a2..1fec9038 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -66,7 +66,8 @@ type EventTypeInput = { periodStartDate?: Date | string; periodEndDate?: Date | string; periodCountCalendarDays?: boolean; - enteredRequiresConfirmation: boolean; + requiresConfirmation: boolean; + minimumBookingNotice: number; }; const PERIOD_TYPES = [ @@ -92,7 +93,6 @@ export default function EventTypePage({ }: Props): JSX.Element { const router = useRouter(); - console.log(eventType); const inputOptions: OptionBase[] = [ { value: EventTypeCustomInputType.Text, label: "Text" }, { value: EventTypeCustomInputType.TextLong, label: "Multiline Text" }, @@ -174,6 +174,7 @@ export default function EventTypePage({ const lengthRef = useRef(); const isHiddenRef = useRef(); const requiresConfirmationRef = useRef(); + const minimumBookingNoticeRef = useRef(); const eventNameRef = useRef(); const periodDaysRef = useRef(); const periodDaysTypeRef = useRef(); @@ -190,6 +191,7 @@ export default function EventTypePage({ const enteredDescription: string = descriptionRef.current.value; const enteredLength: number = parseInt(lengthRef.current.value); const enteredIsHidden: boolean = isHiddenRef.current.checked; + const enteredMinimumBookingNotice: number = parseInt(minimumBookingNoticeRef.current.value); const enteredRequiresConfirmation: boolean = requiresConfirmationRef.current.checked; const enteredEventName: string = eventNameRef.current.value; @@ -200,14 +202,6 @@ export default function EventTypePage({ const enteredPeriodStartDate = periodStartDate ? periodStartDate.toDate() : null; const enteredPeriodEndDate = periodEndDate ? periodEndDate.toDate() : null; - console.log("values", { - type, - periodDaysTypeRef, - enteredPeriodDays, - enteredPeriodDaysType, - enteredPeriodStartDate, - enteredPeriodEndDate, - }); // TODO: Add validation const payload: EventTypeInput = { @@ -226,6 +220,7 @@ export default function EventTypePage({ periodStartDate: enteredPeriodStartDate, periodEndDate: enteredPeriodEndDate, periodCountCalendarDays: enteredPeriodDaysType, + minimumBookingNotice: enteredMinimumBookingNotice, requiresConfirmation: enteredRequiresConfirmation, }; @@ -671,6 +666,25 @@ export default function EventTypePage({
When can people book this event? +
+ +
+ +
+ minutes +
+
+

@@ -1019,6 +1033,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, query periodEndDate: true, periodCountCalendarDays: true, requiresConfirmation: true, + minimumBookingNotice: true, }, }); From 749693b6bfc372cfd5901ca5c4e25679d6182575 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sat, 24 Jul 2021 21:23:15 +0200 Subject: [PATCH 10/16] Always use plain text event descriptions for now --- lib/calendarClient.ts | 6 ++---- lib/emails/helpers.ts | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 711cfc14..7eb37632 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -513,8 +513,7 @@ const createEvent = async (credential: Credential, calEvent: CalendarEvent): Pro * We need HTML there. Google Calendar understands newlines and Apple Calendar cannot show HTML, so no HTML should * be used for Google and Apple Calendar. */ - const richEvent: CalendarEvent = - credential.type === "office365_calendar" ? parser.asRichEvent() : parser.asRichEventPlain(); + const richEvent: CalendarEvent = parser.asRichEventPlain(); const creationResult = credential ? await calendars([credential])[0].createEvent(richEvent) : null; @@ -561,8 +560,7 @@ const updateEvent = async ( ): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const newUid: string = parser.getUid(); - const richEvent: CalendarEvent = - credential.type === "office365_calendar" ? parser.asRichEvent() : parser.asRichEventPlain(); + const richEvent: CalendarEvent = parser.asRichEventPlain(); const updateResult = credential ? await calendars([credential])[0].updateEvent(uidToUpdate, richEvent) diff --git a/lib/emails/helpers.ts b/lib/emails/helpers.ts index 7c778ce4..5c07dbbe 100644 --- a/lib/emails/helpers.ts +++ b/lib/emails/helpers.ts @@ -25,9 +25,11 @@ export function getFormattedMeetingId(videoCallData: VideoCallData): string { } export function stripHtml(html: string): string { + const aMailToRegExp = /"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g; const aLinkRegExp = /"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g; return html .replace(//g, "\n") + .replace(aMailToRegExp, "$1") .replace(aLinkRegExp, "$2: $1") .replace(/<[^>]+>/g, ""); } From f4553aade11f2d600d7aa4358a7e1b81c2fd2465 Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Mon, 26 Jul 2021 14:37:45 +0100 Subject: [PATCH 11/16] Formatting fixes for README.md --- README.md | 102 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 39964b50..dca19ff0 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@

+ ## About The Project calendso-screenshot @@ -28,18 +29,17 @@ Let's face it: Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes and even calls with our families. However, most tools are very limited in terms of control and customisations. That's where Calendso comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data. Calendso is to Calendly what GitLab is to GitHub. ### Product of the Month: April + #### Support us on [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso) Calendso - The open source Calendly alternative | Product Hunt Calendso - The open source Calendly alternative | Product Hunt Calendso - The open source Calendly alternative | Product Hunt - - ### Built With -* [Next.js](https://nextjs.org/) -* [React](https://reactjs.org/) -* [Tailwind](https://tailwindcss.com/) -* [Prisma](https://prisma.io/) +- [Next.js](https://nextjs.org/) +- [React](https://reactjs.org/) +- [Tailwind](https://tailwindcss.com/) +- [Prisma](https://prisma.io/) ## Stay Up-to-Date @@ -47,9 +47,8 @@ Calendso is currently in alpha. Watch **releases** of this repository to be noti ![calendso-star-github](https://user-images.githubusercontent.com/8019099/116010176-5d9c9900-a615-11eb-92d0-aa0e892f7056.gif) - - + ## Getting Started To get a local copy up and running, please follow these simple steps. @@ -57,9 +56,10 @@ To get a local copy up and running, please follow these simple steps. ### Prerequisites Here is what you need to be able to run Calendso. -* Node.js -* PostgreSQL -* Yarn _(recommended)_ + +- Node.js +- PostgreSQL +- Yarn _(recommended)_ You will also need Google API credentials. You can get this from the [Google API Console](https://console.cloud.google.com/apis/dashboard). More details on this can be found below under the [Obtaining the Google API Credentials section](#Obtaining-the-Google-API-Credentials). @@ -75,33 +75,35 @@ You will also need Google API credentials. You can get this from the [Google API ``` 3. Copy `.env.example` to `.env` 4. Configure environment variables in the .env file. Replace ``, ``, ``, `` with their applicable values + ``` DATABASE_URL='postgresql://:@:' GOOGLE_API_CREDENTIALS='secret' ``` +
If you don't know how to configure the DATABASE_URL, then follow the steps here 1. Create a free account with [Heroku](https://www.heroku.com/). 2. Create a new app. - Google Chrome — CleanShotX | 2021-04-20 at 02 01 56 + Google Chrome — CleanShotX | 2021-04-20 at 02 01 56 3. In your new app, go to `Overview` and next to `Installed add-ons`, click `Configure Add-ons`. We need this to set up our database. - ![image](https://user-images.githubusercontent.com/16905768/115323232-a53ba480-a17f-11eb-98db-58e2f8c52426.png) + ![image](https://user-images.githubusercontent.com/16905768/115323232-a53ba480-a17f-11eb-98db-58e2f8c52426.png) 4. Once you clicked on `Configure Add-ons`, click on `Find more add-ons` and search for `postgres`. One of the options will be `Heroku Postgres` - click on that option. - ![image](https://user-images.githubusercontent.com/16905768/115323126-5beb5500-a17f-11eb-8030-7380310807a9.png) + ![image](https://user-images.githubusercontent.com/16905768/115323126-5beb5500-a17f-11eb-8030-7380310807a9.png) 5. Once the pop-up appears, click `Submit Order Form` - plan name should be `Hobby Dev - Free`. - Google Chrome — CleanShotX | 2021-04-20 at 02 04 29 + Google Chrome — CleanShotX | 2021-04-20 at 02 04 29 6. Once you completed the above steps, click on your newly created `Heroku Postgres` and go to its `Settings`. - ![image](https://user-images.githubusercontent.com/16905768/115323367-e92ea980-a17f-11eb-9ff4-dec95f2ec349.png) + ![image](https://user-images.githubusercontent.com/16905768/115323367-e92ea980-a17f-11eb-9ff4-dec95f2ec349.png) 7. In `Settings`, copy your URI to your Calendso .env file and replace the `postgresql://:@:` with it. - ![image](https://user-images.githubusercontent.com/16905768/115323556-4591c900-a180-11eb-9808-2f55d2aa3995.png) - ![image](https://user-images.githubusercontent.com/16905768/115323697-7a9e1b80-a180-11eb-9f08-a742b1037f90.png) + ![image](https://user-images.githubusercontent.com/16905768/115323556-4591c900-a180-11eb-9808-2f55d2aa3995.png) + ![image](https://user-images.githubusercontent.com/16905768/115323697-7a9e1b80-a180-11eb-9f08-a742b1037f90.png) 8. To view your DB, once you add new data in Prisma, you can use [Heroku Data Explorer](https://heroku-data-explorer.herokuapp.com/).
@@ -119,10 +121,11 @@ You will also need Google API credentials. You can get this from the [Google API npx prisma studio ``` 8. Click on the `User` model to add a new user record. -9. Fill out the fields (remembering to encrypt your password with [BCrypt](https://bcrypt-generator.com/)) and click `Save 1 Record` to create your first user. +9. Fill out the fields (remembering to encrypt your password with [BCrypt](https://bcrypt-generator.com/)) and click `Save 1 Record` to create your first user. 10. Open a browser to [http://localhost:3000](http://localhost:3000) and login with your just created, first user. ### Upgrading from earlier versions + 1. Pull the current version: ``` git pull @@ -130,22 +133,28 @@ You will also need Google API credentials. You can get this from the [Google API 2. Apply database migrations by running one of the following commands: In a development environment, run: + ``` npx prisma migrate dev ``` + (this can clear your development database in some cases) In a production environment, run: + ``` npx prisma migrate deploy ``` + 3. Check the `.env.example` and compare it to your current `.env` file. In case there are any fields not present in your current `.env`, add them there. For the current version, especially check if the variable `BASE_URL` is present and properly set in your environment, for example: + ``` BASE_URL='https://yourdomain.com' ``` + 4. Start the server. In a development environment, just do: ``` yarn dev @@ -157,27 +166,33 @@ You will also need Google API credentials. You can get this from the [Google API ``` 5. Enjoy the new version. + ## Deployment + ### Docker + The Docker configuration for Calendso is an effort powered by people within the community. Calendso does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk. - + The Docker configuration can be found [in our docker repository](https://github.com/calendso/docker). + ### Railway + [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fcalendso%2Fcalendso&plugins=postgresql&envs=GOOGLE_API_CREDENTIALS%2CBASE_URL%2CNEXTAUTH_URL%2CPORT&BASE_URLDefault=http%3A%2F%2Flocalhost%3A3000&NEXTAUTH_URLDefault=http%3A%2F%2Flocalhost%3A3000&PORTDefault=3000) You can deploy Calendso on [Railway](https://railway.app/) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Calendso on their platform. + ## Roadmap See the [open issues](https://github.com/calendso/calendso/issues) for a list of proposed features (and known issues). + ## Contributing Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. - 1. Fork the project 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 3. Make your changes @@ -185,7 +200,6 @@ Contributions are what make the open source community such an amazing place to b 5. Push to the branch (`git push origin feature/AmazingFeature`) 6. Open a pull request - ## Obtaining the Google API Credentials 1. Open [Google API Console](https://console.cloud.google.com/apis/dashboard). If you don't have a project in your Google Cloud subscription, you'll need to create one before proceeding further. Under Dashboard pane, select Enable APIS and Services. @@ -196,11 +210,12 @@ Contributions are what make the open source community such an amazing place to b 6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured. 7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option. 8. Select Web Application as the Application Type. -9. Under Authorized redirect URI's, select Add URI and then add the URI `/api/integrations/googlecalendar/callback` replacing CALENDSO URL with the URI at which your application runs. +9. Under Authorized redirect URI's, select Add URI and then add the URI `/api/integrations/googlecalendar/callback` replacing CALENDSO URL with the URI at which your application runs. 10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs. 11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key. ## Obtaining Microsoft Graph Client ID and Secret + 1. Open [Azure App Registration](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and select New registration 2. Name your application 3. Set **Who can use this application or access this API?** to **Accounts in any organizational directory (Any Azure AD directory - Multitenant)** @@ -209,6 +224,7 @@ Contributions are what make the open source community such an amazing place to b 6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attriubte ## Obtaining Zoom Client ID and Secret + 1. Open [Zoom Marketplace](https://marketplace.zoom.us/) and sign in with your Zoom account. 2. On the upper right, click "Develop" => "Build App". 3. On "OAuth", select "Create". @@ -217,37 +233,39 @@ Contributions are what make the open source community such an amazing place to b 6. De-select the option to publish the app on the Zoom App Marketplace. 7. Click "Create". 8. Now copy the Client ID and Client Secret to your .env file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields. -4. Set the Redirect URL for OAuth `/api/integrations/zoomvideo/callback` replacing CALENDSO URL with the URI at which your application runs. -5. Also add the redirect URL given above as a whitelist URL and enable "Subdomain check". Make sure, it says "saved" below the form. -7. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". Search for and check the following scopes: - 1. account:master - 2. account:read:admin - 3. account:write:admin - 4. meeting:master - 5. meeting:read:admin - 6. meeting:write:admin - 7. user:master - 8. user:read:admin - 9. user:write:admin -8. Click "Done". -9. You're good to go. Now you can easily add your Zoom integration in the Calendso settings. +9. Set the Redirect URL for OAuth `/api/integrations/zoomvideo/callback` replacing CALENDSO URL with the URI at which your application runs. +10. Also add the redirect URL given above as a whitelist URL and enable "Subdomain check". Make sure, it says "saved" below the form. +11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". Search for and check the following scopes: + 1. account:master + 2. account:read:admin + 3. account:write:admin + 4. meeting:master + 5. meeting:read:admin + 6. meeting:write:admin + 7. user:master + 8. user:read:admin + 9. user:write:admin +12. Click "Done". +13. You're good to go. Now you can easily add your Zoom integration in the Calendso settings. + ## License Distributed under the MIT License. See `LICENSE` for more information. + ## Acknowledgements Special thanks to these amazing projects which help power Calendso: [](https://vercel.com/?utm_source=calend-so&utm_campaign=oss) -* [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss) -* [Next.js](https://nextjs.org/) -* [Day.js](https://day.js.org/) -* [Tailwind CSS](https://tailwindcss.com/) -* [Prisma](https://prisma.io/) +- [Vercel](https://vercel.com/?utm_source=calend-so&utm_campaign=oss) +- [Next.js](https://nextjs.org/) +- [Day.js](https://day.js.org/) +- [Tailwind CSS](https://tailwindcss.com/) +- [Prisma](https://prisma.io/) [product-screenshot]: https://i.imgur.com/4yvFj2E.png From d3569978f568e1edc726be4b611fb55255783f6b Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Mon, 26 Jul 2021 22:08:53 +0200 Subject: [PATCH 12/16] added dark mode classes for custom input fields --- pages/[user]/book.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index 34430e30..c1e0aade 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -264,7 +264,7 @@ export default function Book(props: any): JSX.Element { {input.type !== EventTypeCustomInputType.Bool && ( )} @@ -284,7 +284,7 @@ export default function Book(props: any): JSX.Element { name={"custom_" + input.id} id={"custom_" + input.id} required={input.required} - className="shadow-sm dark:bg-gray-700 dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" + className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="" /> )} @@ -294,7 +294,7 @@ export default function Book(props: any): JSX.Element { name={"custom_" + input.id} id={"custom_" + input.id} required={input.required} - className="shadow-sm dark:bg-gray-700 dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" + className="shadow-sm dark:bg-gray-700 dark:text-white dark:border-gray-900 focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="" /> )} From e37c16a394d386b33163d3758eb6d70eb52bac96 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 27 Jul 2021 12:30:49 +0200 Subject: [PATCH 13/16] Removed sub-account scopes from README --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index dca19ff0..8771a842 100644 --- a/README.md +++ b/README.md @@ -236,15 +236,12 @@ Contributions are what make the open source community such an amazing place to b 9. Set the Redirect URL for OAuth `/api/integrations/zoomvideo/callback` replacing CALENDSO URL with the URI at which your application runs. 10. Also add the redirect URL given above as a whitelist URL and enable "Subdomain check". Make sure, it says "saved" below the form. 11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". Search for and check the following scopes: - 1. account:master - 2. account:read:admin - 3. account:write:admin - 4. meeting:master - 5. meeting:read:admin - 6. meeting:write:admin - 7. user:master - 8. user:read:admin - 9. user:write:admin + 1. account:read:admin + 2. account:write:admin + 3. meeting:read:admin + 4. meeting:write:admin + 5. user:read:admin + 6. user:write:admin 12. Click "Done". 13. You're good to go. Now you can easily add your Zoom integration in the Calendso settings. From a08e502d01ed6ec71337f61b0bcd7494f5f6de5c Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Tue, 27 Jul 2021 13:45:53 +0200 Subject: [PATCH 14/16] changed hardcoded email reminders to 10 vs 60 minutes. adding ability to change reminder time soon --- lib/calendarClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 6abc3ac5..9d3964b0 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -363,7 +363,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { attendees: event.attendees, reminders: { useDefault: false, - overrides: [{ method: "email", minutes: 60 }], + overrides: [{ method: "email", minutes: 10 }], }, }; @@ -410,7 +410,7 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { attendees: event.attendees, reminders: { useDefault: false, - overrides: [{ method: "email", minutes: 60 }], + overrides: [{ method: "email", minutes: 10 }], }, }; From ad8cc4e985c1337b791ffcd807b4bfd3eba70c4c Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 27 Jul 2021 23:17:07 +0000 Subject: [PATCH 15/16] Updated office365 button to UTC --- pages/success.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/success.tsx b/pages/success.tsx index 0f1bff05..4f95936e 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -183,9 +183,9 @@ export default function Success(props) { "https://outlook.office.com/calendar/0/deeplink/compose?body=" + props.eventType.description + "&enddt=" + - date.add(props.eventType.length, "minute").format() + + date.add(props.eventType.length, "minute").utc().format() + "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + - date.format() + + date.utc().format() + "&subject=" + eventName ) + (location ? "&location=" + location : "") From 18c96afc2bf682cc9790b6026bf1437810fdb7ee Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Wed, 28 Jul 2021 12:28:36 +0000 Subject: [PATCH 16/16] Double fix for o365/outlook buttons --- pages/success.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pages/success.tsx b/pages/success.tsx index 4f95936e..70a0a978 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -159,14 +159,14 @@ export default function Success(props) { "https://outlook.live.com/calendar/0/deeplink/compose?body=" + props.eventType.description + "&enddt=" + - date.add(props.eventType.length, "minute").format() + + date.add(props.eventType.length, "minute").utc().format() + "&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + - date.format() + + date.utc().format() + "&subject=" + eventName ) + (location ? "&location=" + location : "") }> - + - +