├── .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 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default Sidebar;
23 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------