diff --git a/apps/landing/src/components/home/hero.tsx b/apps/landing/src/components/home/hero.tsx index d8b9bd125..6b58a4feb 100644 --- a/apps/landing/src/components/home/hero.tsx +++ b/apps/landing/src/components/home/hero.tsx @@ -85,9 +85,9 @@ export const MarketingHero = ({ - + diff --git a/apps/landing/src/components/layouts/page-layout/footer.tsx b/apps/landing/src/components/layouts/page-layout/footer.tsx index 665e5df9a..588e01cea 100644 --- a/apps/landing/src/components/layouts/page-layout/footer.tsx +++ b/apps/landing/src/components/layouts/page-layout/footer.tsx @@ -1,5 +1,6 @@ import { DiscordIcon } from "@rallly/icons"; import languages from "@rallly/languages"; +import { Button } from "@rallly/ui/button"; import { Select, SelectContent, @@ -36,8 +37,10 @@ export const LanguageSelect = () => { }); }} > - - + + {Object.entries(languages).map(([code, name]) => ( diff --git a/apps/web/public/locales/en/app.json b/apps/web/public/locales/en/app.json index 30d18c2d8..d31396ae4 100644 --- a/apps/web/public/locales/en/app.json +++ b/apps/web/public/locales/en/app.json @@ -94,12 +94,10 @@ "errors_notFoundDescription": "We couldn't find the page you're looking for.", "errors_goToHome": "Go to home", "optionCount": "{count, plural, one {# option} other {# options}}", - "participantCount": "{count, plural, one {# participant} other {# participants}}", "addComment": "Add Comment", "profile": "Profile", "polls": "Polls", "showMore": "Show more…", - "timeZoneSelect__defaultValue": "Select time zone…", "timeZoneSelect__noOption": "No option found", "timeZoneSelect__inputPlaceholder": "Search…", "poweredByRallly": "Powered by {name}", @@ -115,13 +113,11 @@ "billing": "Billing", "guestPollAlertDescription": "<0>Create an account or <1>login to claim this poll.", "guestPollAlertTitle": "Your administrator rights can be lost if you clear your cookies", - "attendeeCount": "{count, plural, one {# attendee} other {# attendees}}", "notificationsValue": "Notifications: {value}", "notificationsOn": "On", "notificationsOff": "Off", "pollStatusOpen": "Live", "pollStatusPaused": "Paused", - "pollStatusClosed": "Finalized", "reopenPoll": "Reopen Poll", "resumePoll": "Resume", "pausePoll": "Pause", @@ -131,7 +127,6 @@ "permissionDenied": "Unauthorized", "permissionDeniedDescription": "If you are the poll creator, please login to access your poll", "loginDifferent": "Switch user", - "timeShownIn": "Times shown in {timeZone}", "editDetailsDescription": "Change the details of your event.", "finalizeDescription": "Select a final date for your event.", "notificationsGuestTooltip": "Create an account or login to turn on notifications", @@ -220,11 +215,7 @@ "integrations": "Integrations", "contacts": "Contacts", "unlockFeatures": "Unlock all Pro features.", - "pollStatusAll": "All", - "pollStatusLive": "Live", "pollStatusFinalized": "Finalized", - "pending": "Pending", - "xMore": "{count} more", "share": "Share", "pageXOfY": "Page {currentPage} of {pageCount}", "noParticipants": "No participants", @@ -241,5 +232,19 @@ "accountNotLinkedDescription": "A user with this email already exists. Please log in using the original method.", "or": "Or", "autoTimeZone": "Automatic Time Zone Conversion", - "autoTimeZoneHelp": "Enable this setting to automatically adjust event times to each participant's local time zone." + "autoTimeZoneHelp": "Enable this setting to automatically adjust event times to each participant's local time zone.", + "commentsDisabled": "Comments have been disabled", + "allParticipants": "All Participants", + "host": "Host", + "created": "Created", + "pollStatus": "Status", + "pollsListAll": "All", + "pollsListMine": "Mine", + "pollsListOther": "Other", + "noParticipantsDescription": "Click Share to invite participants", + "back": "Back", + "timeShownIn": "Times shown in {timeZone}", + "pollStatusPausedDescription": "Votes cannot be submitted or edited at this time", + "eventHostTitle": "Manage Access", + "eventHostDescription": "You are the creator of this poll" } diff --git a/apps/web/src/app/[locale]/(admin)/layout.tsx b/apps/web/src/app/[locale]/(admin)/layout.tsx index 1067cc59f..b68bd73f8 100644 --- a/apps/web/src/app/[locale]/(admin)/layout.tsx +++ b/apps/web/src/app/[locale]/(admin)/layout.tsx @@ -1,59 +1,31 @@ import { cn } from "@rallly/ui"; -import { Button } from "@rallly/ui/button"; -import { MenuIcon } from "lucide-react"; -import Link from "next/link"; import React from "react"; +import { MobileNavigation } from "@/app/[locale]/(admin)/mobile-navigation"; import { Sidebar } from "@/app/[locale]/(admin)/sidebar"; import { LogoLink } from "@/app/components/logo-link"; -import { CurrentUserAvatar } from "@/components/user"; - -function MobileNavigation() { - return ( -
- -
- - - - -
-
- ); -} export default async function Layout({ children, }: { children: React.ReactNode; }) { - function SidebarLayout() { - return ( -
- - -
- {children} + return ( +
+ + - ); - } - - return ; +
+ {children} +
+
+ ); } diff --git a/apps/web/src/app/[locale]/(admin)/menu/menu-button.tsx b/apps/web/src/app/[locale]/(admin)/menu/menu-button.tsx new file mode 100644 index 000000000..e7da5d246 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/menu/menu-button.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { Button } from "@rallly/ui/button"; +import { Icon } from "@rallly/ui/icon"; +import { MenuIcon, XIcon } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export function BackButton() { + const router = useRouter(); + return ( + + ); +} + +export function MobileMenuButton({ open }: { open?: boolean }) { + if (open) { + return ; + } + + return ( + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/menu/page.tsx b/apps/web/src/app/[locale]/(admin)/menu/page.tsx new file mode 100644 index 000000000..c1f69cc82 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/menu/page.tsx @@ -0,0 +1,9 @@ +import { Sidebar } from "@/app/[locale]/(admin)/sidebar"; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/apps/web/src/app/[locale]/(admin)/mobile-navigation.tsx b/apps/web/src/app/[locale]/(admin)/mobile-navigation.tsx new file mode 100644 index 000000000..a2890c7ff --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/mobile-navigation.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Button } from "@rallly/ui/button"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { MobileMenuButton } from "@/app/[locale]/(admin)/menu/menu-button"; +import { CurrentUserAvatar } from "@/components/user"; + +export function MobileNavigation() { + const pathname = usePathname(); + + const isOpen = pathname === "/menu"; + + return ( +
+ +
+ +
+
+ ); +} diff --git a/apps/web/src/app/[locale]/(admin)/new/page.tsx b/apps/web/src/app/[locale]/(admin)/new/page.tsx index a920f9bbf..f37b7f5fe 100644 --- a/apps/web/src/app/[locale]/(admin)/new/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/new/page.tsx @@ -1,31 +1,31 @@ import { Button } from "@rallly/ui/button"; +import { Icon } from "@rallly/ui/icon"; +import { ArrowLeftIcon } from "lucide-react"; import Link from "next/link"; import { Trans } from "react-i18next/TransWithoutContext"; +import { Params } from "@/app/[locale]/types"; import { PageContainer, PageContent, PageHeader, - PageTitle, } from "@/app/components/page-layout"; import { getTranslation } from "@/app/i18n"; import { CreatePoll } from "@/components/create-poll"; -export default async function Page({ params }: { params: { locale: string } }) { +export default async function Page({ params }: { params: Params }) { const { t } = await getTranslation(params.locale); return ( -
- - - - -
+
diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/columns.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/columns.tsx new file mode 100644 index 000000000..15715943a --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/columns.tsx @@ -0,0 +1,139 @@ +import { PollStatus } from "@rallly/database"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip"; +import { createColumnHelper } from "@tanstack/react-table"; +import dayjs from "dayjs"; +import Link from "next/link"; +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { PollStatusBadge } from "@/components/poll-status"; +import { Trans } from "@/components/trans"; +import { UserAvatar } from "@/components/user"; +import { useUser } from "@/components/user-provider"; +import { useDayjs } from "@/utils/dayjs"; + +export type PollData = { + id: string; + status: PollStatus; + title: string; + createdAt: Date; + participants: { name: string }[]; + timeZone: string | null; + userId: string; + user: { + name: string; + id: string; + } | null; + event: { + start: Date; + duration: number; + } | null; +}; + +const columnHelper = createColumnHelper(); + +export const usePollColumns = () => { + const { t } = useTranslation(); + const { adjustTimeZone } = useDayjs(); + const { user } = useUser(); + return React.useMemo( + () => [ + columnHelper.accessor("title", { + id: "title", + header: t("title"), + size: 1000, + cell: ({ row }) => { + return ( + + + {row.original.title} + + + ); + }, + }), + columnHelper.accessor("user", { + header: () => ( +
+ {t("host", { defaultValue: "Host" })} +
+ ), + size: 75, + cell: ({ getValue }) => { + const isYou = getValue()?.id === user.id; + return ( +
+ + + + + + {isYou ? t("you") : getValue()?.name ?? t("guest")} + + +
+ ); + }, + }), + columnHelper.accessor("createdAt", { + header: () => , + cell: ({ row }) => { + const { createdAt } = row.original; + return ( +

+ +

+ ); + }, + }), + columnHelper.accessor("status", { + header: t("pollStatus", { defaultValue: "Status" }), + cell: ({ row }) => { + return ( +
+ {row.original.event ? ( + + + + + + {adjustTimeZone( + row.original.event.start, + !row.original.timeZone, + ).format("LLLL")} + + + ) : ( + + )} +
+ ); + }, + }), + + columnHelper.accessor("participants", { + header: () => null, + cell: ({ row }) => { + if (row.original.userId !== user.id) { + return null; + } + + return ( + + + + ); + }, + }), + ], + [adjustTimeZone, t, user.id], + ); +}; diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/loading.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/loading.tsx new file mode 100644 index 000000000..351081a7f --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/loading.tsx @@ -0,0 +1,3 @@ +export default function Loader() { + return null; +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/page.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/page.tsx new file mode 100644 index 000000000..fc3dd94a5 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/page.tsx @@ -0,0 +1,23 @@ +import { PollsList } from "@/app/[locale]/(admin)/polls/[[...list]]/polls-list"; +import { Params } from "@/app/[locale]/types"; +import { getTranslation } from "@/app/i18n"; + +interface PageParams extends Params { + list?: string; +} + +export default async function Page({ params }: { params: PageParams }) { + const list = params.list ? params.list[0] : "all"; + return ; +} + +export async function generateMetadata({ + params, +}: { + params: { locale: string }; +}) { + const { t } = await getTranslation(params.locale); + return { + title: t("polls"), + }; +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-folders.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-folders.tsx new file mode 100644 index 000000000..e19e95bdb --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-folders.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { + ResponsiveMenu, + ResponsiveMenuItem, +} from "@/app/components/responsive-menu"; +import { Trans } from "@/components/trans"; + +export function PollFolders() { + return ( + + + + + + + + + + + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-list.tsx b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-list.tsx new file mode 100644 index 000000000..dad90ba98 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/[[...list]]/polls-list.tsx @@ -0,0 +1,138 @@ +"use client"; +import { Button } from "@rallly/ui/button"; +import { Card } from "@rallly/ui/card"; +import { Flex } from "@rallly/ui/flex"; +import { Icon } from "@rallly/ui/icon"; +import { PaginationState } from "@tanstack/react-table"; +import { BarChart2Icon, PlusIcon } from "lucide-react"; +import Link from "next/link"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { + EmptyState, + EmptyStateDescription, + EmptyStateFooter, + EmptyStateIcon, + EmptyStateTitle, +} from "@/app/components/empty-state"; +import { Spinner } from "@/components/spinner"; +import { Table } from "@/components/table"; +import { Trans } from "@/components/trans"; +import { trpc } from "@/utils/trpc/client"; + +import { PollData, usePollColumns } from "./columns"; + +function PollsEmptyState() { + const { t } = useTranslation(); + return ( + + + + + + {t("noPolls", { defaultValue: "No Polls" })} + + {t("noPollsDescription")} + + + + + ); +} + +export function PollsList({ list }: { list?: string }) { + const searchParams = useSearchParams(); + const pathname = usePathname(); + + const router = useRouter(); + const pagination = React.useMemo( + () => ({ + pageIndex: (Number(searchParams?.get("page")) || 1) - 1, + pageSize: Number(searchParams?.get("pageSize")) || 10, + }), + [searchParams], + ); + + // const sorting = React.useMemo(() => { + // const id = searchParams?.get("sort"); + // const desc = searchParams?.get("desc"); + // if (!id) { + // return [{ id: "createdAt", desc: true }]; + // } + // return [{ id, desc: desc === "desc" }]; + // }, [searchParams]); + + const { data, isFetching } = trpc.polls.paginatedList.useQuery( + { list, pagination }, + { + staleTime: Infinity, + cacheTime: Infinity, + keepPreviousData: true, + }, + ); + const columns = usePollColumns(); + + if (!data) { + // return a table using components + return ( + + + + ); + } + + return ( +
+ {data.total ? ( + + { + // const newSorting = + // typeof updater === "function" ? updater(sorting) : updater; + + // const current = new URLSearchParams(searchParams ?? undefined); + // const sortColumn = newSorting[0]; + // if (sortColumn === undefined) { + // current.delete("sort"); + // current.delete("desc"); + // } else { + // current.set("sort", sortColumn.id); + // current.set("desc", sortColumn.desc ? "desc" : "asc"); + // } + // // current.set("pageSize", String(newPagination.pageSize)); + // router.replace(`${pathname}?${current.toString()}`); + // }} + onPaginationChange={(updater) => { + const newPagination = + typeof updater === "function" ? updater(pagination) : updater; + + const current = new URLSearchParams(searchParams ?? undefined); + current.set("page", String(newPagination.pageIndex + 1)); + // current.set("pageSize", String(newPagination.pageSize)); + router.replace(`${pathname}?${current.toString()}`); + }} + columns={columns} + /> + + ) : ( + + )} + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/polls/layout.tsx b/apps/web/src/app/[locale]/(admin)/polls/layout.tsx index 80f2fd334..245d57936 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/layout.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/layout.tsx @@ -1,8 +1,10 @@ import { Button } from "@rallly/ui/button"; -import { PenBoxIcon } from "lucide-react"; +import { Icon } from "@rallly/ui/icon"; +import { PlusIcon } from "lucide-react"; import Link from "next/link"; -import { Trans } from "react-i18next/TransWithoutContext"; +import { PollFolders } from "@/app/[locale]/(admin)/polls/[[...list]]/polls-folders"; +import { Params } from "@/app/[locale]/types"; import { PageContainer, PageContent, @@ -11,43 +13,36 @@ import { } from "@/app/components/page-layout"; import { getTranslation } from "@/app/i18n"; +interface PageParams extends Params { + list?: string; +} + export default async function Layout({ - params, children, + params, }: { - children: React.ReactNode; - params: { locale: string }; + children?: React.ReactNode; + params: PageParams; }) { const { t } = await getTranslation(params.locale); return ( -
- - - -
- {children} + + + {children} +
); } - -export async function generateMetadata({ - params, -}: { - params: { locale: string }; -}) { - const { t } = await getTranslation(params.locale); - return { - title: t("polls"), - }; -} diff --git a/apps/web/src/app/[locale]/(admin)/polls/loading.tsx b/apps/web/src/app/[locale]/(admin)/polls/loading.tsx deleted file mode 100644 index 0f0d31d89..000000000 --- a/apps/web/src/app/[locale]/(admin)/polls/loading.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Skeleton } from "@/components/skeleton"; - -function Row() { - return ( -
-
- - -
-
- -
-
- -
-
- -
-
- ); -} -export default function Loader() { - return ( -
- - - - -
- ); -} diff --git a/apps/web/src/app/[locale]/(admin)/polls/page.tsx b/apps/web/src/app/[locale]/(admin)/polls/page.tsx deleted file mode 100644 index 44627743e..000000000 --- a/apps/web/src/app/[locale]/(admin)/polls/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { getTranslation } from "@/app/i18n"; - -import { PollsList } from "./polls-list"; - -export default async function Page() { - return ; -} - -export async function generateMetadata({ - params, -}: { - params: { locale: string }; -}) { - const { t } = await getTranslation(params.locale); - return { - title: t("polls"), - }; -} diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx deleted file mode 100644 index ed5053c89..000000000 --- a/apps/web/src/app/[locale]/(admin)/polls/polls-folders.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import { cn } from "@rallly/ui"; -import { Button } from "@rallly/ui/button"; -import Link from "next/link"; -import { usePathname, useSearchParams } from "next/navigation"; - -import { Trans } from "@/components/trans"; - -function PollFolder({ - href, - children, -}: { - href: string; - children: React.ReactNode; -}) { - const pathname = usePathname() ?? ""; - const searchParams = useSearchParams(); - const query = searchParams?.has("status") - ? `?${searchParams?.toString()}` - : ""; - const currentUrl = pathname + query; - const isActive = href === currentUrl; - return ( - - ); -} - -export function PollFolders() { - return ( -
- - - - - - - - - - - - -
- ); -} diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx deleted file mode 100644 index f876f7f50..000000000 --- a/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx +++ /dev/null @@ -1,216 +0,0 @@ -"use client"; -import { PollStatus } from "@rallly/database"; -import { Button } from "@rallly/ui/button"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip"; -import { createColumnHelper, PaginationState } from "@tanstack/react-table"; -import dayjs from "dayjs"; -import { ArrowRightIcon, InboxIcon, PlusIcon, UsersIcon } from "lucide-react"; -import Link from "next/link"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import React from "react"; - -import { PollStatusBadge } from "@/components/poll-status"; -import { Table } from "@/components/table"; -import { Trans } from "@/components/trans"; -import { useDayjs } from "@/utils/dayjs"; -import { trpc } from "@/utils/trpc/client"; - -import Loader from "./loading"; - -const EmptyState = () => { - return ( -
-
-
- -
-

- -

-

- -

-
- -
-
-
- ); -}; - -type Column = { - id: string; - status: PollStatus; - title: string; - createdAt: Date; - participants: { name: string }[]; - timeZone: string | null; - event: { - start: Date; - duration: number; - } | null; -}; - -const columnHelper = createColumnHelper(); - -export function PollsList() { - const searchParams = useSearchParams(); - const router = useRouter(); - const pathname = usePathname(); - const pagination = React.useMemo( - () => ({ - pageIndex: (Number(searchParams?.get("page")) || 1) - 1, - pageSize: Number(searchParams?.get("pageSize")) || 10, - }), - [searchParams], - ); - - const { data } = trpc.polls.paginatedList.useQuery({ pagination }); - const { adjustTimeZone } = useDayjs(); - const columns = React.useMemo( - () => [ - columnHelper.display({ - id: "title", - header: () => null, - size: 5000, - cell: ({ row }) => { - return ( - -
-

- {row.original.title} -

- -
- {row.original.event ? ( -

- {row.original.event.duration === 0 - ? adjustTimeZone( - row.original.event.start, - !row.original.timeZone, - ).format("LL") - : `${adjustTimeZone( - row.original.event.start, - !row.original.timeZone, - ).format("LL LT")} - ${adjustTimeZone( - dayjs(row.original.event.start).add( - row.original.event.duration, - "minutes", - ), - !row.original.timeZone, - ).format("LT")}`} -

- ) : ( -

- -

- )} - - ); - }, - }), - columnHelper.accessor("status", { - header: () => null, - size: 200, - cell: ({ row }) => { - return ( -
- -
- ); - }, - }), - columnHelper.accessor("createdAt", { - header: () => null, - size: 1000, - cell: ({ row }) => { - const { createdAt } = row.original; - return ( -

- -

- ); - }, - }), - columnHelper.accessor("participants", { - header: () => null, - cell: ({ row }) => { - return ( - - - - - {row.original.participants.length} - - - - {row.original.participants.length > 0 ? ( - <> - {row.original.participants - .slice(0, 10) - .map((participant, i) => ( -

{participant.name}

- ))} - {row.original.participants.length > 10 ? ( -

- -

- ) : null} - - ) : ( - - )} -
-
- ); - }, - }), - ], - [adjustTimeZone], - ); - - if (!data) { - // return a table using components - return ; - } - - if (data.total === 0) return ; - - return ( -
{ - const newPagination = - typeof updater === "function" ? updater(pagination) : updater; - - const current = new URLSearchParams(searchParams ?? undefined); - current.set("page", String(newPagination.pageIndex + 1)); - // current.set("pageSize", String(newPagination.pageSize)); - router.push(`${pathname}?${current.toString()}`); - }} - columns={columns} - /> - ); -} diff --git a/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx b/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx index 69dfefd29..296da19eb 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/billing/billing-page.tsx @@ -67,16 +67,21 @@ const SubscriptionStatus = () => { return (
{!data.active ? ( -
- - -
+ ) : data.legacy ? ( ) : ( - + } + description={ + + } + > + + )}
); @@ -242,17 +247,7 @@ export function BillingPage() { {t("billing")} - } - description={ - - } - > - - +
} diff --git a/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx b/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx index 8db46bf46..5c20e45c1 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/billing/page.tsx @@ -1,8 +1,9 @@ -import { BillingPage } from "@/app/[locale]/(admin)/settings/billing/billing-page"; import { Params } from "@/app/[locale]/types"; import { getTranslation } from "@/app/i18n"; -export default function Page() { +import { BillingPage } from "./billing-page"; + +export default async function Page() { return ; } diff --git a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx index 255920bba..9fe77296d 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/layout.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/layout.tsx @@ -1,6 +1,4 @@ -import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react"; import React from "react"; -import { Trans } from "react-i18next/TransWithoutContext"; import { PageContainer, @@ -9,9 +7,8 @@ import { PageTitle, } from "@/app/components/page-layout"; import { getTranslation } from "@/app/i18n"; -import { isSelfHosted } from "@/utils/constants"; -import { SettingsMenu } from "./menu-item"; +import { SettingsMenu } from "./settings-menu"; export default async function ProfileLayout({ children, @@ -20,41 +17,16 @@ export default async function ProfileLayout({ params: { locale: string }; }>) { const { t } = await getTranslation(params.locale); - const menuItems = [ - { - title: t("profile"), - href: "/settings/profile", - icon: UserIcon, - }, - { - title: t("preferences"), - href: "/settings/preferences", - icon: Settings2Icon, - }, - ]; - - if (!isSelfHosted) { - menuItems.push({ - title: t("billing"), - href: "/settings/billing", - icon: CreditCardIcon, - }); - } - return ( -
- - - -
+ {t("settings")}
- +
-
{children}
+
{children}
); diff --git a/apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx b/apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx deleted file mode 100644 index c879ef072..000000000 --- a/apps/web/src/app/[locale]/(admin)/settings/menu-item.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@rallly/ui/select"; -import clsx from "clsx"; -import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { isSelfHosted } from "@/utils/constants"; - -export function MenuItem(props: { href: string; children: React.ReactNode }) { - const pathname = usePathname(); - return ( - - {props.children} - - ); -} - -export function SettingsMenu() { - const { t } = useTranslation(); - const pathname = usePathname(); - const menuItems = React.useMemo(() => { - const items = [ - { - title: t("profile"), - href: "/settings/profile", - icon: UserIcon, - }, - { - title: t("preferences"), - href: "/settings/preferences", - icon: Settings2Icon, - }, - ]; - if (!isSelfHosted) { - items.push({ - title: t("billing"), - href: "/settings/billing", - icon: CreditCardIcon, - }); - } - return items; - }, [t]); - - const router = useRouter(); - const value = React.useMemo( - () => menuItems.find((item) => item.href === pathname), - [menuItems, pathname], - ); - - return ( - <> -
- {menuItems.map((item, i) => ( - - - {item.title} - - ))} -
- - - ); -} diff --git a/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx b/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx index 897bf68b7..377df6db4 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/preferences/page.tsx @@ -1,8 +1,9 @@ -import { PreferencesPage } from "@/app/[locale]/(admin)/settings/preferences/preferences-page"; import { Params } from "@/app/[locale]/types"; import { getTranslation } from "@/app/i18n"; -export default function Page() { +import { PreferencesPage } from "./preferences-page"; + +export default async function Page() { return ; } diff --git a/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx b/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx index a58539380..30a7eaa6c 100644 --- a/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/settings/profile/page.tsx @@ -1,8 +1,9 @@ -import { ProfilePage } from "@/app/[locale]/(admin)/settings/profile/profile-page"; import { Params } from "@/app/[locale]/types"; import { getTranslation } from "@/app/i18n"; -export default function Page() { +import { ProfilePage } from "./profile-page"; + +export default async function Page() { return ; } diff --git a/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx b/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx new file mode 100644 index 000000000..17edc69e4 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/settings/settings-menu.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Icon } from "@rallly/ui/icon"; +import { CreditCardIcon, Settings2Icon, UserIcon } from "lucide-react"; +import { Trans } from "react-i18next"; + +import { + ResponsiveMenu, + ResponsiveMenuItem, +} from "@/app/components/responsive-menu"; +import { IfCloudHosted } from "@/contexts/environment"; + +export function SettingsMenu() { + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/app/[locale]/(admin)/sidebar.tsx b/apps/web/src/app/[locale]/(admin)/sidebar.tsx index 03dfd40b2..cdb2bd4b9 100644 --- a/apps/web/src/app/[locale]/(admin)/sidebar.tsx +++ b/apps/web/src/app/[locale]/(admin)/sidebar.tsx @@ -2,16 +2,20 @@ import { cn } from "@rallly/ui"; import { Button } from "@rallly/ui/button"; +import { Icon } from "@rallly/ui/icon"; import { + ArrowUpRightIcon, + BarChart2Icon, BlocksIcon, BookMarkedIcon, CalendarIcon, ChevronRightIcon, + LifeBuoyIcon, LogInIcon, + PlusIcon, Settings2Icon, SparklesIcon, UsersIcon, - VoteIcon, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -26,10 +30,12 @@ import { IconComponent } from "@/types"; function NavItem({ href, children, + target, icon: Icon, current, }: { href: string; + target?: string; icon: IconComponent; children: React.ReactNode; current?: boolean; @@ -37,11 +43,12 @@ function NavItem({ return (
  • -
      +
      +
    • + +
    • -
        +
        • @@ -97,7 +114,7 @@ export function Sidebar() {
      • -
          +
          • +
          • + + + + + + +
          • @@ -134,7 +163,7 @@ export function Sidebar() {

          -
            +
            • - -
              - -
              +
              + + + +
              +

              + +

              +

              + +

              - + + + + + + +
              ); }; export function InvitePage() { + useTouchBeacon(); return ( - - - - -
              -
              -
              - -
              -
              -
              -
              -
              -
              +
              + + + + + + + + + +
              ); } diff --git a/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx b/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx new file mode 100644 index 000000000..c7a3cdf49 --- /dev/null +++ b/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx @@ -0,0 +1,57 @@ +"use client"; +import { useParams, useSearchParams } from "next/navigation"; +import React from "react"; + +import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider"; +import { VisibilityProvider } from "@/components/visibility"; +import { PermissionsContext } from "@/contexts/permissions"; +import { trpc } from "@/utils/trpc/client"; + +import Loader from "./loading"; + +const Prefetch = ({ children }: React.PropsWithChildren) => { + const searchParams = useSearchParams(); + const token = searchParams?.get("token") as string; + const params = useParams<{ urlId: string }>(); + const urlId = params?.urlId as string; + const { data: permission } = trpc.auth.getUserPermission.useQuery( + { token }, + { + enabled: !!token, + }, + ); + + const { data: poll, error } = trpc.polls.get.useQuery( + { urlId }, + { + retry: false, + }, + ); + + const { data: participants } = trpc.polls.participants.list.useQuery({ + pollId: urlId, + }); + + if (error?.data?.code === "NOT_FOUND") { + return
              Not found
              ; + } + if (!poll || !participants) { + return ; + } + + return ( + + {children} + + ); +}; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/apps/web/src/app/[locale]/invite/[urlId]/loading.tsx b/apps/web/src/app/[locale]/invite/[urlId]/loading.tsx index 9b0ec733f..4349ac3a6 100644 --- a/apps/web/src/app/[locale]/invite/[urlId]/loading.tsx +++ b/apps/web/src/app/[locale]/invite/[urlId]/loading.tsx @@ -1,12 +1,3 @@ -import { PageContainer, PageHeader } from "@/app/components/page-layout"; -import { Skeleton } from "@/components/skeleton"; - export default function Loading() { - return ( - - - - - - ); + return null; } diff --git a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx index 954600af8..d57b7bcc2 100644 --- a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx +++ b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx @@ -3,16 +3,11 @@ import { Metadata } from "next"; import { notFound } from "next/navigation"; import { InvitePage } from "@/app/[locale]/invite/[urlId]/invite-page"; -import { PageContainer } from "@/app/components/page-layout"; import { getTranslation } from "@/app/i18n"; import { absoluteUrl } from "@/utils/absolute-url"; export default async function Page() { - return ( - - - - ); + return ; } export async function generateMetadata({ diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx index 8f8d61884..2ed4acd75 100644 --- a/apps/web/src/app/[locale]/layout.tsx +++ b/apps/web/src/app/[locale]/layout.tsx @@ -3,6 +3,7 @@ import "../../style.css"; import languages from "@rallly/languages"; import { Toaster } from "@rallly/ui/toaster"; +import { Viewport } from "next"; import { Inter } from "next/font/google"; import React from "react"; @@ -17,6 +18,13 @@ export async function generateStaticParams() { return Object.keys(languages).map((locale) => ({ locale })); } +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + export default function Root({ children, params: { locale }, diff --git a/apps/web/src/app/[locale]/menu/back-button.tsx b/apps/web/src/app/[locale]/menu/back-button.tsx deleted file mode 100644 index d7faef921..000000000 --- a/apps/web/src/app/[locale]/menu/back-button.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -import { Button } from "@rallly/ui/button"; -import { XIcon } from "lucide-react"; -import { useRouter } from "next/navigation"; - -export function BackButton() { - const router = useRouter(); - return ( - - ); -} diff --git a/apps/web/src/app/[locale]/menu/page.tsx b/apps/web/src/app/[locale]/menu/page.tsx deleted file mode 100644 index 570f9261c..000000000 --- a/apps/web/src/app/[locale]/menu/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; - -import { Sidebar } from "@/app/[locale]/(admin)/sidebar"; -import { BackButton } from "@/app/[locale]/menu/back-button"; - -export default function Page() { - return ( -
              -
              - - - - -
              -
              - -
              -
              - ); -} diff --git a/apps/web/src/app/[locale]/poll/[urlId]/admin-page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/admin-page.tsx new file mode 100644 index 000000000..3d4e4711a --- /dev/null +++ b/apps/web/src/app/[locale]/poll/[urlId]/admin-page.tsx @@ -0,0 +1,28 @@ +"use client"; +import Discussion from "@/components/discussion"; +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 { 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"; + +export function AdminPage() { + useTouchBeacon(); + return ( +
              + + + + + + + + + +
              + ); +} diff --git a/apps/web/src/app/[locale]/poll/[urlId]/duplicate/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/duplicate/page.tsx index 97f05078e..99660b234 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/duplicate/page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/duplicate/page.tsx @@ -51,7 +51,6 @@ const Page: NextPageWithLayout = () => {
              { //submit duplicate.mutate( @@ -90,7 +89,7 @@ const Page: NextPageWithLayout = () => { - + { return ( { //submit updatePollMutation( diff --git a/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx index e4ba24e2c..826f88610 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/edit-options/page.tsx @@ -86,7 +86,6 @@ const Page = () => { return ( { const encodedOptions = data.options.map(encodeDateOption); const optionsToDelete = poll.options.filter((option) => { diff --git a/apps/web/src/app/[locale]/poll/[urlId]/edit-settings/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/edit-settings/page.tsx index b6ef51852..8c60d9d1e 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/edit-settings/page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/edit-settings/page.tsx @@ -39,7 +39,6 @@ const Page = () => { return ( { //submit await update.mutateAsync( diff --git a/apps/web/src/app/[locale]/poll/[urlId]/finalize/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/finalize/page.tsx index 3ffecaf78..4fe0c6e66 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/finalize/page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/finalize/page.tsx @@ -38,7 +38,7 @@ const FinalizationForm = () => { }); return ( - + diff --git a/apps/web/src/app/[locale]/poll/[urlId]/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/page.tsx index a6404bebd..070dcddb2 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/page.tsx @@ -1,16 +1,5 @@ -import { cn } from "@rallly/ui"; +import { AdminPage } from "@/app/[locale]/poll/[urlId]/admin-page"; -import { Poll } from "@/components/poll"; - -import { GuestPollAlert } from "./guest-poll-alert"; - -export default async function Page() { - return ( -
              -
              - - -
              -
              - ); +export default function Page() { + return ; } diff --git a/apps/web/src/app/components/empty-state.tsx b/apps/web/src/app/components/empty-state.tsx new file mode 100644 index 000000000..662197216 --- /dev/null +++ b/apps/web/src/app/components/empty-state.tsx @@ -0,0 +1,45 @@ +import { cn } from "@rallly/ui"; +import { Icon } from "@rallly/ui/icon"; + +export function EmptyState({ + children, + className, +}: { + children?: React.ReactNode; + className?: string; +}) { + return ( +
              + {children} +
              + ); +} + +export function EmptyStateIcon({ children }: { children: React.ReactNode }) { + return ( +
              + {children} +
              + ); +} + +export function EmptyStateTitle({ children }: { children: React.ReactNode }) { + return

              {children}

              ; +} + +export function EmptyStateDescription({ + children, +}: { + children: React.ReactNode; +}) { + return

              {children}

              ; +} + +export function EmptyStateFooter({ children }: { children: React.ReactNode }) { + return
              {children}
              ; +} diff --git a/apps/web/src/app/components/page-layout.tsx b/apps/web/src/app/components/page-layout.tsx index 821075456..a7b386f25 100644 --- a/apps/web/src/app/components/page-layout.tsx +++ b/apps/web/src/app/components/page-layout.tsx @@ -1,11 +1,35 @@ "use client"; import { cn } from "@rallly/ui"; +import { Icon } from "@rallly/ui/icon"; export function PageContainer({ children, className, }: React.PropsWithChildren<{ className?: string }>) { - return
              {children}
              ; + return ( +
              + {children} +
              + ); +} + +export function PageIcon({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
              + {children} +
              + ); } export function PageTitle({ @@ -16,7 +40,7 @@ export function PageTitle({ className?: string; }) { return ( -

              +

              {children}

              ); @@ -25,25 +49,12 @@ export function PageTitle({ export function PageHeader({ children, className, - variant = "default", }: { children?: React.ReactNode; className?: string; variant?: "default" | "ghost"; }) { - return ( -
              - {children} -
              - ); + return
              {children}
              ; } export function PageContent({ @@ -53,5 +64,5 @@ export function PageContent({ children?: React.ReactNode; className?: string; }) { - return
              {children}
              ; + return
              {children}
              ; } diff --git a/apps/web/src/app/components/responsive-menu.tsx b/apps/web/src/app/components/responsive-menu.tsx new file mode 100644 index 000000000..90bdf1081 --- /dev/null +++ b/apps/web/src/app/components/responsive-menu.tsx @@ -0,0 +1,118 @@ +"use client"; +import { cn } from "@rallly/ui"; +import { Button } from "@rallly/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@rallly/ui/select"; +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; +import React from "react"; + +import { useBreakpoint } from "@/utils/breakpoint"; + +const ResponsiveMenuContext = React.createContext<"desktop" | "mobile">( + "desktop", +); + +function DesktopMenuItem({ + href, + children, +}: { + href: string; + children: React.ReactNode; +}) { + const pathname = usePathname(); + return ( +
            • + + {children} + +
            • + ); +} + +function MobileMenuItem({ + href, + children, +}: { + href: string; + children: React.ReactNode; +}) { + return ( + +
              {children}
              +
              + ); +} + +export function ResponsiveMenuItem(props: { + href: string; + children: React.ReactNode; +}) { + const breakpoint = React.useContext(ResponsiveMenuContext); + + switch (breakpoint) { + case "desktop": + return ; + case "mobile": + return ; + } +} + +function DesktopMenu({ children }: { children: React.ReactNode }) { + return ( + +
                {children}
              +
              + ); +} + +function MobileMenu({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + + if (!pathname) { + return null; + } + + return ( + + + + ); +} + +export function ResponsiveMenu({ children }: { children?: React.ReactNode }) { + const breakpoint = useBreakpoint(); + + switch (breakpoint) { + case "desktop": + return {children}; + case "mobile": + return {children}; + } +} diff --git a/apps/web/src/components/billing/billing-plans.tsx b/apps/web/src/components/billing/billing-plans.tsx index 688539e90..c75f7260f 100644 --- a/apps/web/src/components/billing/billing-plans.tsx +++ b/apps/web/src/components/billing/billing-plans.tsx @@ -21,16 +21,18 @@ export const BillingPlans = () => { const [tab, setTab] = React.useState("yearly"); return ( -
              - - - - - - - - - + +
              +
              + + + + + + + + +
              @@ -69,7 +71,7 @@ export const BillingPlans = () => { -
              +
              @@ -126,26 +128,26 @@ export const BillingPlans = () => {
              - -
              -
              - -
              -
              -

              +
              +
              + +
              +
              +

              + +

              +
              +

              -

              +

              -

              - -

              -
              + ); }; diff --git a/apps/web/src/components/clock.tsx b/apps/web/src/components/clock.tsx index 102d92099..759c43bd7 100644 --- a/apps/web/src/components/clock.tsx +++ b/apps/web/src/components/clock.tsx @@ -78,10 +78,11 @@ export const TimesShownIn = () => { return ( - diff --git a/apps/web/src/components/create-poll.tsx b/apps/web/src/components/create-poll.tsx index 9cfdae897..f8b113dc8 100644 --- a/apps/web/src/components/create-poll.tsx +++ b/apps/web/src/components/create-poll.tsx @@ -98,14 +98,14 @@ export const CreatePoll: React.FunctionComponent = () => { numberOfOptions: formData.options?.length, optionsView: formData?.view, }); - queryClient.polls.list.invalidate(); + queryClient.invalidate(); router.push(`/poll/${res.id}`); }, }, ); })} > -
              +
              diff --git a/apps/web/src/components/date-icon.tsx b/apps/web/src/components/date-icon.tsx index ae5662e5f..ef62c92dd 100644 --- a/apps/web/src/components/date-icon.tsx +++ b/apps/web/src/components/date-icon.tsx @@ -10,14 +10,14 @@ export const DateIconInner = (props: { return (
              {props.dow}
              -
              +
              {props.day}
              diff --git a/apps/web/src/components/discussion/discussion.tsx b/apps/web/src/components/discussion/discussion.tsx index ed9bd5f28..a337fa3fe 100644 --- a/apps/web/src/components/discussion/discussion.tsx +++ b/apps/web/src/components/discussion/discussion.tsx @@ -1,19 +1,36 @@ +"use client"; +import { cn } from "@rallly/ui"; +import { Badge } from "@rallly/ui/badge"; import { Button } from "@rallly/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@rallly/ui/card"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@rallly/ui/dropdown-menu"; +import { Icon } from "@rallly/ui/icon"; import { Textarea } from "@rallly/ui/textarea"; import dayjs from "dayjs"; -import { MessageCircleIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react"; +import { + MessageSquareOffIcon, + MoreHorizontalIcon, + TrashIcon, +} from "lucide-react"; import { useTranslation } from "next-i18next"; import * as React from "react"; import { Controller, useForm } from "react-hook-form"; +import { useParticipants } from "@/components/participants-provider"; import { Trans } from "@/components/trans"; import { usePermissions } from "@/contexts/permissions"; +import { usePoll } from "@/contexts/poll"; import { useRole } from "@/contexts/role"; import { usePostHog } from "@/utils/posthog"; import { trpc } from "@/utils/trpc/client"; @@ -22,7 +39,6 @@ import { requiredString } from "../../utils/form-validation"; import NameInput from "../name-input"; import TruncatedLinkify from "../poll/truncated-linkify"; import UserAvatar from "../poll/user-avatar"; -import { usePoll } from "../poll-context"; import { useUser } from "../user-provider"; interface CommentForm { @@ -30,9 +46,107 @@ interface CommentForm { content: string; } -const Discussion: React.FunctionComponent = () => { +function NewCommentForm({ + onSubmit, + onCancel, +}: { + onSubmit?: () => void; + onCancel?: () => void; +}) { const { t } = useTranslation(); - const { poll } = usePoll(); + const poll = usePoll(); + const { user } = useUser(); + const { participants } = useParticipants(); + + const authorName = React.useMemo(() => { + if (user.isGuest) { + const participant = participants.find((p) => p.userId === user.id); + return participant?.name ?? ""; + } else { + return user.name; + } + }, [user, participants]); + + const pollId = poll.id; + + const posthog = usePostHog(); + + const queryClient = trpc.useUtils(); + + const addComment = trpc.polls.comments.add.useMutation({ + onSuccess: () => { + queryClient.polls.comments.invalidate(); + posthog?.capture("created comment"); + }, + }); + + const session = useUser(); + + const { register, reset, control, handleSubmit, formState } = + useForm({ + defaultValues: { + authorName, + content: "", + }, + }); + + return ( + { + await addComment.mutateAsync({ authorName, content, pollId }); + reset({ authorName, content: "" }); + onSubmit?.(); + })} + > +
              +