├── requests
├── ping.http
└── tasks.http
├── src
├── app
│ ├── tasks
│ │ ├── [id]
│ │ │ └── page.jsx
│ │ └── new
│ │ │ └── page.jsx
│ ├── api
│ │ ├── ping
│ │ │ └── route.js
│ │ └── tasks
│ │ │ ├── route.js
│ │ │ └── [id]
│ │ │ └── route.js
│ ├── not-found.jsx
│ ├── layout.jsx
│ └── page.jsx
├── styles
│ └── globals.css
├── components
│ ├── Navbar.jsx
│ └── TaskCard.jsx
├── utils
│ └── mongoose.js
└── models
│ ├── Task.js
│ └── User.js
├── public
├── favicon.ico
├── vercel.svg
└── react.svg
├── postcss.config.js
├── next.config.js
├── jsconfig.json
├── tailwind.config.js
├── .vscode
└── settings.json
├── README.md
├── .gitignore
└── package.json
/requests/ping.http:
--------------------------------------------------------------------------------
1 | GET http://localhost:3000/api/ping
--------------------------------------------------------------------------------
/src/app/tasks/[id]/page.jsx:
--------------------------------------------------------------------------------
1 | import NewPage from "../new/page";
2 | export default NewPage;
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FaztWeb/nextjs-mongodb-crud/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | };
4 |
5 | module.exports = nextConfig;
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | @apply bg-gray-900;
7 | color: white;
8 | }
--------------------------------------------------------------------------------
/src/app/api/ping/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export function GET() {
4 | return NextResponse.json({ hello: "world" });
5 | }
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "paths": {
5 | "@/*": [
6 | "*"
7 | ]
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.git": true,
4 | "**/.svn": true,
5 | "**/.hg": true,
6 | "**/CVS": true,
7 | "**/.DS_Store": true,
8 | "**/Thumbs.db": true,
9 | "**/node_modules": true,
10 | }
11 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nextjs & Mongodb CRUD
2 |
3 | A web aplication CRUD using Nodejs y Mongodb (with mongoose)
4 |
5 | ### Installation
6 |
7 | ```
8 | git clone https://github.com/FaztWeb/nextjs-mongodb-crud
9 | cd next-mongodb-crud
10 | npm i
11 | npm run dev
12 | ```
13 |
14 | its necessary to have a mongodb env:
15 |
16 | ```
17 | MONGODB_URI=mongodb://localhost:27017/nextcrud
18 | ```
19 |
--------------------------------------------------------------------------------
/src/app/not-found.jsx:
--------------------------------------------------------------------------------
1 | function NotFound() {
2 | return (
3 |
4 | 404
5 |
6 | Page not found :(
7 |
8 |
9 | );
10 | }
11 |
12 | export default NotFound;
13 |
--------------------------------------------------------------------------------
/src/app/layout.jsx:
--------------------------------------------------------------------------------
1 | import { Navbar } from "components/Navbar";
2 | import "../styles/globals.css";
3 |
4 | export const metadata = {
5 | title: "NextMongo",
6 | description: "NextMongo is a simple app to manage tasks.",
7 | }
8 |
9 | function RootLayout({ children }) {
10 | return (
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 | }
19 |
20 | export default RootLayout;
21 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export const Navbar = () => {
4 | return (
5 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/.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
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
--------------------------------------------------------------------------------
/requests/tasks.http:
--------------------------------------------------------------------------------
1 | @api = http://localhost:3000/api/tasks
2 |
3 | ### get tasks
4 | {{api}}
5 |
6 | ### create task
7 | POST {{api}}
8 | Content-Type: application/json
9 |
10 | {
11 | "title": "second task 2",
12 | "description": "second desc"
13 | }
14 |
15 | ### some validation
16 | POST {{api}}
17 |
18 | ### get single task
19 | GET {{api}}/64c347322fb76f285655e882
20 |
21 | ### Update a single task
22 | PUT {{api}}/64c347322fb76f285655e882
23 | Content-Type: application/json
24 |
25 | {
26 | "title": "I have to create a next app"
27 | }
28 |
29 | ### delete a single task
30 | DELETE {{api}}/64c347322fb76f285655e882
--------------------------------------------------------------------------------
/src/utils/mongoose.js:
--------------------------------------------------------------------------------
1 | import { connect, connection } from "mongoose";
2 |
3 | const conn = {
4 | isConnected: false,
5 | };
6 |
7 | export async function dbConnect() {
8 | if (conn.isConnected) {
9 | return;
10 | }
11 |
12 | const db = await connect(
13 | process.env.MONGODB_URI || "mongodb://localhost:27017/nextjs"
14 | );
15 | // console.log(db.connection.db.databaseName);
16 | conn.isConnected = db.connections[0].readyState;
17 | }
18 |
19 | connection.on("connected", () => console.log("Mongodb connected to db"));
20 |
21 | connection.on("error", (err) => console.error("Mongodb Errro:", err.message));
22 |
--------------------------------------------------------------------------------
/src/app/page.jsx:
--------------------------------------------------------------------------------
1 | import { dbConnect } from "@/utils/mongoose";
2 | import TaskCard from "@/components/TaskCard";
3 | import Task from "@/models/Task";
4 |
5 | export const dynamic = "force-dynamic";
6 |
7 | export async function loadTasks() {
8 | await dbConnect();
9 | const tasks = await Task.find();
10 | return tasks;
11 | }
12 |
13 | export default async function HomePage() {
14 | const tasks = await loadTasks();
15 |
16 | return (
17 |
18 | {tasks.map((task) => (
19 |
20 | ))}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/TaskCard.jsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export function TaskCard({ task }) {
4 | return (
5 |
6 |
7 |
{task.title}
8 |
{task.description}
9 |
10 | Created at:
11 | {new Date(task.createdAt).toLocaleDateString()}
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | export default TaskCard;
19 |
--------------------------------------------------------------------------------
/src/app/api/tasks/route.js:
--------------------------------------------------------------------------------
1 | import Task from "@/models/Task";
2 | import { dbConnect } from "@/utils/mongoose";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function GET() {
6 | await dbConnect();
7 | const tasks = await Task.find();
8 | return NextResponse.json(tasks);
9 | }
10 |
11 | export async function POST(request) {
12 | try {
13 | const body = await request.json();
14 | const newTask = new Task(body);
15 | const savedTask = await newTask.save();
16 | return NextResponse.json(savedTask);
17 | } catch (error) {
18 | return NextResponse.json(error.message, {
19 | status: 400,
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/models/Task.js:
--------------------------------------------------------------------------------
1 | import { Schema, model, models } from "mongoose";
2 |
3 | const TaskSchema = new Schema(
4 | {
5 | title: {
6 | type: String,
7 | required: [true, "The Task title is required "],
8 | unique: true,
9 | trim: true,
10 | maxlength: [40, "title cannot be grater than 40 characters"],
11 | },
12 | description: {
13 | type: String,
14 | required: true,
15 | trim: true,
16 | maxlength: [200, "title cannot be grater than 200 characters"],
17 | },
18 | },
19 | {
20 | timestamps: true,
21 | versionKey: false,
22 | }
23 | );
24 |
25 | export default models.Task || model("Task", TaskSchema);
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-mongodb-tasks",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "axios": "^1.4.0",
12 | "bcrypt": "^5.1.0",
13 | "cookie": "^0.5.0",
14 | "formik": "^2.4.2",
15 | "jose": "^4.14.4",
16 | "jsonwebtoken": "^9.0.1",
17 | "mongoose": "^7.4.1",
18 | "morgan": "^1.10.0",
19 | "next": "13.4.12",
20 | "react": "18.2.0",
21 | "react-dom": "18.2.0",
22 | "semantic-ui-css": "^2.5.0",
23 | "semantic-ui-react": "^2.1.4"
24 | },
25 | "devDependencies": {
26 | "@types/react": "18.2.17",
27 | "autoprefixer": "^10.4.14",
28 | "eslint-config-next": "^13.4.12",
29 | "postcss": "^8.4.27",
30 | "tailwindcss": "^3.3.3"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/models/User.js:
--------------------------------------------------------------------------------
1 | import { Schema, model, models } from "mongoose";
2 | import bcrypt from "bcrypt";
3 | import jwt from "jsonwebtoken";
4 |
5 | const userSchema = new Schema(
6 | {
7 | name: String,
8 | lastname: String,
9 | email: String,
10 | password: String,
11 | },
12 | {
13 | timestamps: true,
14 | }
15 | );
16 |
17 | userSchema.methods.encryptPassword = async function () {
18 | const salt = await bcrypt.genSalt(10);
19 | this.password = await bcrypt.hash(this.password, salt);
20 | };
21 |
22 | userSchema.statics.comparePassword = async function (password, hash) {
23 | return await bcrypt.compare(password, hash);
24 | };
25 |
26 | userSchema.methods.generateToken = function () {
27 | return jwt.sign({ id: this._id }, process.env.JWT_SECRET);
28 | };
29 |
30 | export default models.User || model("User", userSchema);
31 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/api/tasks/[id]/route.js:
--------------------------------------------------------------------------------
1 | import Task from "@/models/Task";
2 | import { dbConnect } from "@/utils/mongoose";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function GET(request, { params }) {
6 | dbConnect();
7 | try {
8 | const taskFound = await Task.findById(params.id);
9 |
10 | if (!taskFound)
11 | return NextResponse.json(
12 | {
13 | message: "Task not found",
14 | },
15 | {
16 | status: 404,
17 | }
18 | );
19 |
20 | return NextResponse.json(taskFound);
21 | } catch (error) {
22 | return NextResponse.json(error.message, {
23 | status: 400,
24 | });
25 | }
26 | }
27 |
28 | export async function PUT(request, { params }) {
29 | const body = await request.json();
30 | dbConnect();
31 |
32 | try {
33 | const taskUpdated = await Task.findByIdAndUpdate(params.id, body, {
34 | new: true,
35 | });
36 |
37 | if (!taskUpdated)
38 | return NextResponse.json(
39 | {
40 | message: "Task not found",
41 | },
42 | {
43 | status: 404,
44 | }
45 | );
46 |
47 | return NextResponse.json(taskUpdated);
48 | } catch (error) {
49 | return NextResponse.json(error.message, {
50 | status: 400,
51 | });
52 | }
53 | }
54 |
55 | export async function DELETE(request, { params }) {
56 | dbConnect();
57 |
58 | try {
59 | const taskDeleted = await Task.findByIdAndDelete(params.id);
60 |
61 | if (!taskDeleted)
62 | return NextResponse.json(
63 | {
64 | message: "Task not found",
65 | },
66 | {
67 | status: 404,
68 | }
69 | );
70 |
71 | return NextResponse.json(taskDeleted);
72 | } catch (error) {
73 | return NextResponse.json(error.message, {
74 | status: 400,
75 | });
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/public/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/tasks/new/page.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState, useEffect } from "react";
3 | import { useRouter, useParams } from "next/navigation";
4 |
5 | const NewTask = () => {
6 | const [newTask, setNewTask] = useState({
7 | title: "",
8 | description: "",
9 | });
10 | const params = useParams();
11 | const router = useRouter();
12 |
13 | const [isSubmitting, setIsSubmitting] = useState(false);
14 | const [errors, setErrors] = useState({});
15 |
16 | const getTask = async () => {
17 | const res = await fetch(`/api/tasks/${params.id}`);
18 | const data = await res.json();
19 | setNewTask({ title: data.title, description: data.description });
20 | };
21 |
22 | useEffect(() => {
23 | if (params.id) {
24 | getTask();
25 | }
26 | }, []);
27 |
28 | const handleSubmit = async (e) => {
29 | e.preventDefault();
30 | let errs = validate();
31 |
32 | if (Object.keys(errs).length) return setErrors(errs);
33 |
34 | setIsSubmitting(true);
35 |
36 | if (params.id) {
37 | await updateTask();
38 | } else {
39 | await createTask();
40 | }
41 |
42 | router.push("/");
43 | };
44 |
45 | const handleChange = (e) =>
46 | setNewTask({ ...newTask, [e.target.name]: e.target.value });
47 |
48 | const validate = () => {
49 | let errors = {};
50 |
51 | if (!newTask.title) {
52 | errors.title = "Title is required";
53 | }
54 | if (!newTask.description) {
55 | errors.description = "Description is required";
56 | }
57 |
58 | return errors;
59 | };
60 |
61 | const createTask = async () => {
62 | try {
63 | await fetch("/api/tasks", {
64 | method: "POST",
65 | headers: {
66 | "Content-Type": "application/json",
67 | },
68 | body: JSON.stringify(newTask),
69 | });
70 | router.push("/");
71 | router.refresh();
72 | } catch (error) {
73 | console.error(error);
74 | }
75 | };
76 |
77 | const handleDelete = async () => {
78 | if (window.confirm("Are you sure you want to delete this task?")) {
79 | try {
80 | const res = await fetch(`/api/tasks/${params.id}`, {
81 | method: "DELETE",
82 | });
83 | router.push("/");
84 | router.refresh();
85 | } catch (error) {
86 | console.error(error);
87 | }
88 | }
89 | };
90 |
91 | const updateTask = async () => {
92 | try {
93 | await fetch(`/api/tasks/${params.id}`, {
94 | method: "PUT",
95 | headers: {
96 | "Content-Type": "application/json",
97 | },
98 | body: JSON.stringify(newTask),
99 | });
100 | router.push("/");
101 | router.refresh();
102 | } catch (error) {
103 | console.error(error);
104 | }
105 | };
106 |
107 | return (
108 |
109 |
146 |
147 | );
148 | };
149 |
150 | export default NewTask;
151 |
--------------------------------------------------------------------------------