mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-15 18:06:48 +02:00
Prefetch user (#429)
This commit is contained in:
parent
37f777cace
commit
249376c43e
20 changed files with 220 additions and 181 deletions
|
@ -18,9 +18,9 @@ import {
|
||||||
UserDetailsData,
|
UserDetailsData,
|
||||||
UserDetailsForm,
|
UserDetailsForm,
|
||||||
} from "./forms";
|
} from "./forms";
|
||||||
import { SessionProps, useSession, withSession } from "./session";
|
|
||||||
import StandardLayout from "./standard-layout";
|
import StandardLayout from "./standard-layout";
|
||||||
import Steps from "./steps";
|
import Steps from "./steps";
|
||||||
|
import { useUser } from "./user-provider";
|
||||||
|
|
||||||
type StepName = "eventDetails" | "options" | "userDetails";
|
type StepName = "eventDetails" | "options" | "userDetails";
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ const required = <T,>(v: T | undefined): T => {
|
||||||
const initialNewEventData: NewEventData = { currentStep: 0 };
|
const initialNewEventData: NewEventData = { currentStep: 0 };
|
||||||
const sessionStorageKey = "newEventFormData";
|
const sessionStorageKey = "newEventFormData";
|
||||||
|
|
||||||
export interface CreatePollPageProps extends SessionProps {
|
export interface CreatePollPageProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -54,7 +54,7 @@ const Page: NextPage<CreatePollPageProps> = ({
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const session = useSession();
|
const session = useUser();
|
||||||
|
|
||||||
const [persistedFormData, setPersistedFormData] =
|
const [persistedFormData, setPersistedFormData] =
|
||||||
useSessionStorage<NewEventData>(sessionStorageKey, {
|
useSessionStorage<NewEventData>(sessionStorageKey, {
|
||||||
|
@ -228,4 +228,4 @@ const Page: NextPage<CreatePollPageProps> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withSession(Page);
|
export default Page;
|
||||||
|
|
|
@ -17,7 +17,7 @@ import NameInput from "../name-input";
|
||||||
import TruncatedLinkify from "../poll/truncated-linkify";
|
import TruncatedLinkify from "../poll/truncated-linkify";
|
||||||
import UserAvatar from "../poll/user-avatar";
|
import UserAvatar from "../poll/user-avatar";
|
||||||
import { usePoll } from "../poll-context";
|
import { usePoll } from "../poll-context";
|
||||||
import { isUnclaimed, useSession } from "../session";
|
import { isUnclaimed, useUser } from "../user-provider";
|
||||||
|
|
||||||
interface CommentForm {
|
interface CommentForm {
|
||||||
authorName: string;
|
authorName: string;
|
||||||
|
@ -68,7 +68,7 @@ const Discussion: React.VoidFunctionComponent = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const session = useSession();
|
const session = useUser();
|
||||||
|
|
||||||
const { register, reset, control, handleSubmit, formState } =
|
const { register, reset, control, handleSubmit, formState } =
|
||||||
useForm<CommentForm>({
|
useForm<CommentForm>({
|
||||||
|
|
|
@ -15,8 +15,8 @@ import { GetPollApiResponse } from "@/utils/trpc/types";
|
||||||
import { useDayjs } from "../utils/dayjs";
|
import { useDayjs } from "../utils/dayjs";
|
||||||
import ErrorPage from "./error-page";
|
import ErrorPage from "./error-page";
|
||||||
import { useParticipants } from "./participants-provider";
|
import { useParticipants } from "./participants-provider";
|
||||||
import { useSession } from "./session";
|
|
||||||
import { useRequiredContext } from "./use-required-context";
|
import { useRequiredContext } from "./use-required-context";
|
||||||
|
import { useUser } from "./user-provider";
|
||||||
|
|
||||||
type PollContextValue = {
|
type PollContextValue = {
|
||||||
userAlreadyVoted: boolean;
|
userAlreadyVoted: boolean;
|
||||||
|
@ -61,7 +61,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { participants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
const [isDeleted, setDeleted] = React.useState(false);
|
const [isDeleted, setDeleted] = React.useState(false);
|
||||||
const { user } = useSession();
|
const { user } = useUser();
|
||||||
const [targetTimeZone, setTargetTimeZone] =
|
const [targetTimeZone, setTargetTimeZone] =
|
||||||
React.useState(getBrowserTimeZone);
|
React.useState(getBrowserTimeZone);
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ import { useTouchBeacon } from "./poll/use-touch-beacon";
|
||||||
import { UserAvatarProvider } from "./poll/user-avatar";
|
import { UserAvatarProvider } from "./poll/user-avatar";
|
||||||
import VoteIcon from "./poll/vote-icon";
|
import VoteIcon from "./poll/vote-icon";
|
||||||
import { usePoll } from "./poll-context";
|
import { usePoll } from "./poll-context";
|
||||||
import { useSession } from "./session";
|
|
||||||
import Sharing from "./sharing";
|
import Sharing from "./sharing";
|
||||||
|
import { useUser } from "./user-provider";
|
||||||
|
|
||||||
const PollPage: NextPage = () => {
|
const PollPage: NextPage = () => {
|
||||||
const { poll, urlId, admin } = usePoll();
|
const { poll, urlId, admin } = usePoll();
|
||||||
|
@ -40,7 +40,7 @@ const PollPage: NextPage = () => {
|
||||||
|
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
|
|
||||||
const session = useSession();
|
const session = useUser();
|
||||||
|
|
||||||
const queryClient = trpc.useContext();
|
const queryClient = trpc.useContext();
|
||||||
const plausible = usePlausible();
|
const plausible = usePlausible();
|
||||||
|
|
|
@ -6,7 +6,7 @@ import CompactButton from "@/components/compact-button";
|
||||||
import Pencil from "@/components/icons/pencil-alt.svg";
|
import Pencil from "@/components/icons/pencil-alt.svg";
|
||||||
import Trash from "@/components/icons/trash.svg";
|
import Trash from "@/components/icons/trash.svg";
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
import { useSession } from "@/components/session";
|
import { useUser } from "@/components/user-provider";
|
||||||
|
|
||||||
import { ParticipantFormSubmitted } from "../types";
|
import { ParticipantFormSubmitted } from "../types";
|
||||||
import { useDeleteParticipantModal } from "../use-delete-participant-modal";
|
import { useDeleteParticipantModal } from "../use-delete-participant-modal";
|
||||||
|
@ -108,7 +108,7 @@ const ParticipantRow: React.VoidFunctionComponent<ParticipantRowProps> = ({
|
||||||
|
|
||||||
const confirmDeleteParticipant = useDeleteParticipantModal();
|
const confirmDeleteParticipant = useDeleteParticipantModal();
|
||||||
|
|
||||||
const session = useSession();
|
const session = useUser();
|
||||||
const { poll, getVote, options } = usePoll();
|
const { poll, getVote, options } = usePoll();
|
||||||
|
|
||||||
const isYou = session.user && session.ownsObject(participant) ? true : false;
|
const isYou = session.user && session.ownsObject(participant) ? true : false;
|
||||||
|
|
|
@ -18,8 +18,8 @@ import { Button } from "../button";
|
||||||
import { styleMenuItem } from "../menu-styles";
|
import { styleMenuItem } from "../menu-styles";
|
||||||
import NameInput from "../name-input";
|
import NameInput from "../name-input";
|
||||||
import { useParticipants } from "../participants-provider";
|
import { useParticipants } from "../participants-provider";
|
||||||
import { isUnclaimed, useSession } from "../session";
|
|
||||||
import TimeZonePicker from "../time-zone-picker";
|
import TimeZonePicker from "../time-zone-picker";
|
||||||
|
import { isUnclaimed, useUser } from "../user-provider";
|
||||||
import GroupedOptions from "./mobile-poll/grouped-options";
|
import GroupedOptions from "./mobile-poll/grouped-options";
|
||||||
import {
|
import {
|
||||||
normalizeVotes,
|
normalizeVotes,
|
||||||
|
@ -50,7 +50,7 @@ const MobilePoll: React.VoidFunctionComponent = () => {
|
||||||
const { participants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
const { timeZone } = poll;
|
const { timeZone } = poll;
|
||||||
|
|
||||||
const session = useSession();
|
const session = useUser();
|
||||||
|
|
||||||
const form = useForm<ParticipantForm>({
|
const form = useForm<ParticipantForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { usePlausible } from "next-plausible";
|
||||||
|
|
||||||
import { trpc } from "../../utils/trpc";
|
import { trpc } from "../../utils/trpc";
|
||||||
import { usePoll } from "../poll-context";
|
import { usePoll } from "../poll-context";
|
||||||
import { useSession } from "../session";
|
import { useUser } from "../user-provider";
|
||||||
import { ParticipantForm } from "./types";
|
import { ParticipantForm } from "./types";
|
||||||
|
|
||||||
export const normalizeVotes = (
|
export const normalizeVotes = (
|
||||||
|
@ -17,7 +17,7 @@ export const normalizeVotes = (
|
||||||
|
|
||||||
export const useAddParticipantMutation = () => {
|
export const useAddParticipantMutation = () => {
|
||||||
const queryClient = trpc.useContext();
|
const queryClient = trpc.useContext();
|
||||||
const session = useSession();
|
const session = useUser();
|
||||||
const plausible = usePlausible();
|
const plausible = usePlausible();
|
||||||
|
|
||||||
return trpc.useMutation(["polls.participants.add"], {
|
return trpc.useMutation(["polls.participants.add"], {
|
||||||
|
|
|
@ -12,10 +12,10 @@ import { trpc } from "../utils/trpc";
|
||||||
import { EmptyState } from "./empty-state";
|
import { EmptyState } from "./empty-state";
|
||||||
import LoginForm from "./login-form";
|
import LoginForm from "./login-form";
|
||||||
import { UserDetails } from "./profile/user-details";
|
import { UserDetails } from "./profile/user-details";
|
||||||
import { useSession } from "./session";
|
import { useUser } from "./user-provider";
|
||||||
|
|
||||||
export const Profile: React.VoidFunctionComponent = () => {
|
export const Profile: React.VoidFunctionComponent = () => {
|
||||||
const { user } = useSession();
|
const { user } = useUser();
|
||||||
const { dayjs } = useDayjs();
|
const { dayjs } = useDayjs();
|
||||||
|
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { useForm } from "react-hook-form";
|
||||||
import { requiredString, validEmail } from "../../utils/form-validation";
|
import { requiredString, validEmail } from "../../utils/form-validation";
|
||||||
import { trpc } from "../../utils/trpc";
|
import { trpc } from "../../utils/trpc";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useSession } from "../session";
|
|
||||||
import { TextInput } from "../text-input";
|
import { TextInput } from "../text-input";
|
||||||
|
import { useUser } from "../user-provider";
|
||||||
|
|
||||||
export interface UserDetailsProps {
|
export interface UserDetailsProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
@ -30,7 +30,7 @@ export const UserDetails: React.VoidFunctionComponent<UserDetailsProps> = ({
|
||||||
defaultValues: { name, email },
|
defaultValues: { name, email },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { refresh } = useSession();
|
const { refresh } = useUser();
|
||||||
|
|
||||||
const changeName = trpc.useMutation("user.changeName", {
|
const changeName = trpc.useMutation("user.changeName", {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
import { IronSessionData } from "iron-session";
|
|
||||||
import React from "react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
import { trpc } from "@/utils/trpc";
|
|
||||||
|
|
||||||
import FullPageLoader from "./full-page-loader";
|
|
||||||
import { useRequiredContext } from "./use-required-context";
|
|
||||||
|
|
||||||
export type UserSessionData = NonNullable<IronSessionData["user"]>;
|
|
||||||
|
|
||||||
export type SessionProps = {
|
|
||||||
user: UserSessionData;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ParticipantOrComment = {
|
|
||||||
userId: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UserSessionDataExtended =
|
|
||||||
| {
|
|
||||||
isGuest: true;
|
|
||||||
id: string;
|
|
||||||
shortName: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
isGuest: false;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
shortName: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SessionContextValue = {
|
|
||||||
logout: () => void;
|
|
||||||
user: UserSessionDataExtended;
|
|
||||||
refresh: () => void;
|
|
||||||
ownsObject: (obj: ParticipantOrComment) => boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SessionContext =
|
|
||||||
React.createContext<SessionContextValue | null>(null);
|
|
||||||
|
|
||||||
SessionContext.displayName = "SessionContext";
|
|
||||||
|
|
||||||
export const SessionProvider: React.VoidFunctionComponent<{
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}> = ({ children }) => {
|
|
||||||
const queryClient = trpc.useContext();
|
|
||||||
const { data: user, refetch, isLoading } = trpc.useQuery(["session.get"]);
|
|
||||||
|
|
||||||
const logout = trpc.useMutation(["session.destroy"], {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["session.get"]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <FullPageLoader>Loading user…</FullPageLoader>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionData: SessionContextValue = {
|
|
||||||
user: {
|
|
||||||
...user,
|
|
||||||
shortName:
|
|
||||||
// try to get the first name in the event
|
|
||||||
// that the user entered a full name
|
|
||||||
user.isGuest
|
|
||||||
? user.id.substring(0, 10)
|
|
||||||
: user.name.length > 12 && user.name.indexOf(" ") !== -1
|
|
||||||
? user.name.substring(0, user.name.indexOf(" "))
|
|
||||||
: user.name,
|
|
||||||
},
|
|
||||||
refresh: () => {
|
|
||||||
refetch();
|
|
||||||
},
|
|
||||||
isLoading,
|
|
||||||
logout: () => {
|
|
||||||
toast.promise(logout.mutateAsync(), {
|
|
||||||
loading: "Logging out…",
|
|
||||||
success: "Logged out",
|
|
||||||
error: "Failed to log out",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
ownsObject: (obj) => {
|
|
||||||
return obj.userId === user.id;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SessionContext.Provider value={sessionData}>
|
|
||||||
{children}
|
|
||||||
</SessionContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSession = () => {
|
|
||||||
return useRequiredContext(SessionContext);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const withSession = <P extends SessionProps>(
|
|
||||||
component: React.ComponentType<P>,
|
|
||||||
) => {
|
|
||||||
const ComposedComponent: React.VoidFunctionComponent<P> = (props: P) => {
|
|
||||||
const Component = component;
|
|
||||||
return (
|
|
||||||
<SessionProvider>
|
|
||||||
<Component {...props} />
|
|
||||||
</SessionProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ComposedComponent.displayName = component.displayName;
|
|
||||||
return ComposedComponent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isUnclaimed = (obj: ParticipantOrComment) => !obj.userId;
|
|
|
@ -27,7 +27,7 @@ import { useModal } from "./modal";
|
||||||
import ModalProvider, { useModalContext } from "./modal/modal-provider";
|
import ModalProvider, { useModalContext } from "./modal/modal-provider";
|
||||||
import Popover from "./popover";
|
import Popover from "./popover";
|
||||||
import Preferences from "./preferences";
|
import Preferences from "./preferences";
|
||||||
import { useSession } from "./session";
|
import { useUser } from "./user-provider";
|
||||||
|
|
||||||
const HomeLink = () => {
|
const HomeLink = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -40,7 +40,7 @@ const HomeLink = () => {
|
||||||
const MobileNavigation: React.VoidFunctionComponent<{
|
const MobileNavigation: React.VoidFunctionComponent<{
|
||||||
openLoginModal: () => void;
|
openLoginModal: () => void;
|
||||||
}> = ({ openLoginModal }) => {
|
}> = ({ openLoginModal }) => {
|
||||||
const { user } = useSession();
|
const { user } = useUser();
|
||||||
const { t } = useTranslation(["common", "app"]);
|
const { t } = useTranslation(["common", "app"]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -152,7 +152,7 @@ const AppMenu: React.VoidFunctionComponent<{ className?: string }> = ({
|
||||||
const UserDropdown: React.VoidFunctionComponent<
|
const UserDropdown: React.VoidFunctionComponent<
|
||||||
DropdownProps & { openLoginModal: () => void }
|
DropdownProps & { openLoginModal: () => void }
|
||||||
> = ({ children, openLoginModal, ...forwardProps }) => {
|
> = ({ children, openLoginModal, ...forwardProps }) => {
|
||||||
const { logout, user } = useSession();
|
const { logout, user } = useUser();
|
||||||
const { t } = useTranslation(["common", "app"]);
|
const { t } = useTranslation(["common", "app"]);
|
||||||
const modalContext = useModalContext();
|
const modalContext = useModalContext();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -243,7 +243,7 @@ const UserDropdown: React.VoidFunctionComponent<
|
||||||
const StandardLayout: React.VoidFunctionComponent<{
|
const StandardLayout: React.VoidFunctionComponent<{
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}> = ({ children, ...rest }) => {
|
}> = ({ children, ...rest }) => {
|
||||||
const { user } = useSession();
|
const { user } = useUser();
|
||||||
const { t } = useTranslation(["common", "app"]);
|
const { t } = useTranslation(["common", "app"]);
|
||||||
const [loginModal, openLoginModal] = useModal({
|
const [loginModal, openLoginModal] = useModal({
|
||||||
footer: null,
|
footer: null,
|
||||||
|
|
109
src/components/user-provider.tsx
Normal file
109
src/components/user-provider.tsx
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { UserSession } from "@/utils/auth";
|
||||||
|
|
||||||
|
import { trpcNext } from "../utils/trpc";
|
||||||
|
import { useRequiredContext } from "./use-required-context";
|
||||||
|
|
||||||
|
export const UserContext =
|
||||||
|
React.createContext<{
|
||||||
|
user: UserSession & { shortName: string };
|
||||||
|
refresh: () => void;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
ownsObject: (obj: { userId: string | null }) => boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
export const useUser = () => {
|
||||||
|
return useRequiredContext(UserContext, "UserContext");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthenticatedUser = () => {
|
||||||
|
const { user, ...rest } = useRequiredContext(UserContext, "UserContext");
|
||||||
|
if (user.isGuest) {
|
||||||
|
throw new Error("Forget to prefetch user identity");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, ...rest };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IfAuthenticated = (props: { children?: React.ReactNode }) => {
|
||||||
|
const { user } = useUser();
|
||||||
|
if (user.isGuest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{props.children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IfGuest = (props: { children?: React.ReactNode }) => {
|
||||||
|
const { user } = useUser();
|
||||||
|
if (!user.isGuest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{props.children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
|
const { t } = useTranslation("app");
|
||||||
|
|
||||||
|
const { data: user, refetch } = trpcNext.whoami.get.useQuery();
|
||||||
|
const logout = trpcNext.whoami.destroy.useMutation();
|
||||||
|
|
||||||
|
const shortName = user
|
||||||
|
? user.isGuest === false
|
||||||
|
? user.name
|
||||||
|
: `${t("guest")}-${user.id.substring(user.id.length - 4)}`
|
||||||
|
: t("guest");
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Provider
|
||||||
|
value={{
|
||||||
|
user: { ...user, shortName },
|
||||||
|
refresh: refetch,
|
||||||
|
ownsObject: ({ userId }) => {
|
||||||
|
if (userId && user.id === userId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
logout: async () => {
|
||||||
|
await logout.mutateAsync();
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ParticipantOrComment = {
|
||||||
|
userId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
export const withSession = <P extends {} = {}>(
|
||||||
|
component: React.ComponentType<P>,
|
||||||
|
) => {
|
||||||
|
const ComposedComponent: React.VoidFunctionComponent<P> = (props: P) => {
|
||||||
|
const Component = component;
|
||||||
|
return (
|
||||||
|
<UserProvider>
|
||||||
|
<Component {...props} />
|
||||||
|
</UserProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ComposedComponent.displayName = `withUser(${component.displayName})`;
|
||||||
|
return ComposedComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Stop using this function. All object
|
||||||
|
*/
|
||||||
|
export const isUnclaimed = (obj: ParticipantOrComment) => !obj.userId;
|
|
@ -49,10 +49,7 @@ const MyApp: NextPage<AppProps> = ({ Component, pageProps }) => {
|
||||||
customDomain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
|
customDomain={process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
|
||||||
trackOutboundLinks={true}
|
trackOutboundLinks={true}
|
||||||
selfHosted={true}
|
selfHosted={true}
|
||||||
enabled={
|
enabled={!!process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN}
|
||||||
typeof window !== undefined &&
|
|
||||||
!!process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DefaultSeo
|
<DefaultSeo
|
||||||
openGraph={{
|
openGraph={{
|
||||||
|
|
|
@ -2,10 +2,11 @@ import { GetServerSideProps } from "next";
|
||||||
|
|
||||||
import CreatePoll from "@/components/create-poll";
|
import CreatePoll from "@/components/create-poll";
|
||||||
|
|
||||||
|
import { withSession } from "../components/user-provider";
|
||||||
import { withSessionSsr } from "../utils/auth";
|
import { withSessionSsr } from "../utils/auth";
|
||||||
import { withPageTranslations } from "../utils/with-page-translations";
|
import { withPageTranslations } from "../utils/with-page-translations";
|
||||||
|
|
||||||
export default CreatePoll;
|
export default withSession(CreatePoll);
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
||||||
withPageTranslations(["common", "app"]),
|
withPageTranslations(["common", "app"]),
|
||||||
|
|
|
@ -6,10 +6,10 @@ import React from "react";
|
||||||
import FullPageLoader from "@/components/full-page-loader";
|
import FullPageLoader from "@/components/full-page-loader";
|
||||||
import PollPage from "@/components/poll";
|
import PollPage from "@/components/poll";
|
||||||
import { PollContextProvider } from "@/components/poll-context";
|
import { PollContextProvider } from "@/components/poll-context";
|
||||||
import { withSession } from "@/components/session";
|
|
||||||
|
|
||||||
import { ParticipantsProvider } from "../components/participants-provider";
|
import { ParticipantsProvider } from "../components/participants-provider";
|
||||||
import StandardLayout from "../components/standard-layout";
|
import StandardLayout from "../components/standard-layout";
|
||||||
|
import { withSession } from "../components/user-provider";
|
||||||
import { withSessionSsr } from "../utils/auth";
|
import { withSessionSsr } from "../utils/auth";
|
||||||
import { trpc } from "../utils/trpc";
|
import { trpc } from "../utils/trpc";
|
||||||
import { withPageTranslations } from "../utils/with-page-translations";
|
import { withPageTranslations } from "../utils/with-page-translations";
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { NextPage } from "next";
|
||||||
import { withSessionSsr } from "@/utils/auth";
|
import { withSessionSsr } from "@/utils/auth";
|
||||||
|
|
||||||
import { Profile } from "../components/profile";
|
import { Profile } from "../components/profile";
|
||||||
import { withSession } from "../components/session";
|
|
||||||
import StandardLayout from "../components/standard-layout";
|
import StandardLayout from "../components/standard-layout";
|
||||||
|
import { withSession } from "../components/user-provider";
|
||||||
import { withPageTranslations } from "../utils/with-page-translations";
|
import { withPageTranslations } from "../utils/with-page-translations";
|
||||||
|
|
||||||
const Page: NextPage = () => {
|
const Page: NextPage = () => {
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { prisma } from "~/prisma/db";
|
|
||||||
|
|
||||||
import { createRouter } from "../createRouter";
|
import { createRouter } from "../createRouter";
|
||||||
import { mergeRouters, publicProcedure, router } from "../trpc";
|
import { mergeRouters, router } from "../trpc";
|
||||||
import { login } from "./login";
|
import { login } from "./login";
|
||||||
import { polls } from "./polls";
|
import { polls } from "./polls";
|
||||||
import { session } from "./session";
|
import { session } from "./session";
|
||||||
import { user } from "./user";
|
import { user } from "./user";
|
||||||
|
import { whoami } from "./whoami";
|
||||||
|
|
||||||
const legacyRouter = createRouter()
|
const legacyRouter = createRouter()
|
||||||
.merge("user.", user)
|
.merge("user.", user)
|
||||||
|
@ -18,24 +15,7 @@ const legacyRouter = createRouter()
|
||||||
export const appRouter = mergeRouters(
|
export const appRouter = mergeRouters(
|
||||||
legacyRouter.interop(),
|
legacyRouter.interop(),
|
||||||
router({
|
router({
|
||||||
p: router({
|
whoami,
|
||||||
touch: publicProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
pollId: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
await prisma.poll.update({
|
|
||||||
where: {
|
|
||||||
id: input.pollId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
touchedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
30
src/server/routers/whoami.ts
Normal file
30
src/server/routers/whoami.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { prisma } from "~/prisma/db";
|
||||||
|
|
||||||
|
import { createGuestUser, UserSession } from "../../utils/auth";
|
||||||
|
import { publicProcedure, router } from "../trpc";
|
||||||
|
|
||||||
|
export const whoami = router({
|
||||||
|
get: publicProcedure.query(async ({ ctx }): Promise<UserSession> => {
|
||||||
|
if (ctx.user.isGuest) {
|
||||||
|
return { isGuest: true, id: ctx.user.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
select: { id: true, name: true, email: true },
|
||||||
|
where: { id: ctx.user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
const guestUser = await createGuestUser();
|
||||||
|
ctx.session.user = guestUser;
|
||||||
|
await ctx.session.save();
|
||||||
|
|
||||||
|
return guestUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isGuest: false, ...user };
|
||||||
|
}),
|
||||||
|
destroy: publicProcedure.mutation(async ({ ctx }) => {
|
||||||
|
ctx.session.destroy();
|
||||||
|
}),
|
||||||
|
});
|
|
@ -9,6 +9,7 @@ import { GetServerSideProps, NextApiHandler } from "next";
|
||||||
|
|
||||||
import { prisma } from "~/prisma/db";
|
import { prisma } from "~/prisma/db";
|
||||||
|
|
||||||
|
import { createSSGHelperFromContext } from "../server/context";
|
||||||
import { randomid } from "./nanoid";
|
import { randomid } from "./nanoid";
|
||||||
|
|
||||||
const sessionOptions: IronSessionOptions = {
|
const sessionOptions: IronSessionOptions = {
|
||||||
|
@ -20,12 +21,42 @@ const sessionOptions: IronSessionOptions = {
|
||||||
ttl: 0, // basically forever
|
ttl: 0, // basically forever
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RegisteredUserSession = {
|
||||||
|
isGuest: false;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GuestUserSession = {
|
||||||
|
isGuest: true;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserSession = GuestUserSession | RegisteredUserSession;
|
||||||
|
|
||||||
|
const setUser = async (session: IronSession) => {
|
||||||
|
if (!session.user) {
|
||||||
|
session.user = await createGuestUser();
|
||||||
|
await session.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.user.isGuest) {
|
||||||
|
// Check registered user still exists
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
session.user = await createGuestUser();
|
||||||
|
await session.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function withSessionRoute(handler: NextApiHandler) {
|
export function withSessionRoute(handler: NextApiHandler) {
|
||||||
return withIronSessionApiRoute(async (req, res) => {
|
return withIronSessionApiRoute(async (req, res) => {
|
||||||
if (!req.session.user) {
|
await setUser(req.session);
|
||||||
req.session.user = await createGuestUser();
|
|
||||||
await req.session.save();
|
|
||||||
}
|
|
||||||
return await handler(req, res);
|
return await handler(req, res);
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
}
|
}
|
||||||
|
@ -33,14 +64,23 @@ export function withSessionRoute(handler: NextApiHandler) {
|
||||||
export function withSessionSsr(handler: GetServerSideProps) {
|
export function withSessionSsr(handler: GetServerSideProps) {
|
||||||
return withIronSessionSsr(async (context) => {
|
return withIronSessionSsr(async (context) => {
|
||||||
const { req } = context;
|
const { req } = context;
|
||||||
if (!req.session.user) {
|
|
||||||
req.session.user = await createGuestUser();
|
await setUser(req.session);
|
||||||
await req.session.save();
|
|
||||||
}
|
const ssg = await createSSGHelperFromContext(context);
|
||||||
|
await ssg.whoami.get.prefetch();
|
||||||
|
|
||||||
const res = await handler(context);
|
const res = await handler(context);
|
||||||
|
|
||||||
if ("props" in res) {
|
if ("props" in res) {
|
||||||
return { ...res, props: { ...res.props, user: req.session.user } };
|
return {
|
||||||
|
...res,
|
||||||
|
props: {
|
||||||
|
...res.props,
|
||||||
|
user: req.session.user,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -18,7 +18,6 @@ test("should be able to vote and comment on a poll", async ({ page }) => {
|
||||||
await page.locator("data-testid=vote-selector >> nth=2").click();
|
await page.locator("data-testid=vote-selector >> nth=2").click();
|
||||||
await page.click("text='Save'");
|
await page.click("text='Save'");
|
||||||
await expect(page.locator("text='Test user'")).toBeVisible();
|
await expect(page.locator("text='Test user'")).toBeVisible();
|
||||||
await expect(page.locator("text=Guest")).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator("data-testid=participant-row >> nth=4").locator("text=You"),
|
page.locator("data-testid=participant-row >> nth=4").locator("text=You"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue