From b14dd1d89a21845c9ed37e2bdfb7c041d387737f Mon Sep 17 00:00:00 2001 From: Ben Awad Date: Wed, 12 Aug 2020 10:01:29 -0500 Subject: [PATCH] global-error-handling --- web/src/components/InputField.tsx | 9 +++- web/src/components/Layout.tsx | 16 ++++++ web/src/components/NavBar.tsx | 2 +- web/src/components/Wrapper.tsx | 4 +- web/src/generated/graphql.tsx | 46 +++++++++++++++-- web/src/graphql/mutations/createPost.graphql | 11 ++++ web/src/pages/create-post.tsx | 53 ++++++++++++++++++++ web/src/pages/index.tsx | 12 +++-- web/src/utils/createUrqlClient.ts | 20 ++++++-- web/src/utils/useIsAuth.ts | 13 +++++ 10 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 web/src/components/Layout.tsx create mode 100644 web/src/graphql/mutations/createPost.graphql create mode 100644 web/src/pages/create-post.tsx create mode 100644 web/src/utils/useIsAuth.ts diff --git a/web/src/components/InputField.tsx b/web/src/components/InputField.tsx index 8372a106..6adedf6e 100644 --- a/web/src/components/InputField.tsx +++ b/web/src/components/InputField.tsx @@ -5,11 +5,13 @@ import { FormLabel, Input, FormErrorMessage, + Textarea, } from "@chakra-ui/core"; type InputFieldProps = InputHTMLAttributes & { label: string; name: string; + textarea?: boolean; }; // '' => false @@ -17,14 +19,19 @@ type InputFieldProps = InputHTMLAttributes & { export const InputField: React.FC = ({ label, + textarea, size: _, ...props }) => { + let InputOrTextarea = Input; + if (textarea) { + InputOrTextarea = Textarea; + } const [field, { error }] = useField(props); return ( {label} - + {error ? {error} : null} ); diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx new file mode 100644 index 00000000..6df36516 --- /dev/null +++ b/web/src/components/Layout.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Wrapper, WrapperVariant } from "./Wrapper"; +import { NavBar } from "./NavBar"; + +interface LayoutProps { + variant?: WrapperVariant; +} + +export const Layout: React.FC = ({ children, variant }) => { + return ( + <> + + {children} + + ); +}; diff --git a/web/src/components/NavBar.tsx b/web/src/components/NavBar.tsx index cc81aca6..763dc694 100644 --- a/web/src/components/NavBar.tsx +++ b/web/src/components/NavBar.tsx @@ -47,7 +47,7 @@ export const NavBar: React.FC = ({}) => { } return ( - + {body} ); diff --git a/web/src/components/Wrapper.tsx b/web/src/components/Wrapper.tsx index 6e269e45..1ea789af 100644 --- a/web/src/components/Wrapper.tsx +++ b/web/src/components/Wrapper.tsx @@ -1,8 +1,10 @@ import React from "react"; import { Box } from "@chakra-ui/core"; +export type WrapperVariant = "small" | "regular"; + interface WrapperProps { - variant?: "small" | "regular"; + variant?: WrapperVariant; } export const Wrapper: React.FC = ({ diff --git a/web/src/generated/graphql.tsx b/web/src/generated/graphql.tsx index 902b0cb2..e5122b74 100644 --- a/web/src/generated/graphql.tsx +++ b/web/src/generated/graphql.tsx @@ -28,18 +28,21 @@ export type QueryPostArgs = { export type Post = { __typename?: 'Post'; id: Scalars['Float']; + title: Scalars['String']; + text: Scalars['String']; + points: Scalars['Float']; + creatorId: Scalars['Float']; createdAt: Scalars['String']; updatedAt: Scalars['String']; - title: Scalars['String']; }; export type User = { __typename?: 'User'; id: Scalars['Float']; - createdAt: Scalars['String']; - updatedAt: Scalars['String']; username: Scalars['String']; email: Scalars['String']; + createdAt: Scalars['String']; + updatedAt: Scalars['String']; }; export type Mutation = { @@ -56,7 +59,7 @@ export type Mutation = { export type MutationCreatePostArgs = { - title: Scalars['String']; + input: PostInput; }; @@ -92,6 +95,11 @@ export type MutationLoginArgs = { usernameOrEmail: Scalars['String']; }; +export type PostInput = { + title: Scalars['String']; + text: Scalars['String']; +}; + export type UserResponse = { __typename?: 'UserResponse'; errors?: Maybe>; @@ -145,6 +153,19 @@ export type ChangePasswordMutation = ( ) } ); +export type CreatePostMutationVariables = Exact<{ + input: PostInput; +}>; + + +export type CreatePostMutation = ( + { __typename?: 'Mutation' } + & { createPost: ( + { __typename?: 'Post' } + & Pick + ) } +); + export type ForgotPasswordMutationVariables = Exact<{ email: Scalars['String']; }>; @@ -246,6 +267,23 @@ export const ChangePasswordDocument = gql` export function useChangePasswordMutation() { return Urql.useMutation(ChangePasswordDocument); }; +export const CreatePostDocument = gql` + mutation CreatePost($input: PostInput!) { + createPost(input: $input) { + id + createdAt + updatedAt + title + text + points + creatorId + } +} + `; + +export function useCreatePostMutation() { + return Urql.useMutation(CreatePostDocument); +}; export const ForgotPasswordDocument = gql` mutation ForgotPassword($email: String!) { forgotPassword(email: $email) diff --git a/web/src/graphql/mutations/createPost.graphql b/web/src/graphql/mutations/createPost.graphql new file mode 100644 index 00000000..eba1bc1f --- /dev/null +++ b/web/src/graphql/mutations/createPost.graphql @@ -0,0 +1,11 @@ +mutation CreatePost($input: PostInput!) { + createPost(input: $input) { + id + createdAt + updatedAt + title + text + points + creatorId + } +} diff --git a/web/src/pages/create-post.tsx b/web/src/pages/create-post.tsx new file mode 100644 index 00000000..17900635 --- /dev/null +++ b/web/src/pages/create-post.tsx @@ -0,0 +1,53 @@ +import { Box, Button } from "@chakra-ui/core"; +import { Form, Formik } from "formik"; +import { withUrqlClient } from "next-urql"; +import { useRouter } from "next/router"; +import React from "react"; +import { InputField } from "../components/InputField"; +import { Layout } from "../components/Layout"; +import { useCreatePostMutation } from "../generated/graphql"; +import { createUrqlClient } from "../utils/createUrqlClient"; +import { useIsAuth } from "../utils/useIsAuth"; + +const CreatePost: React.FC<{}> = ({}) => { + const router = useRouter(); + useIsAuth(); + const [, createPost] = useCreatePostMutation(); + return ( + + { + const { error } = await createPost({ input: values }); + if (!error) { + router.push("/"); + } + }} + > + {({ isSubmitting }) => ( +
+ + + + + + + )} +
+
+ ); +}; + +export default withUrqlClient(createUrqlClient)(CreatePost); diff --git a/web/src/pages/index.tsx b/web/src/pages/index.tsx index f1c5ba85..ff36fe76 100644 --- a/web/src/pages/index.tsx +++ b/web/src/pages/index.tsx @@ -2,20 +2,24 @@ import { NavBar } from "../components/NavBar"; import { withUrqlClient } from "next-urql"; import { createUrqlClient } from "../utils/createUrqlClient"; import { usePostsQuery } from "../generated/graphql"; +import { Layout } from "../components/Layout"; +import { Link } from "@chakra-ui/core"; +import NextLink from "next/link"; const Index = () => { const [{ data }] = usePostsQuery(); return ( - <> - -
hello world
+ + + create post +
{!data ? (
loading...
) : ( data.posts.map((p) =>
{p.title}
) )} - +
); }; diff --git a/web/src/utils/createUrqlClient.ts b/web/src/utils/createUrqlClient.ts index 54d3b36f..7a87f256 100644 --- a/web/src/utils/createUrqlClient.ts +++ b/web/src/utils/createUrqlClient.ts @@ -1,13 +1,26 @@ -import { dedupExchange, fetchExchange } from "urql"; import { cacheExchange } from "@urql/exchange-graphcache"; +import { dedupExchange, Exchange, fetchExchange } from "urql"; +import { pipe, tap } from "wonka"; import { + LoginMutation, LogoutMutation, - MeQuery, MeDocument, - LoginMutation, + MeQuery, RegisterMutation, } from "../generated/graphql"; import { betterUpdateQuery } from "./betterUpdateQuery"; +import Router from "next/router"; + +const errorExchange: Exchange = ({ forward }) => (ops$) => { + return pipe( + forward(ops$), + tap(({ error }) => { + if (error?.message.includes("not authenticated")) { + Router.replace("/login"); + } + }) + ); +}; export const createUrqlClient = (ssrExchange: any) => ({ url: "http://localhost:4000/graphql", @@ -62,6 +75,7 @@ export const createUrqlClient = (ssrExchange: any) => ({ }, }, }), + errorExchange, ssrExchange, fetchExchange, ], diff --git a/web/src/utils/useIsAuth.ts b/web/src/utils/useIsAuth.ts new file mode 100644 index 00000000..e3fd307a --- /dev/null +++ b/web/src/utils/useIsAuth.ts @@ -0,0 +1,13 @@ +import { useMeQuery } from "../generated/graphql"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +export const useIsAuth = () => { + const [{ data, fetching }] = useMeQuery(); + const router = useRouter(); + useEffect(() => { + if (!fetching && !data?.me) { + router.replace("/login"); + } + }, [fetching, data, router]); +};