├── .eslintrc.json ├── public ├── ava.png ├── favicon.ico └── vercel.svg ├── next.config.js ├── components ├── Layout │ └── Layout.js └── Sidebar │ └── Sidebar.js ├── .gitignore ├── package.json ├── pages ├── _app.js ├── api │ ├── invoices │ │ └── [invoiceId] │ │ │ └── index.js │ ├── add-new │ │ └── index.js │ └── edit │ │ └── [invoiceId] │ │ └── index.js ├── index.js ├── invoices │ └── [invoiceId] │ │ └── index.js ├── add-new │ └── index.js └── edit │ └── [invoiceId] │ └── index.js ├── README.md └── styles └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/ava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmuhib/Invoice-application/HEAD/public/ava.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmuhib/Invoice-application/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /components/Layout/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import Sidebar from "../Sidebar/Sidebar"; 3 | 4 | const Layout = (props) => { 5 | return ( 6 | 7 | 8 |
{props.children}
9 |
10 | ); 11 | }; 12 | 13 | export default Layout; 14 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invoice-app", 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 | "mongodb": "^4.8.1", 13 | "next": "12.2.3", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "react-toastify": "^9.0.7" 17 | }, 18 | "devDependencies": { 19 | "eslint": "8.20.0", 20 | "eslint-config-next": "12.2.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import Layout from "../components/Layout/Layout"; 3 | import { ToastContainer } from "react-toastify"; 4 | import "react-toastify/dist/ReactToastify.css"; 5 | 6 | function MyApp({ Component, pageProps }) { 7 | return ( 8 | 9 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default MyApp; 22 | -------------------------------------------------------------------------------- /components/Sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | const Sidebar = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 |

Muhib

11 |
12 |
13 | 14 |
15 | avatar 16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Sidebar; 23 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /pages/api/invoices/[invoiceId]/index.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from "mongodb"; 2 | 3 | async function handler(req, res) { 4 | const { invoiceId } = req.query; 5 | 6 | const client = await MongoClient.connect( 7 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 8 | { useNewUrlParser: true } 9 | ); 10 | 11 | const db = client.db(); 12 | const collection = db.collection("allInvoices"); 13 | 14 | if (req.method === "PUT") { 15 | await collection.updateOne( 16 | { _id: ObjectId(invoiceId) }, 17 | { 18 | $set: { 19 | status: "paid", 20 | }, 21 | } 22 | ); 23 | 24 | res.status(200).json({ message: "Invoice paid" }); 25 | client.close(); 26 | } 27 | 28 | // delete request 29 | if (req.method === "DELETE") { 30 | await collection.deleteOne({ _id: ObjectId(invoiceId) }); 31 | 32 | res.status(200).json({ message: "Invoice deleted successfully" }); 33 | client.close(); 34 | } 35 | } 36 | 37 | export default handler; 38 | -------------------------------------------------------------------------------- /pages/api/add-new/index.js: -------------------------------------------------------------------------------- 1 | import { MongoClient } from "mongodb"; 2 | 3 | async function handler(req, res) { 4 | const client = await MongoClient.connect( 5 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 6 | { useNewUrlParser: true } 7 | ); 8 | 9 | if (req.method === "POST") { 10 | const invoice = { 11 | senderAddress: { 12 | street: req.body.senderStreet, 13 | city: req.body.senderCity, 14 | postalCode: req.body.senderPostalCode, 15 | country: req.body.senderCountry, 16 | }, 17 | clientName: req.body.clientName, 18 | clientEmail: req.body.clientEmail, 19 | clientAddress: { 20 | street: req.body.clientStreet, 21 | city: req.body.clientCity, 22 | postalCode: req.body.clientPostalCode, 23 | country: req.body.clientCountry, 24 | }, 25 | createdAt: req.body.createdAt, 26 | paymentDue: req.body.createdAt, 27 | paymentTerms: req.body.paymentTerms, 28 | description: req.body.description, 29 | status: req.body.status, 30 | items: req.body.items, 31 | total: req.body.total, 32 | }; 33 | 34 | const db = client.db(); 35 | const collection = db.collection("allInvoices"); 36 | await collection.insertOne(invoice); 37 | 38 | res.status(200).json({ message: "Invoice added successfully" }); 39 | 40 | client.close(); 41 | } 42 | } 43 | 44 | export default handler; 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /pages/api/edit/[invoiceId]/index.js: -------------------------------------------------------------------------------- 1 | import { MongoClient, ObjectId } from "mongodb"; 2 | 3 | async function handler(req, res) { 4 | const { invoiceId } = req.query; 5 | const client = await MongoClient.connect( 6 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 7 | { useNewUrlParser: true } 8 | ); 9 | const db = client.db(); 10 | const collection = db.collection("allInvoices"); 11 | 12 | if (req.method === "PUT") { 13 | await collection.updateOne( 14 | { 15 | _id: ObjectId(invoiceId), 16 | }, 17 | { 18 | $set: { 19 | senderAddress: { 20 | street: req.body.senderStreet, 21 | city: req.body.senderCity, 22 | postalCode: req.body.senderPostalCode, 23 | country: req.body.senderCountry, 24 | }, 25 | clientName: req.body.clientName, 26 | clientEmail: req.body.clientEmail, 27 | clientAddress: { 28 | street: req.body.clientStreet, 29 | city: req.body.clientCity, 30 | postalCode: req.body.clientPostalCode, 31 | country: req.body.clientCountry, 32 | }, 33 | createdAt: req.body.createdAt, 34 | paymentDue: req.body.createdAt, 35 | paymentTerms: req.body.paymentTerms, 36 | description: req.body.description, 37 | status: req.body.status, 38 | items: req.body.items, 39 | total: req.body.total, 40 | }, 41 | } 42 | ); 43 | 44 | res.status(200).json({ message: "Invoice updated successfully" }); 45 | } 46 | 47 | client.close(); 48 | } 49 | 50 | export default handler; 51 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useRouter } from "next/router"; 3 | import { MongoClient } from "mongodb"; 4 | 5 | export default function Home(props) { 6 | const router = useRouter(); 7 | const { data } = props; 8 | 9 | const navigatePage = () => router.push("/add-new"); 10 | 11 | return ( 12 |
13 |
14 |
15 |

Invoices

16 |

There are total {data.length} invoices

17 |
18 | 19 | 22 |
23 | 24 |
25 | {/* ======= invoice item =========== */} 26 | {data?.map((invoice) => ( 27 | 28 |
29 |
30 |
31 | {invoice.id.substr(0, 6).toUpperCase()} 32 |
33 |
34 | 35 |
36 |
{invoice.clientName}
37 |
38 | 39 |
40 |

{invoice.createdAt}

41 |
42 | 43 |
44 |

${invoice.total}

45 |
46 | 47 |
48 | 59 |
60 |
61 | 62 | ))} 63 |
64 |
65 | ); 66 | } 67 | 68 | export async function getStaticProps() { 69 | const client = await MongoClient.connect( 70 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 71 | { useNewUrlParser: true } 72 | ); 73 | 74 | const db = client.db(); 75 | const collection = db.collection("allInvoices"); 76 | 77 | const invoices = await collection.find({}).toArray(); 78 | 79 | return { 80 | props: { 81 | data: invoices.map((invoice) => { 82 | return { 83 | id: invoice._id.toString(), 84 | clientName: invoice.clientName, 85 | createdAt: invoice.createdAt, 86 | total: invoice.total, 87 | status: invoice.status, 88 | }; 89 | }), 90 | }, 91 | revalidate: 1, 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /pages/invoices/[invoiceId]/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { MongoClient, ObjectId } from "mongodb"; 4 | import { toast } from "react-toastify"; 5 | 6 | const InvoiceDetails = (props) => { 7 | const router = useRouter(); 8 | const { data } = props; 9 | const modalRef = useRef(null); 10 | 11 | const goBack = () => router.push("/"); 12 | 13 | // update invoice status in database 14 | const updateStatus = async (invoiceId) => { 15 | const res = await fetch(`/api/invoices/${invoiceId}`, { 16 | method: "PUT", 17 | }); 18 | const data = await res.json(); 19 | }; 20 | 21 | // delete invoice from the database 22 | const deleteInvoice = async (invoiceId) => { 23 | try { 24 | const res = await fetch(`/api/invoices/${invoiceId}`, { 25 | method: "DELETE", 26 | }); 27 | 28 | const data = await res.json(); 29 | toast.success(data.message); 30 | router.push("/"); 31 | } catch (error) { 32 | toast.error("Something went wrong!"); 33 | } 34 | }; 35 | 36 | // open modal 37 | const modalToggle = () => modalRef.current.classList.toggle("showModal"); 38 | 39 | return ( 40 |
41 |
42 |
Go Back
43 |
44 | 45 | {/* ======= invoice details header ========== */} 46 |
47 |
48 |

Status

49 | 50 | 61 |
62 | 63 |
64 | 70 | 71 | {/* ========= confirm deletion modal start ========== */} 72 |
73 |
74 |

Confirm Deletion

75 |

76 | Are you sure you want to delete invoice # 77 | {data.id.substr(0, 6).toUpperCase()}? This action cannon be 78 | undone. 79 |

80 | 81 |
82 | 85 | 86 | 92 |
93 |
94 |
95 | 96 | {/* ======== confirm deletion modal end */} 97 | 98 | 101 | 102 | 110 |
111 |
112 | 113 | {/* ========= invoice details =========== */} 114 | 115 |
116 |
117 |
118 |

{data.id.substr(0, 6).toUpperCase()}

119 |

{data.description}

120 |
121 |
122 |

{data.senderAddress.street}

123 |

{data.senderAddress.city}

124 |

{data.senderAddress.postalCode}

125 |

{data.senderAddress.country}

126 |
127 |
128 | 129 | {/* =========== details box 2 =========== */} 130 |
131 |
132 |
133 |

Invoice Date

134 |

{data.createdAt}

135 |
136 |
137 |

Payment Due

138 |

{data.paymentDue}

139 |
140 |
141 | 142 | {/* ======= invoice client address ========== */} 143 |
144 |

Bill to

145 |

{data.clientName}

146 |
147 |

{data.clientAddress.street}

148 |

{data.clientAddress.city}

149 |

{data.clientAddress.postalCode}

150 |

{data.clientAddress.country}

151 |
152 |
153 | 154 |
155 |

Send to

156 |

{data.clientEmail}

157 |
158 |
159 | 160 | {/* ========= invoice items ============= */} 161 |
162 |
    163 |
  • 164 |

    Item Name

    165 |

    Qty

    166 |

    Price

    167 |

    Total

    168 |
  • 169 | 170 | {/* ======== invoice item ======= */} 171 | 172 | {data.items?.map((item, index) => ( 173 |
  • 174 |
    175 |
    {item.name}
    176 |
    177 | 178 |
    179 |

    {item.quantity}

    180 |
    181 |
    182 |

    ${item.price}

    183 |
    184 |
    185 |
    ${item.total}
    186 |
    187 |
  • 188 | ))} 189 |
190 |
191 | 192 | {/* ========== grand total ============= */} 193 |
194 |
Grand Total
195 |

${data.total}

196 |
197 |
198 |
199 | ); 200 | }; 201 | 202 | export default InvoiceDetails; 203 | 204 | export async function getStaticPaths() { 205 | const client = await MongoClient.connect( 206 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 207 | { useNewUrlParser: true } 208 | ); 209 | 210 | const db = client.db(); 211 | const collection = db.collection("allInvoices"); 212 | 213 | const invoices = await collection.find({}, { _id: 1 }).toArray(); 214 | 215 | return { 216 | fallback: "blocking", 217 | paths: invoices.map((invoice) => ({ 218 | params: { 219 | invoiceId: invoice._id.toString(), 220 | }, 221 | })), 222 | }; 223 | } 224 | 225 | export async function getStaticProps(context) { 226 | const { invoiceId } = context.params; 227 | 228 | const client = await MongoClient.connect( 229 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 230 | { useNewUrlParser: true } 231 | ); 232 | 233 | const db = client.db(); 234 | const collection = db.collection("allInvoices"); 235 | const invoice = await collection.findOne({ _id: ObjectId(invoiceId) }); 236 | 237 | return { 238 | props: { 239 | data: { 240 | id: invoice._id.toString(), 241 | senderAddress: invoice.senderAddress, 242 | clientAddress: invoice.clientAddress, 243 | clientName: invoice.clientName, 244 | clientEmail: invoice.clientEmail, 245 | description: invoice.description, 246 | createdAt: invoice.createdAt, 247 | paymentDue: invoice.paymentDue, 248 | items: invoice.items, 249 | total: invoice.total, 250 | status: invoice.status, 251 | }, 252 | }, 253 | revalidate: 1, 254 | }; 255 | } 256 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | /* ======= google fonts ============= */ 2 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600&display=swap"); 3 | 4 | /* ======== css variables ========= */ 5 | :root { 6 | --primary-color: #7c5dfa; 7 | --secondary-color: #1e2139; 8 | --body-bg: #141625; 9 | --paid-status-bg: #1f2c3f; 10 | --paid-status-color: #32cf9b; 11 | --pending-status-bg: rgba(255, 143, 0, 0.06); 12 | --pending-status-color: #ff8f00; 13 | --small-text-color: #888eb0; 14 | --draft-bg: #252945; 15 | --delete-btn-bg: #ec5757; 16 | --edit-btn-bg: #252945; 17 | } 18 | 19 | /* ======== base style ============ */ 20 | * { 21 | padding: 0; 22 | margin: 0; 23 | box-sizing: border-box; 24 | font-family: "Poppins", sans-serif; 25 | } 26 | 27 | body { 28 | background: var(--body-bg); 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | color: unset; 34 | } 35 | 36 | .btn { 37 | padding: 7px 25px; 38 | background: var(--primary-color); 39 | color: #fff; 40 | border-radius: 5px; 41 | cursor: pointer; 42 | } 43 | 44 | h1, 45 | h2, 46 | h3, 47 | h4, 48 | h5, 49 | h6 { 50 | color: #fff; 51 | } 52 | 53 | p { 54 | font-size: 0.8rem; 55 | color: var(--small-text-color); 56 | } 57 | 58 | .main__container { 59 | position: absolute; 60 | top: 0; 61 | left: 10%; 62 | width: calc(100% - 13%); 63 | padding: 30px 0px; 64 | } 65 | 66 | .pending__status, 67 | .paid__status, 68 | .draft__status, 69 | .edit__btn, 70 | .delete__btn, 71 | .mark__as-btn, 72 | .draft__btn { 73 | padding: 10px 25px; 74 | width: 100px; 75 | border-radius: 5px; 76 | font-size: 0.8rem; 77 | border: none; 78 | outline: none; 79 | cursor: pointer; 80 | font-weight: 500; 81 | } 82 | 83 | .paid__status { 84 | background: var(--paid-status-bg); 85 | color: var(--paid-status-color); 86 | } 87 | 88 | .pending__status { 89 | background: var(--pending-status-bg); 90 | color: var(--pending-status-color); 91 | } 92 | 93 | /* ========== sidebar css ========== */ 94 | .sidebar { 95 | width: 120px; 96 | height: 100vh; 97 | background: var(--secondary-color) !important; 98 | color: #fff; 99 | position: fixed; 100 | top: 0; 101 | left: 0; 102 | z-index: 9999; 103 | } 104 | 105 | .sidebar__container { 106 | display: flex; 107 | flex-direction: column; 108 | justify-content: space-between; 109 | width: 100%; 110 | height: 100%; 111 | } 112 | 113 | .sidebar__header, 114 | .sidebar__bottom { 115 | width: 100%; 116 | height: 100px; 117 | display: flex; 118 | align-items: center; 119 | justify-content: center; 120 | border-radius: 0px 20px 20px 0px; 121 | background: var(--primary-color); 122 | } 123 | 124 | .sidebar__bottom { 125 | background: var(--secondary-color) !important; 126 | border-top: 1px solid var(--primary-color); 127 | } 128 | 129 | .sidebar__bottom img { 130 | border-radius: 50%; 131 | } 132 | 133 | /* =========== home page style start =========== */ 134 | .invoice__header { 135 | display: flex; 136 | align-items: center; 137 | justify-content: space-between; 138 | margin-bottom: 50px; 139 | } 140 | 141 | .invoice__header-logo h3 { 142 | color: #fff; 143 | } 144 | 145 | .invoice__item { 146 | display: flex; 147 | align-items: center; 148 | justify-content: space-between; 149 | padding: 30px; 150 | background: var(--secondary-color); 151 | border-radius: 0.5rem; 152 | margin-bottom: 1.9rem; 153 | transition: 1s; 154 | color: #fff; 155 | cursor: pointer; 156 | } 157 | 158 | .invoice__item:hover { 159 | border: 1px solid var(--primary-color); 160 | transform: scaleY(1.1); 161 | } 162 | 163 | .invoice__container { 164 | padding-bottom: 200px !important; 165 | } 166 | 167 | /* =========== home page style end =========== */ 168 | /* =========== invoice details style start========= */ 169 | .back__btn { 170 | margin-bottom: 1.9rem; 171 | } 172 | 173 | .back__btn h6 { 174 | font-size: 0.9rem; 175 | cursor: pointer; 176 | } 177 | 178 | .details__status { 179 | display: flex; 180 | align-items: center; 181 | column-gap: 1.5rem; 182 | } 183 | 184 | .invoice__details-header { 185 | display: flex; 186 | align-items: center; 187 | justify-content: space-between; 188 | padding: 30px; 189 | border-radius: 0.5rem; 190 | margin-bottom: 1.9rem; 191 | background: var(--secondary-color); 192 | } 193 | 194 | .details__btns { 195 | display: flex; 196 | align-items: center; 197 | column-gap: 1rem; 198 | } 199 | 200 | .mark__as-btn { 201 | width: 130px !important; 202 | padding: 10px 5px !important; 203 | background: var(--primary-color); 204 | color: #fff; 205 | } 206 | 207 | .delete__btn { 208 | background: var(--delete-btn-bg); 209 | color: #fff; 210 | } 211 | 212 | .edit__btn { 213 | background: var(--edit-btn-bg); 214 | color: #fff; 215 | } 216 | 217 | .invoice__details { 218 | background: var(--secondary-color); 219 | padding: 30px; 220 | border-radius: 10px 10px 0px 0px; 221 | } 222 | 223 | .details__box { 224 | display: flex; 225 | justify-content: space-between; 226 | margin-bottom: 1.9rem; 227 | } 228 | 229 | .invoice__created-date { 230 | margin-bottom: 1.9rem; 231 | } 232 | 233 | .invoice__created-date p, 234 | .invoice__payment { 235 | margin-bottom: 5px; 236 | } 237 | 238 | .invoice__client-address p { 239 | margin-bottom: 5px; 240 | } 241 | 242 | .invoice__client-address h4 { 243 | margin-bottom: 10px; 244 | } 245 | 246 | .invoice__item-box { 247 | background: #252945; 248 | padding: 30px; 249 | border-radius: 7px 7px 0px 0px; 250 | } 251 | 252 | .list { 253 | list-style: none; 254 | } 255 | 256 | .list__item { 257 | display: flex; 258 | align-items: center; 259 | justify-content: space-between; 260 | margin-bottom: 1rem; 261 | } 262 | 263 | .list__item-box { 264 | width: 25% !important; 265 | text-align: right !important; 266 | } 267 | 268 | .item__name-box { 269 | width: 50% !important; 270 | } 271 | 272 | .grand__total { 273 | background: #0c0e16; 274 | padding: 30px; 275 | margin-top: 40px; 276 | margin-bottom: 30px; 277 | border-radius: 0px 0px 7px 7px; 278 | display: flex; 279 | align-items: center; 280 | justify-content: space-between; 281 | } 282 | 283 | /* =========== invoice details style end========= */ 284 | /* =========== add new page style start ======== */ 285 | .new__invoice { 286 | width: 60%; 287 | margin: auto; 288 | } 289 | 290 | .new__invoice-header { 291 | margin-bottom: 1.9rem; 292 | } 293 | 294 | .form__group input { 295 | padding: 12px 20px; 296 | width: 100%; 297 | border-radius: 5px; 298 | background: var(--secondary-color); 299 | color: #fff; 300 | border: none; 301 | outline: none; 302 | cursor: pointer; 303 | } 304 | 305 | .form__group { 306 | margin-bottom: 1.9rem; 307 | } 308 | 309 | .form__group p { 310 | margin-bottom: 10px; 311 | } 312 | 313 | .inline__form-group { 314 | display: flex; 315 | align-items: center; 316 | justify-content: space-between; 317 | } 318 | 319 | .inline__group { 320 | width: 48%; 321 | } 322 | 323 | .new__invoice-body { 324 | padding-bottom: 40px; 325 | } 326 | 327 | .bill__title { 328 | margin-bottom: 20px; 329 | } 330 | 331 | .bill__to { 332 | margin-top: 50px; 333 | } 334 | 335 | .invoice__items { 336 | margin-top: 70px; 337 | } 338 | 339 | .invoice__items h3 { 340 | margin-bottom: 30px; 341 | } 342 | 343 | .add__item-btn { 344 | width: 100%; 345 | background: var(--secondary-color); 346 | padding: 12px 25px; 347 | border-radius: 50px; 348 | border: none; 349 | 350 | outline: none; 351 | color: #fff; 352 | margin-top: 50px; 353 | cursor: pointer; 354 | } 355 | 356 | .draft__btn { 357 | background: #1f2c3f; 358 | color: #fff; 359 | margin-right: 30px; 360 | width: 150px !important; 361 | } 362 | 363 | .new__invoice__btns { 364 | display: flex; 365 | justify-content: space-between; 366 | margin-top: 100px; 367 | } 368 | 369 | /* =========== add new page style end ======== */ 370 | 371 | .disable { 372 | cursor: none; 373 | pointer-events: none; 374 | opacity: 50%; 375 | } 376 | 377 | /* ============== modal style ============ */ 378 | .delete__modal { 379 | background: rgba(9, 10, 17, 0.76); 380 | position: fixed; 381 | top: 0; 382 | left: 0; 383 | width: 100%; 384 | height: 100%; 385 | z-index: 99999; 386 | display: none; 387 | } 388 | 389 | .modal { 390 | position: absolute; 391 | top: 50%; 392 | left: 50%; 393 | transform: translate(-50%, -50%); 394 | width: 330px; 395 | padding: 30px; 396 | background: var(--secondary-color); 397 | z-index: 99999999; 398 | border-radius: 5px; 399 | } 400 | 401 | .modal h3 { 402 | margin-bottom: 15px; 403 | } 404 | 405 | .modal p { 406 | font-size: 0.8rem; 407 | line-height: 25px; 408 | } 409 | 410 | .modal__btns { 411 | margin-top: 25px; 412 | } 413 | 414 | .modal__btns button:first-child { 415 | background: #fff; 416 | color: var(--secondary-color); 417 | } 418 | 419 | .showModal { 420 | display: block; 421 | } 422 | -------------------------------------------------------------------------------- /pages/add-new/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { toast } from "react-toastify"; 4 | 5 | const AddNew = () => { 6 | const router = useRouter(); 7 | const [items, setItems] = useState([]); 8 | 9 | const senderStreet = useRef(""); 10 | const senderCity = useRef(""); 11 | const senderPostalCode = useRef(""); 12 | const senderCountry = useRef(""); 13 | const clientName = useRef(""); 14 | const clientEmail = useRef(""); 15 | const clientStreet = useRef(""); 16 | const clientCity = useRef(""); 17 | const clientPostalCode = useRef(""); 18 | const clientCountry = useRef(""); 19 | const description = useRef(""); 20 | const createdAt = useRef(""); 21 | const paymentTerms = useRef(""); 22 | 23 | // add product item 24 | const addItem = () => { 25 | setItems([...items, { name: "", quantity: 0, price: 0, total: 0 }]); 26 | }; 27 | 28 | // handler change 29 | const handlerChange = (event, i) => { 30 | const { name, value } = event.target; 31 | const list = [...items]; 32 | list[i][name] = value; 33 | list[i]["total"] = list[i]["quantity"] * list[i]["price"]; 34 | setItems(list); 35 | }; 36 | 37 | // delete product item 38 | const deleteItem = (i) => { 39 | const inputData = [...items]; 40 | inputData.splice(i, 1); 41 | setItems(inputData); 42 | }; 43 | 44 | // total amount of all product items 45 | const totalAmount = items.reduce((acc, curr) => acc + curr.total, 0); 46 | 47 | // submit data to the database 48 | const createInvoice = async (status) => { 49 | try { 50 | if ( 51 | senderStreet.current.value === "" || 52 | senderCity.current.value === "" || 53 | senderPostalCode.current.value === "" || 54 | senderCountry.current.value === "" || 55 | clientName.current.value === "" || 56 | clientEmail.current.value === "" || 57 | clientStreet.current.value === "" || 58 | clientCity.current.value === "" || 59 | clientPostalCode.current.value === "" || 60 | clientCountry.current.value === "" || 61 | description.current.value === "" || 62 | createdAt.current.value === "" || 63 | items.length === 0 64 | ) { 65 | toast.warning("All fields are required. Must provide valid data"); 66 | } else { 67 | const res = await fetch("/api/add-new", { 68 | method: "POST", 69 | headers: { 70 | "Content-Type": "application/json", 71 | }, 72 | body: JSON.stringify({ 73 | senderStreet: senderStreet.current.value, 74 | senderCity: senderCity.current.value, 75 | senderPostalCode: senderPostalCode.current.value, 76 | senderCountry: senderCountry.current.value, 77 | clientName: clientName.current.value, 78 | clientEmail: clientEmail.current.value, 79 | clientStreet: clientStreet.current.value, 80 | clientCity: clientCity.current.value, 81 | clientPostalCode: clientPostalCode.current.value, 82 | clientCountry: clientCountry.current.value, 83 | description: description.current.value, 84 | createdAt: createdAt.current.value, 85 | paymentDue: createdAt.current.value, 86 | paymentTerms: paymentTerms.current.value, 87 | status: status, 88 | items: items, 89 | total: totalAmount, 90 | }), 91 | }); 92 | const data = await res.json(); 93 | 94 | toast.success(data.message); 95 | router.push("/"); 96 | } 97 | } catch (error) { 98 | toast.error("Something went wrong!"); 99 | } 100 | }; 101 | 102 | return ( 103 |
104 |
105 |
106 |

New Invoice

107 |
108 | 109 | {/* ======== new invoice body ========= */} 110 |
111 | {/* ======= bill from ========== */} 112 |
113 |

Bill from

114 |
115 |

Street Address

116 | 117 |
118 | 119 |
120 |
121 |

City

122 | 123 |
124 | 125 |
126 |

Postal Code

127 | 128 |
129 | 130 |
131 |

Country

132 | 133 |
134 |
135 |
136 | 137 | {/* ========= bill to ========== */} 138 |
139 |

Bill to

140 |
141 |

Client Name

142 | 143 |
144 | 145 |
146 |

Client Email

147 | 148 |
149 | 150 |
151 |

Street Address

152 | 153 |
154 | 155 |
156 |
157 |

City

158 | 159 |
160 | 161 |
162 |

Postal Code

163 | 164 |
165 | 166 |
167 |

Country

168 | 169 |
170 |
171 | 172 |
173 |
174 |

Invoice Date

175 | 176 |
177 | 178 |
179 |

Payment Terms

180 | 181 |
182 |
183 | 184 |
185 |

Project Description

186 | 187 |
188 |
189 | 190 | {/* ========= invoice product items =========*/} 191 | 192 |
193 |

Item List

194 | {items?.map((item, i) => ( 195 |
196 |
197 |
198 |

Item Name

199 | handlerChange(e, i)} 203 | /> 204 |
205 | 206 |
207 |

Qty

208 | handlerChange(e, i)} 212 | /> 213 |
214 | 215 |
216 |

Price

217 | handlerChange(e, i)} 221 | /> 222 |
223 |
224 |

Total

225 |

{item.total}

226 |
227 | 228 | 231 |
232 |
233 | ))} 234 |
235 | 236 | 239 | 240 |
241 | 244 |
245 | 251 | 252 | 258 |
259 |
260 |
261 |
262 |
263 | ); 264 | }; 265 | 266 | export default AddNew; 267 | -------------------------------------------------------------------------------- /pages/edit/[invoiceId]/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { MongoClient, ObjectId } from "mongodb"; 4 | import { toast } from "react-toastify"; 5 | 6 | const EditItem = (props) => { 7 | const invoice = props.data; 8 | const router = useRouter(); 9 | 10 | const [items, setItems] = useState(invoice.items); 11 | 12 | const [senderStreet, setSenderStreet] = useState(""); 13 | const [senderCity, setSenderCity] = useState(""); 14 | const [senderPostalCode, setSenderPostalCode] = useState(""); 15 | const [senderCountry, setSenderCountry] = useState(""); 16 | const [clientName, setClientName] = useState(""); 17 | const [clientEmail, setClientEmail] = useState(""); 18 | const [clientStreet, setClientStreet] = useState(""); 19 | const [clientCity, setClientCity] = useState(""); 20 | const [clientPostalCode, setClientPostalCode] = useState(""); 21 | const [clientCountry, setClientCountry] = useState(""); 22 | const [description, setDescription] = useState(""); 23 | const [createdAt, setCreatedAt] = useState(""); 24 | const [paymentTerms, setPaymentTerms] = useState(""); 25 | 26 | // add product item 27 | const addItem = () => { 28 | setItems([...items, { name: "", quantity: 0, price: 0, total: 0 }]); 29 | }; 30 | 31 | // handler change 32 | const handlerChange = (event, i) => { 33 | const { name, value } = event.target; 34 | const list = [...items]; 35 | list[i][name] = value; 36 | list[i]["total"] = list[i]["quantity"] * list[i]["price"]; 37 | setItems(list); 38 | }; 39 | 40 | // delete product item 41 | const deleteItem = (i) => { 42 | const inputData = [...items]; 43 | inputData.splice(i, 1); 44 | setItems(inputData); 45 | }; 46 | 47 | // total amount of all product items 48 | const totalAmount = items.reduce((acc, curr) => acc + curr.total, 0); 49 | 50 | // update invoice in database 51 | const updateInvoice = async (invoiceId, status) => { 52 | try { 53 | const res = await fetch(`/api/edit/${invoiceId}`, { 54 | method: "PUT", 55 | headers: { 56 | "Content-Type": "application/json", 57 | }, 58 | body: JSON.stringify({ 59 | senderStreet: senderStreet, 60 | senderCity: senderCity, 61 | senderPostalCode: senderPostalCode, 62 | senderCountry: senderCountry, 63 | clientName: clientName, 64 | clientEmail: clientEmail, 65 | clientStreet: clientStreet, 66 | clientCity: clientCity, 67 | clientPostalCode: clientPostalCode, 68 | clientCountry: clientCountry, 69 | description: description, 70 | createdAt: createdAt, 71 | paymentDue: createdAt, 72 | paymentTerms: paymentTerms, 73 | status: status, 74 | items: items, 75 | total: totalAmount, 76 | }), 77 | }); 78 | 79 | const data = await res.json(); 80 | 81 | router.push(`/invoices/${invoiceId}`); 82 | toast.success(data.message); 83 | } catch (error) { 84 | toast.error("Something went wrong!"); 85 | } 86 | }; 87 | 88 | // set default input data 89 | useEffect(() => { 90 | setSenderCity(invoice.senderAddress.city); 91 | setSenderStreet(invoice.senderAddress.street); 92 | setSenderPostalCode(invoice.senderAddress.postalCode); 93 | setSenderCountry(invoice.senderAddress.country); 94 | 95 | setClientCity(invoice.clientAddress.city); 96 | setClientStreet(invoice.clientAddress.street); 97 | setClientPostalCode(invoice.clientAddress.postalCode); 98 | setClientCountry(invoice.clientAddress.country); 99 | 100 | setClientName(invoice.clientName); 101 | setClientEmail(invoice.clientEmail); 102 | setDescription(invoice.description); 103 | setCreatedAt(invoice.createdAt); 104 | setPaymentTerms(invoice.paymentTerms); 105 | }, [invoice]); 106 | 107 | return ( 108 |
109 |
110 |
111 |

Edit #{invoice.id.substr(0, 6).toUpperCase()}

112 |
113 | 114 | {/* ======== new invoice body ========= */} 115 |
116 | {/* ======= bill from ========== */} 117 |
118 |

Bill from

119 |
120 |

Street Address

121 | setSenderStreet(e.target.value)} 125 | /> 126 |
127 | 128 |
129 |
130 |

City

131 | setSenderCity(e.target.value)} 135 | /> 136 |
137 | 138 |
139 |

Postal Code

140 | setSenderPostalCode(e.target.value)} 144 | /> 145 |
146 | 147 |
148 |

Country

149 | setSenderCountry(e.target.value)} 153 | /> 154 |
155 |
156 |
157 | 158 | {/* ========= bill to ========== */} 159 |
160 |

Bill to

161 |
162 |

Client Name

163 | setClientName(e.target.value)} 167 | /> 168 |
169 | 170 |
171 |

Client Email

172 | setClientEmail(e.target.value)} 176 | /> 177 |
178 | 179 |
180 |

Street Address

181 | setClientStreet(e.target.value)} 185 | /> 186 |
187 | 188 |
189 |
190 |

City

191 | setClientCity(e.target.value)} 195 | /> 196 |
197 | 198 |
199 |

Postal Code

200 | setClientPostalCode(e.target.value)} 204 | /> 205 |
206 | 207 |
208 |

Country

209 | setClientCountry(e.target.value)} 213 | /> 214 |
215 |
216 | 217 |
218 |
219 |

Invoice Date

220 | setCreatedAt(e.target.value)} 224 | /> 225 |
226 | 227 |
228 |

Payment Terms

229 | setPaymentTerms(e.target.value)} 233 | /> 234 |
235 |
236 | 237 |
238 |

Project Description

239 | setDescription(e.target.value)} 243 | /> 244 |
245 |
246 | 247 | {/* ========= invoice product items =========*/} 248 | 249 |
250 |

Item List

251 | {items?.map((item, i) => ( 252 |
253 |
254 |
255 |

Item Name

256 | handlerChange(e, i)} 261 | /> 262 |
263 | 264 |
265 |

Qty

266 | handlerChange(e, i)} 271 | /> 272 |
273 | 274 |
275 |

Price

276 | handlerChange(e, i)} 281 | /> 282 |
283 |
284 |

Total

285 |

{item.total}

286 |
287 | 288 | 291 |
292 |
293 | ))} 294 |
295 | 296 | 299 | 300 |
301 |
302 | 308 | 309 | 315 |
316 |
317 |
318 |
319 |
320 | ); 321 | }; 322 | 323 | export default EditItem; 324 | 325 | export async function getStaticPaths() { 326 | const client = await MongoClient.connect( 327 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 328 | { useNewUrlParser: true } 329 | ); 330 | 331 | const db = client.db(); 332 | const collection = db.collection("allInvoices"); 333 | 334 | const invoices = await collection.find({}, { _id: 1 }).toArray(); 335 | 336 | return { 337 | fallback: "blocking", 338 | paths: invoices.map((invoice) => ({ 339 | params: { 340 | invoiceId: invoice._id.toString(), 341 | }, 342 | })), 343 | }; 344 | } 345 | 346 | export async function getStaticProps(context) { 347 | const { invoiceId } = context.params; 348 | 349 | const client = await MongoClient.connect( 350 | `mongodb+srv://${process.env.USER__NAME}:${process.env.USER__PASSWORD}@cluster0.ishut.mongodb.net/${process.env.DATABASE__NAME}?retryWrites=true&w=majority`, 351 | { useNewUrlParser: true } 352 | ); 353 | 354 | const db = client.db(); 355 | const collection = db.collection("allInvoices"); 356 | const invoice = await collection.findOne({ _id: ObjectId(invoiceId) }); 357 | 358 | return { 359 | props: { 360 | data: { 361 | id: invoice._id.toString(), 362 | senderAddress: invoice.senderAddress, 363 | clientAddress: invoice.clientAddress, 364 | clientName: invoice.clientName, 365 | clientEmail: invoice.clientEmail, 366 | description: invoice.description, 367 | createdAt: invoice.createdAt, 368 | paymentDue: invoice.paymentDue, 369 | items: invoice.items, 370 | total: invoice.total, 371 | status: invoice.status, 372 | paymentTerms: invoice.paymentTerms, 373 | }, 374 | }, 375 | revalidate: 1, 376 | }; 377 | } 378 | --------------------------------------------------------------------------------