mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 10:16:32 +02:00
✨ Add loading pages (#978)
This commit is contained in:
parent
a1bac0c986
commit
0a2e3e3532
17 changed files with 402 additions and 253 deletions
53
apps/web/src/app/[locale]/(admin)/polls/layout.tsx
Normal file
53
apps/web/src/app/[locale]/(admin)/polls/layout.tsx
Normal file
|
@ -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 (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<div className="flex justify-between items-center gap-x-4">
|
||||
<PageTitle>
|
||||
<Trans t={t} i18nKey="polls" />
|
||||
</PageTitle>
|
||||
<Button asChild>
|
||||
<Link href="/new">
|
||||
<PenBoxIcon className="w-4 text-muted-foreground h-4" />
|
||||
<span className="hidden sm:inline">
|
||||
<Trans t={t} i18nKey="newPoll" />
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent>{children}</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { locale: string };
|
||||
}) {
|
||||
const { t } = await getTranslation(params.locale);
|
||||
return {
|
||||
title: t("polls"),
|
||||
};
|
||||
}
|
31
apps/web/src/app/[locale]/(admin)/polls/loading.tsx
Normal file
31
apps/web/src/app/[locale]/(admin)/polls/loading.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Skeleton } from "@/components/skeleton";
|
||||
|
||||
function Row() {
|
||||
return (
|
||||
<div className="flex first:pt-0 py-4 items-center gap-x-4">
|
||||
<div className="grow">
|
||||
<Skeleton className="w-48 h-5 mb-2" />
|
||||
<Skeleton className="w-24 h-4" />
|
||||
</div>
|
||||
<div className="pr-8">
|
||||
<Skeleton className="w-24 h-4" />
|
||||
</div>
|
||||
<div className="pr-8">
|
||||
<Skeleton className="w-24 h-4" />
|
||||
</div>
|
||||
<div className="pr-8">
|
||||
<Skeleton className="w-12 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default function Loader() {
|
||||
return (
|
||||
<div className="divide-y divide-gray-100">
|
||||
<Row />
|
||||
<Row />
|
||||
<Row />
|
||||
<Row />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<div className="flex justify-between items-center gap-x-4">
|
||||
<PageTitle>
|
||||
<Trans t={t} i18nKey="polls" />
|
||||
</PageTitle>
|
||||
<Button asChild>
|
||||
<Link href="/new">
|
||||
<PenBoxIcon className="w-4 text-muted-foreground h-4" />
|
||||
<span className="hidden sm:inline">
|
||||
<Trans t={t} i18nKey="newPoll" />
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<div className="space-y-6">
|
||||
<PollsList />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
export default async function Page() {
|
||||
return <PollsList />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
|
|
|
@ -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 (
|
||||
<div className="py-24">
|
||||
|
@ -186,7 +188,10 @@ export function PollsList() {
|
|||
[adjustTimeZone],
|
||||
);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data) {
|
||||
// return a table using <Skeleton /> components
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (data.total === 0) return <EmptyState />;
|
||||
|
||||
|
|
|
@ -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, {
|
||||
|
|
106
apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx
Normal file
106
apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx
Normal file
|
@ -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 <div>Not found</div>;
|
||||
}
|
||||
if (!poll || !participants) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PermissionsContext.Provider value={{ userId: permission?.userId ?? null }}>
|
||||
<Head>
|
||||
<title>{poll.title}</title>
|
||||
</Head>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const GoToApp = () => {
|
||||
const poll = usePoll();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<PageHeader variant="ghost">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className={poll.userId !== user.id ? "hidden" : ""}
|
||||
>
|
||||
<Link href={`/poll/${poll.id}`}>
|
||||
<ArrowUpLeftIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<Trans i18nKey="manage" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export function InvitePage() {
|
||||
return (
|
||||
<Prefetch>
|
||||
<LegacyPollContextProvider>
|
||||
<VisibilityProvider>
|
||||
<GoToApp />
|
||||
<div className="lg:px-6 lg:py-5 p-3">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="-mx-1">
|
||||
<Poll />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VisibilityProvider>
|
||||
</LegacyPollContextProvider>
|
||||
</Prefetch>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
24
apps/web/src/app/[locale]/invite/[urlId]/loading.tsx
Normal file
24
apps/web/src/app/[locale]/invite/[urlId]/loading.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
PageContainer,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
} from "@/app/components/page-layout";
|
||||
import { Skeleton } from "@/components/skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader className="justify-end flex" variant="ghost">
|
||||
<Skeleton className="w-32 h-9" />
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<Skeleton className="h-72 w-full" />
|
||||
<Skeleton className="h-96 w-full" />
|
||||
<hr />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
37
apps/web/src/app/[locale]/invite/[urlId]/nav.tsx
Normal file
37
apps/web/src/app/[locale]/invite/[urlId]/nav.tsx
Normal file
|
@ -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 (
|
||||
<PageHeader variant="ghost">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className={poll.userId !== user.id ? "hidden" : ""}
|
||||
>
|
||||
<Link href={`/poll/${poll.id}`}>
|
||||
<ArrowUpLeftIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<Trans i18nKey="manage" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
|
@ -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";
|
||||
|
||||
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 <div>Not found</div>;
|
||||
}
|
||||
if (!poll || !participants) {
|
||||
return null;
|
||||
}
|
||||
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 (
|
||||
<PermissionsContext.Provider value={{ userId: permission?.userId ?? null }}>
|
||||
<Head>
|
||||
<title>{poll.title}</title>
|
||||
</Head>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const GoToApp = () => {
|
||||
const poll = usePoll();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<PageHeader variant="ghost">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className={poll.userId !== user.id ? "hidden" : ""}
|
||||
>
|
||||
<Link href={`/poll/${poll.id}`}>
|
||||
<ArrowUpLeftIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<Trans i18nKey="manage" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default function InvitePage() {
|
||||
return (
|
||||
<Prefetch>
|
||||
<LegacyPollContextProvider>
|
||||
<VisibilityProvider>
|
||||
<GoToApp />
|
||||
<div className="lg:px-6 lg:py-5 p-3">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="-mx-1">
|
||||
<Poll />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VisibilityProvider>
|
||||
</LegacyPollContextProvider>
|
||||
</Prefetch>
|
||||
<PageContainer>
|
||||
<InvitePage />
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
45
apps/web/src/app/[locale]/poll/[urlId]/guest-poll-alert.tsx
Normal file
45
apps/web/src/app/[locale]/poll/[urlId]/guest-poll-alert.tsx
Normal file
|
@ -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 (
|
||||
<Alert icon={InfoIcon}>
|
||||
<AlertTitle className="mb-1 text-sm font-medium tracking-normal">
|
||||
<Trans
|
||||
i18nKey="guestPollAlertTitle"
|
||||
defaults="Your administrator rights can be lost if you clear your cookies"
|
||||
/>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-sm">
|
||||
<Trans
|
||||
i18nKey="guestPollAlertDescription"
|
||||
defaults="<0>Create an account</0> or <1>login</1> to claim this poll."
|
||||
components={[
|
||||
<RegisterLink
|
||||
className="hover:text-gray-800 underline"
|
||||
key="register"
|
||||
/>,
|
||||
<LoginLink className="hover:text-gray-800 underline" key="login" />,
|
||||
]}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
export default function Loading() {
|
||||
return null;
|
||||
}
|
|
@ -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 (
|
||||
<Alert icon={InfoIcon}>
|
||||
<AlertTitle className="mb-1 text-sm font-medium tracking-normal">
|
||||
<Trans
|
||||
i18nKey="guestPollAlertTitle"
|
||||
defaults="Your administrator rights can be lost if you clear your cookies"
|
||||
/>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-sm">
|
||||
<Trans
|
||||
i18nKey="guestPollAlertDescription"
|
||||
defaults="<0>Create an account</0> or <1>login</1> to claim this poll."
|
||||
components={[
|
||||
<RegisterLink
|
||||
className="hover:text-gray-800 underline"
|
||||
key="register"
|
||||
/>,
|
||||
<LoginLink className="hover:text-gray-800 underline" key="login" />,
|
||||
]}
|
||||
/>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
export default async function Page() {
|
||||
return (
|
||||
<div className={cn("max-w-4xl space-y-4 mx-auto")}>
|
||||
<div className="-mx-1 space-y-3 sm:space-y-6">
|
||||
|
|
24
apps/web/src/app/[locale]/poll/[urlId]/skeleton.tsx
Normal file
24
apps/web/src/app/[locale]/poll/[urlId]/skeleton.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
PageContainer,
|
||||
PageContent,
|
||||
PageHeader,
|
||||
} from "@/app/components/page-layout";
|
||||
import { Skeleton } from "@/components/skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<Skeleton className="w-32 h-9" />
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<Skeleton className="h-72 w-full" />
|
||||
<Skeleton className="h-96 w-full" />
|
||||
<hr />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</PageContent>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
|
@ -53,5 +53,5 @@ export function PageContent({
|
|||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("lg:p-6 p-4", className)}>{children}</div>;
|
||||
return <div className={cn("lg:px-6 lg:py-5 p-4", className)}>{children}</div>;
|
||||
}
|
||||
|
|
|
@ -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 <Loader />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"use client";
|
||||
import { cn } from "@rallly/ui";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
|
Loading…
Add table
Reference in a new issue