diff --git a/apps/web/src/app/[locale]/(admin)/polls/layout.tsx b/apps/web/src/app/[locale]/(admin)/polls/layout.tsx new file mode 100644 index 00000000000..234a9dfa465 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/layout.tsx @@ -0,0 +1,53 @@ +import { Button } from "@rallly/ui/button"; +import { PenBoxIcon } from "lucide-react"; +import Link from "next/link"; +import { Trans } from "react-i18next/TransWithoutContext"; + +import { + PageContainer, + PageContent, + PageHeader, + PageTitle, +} from "@/app/components/page-layout"; +import { getTranslation } from "@/app/i18n"; + +export default async function Layout({ + params, + children, +}: { + children: React.ReactNode; + params: { locale: string }; +}) { + const { t } = await getTranslation(params.locale); + return ( + + +
+ + + + +
+
+ {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 new file mode 100644 index 00000000000..84244ac05f9 --- /dev/null +++ b/apps/web/src/app/[locale]/(admin)/polls/loading.tsx @@ -0,0 +1,31 @@ +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 index 207a4af57da..44627743ec3 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/page.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/page.tsx @@ -1,44 +1,9 @@ -import { Button } from "@rallly/ui/button"; -import { PenBoxIcon } from "lucide-react"; -import Link from "next/link"; -import { Trans } from "react-i18next/TransWithoutContext"; - -import { - PageContainer, - PageContent, - PageHeader, - PageTitle, -} from "@/app/components/page-layout"; import { getTranslation } from "@/app/i18n"; import { PollsList } from "./polls-list"; -export default async function Page({ params }: { params: { locale: string } }) { - const { t } = await getTranslation(params.locale); - return ( - - -
- - - - -
-
- -
- -
-
-
- ); +export default async function Page() { + return ; } export async function generateMetadata({ diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx index 6c4d636ca8e..b8c0daa8f02 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/polls-list.tsx @@ -15,6 +15,8 @@ import { Trans } from "@/components/trans"; import { useDayjs } from "@/utils/dayjs"; import { trpc } from "@/utils/trpc/client"; +import Loader from "./loading"; + const EmptyState = () => { return (
@@ -186,7 +188,10 @@ export function PollsList() { [adjustTimeZone], ); - if (!data) return null; + if (!data) { + // return a table using components + return ; + } if (data.total === 0) return ; diff --git a/apps/web/src/app/[locale]/(auth)/login/login-form.tsx b/apps/web/src/app/[locale]/(auth)/login/login-form.tsx index 01cc39d33f7..b239031e684 100644 --- a/apps/web/src/app/[locale]/(auth)/login/login-form.tsx +++ b/apps/web/src/app/[locale]/(auth)/login/login-form.tsx @@ -54,7 +54,7 @@ export function LoginForm({ oidcConfig }: { oidcConfig?: { name: string } }) { if (!success) { throw new Error("Failed to authenticate user"); } else { - queryClient.invalidate(); + await queryClient.invalidate(); const s = await session.update(); if (s?.user) { posthog?.identify(s.user.id, { diff --git a/apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx b/apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx new file mode 100644 index 00000000000..6ba9074e3df --- /dev/null +++ b/apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx @@ -0,0 +1,106 @@ +"use client"; +import { Button } from "@rallly/ui/button"; +import { ArrowUpLeftIcon } from "lucide-react"; +import Head from "next/head"; +import Link from "next/link"; +import { useParams, useSearchParams } from "next/navigation"; +import React from "react"; + +import { PageHeader } from "@/app/components/page-layout"; +import { Poll } from "@/components/poll"; +import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider"; +import { Trans } from "@/components/trans"; +import { UserDropdown } from "@/components/user-dropdown"; +import { useUser } from "@/components/user-provider"; +import { VisibilityProvider } from "@/components/visibility"; +import { PermissionsContext } from "@/contexts/permissions"; +import { usePoll } from "@/contexts/poll"; +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 ( + + + {poll.title} + + {children} + + ); +}; + +const GoToApp = () => { + const poll = usePoll(); + const { user } = useUser(); + + return ( + +
+
+ +
+
+ +
+
+
+ ); +}; + +export function InvitePage() { + return ( + + + + +
+
+
+ +
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx b/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx deleted file mode 100644 index 066964b670f..00000000000 --- a/apps/web/src/app/[locale]/invite/[urlId]/layout.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { prisma } from "@rallly/database"; -import { Metadata } from "next"; -import { notFound } from "next/navigation"; - -import { getTranslation } from "@/app/i18n"; -import { absoluteUrl } from "@/utils/absolute-url"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return <>{children}; -} - -export async function generateMetadata({ - params: { urlId, locale }, -}: { - params: { - urlId: string; - locale: string; - }; -}) { - const poll = await prisma.poll.findUnique({ - where: { - id: urlId as string, - }, - select: { - id: true, - title: true, - user: { - select: { - name: true, - }, - }, - }, - }); - - const { t } = await getTranslation(locale); - - if (!poll) { - return notFound(); - } - - const { title, id, user } = poll; - - const author = user?.name || t("guest"); - - return { - title, - metadataBase: new URL(absoluteUrl()), - openGraph: { - title, - description: `By ${author}`, - url: `/invite/${id}`, - images: [ - { - url: `${absoluteUrl("/api/og-image-poll", { - title, - author, - })}`, - width: 1200, - height: 630, - alt: title, - type: "image/png", - }, - ], - }, - } satisfies Metadata; -} diff --git a/apps/web/src/app/[locale]/invite/[urlId]/loading.tsx b/apps/web/src/app/[locale]/invite/[urlId]/loading.tsx new file mode 100644 index 00000000000..ea593d33989 --- /dev/null +++ b/apps/web/src/app/[locale]/invite/[urlId]/loading.tsx @@ -0,0 +1,24 @@ +import { + PageContainer, + PageContent, + PageHeader, +} from "@/app/components/page-layout"; +import { Skeleton } from "@/components/skeleton"; + +export default function Loading() { + return ( + + + + + +
+ + +
+ +
+
+
+ ); +} diff --git a/apps/web/src/app/[locale]/invite/[urlId]/nav.tsx b/apps/web/src/app/[locale]/invite/[urlId]/nav.tsx new file mode 100644 index 00000000000..fa19af8cd22 --- /dev/null +++ b/apps/web/src/app/[locale]/invite/[urlId]/nav.tsx @@ -0,0 +1,37 @@ +"use client"; +import { Button } from "@rallly/ui/button"; +import { ArrowUpLeftIcon } from "lucide-react"; +import Link from "next/link"; + +import { PageHeader } from "@/app/components/page-layout"; +import { Trans } from "@/components/trans"; +import { UserDropdown } from "@/components/user-dropdown"; +import { useUser } from "@/components/user-provider"; +import { usePoll } from "@/contexts/poll"; + +export const Nav = () => { + const poll = usePoll(); + const { user } = useUser(); + + return ( + +
+
+ +
+
+ +
+
+
+ ); +}; diff --git a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx index 1f496b1f5a5..954600af8c2 100644 --- a/apps/web/src/app/[locale]/invite/[urlId]/page.tsx +++ b/apps/web/src/app/[locale]/invite/[urlId]/page.tsx @@ -1,104 +1,72 @@ -"use client"; -import { Button } from "@rallly/ui/button"; -import { ArrowUpLeftIcon } from "lucide-react"; -import Head from "next/head"; -import Link from "next/link"; -import { useParams, useSearchParams } from "next/navigation"; -import React from "react"; +import { prisma } from "@rallly/database"; +import { Metadata } from "next"; +import { notFound } from "next/navigation"; -import { PageHeader } from "@/app/components/page-layout"; -import { Poll } from "@/components/poll"; -import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider"; -import { Trans } from "@/components/trans"; -import { UserDropdown } from "@/components/user-dropdown"; -import { useUser } from "@/components/user-provider"; -import { VisibilityProvider } from "@/components/visibility"; -import { PermissionsContext } from "@/contexts/permissions"; -import { usePoll } from "@/contexts/poll"; -import { trpc } from "@/utils/trpc/client"; +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"; -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, - }, +export default async function Page() { + return ( + + + ); +} - const { data: poll, error } = trpc.polls.get.useQuery( - { urlId }, - { - retry: false, +export async function generateMetadata({ + params: { urlId, locale }, +}: { + params: { + urlId: string; + locale: string; + }; +}) { + const poll = await prisma.poll.findUnique({ + where: { + id: urlId as string, + }, + select: { + id: true, + title: true, + user: { + select: { + name: true, + }, + }, }, - ); - - const { data: participants } = trpc.polls.participants.list.useQuery({ - pollId: urlId, }); - if (error?.data?.code === "NOT_FOUND") { - return
Not found
; - } - if (!poll || !participants) { - return null; - } + const { t } = await getTranslation(locale); - return ( - - - {poll.title} - - {children} - - ); -}; + if (!poll) { + return notFound(); + } -const GoToApp = () => { - const poll = usePoll(); - const { user } = useUser(); + const { title, id, user } = poll; - return ( - -
-
- -
-
- -
-
-
- ); -}; + const author = user?.name || t("guest"); -export default function InvitePage() { - return ( - - - - -
-
-
- -
-
-
-
-
-
- ); + return { + title, + metadataBase: new URL(absoluteUrl()), + openGraph: { + title, + description: `By ${author}`, + url: `/invite/${id}`, + images: [ + { + url: `${absoluteUrl("/api/og-image-poll", { + title, + author, + })}`, + width: 1200, + height: 630, + alt: title, + type: "image/png", + }, + ], + }, + } satisfies Metadata; } diff --git a/apps/web/src/app/[locale]/poll/[urlId]/guest-poll-alert.tsx b/apps/web/src/app/[locale]/poll/[urlId]/guest-poll-alert.tsx new file mode 100644 index 00000000000..533f99b900d --- /dev/null +++ b/apps/web/src/app/[locale]/poll/[urlId]/guest-poll-alert.tsx @@ -0,0 +1,45 @@ +"use client"; +import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert"; +import { InfoIcon } from "lucide-react"; +import { Trans } from "next-i18next"; + +import { LoginLink } from "@/components/login-link"; +import { RegisterLink } from "@/components/register-link"; +import { useUser } from "@/components/user-provider"; +import { usePoll } from "@/contexts/poll"; + +export const GuestPollAlert = () => { + const poll = usePoll(); + const { user } = useUser(); + + if (poll.user) { + return null; + } + + if (!user.isGuest) { + return null; + } + return ( + + + + + + , + , + ]} + /> + + + ); +}; diff --git a/apps/web/src/app/[locale]/poll/[urlId]/loading.tsx b/apps/web/src/app/[locale]/poll/[urlId]/loading.tsx deleted file mode 100644 index 4349ac3a619..00000000000 --- a/apps/web/src/app/[locale]/poll/[urlId]/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Loading() { - return null; -} diff --git a/apps/web/src/app/[locale]/poll/[urlId]/page.tsx b/apps/web/src/app/[locale]/poll/[urlId]/page.tsx index cc89f1bc875..3e95058dc9e 100644 --- a/apps/web/src/app/[locale]/poll/[urlId]/page.tsx +++ b/apps/web/src/app/[locale]/poll/[urlId]/page.tsx @@ -1,52 +1,10 @@ -"use client"; import { cn } from "@rallly/ui"; -import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert"; -import { InfoIcon } from "lucide-react"; -import { Trans } from "next-i18next"; -import { LoginLink } from "@/components/login-link"; import { Poll } from "@/components/poll"; -import { RegisterLink } from "@/components/register-link"; -import { useUser } from "@/components/user-provider"; -import { usePoll } from "@/contexts/poll"; -const GuestPollAlert = () => { - const poll = usePoll(); - const { user } = useUser(); +import { GuestPollAlert } from "./guest-poll-alert"; - if (poll.user) { - return null; - } - - if (!user.isGuest) { - return null; - } - return ( - - - - - - , - , - ]} - /> - - - ); -}; - -export default function Page() { +export default async function Page() { return (
diff --git a/apps/web/src/app/[locale]/poll/[urlId]/skeleton.tsx b/apps/web/src/app/[locale]/poll/[urlId]/skeleton.tsx new file mode 100644 index 00000000000..8633ebb823e --- /dev/null +++ b/apps/web/src/app/[locale]/poll/[urlId]/skeleton.tsx @@ -0,0 +1,24 @@ +import { + PageContainer, + PageContent, + PageHeader, +} from "@/app/components/page-layout"; +import { Skeleton } from "@/components/skeleton"; + +export default function Loading() { + return ( + + + + + +
+ + +
+ +
+
+
+ ); +} diff --git a/apps/web/src/app/components/page-layout.tsx b/apps/web/src/app/components/page-layout.tsx index c5f5bce2ad0..faa35074498 100644 --- a/apps/web/src/app/components/page-layout.tsx +++ b/apps/web/src/app/components/page-layout.tsx @@ -53,5 +53,5 @@ export function PageContent({ children?: React.ReactNode; className?: string; }) { - return
{children}
; + return
{children}
; } diff --git a/apps/web/src/components/layouts/poll-layout.tsx b/apps/web/src/components/layouts/poll-layout.tsx index a58c5e724de..850dde21ae3 100644 --- a/apps/web/src/components/layouts/poll-layout.tsx +++ b/apps/web/src/components/layouts/poll-layout.tsx @@ -23,6 +23,7 @@ import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; import React from "react"; +import Loader from "@/app/[locale]/poll/[urlId]/skeleton"; import { LogoutButton } from "@/app/components/logout-button"; import { PageContainer, @@ -259,7 +260,7 @@ const Prefetch = ({ children }: React.PropsWithChildren) => { const watchers = trpc.polls.getWatchers.useQuery({ pollId: urlId }); if (!poll.data || !watchers.data || !participants.data) { - return null; + return ; } return <>{children}; diff --git a/apps/web/src/components/poll.tsx b/apps/web/src/components/poll.tsx index b348eac868f..fd86731be63 100644 --- a/apps/web/src/components/poll.tsx +++ b/apps/web/src/components/poll.tsx @@ -1,3 +1,4 @@ +"use client"; import { cn } from "@rallly/ui"; import Link from "next/link"; import React from "react";