mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 18:26:34 +02:00
Refactor admin and participant page (#464)
This commit is contained in:
parent
875e48f1fe
commit
d397654de7
20 changed files with 348 additions and 236 deletions
|
@ -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: {
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
|
@ -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");
|
|
@ -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="/">
|
|
@ -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,67 +106,11 @@ 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]);
|
||||
|
||||
const checkIfWideScreen = () => window.innerWidth > 640;
|
||||
|
||||
const [isWideScreen, setIsWideScreen] = React.useState(checkIfWideScreen);
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => setIsWideScreen(checkIfWideScreen());
|
||||
|
||||
window.addEventListener("resize", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<Head>
|
||||
<title>{poll.title}</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<div
|
||||
className="mx-auto max-w-full lg:mx-0"
|
||||
style={{
|
||||
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"}
|
||||
/>
|
||||
<ManagePoll placement={isWideScreen ? "bottom-end" : "bottom-start"} />
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Share />}
|
||||
|
@ -180,17 +157,50 @@ const PollPage: NextPage = () => {
|
|||
</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")} →
|
||||
</a>
|
||||
</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;
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => setIsWideScreen(checkIfWideScreen());
|
||||
|
||||
window.addEventListener("resize", listener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [isWideScreen, setIsWideScreen] = React.useState(checkIfWideScreen);
|
||||
const PollComponent = isWideScreen ? DesktopPoll : MobilePoll;
|
||||
|
||||
return (
|
||||
<UserAvatarProvider seed={poll.id} names={names}>
|
||||
<div className="relative max-w-full py-4 md:px-4">
|
||||
<Head>
|
||||
<title>{poll.title}</title>
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
</Head>
|
||||
<div
|
||||
className="mx-auto max-w-full lg:mx-0"
|
||||
style={{
|
||||
width: Math.max(768, poll.options.length * 95 + 200 + 160),
|
||||
}}
|
||||
>
|
||||
{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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
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
58
src/pages/p/[urlId].tsx
Normal 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);
|
|
@ -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 (
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -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: "" };
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -13,7 +13,6 @@ export type GetPollApiResponse = {
|
|||
participantUrlId: string;
|
||||
verified: boolean;
|
||||
closed: boolean;
|
||||
admin: boolean;
|
||||
legacy: boolean;
|
||||
demo: boolean;
|
||||
notifications: boolean;
|
||||
|
|
Loading…
Add table
Reference in a new issue