├── .eslintrc.json ├── src ├── types │ └── blog.ts ├── app │ ├── favicon.ico │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── posts │ │ │ └── route.ts │ │ └── comments │ │ │ └── route.ts │ ├── page.tsx │ ├── globals.css │ ├── layout.tsx │ └── blogs │ │ ├── [id] │ │ └── page.tsx │ │ └── page.tsx ├── lib │ ├── session.ts │ ├── auth.ts │ └── db.ts ├── components │ ├── button-logout.tsx │ ├── provider.tsx │ ├── header.tsx │ ├── comments.tsx │ ├── form-comments.tsx │ └── form-new-post.tsx └── data │ └── posts.ts ├── next.config.js ├── postcss.config.js ├── .env.example ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json ├── README.md └── prisma └── schema.prisma /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/types/blog.ts: -------------------------------------------------------------------------------- 1 | export interface FormData { 2 | title: string, 3 | content: string 4 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candraKriswinarto/nextjs-blog-example/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/lib/auth" 2 | import NextAuth from "next-auth" 3 | 4 | const handler = NextAuth(authOptions) 5 | 6 | export { handler as GET, handler as POST } -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import FormNewPost from '@/components/form-new-post'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/session.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth"; 2 | import { authOptions } from "./auth"; 3 | 4 | export async function getCurrentUser() { 5 | const session = await getServerSession(authOptions); 6 | return session?.user; 7 | } -------------------------------------------------------------------------------- /src/components/button-logout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { signOut } from 'next-auth/react'; 4 | 5 | const ButtonLogout = () => { 6 | return ( 7 | 10 | ); 11 | }; 12 | 13 | export default ButtonLogout; 14 | -------------------------------------------------------------------------------- /src/components/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SessionProvider } from 'next-auth/react'; 4 | import React, { FC, ReactNode } from 'react'; 5 | 6 | interface ProviderProps { 7 | children: ReactNode; 8 | } 9 | const Provider: FC = ({ children }) => { 10 | return {children}; 11 | }; 12 | 13 | export default Provider; 14 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { PrismaAdapter } from "@auth/prisma-adapter" 2 | import GoogleProvider from "next-auth/providers/google" 3 | import prisma from "./db" 4 | 5 | export const authOptions = { 6 | adapter: PrismaAdapter(prisma), 7 | providers: [ 8 | GoogleProvider({ 9 | clientId: process.env.GOOGLE_CLIENT_ID!, 10 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!, 11 | }), 12 | ], 13 | } -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient() 5 | } 6 | 7 | type PrismaClientSingleton = ReturnType 8 | 9 | const globalForPrisma = globalThis as unknown as { 10 | prisma: PrismaClientSingleton | undefined 11 | } 12 | 13 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton() 14 | 15 | export default prisma 16 | 17 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="" 8 | 9 | GOOGLE_CLIENT_ID="" 10 | GOOGLE_CLIENT_SECRET="" -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/data/posts.ts: -------------------------------------------------------------------------------- 1 | export const posts = [ 2 | { 3 | id: 1, 4 | title: 'Blog Post 1', 5 | content: 'this is content post', 6 | username: 'john123', 7 | }, 8 | { 9 | id: 2, 10 | title: 'Blog Post 2', 11 | content: 'this is content post', 12 | username: 'john123', 13 | }, 14 | { 15 | id: 3, 16 | title: 'Blog Post 3', 17 | content: 'this is content post', 18 | username: 'john123', 19 | }, 20 | { 21 | id: 4, 22 | title: 'Blog Post 4', 23 | content: 'this is content post', 24 | username: 'john123', 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/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': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | height: 100vh; 21 | color: rgb(var(--foreground-rgb)); 22 | background: linear-gradient( 23 | to bottom, 24 | transparent, 25 | rgb(var(--background-end-rgb)) 26 | ) 27 | rgb(var(--background-start-rgb)); 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | import Header from '@/components/header'; 5 | import Provider from '@/components/provider'; 6 | 7 | const inter = Inter({ subsets: ['latin'] }); 8 | 9 | export const metadata: Metadata = { 10 | title: 'Create Next App', 11 | description: 'Generated by create next app', 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 | 23 |
24 | {children} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/app/api/posts/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { getCurrentUser } from "@/lib/session"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function POST(req: Request) { 6 | const user = await getCurrentUser(); 7 | 8 | try { 9 | if(!user?.email) { 10 | return NextResponse.json({ message: 'Not Authenticated!' }, { status: 401 }) 11 | } 12 | 13 | const { title, content } = await req.json(); 14 | const newPost = await prisma.post.create({ 15 | data: { 16 | title, content, authorEmail: user.email 17 | } 18 | }) 19 | return NextResponse.json({newPost}, { status: 200}) 20 | 21 | } catch(error) { 22 | return NextResponse.json({ message: 'Something went wrong!'}, { status: 500 }) 23 | } 24 | } -------------------------------------------------------------------------------- /src/app/api/comments/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/db"; 2 | import { getCurrentUser } from "@/lib/session"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function POST(req: Request) { 6 | const user = await getCurrentUser(); 7 | 8 | try { 9 | if(!user?.email) { 10 | return NextResponse.json({ message: 'Not Authenticated!' }, { status: 401 }) 11 | } 12 | 13 | const { postId, text } = await req.json(); 14 | const newPost = await prisma.comment.create({ 15 | data: { 16 | postId, text, authorEmail: user.email 17 | } 18 | }) 19 | return NextResponse.json({newPost}, { status: 200}) 20 | 21 | } catch(error) { 22 | return NextResponse.json({ message: 'Something went wrong!'}, { status: 500 }) 23 | } 24 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-blogs-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@auth/prisma-adapter": "^1.0.5", 13 | "@prisma/client": "^5.5.2", 14 | "axios": "^1.6.1", 15 | "date-fns": "^2.30.0", 16 | "next": "14.0.2", 17 | "next-auth": "^4.24.4", 18 | "react": "^18", 19 | "react-dom": "^18", 20 | "react-textarea-autosize": "^8.5.3" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "autoprefixer": "^10.0.1", 27 | "eslint": "^8", 28 | "eslint-config-next": "14.0.2", 29 | "postcss": "^8", 30 | "prisma": "^5.5.2", 31 | "tailwindcss": "^3.3.0", 32 | "typescript": "^5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/blogs/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import Comments from '@/components/comments'; 2 | import FormComment from '@/components/form-comments'; 3 | import prisma from '@/lib/db'; 4 | import { FC } from 'react'; 5 | 6 | interface BlogDetailPageProps { 7 | params: { 8 | id: string; 9 | }; 10 | } 11 | const BlogDetailPage: FC = async ({ params }) => { 12 | const post = await prisma.post.findFirst({ 13 | where: { 14 | id: params.id, 15 | }, 16 | include: { 17 | author: true, 18 | }, 19 | }); 20 | 21 | return ( 22 |
23 |

{post?.title}

24 |

Written by: {post?.author?.name}

25 |
{post?.content}
26 | 27 | 28 | 29 |
30 | ); 31 | }; 32 | 33 | export default BlogDetailPage; 34 | -------------------------------------------------------------------------------- /src/app/blogs/page.tsx: -------------------------------------------------------------------------------- 1 | import prisma from '@/lib/db'; 2 | import Link from 'next/link'; 3 | 4 | const BlogsPage = async () => { 5 | const posts = await prisma.post.findMany({ 6 | orderBy: { 7 | createdAt: 'desc', 8 | }, 9 | include: { 10 | author: true, 11 | }, 12 | }); 13 | 14 | return ( 15 |
16 |

Blogs

17 |
18 | {posts.map((post) => ( 19 | 24 |

{post.title}

25 |

Written by: {post.author?.name}

26 | 27 | ))} 28 |
29 |
30 | ); 31 | }; 32 | 33 | export default BlogsPage; 34 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentUser } from '@/lib/session'; 2 | import Link from 'next/link'; 3 | import ButtonLogout from './button-logout'; 4 | 5 | const Header = async () => { 6 | const user = await getCurrentUser(); 7 | 8 | return ( 9 |
10 | 35 |
36 | ); 37 | }; 38 | 39 | export default Header; 40 | -------------------------------------------------------------------------------- /src/components/comments.tsx: -------------------------------------------------------------------------------- 1 | import prisma from '@/lib/db'; 2 | import { format } from 'date-fns'; 3 | import { FC } from 'react'; 4 | 5 | interface CommentsProps { 6 | postId: string; 7 | } 8 | const Comments: FC = async ({ postId }) => { 9 | const comments = await prisma.comment.findMany({ 10 | where: { 11 | postId, 12 | }, 13 | include: { 14 | author: true, 15 | }, 16 | }); 17 | 18 | return ( 19 |
20 |

Comments

21 |
    22 | {comments.map((comment) => ( 23 |
  • 24 |
    25 |
    26 | {comment.author?.name} 27 |
    28 |
    29 | {format(comment.createdAt, 'MMMM d, yyyy')} 30 |
    31 |
    32 |

    {comment.text}

    33 |
  • 34 | ))} 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Comments; 41 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/form-comments.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import axios from 'axios'; 4 | import { useSession } from 'next-auth/react'; 5 | import { useRouter } from 'next/navigation'; 6 | import React, { ChangeEvent, FC, useState } from 'react'; 7 | 8 | interface FormCommentProps { 9 | postId: string; 10 | } 11 | const FormComment: FC = ({ postId }) => { 12 | const [comment, setComment] = useState(''); 13 | const router = useRouter(); 14 | const { data } = useSession(); 15 | 16 | const handleCommentChange = (e: ChangeEvent) => { 17 | setComment(e.target.value); 18 | }; 19 | 20 | const handleSubmitComment = async () => { 21 | if (comment.trim() !== '') { 22 | try { 23 | const newComment = await axios.post('/api/comments', { 24 | postId, 25 | text: comment, 26 | }); 27 | if (newComment.status === 200) { 28 | router.refresh(); 29 | } 30 | } catch (error) { 31 | console.error(error); 32 | } 33 | } 34 | }; 35 | 36 | return ( 37 |
38 |
39 | 45 | 52 | 59 |
60 |
61 | ); 62 | }; 63 | 64 | export default FormComment; 65 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Account { 14 | id String @id @default(cuid()) 15 | userId String 16 | type String 17 | provider String 18 | providerAccountId String 19 | refresh_token String? @db.Text 20 | access_token String? @db.Text 21 | expires_at Int? 22 | token_type String? 23 | scope String? 24 | id_token String? @db.Text 25 | session_state String? 26 | 27 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 28 | 29 | @@unique([provider, providerAccountId]) 30 | } 31 | 32 | model Session { 33 | id String @id @default(cuid()) 34 | sessionToken String @unique 35 | userId String 36 | expires DateTime 37 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 | } 39 | 40 | model User { 41 | id String @id @default(cuid()) 42 | name String? 43 | email String? @unique 44 | emailVerified DateTime? 45 | image String? 46 | accounts Account[] 47 | sessions Session[] 48 | posts Post[] 49 | comments Comment[] 50 | } 51 | 52 | model VerificationToken { 53 | identifier String 54 | token String @unique 55 | expires DateTime 56 | 57 | @@unique([identifier, token]) 58 | } 59 | 60 | model Post { 61 | id String @id @default(cuid()) 62 | title String 63 | content String 64 | author User? @relation(fields: [authorEmail], references: [email]) 65 | authorEmail String? 66 | createdAt DateTime @default(now()) 67 | comments Comment[] 68 | } 69 | 70 | model Comment { 71 | id String @id @default(cuid()) 72 | text String 73 | createdAt DateTime @default(now()) 74 | 75 | author User? @relation(fields: [authorEmail], references: [email]) 76 | authorEmail String? 77 | Post Post? @relation(fields: [postId], references: [id]) 78 | postId String? 79 | } 80 | -------------------------------------------------------------------------------- /src/components/form-new-post.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FormData } from '@/types/blog'; 4 | import axios from 'axios'; 5 | import { useSession } from 'next-auth/react'; 6 | import { useRouter } from 'next/navigation'; 7 | import { ChangeEvent, FormEvent, useState } from 'react'; 8 | import ReactTextareaAutosize from 'react-textarea-autosize'; 9 | 10 | const inputClass = 11 | 'w-full py-2 px-3 border border-gray-300 rounded-md focus:outline-none focus:ring focus:border-blue-300'; 12 | 13 | const FormNewPost = () => { 14 | const [formData, setFormData] = useState({ 15 | title: '', 16 | content: '', 17 | }); 18 | const { data } = useSession(); 19 | const router = useRouter(); 20 | 21 | const handleChange = ( 22 | e: ChangeEvent 23 | ) => { 24 | e.preventDefault(); 25 | const { name, value } = e.target; 26 | setFormData({ 27 | ...formData, 28 | [name]: value, 29 | }); 30 | }; 31 | 32 | const handleSubmit = async (e: FormEvent) => { 33 | e.preventDefault(); 34 | 35 | try { 36 | const response = await axios.post('api/posts', formData); 37 | 38 | if (response.status === 200) { 39 | router.push(`/blogs/${response.data.newPost.id}`); 40 | } 41 | } catch (error) { 42 | console.error(error); 43 | } 44 | }; 45 | 46 | return ( 47 |
48 |
49 | 57 |
58 |
59 | 67 |
68 | 75 |
76 | ); 77 | }; 78 | 79 | export default FormNewPost; 80 | --------------------------------------------------------------------------------