mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-16 04:11:38 +00:00
Merge branch 'main' into prettier-eslint-root
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Page() {
|
||||
return <div>Individual Invoice Page</div>
|
||||
}
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
79
dashboard/15-final/app/ui/customers/table.tsx
Normal file
79
dashboard/15-final/app/ui/customers/table.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user