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 (
+
@@ -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";