Merge branch 'main' into prettier-eslint-root

This commit is contained in:
Michael Novotny
2023-09-06 12:02:50 -05:00
13 changed files with 213 additions and 39 deletions

View File

@@ -11,7 +11,7 @@ export default function Home() {
<main>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
Learn <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
@@ -112,4 +112,4 @@ export default function Home() {
`}</style>
</div>
)
}
}

View File

@@ -1,3 +1,9 @@
import CustomersTable from "@/app/ui/customers/table";
export default function Page() {
return <div>List of customers</div>
return (
<div>
<CustomersTable />
</div>
)
}

View File

@@ -1,3 +1,18 @@
export default function Page() {
return <div>Edit Invoice Page</div>
import InvoiceForm from "@/app/ui/invoices/form";
import { invoices } from "@/app/lib/dummy-data";
import { notFound } from "next/navigation";
export default function Page({ params }: { params: { id: string } }) {
const id = params.id ? parseInt(params.id) : null;
const invoice = invoices.find((invoice) => invoice.id === id);
if (!invoice) {
notFound();
}
return (
<div>
<InvoiceForm type="edit" invoice={invoice} />
</div>
);
}

View File

@@ -1,3 +0,0 @@
export default function Page() {
return <div>Individual Invoice Page</div>
}

View File

@@ -1,5 +1,5 @@
import AddInvoiceForm from "@/app/ui/invoices/add-invoice-form";
import InvoiceForm from "@/app/ui/invoices/form";
export default function Page() {
return <AddInvoiceForm />;
return <InvoiceForm type="new" />;
}

View File

@@ -1,9 +1,9 @@
import Table from "@/app/ui/invoices/table";
import InvoicesTable from "@/app/ui/invoices/table";
export default function Page() {
return (
<div>
<Table />
<InvoicesTable />
</div>
);
}

View File

@@ -4,3 +4,8 @@ export async function deleteInvoice(id: number) {
// TO DO: Add delete invoice logic
console.log("Delete invoice", id);
}
export async function addOrUpdateInvoice(formData: FormData) {
// TO DO: Add create/update invoice logic
console.log("Edit Invoice");
}

View File

@@ -1,6 +1,6 @@
import { Invoice, Revenue } from "./definitions";
export const calculateInvoices = (
export const calculateAllInvoices = (
invoices: Invoice[],
status: "pending" | "paid",
) => {
@@ -13,11 +13,34 @@ export const calculateInvoices = (
});
};
export const calculateCustomerInvoices = (
invoices: Invoice[],
status: "pending" | "paid",
customerId: number,
) => {
return invoices
.filter((invoice) => invoice.customerId === customerId)
.filter((invoice) => !status || invoice.status === status)
.reduce((total, invoice) => total + invoice.amount / 100, 0)
.toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
};
// Once a database is connected, we can use SQL to query the database directly
// This will be more efficient than querying all invoices and then filtering them
// E.g. "SELECT * FROM invoices
// ORDER BY date DESC
// LIMIT 5;"
export const countCustomerInvoices = (
invoices: Invoice[],
customerId: number,
) => {
return invoices.filter((invoice) => invoice.customerId === customerId).length;
};
export const findLatestInvoices = (invoices: Invoice[]) => {
return [...invoices]
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())

View File

@@ -84,6 +84,13 @@ export const invoices: Invoice[] = [
id: 7,
customerId: 3,
amount: 8945,
status: "pending",
date: "2023-06-01",
},
{
id: 8,
customerId: 4,
amount: 32545,
status: "paid",
date: "2023-06-01",
},

View File

@@ -0,0 +1,79 @@
import { customers, invoices } from "@/app/lib/dummy-data";
import {
countCustomerInvoices,
calculateCustomerInvoices,
} from "@/app/lib/calculations";
export default function CustomersTable() {
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className="text-base font-semibold">Customers</h1>
</div>
<div className="mt-8">
<div className="overflow-x-auto">
<div className="overflow-hidden rounded-md border">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50 text-left text-sm">
<tr>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Profile</span>
</th>
<th scope="col" className="px-3.5 py-3.5 sm:pl-6">
Name
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Email
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Invoices
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Pending
</th>
<th scope="col" className="px-3 py-3.5 font-semibold">
Total Paid
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white text-gray-500">
{customers.map((customer) => (
<tr key={customer.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6">
<div className="flex w-7 flex-none items-center">
<img
src={customer.imageUrl}
alt={customer.name}
className="h-7 w-full flex-none rounded-full"
/>
</div>
</td>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6">
{customer.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{customer.email}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{countCustomerInvoices(invoices, customer.id)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{calculateCustomerInvoices(
invoices,
"pending",
customer.id,
)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{calculateCustomerInvoices(invoices, "paid", customer.id)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,12 +1,12 @@
import Card from "@/app/ui/dashboard/card";
import { invoices, customers, revenue } from "@/app/lib/dummy-data";
import { calculateInvoices } from "@/app/lib/calculations";
import { calculateAllInvoices } from "@/app/lib/calculations";
import RevenueChart from "@/app/ui/dashboard/revenue-chart";
import LatestInvoices from "@/app/ui/dashboard/latest-invoices";
export default function DashboardOverview() {
const totalPaidInvoices = calculateInvoices(invoices, "paid");
const totalPendingInvoices = calculateInvoices(invoices, "pending");
const totalPaidInvoices = calculateAllInvoices(invoices, "paid");
const totalPendingInvoices = calculateAllInvoices(invoices, "pending");
const numberOfInvoices = invoices.length;
const numberOfCustomers = customers.length;

View File

@@ -1,11 +1,30 @@
"use client";
import { Invoice } from "@/app/lib/definitions";
import { customers } from "@/app/lib/dummy-data";
import { useState, FormEvent } from "react";
export default function AddInvoiceForm() {
const [selectedCustomer, setSelectedCustomer] = useState(0);
const [amount, setAmount] = useState("");
// import { addOrUpdateInvoice } from "@/app/lib/actions";
// export const dynamic = "force-dynamic";
export default function InvoiceForm({
type,
invoice,
}: {
type: "new" | "edit";
invoice?: Invoice;
}) {
// TO DO: Replace state and handleSubmit with a Server Action
const customer = customers.find(
(customer) => customer.id === invoice?.customerId,
);
const initialCustomer = customer ? customer.id : 0;
const initialAmount = invoice?.amount ? invoice.amount / 100 : 0;
const initialStatus = invoice?.status || "pending";
const [selectedCustomer, setSelectedCustomer] = useState(initialCustomer);
const [amount, setAmount] = useState<number>(initialAmount);
const [status, setStatus] = useState<"pending" | "paid">(initialStatus);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@@ -13,11 +32,11 @@ export default function AddInvoiceForm() {
if (selectedCustomer && amount) {
const newInvoice: Invoice = {
customerId: selectedCustomer,
amount: parseInt(amount) * 100, // Convert to cents
amount: amount * 100, // Convert to cents
// These would be generated on the server
id: 1, // Record ID will be automatically incremented
status: "pending", // Default status for a new invoice
status: status, // Default status for a new invoice
date: new Date().toISOString().split("T")[0],
};
@@ -28,7 +47,9 @@ export default function AddInvoiceForm() {
return (
<div className="mx-auto max-w-md p-4">
<h2 className="mb-6 text-xl font-semibold text-gray-900">New Invoice</h2>
<h2 className="mb-6 text-xl font-semibold text-gray-900">
{type === "new" ? "New Invoice" : "Edit Invoice"}
</h2>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label
@@ -37,10 +58,12 @@ export default function AddInvoiceForm() {
>
Customer
</label>
<select
id="customer"
onChange={(e) => setSelectedCustomer(Number(e.target.value))}
className="block w-full rounded-md border-0 py-1.5 pl-3 text-sm ring-1 ring-inset ring-gray-200 placeholder:text-gray-200"
value={selectedCustomer}
>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
@@ -60,12 +83,38 @@ export default function AddInvoiceForm() {
type="number"
value={amount}
placeholder="00.00"
onChange={(e) => setAmount(e.target.value)}
onChange={(e) => setAmount(Number(e.target.value))}
className="block w-full rounded-md border-0 py-1.5 pl-7 text-sm leading-6 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400"
/>
</div>
</div>
{type === "edit" ? (
<div className="mb-4">
<label
className="mb-2 block text-sm font-semibold"
htmlFor="status"
>
Status
</label>
<select
id="status"
className="block w-full rounded-md border-0 py-1.5 pl-3 text-sm ring-1 ring-inset ring-gray-200 placeholder:text-gray-200"
onChange={(e) => {
const value = e.target.value;
if (value === "paid" || value === "pending") {
setStatus(value);
}
}}
value={status}
>
<option value="pending">Pending</option>
<option value="paid">Paid</option>
</select>
</div>
) : null}
<button
type="submit"
className="rounded-md bg-blue-500 px-4 py-2 text-center text-sm font-semibold text-white hover:bg-blue-600"

View File

@@ -1,5 +1,6 @@
import { invoices, customers } from "@/app/lib/dummy-data";
import { Customer } from "@/app/lib/definitions";
import Link from "next/link";
import {
PencilSquareIcon,
@@ -26,7 +27,7 @@ function renderInvoiceStatus(status: string) {
}
}
export default function Table() {
export default function InvoicesTable() {
function getCustomerById(customerId: number): Customer | null {
const customer = customers.find((customer) => customer.id === customerId);
return customer ? customer : null;
@@ -45,7 +46,7 @@ export default function Table() {
</div>
<div className="mt-8">
<div className="overflow-x-auto">
<div className="rounded-md border">
<div className="overflow-hidden rounded-md border">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50 text-left text-sm">
<tr>
@@ -71,12 +72,9 @@ export default function Table() {
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Edit</span>
</th>
{/* <th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">View</span>
</th> */}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white text-gray-500">
<tbody className="divide-y divide-gray-200 text-gray-500">
{invoices.map((invoice) => (
<tr key={invoice.id}>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6">
@@ -107,19 +105,14 @@ export default function Table() {
{renderInvoiceStatus(invoice.status)}
</td>
<td className="flex justify-end gap-2 whitespace-nowrap py-4 pl-3 pr-6 text-sm">
<button className="rounded-md border p-1">
<Link
href={`/dashboard/invoices/${invoice.id}/edit`}
className="rounded-md border p-1"
>
<PencilSquareIcon className="w-4" />
</button>
</Link>
<DeleteInvoice id={invoice.id} />
</td>
{/* <td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<Link
href={`/dashboard/invoices/${invoice.id}`}
className="text-blue-600 hover:text-blue-900"
>
View<span className="sr-only">, {invoice.id}</span>
</Link>
</td> */}
</tr>
))}
</tbody>