├── .eslintrc.json
├── app
├── favicon.ico
├── (components)
│ ├── ProgressDisplay.jsx
│ ├── DeleteBlock.jsx
│ ├── Nav.jsx
│ ├── StatusDisplay.jsx
│ ├── PriorityDisplay.jsx
│ ├── TicketCard.jsx
│ └── EditTicketForm.jsx
├── models
│ └── Ticket.js
├── api
│ └── Tickets
│ │ ├── route.js
│ │ └── [id]
│ │ └── route.js
├── layout.jsx
├── TicketPage
│ └── [id]
│ │ └── page.jsx
├── globals.css
└── page.jsx
├── SAMPLE.env.local
├── jsconfig.json
├── postcss.config.js
├── next.config.js
├── .gitignore
├── public
├── vercel.svg
└── next.svg
├── package.json
├── tailwind.config.js
└── README.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/babel","next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClarityCoders/Ticket-Tutorial-App/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/SAMPLE.env.local:
--------------------------------------------------------------------------------
1 | MONGODB_URI=mongodb+srv://yourUSER:yourPASSWORD@cluster0.ukunedo.mongodb.net/MondayClone
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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 | images: {
4 | domains: ["www.pinclipart.com"],
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/app/(components)/ProgressDisplay.jsx:
--------------------------------------------------------------------------------
1 | const ProgressDisplay = ({ progress }) => {
2 | return (
3 |
9 | );
10 | };
11 |
12 | export default ProgressDisplay;
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/models/Ticket.js:
--------------------------------------------------------------------------------
1 | import mongoose, { Schema } from "mongoose";
2 |
3 | mongoose.connect(process.env.MONGODB_URI);
4 | mongoose.Promise = global.Promise;
5 |
6 | const ticketSchema = new Schema(
7 | {
8 | title: String,
9 | description: String,
10 | category: String,
11 | priority: Number,
12 | progress: Number,
13 | status: String,
14 | active: Boolean,
15 | },
16 | {
17 | timestamps: true,
18 | }
19 | );
20 |
21 | const Ticket = mongoose.models.Ticket || mongoose.model("Ticket", ticketSchema);
22 |
23 | export default Ticket;
24 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monday-tutorial",
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 | "@fortawesome/free-solid-svg-icons": "^6.4.2",
13 | "@fortawesome/react-fontawesome": "^0.2.0",
14 | "autoprefixer": "10.4.14",
15 | "eslint": "8.46.0",
16 | "eslint-config-next": "13.4.13",
17 | "mongodb": "^5.7.0",
18 | "mongoose": "^7.4.3",
19 | "next": "13.4.13",
20 | "postcss": "8.4.27",
21 | "react": "18.2.0",
22 | "react-dom": "18.2.0",
23 | "tailwindcss": "3.3.3"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/(components)/DeleteBlock.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { faX } from "@fortawesome/free-solid-svg-icons";
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 | import { useRouter } from "next/navigation";
6 |
7 | const DeleteBlock = ({ id }) => {
8 | const router = useRouter();
9 |
10 | const deleteTicket = async () => {
11 | const res = await fetch(`http://localhost:3000/api/Tickets/${id}`, {
12 | method: "DELETE",
13 | });
14 | if (res.ok) {
15 | router.refresh();
16 | }
17 | };
18 |
19 | return (
20 |
25 | );
26 | };
27 |
28 | export default DeleteBlock;
29 |
--------------------------------------------------------------------------------
/app/(components)/Nav.jsx:
--------------------------------------------------------------------------------
1 | import { faHome, faTicket } from "@fortawesome/free-solid-svg-icons";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 | import Link from "next/link";
4 |
5 | const Nav = () => {
6 | return (
7 |
20 | );
21 | };
22 |
23 | export default Nav;
24 |
--------------------------------------------------------------------------------
/app/(components)/StatusDisplay.jsx:
--------------------------------------------------------------------------------
1 | const StatusDisplay = ({ status }) => {
2 | const getColor = (status) => {
3 | let color;
4 | switch (status.toLowerCase()) {
5 | case "done":
6 | color = "bg-green-200";
7 | return color;
8 |
9 | case "started":
10 | color = "bg-yellow-200";
11 | return color;
12 |
13 | case "not started":
14 | color = "bg-red-200";
15 | return color;
16 | default:
17 | color = "bg-slate-700";
18 | }
19 | return color;
20 | };
21 | return (
22 |
27 | {status}
28 |
29 | );
30 | };
31 |
32 | export default StatusDisplay;
33 |
--------------------------------------------------------------------------------
/app/api/Tickets/route.js:
--------------------------------------------------------------------------------
1 | import Ticket from "@/app/models/Ticket";
2 | import { NextResponse } from "next/server";
3 |
4 | export async function GET() {
5 | try {
6 | const tickets = await Ticket.find();
7 |
8 | return NextResponse.json({ tickets }, { status: 200 });
9 | } catch (err) {
10 | console.log(err);
11 | return NextResponse.json({ message: "Error", err }, { status: 500 });
12 | }
13 | }
14 |
15 | export async function POST(req) {
16 | try {
17 | const body = await req.json();
18 | const ticketData = body.formData;
19 |
20 | await Ticket.create(ticketData);
21 |
22 | return NextResponse.json({ message: "Ticket Created" }, { status: 201 });
23 | } catch (err) {
24 | console.log(err);
25 | return NextResponse.json({ message: "Error", err }, { status: 500 });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
12 | "gradient-conic":
13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
14 | },
15 | colors: {
16 | nav: "#18222f",
17 | page: "#2b3441",
18 | card: "#47566a",
19 | "card-hover": "#4f5e74",
20 | "default-text": "#f1f3f5",
21 | "blue-accent": "#0084d4",
22 | "blue-accent-hover": "#009fff",
23 | },
24 | },
25 | },
26 | plugins: [],
27 | };
28 |
--------------------------------------------------------------------------------
/app/layout.jsx:
--------------------------------------------------------------------------------
1 | import Nav from "./(components)/Nav";
2 | import "./globals.css";
3 | import { Inter } from "next/font/google";
4 |
5 | import { config } from "@fortawesome/fontawesome-svg-core";
6 | import "@fortawesome/fontawesome-svg-core/styles.css";
7 |
8 | config.autoAddCss = false;
9 |
10 | const inter = Inter({ subsets: ["latin"] });
11 |
12 | export const metadata = {
13 | title: "Ticket System",
14 | description: "Creating a functional ticketing system.",
15 | };
16 |
17 | export default function RootLayout({ children }) {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/TicketPage/[id]/page.jsx:
--------------------------------------------------------------------------------
1 | import EditTicketForm from "@/app/(components)/EditTicketForm";
2 |
3 | const getTicketById = async (id) => {
4 | try {
5 | const res = await fetch(`http://localhost:3000/api/Tickets/${id}`, {
6 | cache: "no-store",
7 | });
8 |
9 | if (!res.ok) {
10 | throw new Error("Failed to fetch topic");
11 | }
12 |
13 | return res.json();
14 | } catch (error) {
15 | console.log(error);
16 | }
17 | };
18 |
19 | let updateTicketData = {};
20 | const TicketPage = async ({ params }) => {
21 | const EDITMODE = params.id === "new" ? false : true;
22 |
23 | if (EDITMODE) {
24 | updateTicketData = await getTicketById(params.id);
25 | updateTicketData = updateTicketData.foundTicket;
26 | } else {
27 | updateTicketData = {
28 | _id: "new",
29 | };
30 | }
31 |
32 | return ;
33 | };
34 |
35 | export default TicketPage;
36 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body {
7 | margin: 0;
8 | padding: 0;
9 | font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
10 | "Lucida Sans", Arial, sans-serif;
11 | }
12 |
13 | @layer base {
14 | h1,
15 | h2,
16 | h3,
17 | h4,
18 | h5,
19 | h6 {
20 | @apply font-bold;
21 | }
22 |
23 | h1 {
24 | @apply text-4xl;
25 | margin: 0 0 5px 0;
26 | }
27 | h2 {
28 | @apply text-3xl;
29 | }
30 | h3 {
31 | @apply text-2xl;
32 | }
33 | h4 {
34 | @apply text-xl;
35 | }
36 | h5 {
37 | @apply text-lg;
38 | }
39 | p {
40 | @apply text-sm;
41 | }
42 | form {
43 | @apply rounded-xl p-4
44 | }
45 | label {
46 | @apply mt-4
47 | }
48 | input,
49 | select,
50 | textarea {
51 | @apply m-1 rounded bg-card p-1;
52 | }
53 | }
54 |
55 | @layer components {
56 | .icon {
57 | @apply text-default-text text-xl;
58 | }
59 | .btn {
60 | @apply hover:no-underline bg-blue-accent tracking-wider w-full text-center text-nav font-bold cursor-pointer uppercase px-4 py-2 rounded-md hover:bg-blue-accent-hover transition-colors block;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/(components)/PriorityDisplay.jsx:
--------------------------------------------------------------------------------
1 | import { faFire } from "@fortawesome/free-solid-svg-icons";
2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3 |
4 | const PriorityDisplay = ({ priority }) => {
5 | return (
6 |
7 | 0 ? " text-red-400" : " text-slate-400"
11 | }`}
12 | />
13 | 1 ? " text-red-400" : " text-slate-400"
17 | }`}
18 | />
19 | 2 ? " text-red-400" : " text-slate-400"
23 | }`}
24 | />
25 | 3 ? " text-red-400" : " text-slate-400"
29 | }`}
30 | />
31 | 4 ? " text-red-400" : " text-slate-400"}`}
34 | />
35 |
36 | );
37 | };
38 |
39 | export default PriorityDisplay;
40 |
--------------------------------------------------------------------------------
/app/api/Tickets/[id]/route.js:
--------------------------------------------------------------------------------
1 | import Ticket from "@/app/models/Ticket";
2 | import { NextResponse } from "next/server";
3 |
4 | export async function GET(request, { params }) {
5 | const { id } = params;
6 |
7 | const foundTicket = await Ticket.findOne({ _id: id });
8 | return NextResponse.json({ foundTicket }, { status: 200 });
9 | }
10 |
11 | export async function PUT(req, { params }) {
12 | try {
13 | const { id } = params;
14 |
15 | const body = await req.json();
16 | const ticketData = body.formData;
17 |
18 | const updateTicketData = await Ticket.findByIdAndUpdate(id, {
19 | ...ticketData,
20 | });
21 |
22 | return NextResponse.json({ message: "Ticket updated" }, { status: 200 });
23 | } catch (error) {
24 | console.log(error);
25 | return NextResponse.json({ message: "Error", error }, { status: 500 });
26 | }
27 | }
28 |
29 | export async function DELETE(req, { params }) {
30 | try {
31 | const { id } = params;
32 |
33 | await Ticket.findByIdAndDelete(id);
34 | return NextResponse.json({ message: "Ticket Deleted" }, { status: 200 });
35 | } catch (error) {
36 | console.log(error);
37 | return NextResponse.json({ message: "Error", error }, { status: 500 });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Updated Version Ticket Application using Typscript
2 | Want to take this project to the next level join our
3 | Udemy Course. Support the channel for only $13
4 | - New Features
5 | - Prisma
6 | - Typscript
7 | - Server Side Pagination
8 | - Future Updates
9 | - Server Actions
10 | - Deployment
11 | - Any new Next.js 14 features
12 |
13 | Clarity Coders YouTube
14 | # NextJS 13.4 Example App!
15 | - Simple CRUD app to showcase how to use 13.4 app router
16 | - Tech Stack
17 | - Tailwind CSS
18 | - fontawesome
19 | - MongoDB
20 | - Mongoose
21 |
22 | # Want to add authentication? Check out our free Udemy Course! Next Auth Tutorial Video
23 | - Free Using link below.
24 | - Covers rolling your own auth system and using providers like github and google.
25 | - An FREE! Updated version can be found on Udemy by clicking the link Free Udemy Course
26 |
27 | ## Contact!
28 | - YouTube Clarity Coders
29 | - Chat with me! Discord
30 |
--------------------------------------------------------------------------------
/app/page.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TicketCard from "./(components)/TicketCard";
3 |
4 | const getTickets = async () => {
5 | try {
6 | const res = await fetch("http://localhost:3000/api/Tickets", {
7 | cache: "no-store",
8 | });
9 |
10 | if (!res.ok) {
11 | throw new Error("Failed to fetch topics");
12 | }
13 |
14 | return res.json();
15 | } catch (error) {
16 | console.log("Error loading topics: ", error);
17 | }
18 | };
19 |
20 | const Dashboard = async () => {
21 | const data = await getTickets();
22 |
23 | // Make sure we have tickets needed for production build.
24 | if (!data?.tickets) {
25 | return No tickets.
;
26 | }
27 |
28 | const tickets = data.tickets;
29 |
30 | const uniqueCategories = [
31 | ...new Set(tickets?.map(({ category }) => category)),
32 | ];
33 |
34 | return (
35 |
36 |
37 | {tickets &&
38 | uniqueCategories?.map((uniqueCategory, categoryIndex) => (
39 |
40 |
{uniqueCategory}
41 |
42 | {tickets
43 | .filter((ticket) => ticket.category === uniqueCategory)
44 | .map((filteredTicket, _index) => (
45 |
50 | ))}
51 |
52 |
53 | ))}
54 |
55 |
56 | );
57 | };
58 |
59 | export default Dashboard;
60 |
--------------------------------------------------------------------------------
/app/(components)/TicketCard.jsx:
--------------------------------------------------------------------------------
1 | import StatusDisplay from "./StatusDisplay";
2 | import PriorityDisplay from "./PriorityDisplay";
3 | import DeleteBlock from "./DeleteBlock";
4 | import ProgressDisplay from "./ProgressDisplay";
5 | import Link from "next/link";
6 |
7 | const TicketCard = ({ ticket }) => {
8 | function formatTimestamp(timestamp) {
9 | const options = {
10 | year: "numeric",
11 | month: "2-digit",
12 | day: "2-digit",
13 | hour: "2-digit",
14 | minute: "2-digit",
15 | hour12: true,
16 | };
17 |
18 | const date = new Date(timestamp);
19 | const formattedDate = date.toLocaleString("en-US", options);
20 |
21 | return formattedDate;
22 | }
23 |
24 | const createdDateTime = formatTimestamp(ticket.createdAt);
25 |
26 | return (
27 |
28 |
34 |
35 |
{ticket.title}
36 |
37 |
{ticket.description}
38 |
39 |
40 |
41 |
42 |
{createdDateTime}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default TicketCard;
55 |
--------------------------------------------------------------------------------
/app/(components)/EditTicketForm.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter } from "next/navigation";
3 | import React, { useState } from "react";
4 |
5 | const EditTicketForm = ({ ticket }) => {
6 | const EDITMODE = ticket._id === "new" ? false : true;
7 | const router = useRouter();
8 | const startingTicketData = {
9 | title: "",
10 | description: "",
11 | priority: 1,
12 | progress: 0,
13 | status: "not started",
14 | category: "Hardware Problem",
15 | };
16 |
17 | if (EDITMODE) {
18 | startingTicketData["title"] = ticket.title;
19 | startingTicketData["description"] = ticket.description;
20 | startingTicketData["priority"] = ticket.priority;
21 | startingTicketData["progress"] = ticket.progress;
22 | startingTicketData["status"] = ticket.status;
23 | startingTicketData["category"] = ticket.category;
24 | }
25 |
26 | const [formData, setFormData] = useState(startingTicketData);
27 |
28 | const handleChange = (e) => {
29 | const value = e.target.value;
30 | const name = e.target.name;
31 |
32 | setFormData((preState) => ({
33 | ...preState,
34 | [name]: value,
35 | }));
36 | };
37 |
38 | const handleSubmit = async (e) => {
39 | e.preventDefault();
40 |
41 | if (EDITMODE) {
42 | const res = await fetch(`/api/Tickets/${ticket._id}`, {
43 | method: "PUT",
44 | headers: {
45 | "Content-type": "application/json",
46 | },
47 | body: JSON.stringify({ formData }),
48 | });
49 | if (!res.ok) {
50 | throw new Error("Failed to update ticket");
51 | }
52 | } else {
53 | const res = await fetch("/api/Tickets", {
54 | method: "POST",
55 | body: JSON.stringify({ formData }),
56 | //@ts-ignore
57 | "Content-Type": "application/json",
58 | });
59 | if (!res.ok) {
60 | throw new Error("Failed to create ticket");
61 | }
62 | }
63 |
64 | router.refresh();
65 | router.push("/");
66 | };
67 |
68 | const categories = [
69 | "Hardware Problem",
70 | "Software Problem",
71 | "Application Deveopment",
72 | "Project",
73 | ];
74 |
75 | return (
76 |
185 | );
186 | };
187 |
188 | export default EditTicketForm;
189 |
--------------------------------------------------------------------------------