diff --git a/apps/web/package.json b/apps/web/package.json index 02ee405d0..3831a589d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,6 +44,7 @@ "@vercel/kv": "^2.0.0", "accept-language-parser": "^1.5.0", "autoprefixer": "^10.4.13", + "calendar-link": "^2.6.0", "class-variance-authority": "^0.7.0", "color-hash": "^2.0.2", "cookie": "^0.6.0", diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 889373a7b..fb859a246 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -263,5 +263,11 @@ "activePollCount": "{{activePollCount}} Live", "createPoll": "Create poll", "yearlyDiscount": "Save {amount}", - "yearlyBillingDescription": "per year" + "yearlyBillingDescription": "per year", + "addToCalendar": "Add to Calendar", + "microsoft365": "Microsoft 365", + "outlook": "Outlook", + "yahoo": "Yahoo", + "downloadICSFile": "Download ICS File", + "schedulateDate": "Scheduled Date" } diff --git a/apps/web/public/static/google-calendar.svg b/apps/web/public/static/google-calendar.svg new file mode 100644 index 000000000..c32c0c772 --- /dev/null +++ b/apps/web/public/static/google-calendar.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/static/microsoft-365.svg b/apps/web/public/static/microsoft-365.svg new file mode 100644 index 000000000..aa6e0f0e5 --- /dev/null +++ b/apps/web/public/static/microsoft-365.svg @@ -0,0 +1,46 @@ + + +Microsoft 365 logo (2022) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/public/static/outlook.svg b/apps/web/public/static/outlook.svg new file mode 100644 index 000000000..586f39ffb --- /dev/null +++ b/apps/web/public/static/outlook.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/public/static/yahoo.svg b/apps/web/public/static/yahoo.svg new file mode 100644 index 000000000..c3e68fe21 --- /dev/null +++ b/apps/web/public/static/yahoo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx b/apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx index 12ffbf49d..04681d726 100644 --- a/apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx +++ b/apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx @@ -7,9 +7,9 @@ import { EventCard } from "@/components/event-card"; import { PollFooter } from "@/components/poll/poll-footer"; import { PollHeader } from "@/components/poll/poll-header"; import { ResponsiveResults } from "@/components/poll/responsive-results"; +import { ScheduledEvent } from "@/components/poll/scheduled-event"; import { useTouchBeacon } from "@/components/poll/use-touch-beacon"; import { VotingForm } from "@/components/poll/voting-form"; -import { ScheduledEvent } from "@/components/scheduled-event"; import { Trans } from "@/components/trans"; import { useUser } from "@/components/user-provider"; import { usePoll } from "@/contexts/poll"; diff --git a/apps/web/src/app/[locale]/poll/[urlId]/admin-page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/admin-page.tsx index 3d4e4711a..52adbdae4 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/admin-page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/admin-page.tsx @@ -4,9 +4,9 @@ import { EventCard } from "@/components/event-card"; import { PollFooter } from "@/components/poll/poll-footer"; import { PollHeader } from "@/components/poll/poll-header"; import { ResponsiveResults } from "@/components/poll/responsive-results"; +import { ScheduledEvent } from "@/components/poll/scheduled-event"; import { useTouchBeacon } from "@/components/poll/use-touch-beacon"; import { VotingForm } from "@/components/poll/voting-form"; -import { ScheduledEvent } from "@/components/scheduled-event"; import { GuestPollAlert } from "./guest-poll-alert"; diff --git a/apps/web/src/components/add-to-calendar-button.tsx b/apps/web/src/components/add-to-calendar-button.tsx new file mode 100644 index 000000000..83a5e8e3d --- /dev/null +++ b/apps/web/src/components/add-to-calendar-button.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { Button } from "@rallly/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@rallly/ui/dropdown-menu"; +import { Icon } from "@rallly/ui/icon"; +import { + CalendarEvent, + google, + ics, + office365, + outlook, + yahoo, +} from "calendar-link"; +import { DownloadIcon, PlusIcon } from "lucide-react"; +import Image from "next/image"; + +import { Trans } from "@/components/trans"; + +export function AddToCalendarButton({ + title, + description, + location, + start, + duration, + organizer, + guests, +}: { + title: string; + description?: string; + location?: string; + start: Date; + duration: number; + organizer?: { + name: string; + email: string; + }; + guests?: string[]; +}) { + const calendarEvent: CalendarEvent = { + title, + description, + start, + allDay: duration === 0, + duration: duration > 0 ? [duration, "minutes"] : undefined, + location, + organizer, + guests, + busy: true, + }; + + return ( + + + + + + { + const res = google(calendarEvent); + window.open(res, "_blank"); + }} + > + Google Calendar + Google Calendar + + { + const res = office365(calendarEvent); + window.open(res, "_blank"); + }} + > + Microsoft 365 + + + { + const res = outlook(calendarEvent); + window.open(res, "_blank"); + }} + > + Outlook + + + { + const res = yahoo(calendarEvent); + window.open(res, "_blank"); + }} + > + Yahoo + + + + + + + + + + + + ); +} diff --git a/apps/web/src/components/poll/scheduled-event.tsx b/apps/web/src/components/poll/scheduled-event.tsx new file mode 100644 index 000000000..3c42b2aa4 --- /dev/null +++ b/apps/web/src/components/poll/scheduled-event.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { AddToCalendarButton } from "@/components/add-to-calendar-button"; +import { ParticipantAvatarBar } from "@/components/participant-avatar-bar"; +import { useVisibleParticipants } from "@/components/participants-provider"; +import { Trans } from "@/components/trans"; +import { IfParticipantsVisible } from "@/components/visibility"; +import { usePoll } from "@/contexts/poll"; +import { useDayjs } from "@/utils/dayjs"; + +function FinalDate({ start }: { start: Date }) { + const poll = usePoll(); + const { adjustTimeZone } = useDayjs(); + return {adjustTimeZone(start, !poll.timeZone).format("LL")}; +} + +function DateIcon({ start }: { start: Date }) { + const poll = usePoll(); + const { adjustTimeZone } = useDayjs(); + const d = adjustTimeZone(start, !poll.timeZone); + return ( + + ); +} + +function FinalTime({ start, duration }: { start: Date; duration: number }) { + const poll = usePoll(); + const { adjustTimeZone, dayjs } = useDayjs(); + if (duration === 0) { + return ; + } + return ( + {`${adjustTimeZone(start, !poll.timeZone).format("LT")} - ${adjustTimeZone(dayjs(start).add(duration, "minutes"), !poll.timeZone).format(poll.timeZone ? "LT z" : "LT")}`} + ); +} + +function useAttendees() { + const participants = useVisibleParticipants(); + const poll = usePoll(); + return participants.filter((participant) => + participant.votes.some( + (vote) => + vote.optionId === poll?.event?.optionId && + (vote.type === "yes" || vote.type === "ifNeedBe"), + ), + ); +} + +function Attendees() { + const attendees = useAttendees(); + + return ; +} + +export function ScheduledEvent() { + const poll = usePoll(); + const { event } = poll; + + const attendees = useAttendees(); + + if (!event) { + return null; + } + + return ( + <> +
+
+

+ +

+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ + + + !!participant.email) + .map((participant) => participant.email!)} + /> +
+
+
+
+ + ); +} diff --git a/apps/web/src/components/scheduled-event.tsx b/apps/web/src/components/scheduled-event.tsx deleted file mode 100644 index 70ea42c18..000000000 --- a/apps/web/src/components/scheduled-event.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import { Card, CardContent } from "@rallly/ui/card"; - -import { DateIconInner } from "@/components/date-icon"; -import { ParticipantAvatarBar } from "@/components/participant-avatar-bar"; -import { useParticipants } from "@/components/participants-provider"; -import { Trans } from "@/components/trans"; -import { IfParticipantsVisible } from "@/components/visibility"; -import { usePoll } from "@/contexts/poll"; -import { useDayjs } from "@/utils/dayjs"; - -function FinalDate({ start }: { start: Date }) { - const poll = usePoll(); - const { adjustTimeZone } = useDayjs(); - return {adjustTimeZone(start, !poll.timeZone).format("LL")}; -} - -function DateIcon({ start }: { start: Date }) { - const poll = usePoll(); - const { adjustTimeZone } = useDayjs(); - const d = adjustTimeZone(start, !poll.timeZone); - return ; -} - -function FinalTime({ start, duration }: { start: Date; duration: number }) { - const poll = usePoll(); - const { adjustTimeZone, dayjs } = useDayjs(); - if (duration === 0) { - return ; - } - return ( - {`${adjustTimeZone(start, !poll.timeZone).format("LT")} - ${adjustTimeZone(dayjs(start).add(duration, "minutes"), !poll.timeZone).format(poll.timeZone ? "LT z" : "LT")}`} - ); -} - -function Attendees() { - const { participants } = useParticipants(); - const poll = usePoll(); - const attendees = participants.filter((participant) => - participant.votes.some( - (vote) => - vote.optionId === poll?.event?.optionId && - (vote.type === "yes" || vote.type === "ifNeedBe"), - ), - ); - - return ; -} - -export function ScheduledEvent() { - const poll = usePoll(); - if (!poll.event) { - return null; - } - return ( - - -
-
- -
-
- -
-
- -
-
-
-
- - - -
-
-
-
- ); -} diff --git a/packages/ui/src/dropdown-menu.tsx b/packages/ui/src/dropdown-menu.tsx index ade17a406..1507868bd 100644 --- a/packages/ui/src/dropdown-menu.tsx +++ b/packages/ui/src/dropdown-menu.tsx @@ -96,7 +96,7 @@ const DropdownMenuItem = React.forwardRef< ref={ref} className={cn( "flex items-center gap-x-2.5", - "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded px-2 py-1.5 text-sm font-medium outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", className, )} diff --git a/yarn.lock b/yarn.lock index 5ab018ce6..7e63d592c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6811,6 +6811,14 @@ cac@^6.7.14: resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== +calendar-link@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/calendar-link/-/calendar-link-2.6.0.tgz#47cec3d9a2d0c13ac87f732898867e92ea423c98" + integrity sha512-ypgYoFBz2w0WkJV1m6LoO31i8F5te3/rsTdJp/ONacVmr+C4Ny7rHhvwmIZS3yrbG2abbcohn2D8DZbcFZvwfA== + dependencies: + dayjs "^1.9.3" + query-string "^6.13.6" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" @@ -7395,6 +7403,11 @@ dayjs@^1.11.7: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== +dayjs@^1.9.3: + version "1.11.12" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.12.tgz#5245226cc7f40a15bf52e0b99fd2a04669ccac1d" + integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg== + debounce@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/debounce/-/debounce-2.0.0.tgz#b2f914518a1481466f4edaee0b063e4d473ad549" @@ -7441,6 +7454,11 @@ decode-named-character-reference@^1.0.0: dependencies: character-entities "^2.0.0" +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" @@ -8535,6 +8553,11 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + find-up@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" @@ -12050,6 +12073,16 @@ qs@^6.11.0: dependencies: side-channel "^1.0.4" +query-string@^6.13.6: + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -13052,6 +13085,11 @@ spdx-license-ids@^3.0.0: resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz" integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" @@ -13123,6 +13161,11 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"