├── .eslintrc.json
├── .gitignore
├── README.md
├── jsconfig.json
├── next.config.js
├── package-lock.json
├── package.json
├── public
├── about.png
├── brands.png
├── contact.png
├── favicon.ico
├── hero.gif
├── menu.png
└── noavatar.png
└── src
├── app
├── (auth)
│ ├── login
│ │ ├── login.module.css
│ │ └── page.jsx
│ └── register
│ │ ├── page.jsx
│ │ └── register.module.css
├── about
│ ├── about.module.css
│ └── page.jsx
├── admin
│ ├── admin.module.css
│ └── page.jsx
├── api
│ ├── auth
│ │ └── [...nextauth]
│ │ │ └── route.js
│ └── blog
│ │ ├── [slug]
│ │ └── route.js
│ │ └── route.js
├── blog
│ ├── [slug]
│ │ ├── page.jsx
│ │ └── singlePost.module.css
│ ├── blog.module.css
│ └── page.jsx
├── contact
│ ├── contact.module.css
│ └── page.jsx
├── error.jsx
├── globals.css
├── home.module.css
├── layout.js
├── loading.jsx
├── navigationtest
│ └── page.jsx
├── not-found.jsx
├── page.jsx
└── serveractiontest
│ └── page.jsx
├── components
├── adminPostForm
│ ├── adminPostForm.jsx
│ └── adminPostForm.module.css
├── adminPosts
│ ├── adminPosts.jsx
│ └── adminPosts.module.css
├── adminUserForm
│ ├── adminUserForm.jsx
│ └── adminUserForm.module.css
├── adminUsers
│ ├── adminUsers.jsx
│ └── adminUsers.module.css
├── clientSideProviderTest.jsx
├── footer
│ ├── Footer.jsx
│ └── footer.module.css
├── hydrationTest.jsx
├── loginForm
│ ├── loginForm.jsx
│ └── loginForm.module.css
├── navbar
│ ├── Navbar.jsx
│ ├── links
│ │ ├── Links.jsx
│ │ ├── links.module.css
│ │ └── navLink
│ │ │ ├── navLink.jsx
│ │ │ └── navLink.module.css
│ └── navbar.module.css
├── postCard
│ ├── postCard.jsx
│ └── postCard.module.css
├── postUser
│ ├── postUser.jsx
│ └── postUser.module.css
└── registerForm
│ ├── registerForm.jsx
│ └── registerForm.module.css
├── lib
├── action.js
├── auth.config.js
├── auth.js
├── data.js
├── models.js
└── utils.js
└── middleware.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "next/core-web-vitals"
5 | ],
6 | "rules": {
7 | "react/prop-types": "off",
8 | "no-unused-vars": "warn"
9 | }
10 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | First, install the dependencies:
4 |
5 | ```bash
6 | npm install
7 | # or
8 | yarn install
9 | # or
10 | pnpm install
11 | # or
12 | bun install
13 | ```
14 |
15 |
16 | run the development server:
17 |
18 | ```bash
19 | npm run dev
20 | # or
21 | yarn dev
22 | # or
23 | pnpm dev
24 | # or
25 | bun dev
26 | ```
27 |
28 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
29 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images:{
4 | remotePatterns:[
5 | {
6 | protocol: 'https',
7 | hostname: "images.pexels.com"
8 | }
9 | ]
10 | }
11 | }
12 |
13 | module.exports = nextConfig
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next14starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "bcryptjs": "^2.4.3",
13 | "install": "^0.13.0",
14 | "mongoose": "^8.0.0",
15 | "next": "14.0.4",
16 | "next-auth": "^5.0.0-beta.3",
17 | "npm": "^10.2.5",
18 | "react": "^18",
19 | "react-dom": "^18"
20 | },
21 | "devDependencies": {
22 | "eslint": "^8",
23 | "eslint-config-next": "14.0.4"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/public/about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/safak/next14-tutorial/d8383e400365dae7e1eef218a7d776ed7897a0bb/public/about.png
--------------------------------------------------------------------------------
/public/brands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/safak/next14-tutorial/d8383e400365dae7e1eef218a7d776ed7897a0bb/public/brands.png
--------------------------------------------------------------------------------
/public/contact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/safak/next14-tutorial/d8383e400365dae7e1eef218a7d776ed7897a0bb/public/contact.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/safak/next14-tutorial/d8383e400365dae7e1eef218a7d776ed7897a0bb/public/favicon.ico
--------------------------------------------------------------------------------
/public/hero.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/safak/next14-tutorial/d8383e400365dae7e1eef218a7d776ed7897a0bb/public/hero.gif
--------------------------------------------------------------------------------
/public/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/safak/next14-tutorial/d8383e400365dae7e1eef218a7d776ed7897a0bb/public/menu.png
--------------------------------------------------------------------------------
/public/noavatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/safak/next14-tutorial/d8383e400365dae7e1eef218a7d776ed7897a0bb/public/noavatar.png
--------------------------------------------------------------------------------
/src/app/(auth)/login/login.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
6 |
7 | .wrapper{
8 | width: 500px;
9 | background-color: var(--bgSoft);
10 | padding: 50px;
11 | display: flex;
12 | flex-direction: column;
13 | text-align: center;
14 | gap: 30px;
15 | border-radius: 5px;
16 | }
17 |
18 | .github{
19 | width: 100%;
20 | padding: 20px;
21 | cursor: pointer;
22 | background-color: #fff;
23 | color: #000;
24 | font-weight: bold;
25 | border: none;
26 | border-radius: 5px;
27 | }
--------------------------------------------------------------------------------
/src/app/(auth)/login/page.jsx:
--------------------------------------------------------------------------------
1 | import LoginForm from "@/components/loginForm/loginForm";
2 | import { handleGithubLogin } from "@/lib/action";
3 | import styles from "./login.module.css";
4 |
5 | const LoginPage = () => {
6 |
7 | return (
8 |
16 | );
17 | };
18 |
19 | export default LoginPage;
20 |
--------------------------------------------------------------------------------
/src/app/(auth)/register/page.jsx:
--------------------------------------------------------------------------------
1 | import styles from "./register.module.css";
2 | import RegisterForm from "@/components/registerForm/registerForm";
3 |
4 | const RegisterPage = () => {
5 | return (
6 |
11 | );
12 | };
13 |
14 | export default RegisterPage;
15 |
--------------------------------------------------------------------------------
/src/app/(auth)/register/register.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
6 |
7 | .wrapper{
8 | width: 500px;
9 | background-color: var(--bgSoft);
10 | padding: 50px;
11 | display: flex;
12 | flex-direction: column;
13 | text-align: center;
14 | gap: 30px;
15 | border-radius: 5px;
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/app/about/about.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | gap: 100px;
4 | }
5 |
6 | .textContainer {
7 | flex: 1;
8 | display: flex;
9 | flex-direction: column;
10 | gap: 50px;
11 | }
12 |
13 | .subtitle{
14 | color:var(--btn)
15 | }
16 |
17 | .title{
18 | font-size: 54px;
19 | }
20 |
21 | .desc{
22 | font-size: 20px;
23 | font-weight: 300;
24 | }
25 |
26 | .boxes{
27 | display: flex;
28 | align-items: center;
29 | justify-content: space-between;
30 | }
31 |
32 | .box{
33 | display: flex;
34 | flex-direction: column;
35 | gap: 10px;
36 | }
37 |
38 | .box h1{
39 | color: var(--btn);
40 | }
41 |
42 | .imgContainer {
43 | flex: 1;
44 | position: relative;
45 | }
46 |
47 | .img{
48 | object-fit: contain;
49 | }
50 |
51 | @media (max-width: 768px) {
52 | .container {
53 | flex-direction: column;
54 | text-align: center;
55 | }
56 |
57 | .boxes{
58 | flex-direction: column;
59 | gap: 50px;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/app/about/page.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import styles from "./about.module.css";
3 |
4 | export const metadata = {
5 | title: "About Page",
6 | description: "About description",
7 | };
8 |
9 |
10 | const AboutPage = () => {
11 |
12 | // console.log("lets check where it works")
13 | return (
14 |
15 |
16 |
About Agency
17 |
18 | We create digital ideas that are bigger, bolder, braver and better.
19 |
20 |
21 | We create digital ideas that are bigger, bolder, braver and better. We
22 | believe in good ideas flexibility and precission We’re world’s Our
23 | Special Team best consulting & finance solution provider. Wide range
24 | of web and software development services.
25 |
26 |
27 |
28 |
10 K+
29 |
Year of experience
30 |
31 |
32 |
10 K+
33 |
Year of experience
34 |
35 |
36 |
10 K+
37 |
Year of experience
38 |
39 |
40 |
41 |
42 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default AboutPage;
54 |
--------------------------------------------------------------------------------
/src/app/admin/admin.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-top: 50px;
3 | display: flex;
4 | flex-direction: column;
5 | gap: 100px;
6 | }
7 |
8 | .row {
9 | display: flex;
10 | gap: 100px;
11 | }
12 |
13 | .row h1{
14 | font-size: 24px;
15 | font-weight: 300;
16 | }
17 |
18 | .col {
19 | flex: 1;
20 | }
21 |
22 | .col img {
23 | object-fit: cover;
24 | border-radius: 50%;
25 | }
26 |
27 | .line{
28 | margin: 100px;
29 | color: var(--bgSoft)
30 | }
31 |
32 | @media (max-width: 768px) {
33 | .row {
34 | flex-direction: column;
35 | }
36 | }
--------------------------------------------------------------------------------
/src/app/admin/page.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import styles from "./admin.module.css";
3 | import AdminPosts from "@/components/adminPosts/adminPosts";
4 | import AdminPostForm from "@/components/adminPostForm/adminPostForm";
5 | import AdminUsers from "@/components/adminUsers/adminUsers";
6 | import AdminUserForm from "@/components/adminUserForm/adminUserForm";
7 | import { auth } from "@/lib/auth";
8 |
9 | const AdminPage = async () => {
10 |
11 | const session = await auth();
12 |
13 | return (
14 |
15 |
16 |
17 | Loading...
}>
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 | Loading...
}>
28 |
29 |
30 |
31 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default AdminPage;
40 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.js:
--------------------------------------------------------------------------------
1 | export { GET, POST } from "@/lib/auth"
--------------------------------------------------------------------------------
/src/app/api/blog/[slug]/route.js:
--------------------------------------------------------------------------------
1 | import { Post } from "@/lib/models";
2 | import { connectToDb } from "@/lib/utils";
3 | import { NextResponse } from "next/server";
4 |
5 | export const GET = async (request, { params }) => {
6 | const { slug } = params;
7 |
8 | try {
9 | connectToDb();
10 |
11 | const post = await Post.findOne({ slug });
12 | return NextResponse.json(post);
13 | } catch (err) {
14 | console.log(err);
15 | throw new Error("Failed to fetch post!");
16 | }
17 | };
18 |
19 | export const DELETE = async (request, { params }) => {
20 | const { slug } = params;
21 |
22 | try {
23 | connectToDb();
24 |
25 | await Post.deleteOne({ slug });
26 | return NextResponse.json("Post deleted");
27 | } catch (err) {
28 | console.log(err);
29 | throw new Error("Failed to delete post!");
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/src/app/api/blog/route.js:
--------------------------------------------------------------------------------
1 | import { Post } from "@/lib/models";
2 | import { connectToDb } from "@/lib/utils";
3 | import { NextResponse } from "next/server";
4 |
5 | export const GET = async (request) => {
6 | try {
7 | connectToDb();
8 |
9 | const posts = await Post.find();
10 | return NextResponse.json(posts);
11 | } catch (err) {
12 | console.log(err);
13 | throw new Error("Failed to fetch posts!");
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/app/blog/[slug]/page.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import styles from "./singlePost.module.css";
3 | import PostUser from "@/components/postUser/postUser";
4 | import { Suspense } from "react";
5 | import { getPost } from "@/lib/data";
6 |
7 | // FETCH DATA WITH AN API
8 | const getData = async (slug) => {
9 | const res = await fetch(`http://localhost:3000/api/blog/${slug}`);
10 |
11 | if (!res.ok) {
12 | throw new Error("Something went wrong");
13 | }
14 |
15 | return res.json();
16 | };
17 |
18 | export const generateMetadata = async ({ params }) => {
19 | const { slug } = params;
20 |
21 | const post = await getPost(slug);
22 |
23 | return {
24 | title: post.title,
25 | description: post.desc,
26 | };
27 | };
28 |
29 | const SinglePostPage = async ({ params }) => {
30 | const { slug } = params;
31 |
32 | // FETCH DATA WITH AN API
33 | const post = await getData(slug);
34 |
35 | // FETCH DATA WITHOUT AN API
36 | // const post = await getPost(slug);
37 |
38 | return (
39 |
40 | {post.img && (
41 |
42 |
43 |
44 | )}
45 |
46 |
{post.title}
47 |
48 | {post && (
49 | Loading...
}>
50 |
51 |
52 | )}
53 |
54 | Published
55 |
56 | {post.createdAt.toString().slice(4, 16)}
57 |
58 |
59 |
60 |
{post.desc}
61 |
62 |
63 | );
64 | };
65 |
66 | export default SinglePostPage;
67 |
--------------------------------------------------------------------------------
/src/app/blog/[slug]/singlePost.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | gap: 100px;
4 | }
5 |
6 | .imgContainer {
7 | flex: 1;
8 | position: relative;
9 | height: calc(100vh - 200px);
10 | }
11 |
12 | .img{
13 | object-fit: cover;
14 | }
15 |
16 | .textContainer {
17 | flex: 2;
18 | display: flex;
19 | flex-direction: column;
20 | gap: 50px;
21 | }
22 |
23 | .title{
24 | font-size: 64px;
25 | }
26 |
27 | .detail{
28 | display: flex;
29 | gap: 20px;
30 | }
31 |
32 | .detailText{
33 | display: flex;
34 | flex-direction: column;
35 | gap: 10px;
36 | }
37 |
38 | .detailTitle{
39 | color: gray;
40 | font-weight: bold;
41 | }
42 |
43 | .detailValue{
44 | font-weight: 500;
45 | }
46 |
47 | .content{
48 | font-size: 20px;
49 | }
50 |
51 | @media (max-width:768px){
52 | .imgContainer{
53 | display: none;
54 | }
55 | }
--------------------------------------------------------------------------------
/src/app/blog/blog.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-wrap: wrap;
4 | gap: 20px;
5 | }
6 |
7 | .post {
8 | width: 30%;
9 | }
10 |
11 | @media screen and (max-width: 1280px) {
12 | .post {
13 | width: 45%;
14 | }
15 | }
16 |
17 | @media screen and (max-width: 768px) {
18 | .post {
19 | width: 100%;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/blog/page.jsx:
--------------------------------------------------------------------------------
1 | import PostCard from "@/components/postCard/postCard";
2 | import styles from "./blog.module.css";
3 | import { getPosts } from "@/lib/data";
4 |
5 | // FETCH DATA WITH AN API
6 | const getData = async () => {
7 | const res = await fetch("http://localhost:3000/api/blog", {next:{revalidate:3600}});
8 |
9 | if (!res.ok) {
10 | throw new Error("Something went wrong");
11 | }
12 |
13 | return res.json();
14 | };
15 |
16 | const BlogPage = async () => {
17 |
18 | // FETCH DATA WITH AN API
19 | const posts = await getData();
20 |
21 | // FETCH DATA WITHOUT AN API
22 | // const posts = await getPosts();
23 |
24 | return (
25 |
26 | {posts.map((post) => (
27 |
30 | ))}
31 |
32 | );
33 | };
34 |
35 | export default BlogPage;
36 |
--------------------------------------------------------------------------------
/src/app/contact/contact.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | gap: 100px;
5 | }
6 |
7 | .imgContainer {
8 | flex: 1;
9 | height: 500px;
10 | position: relative;
11 | }
12 |
13 | .img {
14 | object-fit: contain;
15 | }
16 |
17 | .formContainer {
18 | flex: 1;
19 | }
20 |
21 | .form {
22 | display: flex;
23 | flex-direction: column;
24 | gap: 20px;
25 | }
26 |
27 | .form input,
28 | .form textarea {
29 | padding: 20px;
30 | border-radius: 5px;
31 | border: none;
32 | outline: none;
33 | background-color: var(--bgSoft);
34 | color: var(--text);
35 | }
36 |
37 | .form button {
38 | padding: 20px;
39 | background-color: var(--btn);
40 | color: var(--text);
41 | font-weight: bold;
42 | border: none;
43 | border-radius: 5px;
44 | cursor: pointer;
45 | }
46 |
47 | @media (max-width: 768px) {
48 | .container {
49 | flex-direction: column;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/contact/page.jsx:
--------------------------------------------------------------------------------
1 | // "use client";
2 | import Image from "next/image";
3 | import styles from "./contact.module.css";
4 | // import dynamic from "next/dynamic";
5 | // import HydrationTest from "@/components/hydrationTest";
6 |
7 | // const HydrationTestNoSSR = dynamic(()=>import("@/components/hydrationTest"), {ssr: false})
8 |
9 | export const metadata = {
10 | title: "Contact Page",
11 | description: "Contact description",
12 | };
13 |
14 | const ContactPage = () => {
15 | // const a = Math.random();
16 |
17 | // console.log(a);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | {/*
*/}
26 | {/*
{a}
*/}
27 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default ContactPage;
46 |
--------------------------------------------------------------------------------
/src/app/error.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | const Error = () => {
4 | return (
5 | Error
6 | )
7 | }
8 |
9 | export default Error
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: #0d0c22;
3 | --bgSoft: #2d2b42;
4 | --text: white;
5 | --textSoft: #e5e5e5;
6 | --btn: #3673fd;
7 | }
8 |
9 | * {
10 | margin: 0;
11 | padding: 0;
12 | box-sizing: border-box;
13 | }
14 |
15 | body {
16 | background-color: var(--bg);
17 | color: var(--text);
18 |
19 | }
20 |
21 | a {
22 | text-decoration: none;
23 | color: inherit;
24 | }
25 |
26 | .container {
27 | width: 1536px;
28 | margin: auto;
29 | padding-left: 50px;
30 | padding-right: 50px;
31 | min-height: 100vh;
32 | display: flex;
33 | flex-direction: column;
34 | justify-content: space-between;
35 | }
36 |
37 | @media (max-width: 1536px) {
38 | .container {
39 | width: 1366px;
40 | }
41 | }
42 |
43 | @media (max-width: 1366px) {
44 | .container {
45 | width: 1280px;
46 | }
47 | }
48 |
49 | @media (max-width: 1280px) {
50 | .container {
51 | width: 1024px;
52 | padding-left: 20px;
53 | padding-right: 20px;
54 | }
55 | }
56 |
57 | @media (max-width: 1024px) {
58 | .container {
59 | width: 768px;
60 | }
61 | }
62 |
63 | @media (max-width: 768px) {
64 | .container {
65 | width: 640px;
66 | }
67 | }
68 |
69 | @media (max-width: 640px) {
70 | .container {
71 | width: 475px;
72 | }
73 | }
74 | @media (max-width: 475px) {
75 | .container {
76 | width: 380px;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | gap: 100px;
4 | }
5 |
6 | .textContainer {
7 | flex: 1;
8 | display: flex;
9 | flex-direction: column;
10 | gap: 50px;
11 | }
12 |
13 | .title{
14 | font-size: 96px;
15 | }
16 |
17 | .desc{
18 | font-size: 20px;
19 | }
20 |
21 | .buttons{
22 | display: flex;
23 | gap: 20px;
24 | }
25 |
26 | .button{
27 | padding: 20px;
28 | min-width: 120px;
29 | cursor: pointer;
30 | border: none;
31 | border-radius: 5px;
32 | }
33 |
34 | .button:first-child{
35 | background-color: var(--btn);
36 | color: var(--text);
37 | }
38 |
39 | .brands{
40 | width: 500px;
41 | height: 50px;
42 | position: relative;
43 | filter: grayscale(1);
44 | }
45 |
46 | .imgContainer {
47 | flex: 1;
48 | position: relative;
49 | }
50 |
51 | @media (max-width: 1024px) {
52 | .container {
53 | flex-direction: column;
54 | text-align: center;
55 | }
56 |
57 | .buttons{
58 | justify-content: center;
59 | }
60 | }
61 |
62 | @media (max-width: 768px) {
63 | .title {
64 | font-size: 64px;
65 | }
66 |
67 | .brands{
68 | width: 100%;
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/src/app/layout.js:
--------------------------------------------------------------------------------
1 | import { Inter } from "next/font/google";
2 | import "./globals.css";
3 | import Navbar from "@/components/navbar/Navbar";
4 | import Footer from "@/components/footer/Footer";
5 | import ClientSideProviderTest from "@/components/clientSideProviderTest";
6 |
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | export const metadata = {
10 | title: {
11 | default:"Next.js 14 Homepage",
12 | template:"%s | Next.js 14"
13 | },
14 | description: "Next.js starter app description",
15 | };
16 |
17 | export default function RootLayout({ children }) {
18 | return (
19 |
20 |
21 | {/* */}
22 |
23 |
24 | {children}
25 |
26 |
27 | {/* */}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/loading.jsx:
--------------------------------------------------------------------------------
1 | const Loading = () => {
2 | return (
3 | Loading
4 | )
5 | }
6 |
7 | export default Loading
--------------------------------------------------------------------------------
/src/app/navigationtest/page.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import Link from "next/link"
3 | import { usePathname, useRouter, useSearchParams } from "next/navigation"
4 |
5 | const NavigationTestPage = () => {
6 |
7 | // CLIENT SIDE NAVIGATION
8 | const router = useRouter()
9 | const pathname = usePathname()
10 | const searchParams = useSearchParams()
11 |
12 | const q = searchParams.get("q")
13 |
14 | console.log(q)
15 |
16 | const handleClick = ()=>{
17 | console.log("clicked")
18 | router.forward()
19 | }
20 |
21 | return (
22 |
23 | Click here
24 | Write and Redirect
25 |
26 | )
27 | }
28 |
29 | export default NavigationTestPage
--------------------------------------------------------------------------------
/src/app/not-found.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | const NotFound = () => {
4 | return (
5 |
6 |
Not Found
7 |
Sorry, the page you are looking for does not exist.
8 |
Return Home
9 |
10 | )
11 | }
12 |
13 | export default NotFound
--------------------------------------------------------------------------------
/src/app/page.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import styles from "./home.module.css";
3 |
4 | const Home = () => {
5 | return (
6 |
7 |
8 |
Creative Thoughts Agency.
9 |
10 | Lorem, ipsum dolor sit amet consectetur adipisicing elit. Vero
11 | blanditiis adipisci minima reiciendis a autem assumenda dolore.
12 |
13 |
14 | Learn More
15 | Contact
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default Home;
29 |
--------------------------------------------------------------------------------
/src/app/serveractiontest/page.jsx:
--------------------------------------------------------------------------------
1 | import { addPost, deletePost } from "@/lib/action"
2 |
3 | const ServerActionTestPage = () => {
4 |
5 | // const actionInComponent = async ()=>{
6 | // "use server"
7 | // console.log("it works!")
8 | // }
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 | Create
18 |
19 |
20 |
21 |
22 | Delete
23 |
24 |
25 | )
26 | }
27 |
28 | export default ServerActionTestPage
--------------------------------------------------------------------------------
/src/components/adminPostForm/adminPostForm.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { addPost } from "@/lib/action";
4 | import styles from "./adminPostForm.module.css";
5 | import { useFormState } from "react-dom";
6 |
7 | const AdminPostForm = ({userId}) => {
8 | const [state, formAction] = useFormState(addPost, undefined);
9 |
10 | return (
11 |
12 | Add New Post
13 |
14 |
15 |
16 |
17 |
18 | Add
19 | {state?.error}
20 |
21 | );
22 | };
23 |
24 | export default AdminPostForm;
25 |
--------------------------------------------------------------------------------
/src/components/adminPostForm/adminPostForm.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 20px;
5 | }
6 |
7 | .container input,
8 | .container textarea,
9 | .container select {
10 | padding: 20px;
11 | background-color: var(--bgSoft);
12 | border: none;
13 | border-radius: 5px;
14 | color: var(--textColor);
15 | }
16 |
17 | .container button {
18 | padding: 20px;
19 | cursor: pointer;
20 | background-color: var(--btn);
21 | border: none;
22 | border-radius: 5px;
23 | color: var(--textColor);
24 | font-weight: bold;
25 | }
--------------------------------------------------------------------------------
/src/components/adminPosts/adminPosts.jsx:
--------------------------------------------------------------------------------
1 | import { getPosts } from "@/lib/data";
2 | import styles from "./adminPosts.module.css";
3 | import Image from "next/image";
4 | import { deletePost } from "@/lib/action";
5 |
6 | const AdminPosts = async () => {
7 | const posts = await getPosts();
8 |
9 | return (
10 |
11 |
Posts
12 | {posts.map((post) => (
13 |
14 |
15 |
21 | {post.title}
22 |
23 |
24 |
25 | Delete
26 |
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default AdminPosts;
34 |
--------------------------------------------------------------------------------
/src/components/adminPosts/adminPosts.module.css:
--------------------------------------------------------------------------------
1 | .post {
2 | margin: 20px 0px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | gap: 20px;
7 | }
8 |
9 | .detail {
10 | display: flex;
11 | align-items: center;
12 | gap: 20px;
13 | }
14 |
15 | .postButton{
16 | padding: 5px 10px;
17 | background-color: rgba(220, 20, 60, 0.593);
18 | color: var(--textColor);
19 | border-radius: 5px;
20 | border: none;
21 | cursor: pointer;
22 | }
--------------------------------------------------------------------------------
/src/components/adminUserForm/adminUserForm.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { addUser } from "@/lib/action";
4 | import styles from "./adminUserForm.module.css";
5 | import { useFormState } from "react-dom";
6 |
7 | const AdminUserForm = () => {
8 | const [state, formAction] = useFormState(addUser, undefined);
9 |
10 | return (
11 |
12 | Add New User
13 |
14 |
15 |
16 |
17 |
18 | Is Admin?
19 | No
20 | Yes
21 |
22 | Add
23 | {state?.error}
24 |
25 | );
26 | };
27 |
28 | export default AdminUserForm;
29 |
--------------------------------------------------------------------------------
/src/components/adminUserForm/adminUserForm.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 20px;
5 | }
6 |
7 | .container input,
8 | .container textarea,
9 | .container select {
10 | padding: 20px;
11 | background-color: var(--bgSoft);
12 | border: none;
13 | border-radius: 5px;
14 | color: var(--textColor);
15 | }
16 |
17 | .container button {
18 | padding: 20px;
19 | cursor: pointer;
20 | background-color: var(--btn);
21 | border: none;
22 | border-radius: 5px;
23 | color: var(--textColor);
24 | font-weight: bold;
25 | }
--------------------------------------------------------------------------------
/src/components/adminUsers/adminUsers.jsx:
--------------------------------------------------------------------------------
1 | import { getUsers } from "@/lib/data";
2 | import styles from "./adminUsers.module.css";
3 | import Image from "next/image";
4 | import { deleteUser } from "@/lib/action";
5 |
6 | const AdminUsers = async () => {
7 | const users = await getUsers();
8 |
9 | return (
10 |
11 |
Users
12 | {users.map((user) => (
13 |
14 |
15 |
21 | {user.username}
22 |
23 |
24 |
25 | Delete
26 |
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default AdminUsers;
34 |
--------------------------------------------------------------------------------
/src/components/adminUsers/adminUsers.module.css:
--------------------------------------------------------------------------------
1 | .user {
2 | margin: 20px 0px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | gap: 20px;
7 | }
8 |
9 | .detail {
10 | display: flex;
11 | align-items: center;
12 | gap: 20px;
13 | }
14 |
15 | .userButton{
16 | padding: 5px 10px;
17 | background-color: rgba(220, 20, 60, 0.593);
18 | color: var(--textColor);
19 | border-radius: 5px;
20 | border: none;
21 | cursor: pointer;
22 | }
--------------------------------------------------------------------------------
/src/components/clientSideProviderTest.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | const ClientSideProviderTest = ({children}) => {
4 | return (
5 |
6 | {children}
7 |
8 | )
9 | }
10 |
11 | export default ClientSideProviderTest
--------------------------------------------------------------------------------
/src/components/footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import styles from "./footer.module.css";
2 |
3 | const Footer = () => {
4 | return (
5 |
6 |
lamadev
7 |
8 | Lama creative thoughts agency © All rights reserved.
9 |
10 |
11 | );
12 | };
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/src/components/footer/footer.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | height: 100px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | color: gray;
7 | }
8 |
9 | .logo{
10 | font-weight: bold;
11 | }
12 |
13 | .text{
14 | font-size: 12px;
15 | }
16 |
17 | @media (max-width:768px){
18 | .container{
19 | flex-direction: column;
20 | align-items: center;
21 | justify-content: space-around;
22 | }
23 | }
--------------------------------------------------------------------------------
/src/components/hydrationTest.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | const HydrationTest = () => {
4 |
5 | const a = Math.random();
6 |
7 | console.log(a);
8 |
9 | return (
10 | {a}
11 | )
12 | }
13 |
14 | export default HydrationTest
--------------------------------------------------------------------------------
/src/components/loginForm/loginForm.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { login } from "@/lib/action";
4 | import styles from "./loginForm.module.css";
5 | import { useFormState } from "react-dom";
6 | import Link from "next/link";
7 |
8 | const LoginForm = () => {
9 | const [state, formAction] = useFormState(login, undefined);
10 |
11 | return (
12 |
13 |
14 |
15 | Login
16 | {state?.error}
17 |
18 | {"Don't have an account?"} Register
19 |
20 |
21 | );
22 | };
23 |
24 | export default LoginForm;
25 |
--------------------------------------------------------------------------------
/src/components/loginForm/loginForm.module.css:
--------------------------------------------------------------------------------
1 | .form{
2 | display: flex;
3 | flex-direction: column;
4 | text-align: center;
5 | gap: 30px;
6 | }
7 |
8 | .form input{
9 | padding: 20px;
10 | background-color: var(--bg);
11 | color: var(--textColor);
12 | border: none;
13 | border-radius: 5px;
14 | }
15 |
16 | .form button{
17 | padding: 20px;
18 | cursor: pointer;
19 | background-color: var(--btn);
20 | color: var(--textColor);
21 | font-weight: bold;
22 | border: none;
23 | border-radius: 5px;
24 | }
--------------------------------------------------------------------------------
/src/components/navbar/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import Links from "./links/Links"
3 | import styles from "./navbar.module.css"
4 | import { auth } from "@/lib/auth";
5 |
6 | const Navbar = async () => {
7 |
8 | const session = await auth();
9 |
10 | return (
11 |
12 |
Logo
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default Navbar
--------------------------------------------------------------------------------
/src/components/navbar/links/Links.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import styles from "./links.module.css";
5 | import NavLink from "./navLink/navLink";
6 | import Image from "next/image";
7 | import { handleLogout } from "@/lib/action";
8 |
9 | const links = [
10 | {
11 | title: "Homepage",
12 | path: "/",
13 | },
14 | {
15 | title: "About",
16 | path: "/about",
17 | },
18 | {
19 | title: "Contact",
20 | path: "/contact",
21 | },
22 | {
23 | title: "Blog",
24 | path: "/blog",
25 | },
26 | ];
27 |
28 | const Links = ({session}) => {
29 | const [open, setOpen] = useState(false);
30 |
31 | // TEMPORARY
32 | // const session = true;
33 | // const isAdmin = true;
34 |
35 | return (
36 |
37 |
38 | {links.map((link) => (
39 |
40 | ))}
41 | {session?.user ? (
42 | <>
43 | {session.user?.isAdmin && }
44 |
45 | Logout
46 |
47 | >
48 | ) : (
49 |
50 | )}
51 |
52 |
setOpen((prev) => !prev)}
59 | />
60 | {open && (
61 |
62 | {links.map((link) => (
63 |
64 | ))}
65 |
66 | )}
67 |
68 | );
69 | };
70 |
71 | export default Links;
72 |
--------------------------------------------------------------------------------
/src/components/navbar/links/links.module.css:
--------------------------------------------------------------------------------
1 | .links {
2 | display: flex;
3 | align-items: center;
4 | gap: 10px;
5 | }
6 |
7 | .logout {
8 | padding: 10px;
9 | cursor: pointer;
10 | font-weight: bold;
11 | }
12 |
13 | .menuButton,
14 | .mobileLinks {
15 | display: none;
16 | }
17 |
18 | @media (max-width: 768px) {
19 | .links {
20 | display: none;
21 | }
22 |
23 | .menuButton {
24 | display: block;
25 | cursor: pointer;
26 | }
27 |
28 | .mobileLinks {
29 | position: absolute;
30 | top: 100px;
31 | right: 0;
32 | width: 50%;
33 | height: calc(100vh - 100px);
34 | background-color: var(--bg);
35 | display: flex;
36 | flex-direction: column;
37 | align-items: center;
38 | justify-content: center;
39 | gap: 10px;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/navbar/links/navLink/navLink.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import styles from "./navLink.module.css";
5 | import { usePathname } from "next/navigation";
6 |
7 | const NavLink = ({ item }) => {
8 | const pathName = usePathname();
9 |
10 | return (
11 |
17 | {item.title}
18 |
19 | );
20 | };
21 |
22 | export default NavLink;
23 |
--------------------------------------------------------------------------------
/src/components/navbar/links/navLink/navLink.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | min-width: 100px;
3 | padding: 10px;
4 | border-radius: 20px;
5 | font-weight: 500;
6 | text-align: center;
7 | }
8 |
9 | .active{
10 | background-color: var(--text);
11 | color: var(--bg);
12 | }
--------------------------------------------------------------------------------
/src/components/navbar/navbar.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | height: 100px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | }
7 |
8 | .logo{
9 | font-size: 30px;
10 | font-weight: bold;
11 | }
--------------------------------------------------------------------------------
/src/components/postCard/postCard.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import styles from "./postCard.module.css"
3 | import Link from "next/link"
4 |
5 | const PostCard = ({post}) => {
6 | return (
7 |
8 |
9 | {post.img &&
10 |
11 |
}
12 |
{post.createdAt?.toString().slice(4, 16)}
13 |
14 |
15 |
{post.title}
16 |
{post.body}
17 |
READ MORE
18 |
19 |
20 | )
21 | }
22 |
23 | export default PostCard
--------------------------------------------------------------------------------
/src/components/postCard/postCard.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | display: flex;
3 | flex-direction: column;
4 | gap: 20px;
5 | margin-bottom: 20px;
6 | }
7 |
8 | .top{
9 | display: flex;
10 | }
11 |
12 | .imgContainer{
13 | width: 90%;
14 | height: 400px;
15 | position: relative;
16 | }
17 |
18 | .img{
19 | object-fit: cover;
20 | }
21 |
22 | .date{
23 | font-size: 12px;
24 | transform: rotate(270deg);
25 | margin: auto;
26 | }
27 |
28 | .title{
29 | width: 90%;
30 | font-size: 24px;
31 | margin-bottom: 20px;
32 | }
33 |
34 | .desc{
35 | width: 90%;
36 | margin-bottom: 20px;
37 | font-weight: 300;
38 | color: gray;
39 | }
40 |
41 | .link{
42 | text-decoration: underline;
43 | }
--------------------------------------------------------------------------------
/src/components/postUser/postUser.jsx:
--------------------------------------------------------------------------------
1 | import { getUser } from "@/lib/data";
2 | import styles from "./postUser.module.css";
3 | import Image from "next/image";
4 |
5 | // FETCH DATA WITH AN API
6 | // const getData = async (userId) => {
7 | // const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}` ,{cache:"no-store"});
8 |
9 | // if (!res.ok) {
10 | // throw new Error("Something went wrong");
11 | // }
12 |
13 | // return res.json();
14 | // };
15 |
16 | const PostUser = async ({ userId }) => {
17 | // FETCH DATA WITH AN API
18 | // const user = await getData(userId);
19 |
20 | // FETCH DATA WITHOUT AN API
21 | const user = await getUser(userId);
22 |
23 | return (
24 |
25 |
32 |
33 | Author
34 | {user.username}
35 |
36 |
37 | );
38 | };
39 |
40 | export default PostUser;
41 |
--------------------------------------------------------------------------------
/src/components/postUser/postUser.module.css:
--------------------------------------------------------------------------------
1 | .container{
2 | display: flex;
3 | align-items: center;
4 | gap: 20px;
5 | }
6 |
7 | .avatar{
8 | object-fit: cover;
9 | border-radius: 50%;
10 | }
11 |
12 | .texts{
13 | display: flex;
14 | flex-direction: column;
15 | gap: 10px;
16 | }
17 |
18 | .title{
19 | color: gray;
20 | font-weight: bold;
21 | }
22 |
23 | .username{
24 | font-weight: 500;
25 | }
--------------------------------------------------------------------------------
/src/components/registerForm/registerForm.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { register } from "@/lib/action";
4 | import styles from "./registerForm.module.css";
5 | import { useFormState } from "react-dom";
6 | import { useEffect } from "react";
7 | import { useRouter } from "next/navigation";
8 | import Link from "next/link";
9 |
10 | const RegisterForm = () => {
11 | const [state, formAction] = useFormState(register, undefined);
12 |
13 | const router = useRouter();
14 |
15 | useEffect(() => {
16 | state?.success && router.push("/login");
17 | }, [state?.success, router]);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
29 | Register
30 | {state?.error}
31 |
32 | Have an account? Login
33 |
34 |
35 | );
36 | };
37 |
38 | export default RegisterForm;
39 |
--------------------------------------------------------------------------------
/src/components/registerForm/registerForm.module.css:
--------------------------------------------------------------------------------
1 | .form{
2 | display: flex;
3 | flex-direction: column;
4 | text-align: center;
5 | gap: 30px;
6 | }
7 |
8 | .form input{
9 | padding: 20px;
10 | background-color: var(--bg);
11 | color: var(--textColor);
12 | border: none;
13 | border-radius: 5px;
14 | }
15 |
16 | .form button{
17 | padding: 20px;
18 | cursor: pointer;
19 | background-color: var(--btn);
20 | color: var(--textColor);
21 | font-weight: bold;
22 | border: none;
23 | border-radius: 5px;
24 | }
--------------------------------------------------------------------------------
/src/lib/action.js:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { Post, User } from "./models";
5 | import { connectToDb } from "./utils";
6 | import { signIn, signOut } from "./auth";
7 | import bcrypt from "bcryptjs";
8 |
9 | export const addPost = async (prevState,formData) => {
10 | // const title = formData.get("title");
11 | // const desc = formData.get("desc");
12 | // const slug = formData.get("slug");
13 |
14 | const { title, desc, slug, userId } = Object.fromEntries(formData);
15 |
16 | try {
17 | connectToDb();
18 | const newPost = new Post({
19 | title,
20 | desc,
21 | slug,
22 | userId,
23 | });
24 |
25 | await newPost.save();
26 | console.log("saved to db");
27 | revalidatePath("/blog");
28 | revalidatePath("/admin");
29 | } catch (err) {
30 | console.log(err);
31 | return { error: "Something went wrong!" };
32 | }
33 | };
34 |
35 | export const deletePost = async (formData) => {
36 | const { id } = Object.fromEntries(formData);
37 |
38 | try {
39 | connectToDb();
40 |
41 | await Post.findByIdAndDelete(id);
42 | console.log("deleted from db");
43 | revalidatePath("/blog");
44 | revalidatePath("/admin");
45 | } catch (err) {
46 | console.log(err);
47 | return { error: "Something went wrong!" };
48 | }
49 | };
50 |
51 | export const addUser = async (prevState,formData) => {
52 | const { username, email, password, img } = Object.fromEntries(formData);
53 |
54 | try {
55 | connectToDb();
56 | const newUser = new User({
57 | username,
58 | email,
59 | password,
60 | img,
61 | });
62 |
63 | await newUser.save();
64 | console.log("saved to db");
65 | revalidatePath("/admin");
66 | } catch (err) {
67 | console.log(err);
68 | return { error: "Something went wrong!" };
69 | }
70 | };
71 |
72 | export const deleteUser = async (formData) => {
73 | const { id } = Object.fromEntries(formData);
74 |
75 | try {
76 | connectToDb();
77 |
78 | await Post.deleteMany({ userId: id });
79 | await User.findByIdAndDelete(id);
80 | console.log("deleted from db");
81 | revalidatePath("/admin");
82 | } catch (err) {
83 | console.log(err);
84 | return { error: "Something went wrong!" };
85 | }
86 | };
87 |
88 | export const handleGithubLogin = async () => {
89 | "use server";
90 | await signIn("github");
91 | };
92 |
93 | export const handleLogout = async () => {
94 | "use server";
95 | await signOut();
96 | };
97 |
98 | export const register = async (previousState, formData) => {
99 | const { username, email, password, img, passwordRepeat } =
100 | Object.fromEntries(formData);
101 |
102 | if (password !== passwordRepeat) {
103 | return { error: "Passwords do not match" };
104 | }
105 |
106 | try {
107 | connectToDb();
108 |
109 | const user = await User.findOne({ username });
110 |
111 | if (user) {
112 | return { error: "Username already exists" };
113 | }
114 |
115 | const salt = await bcrypt.genSalt(10);
116 | const hashedPassword = await bcrypt.hash(password, salt);
117 |
118 | const newUser = new User({
119 | username,
120 | email,
121 | password: hashedPassword,
122 | img,
123 | });
124 |
125 | await newUser.save();
126 | console.log("saved to db");
127 |
128 | return { success: true };
129 | } catch (err) {
130 | console.log(err);
131 | return { error: "Something went wrong!" };
132 | }
133 | };
134 |
135 | export const login = async (prevState, formData) => {
136 | const { username, password } = Object.fromEntries(formData);
137 |
138 | try {
139 | await signIn("credentials", { username, password });
140 | } catch (err) {
141 | console.log(err);
142 |
143 | if (err.message.includes("CredentialsSignin")) {
144 | return { error: "Invalid username or password" };
145 | }
146 | throw err;
147 | }
148 | };
149 |
--------------------------------------------------------------------------------
/src/lib/auth.config.js:
--------------------------------------------------------------------------------
1 | export const authConfig = {
2 | pages: {
3 | signIn: "/login",
4 | },
5 | providers: [],
6 | callbacks: {
7 | // FOR MORE DETAIL ABOUT CALLBACK FUNCTIONS CHECK https://next-auth.js.org/configuration/callbacks
8 | async jwt({ token, user }) {
9 | if (user) {
10 | token.id = user.id;
11 | token.isAdmin = user.isAdmin;
12 | }
13 | return token;
14 | },
15 | async session({ session, token }) {
16 | if (token) {
17 | session.user.id = token.id;
18 | session.user.isAdmin = token.isAdmin;
19 | }
20 | return session;
21 | },
22 | authorized({ auth, request }) {
23 | const user = auth?.user;
24 | const isOnAdminPanel = request.nextUrl?.pathname.startsWith("/admin");
25 | const isOnBlogPage = request.nextUrl?.pathname.startsWith("/blog");
26 | const isOnLoginPage = request.nextUrl?.pathname.startsWith("/login");
27 |
28 | // ONLY ADMIN CAN REACH THE ADMIN DASHBOARD
29 |
30 | if (isOnAdminPanel && !user?.isAdmin) {
31 | return false;
32 | }
33 |
34 | // ONLY AUTHENTICATED USERS CAN REACH THE BLOG PAGE
35 |
36 | if (isOnBlogPage && !user) {
37 | return false;
38 | }
39 |
40 | // ONLY UNAUTHENTICATED USERS CAN REACH THE LOGIN PAGE
41 |
42 | if (isOnLoginPage && user) {
43 | return Response.redirect(new URL("/", request.nextUrl));
44 | }
45 |
46 | return true
47 | },
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/src/lib/auth.js:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import GitHub from "next-auth/providers/github";
3 | import CredentialsProvider from "next-auth/providers/credentials";
4 | import { connectToDb } from "./utils";
5 | import { User } from "./models";
6 | import bcrypt from "bcryptjs";
7 | import { authConfig } from "./auth.config";
8 |
9 | const login = async (credentials) => {
10 | try {
11 | connectToDb();
12 | const user = await User.findOne({ username: credentials.username });
13 |
14 | if (!user) throw new Error("Wrong credentials!");
15 |
16 | const isPasswordCorrect = await bcrypt.compare(
17 | credentials.password,
18 | user.password
19 | );
20 |
21 | if (!isPasswordCorrect) throw new Error("Wrong credentials!");
22 |
23 | return user;
24 | } catch (err) {
25 | console.log(err);
26 | throw new Error("Failed to login!");
27 | }
28 | };
29 |
30 | export const {
31 | handlers: { GET, POST },
32 | auth,
33 | signIn,
34 | signOut,
35 | } = NextAuth({
36 | ...authConfig,
37 | providers: [
38 | GitHub({
39 | clientId: process.env.GITHUB_ID,
40 | clientSecret: process.env.GITHUB_SECRET,
41 | }),
42 | CredentialsProvider({
43 | async authorize(credentials) {
44 | try {
45 | const user = await login(credentials);
46 | return user;
47 | } catch (err) {
48 | return null;
49 | }
50 | },
51 | }),
52 | ],
53 | callbacks: {
54 | async signIn({ user, account, profile }) {
55 | if (account.provider === "github") {
56 | connectToDb();
57 | try {
58 | const user = await User.findOne({ email: profile.email });
59 |
60 | if (!user) {
61 | const newUser = new User({
62 | username: profile.login,
63 | email: profile.email,
64 | image: profile.avatar_url,
65 | });
66 |
67 | await newUser.save();
68 | }
69 | } catch (err) {
70 | console.log(err);
71 | return false;
72 | }
73 | }
74 | return true;
75 | },
76 | ...authConfig.callbacks,
77 | },
78 | });
--------------------------------------------------------------------------------
/src/lib/data.js:
--------------------------------------------------------------------------------
1 | import { Post, User } from "./models";
2 | import { connectToDb } from "./utils";
3 | import { unstable_noStore as noStore } from "next/cache";
4 |
5 | // TEMPORARY DATA
6 | // const users = [
7 | // { id: 1, name: "John" },
8 | // { id: 2, name: "Jane" },
9 | // ];
10 |
11 | // const posts = [
12 | // { id: 1, title: "Post 1", body: "......", userId: 1 },
13 | // { id: 2, title: "Post 2", body: "......", userId: 1 },
14 | // { id: 3, title: "Post 3", body: "......", userId: 2 },
15 | // { id: 4, title: "Post 4", body: "......", userId: 2 },
16 | // ];
17 |
18 | export const getPosts = async () => {
19 | try {
20 | connectToDb();
21 | const posts = await Post.find();
22 | return posts;
23 | } catch (err) {
24 | console.log(err);
25 | throw new Error("Failed to fetch posts!");
26 | }
27 | };
28 |
29 | export const getPost = async (slug) => {
30 | try {
31 | connectToDb();
32 | const post = await Post.findOne({ slug });
33 | return post;
34 | } catch (err) {
35 | console.log(err);
36 | throw new Error("Failed to fetch post!");
37 | }
38 | };
39 |
40 | export const getUser = async (id) => {
41 | noStore();
42 | try {
43 | connectToDb();
44 | const user = await User.findById(id);
45 | return user;
46 | } catch (err) {
47 | console.log(err);
48 | throw new Error("Failed to fetch user!");
49 | }
50 | };
51 |
52 | export const getUsers = async () => {
53 | try {
54 | connectToDb();
55 | const users = await User.find();
56 | return users;
57 | } catch (err) {
58 | console.log(err);
59 | throw new Error("Failed to fetch users!");
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/src/lib/models.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = new mongoose.Schema(
4 | {
5 | username: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | min: 3,
10 | max: 20,
11 | },
12 | email: {
13 | type: String,
14 | required: true,
15 | unique: true,
16 | max: 50,
17 | },
18 | password: {
19 | type: String,
20 | },
21 | img: {
22 | type: String,
23 | },
24 | isAdmin: {
25 | type: Boolean,
26 | default: false,
27 | },
28 | },
29 | { timestamps: true }
30 | );
31 |
32 | const postSchema = new mongoose.Schema(
33 | {
34 | title: {
35 | type: String,
36 | required: true,
37 | },
38 | desc: {
39 | type: String,
40 | required: true,
41 | },
42 | img: {
43 | type: String,
44 | },
45 | userId: {
46 | type: String,
47 | required: true,
48 | },
49 | slug: {
50 | type: String,
51 | required: true,
52 | unique: true,
53 | },
54 | },
55 | { timestamps: true }
56 | );
57 |
58 | export const User = mongoose.models?.User || mongoose.model("User", userSchema);
59 | export const Post = mongoose.models?.Post || mongoose.model("Post", postSchema);
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 |
3 | const connection = {};
4 |
5 | export const connectToDb = async () => {
6 | try {
7 | if(connection.isConnected) {
8 | console.log("Using existing connection");
9 | return;
10 | }
11 | const db = await mongoose.connect(process.env.MONGO);
12 | connection.isConnected = db.connections[0].readyState;
13 | } catch (error) {
14 | console.log(error);
15 | throw new Error(error);
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/src/middleware.js:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { authConfig } from "./lib/auth.config";
3 |
4 | export default NextAuth(authConfig).auth;
5 |
6 | export const config = {
7 | matcher: ["/((?!api|static|.*\\..*|_next).*)"],
8 | };
9 |
10 | // FOR MORE INFORMATION CHECK: https://nextjs.org/docs/app/building-your-application/routing/middleware
--------------------------------------------------------------------------------