├── 01-pages-router ├── .eslintrc.json ├── .gitignore ├── README.md ├── jest.config.ts ├── next.config.js ├── note.txt ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── next.svg │ ├── not_found.png │ └── vercel.svg ├── src │ ├── __test__ │ │ └── pages │ │ │ ├── __snapshots__ │ │ │ ├── about.spec.tsx.snap │ │ │ └── product.spec.tsx.snap │ │ │ ├── about.spec.tsx │ │ │ └── product.spec.tsx │ ├── components │ │ └── layouts │ │ │ ├── AppShell │ │ │ └── index.tsx │ │ │ └── Navbar │ │ │ ├── Navbar.module.css │ │ │ └── index.tsx │ ├── lib │ │ ├── firebase │ │ │ ├── init.ts │ │ │ └── service.ts │ │ └── swr │ │ │ └── fetcher.ts │ ├── middleware.ts │ ├── middlewares │ │ └── withAuth.ts │ ├── pages │ │ ├── 404.tsx │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── about │ │ │ └── index.tsx │ │ ├── admin │ │ │ └── index.tsx │ │ ├── api │ │ │ ├── [[...product]].ts │ │ │ ├── auth │ │ │ │ └── [...nextauth].ts │ │ │ ├── hello.ts │ │ │ ├── register.ts │ │ │ └── revalidate.ts │ │ ├── auth │ │ │ ├── login.tsx │ │ │ └── register.tsx │ │ ├── index.tsx │ │ ├── product │ │ │ ├── [product].tsx │ │ │ ├── index.tsx │ │ │ ├── server.tsx │ │ │ └── static.tsx │ │ ├── profile │ │ │ └── index.tsx │ │ ├── setting │ │ │ ├── app.tsx │ │ │ └── user │ │ │ │ ├── index.tsx │ │ │ │ └── password │ │ │ │ └── index.tsx │ │ └── shop │ │ │ └── [[...slug]].tsx │ ├── styles │ │ ├── 404.module.scss │ │ ├── Home.module.css │ │ ├── colors.scss │ │ └── globals.css │ ├── types │ │ └── product.type.ts │ └── views │ │ ├── Auth │ │ ├── Login │ │ │ ├── Login.module.scss │ │ │ └── index.tsx │ │ └── Register │ │ │ ├── Register.module.scss │ │ │ └── index.tsx │ │ ├── DetailProduct │ │ ├── DetailProduct.module.scss │ │ └── index.tsx │ │ └── Product │ │ ├── Product.module.scss │ │ └── index.tsx ├── tailwind.config.js └── tsconfig.json └── 02-app-router ├── .eslintrc.json ├── .gitignore ├── README.md ├── jest.config.ts ├── next.config.js ├── note.txt ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── icon.png ├── images │ └── profile.png ├── next.svg └── vercel.svg ├── src ├── __test__ │ └── app │ │ ├── __snapshots__ │ │ └── about.spec.tsx.snap │ │ └── about.spec.tsx ├── app │ ├── (admin) │ │ ├── dashboard │ │ │ ├── @analytics │ │ │ │ └── page.tsx │ │ │ ├── @payments │ │ │ │ ├── default.tsx │ │ │ │ └── page.tsx │ │ │ ├── @products │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── profile │ │ │ └── page.tsx │ ├── (auth) │ │ ├── login │ │ │ └── page.tsx │ │ └── register │ │ │ └── page.tsx │ ├── about │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── profile │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ ├── [...nextauth] │ │ │ │ └── route.ts │ │ │ └── register │ │ │ │ └── route.ts │ │ ├── product │ │ │ └── route.ts │ │ ├── revalidate │ │ │ └── route.ts │ │ └── route.ts │ ├── error.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── navbar.tsx │ ├── not-found.tsx │ ├── page.tsx │ ├── product │ │ ├── @modal │ │ │ ├── (.)detail │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ └── default.tsx │ │ ├── detail │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ ├── robots.ts │ ├── sitemap.ts │ └── template.tsx ├── components │ └── core │ │ └── Modal │ │ └── index.tsx ├── lib │ └── firebase │ │ ├── init.ts │ │ └── service.ts ├── middleware.ts ├── middlewares │ └── withAuth.ts └── services │ └── products │ └── index.tsx ├── tailwind.config.ts └── tsconfig.json /01-pages-router/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react-hooks/exhaustive-deps": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /01-pages-router/.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 | -------------------------------------------------------------------------------- /01-pages-router/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 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 20 | 21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | 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. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | -------------------------------------------------------------------------------- /01-pages-router/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | import nextJest from "next/jest.js"; 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const config: Config = { 11 | coverageProvider: "v8", 12 | testEnvironment: "jsdom", 13 | // Add more setup options before each test is run 14 | // setupFilesAfterEnv: ['/jest.setup.ts'], 15 | modulePaths: ["/src"], 16 | collectCoverage: true, 17 | collectCoverageFrom: [ 18 | "**/*.{js,jsx,ts,tsx}", 19 | "!**/*.d.ts", 20 | "!**/node_modules/**", 21 | "!**/*.types.ts", 22 | "!/coverage/**", 23 | "!/*.config.ts", 24 | "!/src/middleware.ts", 25 | "!/src/lib/**", 26 | "!/src/middlewares/**", 27 | "!/src/pages/api/**", 28 | ], 29 | }; 30 | 31 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 32 | export default createJestConfig(config); 33 | -------------------------------------------------------------------------------- /01-pages-router/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "static.nike.com", 9 | port: "", 10 | pathname: "/**", 11 | }, 12 | { 13 | protocol: "https", 14 | hostname: "lh3.googleusercontent.com", 15 | port: "", 16 | pathname: "/**", 17 | }, 18 | ], 19 | }, 20 | }; 21 | 22 | module.exports = nextConfig; 23 | -------------------------------------------------------------------------------- /01-pages-router/note.txt: -------------------------------------------------------------------------------- 1 | Belajar Framework Next Js Menggunakan Pages Router 2 | 3 | 1. Pendahuluan: 4 | - untuk membagun aplikasi menggunakan framework next js hal yang perlu di perhatikan yaitu, user interface, routing, data fetching, rendering, integrations, infrastructure, performance, scalabilitiy, dan yang terakhir developer experience 5 | - Next Js adalah sebuah framewok yang diatas react jadi bisa di katakana yaitu react framework yang mana sifatnya flexible dan menyediakan fitur-fitur untuk membuat aplikasi web dengan cepat 6 | - apakah react aja nggak cukup? kenapa harus menggunakan next Js? alasannya adalah react itu memang menyediakan fungsi-fungsi yang bisa dimanfaatkan untuk membangun ui tapi dia sifatnya unopinionated artinnya kita sebagai developer harus menentukan Kembali misalnya system untuk routingnya harus pakai react router dan lain sebagainya dan akhirnya banyak kali ekosistem reactnya yang di kembangkan oleh third party gtunya dan perkembangannya sangat tentu cepat hal tersebut tentu hal positif banyak sekali pilihan, tapi di satu sisi Ketika kita nggak ngerti setupnya nya bukan nya malah jadi bagus malah bikin programmernya binggung untuk melakukan setupnya seperti apa, sehingga untuk membagun aplikasi dengan react kalau secara utuh pasti butuh effort ter Utama untuk mengkonfigurasi alat dan juga mungkin solusi-solusi yang nanti akan di berikan aplikasinya, solusinya untuk menghandle hal tersebut kita bisa pakai yang Namanya next js dimana next js itu menyediakan tools dan konfigurasi yang mana bisa kita manfaatkan untuk mendevolp aplikasi mennggunakan react dimana dia menyediakan struktur, fitur, dan optimasi 7 | - bagaimana next js bekerja? jika kita mendevelop sebuah aplikasi itu menjadi terbagi beberapa bagian ada development, kemudian production kalau di indsutri itu ada juga staging dimana development next js memanjakan developer sedangkan production memanjakan end users, hal tersebut bisa terjadi karena next js menggunakan swc atau speedy web compiler yang dimana dia sebagai compiler yang ditulis dengan Bahasa rust yang dapat digunakan untuk melakukan compiling, bundling, minifiying dan code spiliting yang dimana next js dibagun dengan react, turbopack dan speedy web compiler 8 | - compiling yaitu adalah proses mentransformasi kode dari satu Bahasa ke dalam Bahasa lain atau versi lain dari Bahasa tersebut 9 | - kemudian setelah proses compiling dia melakukan minifying dimana yaitu proses menghapus formatting dan komentar-komentar yang tidak digunakan tanpa mengubah fungsionalitas kode dengan tujuan untuk meningkatkan kinerja aplikasi dengan mengurangi ukuran fie 10 | - setelah melakukan minifying next js akan melakukan proses bundling dimana proses bundling ini adalah mengabungkan file kedalam bundel yang kemudian di optimalkan untuk browser, yang mana tujuannya mengurangi jumlah permintaan file saat pengguna mengunjungi halaman web 11 | - setelah di bundle next js akan melakukan yang Namanya code spitting artinya setelah dibungkus satu bundle besar dia akan pisah-pisah lagi bagian-bagian lebih kecil yang mana nantinya mungkin bagian-bagian tersebut akan dibutuhkan oleh setiap entry point, entry point ini adalah url yang tujuannya untuk meningkatkan initial load time, jadi Ketika kita membuka sebuah halaman itu kita hanya menggunakan file yang di butuhkan halaman tersebut jadi kita tidak perlu me load halaman lain Ketika halaman nya dibuka itu akan jauh lebih cepat 12 | - kecantikan dari next js yaitu kita bisa menentukan untuk halaman mana yang mau dirender secara server side kemudian halaman mana yang mau dirender secara client side dan halaman mana yang bisa di render secara static side dan itu bisa kita atur per halaman contohnya halaman homepage kita bisa atur dengan client side dan halaman blog kita atur sebagai sebagai server side dan itu adalah sebuah kecantikan dari next js 13 | 14 | 2. Setup Project: 15 | - ada 2 using di next js, yaitu using pages router dan using app router 16 | - npx create-next-app@latest 17 | 18 | 3. Pages & Layout: 19 | - Ketika kita membuat routing menngunakan pages router apapupun yang ada di dalam folder pages itu menjadi routingnya 20 | - nested routing yaitu roting nya didalam folder sesuai dengan nama atau roting juga bisa sesuai nama file 21 | - nesting roting yaitu sebuah struktur bersarang didalam folder pages 22 | - dynamic routing yaitu memungkinkan kita mengakses beberapa halaman berbeda sesuai dengan parameter yang diinginkan dalam satu file saja. Konsep ini cocok diaplikasikan pada halaman-halaman yang spesifik dan dinamis seperti post article blog dan halaman produk pada e-commerce 23 | 24 | 4. Link & Navigation: 25 | - di next js punya link tersedia sehingga Ketika pindah halaman tidak merefresh halamannya 26 | - untuk link di next js kita bisa gunakan import Link from "next/link" 27 | 28 | 5. Styling: 29 | - install sebuah framework sass => npm i --save-dev sass 30 | - install sebuah framework tailwind => npm i -D tailwindcss postcss autoprefixer setelah itu kita Ketika perintah npx tailwindcss init -p 31 | 32 | 6. Custom Error Page & Document: 33 | - untuk setiap page menampilkan title gunakan tag head untuk title di setiap halamnnya atau pagenya 34 | - import Head from "next/head"; 35 | - Home 36 | - di dalam file _document kita bisa menyimpan yaitu google analitik, pixels analitik, tiktik analitik itu bisa menggunkan head di dalam file_document, karena itu akan digunakan di seluruh halaman 37 | - website referensi ilustrasi page 404 : https://undraw.co/ 38 | 39 | 7. API Routes: 40 | - next js bukan hanya sekedar framework frontend tapi juga bisa digunakan fullstack 41 | - databasenya pakai firebase di firestore database 42 | 43 | 8. Client-Side Rendering: 44 | - proses pengambilan data eksternal dan tranformasi kode menjadi representasi html dari sebuah ui terjadi di client (client-side) dan ini bersifat device karena client side 45 | - Ketika data product nya belum ready solusinya buat loading atau skecelton atau kerangka belulang 46 | - library swr adalah sebuah react hooks untuk data fetching dan ini dari vercel juga, dan swr ini juga bukan digunakan untuk client side rendering aja di support juga static side generation, dan juga server side rendering 47 | 48 | 9. Server-Side Rendering: 49 | - bahwa server side rendering ini merupakan salah satu konsep pre rendering yang ada di dalam next js 50 | - server side rendering itu adalah html di generate kmeudi html, data dan javascript dikirim ke clinet yang dilakukan saat run time 51 | 52 | 10. Static Site Generation: 53 | - static site generation yaitu html akan di generate di server namun hanya di generate sekali saat build time, sehingga content yang ditampikan bersifat statis. 54 | 55 | 11. Rendering Dynamic Routes: 56 | - Ketika kita ingin menggunakan client side rendering didalam dynamic routing itu adalah kita tangkap dulu query nya menggunakan use router, stelah ditangkap query nya baru kita fetch datanya disini mennggunakan use swr 57 | - kalau server side dan static side itu sama tidak membutuhkan loading dahulu jadi langsung aja yang dibutuhkan hanya data productnya langsung 58 | - nah terus untuk pemanggilannya itu untuk server side rendering kita gunakan sama untuk method nya Ketika kita ambil semua datanya itu menggunkan getstaticprops tapi kita tangkap parameternya apa? yaitu berupa product, nah kenapa product? nah kita sesuaikan dengan nama filenya 59 | 60 | 12. Incremental Static Regeneration: 61 | - revalidate ini nantinya akan menjadii yang me trigger untuk melakukan generate page, jadi Ketika kita membuka halamannya dia akan mencoba untuk mencek dulu gitunya ke apinya, apakah api ini perbeda dengan data yang sudah di catch sebelumnya, nah kalau perbeda dia akan mencoba untuk melakukan yang Namanya revalidate 62 | - http://localhost:3000/api/revalidate?data=product&token=12345678 63 | 64 | 13. Middleware: 65 | - Middleware yaitu memungkinkan untuk merunning code sebelum requestnya selesai dilakukan 66 | - Middleware yaitu itu sebuah code yang di jalankan sebelum saya melakukan akses ke halaman 67 | 68 | 14. Setup Next-Auth: 69 | - npm install next-auth 70 | - npm install next-auth --force 71 | 72 | 15. Auth Register: 73 | - npm install bcrypt 74 | - npm i --save-dev @types/bcrypt 75 | 76 | 16. .Login Multi Role: 77 | - if (!token) { 78 | const url = new URL("/auth/login", req.url); 79 | url.searchParams.set("callbackUrl", encodeURI(req.url)); 80 | return NextResponse.redirect(url); 81 | } 82 | - if (token.role !== "admin" && onlyAdmin.includes(pathname)) { 83 | return NextResponse.redirect(new URL("/", req.url)); 84 | } 85 | 86 | 17. Login Google: 87 | - link configurasi untuk login menggunakan google : https://console.developers.google.com/apis/credentials 88 | 89 | 18. Optimization: 90 | - optimasi script misalkan kita memanggil script ini adalah dari script dari luar contohnya adalah goggle analitik nah itu cukup berat kalau kita lakuakan panggil dari awal nah makanya ada yang dinamakan strategi di dalam text script ini, nah ini kerennya kalau misalkan kita gunakan strategi nya, nah ini kepake banget kalau teman-teman sudah pakai google analitik, facebook analik dan lain sebagainya 91 | - dan terakhir yang bisa kita lakukan untuk melakukan optimasi adalah lazy load component menggunakan next dynamic atau bisa juga di sebut sebgai dynamic import, nah disini di react juga ada namnya adalah react lazy dan juga suspend gitu kalau di react, nah kalau misalkan di next kita bisa gunakan next dynamic nya 92 | 93 | 19. Unit Testing: 94 | - npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node 95 | - npm i --save-dev @types/jest 96 | - npm run test:cover 97 | 98 | 20. Hosting Vercel: 99 | - implementasi deploy project ke vercel -------------------------------------------------------------------------------- /01-pages-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "01-pages-router", 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 | "test": "jest --passWithNoTests -u", 11 | "test:cover": "npm run test -- --coverage", 12 | "test:watch": "jest --watch" 13 | }, 14 | "dependencies": { 15 | "@types/node": "22.8.7", 16 | "@types/react": "18.3.12", 17 | "@types/react-dom": "18.3.1", 18 | "bcrypt": "^5.1.1", 19 | "eslint": "9.14.0", 20 | "eslint-config-next": "15.0.2", 21 | "firebase": "^11.0.1", 22 | "next": "15.0.2", 23 | "next-auth": "^4.24.10", 24 | "react": "18.3.1", 25 | "react-dom": "18.3.1", 26 | "swr": "^2.2.5", 27 | "typescript": "5.6.3" 28 | }, 29 | "devDependencies": { 30 | "@testing-library/dom": "^10.4.0", 31 | "@testing-library/jest-dom": "^6.6.3", 32 | "@testing-library/react": "^16.0.1", 33 | "@types/bcrypt": "^5.0.2", 34 | "@types/jest": "^29.5.14", 35 | "autoprefixer": "^10.4.20", 36 | "jest": "^29.7.0", 37 | "jest-environment-jsdom": "^29.7.0", 38 | "postcss": "^8.4.47", 39 | "sass": "^1.80.6", 40 | "tailwindcss": "^3.4.14", 41 | "ts-node": "^10.9.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /01-pages-router/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /01-pages-router/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berthutapea/learn-nextjs/760428394b30f83724c5e79648105c3150b150df/01-pages-router/public/favicon.ico -------------------------------------------------------------------------------- /01-pages-router/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01-pages-router/public/not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berthutapea/learn-nextjs/760428394b30f83724c5e79648105c3150b150df/01-pages-router/public/not_found.png -------------------------------------------------------------------------------- /01-pages-router/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /01-pages-router/src/__test__/pages/__snapshots__/about.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`About Page render about page 1`] = ` 4 | { 5 | "asFragment": [Function], 6 | "baseElement": 7 |
8 |
9 |

13 | About Page 14 |

15 |
16 |
17 | , 18 | "container":
19 |
20 |

24 | About Page 25 |

26 |
27 |
, 28 | "debug": [Function], 29 | "findAllByAltText": [Function], 30 | "findAllByDisplayValue": [Function], 31 | "findAllByLabelText": [Function], 32 | "findAllByPlaceholderText": [Function], 33 | "findAllByRole": [Function], 34 | "findAllByTestId": [Function], 35 | "findAllByText": [Function], 36 | "findAllByTitle": [Function], 37 | "findByAltText": [Function], 38 | "findByDisplayValue": [Function], 39 | "findByLabelText": [Function], 40 | "findByPlaceholderText": [Function], 41 | "findByRole": [Function], 42 | "findByTestId": [Function], 43 | "findByText": [Function], 44 | "findByTitle": [Function], 45 | "getAllByAltText": [Function], 46 | "getAllByDisplayValue": [Function], 47 | "getAllByLabelText": [Function], 48 | "getAllByPlaceholderText": [Function], 49 | "getAllByRole": [Function], 50 | "getAllByTestId": [Function], 51 | "getAllByText": [Function], 52 | "getAllByTitle": [Function], 53 | "getByAltText": [Function], 54 | "getByDisplayValue": [Function], 55 | "getByLabelText": [Function], 56 | "getByPlaceholderText": [Function], 57 | "getByRole": [Function], 58 | "getByTestId": [Function], 59 | "getByText": [Function], 60 | "getByTitle": [Function], 61 | "queryAllByAltText": [Function], 62 | "queryAllByDisplayValue": [Function], 63 | "queryAllByLabelText": [Function], 64 | "queryAllByPlaceholderText": [Function], 65 | "queryAllByRole": [Function], 66 | "queryAllByTestId": [Function], 67 | "queryAllByText": [Function], 68 | "queryAllByTitle": [Function], 69 | "queryByAltText": [Function], 70 | "queryByDisplayValue": [Function], 71 | "queryByLabelText": [Function], 72 | "queryByPlaceholderText": [Function], 73 | "queryByRole": [Function], 74 | "queryByTestId": [Function], 75 | "queryByText": [Function], 76 | "queryByTitle": [Function], 77 | "rerender": [Function], 78 | "unmount": [Function], 79 | } 80 | `; 81 | -------------------------------------------------------------------------------- /01-pages-router/src/__test__/pages/__snapshots__/product.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Product Page render product page 1`] = ` 4 | { 5 | "asFragment": [Function], 6 | "baseElement": 7 |
8 |
9 |
12 |

15 | Product 16 |

17 |
20 |
23 |
26 |
29 |
32 |
35 |
36 |
37 |
38 |
39 |
40 | , 41 | "container":
42 |
43 |
46 |

49 | Product 50 |

51 |
54 |
57 |
60 |
63 |
66 |
69 |
70 |
71 |
72 |
73 |
, 74 | "debug": [Function], 75 | "findAllByAltText": [Function], 76 | "findAllByDisplayValue": [Function], 77 | "findAllByLabelText": [Function], 78 | "findAllByPlaceholderText": [Function], 79 | "findAllByRole": [Function], 80 | "findAllByTestId": [Function], 81 | "findAllByText": [Function], 82 | "findAllByTitle": [Function], 83 | "findByAltText": [Function], 84 | "findByDisplayValue": [Function], 85 | "findByLabelText": [Function], 86 | "findByPlaceholderText": [Function], 87 | "findByRole": [Function], 88 | "findByTestId": [Function], 89 | "findByText": [Function], 90 | "findByTitle": [Function], 91 | "getAllByAltText": [Function], 92 | "getAllByDisplayValue": [Function], 93 | "getAllByLabelText": [Function], 94 | "getAllByPlaceholderText": [Function], 95 | "getAllByRole": [Function], 96 | "getAllByTestId": [Function], 97 | "getAllByText": [Function], 98 | "getAllByTitle": [Function], 99 | "getByAltText": [Function], 100 | "getByDisplayValue": [Function], 101 | "getByLabelText": [Function], 102 | "getByPlaceholderText": [Function], 103 | "getByRole": [Function], 104 | "getByTestId": [Function], 105 | "getByText": [Function], 106 | "getByTitle": [Function], 107 | "queryAllByAltText": [Function], 108 | "queryAllByDisplayValue": [Function], 109 | "queryAllByLabelText": [Function], 110 | "queryAllByPlaceholderText": [Function], 111 | "queryAllByRole": [Function], 112 | "queryAllByTestId": [Function], 113 | "queryAllByText": [Function], 114 | "queryAllByTitle": [Function], 115 | "queryByAltText": [Function], 116 | "queryByDisplayValue": [Function], 117 | "queryByLabelText": [Function], 118 | "queryByPlaceholderText": [Function], 119 | "queryByRole": [Function], 120 | "queryByTestId": [Function], 121 | "queryByText": [Function], 122 | "queryByTitle": [Function], 123 | "rerender": [Function], 124 | "unmount": [Function], 125 | } 126 | `; 127 | -------------------------------------------------------------------------------- /01-pages-router/src/__test__/pages/about.spec.tsx: -------------------------------------------------------------------------------- 1 | import AboutPage from "@/pages/about"; 2 | import { render, screen } from "@testing-library/react"; 3 | 4 | describe("About Page", () => { 5 | it("render about page", () => { 6 | const page = render(); 7 | // expect(screen.getByTestId("title").textContent).toBe("About Page"); 8 | expect(page).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /01-pages-router/src/__test__/pages/product.spec.tsx: -------------------------------------------------------------------------------- 1 | import ProductPage from "@/pages/product"; 2 | import { render, screen } from "@testing-library/react"; 3 | 4 | jest.mock("next/router", () => { 5 | return { 6 | useRouter() { 7 | return { 8 | route: "/product", 9 | pathname: "", 10 | query: "", 11 | asPath: "", 12 | push: jest.fn(), 13 | events: { 14 | on: jest.fn(), 15 | off: jest.fn(), 16 | }, 17 | beforePopState: jest.fn(() => null), 18 | prefetch: jest.fn(() => null), 19 | isReady: true, 20 | }; 21 | }, 22 | }; 23 | }); 24 | 25 | describe("Product Page", () => { 26 | it("render product page", () => { 27 | const page = render(); 28 | expect(page).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /01-pages-router/src/components/layouts/AppShell/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { Roboto } from "next/font/google"; 3 | import dynamic from "next/dynamic"; 4 | 5 | const Navbar = dynamic(() => import("../Navbar")); 6 | 7 | type AppShellProps = { 8 | children: React.ReactNode; 9 | }; 10 | 11 | const roboto = Roboto({ 12 | subsets: ["latin"], 13 | weight: ["400", "700"], 14 | }); 15 | 16 | const disableNavbar = ["/auth/login", "/auth/register", "/404"]; 17 | 18 | const AppShell = (props: AppShellProps) => { 19 | const { children } = props; 20 | const { pathname } = useRouter(); 21 | return ( 22 |
23 | {!disableNavbar.includes(pathname) && } 24 | {children} 25 |
26 | ); 27 | }; 28 | 29 | export default AppShell; 30 | -------------------------------------------------------------------------------- /01-pages-router/src/components/layouts/Navbar/Navbar.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | display: flex; 3 | width: 100%; 4 | height: 40px; 5 | background-color: #000; 6 | color: #fff; 7 | align-items: center; 8 | padding: 0 5%; 9 | justify-content: space-between; 10 | } 11 | 12 | .button { 13 | background-color: #fff; 14 | color: #000; 15 | font-size: 14px; 16 | padding: 4px 10px; 17 | } 18 | 19 | .avatar { 20 | width: 30px; 21 | height: 30px; 22 | border-radius: 50%; 23 | } 24 | 25 | .profile { 26 | display: flex; 27 | align-items: center; 28 | gap: 10px; 29 | } 30 | -------------------------------------------------------------------------------- /01-pages-router/src/components/layouts/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { signIn, signOut, useSession } from "next-auth/react"; 2 | import styles from "./Navbar.module.css"; 3 | import Script from "next/script"; 4 | import Image from "next/image"; 5 | 6 | const Navbar = () => { 7 | const { data }: any = useSession(); 8 | return ( 9 |
10 |
11 | Navbar 12 |
13 | 16 |
17 | {data?.user?.image && ( 18 | {data.user.fullname} 25 | )} 26 | {data && data.user.fullname}{" "} 27 | {data ? ( 28 | 31 | ) : ( 32 | 35 | )} 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Navbar; 42 | -------------------------------------------------------------------------------- /01-pages-router/src/lib/firebase/init.ts: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | // TODO: Add SDKs for Firebase products that you want to use 4 | // https://firebase.google.com/docs/web/setup#available-libraries 5 | 6 | // Your web app's Firebase configuration 7 | const firebaseConfig = { 8 | apiKey: process.env.FIREBASE_API_KEY, 9 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 10 | projectId: process.env.FIREBASE_PROJECT_ID, 11 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET, 12 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, 13 | appId: process.env.FIREBASE_APP_ID, 14 | }; 15 | 16 | // Initialize Firebase 17 | const app = initializeApp(firebaseConfig); 18 | 19 | export default app; 20 | -------------------------------------------------------------------------------- /01-pages-router/src/lib/firebase/service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addDoc, 3 | collection, 4 | doc, 5 | getDoc, 6 | getDocs, 7 | getFirestore, 8 | query, 9 | updateDoc, 10 | where, 11 | } from "firebase/firestore"; 12 | import app from "./init"; 13 | import bcrypt from "bcrypt"; 14 | import { error } from "console"; 15 | 16 | const firestore = getFirestore(app); 17 | 18 | export async function retrieveData(CollectionName: string) { 19 | const snapshot = await getDocs(collection(firestore, CollectionName)); 20 | 21 | const data = snapshot.docs.map((doc) => ({ 22 | id: doc.id, 23 | ...doc.data(), 24 | })); 25 | 26 | return data; 27 | } 28 | 29 | export async function retrieveDataById(CollectionName: string, id: string) { 30 | const snapshot = await getDoc(doc(firestore, CollectionName, id)); 31 | const data = snapshot.data(); 32 | 33 | return data; 34 | } 35 | 36 | export async function signIn(userData: { email: string }) { 37 | const q = query( 38 | collection(firestore, "users"), 39 | where("email", "==", userData.email) 40 | ); 41 | const snapshot = await getDocs(q); 42 | const data = snapshot.docs.map((doc) => ({ 43 | id: doc.id, 44 | ...doc.data(), 45 | })); 46 | if (data) { 47 | return data[0]; 48 | } else { 49 | return null; 50 | } 51 | } 52 | 53 | export async function signUp( 54 | userData: { 55 | email: string; 56 | fullname: string; 57 | password: string; 58 | role?: string; 59 | }, 60 | callback: Function 61 | ) { 62 | const q = query( 63 | collection(firestore, "users"), 64 | where("email", "==", userData.email) 65 | ); 66 | const snapshot = await getDocs(q); 67 | const data = snapshot.docs.map((doc) => ({ 68 | id: doc.id, 69 | ...doc.data(), 70 | })); 71 | if (data.length > 0) { 72 | callback({ status: false, message: "Email already exist" }); 73 | } else { 74 | userData.password = await bcrypt.hash(userData.password, 10); 75 | userData.role = "member"; 76 | await addDoc(collection(firestore, "users"), userData) 77 | .then(() => { 78 | callback({ status: true, message: "Register success" }); 79 | }) 80 | .catch((error) => { 81 | callback({ status: false, message: error }); 82 | }); 83 | } 84 | } 85 | 86 | export async function signInWithGoogle(userData: any, callback: any) { 87 | const q = query( 88 | collection(firestore, "users"), 89 | where("email", "==", userData.email) 90 | ); 91 | const snapshot = await getDocs(q); 92 | const data: any = snapshot.docs.map((doc) => ({ 93 | id: doc.id, 94 | ...doc.data(), 95 | })); 96 | 97 | if (data.length > 0) { 98 | userData.role = data[0].role; 99 | await updateDoc(doc(firestore, "users", data[0].id), userData) 100 | .then(() => { 101 | callback({ 102 | status: true, 103 | message: "Sign in with Google success", 104 | data: userData, 105 | }); 106 | }) 107 | .catch(() => { 108 | callback({ 109 | status: false, 110 | message: "Sign in with Google failed", 111 | }); 112 | }); 113 | } else { 114 | userData.role = "member"; 115 | await addDoc(collection(firestore, "users"), userData) 116 | .then(() => { 117 | callback({ 118 | status: true, 119 | message: "Sign in with Google success", 120 | data: userData, 121 | }); 122 | }) 123 | .catch(() => { 124 | callback({ 125 | status: false, 126 | message: "Sign in with Google failed", 127 | }); 128 | }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /01-pages-router/src/lib/swr/fetcher.ts: -------------------------------------------------------------------------------- 1 | export const fetcher = (url: string) => fetch(url).then((res) => res.json()); 2 | -------------------------------------------------------------------------------- /01-pages-router/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | import withAuth from "./middlewares/withAuth"; 4 | 5 | export function mainMiddleware(req: NextRequest) { 6 | const res = NextResponse.next(); 7 | return res; 8 | } 9 | 10 | export default withAuth(mainMiddleware, ["/profile", "/admin"]); 11 | -------------------------------------------------------------------------------- /01-pages-router/src/middlewares/withAuth.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from "next-auth/jwt"; 2 | import { 3 | NextFetchEvent, 4 | NextMiddleware, 5 | NextRequest, 6 | NextResponse, 7 | } from "next/server"; 8 | 9 | const onlyAdmin = ["/admin"]; 10 | 11 | export default function withAuth( 12 | middleware: NextMiddleware, 13 | requireAuth: string[] = [] 14 | ) { 15 | return async (req: NextRequest, next: NextFetchEvent) => { 16 | const pathname = req.nextUrl.pathname; 17 | if (requireAuth.includes(pathname)) { 18 | const token = await getToken({ 19 | req, 20 | secret: process.env.NEXTAUTH_SECRET, 21 | }); 22 | if (!token) { 23 | const url = new URL("/auth/login", req.url); 24 | url.searchParams.set("callbackUrl", encodeURI(req.url)); 25 | return NextResponse.redirect(url); 26 | } 27 | if (token.role !== "admin" && onlyAdmin.includes(pathname)) { 28 | return NextResponse.redirect(new URL("/", req.url)); 29 | } 30 | } 31 | 32 | return middleware(req, next); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import styles from "@/styles/404.module.scss"; 2 | import Image from "next/image"; 3 | 4 | const Custom404 = () => { 5 | return ( 6 |
7 | {/* 404 */} 8 | 404 15 |
Halaman Tidak Ditemukan
16 |
17 | ); 18 | }; 19 | 20 | export default Custom404; 21 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import AppShell from "@/components/layouts/AppShell"; 2 | import "@/styles/globals.css"; 3 | import type { AppProps } from "next/app"; 4 | import { SessionProvider } from "next-auth/react"; 5 | 6 | export default function App({ 7 | Component, 8 | pageProps: { session, ...pageProps }, 9 | }: AppProps) { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/about/index.tsx: -------------------------------------------------------------------------------- 1 | const AboutPage = () => { 2 | return ( 3 |
4 |

About Page

5 |
6 | ); 7 | }; 8 | 9 | export default AboutPage; 10 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/admin/index.tsx: -------------------------------------------------------------------------------- 1 | const AdminPage = () => { 2 | return ( 3 |
4 |

Admin Page

5 |
6 | ); 7 | }; 8 | 9 | export default AdminPage; 10 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/api/[[...product]].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import { retrieveData, retrieveDataById } from "@/lib/firebase/service"; 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | 5 | type Data = { 6 | status: boolean; 7 | statusCode: Number; 8 | data: any; 9 | }; 10 | 11 | export default async function handler( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | if (req.query.product![1]) { 16 | const data = await retrieveDataById("products", req.query.product![1]); 17 | res.status(200).json({ status: true, statusCode: 200, data }); 18 | } else { 19 | const data = await retrieveData("products"); 20 | res.status(200).json({ status: true, statusCode: 200, data }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import { signIn, signInWithGoogle } from "@/lib/firebase/service"; 2 | import { compare } from "bcrypt"; 3 | import { NextAuthOptions } from "next-auth"; 4 | import NextAuth from "next-auth/next"; 5 | import CredentialsProvider from "next-auth/providers/credentials"; 6 | import GoogleProvider from "next-auth/providers/google"; 7 | 8 | const authOptions: NextAuthOptions = { 9 | session: { 10 | strategy: "jwt", 11 | }, 12 | secret: process.env.NEXTAUTH_SECRET, 13 | providers: [ 14 | CredentialsProvider({ 15 | type: "credentials", 16 | name: "Credentials", 17 | credentials: { 18 | email: { label: "Email", type: "email" }, 19 | password: { label: "Password", type: "password" }, 20 | }, 21 | async authorize(credentials) { 22 | const { email, password } = credentials as { 23 | email: string; 24 | password: string; 25 | }; 26 | const user: any = await signIn({ email }); 27 | if (user) { 28 | const passwordConfirm = await compare(password, user.password); 29 | if (passwordConfirm) { 30 | return user; 31 | } 32 | return null; 33 | } else { 34 | return null; 35 | } 36 | }, 37 | }), 38 | GoogleProvider({ 39 | clientId: process.env.GOOGLE_OAUTH_CLIENT_ID || "", 40 | clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET || "", 41 | }), 42 | ], 43 | callbacks: { 44 | async jwt({ token, account, profile, user }: any) { 45 | if (account?.provider === "credentials") { 46 | token.email = user.email; 47 | token.fullname = user.fullname; 48 | token.role = user.role; 49 | } 50 | if (account?.provider === "google") { 51 | const data = { 52 | fullname: user.name, 53 | email: user.email, 54 | image: user.image, 55 | type: "google", 56 | }; 57 | 58 | await signInWithGoogle( 59 | data, 60 | (result: { status: boolean; messagge: string; data: any }) => { 61 | if (result.status) { 62 | token.email = result.data.email; 63 | token.fullname = result.data.fullname; 64 | token.type = result.data.type; 65 | token.image = result.data.image; 66 | token.role = result.data.role; 67 | } 68 | } 69 | ); 70 | } 71 | return token; 72 | }, 73 | 74 | async session({ session, token }: any) { 75 | if ("email" in token) { 76 | session.user.email = token.email; 77 | } 78 | if ("fullname" in token) { 79 | session.user.fullname = token.fullname; 80 | } 81 | if ("image" in token) { 82 | session.user.image = token.image; 83 | } 84 | if ("role" in token) { 85 | session.user.role = token.role; 86 | } 87 | return session; 88 | }, 89 | }, 90 | pages: { 91 | signIn: "/auth/login", 92 | }, 93 | }; 94 | 95 | export default NextAuth(authOptions); 96 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | name: string; 6 | age: number; 7 | }; 8 | 9 | export default function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | res.status(200).json({ name: "Gilbert Hutapea", age: 22 }); 14 | } 15 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/api/register.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import { signUp } from "@/lib/firebase/service"; 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | 5 | type Data = { 6 | status: boolean; 7 | message: string; 8 | }; 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse 13 | ) { 14 | if (req.method === "POST") { 15 | await signUp( 16 | req.body, 17 | ({ status, message }: { status: boolean; message: string }) => { 18 | if (status) { 19 | res.status(200).json({ status, message }); 20 | } else { 21 | res.status(400).json({ status, message }); 22 | } 23 | } 24 | ); 25 | } else { 26 | res.status(405).json({ status: false, message: "Method not allowed" }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/api/revalidate.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | revalidated: boolean; 6 | message?: string; 7 | }; 8 | 9 | export default async function handler( 10 | req: NextApiRequest, 11 | res: NextApiResponse 12 | ) { 13 | if (req.query.token !== process.env.REVALIDATE_TOKEN) { 14 | return res 15 | .status(401) 16 | .json({ revalidated: false, message: "Insert correct token" }); 17 | } 18 | if (req.query.data === "product") { 19 | try { 20 | await res.revalidate("/product/static"); 21 | return res.json({ revalidated: true }); 22 | } catch (error) { 23 | return res.status(500).send({ revalidated: false }); 24 | } 25 | } 26 | return res.json({ 27 | revalidated: false, 28 | message: "Select your data first", 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/auth/login.tsx: -------------------------------------------------------------------------------- 1 | import LoginView from "@/views/Auth/Login"; 2 | 3 | const LoginPage = () => { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default LoginPage; 12 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/auth/register.tsx: -------------------------------------------------------------------------------- 1 | import RegisterView from "@/views/Auth/Register"; 2 | 3 | const RegisterPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default RegisterPage; 12 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 | Home 8 | 9 |
Hello Gilbert Hutapea
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/product/[product].tsx: -------------------------------------------------------------------------------- 1 | import { fetcher } from "@/lib/swr/fetcher"; 2 | import { ProductType } from "@/types/product.type"; 3 | import DetailProduct from "@/views/DetailProduct"; 4 | import { useRouter } from "next/router"; 5 | import useSWR from "swr"; 6 | 7 | const DetailProductPage = ({ product }: { product: ProductType }) => { 8 | const { query } = useRouter(); 9 | 10 | // Client Side 11 | 12 | // const { data, error, isLoading } = useSWR( 13 | // `/api/product/${query.product}`, 14 | // fetcher 15 | // ); 16 | 17 | return ( 18 |
19 | {/* client-side */} 20 | 21 | {/* */} 22 | 23 | {/* Server-Side & Static Side */} 24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default DetailProductPage; 31 | 32 | // Server Side 33 | 34 | export async function getServerSideProps({ 35 | params, 36 | }: { 37 | params: { product: string }; 38 | }) { 39 | console.log(params.product); 40 | 41 | // fetch data 42 | const res = await fetch( 43 | `http://localhost:3000/api/product/${params.product}` 44 | ); 45 | const response = await res.json(); 46 | 47 | return { 48 | props: { 49 | product: response.data, 50 | }, 51 | }; 52 | } 53 | 54 | // Static Side 55 | 56 | // export async function getStaticPaths() { 57 | // const res = await fetch("http://localhost:3000/api/product"); 58 | // const response = await res.json(); 59 | 60 | // const paths = response.data.map((product: ProductType) => ({ 61 | // params: { product: product.id }, 62 | // })); 63 | 64 | // console.log(paths); 65 | 66 | // return { paths, fallback: false }; 67 | // } 68 | 69 | // export async function getStaticProps({ 70 | // params, 71 | // }: { 72 | // params: { product: string }; 73 | // }) { 74 | // // fetch data 75 | // const res = await fetch( 76 | // `http://localhost:3000/api/product/${params.product}` 77 | // ); 78 | // const response = await res.json(); 79 | 80 | // return { 81 | // props: { 82 | // product: response.data, 83 | // }, 84 | // }; 85 | // } 86 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/product/index.tsx: -------------------------------------------------------------------------------- 1 | import { fetcher } from "@/lib/swr/fetcher"; 2 | import ProductView from "@/views/Product"; 3 | import { useRouter } from "next/router"; 4 | import { useEffect, useState } from "react"; 5 | import useSWR from "swr"; 6 | 7 | const ProductPage = () => { 8 | const [products, setProducts] = useState([]); 9 | 10 | const { data, error, isLoading } = useSWR("/api/product", fetcher); 11 | 12 | // useEffect(() => { 13 | // fetch("/api/product") 14 | // .then((res) => res.json()) 15 | // .then((response) => { 16 | // setProducts(response.data); 17 | // }); 18 | // }, []); 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | }; 26 | 27 | export default ProductPage; 28 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/product/server.tsx: -------------------------------------------------------------------------------- 1 | import ProductView from "@/views/Product"; 2 | import { ProductType } from "@/types/product.type"; 3 | 4 | const ProductPage = (props: { products: ProductType[] }) => { 5 | const { products } = props; 6 | 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | export default ProductPage; 15 | 16 | export async function getServerSideProps() { 17 | // fetch data 18 | const res = await fetch("http://localhost:3000/api/product"); 19 | const response = await res.json(); 20 | 21 | return { 22 | props: { 23 | products: response.data, 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/product/static.tsx: -------------------------------------------------------------------------------- 1 | import { ProductType } from "@/types/product.type"; 2 | import ProductView from "@/views/Product"; 3 | 4 | const ProductPage = (props: { products: ProductType[] }) => { 5 | const { products } = props; 6 | 7 | return ( 8 |
9 | 10 |
11 | ); 12 | }; 13 | 14 | export default ProductPage; 15 | 16 | export async function getStaticProps() { 17 | // fetch data 18 | const res = await fetch("http://localhost:3000/api/product"); 19 | const response = await res.json(); 20 | 21 | return { 22 | props: { 23 | products: response.data, 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSession } from "next-auth/react"; 2 | 3 | const ProfilePage = () => { 4 | const { data }: any = useSession(); 5 | return ( 6 |
7 |

Profile

8 |

{data && data.user.fullname}

9 |
10 | ); 11 | }; 12 | 13 | export default ProfilePage; 14 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/setting/app.tsx: -------------------------------------------------------------------------------- 1 | const AppSettingPage = () => { 2 | return ( 3 |
4 |

App Setting

5 |
6 | ); 7 | }; 8 | 9 | export default AppSettingPage; 10 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/setting/user/index.tsx: -------------------------------------------------------------------------------- 1 | const UserSettingPage = () => { 2 | return ( 3 |
4 |

User Setting

5 |
6 | ); 7 | }; 8 | 9 | export default UserSettingPage; 10 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/setting/user/password/index.tsx: -------------------------------------------------------------------------------- 1 | const UserPasswordSettingPage = () => { 2 | return ( 3 |
4 |

User Password Setting

5 |
6 | ); 7 | }; 8 | 9 | export default UserPasswordSettingPage; 10 | -------------------------------------------------------------------------------- /01-pages-router/src/pages/shop/[[...slug]].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | 3 | const ShopPage = () => { 4 | const { query } = useRouter(); 5 | 6 | return ( 7 |
8 |

Shop Page

9 |

Shop : {`${query.slug && query.slug[0] + "-" + query.slug[1]}`}

10 |
11 | ); 12 | }; 13 | 14 | export default ShopPage; 15 | -------------------------------------------------------------------------------- /01-pages-router/src/styles/404.module.scss: -------------------------------------------------------------------------------- 1 | .error { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | color: #6c63ff; 9 | font-size: 2rem; 10 | &__image { 11 | width: 50%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /01-pages-router/src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | width: var(--max-width); 46 | max-width: 100%; 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo { 108 | position: relative; 109 | } 110 | /* Enable hover only on non-touch devices */ 111 | @media (hover: hover) and (pointer: fine) { 112 | .card:hover { 113 | background: rgba(var(--card-rgb), 0.1); 114 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 115 | } 116 | 117 | .card:hover span { 118 | transform: translateX(4px); 119 | } 120 | } 121 | 122 | @media (prefers-reduced-motion) { 123 | .card:hover span { 124 | transform: none; 125 | } 126 | } 127 | 128 | /* Mobile */ 129 | @media (max-width: 700px) { 130 | .content { 131 | padding: 4rem; 132 | } 133 | 134 | .grid { 135 | grid-template-columns: 1fr; 136 | margin-bottom: 120px; 137 | max-width: 320px; 138 | text-align: center; 139 | } 140 | 141 | .card { 142 | padding: 1rem 2.5rem; 143 | } 144 | 145 | .card h2 { 146 | margin-bottom: 0.5rem; 147 | } 148 | 149 | .center { 150 | padding: 8rem 0 6rem; 151 | } 152 | 153 | .center::before { 154 | transform: none; 155 | height: 300px; 156 | } 157 | 158 | .description { 159 | font-size: 0.8rem; 160 | } 161 | 162 | .description a { 163 | padding: 1rem; 164 | } 165 | 166 | .description p, 167 | .description div { 168 | display: flex; 169 | justify-content: center; 170 | position: fixed; 171 | width: 100%; 172 | } 173 | 174 | .description p { 175 | align-items: center; 176 | inset: 0 0 auto; 177 | padding: 2rem 1rem 1.4rem; 178 | border-radius: 0; 179 | border: none; 180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 181 | background: linear-gradient( 182 | to bottom, 183 | rgba(var(--background-start-rgb), 1), 184 | rgba(var(--callout-rgb), 0.5) 185 | ); 186 | background-clip: padding-box; 187 | backdrop-filter: blur(24px); 188 | } 189 | 190 | .description div { 191 | align-items: flex-end; 192 | pointer-events: none; 193 | inset: auto 0 0; 194 | padding: 2rem; 195 | height: 200px; 196 | background: linear-gradient( 197 | to bottom, 198 | transparent 0%, 199 | rgb(var(--background-end-rgb)) 40% 200 | ); 201 | z-index: 1; 202 | } 203 | } 204 | 205 | /* Tablet and Smaller Desktop */ 206 | @media (min-width: 701px) and (max-width: 1120px) { 207 | .grid { 208 | grid-template-columns: repeat(2, 50%); 209 | } 210 | } 211 | 212 | @media (prefers-color-scheme: dark) { 213 | .vercelLogo { 214 | filter: invert(1); 215 | } 216 | 217 | .logo { 218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 219 | } 220 | } 221 | 222 | @keyframes rotate { 223 | from { 224 | transform: rotate(360deg); 225 | } 226 | to { 227 | transform: rotate(0deg); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /01-pages-router/src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | $schema: ( 2 | primary: #fff, 3 | ); 4 | -------------------------------------------------------------------------------- /01-pages-router/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | box-sizing: border-box; 7 | padding: 0; 8 | margin: 0; 9 | } 10 | 11 | html, 12 | body { 13 | max-width: 100vw; 14 | overflow-x: hidden; 15 | font-family: Arial, Helvetica, sans-serif; 16 | } 17 | 18 | a { 19 | color: inherit; 20 | text-decoration: none; 21 | } 22 | 23 | .big { 24 | font-size: 1rem; 25 | } 26 | -------------------------------------------------------------------------------- /01-pages-router/src/types/product.type.ts: -------------------------------------------------------------------------------- 1 | export type ProductType = { 2 | id: string; 3 | name: string; 4 | price: number; 5 | image: string; 6 | category: string; 7 | }; 8 | -------------------------------------------------------------------------------- /01-pages-router/src/views/Auth/Login/Login.module.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex-direction: column; 6 | height: 100vh; 7 | width: 100vw; 8 | &__title { 9 | font-size: 32px; 10 | margin-bottom: 10px; 11 | } 12 | &__error { 13 | color: #fd3131; 14 | margin-bottom: 10px; 15 | } 16 | &__form { 17 | width: 50%; 18 | padding: 20px; 19 | box-shadow: 0 0 3px rgba($color: #000000, $alpha: 0.5); 20 | margin-bottom: 20px; 21 | &__item { 22 | display: flex; 23 | flex-direction: column; 24 | margin: 20px 0; 25 | &__input { 26 | padding: 10px; 27 | background-color: #eee; 28 | margin-top: 5px; 29 | } 30 | &__button { 31 | background-color: #000000; 32 | color: #fff; 33 | width: 100%; 34 | padding: 10px; 35 | } 36 | &__google { 37 | width: 100%; 38 | text-align: center; 39 | margin-top: 20px; 40 | } 41 | } 42 | } 43 | &__link { 44 | a { 45 | color: #23bebe; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /01-pages-router/src/views/Auth/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import styles from "./Login.module.scss"; 3 | import { useState } from "react"; 4 | import { useRouter } from "next/router"; 5 | import { signIn } from "next-auth/react"; 6 | 7 | const LoginView = () => { 8 | const [isLoading, setIsLoading] = useState(false); 9 | const [error, setError] = useState(""); 10 | 11 | const { push, query } = useRouter(); 12 | 13 | const callbackUrl: any = query.callbackUrl || "/"; 14 | 15 | const handleSubmit = async (event: any) => { 16 | event.preventDefault(); 17 | setError(""); 18 | setIsLoading(true); 19 | try { 20 | const res = await signIn("credentials", { 21 | redirect: false, 22 | email: event.target.email.value, 23 | password: event.target.password.value, 24 | callbackUrl, 25 | }); 26 | if (!res?.error) { 27 | setIsLoading(false); 28 | push(callbackUrl); 29 | } else { 30 | setIsLoading(false); 31 | setError("Email or password is incorrect"); 32 | } 33 | } catch (error: any) { 34 | setIsLoading(false); 35 | setError("Email or password is incorrect"); 36 | } 37 | }; 38 | return ( 39 |
40 |

Login

41 | {error &&

{error}

} 42 |
43 |
44 |
45 | 48 | 55 |
56 |
57 | 63 | 70 |
71 | 78 |
79 | 90 |
91 |

92 | Don{"'"}t have an account? Sign up{" "} 93 | here 94 |

95 |
96 | ); 97 | }; 98 | 99 | export default LoginView; 100 | -------------------------------------------------------------------------------- /01-pages-router/src/views/Auth/Register/Register.module.scss: -------------------------------------------------------------------------------- 1 | .register { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex-direction: column; 6 | height: 100vh; 7 | width: 100vw; 8 | &__title { 9 | font-size: 32px; 10 | margin-bottom: 10px; 11 | } 12 | &__error { 13 | color: #fd3131; 14 | margin-bottom: 10px; 15 | } 16 | &__form { 17 | width: 50%; 18 | padding: 20px; 19 | box-shadow: 0 0 3px rgba($color: #000000, $alpha: 0.5); 20 | margin-bottom: 20px; 21 | &__item { 22 | display: flex; 23 | flex-direction: column; 24 | margin: 20px 0; 25 | &__input { 26 | padding: 10px; 27 | background-color: #eee; 28 | margin-top: 5px; 29 | } 30 | &__button { 31 | background-color: #000000; 32 | color: #fff; 33 | width: 100%; 34 | padding: 10px; 35 | } 36 | } 37 | } 38 | &__link { 39 | a { 40 | color: #23bebe; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /01-pages-router/src/views/Auth/Register/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import styles from "./Register.module.scss"; 3 | import { useState } from "react"; 4 | import { useRouter } from "next/router"; 5 | 6 | const RegisterView = () => { 7 | const [isLoading, setIsLoading] = useState(false); 8 | const [error, setError] = useState(""); 9 | const { push } = useRouter(); 10 | const handleSubmit = async (event: any) => { 11 | event.preventDefault(); 12 | setError(""); 13 | setIsLoading(true); 14 | const data = { 15 | email: event.target.email.value, 16 | fullname: event.target.fullname.value, 17 | password: event.target.password.value, 18 | }; 19 | const result = await fetch("/api/register", { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify(data), 25 | }); 26 | 27 | if (result.status === 200) { 28 | event.target.reset(); 29 | setIsLoading(false); 30 | push("/auth/login"); 31 | } else { 32 | setIsLoading(false); 33 | setError(result.status === 400 ? "Email already exist" : ""); 34 | } 35 | }; 36 | return ( 37 |
38 |

Register

39 | {error &&

{error}

} 40 |
41 |
42 |
43 | 49 | 56 |
57 |
58 | 64 | 71 |
72 |
73 | 79 | 86 |
87 | 94 |
95 |
96 |

97 | Have an account? Sign in here 98 |

99 |
100 | ); 101 | }; 102 | 103 | export default RegisterView; 104 | -------------------------------------------------------------------------------- /01-pages-router/src/views/DetailProduct/DetailProduct.module.scss: -------------------------------------------------------------------------------- 1 | .productDetail { 2 | width: 25%; 3 | padding: 10px; 4 | margin: 0 auto; 5 | &__name { 6 | font-size: 20px; 7 | font-weight: bold; 8 | margin-top: 5px; 9 | } 10 | &__category { 11 | color: gray; 12 | margin-top: 5px; 13 | } 14 | &__price { 15 | font-weight: bold; 16 | margin-top: 10px; 17 | } 18 | } 19 | .title { 20 | text-align: center; 21 | font-size: 32px; 22 | } 23 | -------------------------------------------------------------------------------- /01-pages-router/src/views/DetailProduct/index.tsx: -------------------------------------------------------------------------------- 1 | import { ProductType } from "@/types/product.type"; 2 | import styles from "./DetailProduct.module.scss"; 3 | 4 | const DetailProduct = ({ product }: { product: ProductType }) => { 5 | return ( 6 | <> 7 |

Detail Product

8 |
9 |
10 | {product.name} 11 |
12 |

{product.name}

13 |

{product.category}

14 |

15 | {product.price && 16 | product.price.toLocaleString("id-ID", { 17 | style: "currency", 18 | currency: "IDR", 19 | })} 20 |

21 |
22 | 23 | ); 24 | }; 25 | 26 | export default DetailProduct; 27 | -------------------------------------------------------------------------------- /01-pages-router/src/views/Product/Product.module.scss: -------------------------------------------------------------------------------- 1 | .product { 2 | width: 100%; 3 | padding: 0 5%; 4 | &__title { 5 | text-align: center; 6 | font-size: 32px; 7 | font-weight: bold; 8 | } 9 | &__content { 10 | display: flex; 11 | &__skeleton { 12 | width: 25%; 13 | padding: 10px; 14 | animation: blinking 2s infinite; 15 | &__image { 16 | width: 100%; 17 | aspect-ratio: 1 / 1; 18 | background-color: grey; 19 | } 20 | &__name { 21 | width: 100%; 22 | height: 20px; 23 | margin-top: 5px; 24 | background-color: grey; 25 | } 26 | &__category { 27 | width: 100%; 28 | height: 16px; 29 | background-color: grey; 30 | margin-top: 5px; 31 | } 32 | &__price { 33 | width: 100%; 34 | height: 16px; 35 | margin-top: 10px; 36 | background-color: grey; 37 | } 38 | } 39 | &__item { 40 | width: 25%; 41 | padding: 10px; 42 | &__name { 43 | font-size: 20px; 44 | font-weight: bold; 45 | margin-top: 5px; 46 | } 47 | &__category { 48 | color: gray; 49 | margin-top: 5px; 50 | } 51 | &__price { 52 | font-weight: bold; 53 | margin-top: 10px; 54 | } 55 | } 56 | } 57 | } 58 | 59 | @keyframes blinking { 60 | 0% { 61 | opacity: 1; 62 | } 63 | 50% { 64 | opacity: 0; 65 | } 66 | 100% { 67 | opacity: 1; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /01-pages-router/src/views/Product/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import styles from "./Product.module.scss"; 3 | import { ProductType } from "@/types/product.type"; 4 | import Image from "next/image"; 5 | 6 | const ProductView = ({ products }: { products: ProductType[] }) => { 7 | return ( 8 |
9 |

Product

10 |
11 | {products?.length > 0 ? ( 12 | <> 13 | {products?.map((product: ProductType) => ( 14 | 19 |
20 | {/* {product.name} */} 21 | {product.name} 27 |
28 |

29 | {product.name} 30 |

31 |

32 | {product.category} 33 |

34 |

35 | {product.price.toLocaleString("id-ID", { 36 | style: "currency", 37 | currency: "IDR", 38 | })} 39 |

40 | 41 | ))} 42 | 43 | ) : ( 44 |
45 |
46 |
47 |
48 |
49 |
50 | )} 51 |
52 |
53 | ); 54 | }; 55 | export default ProductView; 56 | -------------------------------------------------------------------------------- /01-pages-router/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx,mdx}", // Note the addition of the `app` directory. 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | 8 | // Or if using `src` directory: 9 | "./src/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | theme: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /01-pages-router/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 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /02-app-router/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /02-app-router/.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 | -------------------------------------------------------------------------------- /02-app-router/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 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | 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. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /02-app-router/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | import nextJest from "next/jest.js"; 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const config: Config = { 11 | coverageProvider: "v8", 12 | testEnvironment: "jsdom", 13 | // Add more setup options before each test is run 14 | // setupFilesAfterEnv: ['/jest.setup.ts'], 15 | modulePaths: ["/src"], 16 | collectCoverage: true, 17 | collectCoverageFrom: [ 18 | "**/*.{js,jsx,ts,tsx}", 19 | "!**/*.d.ts", 20 | "!**/node_modules/**", 21 | "!/coverage/**", 22 | "!**/*.type.ts", 23 | "!/.next/**", 24 | "!/*.config.js", 25 | "!/*.config.ts", 26 | "!/src/app/api/**", 27 | "!/src/lib/**", 28 | "!/src/middlewares/**", 29 | "!/src/middleware.ts", 30 | ], 31 | }; 32 | 33 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 34 | export default createJestConfig(config); 35 | -------------------------------------------------------------------------------- /02-app-router/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "static.nike.com", 8 | port: "", 9 | pathname: "/**", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /02-app-router/note.txt: -------------------------------------------------------------------------------- 1 | Belajar Framework Next Js Menggunakan App Router 2 | 3 | 1. Setup Project: 4 | - npx create-next-app@latest 5 | - Ketika kita membuat sebuah routing di dalam app router kalau kita lihat disini ada segmentasi dulu ya, kita bahas dari segmentasi dulu jadi di setiap folder itu mempresentasikan segmentasi route nya dan url nya nanti akan di mampping ke dalam segmentasi tersebut sehingga membuat nested route itu kita bisa menyimpan folder nya di dalam folder lainnya 6 | 7 | 2. Layout & Template: 8 | - layout adalah dimana ui yang di shared diantara multiple pages jadi nanti layout ini bisa di akses oleh multiple pages, di dalam navigation layouts itu biasanya berisi state, remain interactive dan juga tidak melakukan re render dan layout itu juga bisa kita nesred artinya kita bisa gunakan layout bersarang 9 | - Template ini bisa dikatakan mirip dengan layout tapi dia nggak kayak layout yang dimana dia bisa digunakan di setiap route dan juga bisa digunakan untuk maintance state, nah template ini dia akan membuat instance baru untuk setiap turunan di dalam navigasi nya 10 | - kalua mau menggunakan usestate maka harus menggunakan "use client" jadi dia sifatnya client component bukan server component 11 | - perbedaan layout dan template yaitu untuk layout statenya di bawa sedangkan template state nya nggak di bawa atau new instance dia melakukan instace ulang artinya adalah si template ini di reset Kembali 12 | 13 | 3. Group & Dynamic Routes: 14 | - untuk membuat group kita buat tanda () untuk di folder sehingga tidak menjadi routing 15 | - untuk grouping itu setiap routenya bisa juga kita berikan layout yang berbeda beda jadi sama saja Ketika kita membuat layout kan bisa secara global kalau kita simpannya di luar tapi kalau kita buat juga layout di dalam folder auth atau admin itu bisa juga 16 | - dynamic routes yaitu bisaanya berhubungan dengan slug 17 | - kalau di app router dynamic routes pakai folder sedangkan pages router memakai file 18 | 19 | 4. Link & Navigation: 20 | - di dalam link dan navigating ada 2 metode yaitu yang pertama menggunakan link kompenen dan yang kedua menggunakan useRouter 21 | 22 | 5. API Route Handlers: 23 | - route handler yaitu megizinkan kita untuk membuat custom request handlers menggunakan web request dan respon api nah jadi ini mirip hamper dengan api handler yang ada di dalam pages router 24 | - untuk membuat route handlers yaitu mendefinisikan menjadi sebuah file yaitu route.js|ts di dalam app directory 25 | - route handle bisa di nested di dalam ap directory seperti page dan layout tapi dia tidak bisa di dalam route segemen yang sama dengan page.js, jadi kalua ada page.js route.js itu tidak bisa di akses 26 | - nah kemudian dia mensuport http method yaitu seperti get, post, put, patch, delete, head dan options 27 | 28 | 6. Data Fetching: 29 | - jadi next js itu secara native dia bisa menggunakan fetch nya dari web api yang mana memperbolehkan kita untuk mengkonfigurasi caching dan revalidating dimana kita bisa menggunakan fetch dengan async/await dialam server components kemudian bisa kita lakukan juga di dalam route handler dan juga sever action 30 | 31 | 7. Caching & Revalidating: 32 | - caching itu artinya adalah kita tidak membutuhkan lagi untuk melakukan fetch data setiap mau melakukan request, jadi secra default next js secara otomatis akan melakukan cache dan me return value dari hasil fetch tersebut bukan dari apinya tapi dari data cache di dalam server, jadi sudah di caching terlebih dahulu, nah dalam hal ini dia bisa nge fetch data pada saat build time atau pada saat request time kemudian di cache dan juga bisa digunakan lagi setiap kali kita me request untuk membutuhkan data tersebut 33 | - revalidate yaitu adalah sebuah proses untuk purging data ke dalam data cache dan melakukan refacth terhadap data terakhir dan ini berguna Ketika datanya berubah kita ingin memastikan yang ditampilkan adalah informasi yang terakhir 34 | - ada 2 cara untuk melakukan revalidate tehadap cache yaitu adalah time based revalidation dan juga on demand revalidation 35 | - nah katanya di sini time based revalidation itu dia otomatis akan melakukan revalidate setelah kian Waktu yang kita set yang sudah terlewat dia akan melakukan revalidate, nah jadi dia disini berdasarkan Waktu yang kita set 36 | - kemudian ada yang Namanya on demand revalidation yaitu adalah melakukan revalidate secara manual, jadi dia manual melakukan revalidate berdasarkan event tertentu 37 | - fungsi revalidate yaitu untuk ngejar performace system tersebut 38 | 39 | 8. Loading UI & Error Handling: 40 | - loading ui yaitu merupakan sebuah loading state yang di load terlebih dahulu sembari nunggu content nya seleai di load 41 | - kalau di next js di app router di sediakan langsung untuk fitur loadingnya, cukup buat file loading.jsx|tsx 42 | - kita tidak perlu lagi buat pegecekannya cukup buat file loading.tsx nya di dalam folder yang dibutuhkan 43 | - untuk menghadle halaman error kita cukup buat sebuah file error.jsx|tsx 44 | 45 | 9. Middleware: 46 | - jadi berdasarkan dokumentasi nextjs katanya middleware memungkinkan kita untuk menjalankan code sebelum requestnya selesai kemudian berdasarkan request kita bisa melakukan modifikasi terhadap responnya baik itu rewriting, redicting, modifying the request ataupun response headers dan juga responding directly 47 | - middleware itu di run sebelum content di cache dan juga route match nya dan disini katanya untuk menggunakan file middleware itu bisa di simpannya di dalam route dari projectnya untuk mendefinikasikan middlewarenya contohnya disini adalah dimana kita harus menyimpan middleware itu sejajar dengan folder pages ataupun app di dalam src 48 | - matcher adalah memungkinkan kita untuk memfilter middleware itu berjalan pada path yang spesifik 49 | 50 | 10. Parallel Routes: 51 | - parallel routes ini yaitu memungkinkan kita untuk melakukan codicional render terhadap sebuah page atau lebih di dalam layout yang sama, contohnya di dalam dashboard dan feeds on social sites 52 | - jadi parallel routing itu bisa digunakan untuk mengimplementasikan pattern routing yang lumayan kompleks 53 | 54 | 11. Intercepting Route: 55 | - intercepting route yaitu memungkinkan kita untuk me load sebuah route dari part aplikasi yang lain dengan layout saat ini 56 | - paragdigma route seperti ini bisa berguna Ketika kita ingin menampilkan konten dari sebuah route tanpa user melakukan switch ke konteks yang lain artinya ke page yang lain 57 | - convention (.) yaitu untuk segmen dengan level yang sama 58 | - convention (..) yaitu jika segmennya ada di luar sebuah folder misalnya 59 | - convention (..)(..) yaitu untuk dua level diatasnya 60 | - convention (...) yaitu jika segmen berada di app directory 61 | 62 | 12. Connect Firebase DB: 63 | - npm install firebase 64 | 65 | 13. Setup Next-Auth: 66 | - npm i next-auth@4.24.3 67 | 68 | 14. Auth Register: 69 | - npm i bcrypt 70 | - npm i --save-dev @types/bcrypt 71 | 72 | 15. Login System: 73 | - login system menggunkan database firebase 74 | 75 | 16. Login Multi Role: 76 | - kenapa Ketika belum login bisa kelihatan dashboard nya? karena dia masih dirender di sisi client untuk si pengecekannya, harusnya untuk best practice nya adalah kalau kita buat pembatasan seprti ini kita akan menggunakan middleware 77 | 78 | 17. Login With Google: 79 | - google developer console 80 | - api & services => enabled api & services 81 | 82 | 18. Optimization: 83 | - jadi di dalam built in components itu teryata ada beberapa component yang bisa kita manfaatkan untuk bikin codingan kita menjadi optimal yaitu ada image, link dan juga scriptnya 84 | - images sendiri disini katanya built on the native image element, jadi dia bisa melakukan lazy load image dan juga dia bisa resizing images berdasrkan device 85 | - kemudian kalau link di dibangun dari tag a dimana katanya disini kalau misalkan pake tag a itu load dulu tapi kalua page tag link dia lebih cepat dan smooth untuk page trasnsitions nya, jadi nggak perlu lakukan load halaman dulu nya 86 | - kemudian ada script ini dibangun berdasarkan script tag dimana disini script kalua misalkan menambhakan script seperti google analitik itu kan sesuatu hal yang cukup lemayan berat dan perlu controlling yang baik supaya performance meningkat nah dengan menggunakan tag script itu jauh lebih baik untuk performance ya 87 | - font menggunakan next font yang sudah disediakan next font 88 | - static assets yaitu bisa menampilkan seperti images itu tinggal simpan aja di dalam folder public didalam rootnya, nah disini katanya di dalam public kita bisa mereferesns kode kita dengan memulai mengggunakan base url / 89 | - kemudian tadi lazy load juga sebetulnya bukan cumin di image bukan cumin di script tapi juga bisa gunakannya di component, untuk menggunakn component tinggal menggunakan next dynamic kemudian masukan import komponennya ke dalam dynamic tersebut 90 | - untuk component a di load immediately di dalam client bundle 91 | - untuk component b dirender opsional atu sesuai kebutuhan kalua kondisi nya memenuhi maka dia akan di render 92 | - kemudian utuk component c karena ssr nya di false maka load only on the client side 93 | - kalau misalkan component nya belum berhasil ke load maka tampilkan loading 94 | 95 | 19. SEO & Metadata: 96 | - next js punya metadata api yang bisa digunakan untuk mendefine sebuah metadata dari aplikasi 97 | - biasnya metadata itu berupa meta dan link yang berada di dalam tag head sebuah html tujuannya untuk me improve seo biasanya dan juga web shareabilitiy 98 | - ada 2 cara yang bisanya untuk menambahkan meta data ke dalam sebuah aplikasi yang pertama ada config based metadata yaitu adalah mengunakan static metadata object ataupun dynamic nya dan juga file based metadata 99 | - kalau kita mau pakai metadata itu harus menggunkan server component, nah kalau mau pakai metadata di next js itu nggak bisa untuk client component 100 | - manifest.json ini biasanya adalah untuk meyimpan informasi mengenai website atai web aplication untuk browser kita, nah salah satu kenggunannya untuk apa? mungkin teman-teman sudah pernah tau progresive web app atau pwa jadi web kita bisa di instal di dalam komputer kita atau kedalam device tepatnya karena di mobile juga bisa kita install sehingga nanti dia bisa langsung kita jalankan tanpa harus menggunakan browser nah itu bisa digunakan menggunakan manifest.json ini jadi kita butuh untuk manifest.json 101 | - lalu ada opengraph-image dan twitter-image bisa menggunakan tag meta seperti dokumentasi di next js itu juga bisa kita gunakan di dalam metadata nya, ini bisanya digunakan untuk sharebell, dan untuk image di opengraph itu biasanya diambil dari cover imagenya misalnya kalau blog di share dll 102 | - kemudian ada yang namaya yaitu robots.txt ini yaitu standard file yang digunakan supaya nanti apakah website nya itu diperbolehkan untuk diakses oleh searh engine coller, kalau misalkan di allow nanti akan bisa di tampilkan di dalam search engine nya, tapi kalau misalkan kita dia di disallow maka bisanya tidak ditampilkan 103 | - fungsi dari robots.txt ini apa? kalau kita sudah bekerja dengan yang namanya evnviropment ada env production ada env staging, nah kalau staging mungkin robots.txt ini akan kita lakukan disallow karena staging itu biasa digunakan untuk texting aja, jadi internal dari team yang mendevelop yang boleh mengakses nya, tapi kalua misalnya production itu kan sesuatu hal yang bisa di akses oleh user dan kita pengen gitu kalau kita ngesearch kayak tdi misalnya metanesia dia akan langsung muncul di google dia harus yang pakai namannya robots.txt 104 | - fungsi yang paling Utama untuk robots.txt yaitu untuk memudahkan pencarian atai search engine 105 | - sitemap.xml fungsi yaitu hamper mirip dengan robots.txt adalah bagaimana untuk bantu search engine melakukan crawling terdap setiap halaman yang ada di dalam website kita 106 | - nah secara default itu kita bisa ini menggunakan static sitemap.xml, jadi nanti nya semua di url yang di crawling oleh robotnya itu nanti di daftarkan di sini, kalua kita pakai codingan lebih dynamic atau date nya berubah-rubah 107 | - biasanya pengunaan robots.txr dan sitemap.xml itu dimanana? itu biasanya di google search console 108 | - google search console itu adalah sebuah tools yang disediakan oleh google untuk meningkatkan pencarian kita nanti tinggal didaftarkan aja domainnya disini dan kemudian sitemap nya bisa kita masukkan 109 | - karena next js app router sudah menyediakan fitur metadata ini nya sehingga mudah mengimplementasikannya dan juga tentunya dokumentasi yang jelas lebih mudah untuk mencari tau bagaiman cara untuk kodingannya 110 | 111 | 20. Custom Error Page: 112 | - jadi kalau di dalam next app router itu kita bisa menggunakan sebuah file special Namanya not-found.js|ts ketika nanti halamnya itu tidak di temukan 113 | - error juga bisa kita handle menggunkan error.tsx 114 | 115 | 21. Unit Testing: 116 | - biar kode bisa naikan ke production itu harus di lakukan yang Namanya unit testing dan standardrisasi nya harus ter coverage berapa persen 117 | - untuk mesetup unit testing di dalam next js app roter itu sama saja dengan pages router menggunkan jest 118 | - untuk unit testing menggunakan jest dan react testing library 119 | - npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom 120 | - npm i --save-dev @types/jest 121 | - "test": "jest --passWithNoTests -u", 122 | - "test:cover": "npm run test -- --coverage", 123 | - "test:watch": "jest --watch" 124 | - npm run test:cover 125 | - npm run test 126 | - npm i --save-dev ts-node 127 | - biasanya kalau di perusahaan itu rata-rata adalah di atas 80% untuk coverage testing baru boleh naik ke production 128 | 129 | 22. Hosting Vercel: 130 | - npm i swr => buat library fetch data disis client 131 | - untuk masalah data.data karena data nya belum ke load solusinya data?.data karena kalau datanya sudah siap baru kita render, ya kalau nggak ya udah tidak di render 132 | 133 | -------------------------------------------------------------------------------- /02-app-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-app-router", 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 | "test": "jest --passWithNoTests -u", 11 | "test:cover": "npm run test -- --coverage", 12 | "test:watch": "jest --watch" 13 | }, 14 | "dependencies": { 15 | "@types/node": "22.9.3", 16 | "@types/react": "18.3.12", 17 | "@types/react-dom": "18.3.1", 18 | "autoprefixer": "10.4.20", 19 | "bcrypt": "^5.1.1", 20 | "eslint": "9.15.0", 21 | "eslint-config-next": "15.0.3", 22 | "firebase": "^11.0.2", 23 | "next": "^13.4.19", 24 | "next-auth": "^4.24.3", 25 | "postcss": "8.4.49", 26 | "react": "18.3.1", 27 | "react-dom": "18.3.1", 28 | "swr": "^2.2.5", 29 | "tailwindcss": "3.4.15", 30 | "typescript": "5.7.2" 31 | }, 32 | "devDependencies": { 33 | "@testing-library/jest-dom": "^6.6.3", 34 | "@testing-library/react": "^16.0.1", 35 | "@types/bcrypt": "^5.0.2", 36 | "@types/jest": "^29.5.14", 37 | "jest": "^29.7.0", 38 | "jest-environment-jsdom": "^29.7.0", 39 | "ts-node": "^10.9.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /02-app-router/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /02-app-router/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berthutapea/learn-nextjs/760428394b30f83724c5e79648105c3150b150df/02-app-router/public/icon.png -------------------------------------------------------------------------------- /02-app-router/public/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berthutapea/learn-nextjs/760428394b30f83724c5e79648105c3150b150df/02-app-router/public/images/profile.png -------------------------------------------------------------------------------- /02-app-router/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-app-router/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02-app-router/src/__test__/app/__snapshots__/about.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`About Page should render 1`] = ` 4 | { 5 | "asFragment": [Function], 6 | "baseElement": 7 |
8 | 25 |
26 |
27 |

28 | About Page 29 |

30 |
31 |
32 |
33 | , 34 | "container":
35 | 52 |
53 |
54 |

55 | About Page 56 |

57 |
58 |
59 |
, 60 | "debug": [Function], 61 | "findAllByAltText": [Function], 62 | "findAllByDisplayValue": [Function], 63 | "findAllByLabelText": [Function], 64 | "findAllByPlaceholderText": [Function], 65 | "findAllByRole": [Function], 66 | "findAllByTestId": [Function], 67 | "findAllByText": [Function], 68 | "findAllByTitle": [Function], 69 | "findByAltText": [Function], 70 | "findByDisplayValue": [Function], 71 | "findByLabelText": [Function], 72 | "findByPlaceholderText": [Function], 73 | "findByRole": [Function], 74 | "findByTestId": [Function], 75 | "findByText": [Function], 76 | "findByTitle": [Function], 77 | "getAllByAltText": [Function], 78 | "getAllByDisplayValue": [Function], 79 | "getAllByLabelText": [Function], 80 | "getAllByPlaceholderText": [Function], 81 | "getAllByRole": [Function], 82 | "getAllByTestId": [Function], 83 | "getAllByText": [Function], 84 | "getAllByTitle": [Function], 85 | "getByAltText": [Function], 86 | "getByDisplayValue": [Function], 87 | "getByLabelText": [Function], 88 | "getByPlaceholderText": [Function], 89 | "getByRole": [Function], 90 | "getByTestId": [Function], 91 | "getByText": [Function], 92 | "getByTitle": [Function], 93 | "queryAllByAltText": [Function], 94 | "queryAllByDisplayValue": [Function], 95 | "queryAllByLabelText": [Function], 96 | "queryAllByPlaceholderText": [Function], 97 | "queryAllByRole": [Function], 98 | "queryAllByTestId": [Function], 99 | "queryAllByText": [Function], 100 | "queryAllByTitle": [Function], 101 | "queryByAltText": [Function], 102 | "queryByDisplayValue": [Function], 103 | "queryByLabelText": [Function], 104 | "queryByPlaceholderText": [Function], 105 | "queryByRole": [Function], 106 | "queryByTestId": [Function], 107 | "queryByText": [Function], 108 | "queryByTitle": [Function], 109 | "rerender": [Function], 110 | "unmount": [Function], 111 | } 112 | `; 113 | -------------------------------------------------------------------------------- /02-app-router/src/__test__/app/about.spec.tsx: -------------------------------------------------------------------------------- 1 | import AboutPage from "@/app/about/page"; 2 | import AboutLayout from "@/app/about/layout"; 3 | import { render, screen } from "@testing-library/react"; 4 | 5 | describe("About Page", () => { 6 | it("should render", () => { 7 | const page = render( 8 | 9 | 10 | 11 | ); 12 | expect(page).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /02-app-router/src/app/(admin)/dashboard/@analytics/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Analytics() { 2 | return ( 3 |
4 |

Analytics

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /02-app-router/src/app/(admin)/dashboard/@payments/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /02-app-router/src/app/(admin)/dashboard/@payments/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Payments() { 2 | return ( 3 |
4 |

Payments

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /02-app-router/src/app/(admin)/dashboard/@products/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | export default function AdminProductPage() { 6 | const [status, setStatus] = useState(""); 7 | const revalidate = async () => { 8 | const res = await fetch( 9 | `${process.env.NEXT_PUBLIC_API_URL}/api/revalidate?tag=products&secret=Gilbert12345`, 10 | { 11 | method: "POST", 12 | } 13 | ); 14 | if (!res.ok) { 15 | setStatus("Revalidated Failed"); 16 | } else { 17 | const response = await res.json(); 18 | if (response.revalidate) { 19 | setStatus("Revalidated Success"); 20 | } 21 | } 22 | }; 23 | 24 | return ( 25 |
26 |

{status}

27 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /02-app-router/src/app/(admin)/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ 2 | children, 3 | products, 4 | analytics, 5 | payments, 6 | }: { 7 | children: React.ReactNode; 8 | products: React.ReactNode; 9 | analytics: React.ReactNode; 10 | payments: React.ReactNode; 11 | }) { 12 | return ( 13 |
14 |
{children}
15 |
16 | {products} 17 | {analytics} 18 |
19 | {payments} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /02-app-router/src/app/(admin)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSession } from "next-auth/react"; 4 | import { useRouter } from "next/navigation"; 5 | import { useEffect } from "react"; 6 | 7 | export default function DashboardPage() { 8 | return ( 9 |
10 |

Dashboard

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /02-app-router/src/app/(admin)/profile/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSession } from "next-auth/react"; 4 | 5 | export default function ProfilePage() { 6 | const { data: session }: { data: any } = useSession(); 7 | return ( 8 |
9 |

Profil Page

10 |

{session?.user?.fullname}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /02-app-router/src/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import Link from "next/link"; 5 | import { useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | 8 | export default function LoginPage({ searchParams }: any) { 9 | const { push } = useRouter(); 10 | const [error, setError] = useState(""); 11 | const [isLoading, setIsLoading] = useState(false); 12 | 13 | const callbackUrl = searchParams.callbackUrl || "/"; 14 | const handleLogin = async (e: any) => { 15 | e.preventDefault(); 16 | setError(""); 17 | setIsLoading(true); 18 | try { 19 | const res = await signIn("credentials", { 20 | redirect: false, 21 | email: e.currentTarget.email.value, 22 | password: e.currentTarget.password.value, 23 | callbackUrl, 24 | }); 25 | if (!res?.error) { 26 | e.target.reset(); 27 | setIsLoading(false); 28 | push(callbackUrl); 29 | } else { 30 | setIsLoading(false); 31 | if (res.status === 401) { 32 | setError("Email or password is incorrect"); 33 | } 34 | } 35 | } catch (err) { 36 | console.log(err); 37 | } 38 | }; 39 | 40 | return ( 41 |
42 | {error !== "" && ( 43 |
{error}
44 | )} 45 |
46 |
handleLogin(e)}> 47 |

48 | Sign in to our platform 49 |

50 |
51 | 57 | 65 |
66 |
67 | 73 | 81 |
82 | 89 |
90 | 97 |
98 | Not registered?{" "} 99 | 103 | Create account 104 | 105 |
106 |
107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /02-app-router/src/app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useRouter } from "next/navigation"; 5 | import { useState } from "react"; 6 | 7 | export default function RegisterPage() { 8 | const { push } = useRouter(); 9 | const [error, setError] = useState(""); 10 | const [isLoading, setIsLoading] = useState(false); 11 | const handleSubmit = async (e: any) => { 12 | e.preventDefault(); 13 | setError(""); 14 | setIsLoading(true); 15 | const res = await fetch("/api/auth/register", { 16 | method: "POST", 17 | body: JSON.stringify({ 18 | fullname: e.target.fullname.value, 19 | email: e.target.email.value, 20 | password: e.target.password.value, 21 | }), 22 | }); 23 | 24 | if (res.status === 200) { 25 | e.target.reset(); 26 | setIsLoading(false); 27 | push("/login"); 28 | } else { 29 | setError("Email already exist"); 30 | setIsLoading(false); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 |
37 | {error !== "" && ( 38 |
{error}
39 | )} 40 |
41 |
42 |
handleSubmit(e)}> 43 |

44 | Sign up to our platform 45 |

46 |
47 | 53 | 61 |
62 |
63 | 69 | 77 |
78 |
79 | 85 | 93 |
94 | 101 |
102 | Have registered?{" "} 103 | 107 | Sign in here 108 | 109 |
110 |
111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /02-app-router/src/app/about/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function AboutLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 | <> 8 | 15 |
{children}
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /02-app-router/src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | export default function AboutPage() { 2 | return ( 3 |
4 |

About Page

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /02-app-router/src/app/about/profile/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ProfileLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 | <> 8 |

Title

9 |
{children}
10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /02-app-router/src/app/about/profile/page.tsx: -------------------------------------------------------------------------------- 1 | export default function ProPage() { 2 | return ( 3 |
4 |

Profile Page

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /02-app-router/src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { login, loginWithGoogle } from "@/lib/firebase/service"; 2 | import { compare } from "bcrypt"; 3 | import { NextAuthOptions } from "next-auth"; 4 | import NextAuth from "next-auth/next"; 5 | import CredentialsProvider from "next-auth/providers/credentials"; 6 | import GoogleProvider from "next-auth/providers/google"; 7 | 8 | const authOptions: NextAuthOptions = { 9 | session: { 10 | strategy: "jwt", 11 | }, 12 | secret: process.env.NEXTAUTH_SECRET, 13 | providers: [ 14 | CredentialsProvider({ 15 | type: "credentials", 16 | name: "Credentials", 17 | credentials: { 18 | email: { label: "Email", type: "email" }, 19 | password: { label: "Password", type: "password" }, 20 | }, 21 | async authorize(credentials) { 22 | const { email, password } = credentials as { 23 | email: string; 24 | password: string; 25 | }; 26 | const user: any = await login({ email }); 27 | if (user) { 28 | const passwordConfirm = await compare(password, user.password); 29 | if (passwordConfirm) { 30 | return user; 31 | } 32 | return null; 33 | } else { 34 | return null; 35 | } 36 | }, 37 | }), 38 | GoogleProvider({ 39 | clientId: process.env.GOOGLE_OAUTH_CLIENT_ID || "", 40 | clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET || "", 41 | }), 42 | ], 43 | callbacks: { 44 | async jwt({ token, account, profile, user }: any) { 45 | if (account?.provider === "credentials") { 46 | token.email = user.email; 47 | token.fullname = user.fullname; 48 | token.role = user.role; 49 | } 50 | 51 | if (account?.provider === "google") { 52 | const data = { 53 | fullname: user.name, 54 | email: user.email, 55 | type: "google", 56 | }; 57 | await loginWithGoogle( 58 | data, 59 | (result: { status: boolean; data: any }) => { 60 | if (result.status) { 61 | token.email = result.data.email; 62 | token.fullname = result.data.fullname; 63 | token.role = result.data.role; 64 | } 65 | } 66 | ); 67 | } 68 | return token; 69 | }, 70 | async session({ session, token }: any) { 71 | if ("email" in token) { 72 | session.user.email = token.email; 73 | } 74 | if ("fullname" in token) { 75 | session.user.fullname = token.fullname; 76 | } 77 | if ("role" in token) { 78 | session.user.role = token.role; 79 | } 80 | return session; 81 | }, 82 | }, 83 | pages: { 84 | signIn: "/login", 85 | }, 86 | }; 87 | 88 | const handler = NextAuth(authOptions); 89 | 90 | export { handler as GET, handler as POST }; 91 | -------------------------------------------------------------------------------- /02-app-router/src/app/api/auth/register/route.ts: -------------------------------------------------------------------------------- 1 | import { register } from "@/lib/firebase/service"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function POST(request: NextRequest) { 5 | const req = await request.json(); 6 | const res = await register(req); 7 | return NextResponse.json( 8 | { status: res.status, message: res.message }, 9 | { status: res.statusCode } 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /02-app-router/src/app/api/product/route.ts: -------------------------------------------------------------------------------- 1 | import { retrieveData, retrieveDataById } from "@/lib/firebase/service"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const data = [ 6 | { 7 | id: 1, 8 | title: "Nike Pegasus 41 Blueprint", 9 | price: 2099000, 10 | image: 11 | "https://static.nike.com/a/images/c_limit,w_592,f_auto/t_product_v1/4615e080-c7a7-470e-b96a-02370aba3276/AIR+ZOOM+PEGASUS+41+FP.png", 12 | }, 13 | { 14 | id: 2, 15 | title: "Nike Mercurial Superfly 10 Academy", 16 | price: 1499000, 17 | image: 18 | "https://static.nike.com/a/images/c_limit,w_592,f_auto/t_product_v1/a10e080b-1985-48dc-83d5-ce0d86a294b7/ZM+SUPERFLY+10+ACADEMY+TF.png", 19 | }, 20 | { 21 | id: 3, 22 | title: "Giannis Freak 6 EP", 23 | price: 2199000, 24 | image: 25 | "https://static.nike.com/a/images/c_limit,w_592,f_auto/t_product_v1/cad08379-10fd-45a9-98db-5689453f8353/GIANNIS+FREAK+6+NRG+EP.png", 26 | }, 27 | { 28 | id: 4, 29 | title: "Giannis Freak 6 EP", 30 | price: 2199000, 31 | image: 32 | "https://static.nike.com/a/images/c_limit,w_592,f_auto/t_product_v1/cad08379-10fd-45a9-98db-5689453f8353/GIANNIS+FREAK+6+NRG+EP.png", 33 | }, 34 | ]; 35 | 36 | export async function GET(request: NextRequest) { 37 | const { searchParams } = new URL(request.url); 38 | const id = searchParams.get("id"); 39 | if (id) { 40 | const detailProduct = await retrieveDataById("products", id); 41 | if (detailProduct) { 42 | return NextResponse.json({ 43 | status: 200, 44 | message: "Success", 45 | data: detailProduct, 46 | }); 47 | } 48 | return NextResponse.json({ 49 | status: 404, 50 | message: "Not Found", 51 | data: {}, 52 | }); 53 | } 54 | 55 | const products = await retrieveData("products"); 56 | 57 | return NextResponse.json({ status: 200, message: "Success", data: products }); 58 | } 59 | -------------------------------------------------------------------------------- /02-app-router/src/app/api/revalidate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { revalidateTag } from "next/cache"; 3 | 4 | export async function POST(request: NextRequest) { 5 | const tag = request.nextUrl.searchParams.get("tag"); 6 | const secret = request.nextUrl.searchParams.get("secret"); 7 | 8 | if (secret !== process.env.REVALIDATE_TOKEN) { 9 | return NextResponse.json( 10 | { status: 401, message: "Invalid secret" }, 11 | { status: 401 } 12 | ); 13 | } 14 | 15 | if (!tag) { 16 | return NextResponse.json( 17 | { status: 400, message: "Missing tag param" }, 18 | { 19 | status: 400, 20 | } 21 | ); 22 | } 23 | 24 | revalidateTag(tag); 25 | 26 | return NextResponse.json({ revalidate: true, now: Date.now() }); 27 | } 28 | -------------------------------------------------------------------------------- /02-app-router/src/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export async function GET() { 4 | return NextResponse.json({ status: 200, message: "Success" }); 5 | } 6 | -------------------------------------------------------------------------------- /02-app-router/src/app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export default function Error() { 4 | return ( 5 |
6 |

500

7 |

Something went wrong

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /02-app-router/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berthutapea/learn-nextjs/760428394b30f83724c5e79648105c3150b150df/02-app-router/src/app/favicon.ico -------------------------------------------------------------------------------- /02-app-router/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /02-app-router/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "./globals.css"; 4 | import Navbar from "./navbar"; 5 | import { usePathname } from "next/navigation"; 6 | import { SessionProvider } from "next-auth/react"; 7 | import { Poppins } from "next/font/google"; 8 | 9 | const poppins = Poppins({ 10 | subsets: ["latin"], 11 | weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], 12 | }); 13 | 14 | const disableNavbar = ["/login", "/register"]; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | const pathname = usePathname(); 22 | return ( 23 | 24 | 25 | 26 | {!disableNavbar.includes(pathname) && } 27 | {children} 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /02-app-router/src/app/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { signIn, signOut, useSession } from "next-auth/react"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | export default function Navbar() { 7 | const pathname = usePathname(); 8 | const { data: session, status }: { data: any; status: string } = useSession(); 9 | 10 | return ( 11 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /02-app-router/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

404

7 |

Page Not Found

8 | 9 | Back to Home 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /02-app-router/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | 3 | export const metadata: Metadata = { 4 | metadataBase: new URL( 5 | process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000" 6 | ), 7 | title: "Home - Gilbert Hutapea", 8 | description: "Aplikasi untuk belajar Next JS", 9 | authors: [{ name: "Gilbert Hutapea", url: "http://localhost:3000" }], 10 | icons: { 11 | icon: "/icon.png", 12 | }, 13 | openGraph: { 14 | title: "Home - Gilbert Hutapea", 15 | }, 16 | }; 17 | 18 | export default function Home() { 19 | return ( 20 |
21 | Hello World 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /02-app-router/src/app/product/@modal/(.)detail/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // import { getData } from "@/services/products"; 4 | import dynamic from "next/dynamic"; 5 | import Image from "next/image"; 6 | import useSWR from "swr"; 7 | 8 | const Modal = dynamic(() => import("@/components/core/Modal")); 9 | 10 | const fetcher = (url: string) => fetch(url).then((res) => res.json()); 11 | 12 | export default function DetailProductPage(props: any) { 13 | const { params } = props; 14 | // const product = await getData( 15 | // `${process.env.NEXT_PUBLIC_API_URL}/api/product/?id=${params.id}` 16 | // ); 17 | 18 | const { data, error, isLoading } = useSWR( 19 | `${process.env.NEXT_PUBLIC_API_URL}/api/product/?id=${params.id}`, 20 | fetcher 21 | ); 22 | 23 | const product = { 24 | data: data ? data.data : [], 25 | }; 26 | 27 | return ( 28 | 29 | product 36 |
37 |

{product.data.name}

38 |

Price: ${product.data.price}

39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /02-app-router/src/app/product/@modal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /02-app-router/src/app/product/detail/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // import { getData } from "@/services/products"; 4 | import useSWR from "swr"; 5 | 6 | const fetcher = (url: string) => fetch(url).then((res) => res.json()); 7 | 8 | export default function DetailProductPage(props: any) { 9 | const { params } = props; 10 | // const product = await getData( 11 | // `${process.env.NEXT_PUBLIC_API_URL}/api/product/?id=${params.id}` 12 | // ); 13 | 14 | const { data, error, isLoading } = useSWR( 15 | `${process.env.NEXT_PUBLIC_API_URL}/api/product/?id=${params.id}`, 16 | fetcher 17 | ); 18 | 19 | const product = { 20 | data: data ? data.data : [], 21 | }; 22 | 23 | return ( 24 |
25 |
26 | 31 |
32 |

{product.data.name}

33 |

Price: ${product.data.price}

34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /02-app-router/src/app/product/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | erorr, 7 | reset, 8 | }: { 9 | erorr: Error; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | console.log(erorr); 14 | }, [erorr]); 15 | 16 | return ( 17 |
18 |

Something went wrong!

19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /02-app-router/src/app/product/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ 2 | children, 3 | modal, 4 | }: { 5 | children: React.ReactNode; 6 | modal: React.ReactNode; 7 | }) { 8 | return ( 9 | <> 10 | {children} 11 | {modal} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /02-app-router/src/app/product/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return ( 3 |
4 |
5 |
6 | 7 |
8 | 9 |
10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /02-app-router/src/app/product/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // import { getData } from "@/services/products"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | import useSWR from "swr"; 7 | 8 | type ProductPageProps = { params: { slug: string[] } }; 9 | 10 | const fetcher = (url: string) => fetch(url).then((res) => res.json()); 11 | 12 | export default function ProductPage(props: ProductPageProps) { 13 | const { params } = props; 14 | const { data, error, isLoading } = useSWR( 15 | `${process.env.NEXT_PUBLIC_API_URL}/api/product`, 16 | fetcher 17 | ); 18 | 19 | // const products = await getData(`${process.env.NEXT_PUBLIC_URL}/api/product`); 20 | 21 | const products = { 22 | data: data ? data.data : [], 23 | }; 24 | 25 | return ( 26 |
27 | {/*

{params.slug ? "Detail Product Page" : "Product Page"}

*/} 28 | 29 | {products.data.length > 0 && 30 | products.data.map((product: any) => ( 31 | 36 | product image 44 |
45 |
46 | {product.name} 47 |
48 |
49 | 50 | $ {product.price} 51 | 52 | 58 |
59 |
60 | 61 | ))} 62 | 63 | {params.slug && ( 64 | <> 65 |

Category: {params.slug[0]}

66 |

Gender: {params.slug[1]}

67 |

Id: {params.slug[2]}

68 | 69 | )} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /02-app-router/src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: "/", 8 | disallow: ["/login/", "/register/"], 9 | }, 10 | sitemap: "https://acme.com/sitemap.xml", 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /02-app-router/src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | 3 | export default function sitemap(): MetadataRoute.Sitemap { 4 | return [ 5 | { 6 | url: "https://berthutapea.com", 7 | lastModified: new Date(), 8 | changeFrequency: "yearly", 9 | priority: 1, 10 | }, 11 | { 12 | url: "https://berthutapea.com/about", 13 | lastModified: new Date(), 14 | changeFrequency: "monthly", 15 | priority: 0.8, 16 | }, 17 | { 18 | url: "https://berthutapea.com/blog", 19 | lastModified: new Date(), 20 | changeFrequency: "weekly", 21 | priority: 0.5, 22 | }, 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /02-app-router/src/app/template.tsx: -------------------------------------------------------------------------------- 1 | // "use client"; 2 | 3 | // import { useState } from "react"; 4 | 5 | export default function Template({ children }: { children: React.ReactNode }) { 6 | // const [state, setState] = useState(0); 7 | return ( 8 |
9 | {/*

Template {state}

*/} 10 | {/* */} 11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /02-app-router/src/components/core/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { useRef, ReactNode, MouseEventHandler } from "react"; 4 | 5 | export default function Modal({ children }: { children: ReactNode }) { 6 | const overlay = useRef(null); 7 | const router = useRouter(); 8 | 9 | const close: MouseEventHandler = (e) => { 10 | if (e.target === overlay.current) { 11 | router.back(); 12 | } 13 | }; 14 | 15 | return ( 16 |
21 |
22 | {children} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /02-app-router/src/lib/firebase/init.ts: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | // TODO: Add SDKs for Firebase products that you want to use 4 | // https://firebase.google.com/docs/web/setup#available-libraries 5 | 6 | // Your web app's Firebase configuration 7 | const firebaseConfig = { 8 | apiKey: process.env.FIREBASE_API_KEY, 9 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 10 | projectId: process.env.FIREBASE_PROJECT_ID, 11 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET, 12 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, 13 | appId:process.env.FIREBASE_APP_ID, 14 | }; 15 | 16 | // Initialize Firebase 17 | const app = initializeApp(firebaseConfig); 18 | 19 | export default app; 20 | -------------------------------------------------------------------------------- /02-app-router/src/lib/firebase/service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addDoc, 3 | collection, 4 | doc, 5 | getDoc, 6 | getDocs, 7 | getFirestore, 8 | query, 9 | updateDoc, 10 | where, 11 | } from "firebase/firestore"; 12 | import app from "./init"; 13 | import bcrypt from "bcrypt"; 14 | 15 | const firestore = getFirestore(app); 16 | 17 | export async function retrieveData(CollectionName: string) { 18 | const snapshot = await getDocs(collection(firestore, CollectionName)); 19 | const data = snapshot.docs.map((doc) => ({ 20 | id: doc.id, 21 | ...doc.data(), 22 | })); 23 | 24 | return data; 25 | } 26 | 27 | export async function retrieveDataById(CollectionName: string, id: string) { 28 | const snapshot = await getDoc(doc(firestore, CollectionName, id)); 29 | const data = snapshot.data(); 30 | 31 | return data; 32 | } 33 | 34 | export async function register(data: { 35 | fullname: string; 36 | email: string; 37 | password: string; 38 | role?: string; 39 | }) { 40 | const q = query( 41 | collection(firestore, "users"), 42 | where("email", "==", data.email) 43 | ); 44 | const snapshot = await getDocs(q); 45 | const users = snapshot.docs.map((doc) => ({ 46 | id: doc.id, 47 | ...doc.data(), 48 | })); 49 | if (users.length > 0) { 50 | return { status: false, statusCode: 400, message: "Email already exist" }; 51 | } else { 52 | data.role = "member"; 53 | data.password = await bcrypt.hash(data.password, 10); 54 | 55 | try { 56 | await addDoc(collection(firestore, "users"), data); 57 | return { status: true, statusCode: 200, message: "Register success" }; 58 | } catch (error) { 59 | return { status: false, statusCode: 400, message: "Register failed" }; 60 | } 61 | } 62 | } 63 | 64 | export async function login(data: { email: string }) { 65 | const q = query( 66 | collection(firestore, "users"), 67 | where("email", "==", data.email) 68 | ); 69 | const snapshot = await getDocs(q); 70 | const user = snapshot.docs.map((doc) => ({ 71 | id: doc.id, 72 | ...doc.data(), 73 | })); 74 | 75 | if (user) { 76 | return user[0]; 77 | } else { 78 | return null; 79 | } 80 | } 81 | 82 | export async function loginWithGoogle(data: any, callback: any) { 83 | const q = query( 84 | collection(firestore, "users"), 85 | where("email", "==", data.email) 86 | ); 87 | const snapshot = await getDocs(q); 88 | const user: any = snapshot.docs.map((doc) => ({ 89 | id: doc.id, 90 | ...doc.data(), 91 | })); 92 | 93 | if (user.length > 0) { 94 | data.role = user[0].role; 95 | await updateDoc(doc(firestore, "users", user[0].id), data).then(() => { 96 | callback({ status: true, data: data }); 97 | }); 98 | } else { 99 | data.role = "member"; 100 | await addDoc(collection(firestore, "users"), data).then(() => { 101 | callback({ status: true, data: data }); 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /02-app-router/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | import withAuth from "./middlewares/withAuth"; 4 | 5 | export function mainMiddleware(request: NextRequest) { 6 | const res = NextResponse.next(); 7 | return res; 8 | } 9 | 10 | export default withAuth(mainMiddleware, [ 11 | "/dashboard", 12 | "/profile", 13 | "/login", 14 | "/register", 15 | ]); 16 | -------------------------------------------------------------------------------- /02-app-router/src/middlewares/withAuth.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from "next-auth/jwt"; 2 | import { 3 | NextFetchEvent, 4 | NextMiddleware, 5 | NextRequest, 6 | NextResponse, 7 | } from "next/server"; 8 | 9 | const onlyAdminPage = ["/dashboard"]; 10 | const authPage = ["/login", "/register"]; 11 | 12 | export default function withAuth( 13 | middleware: NextMiddleware, 14 | requireAuth: string[] = [] 15 | ) { 16 | return async (req: NextRequest, next: NextFetchEvent) => { 17 | const pathname = req.nextUrl.pathname; 18 | 19 | if (requireAuth.includes(pathname)) { 20 | const token = await getToken({ 21 | req, 22 | secret: process.env.NEXTAUTH_SECRET, 23 | }); 24 | if (!token && !authPage.includes(pathname)) { 25 | const url = new URL("/login", req.url); 26 | url.searchParams.set("callbackUrl", encodeURI(req.url)); 27 | return NextResponse.redirect(url); 28 | } 29 | 30 | if (token) { 31 | if (authPage.includes(pathname)) { 32 | return NextResponse.redirect(new URL("/", req.url)); 33 | } 34 | if (token.role !== "admin" && onlyAdminPage.includes(pathname)) { 35 | return NextResponse.redirect(new URL("/", req.url)); 36 | } 37 | } 38 | } 39 | 40 | return middleware(req, next); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /02-app-router/src/services/products/index.tsx: -------------------------------------------------------------------------------- 1 | export const getData = async (url: string) => { 2 | // const res = await fetch("https://fakestoreapi.com/products", { 3 | // cache: "no-store", 4 | // }); 5 | const res = await fetch(url, { 6 | cache: "no-store", 7 | next: { 8 | tags: ["products"], 9 | // revalidate: 30, 10 | }, 11 | }); 12 | 13 | if (!res.ok) { 14 | throw new Error("Failed to fetch data"); 15 | } 16 | 17 | return res.json(); 18 | }; 19 | -------------------------------------------------------------------------------- /02-app-router/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 | -------------------------------------------------------------------------------- /02-app-router/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 | --------------------------------------------------------------------------------