├── app ├── page.module.css ├── favicon.ico ├── users │ ├── loading.tsx │ ├── page.module.css │ ├── page.tsx │ ├── error.tsx │ └── [id] │ │ └── page.tsx ├── api │ ├── hello │ │ └── route.ts │ ├── user │ │ └── route.ts │ ├── content │ │ └── route.ts │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ └── follow │ │ └── route.ts ├── AuthProvider.tsx ├── page.tsx ├── about │ └── page.tsx ├── NavMenu.module.css ├── blog │ ├── page.tsx │ └── [slug] │ │ └── page.tsx ├── dashboard │ ├── page.tsx │ └── ProfileForm.tsx ├── globals.css ├── NavMenu.tsx └── layout.tsx ├── public ├── logo.png ├── mememan.webp ├── vercel.svg ├── next.svg └── logo.svg ├── lib └── prisma.ts ├── .env.example ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20230501205637_init │ │ └── migration.sql └── schema.prisma ├── .vscode └── settings.json ├── next.config.js ├── components ├── AuthCheck.tsx ├── UserCard │ ├── UserCard.module.css │ └── UserCard.tsx ├── FollowButton │ ├── FollowButton.tsx │ └── FollowClient.tsx └── buttons.tsx ├── README.md ├── .gitignore ├── tsconfig.json └── package.json /app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | 3 | } -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireship-io/nextjs-course/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireship-io/nextjs-course/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/mememan.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireship-io/nextjs-course/HEAD/public/mememan.webp -------------------------------------------------------------------------------- /app/users/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function LoadingUsers() { 2 | return

Loading user data...

; 3 | } 4 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | export const prisma = new PrismaClient(); 3 | 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_ID= 2 | GITHUB_SECRET= 3 | NEXTAUTH_SECRET= 4 | DATABASE_URL=postgres://... 5 | SHADOW_DATABASE_URL=postgres://... -------------------------------------------------------------------------------- /app/users/page.module.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 4 | grid-gap: 20px; 5 | } -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "prettier.singleQuote": true, 5 | } -------------------------------------------------------------------------------- /app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '@/lib/prisma'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | export async function GET(request: Request) { 5 | const users = await prisma.user.findMany(); 6 | console.log(users); 7 | 8 | return NextResponse.json(users); 9 | } 10 | -------------------------------------------------------------------------------- /app/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SessionProvider } from 'next-auth/react'; 4 | 5 | type Props = { 6 | children: React.ReactNode; 7 | }; 8 | 9 | export default function AuthProvider({ children }: Props) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return ( 3 |
4 |

Welcome to NextSpace!

5 |

6 | A next-gen social media app to connect with frens inspired by MySpace 7 |

8 |

To get started, sign up for an account

9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'avatars.githubusercontent.com', 8 | port: '', 9 | pathname: '/u/**', 10 | } 11 | ] 12 | } 13 | } 14 | 15 | module.exports = nextConfig 16 | -------------------------------------------------------------------------------- /components/AuthCheck.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSession } from 'next-auth/react'; 4 | 5 | export default function AuthCheck({ children }: { children: React.ReactNode }) { 6 | const { data: session, status } = useSession(); 7 | 8 | console.log(session, status); 9 | 10 | if (status === 'authenticated') { 11 | return <>{children}; 12 | } else { 13 | return <>; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/users/page.tsx: -------------------------------------------------------------------------------- 1 | import UserCard from '@/components/UserCard/UserCard'; 2 | import styles from './page.module.css'; 3 | import { prisma } from '@/lib/prisma'; 4 | 5 | export default async function Users() { 6 | const users = await prisma.user.findMany(); 7 | 8 | return ( 9 |
10 | {users.map((user) => { 11 | return ; 12 | })} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | export const dynamic = 'force-static'; // no necessary, just for demonstration 4 | 5 | export const metadata: Metadata = { 6 | title: 'About Us', 7 | description: 'About NextSpace', 8 | }; 9 | 10 | export default function Blog() { 11 | return ( 12 |
13 |

About us

14 |

We are a social media company that wants to bring people together!

15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/UserCard/UserCard.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: #f1f1f1; 3 | padding: 0; 4 | width: 150px; 5 | } 6 | 7 | .card h3 { 8 | margin-top: 0; 9 | margin-bottom: 16px; 10 | color: var(--link-color); 11 | font-weight: bold; 12 | } 13 | 14 | .cardImage { 15 | width: 150px; 16 | height: 120px; 17 | object-fit: cover; 18 | margin-bottom: 0.5rem; 19 | } 20 | 21 | .cardContent { 22 | padding: 0 0.5rem; 23 | } -------------------------------------------------------------------------------- /app/users/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; // Error components must be Client components 2 | 3 | import { useEffect } from 'react'; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | console.error(error); 14 | }, [error]); 15 | 16 | return ( 17 |
18 |

Something went wrong!

19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Full Course Demo 2 | 3 | This repo contains the project code for the [Full Next.js App Router Course](https://fireship.io/courses/nextjs) 4 | 5 | ## Setup 6 | 7 | ``` 8 | git clone nextcourse 9 | cd nextcourse 10 | npm install 11 | ``` 12 | 13 | - Rename the `.env.example` file to `.env` and update env variables (explained in detail in the course). 14 | - I am using a free [NeonDB](https://neon.tech) Postgres database, but any Prisma compatible DB will work. 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/NavMenu.module.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: flex; 3 | background-color: #1d4ed8; 4 | color: #fff; 5 | height: 70px; 6 | justify-content: space-between; 7 | align-items: center; 8 | } 9 | 10 | .logo { 11 | 12 | } 13 | 14 | .links { 15 | list-style: none; 16 | display: flex; 17 | margin-right: 1rem; 18 | } 19 | 20 | .links li { 21 | height: 70px; 22 | display: flex; 23 | align-items: center; 24 | padding: 0.25rem; 25 | } 26 | 27 | .links a { 28 | color: #fff; 29 | } -------------------------------------------------------------------------------- /app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default async function Blog() { 4 | const posts = await fetch('http://localhost:3000/api/content').then((res) => 5 | res.json() 6 | ); 7 | return ( 8 |
9 |

Welcome to our Blog

10 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .env 3 | NOTES.md 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { getServerSession } from 'next-auth'; 3 | import { prisma } from '@/lib/prisma'; 4 | import { authOptions } from "../auth/[...nextauth]/route" 5 | 6 | export async function PUT(req: Request) { 7 | const session = await getServerSession(authOptions); 8 | const currentUserEmail = session?.user?.email!; 9 | 10 | const data = await req.json(); 11 | data.age = Number(data.age); 12 | 13 | const user = await prisma.user.update({ 14 | where: { 15 | email: currentUserEmail, 16 | }, 17 | data, 18 | }); 19 | 20 | return NextResponse.json(user); 21 | } 22 | -------------------------------------------------------------------------------- /components/UserCard/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styles from './UserCard.module.css'; 3 | 4 | interface Props { 5 | id: string; 6 | name: string | null; 7 | age: number | null; 8 | image: string | null; 9 | } 10 | 11 | export default function UserCard({ id, name, age, image }: Props) { 12 | return ( 13 |
14 | {`${name}'s 19 |
20 |

21 | {name} 22 |

23 |

Age: {age}

24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/dashboard/@basic"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { getServerSession } from 'next-auth'; 2 | import { prisma } from '@/lib/prisma'; 3 | import { ProfileForm } from './ProfileForm'; 4 | import { redirect } from 'next/navigation'; 5 | import { SignOutButton } from '@/components/buttons'; 6 | import { authOptions } from "../api/auth/[...nextauth]/route" 7 | 8 | 9 | export default async function Dashboard() { 10 | const session = await getServerSession(authOptions); 11 | 12 | if (!session) { 13 | redirect('/api/auth/signin'); 14 | } 15 | 16 | const currentUserEmail = session?.user?.email!; 17 | const user = await prisma.user.findUnique({ 18 | where: { 19 | email: currentUserEmail, 20 | }, 21 | }); 22 | 23 | return ( 24 | <> 25 |

Dashboard

26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/FollowButton/FollowButton.tsx: -------------------------------------------------------------------------------- 1 | import { getServerSession } from 'next-auth'; 2 | import FollowClient from './FollowClient'; 3 | import { prisma } from '@/lib/prisma'; 4 | import { authOptions } from '../../app/api/auth/[...nextauth]/route' 5 | 6 | 7 | interface Props { 8 | targetUserId: string; 9 | } 10 | 11 | export default async function FollowButton({ targetUserId }: Props) { 12 | const session = await getServerSession(authOptions); 13 | 14 | const currentUserId = await prisma.user 15 | .findFirst({ where: { email: session?.user?.email! } }) 16 | .then((user) => user?.id!); 17 | 18 | const isFollowing = await prisma.follows.findFirst({ 19 | where: { followerId: currentUserId, followingId: targetUserId }, 20 | }); 21 | 22 | return ( 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/buttons.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSession, signIn, signOut } from 'next-auth/react'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | 7 | export function SignInButton() { 8 | const { data: session, status } = useSession(); 9 | console.log(session, status); 10 | 11 | if (status === 'loading') { 12 | return <>...; 13 | } 14 | 15 | if (status === 'authenticated') { 16 | return ( 17 | 18 | Your Name 24 | 25 | ); 26 | } 27 | 28 | return ; 29 | } 30 | 31 | export function SignOutButton() { 32 | return ; 33 | } 34 | -------------------------------------------------------------------------------- /app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | export const revalidate = 1200; // not necessary, just for ISR demonstration 2 | 3 | interface Post { 4 | title: string; 5 | content: string; 6 | slug: string; 7 | } 8 | 9 | export async function generateStaticParams() { 10 | const posts: Post[] = await fetch('http://localhost:3000/api/content').then( 11 | (res) => res.json() 12 | ); 13 | 14 | return posts.map((post) => ({ 15 | slug: post.slug, 16 | })); 17 | } 18 | 19 | interface Props { 20 | params: Promise<{ slug: string }>; 21 | } 22 | 23 | export default async function BlogPostPage(props: Props) { 24 | const params = await props.params; 25 | // deduped 26 | const posts: Post[] = await fetch('http://localhost:3000/api/content').then( 27 | (res) => res.json() 28 | ); 29 | const post = posts.find((post) => post.slug === params.slug)!; 30 | 31 | return ( 32 |
33 |

{post.title}

34 |

{post.content}

35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextcourse", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next-auth/prisma-adapter": "^1.0.6", 13 | "@prisma/client": "^5.11.0", 14 | "@types/node": "18.16.1", 15 | "@types/react": "npm:types-react@19.0.0-rc.1", 16 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", 17 | "eslint-config-next": "15.0.3", 18 | "next": "15.0.3", 19 | "next-auth": "^4.22.1", 20 | "postgres": "^3.3.4", 21 | "react": "19.0.0-rc-66855b96-20241106", 22 | "react-dom": "19.0.0-rc-66855b96-20241106", 23 | "typescript": "5.0.4" 24 | }, 25 | "devDependencies": { 26 | "prisma": "^5.11.0" 27 | }, 28 | "overrides": { 29 | "@types/react": "npm:types-react@19.0.0-rc.1", 30 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | --link-color: #1d4ed8; 4 | } 5 | 6 | html, 7 | body { 8 | max-width: 100vw; 9 | overflow-x: hidden; 10 | background-color: #e3e3e3; 11 | color: black 12 | /* font-family: verdana, arial, sans-serif, helvetica; */ 13 | } 14 | 15 | .container { 16 | width: 810px; 17 | max-width: 100%; 18 | margin: 0 auto 10px; 19 | } 20 | 21 | main { 22 | background-color: white; 23 | padding: 1rem; 24 | min-height: 300px; 25 | } 26 | 27 | footer { 28 | text-align: center; 29 | font-size: 0.8rem; 30 | } 31 | 32 | footer ul { 33 | list-style: none; 34 | display: flex; 35 | justify-content: center; 36 | } 37 | footer li { 38 | padding: 0.1rem; 39 | } 40 | 41 | a { 42 | color: var(--link-color); 43 | text-decoration: underline; 44 | } 45 | 46 | label, textarea, input { 47 | display: block; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | @media (prefers-color-scheme: dark) { 52 | html { 53 | color-scheme: dark; 54 | } 55 | } 56 | 57 | p, h1, h2, h3, h4, h5, h6, label { 58 | color: black; 59 | } -------------------------------------------------------------------------------- /app/NavMenu.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styles from './NavMenu.module.css'; 3 | import Image from 'next/image'; 4 | import { SignInButton, SignOutButton } from '../components/buttons'; 5 | import AuthCheck from '@/components/AuthCheck'; 6 | 7 | export default function NavMenu() { 8 | return ( 9 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/users/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import FollowButton from '@/components/FollowButton/FollowButton'; 2 | import { prisma } from '@/lib/prisma'; 3 | import { Metadata } from 'next'; 4 | 5 | interface Props { 6 | params: Promise<{ 7 | id: string; 8 | }>; 9 | } 10 | 11 | export async function generateMetadata(props: Props): Promise { 12 | const params = await props.params; 13 | const user = await prisma.user.findUnique({ where: { id: params.id } }); 14 | return { title: `User profile of ${user?.name}` }; 15 | } 16 | 17 | export default async function UserProfile(props: Props) { 18 | const params = await props.params; 19 | const user = await prisma.user.findUnique({ where: { id: params.id } }); 20 | const { name, bio, image, id } = user ?? {}; 21 | 22 | return ( 23 |
24 |

{name}

25 | 26 | {`${name}'s 31 | 32 |

Bio

33 |

{bio}

34 | 35 | {/* @ts-expect-error Server Component */} 36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/api/content/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | const posts = [ 4 | { 5 | title: 'Lorem Ipsum', 6 | slug: 'lorem-ipsum', 7 | content: 8 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero.', 9 | }, 10 | { 11 | title: 'Dolor Sit Amet', 12 | slug: 'dolor-sit-amet', 13 | content: 14 | 'Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.', 15 | }, 16 | { 17 | title: 'Consectetur Adipiscing', 18 | slug: 'consectetur-adipiscing', 19 | content: 20 | 'Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta.', 21 | }, 22 | { 23 | title: 'Integer Nec Odio', 24 | slug: 'integer-nec-odio', 25 | content: 26 | 'Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent.', 27 | }, 28 | { 29 | title: 'Praesent Libero', 30 | slug: 'praesent-libero', 31 | content: 32 | 'Suspendisse in justo eu magna luctus suscipit. Sed lectus. Integer euismod lacus luctus magna.', 33 | }, 34 | ]; 35 | 36 | export async function GET() { 37 | return NextResponse.json(posts); 38 | } 39 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import type { NextAuthOptions } from 'next-auth'; 3 | import GithubProvider from 'next-auth/providers/github'; 4 | import CredentialsProvider from 'next-auth/providers/credentials'; 5 | import { PrismaAdapter } from '@next-auth/prisma-adapter'; 6 | import { prisma } from '@/lib/prisma'; 7 | 8 | export const authOptions: NextAuthOptions = { 9 | // session: { 10 | // strategy: 'jwt', 11 | // }, 12 | secret: process.env.NEXTAUTH_SECRET, 13 | adapter: PrismaAdapter(prisma), 14 | providers: [ 15 | GithubProvider({ 16 | clientId: process.env.GITHUB_ID!, 17 | clientSecret: process.env.GITHUB_SECRET!, 18 | }), 19 | // CredentialsProvider({ 20 | // name: 'as Guest', 21 | // credentials: {}, 22 | // async authorize(credentials) { 23 | // const user = { 24 | // id: Math.random().toString(), 25 | // name: 'Guest', 26 | // email: 'guest@example.com', 27 | // }; 28 | // return user; 29 | // }, 30 | // }), 31 | // ], 32 | // callbacks: { 33 | // async signIn({ user }) { 34 | // // block signin if necessary 35 | // return true; 36 | // } 37 | // }, 38 | ] 39 | }; 40 | 41 | const handler = NextAuth(authOptions); 42 | export { handler as GET, handler as POST }; 43 | -------------------------------------------------------------------------------- /app/api/follow/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getServerSession } from 'next-auth'; 3 | import { prisma } from '@/lib/prisma'; 4 | import { authOptions } from '../auth/[...nextauth]/route'; 5 | 6 | export async function POST(req: Request) { 7 | const session = await getServerSession(authOptions); 8 | const currentUserEmail = session?.user?.email!; 9 | const { targetUserId } = await req.json(); 10 | 11 | const currentUserId = await prisma.user 12 | .findUnique({ where: { email: currentUserEmail } }) 13 | .then((user) => user?.id!); 14 | 15 | const record = await prisma.follows.create({ 16 | data: { 17 | followerId: currentUserId, 18 | followingId: targetUserId, 19 | }, 20 | }); 21 | 22 | return NextResponse.json(record); 23 | } 24 | 25 | export async function DELETE(req: NextRequest) { 26 | const session = await getServerSession(authOptions); 27 | const currentUserEmail = session?.user?.email!; 28 | const targetUserId = req.nextUrl.searchParams.get('targetUserId'); 29 | 30 | const currentUserId = await prisma.user 31 | .findUnique({ where: { email: currentUserEmail } }) 32 | .then((user) => user?.id!); 33 | 34 | const record = await prisma.follows.delete({ 35 | where: { 36 | followerId_followingId: { 37 | followerId: currentUserId, 38 | followingId: targetUserId!, 39 | }, 40 | }, 41 | }); 42 | 43 | return NextResponse.json(record); 44 | } 45 | -------------------------------------------------------------------------------- /app/dashboard/ProfileForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export function ProfileForm({ user }: any) { 4 | 5 | const updateUser = async (e: React.FormEvent) => { 6 | 7 | e.preventDefault(); 8 | 9 | const formData = new FormData(e.currentTarget); 10 | 11 | const body = { 12 | name: formData.get('name'), 13 | bio: formData.get('bio'), 14 | age: formData.get('age'), 15 | image: formData.get('image'), 16 | }; 17 | 18 | const res = await fetch('/api/user', { 19 | method: 'PUT', 20 | body: JSON.stringify(body), 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | }); 25 | 26 | await res.json(); 27 | }; 28 | 29 | return ( 30 |
31 |

Edit Your Profile

32 |
33 | 34 | 35 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import NavMenu from '@/app/NavMenu'; 2 | import './globals.css'; 3 | import { Open_Sans } from 'next/font/google'; 4 | import Link from 'next/link'; 5 | import AuthProvider from './AuthProvider'; 6 | 7 | const myFont = Open_Sans({ weight: '400', subsets: ['latin'] }); 8 | 9 | export const metadata = { 10 | title: 'Create Next App', 11 | description: 'Generated by create next app', 12 | }; 13 | 14 | interface Props { 15 | children: React.ReactNode; 16 | } 17 | 18 | export default function RootLayout({ children }: Props) { 19 | return ( 20 | 21 | 22 | 23 |
24 | 25 |
{children}
26 | 27 |
28 |

29 | Created for the{' '} 30 | 31 | Fireship Next.js 14 Full Course 32 | 33 |

34 |
    35 |
  • 36 | About 37 |
  • {' '} 38 | | 39 |
  • 40 | 41 | YouTube 42 | 43 |
  • {' '} 44 | | 45 |
  • 46 | Source Code 47 |
  • {' '} 48 | | 49 |
  • 50 | NextJS Docs 51 |
  • 52 |
53 |
54 |
55 | 56 | 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("POSTGRES_PRISMA_URL") 8 | } 9 | 10 | model Account { 11 | id String @id @default(cuid()) 12 | userId String 13 | type String 14 | provider String 15 | providerAccountId String 16 | refresh_token String? 17 | access_token String? 18 | expires_at Int? 19 | token_type String? 20 | scope String? 21 | id_token String? 22 | session_state String? 23 | refresh_token_expires_in Int? 24 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 25 | 26 | @@unique([provider, providerAccountId]) 27 | } 28 | 29 | model Session { 30 | id String @id @default(cuid()) 31 | sessionToken String @unique 32 | userId String 33 | expires DateTime 34 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 35 | } 36 | 37 | model User { 38 | id String @id @default(cuid()) 39 | name String? 40 | email String? @unique 41 | emailVerified DateTime? 42 | image String? 43 | age Int? 44 | bio String? 45 | accounts Account[] 46 | following Follows[] @relation("follower") 47 | followedBy Follows[] @relation("following") 48 | sessions Session[] 49 | } 50 | 51 | model Follows { 52 | followerId String 53 | followingId String 54 | follower User @relation("follower", fields: [followerId], references: [id]) 55 | following User @relation("following", fields: [followingId], references: [id]) 56 | 57 | @@id([followerId, followingId]) 58 | } 59 | 60 | model VerificationToken { 61 | identifier String 62 | token String @unique 63 | expires DateTime 64 | 65 | @@unique([identifier, token]) 66 | } 67 | -------------------------------------------------------------------------------- /components/FollowButton/FollowClient.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useRouter } from 'next/navigation'; 3 | import { useState, useTransition } from 'react'; 4 | 5 | interface Props { 6 | targetUserId: string; 7 | isFollowing: boolean; 8 | } 9 | 10 | export default function FollowClient({ targetUserId, isFollowing }: Props) { 11 | const router = useRouter(); 12 | const [isPending, startTransition] = useTransition(); 13 | const [isFetching, setIsFetching] = useState(false); 14 | const isMutating = isFetching || isPending; 15 | 16 | const follow = async () => { 17 | setIsFetching(true); 18 | 19 | const res = await fetch('/api/follow', { 20 | method: 'POST', 21 | body: JSON.stringify({ targetUserId }), 22 | headers: { 23 | 'Content-Type': 'application/json' 24 | } 25 | }); 26 | 27 | setIsFetching(false); 28 | 29 | console.log(res) 30 | 31 | startTransition(() => { 32 | // Refresh the current route: 33 | // - Makes a new request to the server for the route 34 | // - Re-fetches data requests and re-renders Server Components 35 | // - Sends the updated React Server Component payload to the client 36 | // - The client merges the payload without losing unaffected 37 | // client-side React state or browser state 38 | router.refresh(); 39 | }); 40 | } 41 | 42 | 43 | const unfollow = async () => { 44 | setIsFetching(true); 45 | 46 | const res = await fetch(`/api/follow?targetUserId=${targetUserId}`, { 47 | method: 'DELETE', 48 | }); 49 | 50 | setIsFetching(false); 51 | startTransition(() => router.refresh() ); 52 | } 53 | 54 | if (isFollowing) { 55 | return ( 56 | 59 | ) 60 | 61 | } else { 62 | return ( 63 | 66 | ) 67 | } 68 | 69 | 70 | } -------------------------------------------------------------------------------- /prisma/migrations/20230501205637_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Account" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "type" TEXT NOT NULL, 6 | "provider" TEXT NOT NULL, 7 | "providerAccountId" TEXT NOT NULL, 8 | "refresh_token" TEXT, 9 | "access_token" TEXT, 10 | "expires_at" INTEGER, 11 | "token_type" TEXT, 12 | "scope" TEXT, 13 | "id_token" TEXT, 14 | "session_state" TEXT, 15 | "refresh_token_expires_in" INTEGER, 16 | 17 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateTable 21 | CREATE TABLE "Session" ( 22 | "id" TEXT NOT NULL, 23 | "sessionToken" TEXT NOT NULL, 24 | "userId" TEXT NOT NULL, 25 | "expires" TIMESTAMP(3) NOT NULL, 26 | 27 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 28 | ); 29 | 30 | -- CreateTable 31 | CREATE TABLE "User" ( 32 | "id" TEXT NOT NULL, 33 | "name" TEXT, 34 | "email" TEXT, 35 | "emailVerified" TIMESTAMP(3), 36 | "image" TEXT, 37 | "age" INTEGER, 38 | "bio" TEXT, 39 | 40 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 41 | ); 42 | 43 | -- CreateTable 44 | CREATE TABLE "Follows" ( 45 | "followerId" TEXT NOT NULL, 46 | "followingId" TEXT NOT NULL, 47 | 48 | CONSTRAINT "Follows_pkey" PRIMARY KEY ("followerId","followingId") 49 | ); 50 | 51 | -- CreateTable 52 | CREATE TABLE "VerificationToken" ( 53 | "identifier" TEXT NOT NULL, 54 | "token" TEXT NOT NULL, 55 | "expires" TIMESTAMP(3) NOT NULL 56 | ); 57 | 58 | -- CreateIndex 59 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 60 | 61 | -- CreateIndex 62 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 63 | 64 | -- CreateIndex 65 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 66 | 67 | -- CreateIndex 68 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 69 | 70 | -- CreateIndex 71 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 72 | 73 | -- AddForeignKey 74 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 75 | 76 | -- AddForeignKey 77 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 78 | 79 | -- AddForeignKey 80 | ALTER TABLE "Follows" ADD CONSTRAINT "Follows_followerId_fkey" FOREIGN KEY ("followerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 81 | 82 | -- AddForeignKey 83 | ALTER TABLE "Follows" ADD CONSTRAINT "Follows_followingId_fkey" FOREIGN KEY ("followingId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 84 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------