├── .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 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/app/components/date-time-display.tsx:
--------------------------------------------------------------------------------
1 | interface DateTimeDisplayProps {
2 | value: Date;
3 | }
4 |
5 | export function DateTimeDisplay({ value }: DateTimeDisplayProps) {
6 | return (
7 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/app/components/delete-post-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import type { Dispatch, SetStateAction } from 'react';
4 | import {
5 | AlertDialog,
6 | AlertDialogAction,
7 | AlertDialogCancel,
8 | AlertDialogContent,
9 | AlertDialogDescription,
10 | AlertDialogFooter,
11 | AlertDialogHeader,
12 | AlertDialogTitle,
13 | } from './ui/alert-dialog';
14 | import { Button } from './ui/button';
15 | import { useToast } from './ui/use-toast';
16 |
17 | interface DeletePostDialogProps {
18 | id: string;
19 | open: boolean;
20 | setOpen: Dispatch>;
21 | username: string;
22 | }
23 |
24 | export function DeletePostDialog({ id, open, setOpen, username }: DeletePostDialogProps) {
25 | // TODO: 💎 Add a mutation to delete a post.
26 | // - `deletePost` should be a mutation that takes an `id` string.
27 | // - Refetch the `AllPostsDocument` and `UserDocument` queries after the mutation.
28 | const { toast } = useToast();
29 |
30 | async function onSubmit() {
31 | try {
32 | // TODO: 💎 Add a mutation to delete a post.
33 | // - Call `deletePost` with the `id` of the post.
34 | // - Throw an error if the mutation has any errors.
35 | console.info({ id, username });
36 |
37 | toast({ description: 'Your post has been deleted.' });
38 | } catch {
39 | toast({
40 | description: 'There was a problem with your request.',
41 | title: 'Uh oh! Something went wrong.',
42 | variant: 'destructive',
43 | });
44 | }
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 | Are you absolutely sure?
52 |
53 | This will permanently delete your post. This action cannot be undone.
54 |
55 |
56 |
57 | Cancel
58 |
59 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/app/components/edit-profile.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { zodResolver } from '@hookform/resolvers/zod';
4 | import { useState } from 'react';
5 | import { useForm } from 'react-hook-form';
6 | import { z } from 'zod';
7 | import { Button } from './ui/button';
8 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
9 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form';
10 | import { Input } from './ui/input';
11 | import { Textarea } from './ui/textarea';
12 | import { useToast } from './ui/use-toast';
13 |
14 | interface EditProfileProps {
15 | username: string;
16 | }
17 |
18 | export function EditProfile({ username }: EditProfileProps) {
19 | // TODO: 💎 Add a query to get the user's profile.
20 | // - `user` should be a query that returns the user with the given `username`.
21 | const bio = undefined;
22 | const displayName = undefined;
23 | const photo = undefined;
24 |
25 | const [open, setOpen] = useState(false);
26 | const formSchema = z.object({
27 | bio: z.string().or(z.undefined()),
28 | displayName: z.string().min(3, 'Please enter a display name.').or(z.undefined()),
29 | photo: z.string().url('Please enter a valid URL.').or(z.undefined()),
30 | username: z.string().min(3, 'Please enter a username.').or(z.undefined()),
31 | });
32 | const form = useForm>({
33 | defaultValues: {
34 | bio,
35 | displayName,
36 | photo,
37 | username,
38 | },
39 | resolver: zodResolver(formSchema),
40 | });
41 | const { toast } = useToast();
42 |
43 | // TODO: 💎 Add a mutation to update a user.
44 | // - `updateUser` should be a mutation that takes a `username` string, `displayName` string, `photo` string, and `bio` string.
45 | // - Refetch the `UserDocument` query after the mutation.
46 |
47 | async function onSubmit(values: z.infer) {
48 | try {
49 | // TODO: 💎 Add a mutation to update a user.
50 | // - Call `updateUser` with variables
51 | console.info({ values });
52 |
53 | form.reset();
54 | setOpen(false);
55 |
56 | toast({ description: 'Your profile has been updated.' });
57 | } catch {
58 | toast({
59 | description: 'There was a problem with your request.',
60 | title: 'Uh oh! Something went wrong.',
61 | variant: 'destructive',
62 | });
63 | }
64 | }
65 |
66 | // TODO: 💎 Add a mutation to update a user.
67 | // - Add a `pending` boolean based on the mutation's loading state.
68 | const pending = false;
69 |
70 | return (
71 |
156 | );
157 | }
158 |
--------------------------------------------------------------------------------
/app/components/feed.tsx:
--------------------------------------------------------------------------------
1 | import { getInitials } from '@/lib/helpers';
2 | import { DateTimeDisplay } from './date-time-display';
3 | import { PostMenu } from './post-menu';
4 | import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
5 |
6 | interface FeedProps {
7 | me?: { username: string };
8 | posts: {
9 | createdAt: string;
10 | id: string;
11 | message: string;
12 | user: { displayName?: string; photo?: string; username: string };
13 | }[];
14 | }
15 |
16 | export function Feed({ posts, me }: FeedProps) {
17 | return (
18 |
19 | {posts.map((post) => (
20 |
21 |
22 |
23 |
24 | {getInitials(post.user.displayName ?? 'Anonymous')}
25 |
26 |
27 |
47 |
48 | ))}
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/app/components/home-feed.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Feed } from './feed';
4 |
5 | export function HomeFeed() {
6 | // TODO: 💎 Add a query to get the current user's feed.
7 | // - `me` should be a query that returns the current user.
8 | // - `allPosts` should be a query that returns all posts.
9 | const username = 'anonymous';
10 | const posts: {
11 | createdAt: string;
12 | id: string;
13 | message: string;
14 | user: { displayName?: string; photo?: string; username: string };
15 | }[] = [];
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/app/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger,
9 | } from '@/components/ui/dropdown-menu';
10 | import { Moon, Sun } from 'lucide-react';
11 | import { useTheme } from 'next-themes';
12 |
13 | export function ModeToggle() {
14 | const { setTheme } = useTheme();
15 |
16 | return (
17 |
18 |
19 |
24 |
25 |
26 | setTheme('light')}>Light
27 | setTheme('dark')}>Dark
28 | setTheme('system')}>System
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/app/components/post-menu.tsx:
--------------------------------------------------------------------------------
1 | import { LucideMoreHorizontal, Trash } from 'lucide-react';
2 | import { useState } from 'react';
3 | import { DeletePostDialog } from './delete-post-dialog';
4 | import { Button } from './ui/button';
5 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './ui/dropdown-menu';
6 |
7 | interface PostMenuProps {
8 | id: string;
9 | username: string;
10 | }
11 |
12 | export function PostMenu({ id, username }: PostMenuProps) {
13 | const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
14 |
15 | return (
16 | <>
17 |
18 |
19 |
22 |
23 |
24 | setDeleteDialogOpen(true)}>
25 |
26 | Delete Post
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/components/profile-page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { getInitials } from '@/lib/helpers';
4 | import { EditProfile } from './edit-profile';
5 | import { Feed } from './feed';
6 | import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
7 |
8 | interface ProfilePageProps {
9 | username: string;
10 | }
11 |
12 | export function ProfilePage({ username }: ProfilePageProps) {
13 | // TODO: 💎 Add a query to get the current user.
14 | // - `me` should be a query that returns the current user.
15 | const me = { username: 'anonymous' };
16 |
17 | // TODO: 💎 Add a query to get a user's profile.
18 | // - `user` should be a query that returns the user with the given `username`.
19 | console.info({ username });
20 | const user: {
21 | bio?: string;
22 | displayName?: string;
23 | photo?: string;
24 | posts: { createdAt: string; id: string; message: string }[];
25 | username: string;
26 | } = {
27 | bio: undefined,
28 | displayName: undefined,
29 | photo: undefined,
30 | posts: [],
31 | username,
32 | };
33 |
34 | // if (!user) {
35 | // notFound();
36 | // }
37 |
38 | const initials = getInitials(user.displayName ?? 'Anonymous');
39 | const isMe = me.username === user.username;
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 | {initials}
48 |
49 |
50 |
51 |
52 |
53 |
{user.displayName}
54 |
@{user.username}
55 |
56 | {isMe ?
: null}
57 |
58 | {user.bio ? (
59 |
62 | ) : null}
63 |
64 | Posts
65 |
66 | ({
69 | ...post,
70 | user: {
71 | displayName: user.displayName,
72 | photo: user.photo,
73 | username: user.username,
74 | },
75 | }))}
76 | />
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/app/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ThemeProvider as NextThemesProvider } from 'next-themes';
4 | import { type ThemeProviderProps } from 'next-themes/dist/types';
5 |
6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7 | return {children};
8 | }
9 |
--------------------------------------------------------------------------------
/app/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { buttonVariants } from '@/components/ui/button';
4 | import { cn } from '@/lib/utils';
5 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
6 | import * as React from 'react';
7 |
8 | const AlertDialog = AlertDialogPrimitive.Root;
9 |
10 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
11 |
12 | const AlertDialogPortal = (props: AlertDialogPrimitive.AlertDialogPortalProps) => (
13 |
14 | );
15 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName;
16 |
17 | const AlertDialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
31 |
32 | const AlertDialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, ...props }, ref) => (
36 |
37 |
38 |
46 |
47 | ));
48 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
49 |
50 | const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
51 |
52 | );
53 | AlertDialogHeader.displayName = 'AlertDialogHeader';
54 |
55 | const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
56 |
57 | );
58 | AlertDialogFooter.displayName = 'AlertDialogFooter';
59 |
60 | const AlertDialogTitle = React.forwardRef<
61 | React.ElementRef,
62 | React.ComponentPropsWithoutRef
63 | >(({ className, ...props }, ref) => (
64 |
65 | ));
66 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
67 |
68 | const AlertDialogDescription = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
73 | ));
74 | AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
75 |
76 | const AlertDialogAction = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
81 | ));
82 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
83 |
84 | const AlertDialogCancel = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
93 | ));
94 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
95 |
96 | export {
97 | AlertDialog,
98 | AlertDialogTrigger,
99 | AlertDialogContent,
100 | AlertDialogHeader,
101 | AlertDialogFooter,
102 | AlertDialogTitle,
103 | AlertDialogDescription,
104 | AlertDialogAction,
105 | AlertDialogCancel,
106 | };
107 |
--------------------------------------------------------------------------------
/app/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 | import * as React from 'react';
4 |
5 | const alertVariants = cva(
6 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
7 | {
8 | defaultVariants: {
9 | variant: 'default',
10 | },
11 | variants: {
12 | variant: {
13 | default: 'bg-background text-foreground',
14 | destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
15 | },
16 | },
17 | },
18 | );
19 |
20 | const Alert = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes & VariantProps
23 | >(({ className, variant, ...props }, ref) => (
24 |
25 | ));
26 | Alert.displayName = 'Alert';
27 |
28 | const AlertTitle = React.forwardRef>(
29 | ({ className, ...props }, ref) => (
30 |
31 | ),
32 | );
33 | AlertTitle.displayName = 'AlertTitle';
34 |
35 | const AlertDescription = React.forwardRef>(
36 | ({ className, ...props }, ref) => (
37 |
38 | ),
39 | );
40 | AlertDescription.displayName = 'AlertDescription';
41 |
42 | export { Alert, AlertTitle, AlertDescription };
43 |
--------------------------------------------------------------------------------
/app/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
5 | import * as React from 'react';
6 |
7 | const Avatar = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
16 | ));
17 | Avatar.displayName = AvatarPrimitive.Root.displayName;
18 |
19 | const AvatarImage = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
26 |
27 | const AvatarFallback = React.forwardRef<
28 | React.ElementRef,
29 | React.ComponentPropsWithoutRef
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
38 |
39 | export { Avatar, AvatarImage, AvatarFallback };
40 |
--------------------------------------------------------------------------------
/app/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 | import * as React from 'react';
5 |
6 | const buttonVariants = cva(
7 | 'inline-flex items-center justify-center rounded-full text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
8 | {
9 | defaultVariants: {
10 | size: 'default',
11 | variant: 'default',
12 | },
13 | variants: {
14 | size: {
15 | default: 'h-10 px-4 py-2',
16 | icon: 'h-10 w-10',
17 | lg: 'h-11 rounded-full px-8',
18 | sm: 'h-9 rounded-full px-3',
19 | },
20 | variant: {
21 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
22 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
23 | ghost: 'hover:bg-accent hover:text-accent-foreground',
24 | link: 'text-primary underline-offset-4 hover:underline',
25 | outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
26 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
27 | },
28 | },
29 | },
30 | );
31 |
32 | export interface ButtonProps
33 | extends React.ButtonHTMLAttributes,
34 | VariantProps {
35 | asChild?: boolean;
36 | }
37 |
38 | const Button = React.forwardRef(
39 | ({ className, variant, size, asChild = false, ...props }, ref) => {
40 | const Comp = asChild ? Slot : 'button';
41 |
42 | return ;
43 | },
44 | );
45 | Button.displayName = 'Button';
46 |
47 | export { Button, buttonVariants };
48 |
--------------------------------------------------------------------------------
/app/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 |
4 | const Card = React.forwardRef>(({ className, ...props }, ref) => (
5 |
6 | ));
7 | Card.displayName = 'Card';
8 |
9 | const CardHeader = React.forwardRef>(
10 | ({ className, ...props }, ref) => (
11 |
12 | ),
13 | );
14 | CardHeader.displayName = 'CardHeader';
15 |
16 | const CardTitle = React.forwardRef>(
17 | ({ className, ...props }, ref) => (
18 |
19 | ),
20 | );
21 | CardTitle.displayName = 'CardTitle';
22 |
23 | const CardDescription = React.forwardRef>(
24 | ({ className, ...props }, ref) => (
25 |
26 | ),
27 | );
28 | CardDescription.displayName = 'CardDescription';
29 |
30 | const CardContent = React.forwardRef>(
31 | ({ className, ...props }, ref) => ,
32 | );
33 | CardContent.displayName = 'CardContent';
34 |
35 | const CardFooter = React.forwardRef>(
36 | ({ className, ...props }, ref) => (
37 |
38 | ),
39 | );
40 | CardFooter.displayName = 'CardFooter';
41 |
42 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
43 |
--------------------------------------------------------------------------------
/app/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
5 | import { Check } from 'lucide-react';
6 | import * as React from 'react';
7 |
8 | const Checkbox = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 | ));
25 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
26 |
27 | export { Checkbox };
28 |
--------------------------------------------------------------------------------
/app/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import { X } from 'lucide-react';
6 | import * as React from 'react';
7 |
8 | const Dialog = DialogPrimitive.Root;
9 |
10 | const DialogTrigger = DialogPrimitive.Trigger;
11 |
12 | const DialogPortal = (props: DialogPrimitive.DialogPortalProps) => ;
13 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ));
52 | DialogContent.displayName = DialogPrimitive.Content.displayName;
53 |
54 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
55 |
56 | );
57 | DialogHeader.displayName = 'DialogHeader';
58 |
59 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
60 |
61 | );
62 | DialogFooter.displayName = 'DialogFooter';
63 |
64 | const DialogTitle = React.forwardRef<
65 | React.ElementRef,
66 | React.ComponentPropsWithoutRef
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
75 |
76 | const DialogDescription = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
81 | ));
82 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
83 |
84 | export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription };
85 |
--------------------------------------------------------------------------------
/app/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
5 | import { Check, ChevronRight, Circle } from 'lucide-react';
6 | import * as React from 'react';
7 |
8 | const DropdownMenu = DropdownMenuPrimitive.Root;
9 |
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
11 |
12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
13 |
14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
15 |
16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
17 |
18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
19 |
20 | const DropdownMenuSubTrigger = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef & {
23 | inset?: boolean;
24 | }
25 | >(({ className, inset, children, ...props }, ref) => (
26 |
35 | {children}
36 |
37 |
38 | ));
39 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
40 |
41 | const DropdownMenuSubContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, ...props }, ref) => (
45 |
53 | ));
54 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
55 |
56 | const DropdownMenuContent = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, sideOffset = 4, ...props }, ref) => (
60 |
61 |
70 |
71 | ));
72 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
73 |
74 | const DropdownMenuItem = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef & {
77 | inset?: boolean;
78 | }
79 | >(({ className, inset, ...props }, ref) => (
80 |
89 | ));
90 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
91 |
92 | const DropdownMenuCheckboxItem = React.forwardRef<
93 | React.ElementRef,
94 | React.ComponentPropsWithoutRef
95 | >(({ className, children, checked, ...props }, ref) => (
96 |
105 |
106 |
107 |
108 |
109 |
110 | {children}
111 |
112 | ));
113 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
114 |
115 | const DropdownMenuRadioItem = React.forwardRef<
116 | React.ElementRef,
117 | React.ComponentPropsWithoutRef
118 | >(({ className, children, ...props }, ref) => (
119 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ));
135 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
136 |
137 | const DropdownMenuLabel = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef & {
140 | inset?: boolean;
141 | }
142 | >(({ className, inset, ...props }, ref) => (
143 |
148 | ));
149 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
150 |
151 | const DropdownMenuSeparator = React.forwardRef<
152 | React.ElementRef,
153 | React.ComponentPropsWithoutRef
154 | >(({ className, ...props }, ref) => (
155 |
156 | ));
157 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
158 |
159 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
160 | return ;
161 | };
162 |
163 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
164 |
165 | export {
166 | DropdownMenu,
167 | DropdownMenuTrigger,
168 | DropdownMenuContent,
169 | DropdownMenuItem,
170 | DropdownMenuCheckboxItem,
171 | DropdownMenuRadioItem,
172 | DropdownMenuLabel,
173 | DropdownMenuSeparator,
174 | DropdownMenuShortcut,
175 | DropdownMenuGroup,
176 | DropdownMenuPortal,
177 | DropdownMenuSub,
178 | DropdownMenuSubContent,
179 | DropdownMenuSubTrigger,
180 | DropdownMenuRadioGroup,
181 | };
182 |
--------------------------------------------------------------------------------
/app/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import { Label } from '@/components/ui/label';
2 | import { cn } from '@/lib/utils';
3 | import type * as LabelPrimitive from '@radix-ui/react-label';
4 | import { Slot } from '@radix-ui/react-slot';
5 | import * as React from 'react';
6 | import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
7 | import { Controller, FormProvider, useFormContext } from 'react-hook-form';
8 |
9 | const Form = FormProvider;
10 |
11 | interface FormFieldContextValue<
12 | TFieldValues extends FieldValues = FieldValues,
13 | TName extends FieldPath = FieldPath,
14 | > {
15 | name: TName;
16 | }
17 |
18 | const FormFieldContext = React.createContext({} as FormFieldContextValue);
19 |
20 | const FormField = <
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath,
23 | >({
24 | ...props
25 | }: ControllerProps) => {
26 | return (
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | const useFormField = () => {
34 | const fieldContext = React.useContext(FormFieldContext);
35 | const itemContext = React.useContext(FormItemContext);
36 | const { getFieldState, formState } = useFormContext();
37 |
38 | const fieldState = getFieldState(fieldContext.name, formState);
39 |
40 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
41 | if (!fieldContext) {
42 | throw new Error('useFormField should be used within ');
43 | }
44 |
45 | const { id } = itemContext;
46 |
47 | return {
48 | formDescriptionId: `${id}-form-item-description`,
49 | formItemId: `${id}-form-item`,
50 | formMessageId: `${id}-form-item-message`,
51 | id,
52 | name: fieldContext.name,
53 | ...fieldState,
54 | };
55 | };
56 |
57 | interface FormItemContextValue {
58 | id: string;
59 | }
60 |
61 | const FormItemContext = React.createContext({} as FormItemContextValue);
62 |
63 | const FormItem = React.forwardRef>(
64 | ({ className, ...props }, ref) => {
65 | const id = React.useId();
66 |
67 | return (
68 |
69 |
70 |
71 | );
72 | },
73 | );
74 | FormItem.displayName = 'FormItem';
75 |
76 | const FormLabel = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => {
80 | const { error, formItemId } = useFormField();
81 |
82 | return ;
83 | });
84 | FormLabel.displayName = 'FormLabel';
85 |
86 | const FormControl = React.forwardRef, React.ComponentPropsWithoutRef>(
87 | ({ ...props }, ref) => {
88 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
89 |
90 | return (
91 |
98 | );
99 | },
100 | );
101 | FormControl.displayName = 'FormControl';
102 |
103 | const FormDescription = React.forwardRef>(
104 | ({ className, ...props }, ref) => {
105 | const { formDescriptionId } = useFormField();
106 |
107 | return ;
108 | },
109 | );
110 | FormDescription.displayName = 'FormDescription';
111 |
112 | const FormMessage = React.forwardRef>(
113 | ({ className, children, ...props }, ref) => {
114 | const { error, formMessageId } = useFormField();
115 | const body = error ? String(error.message) : children;
116 |
117 | if (!body) {
118 | return null;
119 | }
120 |
121 | return (
122 |
123 | {body}
124 |
125 | );
126 | },
127 | );
128 | FormMessage.displayName = 'FormMessage';
129 |
130 | export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
131 |
--------------------------------------------------------------------------------
/app/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 |
4 | export interface InputProps extends React.InputHTMLAttributes {}
5 |
6 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | );
18 | });
19 | Input.displayName = 'Input';
20 |
21 | export { Input };
22 |
--------------------------------------------------------------------------------
/app/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 | import * as React from 'react';
7 |
8 | const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70');
9 |
10 | const Label = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef & VariantProps
13 | >(({ className, ...props }, ref) => (
14 |
15 | ));
16 | Label.displayName = LabelPrimitive.Root.displayName;
17 |
18 | export { Label };
19 |
--------------------------------------------------------------------------------
/app/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import * as TabsPrimitive from '@radix-ui/react-tabs';
5 | import * as React from 'react';
6 |
7 | const Tabs = TabsPrimitive.Root;
8 |
9 | const TabsList = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 | ));
22 | TabsList.displayName = TabsPrimitive.List.displayName;
23 |
24 | const TabsTrigger = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, ...props }, ref) => (
28 |
36 | ));
37 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
38 |
39 | const TabsContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, ...props }, ref) => (
43 |
51 | ));
52 | TabsContent.displayName = TabsPrimitive.Content.displayName;
53 |
54 | export { Tabs, TabsList, TabsTrigger, TabsContent };
55 |
--------------------------------------------------------------------------------
/app/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as React from 'react';
3 |
4 | export interface TextareaProps extends React.TextareaHTMLAttributes {}
5 |
6 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
7 | return (
8 |
16 | );
17 | });
18 | Textarea.displayName = 'Textarea';
19 |
20 | export { Textarea };
21 |
--------------------------------------------------------------------------------
/app/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import * as ToastPrimitives from '@radix-ui/react-toast';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 | import { X } from 'lucide-react';
5 | import * as React from 'react';
6 |
7 | const ToastProvider = ToastPrimitives.Provider;
8 |
9 | const ToastViewport = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 | ));
22 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
23 |
24 | const toastVariants = cva(
25 | 'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
26 | {
27 | defaultVariants: {
28 | variant: 'default',
29 | },
30 | variants: {
31 | variant: {
32 | default: 'border bg-background text-foreground',
33 | destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground',
34 | },
35 | },
36 | },
37 | );
38 |
39 | const Toast = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef & VariantProps
42 | >(({ className, variant, ...props }, ref) => {
43 | return ;
44 | });
45 | Toast.displayName = ToastPrimitives.Root.displayName;
46 |
47 | const ToastAction = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ));
60 | ToastAction.displayName = ToastPrimitives.Action.displayName;
61 |
62 | const ToastClose = React.forwardRef<
63 | React.ElementRef,
64 | React.ComponentPropsWithoutRef
65 | >(({ className, ...props }, ref) => (
66 |
75 |
76 |
77 | ));
78 | ToastClose.displayName = ToastPrimitives.Close.displayName;
79 |
80 | const ToastTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
85 | ));
86 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
87 |
88 | const ToastDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
93 | ));
94 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
95 |
96 | type ToastProps = React.ComponentPropsWithoutRef;
97 |
98 | type ToastActionElement = React.ReactElement;
99 |
100 | export {
101 | type ToastProps,
102 | type ToastActionElement,
103 | ToastProvider,
104 | ToastViewport,
105 | Toast,
106 | ToastTitle,
107 | ToastDescription,
108 | ToastClose,
109 | ToastAction,
110 | };
111 |
--------------------------------------------------------------------------------
/app/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from '@/components/ui/toast';
4 | import { useToast } from '@/components/ui/use-toast';
5 |
6 | export function Toaster() {
7 | const { toasts } = useToast();
8 |
9 | return (
10 |
11 | {toasts.map(function ({ id, title, description, action, ...props }) {
12 | return (
13 |
14 |
15 | {title && {title}}
16 | {description && {description}}
17 |
18 | {action}
19 |
20 |
21 | );
22 | })}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils';
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
5 | import * as React from 'react';
6 |
7 | const TooltipProvider = TooltipPrimitive.Provider;
8 |
9 | const Tooltip = TooltipPrimitive.Root;
10 |
11 | const TooltipTrigger = TooltipPrimitive.Trigger;
12 |
13 | const TooltipContent = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, sideOffset = 4, ...props }, ref) => (
17 |
26 | ));
27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
28 |
29 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
30 |
--------------------------------------------------------------------------------
/app/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
3 | import * as React from 'react';
4 |
5 | const TOAST_LIMIT = 1;
6 | const TOAST_REMOVE_DELAY = 1000000;
7 |
8 | type ToasterToast = ToastProps & {
9 | action?: ToastActionElement;
10 | description?: React.ReactNode;
11 | id: string;
12 | title?: React.ReactNode;
13 | };
14 |
15 | const actionTypes = {
16 | ADD_TOAST: 'ADD_TOAST',
17 | DISMISS_TOAST: 'DISMISS_TOAST',
18 | REMOVE_TOAST: 'REMOVE_TOAST',
19 | UPDATE_TOAST: 'UPDATE_TOAST',
20 | } as const;
21 |
22 | let count = 0;
23 |
24 | function genId() {
25 | count = (count + 1) % Number.MAX_VALUE;
26 |
27 | return count.toString();
28 | }
29 |
30 | type ActionType = typeof actionTypes;
31 |
32 | type Action =
33 | | {
34 | toast: Partial;
35 | type: ActionType['UPDATE_TOAST'];
36 | }
37 | | {
38 | toast: ToasterToast;
39 | type: ActionType['ADD_TOAST'];
40 | }
41 | | {
42 | toastId?: ToasterToast['id'];
43 | type: ActionType['DISMISS_TOAST'];
44 | }
45 | | {
46 | toastId?: ToasterToast['id'];
47 | type: ActionType['REMOVE_TOAST'];
48 | };
49 |
50 | interface State {
51 | toasts: ToasterToast[];
52 | }
53 |
54 | const toastTimeouts = new Map>();
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return;
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId);
63 | dispatch({
64 | toastId,
65 | type: 'REMOVE_TOAST',
66 | });
67 | }, TOAST_REMOVE_DELAY);
68 |
69 | toastTimeouts.set(toastId, timeout);
70 | };
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case 'ADD_TOAST':
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | };
79 |
80 | case 'UPDATE_TOAST':
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
84 | };
85 |
86 | case 'DISMISS_TOAST': {
87 | const { toastId } = action;
88 |
89 | // ! Side effects ! - This could be extracted into a dismissToast() action,
90 | // but I'll keep it here for simplicity
91 | if (toastId) {
92 | addToRemoveQueue(toastId);
93 | } else {
94 | state.toasts.forEach((t) => {
95 | addToRemoveQueue(t.id);
96 | });
97 | }
98 |
99 | return {
100 | ...state,
101 | toasts: state.toasts.map((t) =>
102 | t.id === toastId || toastId === undefined
103 | ? {
104 | ...t,
105 | open: false,
106 | }
107 | : t,
108 | ),
109 | };
110 | }
111 |
112 | case 'REMOVE_TOAST':
113 | if (action.toastId === undefined) {
114 | return {
115 | ...state,
116 | toasts: [],
117 | };
118 | }
119 |
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | };
124 | }
125 | };
126 |
127 | const listeners: ((state: State) => void)[] = [];
128 |
129 | let memoryState: State = { toasts: [] };
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action);
133 | listeners.forEach((listener) => {
134 | listener(memoryState);
135 | });
136 | }
137 |
138 | type Toast = Omit;
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId();
142 |
143 | const update = (p: ToasterToast) =>
144 | dispatch({
145 | toast: { ...p, id },
146 | type: 'UPDATE_TOAST',
147 | });
148 | const dismiss = () => dispatch({ toastId: id, type: 'DISMISS_TOAST' });
149 |
150 | dispatch({
151 | toast: {
152 | ...props,
153 | id,
154 | onOpenChange: (open) => {
155 | if (!open) {
156 | dismiss();
157 | }
158 | },
159 | open: true,
160 | },
161 | type: 'ADD_TOAST',
162 | });
163 |
164 | return {
165 | dismiss,
166 | id,
167 | update,
168 | };
169 | }
170 |
171 | function useToast() {
172 | const [state, setState] = React.useState(memoryState);
173 |
174 | React.useEffect(() => {
175 | listeners.push(setState);
176 |
177 | return () => {
178 | const index = listeners.indexOf(setState);
179 |
180 | if (index > -1) {
181 | listeners.splice(index, 1);
182 | }
183 | };
184 | }, [state]);
185 |
186 | return {
187 | ...state,
188 | dismiss: (toastId?: string) => dispatch({ toastId, type: 'DISMISS_TOAST' }),
189 | toast,
190 | };
191 | }
192 |
193 | export { useToast, toast };
194 |
--------------------------------------------------------------------------------
/app/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { getInitials } from '@/lib/helpers';
4 | import Link from 'next/link';
5 | import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
6 |
7 | export function UserAvatar() {
8 | // TODO: 💎 Add a query to get the current user.
9 | // - `me` should be a query that returns the current user.
10 | const displayName = 'Anonymous';
11 | const photo = undefined;
12 | const username = 'anonymous';
13 |
14 | const initials = getInitials(displayName);
15 |
16 | return (
17 |
18 |
19 |
20 | {initials}
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/app/lib/apollo-client.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
2 | import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc';
3 | import { graphqlEndpoint } from './constants';
4 |
5 | export const { getClient } = registerApolloClient(() => {
6 | return new ApolloClient({
7 | cache: new InMemoryCache(),
8 | link: new HttpLink({ uri: graphqlEndpoint }),
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/app/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const graphqlEndpoint = 'http://localhost:4000/graphql';
2 |
--------------------------------------------------------------------------------
/app/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | export function getInitials(name: string): string {
2 | const initials = (name.match(/\b[a-zA-Z]/g) || []).map((char) => char.toUpperCase());
3 |
4 | if (initials.length <= 2) {
5 | return initials.join('');
6 | }
7 |
8 | return `${initials[0]}${initials[1]}`;
9 | }
10 |
--------------------------------------------------------------------------------
/app/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/app/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | scrollRestoration: true,
5 | serverActions: true,
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-graphql-workshop/app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "next build",
7 | "dev": "next dev",
8 | "generate": "graphql-codegen",
9 | "lint": "next lint",
10 | "start": "next start",
11 | "typecheck": "tsc"
12 | },
13 | "dependencies": {
14 | "@apollo/client": "^3.8.4",
15 | "@apollo/experimental-nextjs-app-support": "^0.4.3",
16 | "@hookform/resolvers": "^3.3.1",
17 | "@radix-ui/react-alert-dialog": "^1.0.5",
18 | "@radix-ui/react-avatar": "^1.0.4",
19 | "@radix-ui/react-checkbox": "^1.0.4",
20 | "@radix-ui/react-dialog": "^1.0.5",
21 | "@radix-ui/react-dropdown-menu": "^2.0.6",
22 | "@radix-ui/react-label": "^2.0.2",
23 | "@radix-ui/react-slot": "^1.0.2",
24 | "@radix-ui/react-tabs": "^1.0.4",
25 | "@radix-ui/react-toast": "^1.1.5",
26 | "@radix-ui/react-tooltip": "^1.0.7",
27 | "class-variance-authority": "^0.7.0",
28 | "clsx": "^2.0.0",
29 | "date-fns": "^2.30.0",
30 | "graphql": "^16.8.1",
31 | "lucide-react": "^0.279.0",
32 | "next": "^13.5.2",
33 | "next-themes": "^0.2.1",
34 | "react": "^18.2.0",
35 | "react-dom": "^18.2.0",
36 | "react-hook-form": "^7.46.2",
37 | "tailwind-merge": "^1.14.0",
38 | "tailwindcss-animate": "^1.0.7",
39 | "zod": "^3.22.2"
40 | },
41 | "devDependencies": {
42 | "@graphql-codegen/cli": "^5.0.0",
43 | "@graphql-codegen/client-preset": "^4.1.0",
44 | "@graphql-typed-document-node/core": "^3.2.0",
45 | "@tsconfig/next": "^2.0.0",
46 | "@types/node": "^20.7.0",
47 | "@types/react": "^18.2.23",
48 | "@types/react-dom": "^18.2.8",
49 | "autoprefixer": "^10.4.16",
50 | "eslint": "^8.50.0",
51 | "eslint-config-next": "^13.5.3",
52 | "postcss": "^8.4.30",
53 | "tailwindcss": "^3.3.3",
54 | "typescript": "^5.2.2"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/app/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'],
5 | theme: {
6 | container: {
7 | center: true,
8 | padding: '2rem',
9 | screens: {
10 | '2xl': '1400px',
11 | },
12 | },
13 | extend: {
14 | colors: {
15 | border: 'hsl(var(--border))',
16 | input: 'hsl(var(--input))',
17 | ring: 'hsl(var(--ring))',
18 | background: 'hsl(var(--background))',
19 | foreground: 'hsl(var(--foreground))',
20 | primary: {
21 | DEFAULT: 'hsl(var(--primary))',
22 | foreground: 'hsl(var(--primary-foreground))',
23 | },
24 | secondary: {
25 | DEFAULT: 'hsl(var(--secondary))',
26 | foreground: 'hsl(var(--secondary-foreground))',
27 | },
28 | destructive: {
29 | DEFAULT: 'hsl(var(--destructive))',
30 | foreground: 'hsl(var(--destructive-foreground))',
31 | },
32 | muted: {
33 | DEFAULT: 'hsl(var(--muted))',
34 | foreground: 'hsl(var(--muted-foreground))',
35 | },
36 | accent: {
37 | DEFAULT: 'hsl(var(--accent))',
38 | foreground: 'hsl(var(--accent-foreground))',
39 | },
40 | popover: {
41 | DEFAULT: 'hsl(var(--popover))',
42 | foreground: 'hsl(var(--popover-foreground))',
43 | },
44 | card: {
45 | DEFAULT: 'hsl(var(--card))',
46 | foreground: 'hsl(var(--card-foreground))',
47 | },
48 | },
49 | borderRadius: {
50 | lg: 'var(--radius)',
51 | md: 'calc(var(--radius) - 2px)',
52 | sm: 'calc(var(--radius) - 4px)',
53 | },
54 | keyframes: {
55 | 'accordion-down': {
56 | from: { height: 0 },
57 | to: { height: 'var(--radix-accordion-content-height)' },
58 | },
59 | 'accordion-up': {
60 | from: { height: 'var(--radix-accordion-content-height)' },
61 | to: { height: 0 },
62 | },
63 | },
64 | animation: {
65 | 'accordion-down': 'accordion-down 0.2s ease-out',
66 | 'accordion-up': 'accordion-up 0.2s ease-out',
67 | },
68 | },
69 | },
70 | plugins: [require('tailwindcss-animate')],
71 | };
72 |
--------------------------------------------------------------------------------
/app/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | };
19 | export default config;
20 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowUnreachableCode": false,
4 | "allowUnusedLabels": false,
5 | "noFallthroughCasesInSwitch": true,
6 | "noImplicitOverride": true,
7 | "noImplicitReturns": true,
8 | "noUncheckedIndexedAccess": true,
9 | "noUnusedParameters": true,
10 | "paths": {
11 | "@/*": ["./*"]
12 | },
13 | "plugins": [{ "name": "next" }],
14 | "strict": true
15 | },
16 | "exclude": ["node_modules"],
17 | "extends": "@tsconfig/next/tsconfig.json",
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".eslintrc.js"]
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-graphql-workshop",
3 | "version": "1.0.0",
4 | "private": true,
5 | "author": "Glenn Reyes ",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "turbo run build",
9 | "dev": "turbo run dev",
10 | "format": "pnpm prettier --write && turbo run format",
11 | "lint": "pnpm prettier && turbo run lint",
12 | "prettier": "prettier --ignore-path .gitignore --list-different \"**/*.{css,graphql,html,js,json,md,ts,tsx,yaml}\"",
13 | "typecheck": "turbo run typecheck"
14 | },
15 | "devDependencies": {
16 | "@ianvs/prettier-plugin-sort-imports": "^4.1.0",
17 | "eslint": "^8.50.0",
18 | "eslint-config-banana": "^1.6.1",
19 | "prettier": "^3.0.3",
20 | "prettier-plugin-tailwindcss": "^0.5.4",
21 | "turbo": "^1.10.14"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - app
3 | - server
4 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | plugins: ['@ianvs/prettier-plugin-sort-imports', 'prettier-plugin-tailwindcss'],
4 | printWidth: 120,
5 | singleQuote: true,
6 | };
7 |
--------------------------------------------------------------------------------
/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import('eslint').Linter.BaseConfig} */
2 | module.exports = {
3 | extends: ['banana'],
4 | parserOptions: { project: true },
5 | };
6 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | *.db
3 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-graphql-workshop/server",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "tsc --noEmit false --outDir dist",
7 | "dev": "tsx watch --clear-screen=false src/index.ts",
8 | "lint": "TIMING=1 eslint .",
9 | "seed": "tsx prisma/seed.ts",
10 | "start": "node dist/index.js",
11 | "typecheck": "tsc"
12 | },
13 | "dependencies": {
14 | "graphql": "^16.8.1",
15 | "graphql-yoga": "^4.0.4"
16 | },
17 | "devDependencies": {
18 | "@tsconfig/recommended": "^1.0.3",
19 | "eslint": "^8.50.0",
20 | "eslint-config-banana": "^1.6.1",
21 | "tsx": "^3.13.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createServer } from 'node:http';
2 | import { createSchema, createYoga } from 'graphql-yoga';
3 |
4 | const yoga = createYoga({
5 | schema: createSchema({
6 | resolvers: {
7 | Query: {
8 | hello: () => 'Hello from Yoga!',
9 | },
10 | },
11 | typeDefs: /* GraphQL */ `
12 | type Query {
13 | hello: String
14 | }
15 | `,
16 | }),
17 | });
18 | const server = createServer(yoga);
19 |
20 | server.listen(4000, () => {
21 | console.info('Server is running on http://localhost:4000/graphql');
22 | });
23 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noEmit": true
4 | },
5 | "extends": "@tsconfig/recommended/tsconfig.json",
6 | "include": ["*.ts", ".eslintrc.js", "prisma/**/*.ts", "src/**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "pipeline": {
4 | "dev": {
5 | "cache": false
6 | },
7 | "build": {
8 | "outputs": [],
9 | "outputMode": "new-only"
10 | },
11 | "compile": {
12 | "outputs": [],
13 | "outputMode": "new-only"
14 | },
15 | "deploy": {
16 | "cache": false
17 | },
18 | "format": {
19 | "outputs": [],
20 | "outputMode": "new-only"
21 | },
22 | "lint": {
23 | "outputs": [],
24 | "outputMode": "new-only"
25 | },
26 | "node": {
27 | "cache": false
28 | },
29 | "typecheck": {
30 | "outputs": [],
31 | "outputMode": "new-only"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------