├── 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 |
11 | {posts.map((post: any) => (
12 | -
13 | {post.title}
14 |
15 | ))}
16 |
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 |

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 |
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 |

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 |
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 |
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 |
15 |
--------------------------------------------------------------------------------