Use client side fetching for user data (#211)

This commit is contained in:
Luke Vella 2022-06-28 12:13:49 +01:00 committed by GitHub
parent 1d768083ee
commit 368f324865
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 93 additions and 63 deletions

View file

@ -2,16 +2,9 @@ import "iron-session";
declare module "iron-session" { declare module "iron-session" {
export interface IronSessionData { export interface IronSessionData {
user: user: {
| { id: string;
id: string; isGuest: boolean;
name: string; };
email: string;
isGuest: false;
}
| {
id: string;
isGuest: true;
};
} }
} }

View file

@ -1,6 +1,6 @@
import { formatRelative } from "date-fns"; import { formatRelative } from "date-fns";
import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
@ -10,6 +10,7 @@ import User from "@/components/icons/user.svg";
import { trpc } from "../utils/trpc"; import { trpc } from "../utils/trpc";
import { EmptyState } from "./empty-state"; import { EmptyState } from "./empty-state";
import LoginForm from "./login-form";
import { UserDetails } from "./profile/user-details"; import { UserDetails } from "./profile/user-details";
import { useSession } from "./session"; import { useSession } from "./session";
@ -19,21 +20,24 @@ export const Profile: React.VoidFunctionComponent = () => {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const { data: userPolls } = trpc.useQuery(["user.getPolls"]); const { data: userPolls } = trpc.useQuery(["user.getPolls"]);
const router = useRouter();
const createdPolls = userPolls?.polls; const createdPolls = userPolls?.polls;
React.useEffect(() => { if (user.isGuest) {
if (!user) { return (
router.replace("/new"); <div className="card my-4 p-0">
} <Head>
}, [user, router]); <title>Profile - Login</title>
</Head>
if (!user || user.isGuest) { <LoginForm />
return null; </div>
);
} }
return ( return (
<div className="mx-auto max-w-3xl py-4 lg:mx-0"> <div className="mx-auto max-w-3xl py-4 lg:mx-0">
<Head>
<title>Profile - {user.name}</title>
</Head>
<div className="mb-4 flex items-center px-4"> <div className="mb-4 flex items-center px-4">
<div className="mr-4 inline-flex h-14 w-14 items-center justify-center rounded-lg bg-primary-50"> <div className="mr-4 inline-flex h-14 w-14 items-center justify-center rounded-lg bg-primary-50">
<User className="h-7 text-primary-500" /> <User className="h-7 text-primary-500" />

View file

@ -11,8 +11,8 @@ import { TextInput } from "../text-input";
export interface UserDetailsProps { export interface UserDetailsProps {
userId: string; userId: string;
name: string; name?: string;
email: string; email?: string;
} }
const MotionButton = motion(Button); const MotionButton = motion(Button);

View file

@ -4,6 +4,7 @@ import toast from "react-hot-toast";
import { trpc } from "@/utils/trpc"; import { trpc } from "@/utils/trpc";
import FullPageLoader from "./full-page-loader";
import { useRequiredContext } from "./use-required-context"; import { useRequiredContext } from "./use-required-context";
export type UserSessionData = NonNullable<IronSessionData["user"]>; export type UserSessionData = NonNullable<IronSessionData["user"]>;
@ -16,13 +17,23 @@ type ParticipantOrComment = {
userId: string | null; userId: string | null;
}; };
export type UserSessionDataExtended = UserSessionData & { export type UserSessionDataExtended =
shortName: string; | {
}; isGuest: true;
id: string;
shortName: string;
}
| {
isGuest: false;
id: string;
name: string;
shortName: string;
email: string;
};
type SessionContextValue = { type SessionContextValue = {
logout: () => void; logout: () => void;
user: UserSessionDataExtended | null; user: UserSessionDataExtended;
refresh: () => void; refresh: () => void;
ownsObject: (obj: ParticipantOrComment) => boolean; ownsObject: (obj: ParticipantOrComment) => boolean;
isLoading: boolean; isLoading: boolean;
@ -35,14 +46,9 @@ SessionContext.displayName = "SessionContext";
export const SessionProvider: React.VoidFunctionComponent<{ export const SessionProvider: React.VoidFunctionComponent<{
children?: React.ReactNode; children?: React.ReactNode;
defaultUser: UserSessionData; }> = ({ children }) => {
}> = ({ children, defaultUser }) => {
const queryClient = trpc.useContext(); const queryClient = trpc.useContext();
const { const { data: user, refetch, isLoading } = trpc.useQuery(["session.get"]);
data: user = defaultUser,
refetch,
isLoading,
} = trpc.useQuery(["session.get"]);
const logout = trpc.useMutation(["session.destroy"], { const logout = trpc.useMutation(["session.destroy"], {
onSuccess: () => { onSuccess: () => {
@ -50,6 +56,10 @@ export const SessionProvider: React.VoidFunctionComponent<{
}, },
}); });
if (!user) {
return <FullPageLoader>Loading user</FullPageLoader>;
}
const sessionData: SessionContextValue = { const sessionData: SessionContextValue = {
user: { user: {
...user, ...user,
@ -95,7 +105,7 @@ export const withSession = <P extends SessionProps>(
const ComposedComponent: React.VoidFunctionComponent<P> = (props: P) => { const ComposedComponent: React.VoidFunctionComponent<P> = (props: P) => {
const Component = component; const Component = component;
return ( return (
<SessionProvider defaultUser={props.user}> <SessionProvider>
<Component {...props} /> <Component {...props} />
</SessionProvider> </SessionProvider>
); );

View file

@ -100,8 +100,6 @@ export const getServerSideProps: GetServerSideProps = withSessionSsr(
req.session.user = { req.session.user = {
isGuest: false, isGuest: false,
name: user.name,
email: user.email,
id: user.id, id: user.id,
}; };

View file

@ -1,40 +1,29 @@
import { NextPage } from "next"; import { NextPage } from "next";
import Head from "next/head";
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { withSessionSsr } from "@/utils/auth"; import { withSessionSsr } from "@/utils/auth";
import { Profile } from "../components/profile"; import { Profile } from "../components/profile";
import { SessionProvider, UserSessionData } from "../components/session"; import { withSession } from "../components/session";
import StandardLayout from "../components/standard-layout"; import StandardLayout from "../components/standard-layout";
const Page: NextPage<{ user: UserSessionData }> = ({ user }) => { const Page: NextPage = () => {
const name = user.isGuest ? user.id : user.name;
return ( return (
<SessionProvider defaultUser={user}> <StandardLayout>
<Head> <Profile />
<title>Profile - {name}</title> </StandardLayout>
</Head>
<StandardLayout>
<Profile />
</StandardLayout>
</SessionProvider>
); );
}; };
export const getServerSideProps = withSessionSsr( export const getServerSideProps = withSessionSsr(
async ({ locale = "en", query, req }) => { async ({ locale = "en", query }) => {
if (!req.session.user || req.session.user.isGuest) {
return { redirect: { destination: "/new" }, props: {} };
}
return { return {
props: { props: {
...(await serverSideTranslations(locale, ["app"])), ...(await serverSideTranslations(locale, ["app"])),
...query, ...query,
user: req.session.user,
}, },
}; };
}, },
); );
export default Page; export default withSession(Page);

View file

@ -96,6 +96,19 @@ export const polls = createRouter()
resolve: async ({ ctx, input }): Promise<{ urlId: string }> => { resolve: async ({ ctx, input }): Promise<{ urlId: string }> => {
const adminUrlId = await nanoid(); const adminUrlId = await nanoid();
let verified = false;
if (ctx.session.user.isGuest === false) {
const user = await prisma.user.findUnique({
where: { id: ctx.session.user.id },
});
// If user is logged in with the same email address
if (user?.email === input.user.email) {
verified = true;
}
}
const poll = await prisma.poll.create({ const poll = await prisma.poll.create({
data: { data: {
id: await nanoid(), id: await nanoid(),
@ -106,9 +119,7 @@ export const polls = createRouter()
description: input.description, description: input.description,
authorName: input.user.name, authorName: input.user.name,
demo: input.demo, demo: input.demo,
verified: verified: verified,
ctx.session.user?.isGuest === false &&
ctx.session.user.email === input.user.email,
adminUrlId, adminUrlId,
participantUrlId: await nanoid(), participantUrlId: await nanoid(),
user: { user: {

View file

@ -49,8 +49,6 @@ export const verification = createRouter()
ctx.session.user = { ctx.session.user = {
id: poll.user.id, id: poll.user.id,
isGuest: false, isGuest: false,
name: poll.user.name,
email: poll.user.email,
}; };
await ctx.session.save(); await ctx.session.save();
}, },

View file

@ -1,9 +1,36 @@
import { prisma } from "~/prisma/db";
import { createGuestUser } from "../../utils/auth";
import { createRouter } from "../createRouter"; import { createRouter } from "../createRouter";
export const session = createRouter() export const session = createRouter()
.query("get", { .query("get", {
async resolve({ ctx }) { async resolve({
return ctx.session.user; ctx,
}): Promise<
| { isGuest: true; id: string }
| { isGuest: false; id: string; name: string; email: string }
> {
if (ctx.session.user.isGuest) {
return { isGuest: true, id: ctx.session.user.id };
}
const user = await prisma.user.findUnique({
where: { id: ctx.session.user.id },
});
if (!user) {
ctx.session.user = await createGuestUser();
await ctx.session.save();
return { isGuest: true, id: ctx.session.user.id };
}
return {
isGuest: false,
id: user.id,
email: user.email,
name: user.name,
};
}, },
}) })
.mutation("destroy", { .mutation("destroy", {

View file

@ -51,7 +51,7 @@ export const createToken = async <T extends Record<string, unknown>>(
}); });
}; };
const createGuestUser = async (): Promise<{ export const createGuestUser = async (): Promise<{
isGuest: true; isGuest: true;
id: string; id: string;
}> => { }> => {