Refactor admin and participant page (#464)

This commit is contained in:
Luke Vella 2023-02-01 10:50:05 +00:00 committed by GitHub
parent 875e48f1fe
commit d397654de7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 348 additions and 236 deletions

View file

@ -40,18 +40,6 @@ const nextConfig = {
source: "/",
destination: "/home",
},
{
source: "/p/:urlId",
destination: "/poll?urlId=:urlId",
},
{
source: "/admin/:urlId",
destination: "/poll?urlId=:urlId",
},
{
source: "/verify/:urlId/code/:code",
destination: "/poll?urlId=:urlId&code=:code",
},
];
},
sentry: {

View file

@ -18,7 +18,7 @@ import {
UserDetailsData,
UserDetailsForm,
} from "./forms";
import StandardLayout from "./standard-layout";
import StandardLayout from "./layouts/standard-layout";
import Steps from "./steps";
import { useUser } from "./user-provider";

View file

@ -28,7 +28,7 @@ const Discussion: React.VoidFunctionComponent = () => {
const { dayjs } = useDayjs();
const queryClient = trpc.useContext();
const { t } = useTranslation("app");
const { poll } = usePoll();
const { poll, admin } = usePoll();
const pollId = poll.id;
@ -93,7 +93,7 @@ const Discussion: React.VoidFunctionComponent = () => {
<AnimatePresence initial={false}>
{comments.map((comment) => {
const canDelete =
poll.admin || session.ownsObject(comment) || isUnclaimed(comment);
admin || session.ownsObject(comment) || isUnclaimed(comment);
return (
<motion.div

View file

@ -5,7 +5,7 @@ import React from "react";
import Bonus from "./home/bonus";
import Features from "./home/features";
import Hero from "./home/hero";
import PageLayout from "./page-layout";
import PageLayout from "./layouts/page-layout";
const Home: React.VoidFunctionComponent = () => {
const { t } = useTranslation("homepage");

View file

@ -8,8 +8,8 @@ import DotsVertical from "@/components/icons/dots-vertical.svg";
import Github from "@/components/icons/github.svg";
import Logo from "~/public/logo.svg";
import Popover from "../popover";
import Footer from "./page-layout/footer";
import Popover from "./popover";
export interface PageLayoutProps {
children?: React.ReactNode;

View file

@ -12,7 +12,7 @@ import Logo from "~/public/logo.svg";
import Sentry from "~/public/sentry.svg";
import Vercel from "~/public/vercel-logotype-dark.svg";
import { LanguageSelect } from "../poll/language-selector";
import { LanguageSelect } from "../../poll/language-selector";
const Footer: React.VoidFunctionComponent = () => {
const { t } = useTranslation("common");

View file

@ -4,31 +4,32 @@ import Link from "next/link";
import { useTranslation } from "next-i18next";
import React from "react";
import { LoginLink, useLoginModal } from "@/components/auth/login-modal";
import Dropdown, { DropdownItem, DropdownProps } from "@/components/dropdown";
import Adjustments from "@/components/icons/adjustments.svg";
import Cash from "@/components/icons/cash.svg";
import Discord from "@/components/icons/discord.svg";
import DotsVertical from "@/components/icons/dots-vertical.svg";
import Github from "@/components/icons/github.svg";
import Login from "@/components/icons/login.svg";
import Logout from "@/components/icons/logout.svg";
import Menu from "@/components/icons/menu.svg";
import Pencil from "@/components/icons/pencil.svg";
import Question from "@/components/icons/question-mark-circle.svg";
import Spinner from "@/components/icons/spinner.svg";
import Support from "@/components/icons/support.svg";
import Twitter from "@/components/icons/twitter.svg";
import User from "@/components/icons/user.svg";
import UserCircle from "@/components/icons/user-circle.svg";
import ModalProvider, {
useModalContext,
} from "@/components/modal/modal-provider";
import Popover from "@/components/popover";
import Preferences from "@/components/preferences";
import { useUser } from "@/components/user-provider";
import { DayjsProvider } from "@/utils/dayjs";
import Logo from "~/public/logo.svg";
import { DayjsProvider } from "../utils/dayjs";
import { LoginLink, useLoginModal } from "./auth/login-modal";
import Dropdown, { DropdownItem, DropdownProps } from "./dropdown";
import Adjustments from "./icons/adjustments.svg";
import Cash from "./icons/cash.svg";
import Discord from "./icons/discord.svg";
import DotsVertical from "./icons/dots-vertical.svg";
import Github from "./icons/github.svg";
import Login from "./icons/login.svg";
import Logout from "./icons/logout.svg";
import Pencil from "./icons/pencil.svg";
import Question from "./icons/question-mark-circle.svg";
import Spinner from "./icons/spinner.svg";
import Support from "./icons/support.svg";
import Twitter from "./icons/twitter.svg";
import ModalProvider, { useModalContext } from "./modal/modal-provider";
import Popover from "./popover";
import Preferences from "./preferences";
import { useUser } from "./user-provider";
const HomeLink = () => {
return (
<Link href="/">

View file

@ -1,5 +1,4 @@
import { AnimatePresence, motion } from "framer-motion";
import { NextPage } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
@ -16,7 +15,7 @@ import DesktopPoll from "@/components/poll/desktop-poll";
import MobilePoll from "@/components/poll/mobile-poll";
import { preventWidows } from "@/utils/prevent-widows";
import { trpc } from "../utils/trpc";
import { trpc, trpcNext } from "../utils/trpc";
import { useParticipants } from "./participants-provider";
import ManagePoll from "./poll/manage-poll";
import { useUpdatePollMutation } from "./poll/mutations";
@ -31,28 +30,62 @@ import { usePoll } from "./poll-context";
import Sharing from "./sharing";
import { useUser } from "./user-provider";
const PollPage: NextPage = () => {
const { poll, urlId, admin } = usePoll();
const { participants } = useParticipants();
const router = useRouter();
const checkIfWideScreen = () => window.innerWidth > 640;
useTouchBeacon(poll.id);
const useWideScreen = () => {
const [isWideScreen, setIsWideScreen] = React.useState(checkIfWideScreen);
React.useEffect(() => {
const listener = () => setIsWideScreen(checkIfWideScreen());
window.addEventListener("resize", listener);
return () => {
window.removeEventListener("resize", listener);
};
}, []);
return isWideScreen;
};
export const AdminControls = () => {
const { poll, urlId } = usePoll();
const { t } = useTranslation("app");
const isWideScreen = useWideScreen();
const router = useRouter();
const [isSharingVisible, setSharingVisible] = React.useState(
!!router.query.sharing,
);
const queryClient = trpcNext.useContext();
const session = useUser();
const queryClient = trpc.useContext();
const { mutate: updatePollMutation } = useUpdatePollMutation();
React.useEffect(() => {
if (router.query.unsubscribe) {
updatePollMutation(
{ urlId: urlId, notifications: false },
{
onSuccess: () => {
toast.success(t("notificationsDisabled"));
posthog.capture("unsubscribed from notifications");
},
},
);
router.replace(`/admin/${router.query.urlId}`, undefined, {
shallow: true,
});
}
}, [urlId, router, updatePollMutation, t]);
const verifyEmail = trpc.useMutation(["polls.verification.verify"], {
onSuccess: () => {
toast.success(t("pollHasBeenVerified"));
queryClient.setQueryData(["polls.get", { urlId, admin }], {
...poll,
verified: true,
});
queryClient.poll.invalidate();
session.refresh();
posthog.capture("verified email");
},
@ -73,27 +106,74 @@ const PollPage: NextPage = () => {
}
});
React.useEffect(() => {
if (router.query.unsubscribe) {
updatePollMutation(
{ urlId: urlId, notifications: false },
{
onSuccess: () => {
toast.success(t("notificationsDisabled"));
posthog.capture("unsubscribed from notifications");
},
},
);
router.replace(`/admin/${router.query.urlId}`, undefined, {
shallow: true,
});
}
}, [urlId, router, updatePollMutation, t]);
return (
<>
<div className="mb-4 flex space-x-2 px-4 md:justify-end md:px-0">
<NotificationsToggle />
<ManagePoll placement={isWideScreen ? "bottom-end" : "bottom-start"} />
<Button
type="primary"
icon={<Share />}
onClick={() => {
setSharingVisible((value) => !value);
}}
>
{t("share")}
</Button>
</div>
<AnimatePresence initial={false}>
{isSharingVisible ? (
<motion.div
initial={{
opacity: 0,
scale: 0.8,
height: 0,
}}
animate={{
opacity: 1,
scale: 1,
height: "auto",
marginBottom: 16,
}}
exit={{
opacity: 0,
scale: 0.8,
height: 0,
marginBottom: 0,
}}
className="overflow-hidden"
>
<Sharing
onHide={() => {
setSharingVisible(false);
}}
/>
</motion.div>
) : null}
</AnimatePresence>
{poll.verified === false ? (
<div className="m-4 overflow-hidden rounded-lg border p-4 md:mx-0 md:mt-0">
<UnverifiedPollNotice />
</div>
) : null}
</>
);
};
export const Poll = (props: { children?: React.ReactNode }) => {
const { t } = useTranslation("app");
const { poll } = usePoll();
useTouchBeacon(poll.id);
const { participants } = useParticipants();
const names = React.useMemo(
() => participants?.map(({ name }) => name) ?? [],
[participants],
);
const checkIfWideScreen = () => window.innerWidth > 640;
const [isWideScreen, setIsWideScreen] = React.useState(checkIfWideScreen);
React.useEffect(() => {
const listener = () => setIsWideScreen(checkIfWideScreen());
@ -104,16 +184,9 @@ const PollPage: NextPage = () => {
};
}, []);
const [isWideScreen, setIsWideScreen] = React.useState(checkIfWideScreen);
const PollComponent = isWideScreen ? DesktopPoll : MobilePoll;
const names = React.useMemo(
() => participants?.map(({ name }) => name) ?? [],
[participants],
);
const [isSharingVisible, setSharingVisible] = React.useState(
!!router.query.sharing,
);
return (
<UserAvatarProvider seed={poll.id} names={names}>
<div className="relative max-w-full py-4 md:px-4">
@ -127,70 +200,7 @@ const PollPage: NextPage = () => {
width: Math.max(768, poll.options.length * 95 + 200 + 160),
}}
>
{admin ? (
<>
<div className="mb-4 flex space-x-2 px-4 md:justify-end md:px-0">
<NotificationsToggle />
<ManagePoll
placement={isWideScreen ? "bottom-end" : "bottom-start"}
/>
<Button
type="primary"
icon={<Share />}
onClick={() => {
setSharingVisible((value) => !value);
}}
>
{t("share")}
</Button>
</div>
<AnimatePresence initial={false}>
{isSharingVisible ? (
<motion.div
initial={{
opacity: 0,
scale: 0.8,
height: 0,
}}
animate={{
opacity: 1,
scale: 1,
height: "auto",
marginBottom: 16,
}}
exit={{
opacity: 0,
scale: 0.8,
height: 0,
marginBottom: 0,
}}
className="overflow-hidden"
>
<Sharing
onHide={() => {
setSharingVisible(false);
}}
/>
</motion.div>
) : null}
</AnimatePresence>
{poll.verified === false ? (
<div className="m-4 overflow-hidden rounded-lg border p-4 md:mx-0 md:mt-0">
<UnverifiedPollNotice />
</div>
) : null}
</>
) : null}
{!poll.admin && poll.adminUrlId ? (
<div className="mb-4 items-center justify-between rounded-lg px-4 md:flex md:space-x-4 md:border md:p-2 md:pl-4">
<div className="mb-4 font-medium md:mb-0">
{t("pollOwnerNotice", { name: poll.user.name })}
</div>
<a href={`/admin/${poll.adminUrlId}`} className="btn-default">
{t("goToAdmin")} &rarr;
</a>
</div>
) : null}
{props.children}
{poll.closed ? (
<div className="flex bg-sky-100 py-3 px-4 text-sky-700 md:mb-4 md:rounded-lg md:shadow-sm">
<div className="mr-2 rounded-md">
@ -255,7 +265,6 @@ const PollPage: NextPage = () => {
{participants ? <PollComponent /> : null}
</React.Suspense>
</div>
<React.Suspense fallback={<div className="p-4">{t("loading")}</div>}>
<Discussion />
</React.Suspense>
@ -264,5 +273,3 @@ const PollPage: NextPage = () => {
</UserAvatarProvider>
);
};
export default PollPage;

View file

@ -109,13 +109,13 @@ const ParticipantRow: React.VoidFunctionComponent<ParticipantRowProps> = ({
const confirmDeleteParticipant = useDeleteParticipantModal();
const session = useUser();
const { poll, getVote, options } = usePoll();
const { poll, getVote, options, admin } = usePoll();
const isYou = session.user && session.ownsObject(participant) ? true : false;
const isUnclaimed = !participant.userId;
const canEdit = !poll.closed && (poll.admin || isYou || isUnclaimed);
const canEdit = !poll.closed && (admin || isYou || isUnclaimed);
if (editMode) {
return (

View file

@ -39,6 +39,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
const {
poll,
admin,
targetTimeZone,
setTargetTimeZone,
getParticipantById,
@ -63,7 +64,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
const [selectedParticipantId, setSelectedParticipantId] = React.useState<
string | undefined
>(() => {
if (poll.admin) {
if (admin) {
// don't select a particpant if admin
return;
}
@ -81,7 +82,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
: undefined;
const [isEditing, setIsEditing] = React.useState(
!userAlreadyVoted && !poll.closed && !poll.admin,
!userAlreadyVoted && !poll.closed && !admin,
);
const formRef = React.useRef<HTMLFormElement>(null);
@ -214,7 +215,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
disabled={
poll.closed ||
// if user is participant (not admin)
(!poll.admin &&
(!admin &&
// and does not own this participant
!session.ownsObject(selectedParticipant) &&
// and the participant has been claimed by a different user
@ -239,7 +240,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
disabled={
poll.closed ||
// if user is participant (not admin)
(!poll.admin &&
(!admin &&
// and does not own this participant
!session.ownsObject(selectedParticipant) &&
// or the participant has been claimed by a different user

View file

@ -1,7 +1,6 @@
import posthog from "posthog-js";
import { trpc } from "../../utils/trpc";
import { usePoll } from "../poll-context";
import { trpc, trpcNext } from "../../utils/trpc";
import { ParticipantForm } from "./types";
export const normalizeVotes = (
@ -83,11 +82,10 @@ export const useDeleteParticipantMutation = () => {
};
export const useUpdatePollMutation = () => {
const { urlId, admin } = usePoll();
const queryClient = trpc.useContext();
const queryClient = trpcNext.useContext();
return trpc.useMutation(["polls.update"], {
onSuccess: (data) => {
queryClient.setQueryData(["polls.get", { urlId, admin }], data);
queryClient.poll.invalidate();
posthog.capture("updated poll", {
id: data.id,
});

View file

@ -7,7 +7,7 @@ import { usePoll } from "../poll-context";
import Tooltip from "../tooltip";
const PollSubheader: React.VoidFunctionComponent = () => {
const { poll } = usePoll();
const { poll, admin } = usePoll();
const { t } = useTranslation("app");
const { dayjs } = useDayjs();
return (
@ -23,7 +23,7 @@ const PollSubheader: React.VoidFunctionComponent = () => {
b: <span />,
}}
/>
{poll.legacy && poll.admin ? (
{poll.legacy && admin ? (
<Tooltip
width={400}
content="This poll was created with an older version of Rallly. Some features might not work."

View file

@ -4,30 +4,26 @@ import { useTranslation } from "next-i18next";
import React from "react";
import FullPageLoader from "@/components/full-page-loader";
import PollPage from "@/components/poll";
import StandardLayout from "@/components/layouts/standard-layout";
import { ParticipantsProvider } from "@/components/participants-provider";
import { AdminControls, Poll } from "@/components/poll";
import { PollContextProvider } from "@/components/poll-context";
import { ParticipantsProvider } from "../components/participants-provider";
import StandardLayout from "../components/standard-layout";
import { withSession } from "../components/user-provider";
import { withSessionSsr } from "../utils/auth";
import { trpc } from "../utils/trpc";
import { withPageTranslations } from "../utils/with-page-translations";
import Custom404 from "./404";
import { withSession } from "@/components/user-provider";
import { withSessionSsr } from "@/utils/auth";
import { trpcNext } from "@/utils/trpc";
import { withPageTranslations } from "@/utils/with-page-translations";
const PollPageLoader: NextPage = () => {
const { query, asPath } = useRouter();
const { query } = useRouter();
const { t } = useTranslation("app");
const urlId = query.urlId as string;
const [notFound, setNotFound] = React.useState(false);
const admin = /^\/admin/.test(asPath);
const pollQuery = trpc.useQuery(["polls.get", { urlId, admin }], {
onError: () => {
setNotFound(true);
const pollQuery = trpcNext.poll.getByAdminUrlId.useQuery(
{ urlId },
{
retry: false,
},
retry: false,
});
);
const poll = pollQuery.data;
@ -35,23 +31,28 @@ const PollPageLoader: NextPage = () => {
return (
<ParticipantsProvider pollId={poll.id}>
<StandardLayout>
<PollContextProvider poll={poll} urlId={urlId} admin={admin}>
<PollPage />
<PollContextProvider poll={poll} urlId={urlId} admin={true}>
<Poll>
<AdminControls />
</Poll>
</PollContextProvider>
</StandardLayout>
</ParticipantsProvider>
);
}
if (notFound) {
return <Custom404 />;
}
return <FullPageLoader>{t("loading")}</FullPageLoader>;
};
export const getServerSideProps: GetServerSideProps = withSessionSsr(
withPageTranslations(["common", "app", "errors"]),
{
onPrefetch: async (ssg, ctx) => {
await ssg.poll.getByAdminUrlId.fetch({
urlId: ctx.params?.urlId as string,
});
},
},
);
export default withSession(PollPageLoader);

58
src/pages/p/[urlId].tsx Normal file
View file

@ -0,0 +1,58 @@
import { GetServerSideProps, NextPage } from "next";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import React from "react";
import FullPageLoader from "@/components/full-page-loader";
import { ParticipantsProvider } from "@/components/participants-provider";
import { Poll } from "@/components/poll";
import { PollContextProvider } from "@/components/poll-context";
import { withSession } from "@/components/user-provider";
import { withSessionSsr } from "@/utils/auth";
import { trpcNext } from "@/utils/trpc";
import { withPageTranslations } from "@/utils/with-page-translations";
import StandardLayout from "../../components/layouts/standard-layout";
import ModalProvider from "../../components/modal/modal-provider";
import { DayjsProvider } from "../../utils/dayjs";
const PollPageLoader: NextPage = () => {
const { query } = useRouter();
const { t } = useTranslation("app");
const urlId = query.urlId as string;
const pollQuery = trpcNext.poll.getByParticipantUrlId.useQuery({ urlId });
const poll = pollQuery.data;
if (poll) {
return (
<ModalProvider>
<DayjsProvider>
<ParticipantsProvider pollId={poll.id}>
<StandardLayout>
<PollContextProvider poll={poll} urlId={urlId} admin={false}>
<Poll />
</PollContextProvider>
</StandardLayout>
</ParticipantsProvider>
</DayjsProvider>
</ModalProvider>
);
}
return <FullPageLoader>{t("loading")}</FullPageLoader>;
};
export const getServerSideProps: GetServerSideProps = withSessionSsr(
withPageTranslations(["common", "app", "errors"]),
{
onPrefetch: async (ssg, ctx) => {
await ssg.poll.getByParticipantUrlId.fetch({
urlId: ctx.params?.urlId as string,
});
},
},
);
export default withSession(PollPageLoader);

View file

@ -1,7 +1,7 @@
import { GetStaticProps } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import PageLayout from "@/components/page-layout";
import PageLayout from "@/components/layouts/page-layout";
const PrivacyPolicy = () => {
return (

View file

@ -2,8 +2,8 @@ import { NextPage } from "next";
import { withSessionSsr } from "@/utils/auth";
import StandardLayout from "../components/layouts/standard-layout";
import { Profile } from "../components/profile";
import StandardLayout from "../components/standard-layout";
import { withSession } from "../components/user-provider";
import { withPageTranslations } from "../utils/with-page-translations";

View file

@ -2,20 +2,21 @@ import { createRouter } from "../createRouter";
import { mergeRouters, router } from "../trpc";
import { auth } from "./auth";
import { login } from "./login";
import { polls } from "./polls";
import { legacyPolls, poll } from "./polls";
import { user } from "./user";
import { whoami } from "./whoami";
const legacyRouter = createRouter()
.merge("user.", user)
.merge(login)
.merge("polls.", polls);
.merge("polls.", legacyPolls);
export const appRouter = mergeRouters(
legacyRouter.interop(),
router({
whoami,
auth,
poll,
}),
);

View file

@ -11,6 +11,7 @@ import { createToken } from "../../utils/auth";
import { nanoid } from "../../utils/nanoid";
import { GetPollApiResponse } from "../../utils/trpc/types";
import { createRouter } from "../createRouter";
import { publicProcedure, router } from "../trpc";
import { comments } from "./polls/comments";
import { demo } from "./polls/demo";
import { participants } from "./polls/participants";
@ -24,8 +25,8 @@ const defaultSelectFields: {
location: true;
description: true;
createdAt: true;
participantUrlId: true;
adminUrlId: true;
participantUrlId: true;
verified: true;
closed: true;
legacy: true;
@ -45,8 +46,8 @@ const defaultSelectFields: {
location: true,
description: true,
createdAt: true,
participantUrlId: true,
adminUrlId: true,
participantUrlId: true,
verified: true,
closed: true,
legacy: true,
@ -76,7 +77,7 @@ const getPollIdFromAdminUrlId = async (urlId: string) => {
return res.id;
};
export const polls = createRouter()
export const legacyPolls = createRouter()
.merge("demo.", demo)
.merge("participants.", participants)
.merge("comments.", comments)
@ -189,37 +190,6 @@ export const polls = createRouter()
return { urlId: adminUrlId };
},
})
.query("get", {
input: z.object({
urlId: z.string(),
admin: z.boolean(),
}),
resolve: async ({ input, ctx }): Promise<GetPollApiResponse> => {
const poll = await prisma.poll.findFirst({
select: defaultSelectFields,
where: input.admin
? {
adminUrlId: input.urlId,
}
: {
participantUrlId: input.urlId,
},
});
if (!poll) {
throw new TRPCError({
code: "NOT_FOUND",
});
}
// We want to keep the adminUrlId in if the user is view
if (!input.admin && ctx.session.user?.id !== poll.user.id) {
return { ...poll, admin: input.admin, adminUrlId: "" };
}
return { ...poll, admin: input.admin };
},
})
.mutation("update", {
input: z.object({
urlId: z.string(),
@ -270,7 +240,7 @@ export const polls = createRouter()
},
});
return { ...poll, admin: true };
return { ...poll };
},
})
.mutation("delete", {
@ -297,3 +267,58 @@ export const polls = createRouter()
});
},
});
export const poll = router({
getByAdminUrlId: publicProcedure
.input(
z.object({
urlId: z.string(),
}),
)
.query(async ({ input }) => {
const res = await prisma.poll.findUnique({
select: defaultSelectFields,
where: {
adminUrlId: input.urlId,
},
rejectOnNotFound: false,
});
if (!res) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Poll not found",
});
}
return res;
}),
getByParticipantUrlId: publicProcedure
.input(
z.object({
urlId: z.string(),
}),
)
.query(async ({ input, ctx }) => {
const res = await prisma.poll.findUnique({
select: defaultSelectFields,
where: {
participantUrlId: input.urlId,
},
rejectOnNotFound: false,
});
if (!res) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Poll not found",
});
}
if (ctx.user.id === res.user.id) {
return res;
} else {
return { ...res, adminUrlId: "" };
}
}),
});

View file

@ -5,7 +5,11 @@ import {
unsealData,
} from "iron-session";
import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next";
import { GetServerSideProps, NextApiHandler } from "next";
import {
GetServerSideProps,
GetServerSidePropsContext,
NextApiHandler,
} from "next";
import { prisma } from "~/prisma/db";
@ -72,23 +76,56 @@ export function withSessionRoute(handler: NextApiHandler) {
}, sessionOptions);
}
export function withSessionSsr(handler: GetServerSideProps) {
return withIronSessionSsr(async (context) => {
const { req } = context;
const compose = (...fns: GetServerSideProps[]): GetServerSideProps => {
return async (ctx) => {
const res = { props: {} };
for (const getServerSideProps of fns) {
const fnRes = await getServerSideProps(ctx);
await setUser(req.session);
if ("props" in fnRes) {
res.props = {
...res.props,
...fnRes.props,
};
} else {
return { notFound: true };
}
}
const ssg = await createSSGHelperFromContext(context);
await ssg.whoami.get.prefetch();
return res;
};
};
const res = await handler(context);
export function withSessionSsr(
handler: GetServerSideProps | GetServerSideProps[],
options?: {
onPrefetch?: (
ssg: Awaited<ReturnType<typeof createSSGHelperFromContext>>,
ctx: GetServerSidePropsContext,
) => Promise<void>;
},
): GetServerSideProps {
const composedHandler = Array.isArray(handler)
? compose(...handler)
: handler;
return withIronSessionSsr(async (ctx) => {
const ssg = await createSSGHelperFromContext(ctx);
await ssg.whoami.get.prefetch(); // always prefetch user
if (options?.onPrefetch) {
try {
await options.onPrefetch(ssg, ctx);
} catch {
return {
notFound: true,
};
}
}
const res = await composedHandler(ctx);
if ("props" in res) {
return {
...res,
props: {
...res.props,
user: req.session.user,
trpcState: ssg.dehydrate(),
},
};
@ -156,11 +193,7 @@ export const mergeGuestsIntoUser = async (
export const getCurrentUser = async (
session: IronSession,
): Promise<{ isGuest: boolean; id: string }> => {
const user = session.user;
await setUser(session);
if (!user) {
throw new Error("Tried to get user but no user found.");
}
return user;
return session.user;
};

View file

@ -13,7 +13,6 @@ export type GetPollApiResponse = {
participantUrlId: string;
verified: boolean;
closed: boolean;
admin: boolean;
legacy: boolean;
demo: boolean;
notifications: boolean;