├── .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 |
4 |
8 |
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 |
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 |
29 | 30 |
31 | 32 |
33 |
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 |
77 |
82 |

{EDITMODE ? "Update Your Ticket" : "Create New Ticket"}

83 | 84 | 92 | 93 |