├── .eslintignore ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .eslintrc.js ├── .gitignore ├── README.md ├── app │ ├── [username] │ │ ├── error.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── codegen.ts ├── components.json ├── components │ ├── apollo-wrapper.tsx │ ├── create-post-form.tsx │ ├── date-time-display.tsx │ ├── delete-post-dialog.tsx │ ├── edit-profile.tsx │ ├── feed.tsx │ ├── home-feed.tsx │ ├── mode-toggle.tsx │ ├── post-menu.tsx │ ├── profile-page.tsx │ ├── theme-provider.tsx │ ├── ui │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts │ └── user-avatar.tsx ├── lib │ ├── apollo-client.ts │ ├── constants.ts │ ├── helpers.ts │ └── utils.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public │ ├── next.svg │ └── vercel.svg ├── tailwind.config.js ├── tailwind.config.ts └── tsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.js ├── server ├── .eslintrc.js ├── .gitignore ├── package.json ├── src │ └── index.ts └── tsconfig.json └── turbo.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .turbo 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Glenn Reyes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL for React developers 2 | 3 | Welcome to the GraphQL workshop for React developers! ☀️ 4 | 5 | In this workshop, we'll be building a Twitter clone using GraphQL and React. We'll be using [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) for the GraphQL server and [Apollo Client](https://www.apollographql.com/docs/react) for the React app. 6 | 7 | - 🌱 Learn GraphQL basics 8 | - 🥑 Build GraphQL queries & mutations 9 | - 🥝 Get familiar with the GraphQL client 10 | - 🍇 Implement queries & mutations on the client 11 | - 🔑 Access control & authorization 12 | - 🎛 Production deployment 13 | 14 | ## 🔧 Setup 15 | 16 | 1. Get started by cloning this repo and installing the dependencies: 17 | 18 | ```sh 19 | git clone https://github.com/glennreyes/react-graphql-workshop.git 20 | cd react-graphql-workshop 21 | pnpm install 22 | ``` 23 | 24 | 2. Start the development servers: 25 | 26 | ```sh 27 | pnpm dev 28 | ``` 29 | 30 | 3. Open GraphiQL at http://localhost:4000/graphql and the React app at http://localhost:3000. 31 | 32 | ## 📚 Exercises 33 | 34 | ### Learn GraphQL basics 35 | 36 | - GraphiQL 37 | - Schema 38 | - Types 39 | - Resolvers 40 | 41 | Create a query `hello` that takes an argument `name`. Based on what the user inputs, return a greeting. For example, if the user inputs `Glenn`, return `Hello Glenn!`. 42 | 43 | > #### Useful links 44 | > 45 | > - https://the-guild.dev/graphql/yoga-server/docs 46 | > - https://graphql.org/learn 47 | > 48 | > #### Default types 49 | > 50 | > - `String` 51 | > - `Int` 52 | > - `Float` 53 | > - `Boolean` 54 | > - `ID` 55 | > 56 | > #### Schema definition example 57 | > 58 | > ```graphql 59 | > type Person { 60 | > id: ID! # Not nullable 61 | > name: String # Nullable 62 | > age: Int 63 | > weight: Float 64 | > isOnline: Boolean 65 | > posts: [Post!]! # Not nullable (but empty list is fine) 66 | > } 67 | > 68 | > type Post { 69 | > id: ID! 70 | > slug: String! 71 | > text: String! 72 | > } 73 | > 74 | > type Query { 75 | > allPersons: [Person!]! 76 | > personById(id: ID!): Person 77 | > allPosts: [Post!]! 78 | > postBySlug(slug: String!): Post 79 | > } 80 | > 81 | > type Mutation { 82 | > createPost(message: String!): Post! 83 | > } 84 | > ``` 85 | 86 | > #### Resolver function 87 | > 88 | > ```ts 89 | > (parent, args, context, info) => result; 90 | > ``` 91 | 92 | ### Build GraphQL queries & mutations 93 | 94 | #### Build queries 95 | 96 | 1. 💎 Implement `allPosts` query 97 | 2. 💎 Implement `me` query 98 | 3. 💎 Implement `user` query 99 | 100 | #### Build mutations 101 | 102 | 1. 💎 Implement `createPost` mutation 103 | 2. 💎 Implement `deletePost` mutation 104 | 3. 💎 Implement `updateUser` mutation 105 | 106 | > #### Useful links 107 | > 108 | > Query & mutation field: 109 | > 110 | > - https://pothos-graphql.dev/docs/guide/queries-and-mutations 111 | > - https://pothos-graphql.dev/docs/api/schema-builder#queryfieldname-field 112 | > - https://pothos-graphql.dev/docs/api/schema-builder#mutationfieldname-field 113 | > 114 | > #### Prisma 115 | > 116 | > Make sure you're in the `server` directory: 117 | > 118 | > ```sh 119 | > pnpm prisma migrate reset --skip-generate # Reset database 120 | > pnpm prisma db push # Push prisma schema to database 121 | > pnpm prisma generate # Generate Prisma client 122 | > pnpm seed # Seed database with fake data 123 | > ``` 124 | 125 | ### Get familiar with the GraphQL client 126 | 127 | - https://github.com/apollographql/apollo-client-nextjs 128 | 129 | ### Implement queries & mutations on the client 130 | 131 | #### Queries 132 | 133 | 1. Implement query in `user-avatar.tsx` 134 | 2. Implement query in `home-feed.tsx` 135 | 3. Implement queries in `profile-page.tsx` 136 | 4. Implement query in `edit-profile.tsx` 137 | 138 | > Use `useSuspenseQuery` from `@apollo/experimental-nextjs-app-support/ssr` to fetch data on the server. 139 | > 140 | > - https://github.com/apollographql/apollo-client-nextjs#in-ssr 141 | 142 | #### Mutations 143 | 144 | 1. Implement mutation in `create-post-form.tsx` 145 | 2. Implement mutation in `delete-post-dialog.tsx` 146 | 3. Implement mutation in `edit-profile.tsx` 147 | 148 | > Use `useMutation` from `@apollo/client` 149 | > 150 | > - https://www.apollographql.com/docs/react/data/mutations 151 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.BaseConfig} */ 2 | module.exports = { 3 | extends: ['next/core-web-vitals', 'banana/react'], 4 | parserOptions: { project: true }, 5 | rules: { 6 | '@typescript-eslint/strict-boolean-expressions': 'off', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # graphql code generator 38 | graphql/generated/ 39 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /app/app/[username]/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { notFound } from 'next/navigation'; 4 | 5 | export default function Error() { 6 | notFound(); 7 | } 8 | -------------------------------------------------------------------------------- /app/app/[username]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ProfilePage } from '@/components/profile-page'; 2 | import { notFound } from 'next/navigation'; 3 | 4 | interface UserProfileProps { 5 | params: { username: string }; 6 | } 7 | 8 | export default async function UserProfile({ params }: UserProfileProps) { 9 | const handle = decodeURIComponent(params.username); 10 | 11 | if (!handle.startsWith('@')) { 12 | notFound(); 13 | } 14 | 15 | const username = handle.slice(1); 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glennreyes/react-graphql-workshop/782b1e19b3a468be507eddf93ef8d0f4b97d5b02/app/app/favicon.ico -------------------------------------------------------------------------------- /app/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@/components/theme-provider'; 2 | import './globals.css'; 3 | import { ApolloWrapper } from '@/components/apollo-wrapper'; 4 | import { ModeToggle } from '@/components/mode-toggle'; 5 | import { Toaster } from '@/components/ui/toaster'; 6 | import { UserAvatar } from '@/components/user-avatar'; 7 | import { cn } from '@/lib/utils'; 8 | import { Twitter } from 'lucide-react'; 9 | import type { Metadata } from 'next'; 10 | import { Inter } from 'next/font/google'; 11 | import Link from 'next/link'; 12 | 13 | const inter = Inter({ subsets: ['latin'], variable: '--font-sans', weight: 'variable' }); 14 | 15 | export const metadata: Metadata = { 16 | description: 'Generated by create next app', 17 | title: 'Create Next App', 18 | }; 19 | 20 | export default function RootLayout({ children }: { children: React.ReactNode }) { 21 | return ( 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | 34 |
35 |
36 |
37 |
{children}
38 |
39 |
40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { CreatePostForm } from '@/components/create-post-form'; 2 | import { HomeFeed } from '@/components/home-feed'; 3 | 4 | export default async function Home() { 5 | return ( 6 |
7 | 8 |
9 |

Feed

10 | 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/codegen.ts: -------------------------------------------------------------------------------- 1 | import type { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const config: CodegenConfig = { 4 | config: { 5 | scalars: { 6 | DateTime: 'string', 7 | }, 8 | strictScalars: true, 9 | }, 10 | documents: 'graphql/**/*.graphql', 11 | generates: { 12 | 'graphql/generated/': { 13 | preset: 'client', 14 | presetConfig: { 15 | fragmentMasking: false, 16 | }, 17 | }, 18 | }, 19 | overwrite: true, 20 | schema: 'http://localhost:4000/graphql', 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/components/apollo-wrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { graphqlEndpoint } from '@/lib/constants'; 4 | import { ApolloLink, HttpLink } from '@apollo/client'; 5 | import { 6 | ApolloNextAppProvider, 7 | NextSSRApolloClient, 8 | NextSSRInMemoryCache, 9 | SSRMultipartLink, 10 | } from '@apollo/experimental-nextjs-app-support/ssr'; 11 | 12 | function makeClient() { 13 | const httpLink = new HttpLink({ 14 | fetchOptions: { cache: 'no-store' }, 15 | uri: graphqlEndpoint, 16 | }); 17 | 18 | return new NextSSRApolloClient({ 19 | cache: new NextSSRInMemoryCache(), 20 | link: 21 | typeof window === 'undefined' 22 | ? ApolloLink.from([new SSRMultipartLink({ stripDefer: true }), httpLink]) 23 | : httpLink, 24 | }); 25 | } 26 | 27 | export function ApolloWrapper({ children }: React.PropsWithChildren) { 28 | return {children}; 29 | } 30 | -------------------------------------------------------------------------------- /app/components/create-post-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { zodResolver } from '@hookform/resolvers/zod'; 4 | import { useForm } from 'react-hook-form'; 5 | import { z } from 'zod'; 6 | import { Button } from './ui/button'; 7 | import { Form, FormControl, FormField, FormItem } from './ui/form'; 8 | import { Textarea } from './ui/textarea'; 9 | import { useToast } from './ui/use-toast'; 10 | 11 | export function CreatePostForm() { 12 | const formSchema = z.object({ 13 | message: z.string().min(1, 'Please enter a message.'), 14 | }); 15 | const form = useForm>({ 16 | defaultValues: { 17 | message: '', 18 | }, 19 | resolver: zodResolver(formSchema), 20 | }); 21 | const { toast } = useToast(); 22 | 23 | // TODO: 💎 Add a mutation to create a post. 24 | // - `createPost` should be a mutation that takes a `message` string. 25 | // - Refetch the `AllPostsDocument` query after the mutation. 26 | 27 | async function onSubmit(values: z.infer) { 28 | try { 29 | // TODO: 💎 Add a mutation to create a post. 30 | // - Call `createPost` with the `message` from `values`. 31 | // - Throw an error if the mutation has any errors. 32 | console.info({ values }); 33 | 34 | toast({ description: 'Your message has been sent.' }); 35 | form.reset(); 36 | } catch { 37 | toast({ 38 | description: 'There was a problem with your request.', 39 | title: 'Uh oh! Something went wrong.', 40 | variant: 'destructive', 41 | }); 42 | } 43 | } 44 | 45 | // TODO: 💎 Add a mutation to create a post. 46 | // - Add a `pending` boolean based on the mutation's loading state. 47 | const pending = false; 48 | 49 | return ( 50 |
51 | 52 |

Post

53 | ( 57 | 58 | 59 |