From 3cfc073dd8163fed1c330f91272abe7bb5babbc6 Mon Sep 17 00:00:00 2001 From: behnam Date: Thu, 31 Oct 2024 20:40:39 +0300 Subject: [PATCH] Add base of dashboard logics --- package.json | 4 +- public/customers/amy-burns.png | Bin 0 -> 7954 bytes public/customers/balazs-orban.png | Bin 0 -> 7951 bytes public/customers/delba-de-oliveira.png | Bin 0 -> 5824 bytes public/customers/emil-kowalski.png | Bin 0 -> 4141 bytes public/customers/evil-rabbit.png | Bin 0 -> 1019 bytes public/customers/guillermo-rauch.png | Bin 0 -> 6284 bytes public/customers/hector-simpson.png | Bin 0 -> 5602 bytes public/customers/jared-palmer.png | Bin 0 -> 6460 bytes public/customers/lee-robinson.png | Bin 0 -> 5653 bytes public/customers/michael-novotny.png | Bin 0 -> 9902 bytes public/customers/steph-dietz.png | Bin 0 -> 7151 bytes public/customers/steven-tey.png | Bin 0 -> 7345 bytes src/app/dashboard/(overview)/page.tsx | 15 +- src/app/dashboard/components/cards.tsx | 62 +++++ .../dashboard/components/latest-invoices.tsx | 60 +++++ .../dashboard/components/revenue-chart.tsx | 53 ++++ src/app/lib/actions.ts | 117 ++++++++ src/app/lib/data.ts | 254 ++++++++++++++++++ src/app/lib/definitions.ts | 86 ++++++ src/app/lib/placeholder-data.js | 188 +++++++++++++ src/app/lib/utils.ts | 69 +++++ src/bootstrap/db/db.ts | 13 + yarn.lock | 10 + 24 files changed, 925 insertions(+), 6 deletions(-) create mode 100644 public/customers/amy-burns.png create mode 100644 public/customers/balazs-orban.png create mode 100644 public/customers/delba-de-oliveira.png create mode 100644 public/customers/emil-kowalski.png create mode 100644 public/customers/evil-rabbit.png create mode 100644 public/customers/guillermo-rauch.png create mode 100644 public/customers/hector-simpson.png create mode 100644 public/customers/jared-palmer.png create mode 100644 public/customers/lee-robinson.png create mode 100644 public/customers/michael-novotny.png create mode 100644 public/customers/steph-dietz.png create mode 100644 public/customers/steven-tey.png create mode 100644 src/app/dashboard/components/cards.tsx create mode 100644 src/app/dashboard/components/latest-invoices.tsx create mode 100644 src/app/dashboard/components/revenue-chart.tsx create mode 100644 src/app/lib/actions.ts create mode 100644 src/app/lib/data.ts create mode 100644 src/app/lib/definitions.ts create mode 100644 src/app/lib/placeholder-data.js create mode 100644 src/app/lib/utils.ts create mode 100644 src/bootstrap/db/db.ts diff --git a/package.json b/package.json index b9fb46b..0cd01c1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "seed": "node -r dotenv/config ./src/bootstrap/db/seed.js" }, "dependencies": { + "@heroicons/react": "^2.1.5", "@radix-ui/react-icons": "^1.3.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -21,7 +22,8 @@ "reflect-metadata": "^0.2.2", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", - "tsyringe": "^4.8.0" + "tsyringe": "^4.8.0", + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^20", diff --git a/public/customers/amy-burns.png b/public/customers/amy-burns.png new file mode 100644 index 0000000000000000000000000000000000000000..7b29d72526a0644b11519442aac7d6d95f305702 GIT binary patch literal 7954 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 KMNUMnLSTaFb|M-8 literal 0 HcmV?d00001 diff --git a/public/customers/emil-kowalski.png b/public/customers/emil-kowalski.png new file mode 100644 index 0000000000000000000000000000000000000000..8411e219dcd6484e52f53036066be7bd14995fab GIT binary patch literal 4141 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 literal 0 HcmV?d00001 diff --git a/public/customers/evil-rabbit.png b/public/customers/evil-rabbit.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7990ff2f533d3d053a97f631800721e1dc64b6 GIT binary patch literal 1019 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 WLj=Co%Z9=L0000}YN0f`qj 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 literal 0 HcmV?d00001 diff --git a/public/customers/michael-novotny.png b/public/customers/michael-novotny.png new file mode 100644 index 0000000000000000000000000000000000000000..96a13a69892c1c90445cccca50ec3d28b1681971 GIT binary patch literal 9902 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 XBRfi*vO-@%00000NkvXXu0mjf;hzxo literal 0 HcmV?d00001 diff --git a/src/app/dashboard/(overview)/page.tsx b/src/app/dashboard/(overview)/page.tsx index 60a12e0..6d632b5 100644 --- a/src/app/dashboard/(overview)/page.tsx +++ b/src/app/dashboard/(overview)/page.tsx @@ -1,3 +1,8 @@ +import { LatestInvoicesSkeleton, RevenueChartSkeleton } from "@/app/components/skeleton/skeletons"; +import CardWrapper from "@/app/dashboard/components/cards"; +import LatestInvoices from "@/app/dashboard/components/latest-invoices"; +import RevenueChart from "@/app/dashboard/components/revenue-chart"; +import { Suspense } from "react"; export default async function Dashboard() { @@ -7,15 +12,15 @@ export default async function Dashboard() { Dashboard
- {/* */} +
- {/* }> + }> - */} - {/* }> + + }> - */} +
) diff --git a/src/app/dashboard/components/cards.tsx b/src/app/dashboard/components/cards.tsx new file mode 100644 index 0000000..72bb37b --- /dev/null +++ b/src/app/dashboard/components/cards.tsx @@ -0,0 +1,62 @@ +import { + BanknotesIcon, + ClockIcon, + UserGroupIcon, + InboxIcon, +} from '@heroicons/react/24/outline'; +import { fetchCardData } from '@/app/lib/data'; + +const iconMap = { + collected: BanknotesIcon, + customers: UserGroupIcon, + pending: ClockIcon, + invoices: InboxIcon, +}; + +export default async function CardWrapper() { + const { + numberOfInvoices, + numberOfCustomers, + totalPaidInvoices, + totalPendingInvoices, + } = await fetchCardData(); + + return ( + <> + + + + + + ); +} + +export function Card({ + title, + value, + type, +}: { + title: string; + value: number | string; + type: 'invoices' | 'customers' | 'pending' | 'collected'; +}) { + const Icon = iconMap[type]; + + return ( +
+
+ {Icon ? : null} +

{title}

+
+

+ {value} +

+
+ ); +} diff --git a/src/app/dashboard/components/latest-invoices.tsx b/src/app/dashboard/components/latest-invoices.tsx new file mode 100644 index 0000000..5c7cd95 --- /dev/null +++ b/src/app/dashboard/components/latest-invoices.tsx @@ -0,0 +1,60 @@ +import { ArrowPathIcon } from '@heroicons/react/24/outline'; +import clsx from 'clsx'; +import Image from 'next/image'; +import { fetchLatestInvoices } from '@/app/lib/data'; +export default async function LatestInvoices() { + const latestInvoices = await fetchLatestInvoices(); + + return ( +
+

+ Latest Invoices +

+
+ +
+ {latestInvoices.map((invoice, i) => { + return ( +
+
+ {`${invoice.name}'s +
+

+ {invoice.name} +

+

+ {invoice.email} +

+
+
+

+ {invoice.amount} +

+
+ ); + })} +
+
+ +

Updated just now

+
+
+
+ ); +} diff --git a/src/app/dashboard/components/revenue-chart.tsx b/src/app/dashboard/components/revenue-chart.tsx new file mode 100644 index 0000000..49d888c --- /dev/null +++ b/src/app/dashboard/components/revenue-chart.tsx @@ -0,0 +1,53 @@ +import { generateYAxis } from '@/app/lib/utils'; +import { CalendarIcon } from '@heroicons/react/24/outline'; +import { fetchRevenue } from '@/app/lib/data'; + +export default async function RevenueChart() { + const revenue = await fetchRevenue(); + + const chartHeight = 350; + + const { yAxisLabels, topLabel } = generateYAxis(revenue); + + if (!revenue || revenue.length === 0) { + return

No data available.

; + } + + return ( +
+

+ Recent Revenue +

+
+
+
+ {yAxisLabels.map((label) => ( +

{label}

+ ))} +
+ + {revenue.map((month) => ( +
+
+

+ {month.month} +

+
+ ))} +
+
+ +

Last 12 months

+
+
+
+ ); +} diff --git a/src/app/lib/actions.ts b/src/app/lib/actions.ts new file mode 100644 index 0000000..617b4d7 --- /dev/null +++ b/src/app/lib/actions.ts @@ -0,0 +1,117 @@ +'use server'; + +import { z } from 'zod'; +import { revalidatePath } from 'next/cache'; +import { redirect } from 'next/navigation'; +import { sql } from '@/bootstrap/db/db'; + +const FormSchema = z.object({ + id: z.string(), + customerId: z.string({ + invalid_type_error: 'Please select a customer.', + }), + amount: z.coerce.number().gt(0, { message: 'Please enter an amount greater than $0.' }), + status: z.enum(['pending', 'paid'], { + invalid_type_error: 'Please select an invoice status.', + }), + date: z.string(), +}); +const CreateInvoice = FormSchema.omit({ id: true, date: true }); + + + +// This is temporary +export type State = { + errors?: { + customerId?: string[]; + amount?: string[]; + status?: string[]; + }; + message?: string | null; +}; + +export async function createInvoice(_prevState: State, formData: FormData) { + // Validate form fields using Zod + console.log('ffor', formData) + const validatedFields = CreateInvoice.safeParse({ + customerId: formData?.get('customerId') || undefined, + amount: formData?.get('amount') || undefined, + status: formData?.get('status') || undefined, + }); + + // If form validation fails, return errors early. Otherwise, continue. + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + message: 'Missing Fields. Failed to Create Invoice.', + }; + } + + // Prepare data for insertion into the database + const { customerId, amount, status } = validatedFields.data; + const amountInCents = amount * 100; + const date = new Date().toISOString().split('T')[0]; + + // Insert data into the database + try { + await sql` + INSERT INTO invoices (customer_id, amount, status, date) + VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) + `; + } catch { + // If a database error occurs, return a more specific error. + return { + message: 'Database Error: Failed to Create Invoice.', + }; + } + + // Revalidate the cache for the invoices page and redirect the user. + revalidatePath('/dashboard/invoices'); + redirect('/dashboard/invoices'); +} + +const UpdateInvoice = FormSchema.omit({ id: true, date: true }); +export async function updateInvoice( + id: string, + _prevState: State, + formData: FormData, +) { + const validatedFields = UpdateInvoice.safeParse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + message: 'Missing Fields. Failed to Update Invoice.', + }; + } + + const { customerId, amount, status } = validatedFields.data; + const amountInCents = amount * 100; + + try { + await sql` + UPDATE invoices + SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status} + WHERE id = ${id} + `; + } catch { + return { message: 'Database Error: Failed to Update Invoice.' }; + } + + revalidatePath('/dashboard/invoices'); + redirect('/dashboard/invoices'); +} +export async function deleteInvoice(id: string) { + try { + await sql`DELETE FROM invoices WHERE id = ${id}`; + revalidatePath('/dashboard/invoices'); + return { message: 'Deleted Invoice.' }; + } catch { + return { message: 'Database Error: Failed to Delete Invoice.' }; + } +} + diff --git a/src/app/lib/data.ts b/src/app/lib/data.ts new file mode 100644 index 0000000..c655f10 --- /dev/null +++ b/src/app/lib/data.ts @@ -0,0 +1,254 @@ +import { sql } from '@/bootstrap/db/db'; +import { + CustomerField, + CustomersTableType, + InvoiceForm, + InvoicesTable, + User, + Revenue, + Invoice, + Customer, + LatestInvoice, +} from './definitions'; +import { formatCurrency } from './utils'; +import postgres from 'postgres'; +import { connection } from 'next/server'; + +export async function fetchRevenue() { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + + try { + // Artificially delay a response for demo purposes. + // Don't do this in production :) + + console.log('Fetching revenue data...'); + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const data = await sql`SELECT * FROM revenue`; + + console.log('Data fetch completed after 3 seconds.'); + + return data as postgres.RowList; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch revenue data.'); + } +} + +export async function fetchLatestInvoices() { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + + try { + const data = await sql` + SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id + FROM invoices + JOIN customers ON invoices.customer_id = customers.id + ORDER BY invoices.date DESC + LIMIT 5` as postgres.RowList; + + const latestInvoices = data.map((invoice) => ({ + ...invoice, + amount: formatCurrency(+invoice.amount), + })); + return latestInvoices; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch the latest invoices.'); + } +} + +export async function fetchCardData() { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + + try { + // You can probably combine these into a single SQL query + // However, we are intentionally splitting them to demonstrate + // how to initialize multiple queries in parallel with JS. + const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`; + const customerCountPromise = sql`SELECT COUNT(*) FROM customers`; + const invoiceStatusPromise = sql`SELECT + SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid", + SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending" + FROM invoices`; + + const data = await Promise.all([ + invoiceCountPromise, + customerCountPromise, + invoiceStatusPromise, + ]); + + const invoices = data[0] as postgres.RowList + const customres = data[1] as postgres.RowList + const invoiceStatus = data[2] as postgres.RowList<({paid: string, pending: string})[]> + const numberOfInvoices = Number(invoices.count ?? '0'); + const numberOfCustomers = Number(customres.count ?? '0'); + const totalPaidInvoices = formatCurrency(Number(invoiceStatus.at(0)?.paid ?? '0')); + const totalPendingInvoices = formatCurrency(Number(invoiceStatus.at(0)?.pending ?? '0')); + + return { + numberOfCustomers, + numberOfInvoices, + totalPaidInvoices, + totalPendingInvoices, + }; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch card data.'); + } +} + +const ITEMS_PER_PAGE = 6; +export async function fetchFilteredInvoices( + query: string, + currentPage: number, +) { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + + const offset = (currentPage - 1) * ITEMS_PER_PAGE; + + try { + const invoices = await sql` + SELECT + invoices.id, + invoices.amount, + invoices.date, + invoices.status, + customers.name, + customers.email, + customers.image_url + FROM invoices + JOIN customers ON invoices.customer_id = customers.id + WHERE + customers.name ILIKE ${`%${query}%`} OR + customers.email ILIKE ${`%${query}%`} OR + invoices.amount::text ILIKE ${`%${query}%`} OR + invoices.date::text ILIKE ${`%${query}%`} OR + invoices.status ILIKE ${`%${query}%`} + ORDER BY invoices.date DESC + LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset} + ` as postgres.RowList; + + return invoices; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch invoices.'); + } +} + +export async function fetchInvoicesPages(query: string) { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + try { + const count = await sql`SELECT COUNT(*) + FROM invoices + JOIN customers ON invoices.customer_id = customers.id + WHERE + customers.name ILIKE ${`%${query}%`} OR + customers.email ILIKE ${`%${query}%`} OR + invoices.amount::text ILIKE ${`%${query}%`} OR + invoices.date::text ILIKE ${`%${query}%`} OR + invoices.status ILIKE ${`%${query}%`} + `; + + const totalPages = Math.ceil(Number(count.at(0)?.count) / ITEMS_PER_PAGE); + return totalPages; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch total number of invoices.'); + } +} + +export async function fetchInvoiceById(id: string) { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + try { + const data = await sql` + SELECT + invoices.id, + invoices.customer_id, + invoices.amount, + invoices.status + FROM invoices + WHERE invoices.id = ${id}; + ` as postgres.RowList; + + const invoice = data.map((invoice) => ({ + ...invoice, + // Convert amount from cents to dollars + amount: invoice.amount / 100, + })); + + return invoice[0]; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch invoice.'); + } +} + +export async function fetchCustomers() { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + try { + const data = await sql` + SELECT + id, + name + FROM customers + ORDER BY name ASC + ` as postgres.RowList; + + return data; + } catch (err) { + console.error('Database Error:', err); + throw new Error('Failed to fetch all customers.'); + } +} + +export async function fetchFilteredCustomers(query: string) { + // This is equivalent to in fetch(..., {cache: 'no-store'}). + connection() + try { + const data = await sql` + SELECT + customers.id, + customers.name, + customers.email, + customers.image_url, + COUNT(invoices.id) AS total_invoices, + SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending, + SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid + FROM customers + LEFT JOIN invoices ON customers.id = invoices.customer_id + WHERE + customers.name ILIKE ${`%${query}%`} OR + customers.email ILIKE ${`%${query}%`} + GROUP BY customers.id, customers.name, customers.email, customers.image_url + ORDER BY customers.name ASC + ` as postgres.RowList; + + const customers = data.map((customer) => ({ + ...customer, + total_pending: formatCurrency(customer.total_pending), + total_paid: formatCurrency(customer.total_paid), + })); + + return customers; + } catch (err) { + console.error('Database Error:', err); + throw new Error('Failed to fetch customer table.'); + } +} + +export async function getUser(email: string) { + try { + const user = await sql`SELECT * FROM users WHERE email=${email}`; + return user.at(0) as User; + } catch (error) { + console.error('Failed to fetch user:', error); + throw new Error('Failed to fetch user.'); + } +} diff --git a/src/app/lib/definitions.ts b/src/app/lib/definitions.ts new file mode 100644 index 0000000..e439b66 --- /dev/null +++ b/src/app/lib/definitions.ts @@ -0,0 +1,86 @@ +// This file contains type definitions for your data. +// It describes the shape of the data, and what data type each property should accept. +// For simplicity of teaching, we're manually defining these types. +// However, these types are generated automatically if you're using an ORM such as Prisma. +export type User = { + id: string; + name: string; + email: string; + password: string; +}; + +export type Customer = { + id: string; + name: string; + email: string; + image_url: string; +}; + +export type Invoice = { + id: string; // Will be created on the database + customer_id: string; + amount: number; // Stored in cents + status: 'pending' | 'paid'; + date: string; +}; + +export type Revenue = { + month: string; + revenue: number; +}; + +export type LatestInvoice = { + id: string; + name: string; + image_url: string; + email: string; + amount: string; +}; + +// The database returns a number for amount, but we later format it to a string with the formatCurrency function +export type LatestInvoiceRaw = Omit & { + amount: number; +}; + +export type InvoicesTable = { + id: string; + customer_id: string; + name: string; + email: string; + image_url: string; + date: string; + amount: number; + status: 'pending' | 'paid'; +}; + +export type CustomersTableType = { + id: string; + name: string; + email: string; + image_url: string; + total_invoices: number; + total_pending: number; + total_paid: number; +}; + +export type FormattedCustomersTable = { + id: string; + name: string; + email: string; + image_url: string; + total_invoices: number; + total_pending: string; + total_paid: string; +}; + +export type CustomerField = { + id: string; + name: string; +}; + +export type InvoiceForm = { + id: string; + customer_id: string; + amount: number; + status: 'pending' | 'paid'; +}; diff --git a/src/app/lib/placeholder-data.js b/src/app/lib/placeholder-data.js new file mode 100644 index 0000000..15a4156 --- /dev/null +++ b/src/app/lib/placeholder-data.js @@ -0,0 +1,188 @@ +// This file contains placeholder data that you'll be replacing with real data in the Data Fetching chapter: +// https://nextjs.org/learn/dashboard-app/fetching-data +const users = [ + { + id: '410544b2-4001-4271-9855-fec4b6a6442a', + name: 'User', + email: 'user@nextmail.com', + password: '123456', + }, +]; + +const customers = [ + { + id: '3958dc9e-712f-4377-85e9-fec4b6a6442a', + name: 'Delba de Oliveira', + email: 'delba@oliveira.com', + image_url: '/customers/delba-de-oliveira.png', + }, + { + id: '3958dc9e-742f-4377-85e9-fec4b6a6442a', + name: 'Lee Robinson', + email: 'lee@robinson.com', + image_url: '/customers/lee-robinson.png', + }, + { + id: '3958dc9e-737f-4377-85e9-fec4b6a6442a', + name: 'Hector Simpson', + email: 'hector@simpson.com', + image_url: '/customers/hector-simpson.png', + }, + { + id: '50ca3e18-62cd-11ee-8c99-0242ac120002', + name: 'Steven Tey', + email: 'steven@tey.com', + image_url: '/customers/steven-tey.png', + }, + { + id: '3958dc9e-787f-4377-85e9-fec4b6a6442a', + name: 'Steph Dietz', + email: 'steph@dietz.com', + image_url: '/customers/steph-dietz.png', + }, + { + id: '76d65c26-f784-44a2-ac19-586678f7c2f2', + name: 'Michael Novotny', + email: 'michael@novotny.com', + image_url: '/customers/michael-novotny.png', + }, + { + id: 'd6e15727-9fe1-4961-8c5b-ea44a9bd81aa', + name: 'Evil Rabbit', + email: 'evil@rabbit.com', + image_url: '/customers/evil-rabbit.png', + }, + { + id: '126eed9c-c90c-4ef6-a4a8-fcf7408d3c66', + name: 'Emil Kowalski', + email: 'emil@kowalski.com', + image_url: '/customers/emil-kowalski.png', + }, + { + id: 'CC27C14A-0ACF-4F4A-A6C9-D45682C144B9', + name: 'Amy Burns', + email: 'amy@burns.com', + image_url: '/customers/amy-burns.png', + }, + { + id: '13D07535-C59E-4157-A011-F8D2EF4E0CBB', + name: 'Balazs Orban', + email: 'balazs@orban.com', + image_url: '/customers/balazs-orban.png', + }, +]; + +const invoices = [ + { + customer_id: customers[0].id, + amount: 15795, + status: 'pending', + date: '2022-12-06', + }, + { + customer_id: customers[1].id, + amount: 20348, + status: 'pending', + date: '2022-11-14', + }, + { + customer_id: customers[4].id, + amount: 3040, + status: 'paid', + date: '2022-10-29', + }, + { + customer_id: customers[3].id, + amount: 44800, + status: 'paid', + date: '2023-09-10', + }, + { + customer_id: customers[5].id, + amount: 34577, + status: 'pending', + date: '2023-08-05', + }, + { + customer_id: customers[7].id, + amount: 54246, + status: 'pending', + date: '2023-07-16', + }, + { + customer_id: customers[6].id, + amount: 666, + status: 'pending', + date: '2023-06-27', + }, + { + customer_id: customers[3].id, + amount: 32545, + status: 'paid', + date: '2023-06-09', + }, + { + customer_id: customers[4].id, + amount: 1250, + status: 'paid', + date: '2023-06-17', + }, + { + customer_id: customers[5].id, + amount: 8546, + status: 'paid', + date: '2023-06-07', + }, + { + customer_id: customers[1].id, + amount: 500, + status: 'paid', + date: '2023-08-19', + }, + { + customer_id: customers[5].id, + amount: 8945, + status: 'paid', + date: '2023-06-03', + }, + { + customer_id: customers[2].id, + amount: 8945, + status: 'paid', + date: '2023-06-18', + }, + { + customer_id: customers[0].id, + amount: 8945, + status: 'paid', + date: '2023-10-04', + }, + { + customer_id: customers[2].id, + amount: 1000, + status: 'paid', + date: '2022-06-05', + }, +]; + +const revenue = [ + { month: 'Jan', revenue: 2000 }, + { month: 'Feb', revenue: 1800 }, + { month: 'Mar', revenue: 2200 }, + { month: 'Apr', revenue: 2500 }, + { month: 'May', revenue: 2300 }, + { month: 'Jun', revenue: 3200 }, + { month: 'Jul', revenue: 3500 }, + { month: 'Aug', revenue: 3700 }, + { month: 'Sep', revenue: 2500 }, + { month: 'Oct', revenue: 2800 }, + { month: 'Nov', revenue: 3000 }, + { month: 'Dec', revenue: 4800 }, +]; + +module.exports = { + users, + customers, + invoices, + revenue, +}; diff --git a/src/app/lib/utils.ts b/src/app/lib/utils.ts new file mode 100644 index 0000000..b7f7cff --- /dev/null +++ b/src/app/lib/utils.ts @@ -0,0 +1,69 @@ +import { Revenue } from './definitions'; + +export const formatCurrency = (amount: number) => { + return (amount / 100).toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + }); +}; + +export const formatDateToLocal = ( + dateStr: string, + locale: string = 'en-US', +) => { + const date = new Date(dateStr); + const options: Intl.DateTimeFormatOptions = { + day: 'numeric', + month: 'short', + year: 'numeric', + }; + const formatter = new Intl.DateTimeFormat(locale, options); + return formatter.format(date); +}; + +export const generateYAxis = (revenue: Revenue[]) => { + // Calculate what labels we need to display on the y-axis + // based on highest record and in 1000s + const yAxisLabels = []; + const highestRecord = Math.max(...revenue.map((month) => month.revenue)); + const topLabel = Math.ceil(highestRecord / 1000) * 1000; + + for (let i = topLabel; i >= 0; i -= 1000) { + yAxisLabels.push(`$${i / 1000}K`); + } + + return { yAxisLabels, topLabel }; +}; + +export const generatePagination = (currentPage: number, totalPages: number) => { + // If the total number of pages is 7 or less, + // display all pages without any ellipsis. + if (totalPages <= 7) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + + // If the current page is among the first 3 pages, + // show the first 3, an ellipsis, and the last 2 pages. + if (currentPage <= 3) { + return [1, 2, 3, '...', totalPages - 1, totalPages]; + } + + // If the current page is among the last 3 pages, + // show the first 2, an ellipsis, and the last 3 pages. + if (currentPage >= totalPages - 2) { + return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages]; + } + + // If the current page is somewhere in the middle, + // show the first page, an ellipsis, the current page and its neighbors, + // another ellipsis, and the last page. + return [ + 1, + '...', + currentPage - 1, + currentPage, + currentPage + 1, + '...', + totalPages, + ]; +}; diff --git a/src/bootstrap/db/db.ts b/src/bootstrap/db/db.ts new file mode 100644 index 0000000..dbcd877 --- /dev/null +++ b/src/bootstrap/db/db.ts @@ -0,0 +1,13 @@ +import postgres from "postgres"; + + +const envs = process.env; +const dbConfigs = { + host: envs.POSTGRES_HOST, + port: Number(envs.POSTGRES_PORT), + username: envs.POSTGRES_USER, + password: envs.POSTGRES_PASS, + database: envs.POSTGRES_DB, +} + +export const sql = postgres(dbConfigs); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9b2101a..ee61d15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46,6 +46,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@heroicons/react@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.1.5.tgz#1e13f34976cc542deae92353c01c8b3d7942e9ba" + integrity sha512-FuzFN+BsHa+7OxbvAERtgBTNeZpUjgM/MIizfVkSCL2/edriN0Hx/DWRCR//aPYwO5QX/YlgLGXk+E3PcfZwjA== + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -3161,3 +3166,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==