mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-30 18:56:45 +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 { getTranslation } from "@/app/i18n";
|
||||||
|
|
||||||
import { PollsList } from "./polls-list";
|
import { PollsList } from "./polls-list";
|
||||||
|
|
||||||
export default async function Page({ params }: { params: { locale: string } }) {
|
export default async function Page() {
|
||||||
const { t } = await getTranslation(params.locale);
|
return <PollsList />;
|
||||||
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 async function generateMetadata({
|
export async function generateMetadata({
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { Trans } from "@/components/trans";
|
||||||
import { useDayjs } from "@/utils/dayjs";
|
import { useDayjs } from "@/utils/dayjs";
|
||||||
import { trpc } from "@/utils/trpc/client";
|
import { trpc } from "@/utils/trpc/client";
|
||||||
|
|
||||||
|
import Loader from "./loading";
|
||||||
|
|
||||||
const EmptyState = () => {
|
const EmptyState = () => {
|
||||||
return (
|
return (
|
||||||
<div className="py-24">
|
<div className="py-24">
|
||||||
|
@ -186,7 +188,10 @@ export function PollsList() {
|
||||||
[adjustTimeZone],
|
[adjustTimeZone],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) {
|
||||||
|
// return a table using <Skeleton /> components
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.total === 0) return <EmptyState />;
|
if (data.total === 0) return <EmptyState />;
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ export function LoginForm({ oidcConfig }: { oidcConfig?: { name: string } }) {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error("Failed to authenticate user");
|
throw new Error("Failed to authenticate user");
|
||||||
} else {
|
} else {
|
||||||
queryClient.invalidate();
|
await queryClient.invalidate();
|
||||||
const s = await session.update();
|
const s = await session.update();
|
||||||
if (s?.user) {
|
if (s?.user) {
|
||||||
posthog?.identify(s.user.id, {
|
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 { prisma } from "@rallly/database";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Metadata } from "next";
|
||||||
import { ArrowUpLeftIcon } from "lucide-react";
|
import { notFound } from "next/navigation";
|
||||||
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 { InvitePage } from "@/app/[locale]/invite/[urlId]/invite-page";
|
||||||
import { Poll } from "@/components/poll";
|
import { PageContainer } from "@/app/components/page-layout";
|
||||||
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
|
import { getTranslation } from "@/app/i18n";
|
||||||
import { Trans } from "@/components/trans";
|
import { absoluteUrl } from "@/utils/absolute-url";
|
||||||
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) => {
|
export default async function Page() {
|
||||||
const searchParams = useSearchParams();
|
return (
|
||||||
const token = searchParams?.get("token") as string;
|
<PageContainer>
|
||||||
const params = useParams<{ urlId: string }>();
|
<InvitePage />
|
||||||
const urlId = params?.urlId as string;
|
</PageContainer>
|
||||||
const { data: permission } = trpc.auth.getUserPermission.useQuery(
|
|
||||||
{ token },
|
|
||||||
{
|
|
||||||
enabled: !!token,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { data: poll, error } = trpc.polls.get.useQuery(
|
export async function generateMetadata({
|
||||||
{ urlId },
|
params: { urlId, locale },
|
||||||
{
|
}: {
|
||||||
retry: false,
|
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") {
|
const { t } = await getTranslation(locale);
|
||||||
return <div>Not found</div>;
|
|
||||||
}
|
if (!poll) {
|
||||||
if (!poll || !participants) {
|
return notFound();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const { title, id, user } = poll;
|
||||||
<PermissionsContext.Provider value={{ userId: permission?.userId ?? null }}>
|
|
||||||
<Head>
|
|
||||||
<title>{poll.title}</title>
|
|
||||||
</Head>
|
|
||||||
{children}
|
|
||||||
</PermissionsContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GoToApp = () => {
|
const author = user?.name || t("guest");
|
||||||
const poll = usePoll();
|
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
return (
|
return {
|
||||||
<PageHeader variant="ghost">
|
title,
|
||||||
<div className="flex justify-between">
|
metadataBase: new URL(absoluteUrl()),
|
||||||
<div>
|
openGraph: {
|
||||||
<Button
|
title,
|
||||||
variant="ghost"
|
description: `By ${author}`,
|
||||||
asChild
|
url: `/invite/${id}`,
|
||||||
className={poll.userId !== user.id ? "hidden" : ""}
|
images: [
|
||||||
>
|
{
|
||||||
<Link href={`/poll/${poll.id}`}>
|
url: `${absoluteUrl("/api/og-image-poll", {
|
||||||
<ArrowUpLeftIcon className="h-4 w-4 text-muted-foreground" />
|
title,
|
||||||
<Trans i18nKey="manage" />
|
author,
|
||||||
</Link>
|
})}`,
|
||||||
</Button>
|
width: 1200,
|
||||||
</div>
|
height: 630,
|
||||||
<div>
|
alt: title,
|
||||||
<UserDropdown />
|
type: "image/png",
|
||||||
</div>
|
},
|
||||||
</div>
|
],
|
||||||
</PageHeader>
|
},
|
||||||
);
|
} satisfies Metadata;
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
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 { 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 { Poll } from "@/components/poll";
|
||||||
import { RegisterLink } from "@/components/register-link";
|
|
||||||
import { useUser } from "@/components/user-provider";
|
|
||||||
import { usePoll } from "@/contexts/poll";
|
|
||||||
|
|
||||||
const GuestPollAlert = () => {
|
import { GuestPollAlert } from "./guest-poll-alert";
|
||||||
const poll = usePoll();
|
|
||||||
const { user } = useUser();
|
|
||||||
|
|
||||||
if (poll.user) {
|
export default async function Page() {
|
||||||
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() {
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("max-w-4xl space-y-4 mx-auto")}>
|
<div className={cn("max-w-4xl space-y-4 mx-auto")}>
|
||||||
<div className="-mx-1 space-y-3 sm:space-y-6">
|
<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;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
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 { useParams, usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import Loader from "@/app/[locale]/poll/[urlId]/skeleton";
|
||||||
import { LogoutButton } from "@/app/components/logout-button";
|
import { LogoutButton } from "@/app/components/logout-button";
|
||||||
import {
|
import {
|
||||||
PageContainer,
|
PageContainer,
|
||||||
|
@ -259,7 +260,7 @@ const Prefetch = ({ children }: React.PropsWithChildren) => {
|
||||||
const watchers = trpc.polls.getWatchers.useQuery({ pollId: urlId });
|
const watchers = trpc.polls.getWatchers.useQuery({ pollId: urlId });
|
||||||
|
|
||||||
if (!poll.data || !watchers.data || !participants.data) {
|
if (!poll.data || !watchers.data || !participants.data) {
|
||||||
return null;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client";
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
Loading…
Add table
Reference in a new issue