mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-11 09:51:47 +00:00
Add code for chapters 7-8 (#164)
* Rename file and add data fetches for overview page * Rename calculations.ts to utils.ts * Update code to match course * Add temporary calculation * Fix error * Move types to data fetching file * Add error handling * Parallelize data fetches * Add skeletons * Add delayed data request * Fix ts errors * Code for chapter 8 * Fix error * Clean up --------- Co-authored-by: Stephanie Dietz <49788645+StephDietz@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
2ccc7c6461
commit
24bcb816e5
@@ -1,7 +1,7 @@
|
||||
import InvoiceForm from '@/app/ui/invoices/form';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { Invoice } from '@/app/lib/definitions';
|
||||
import { fetchInvoiceById } from '@/app/lib/data-fetches';
|
||||
import { fetchInvoiceById } from '@/app/lib/data';
|
||||
|
||||
export default async function Page({ params }: { params: { id: string } }) {
|
||||
const id = params.id ? parseInt(params.id) : null;
|
||||
|
||||
5
dashboard/15-final/app/dashboard/loading.tsx
Normal file
5
dashboard/15-final/app/dashboard/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import DashboardSkeleton from '@/app/ui/skeletons';
|
||||
|
||||
export default function Loading() {
|
||||
return <DashboardSkeleton />;
|
||||
}
|
||||
@@ -1,9 +1,40 @@
|
||||
import DashboardOverview from '@/app/ui/dashboard/overview';
|
||||
import Card from '@/app/ui/dashboard/card';
|
||||
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
|
||||
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
|
||||
import {
|
||||
fetchLatestInvoices,
|
||||
fetchCounts,
|
||||
fetchTotalAmountByStatus,
|
||||
} from '@/app/lib/data';
|
||||
import { Suspense } from 'react';
|
||||
import { RevenueChartSkeleton } from '@/app/ui/skeletons';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function Page() {
|
||||
const latestInvoices = await fetchLatestInvoices();
|
||||
const { numberOfInvoices, numberOfCustomers } = await fetchCounts();
|
||||
const { totalPaidInvoices, totalPendingInvoices } =
|
||||
await fetchTotalAmountByStatus();
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main>
|
||||
<DashboardOverview />
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Card title="Collected" value={totalPaidInvoices} type="collected" />
|
||||
<Card title="Pending" value={totalPendingInvoices} type="pending" />
|
||||
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
|
||||
<Card
|
||||
title="Total Customers"
|
||||
value={numberOfCustomers}
|
||||
type="customers"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
|
||||
<Suspense fallback={<RevenueChartSkeleton />}>
|
||||
<RevenueChart />
|
||||
</Suspense>
|
||||
<LatestInvoices latestInvoices={latestInvoices} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { sql } from '@vercel/postgres';
|
||||
|
||||
export async function fetchAllInvoices() {
|
||||
const invoicesData = await sql`SELECT * FROM invoices`;
|
||||
return invoicesData.rows;
|
||||
}
|
||||
|
||||
export async function fetchAllCustomers() {
|
||||
const customersData = await sql`SELECT * FROM customers`;
|
||||
return customersData.rows;
|
||||
}
|
||||
|
||||
export async function fetchAllRevenue() {
|
||||
const revenueData = await sql`SELECT * FROM revenue`;
|
||||
return revenueData.rows;
|
||||
}
|
||||
|
||||
export async function fetchFilteredInvoices(
|
||||
searchTerm: string,
|
||||
currentPage: number,
|
||||
ITEMS_PER_PAGE: number,
|
||||
) {
|
||||
const invoicesData = await sql`
|
||||
SELECT
|
||||
invoices.*,
|
||||
customers.name AS customer_name,
|
||||
customers.email AS customer_email,
|
||||
customers.image_url AS customer_image
|
||||
FROM
|
||||
invoices
|
||||
JOIN
|
||||
customers ON invoices.customer_id = customers.id
|
||||
WHERE
|
||||
invoices.id::text ILIKE ${`%${searchTerm}%`} OR
|
||||
customers.name ILIKE ${`%${searchTerm}%`} OR
|
||||
customers.email ILIKE ${`%${searchTerm}%`} OR
|
||||
invoices.amount::text ILIKE ${`%${searchTerm}%`} OR
|
||||
invoices.date::text ILIKE ${`%${searchTerm}%`} OR
|
||||
invoices.status ILIKE ${`%${searchTerm}%`}
|
||||
LIMIT ${ITEMS_PER_PAGE}
|
||||
OFFSET ${(currentPage - 1) * ITEMS_PER_PAGE}
|
||||
`;
|
||||
return invoicesData.rows;
|
||||
}
|
||||
|
||||
export async function fetchInvoiceCountBySearchTerm(searchTerm: string) {
|
||||
const { rows: countRows } = await sql`
|
||||
SELECT COUNT(*)
|
||||
FROM invoices
|
||||
LEFT JOIN customers ON invoices.customer_id = customers.id
|
||||
WHERE (invoices.id::text ILIKE ${`%${searchTerm}%`} OR customers.name ILIKE ${`%${searchTerm}%`} OR customers.email ILIKE ${`%${searchTerm}%`})
|
||||
`;
|
||||
return countRows[0].count;
|
||||
}
|
||||
|
||||
export async function fetchInvoiceById(id: number | null) {
|
||||
return await sql`SELECT * from INVOICES where id=${id}`;
|
||||
}
|
||||
|
||||
export async function fetchLatestInvoices() {
|
||||
const latestInvoices = await sql`SELECT * FROM invoices
|
||||
ORDER BY date DESC
|
||||
LIMIT 5;`;
|
||||
return latestInvoices.rows;
|
||||
}
|
||||
132
dashboard/15-final/app/lib/data.ts
Normal file
132
dashboard/15-final/app/lib/data.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { formatCurrency } from './utils';
|
||||
import { Revenue, LatestInvoice } from './definitions';
|
||||
|
||||
export async function fetchRevenue(): Promise<Revenue[]> {
|
||||
try {
|
||||
const revenueData = await sql`SELECT * FROM revenue`;
|
||||
return revenueData.rows as Revenue[];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch revenue data:', error);
|
||||
throw new Error('Failed to fetch revenue data.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRevenueDelayed(): Promise<Revenue[]> {
|
||||
try {
|
||||
// We artificially delay a reponse for demo purposes.
|
||||
// Don't do this in real life :)
|
||||
console.log('Fetching revenue data...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
const revenueData = await sql`SELECT * FROM revenue`;
|
||||
console.log('Data fetch complete after 3 seconds.');
|
||||
|
||||
return revenueData.rows as Revenue[];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch revenue data:', error);
|
||||
throw new Error('Failed to fetch revenue data.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCounts() {
|
||||
try {
|
||||
const invoiceCount = await sql`SELECT COUNT(*) FROM invoices`;
|
||||
const numberOfInvoices = parseInt(invoiceCount.rows[0].count, 10);
|
||||
|
||||
const customerCount = await sql`SELECT COUNT(*) FROM customers`;
|
||||
const numberOfCustomers = parseInt(customerCount.rows[0].count, 10);
|
||||
|
||||
return { numberOfCustomers, numberOfInvoices };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch counts:', error);
|
||||
throw new Error('Failed to fetch counts.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTotalAmountByStatus() {
|
||||
try {
|
||||
const totalAmount = await 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 totalPaidInvoices = formatCurrency(totalAmount.rows[0].paid);
|
||||
const totalPendingInvoices = formatCurrency(totalAmount.rows[0].pending);
|
||||
|
||||
return { totalPaidInvoices, totalPendingInvoices };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch total amounts by status:', error);
|
||||
throw new Error('Failed to fetch total amounts by status.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLatestInvoices() {
|
||||
try {
|
||||
const data = await sql`
|
||||
SELECT invoices.amount, customers.name, customers.image_url, customers.email
|
||||
FROM invoices
|
||||
JOIN customers ON invoices.customer_id = customers.id
|
||||
ORDER BY invoices.date DESC
|
||||
LIMIT 5`;
|
||||
const latestInvoices = data.rows.map((invoice) => ({
|
||||
...invoice,
|
||||
amount: formatCurrency(invoice.amount),
|
||||
}));
|
||||
return latestInvoices as LatestInvoice[];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch the latest invoices:', error);
|
||||
throw new Error('Failed to fetch the latest invoices.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllInvoices() {
|
||||
const invoicesData = await sql`SELECT * FROM invoices`;
|
||||
return invoicesData.rows;
|
||||
}
|
||||
|
||||
export async function fetchAllCustomers() {
|
||||
const customersData = await sql`SELECT * FROM customers`;
|
||||
return customersData.rows;
|
||||
}
|
||||
|
||||
export async function fetchFilteredInvoices(
|
||||
searchTerm: string,
|
||||
currentPage: number,
|
||||
ITEMS_PER_PAGE: number,
|
||||
) {
|
||||
const invoicesData = await sql`
|
||||
SELECT
|
||||
invoices.*,
|
||||
customers.name AS customer_name,
|
||||
customers.email AS customer_email,
|
||||
customers.image_url AS customer_image
|
||||
FROM
|
||||
invoices
|
||||
JOIN
|
||||
customers ON invoices.customer_id = customers.id
|
||||
WHERE
|
||||
invoices.id::text ILIKE ${`%${searchTerm}%`} OR
|
||||
customers.name ILIKE ${`%${searchTerm}%`} OR
|
||||
customers.email ILIKE ${`%${searchTerm}%`} OR
|
||||
invoices.amount::text ILIKE ${`%${searchTerm}%`} OR
|
||||
invoices.date::text ILIKE ${`%${searchTerm}%`} OR
|
||||
invoices.status ILIKE ${`%${searchTerm}%`}
|
||||
LIMIT ${ITEMS_PER_PAGE}
|
||||
OFFSET ${(currentPage - 1) * ITEMS_PER_PAGE}
|
||||
`;
|
||||
return invoicesData.rows;
|
||||
}
|
||||
|
||||
export async function fetchInvoiceCountBySearchTerm(searchTerm: string) {
|
||||
const { rows: countRows } = await sql`
|
||||
SELECT COUNT(*)
|
||||
FROM invoices
|
||||
LEFT JOIN customers ON invoices.customer_id = customers.id
|
||||
WHERE (invoices.id::text ILIKE ${`%${searchTerm}%`} OR customers.name ILIKE ${`%${searchTerm}%`} OR customers.email ILIKE ${`%${searchTerm}%`})
|
||||
`;
|
||||
return countRows[0].count;
|
||||
}
|
||||
|
||||
export async function fetchInvoiceById(id: number | null) {
|
||||
return await sql`SELECT * from INVOICES where id=${id}`;
|
||||
}
|
||||
@@ -25,6 +25,14 @@ export type Invoice = {
|
||||
date: string;
|
||||
};
|
||||
|
||||
export type LatestInvoice = {
|
||||
id: number;
|
||||
name: string;
|
||||
image_url: string;
|
||||
email: string;
|
||||
amount: string;
|
||||
};
|
||||
|
||||
export type Revenue = {
|
||||
month: string;
|
||||
revenue: number;
|
||||
|
||||
@@ -1,7 +1,43 @@
|
||||
import { Invoice, Revenue } from './definitions';
|
||||
import { fetchLatestInvoices } from './data-fetches';
|
||||
import { Invoice, Revenue, Customer, LatestInvoice } from './definitions';
|
||||
|
||||
export const calculateAllInvoices = (
|
||||
export const formatCurrency = (amount: number) => {
|
||||
return (amount / 100).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
});
|
||||
};
|
||||
|
||||
export function findLatestInvoices(invoices: Invoice[], customers: Customer[]) {
|
||||
// Sort the invoices by date in descending order and take the top 5
|
||||
const latestInvoices = [...invoices]
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.slice(0, 5);
|
||||
|
||||
// Find corresponding customers for the latest 5 invoices
|
||||
const latestInvoicesWithCustomerInfo = latestInvoices.map((invoice) => {
|
||||
const customer = customers.find(
|
||||
(customer) => customer.id === invoice.customer_id,
|
||||
);
|
||||
|
||||
// Format the amount to USD
|
||||
const formattedAmount = invoice.amount.toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
});
|
||||
|
||||
return {
|
||||
id: invoice.id,
|
||||
name: customer?.name,
|
||||
image_url: customer?.image_url,
|
||||
email: customer?.email,
|
||||
amount: formattedAmount,
|
||||
};
|
||||
});
|
||||
|
||||
return latestInvoicesWithCustomerInfo;
|
||||
}
|
||||
|
||||
export const calculateInvoicesByStatus = (
|
||||
invoices: Invoice[],
|
||||
status: 'pending' | 'paid',
|
||||
) => {
|
||||
@@ -29,12 +65,6 @@ export const calculateCustomerInvoices = (
|
||||
});
|
||||
};
|
||||
|
||||
// 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,
|
||||
@@ -43,10 +73,6 @@ export const countCustomerInvoices = (
|
||||
.length;
|
||||
};
|
||||
|
||||
export const findLatestInvoices = async () => {
|
||||
return await fetchLatestInvoices();
|
||||
};
|
||||
|
||||
export const generateYAxis = (revenue: Revenue[]) => {
|
||||
// Calculate what labels we need to display on the y-axis
|
||||
// based on highest record and in 1000s
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
countCustomerInvoices,
|
||||
calculateCustomerInvoices,
|
||||
} from '@/app/lib/calculations';
|
||||
} from '@/app/lib/utils';
|
||||
import { Customer, Invoice } from '@/app/lib/definitions';
|
||||
import { fetchAllCustomers, fetchAllInvoices } from '@/app/lib/data-fetches';
|
||||
import { fetchAllCustomers, fetchAllInvoices } from '@/app/lib/data';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default async function CustomersTable() {
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
// InvoiceList.tsx
|
||||
import { Customer, Invoice } from '@/app/lib/definitions';
|
||||
import { findLatestInvoices } from '@/app/lib/calculations';
|
||||
import { LatestInvoice } from '@/app/lib/definitions';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default async function LatestInvoices({
|
||||
invoices,
|
||||
customers,
|
||||
latestInvoices,
|
||||
}: {
|
||||
invoices: Invoice[];
|
||||
customers: Customer[];
|
||||
latestInvoices: LatestInvoice[];
|
||||
}) {
|
||||
const lastFiveInvoices = await findLatestInvoices();
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-xl border p-6 shadow-sm md:col-span-4 lg:col-span-3">
|
||||
<div className="w-full rounded-xl border bg-white p-6 shadow-sm md:col-span-4 lg:col-span-3">
|
||||
<h2 className="font-semibold">Latest Invoices</h2>
|
||||
|
||||
{lastFiveInvoices.map((invoice) => {
|
||||
const customer = customers.find(
|
||||
(customer) => customer.id === invoice.customer_id,
|
||||
);
|
||||
{latestInvoices.map((invoice) => {
|
||||
return (
|
||||
<div
|
||||
key={invoice.id}
|
||||
@@ -27,26 +18,23 @@ export default async function LatestInvoices({
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Image
|
||||
src={customer?.image_url || ''}
|
||||
alt={customer?.name || ''}
|
||||
src={invoice.image_url}
|
||||
alt={invoice.name}
|
||||
className="mr-4 rounded-full"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold md:text-base">
|
||||
{customer?.name}
|
||||
{invoice.name}
|
||||
</p>
|
||||
<p className="hidden text-sm text-gray-500 sm:block">
|
||||
{customer?.email}
|
||||
{invoice.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="truncate text-sm font-medium md:text-base">
|
||||
{(invoice.amount / 100).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
})}
|
||||
{invoice.amount}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import Card from '@/app/ui/dashboard/card';
|
||||
import { calculateAllInvoices } from '@/app/lib/calculations';
|
||||
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
|
||||
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
|
||||
import { Customer, Invoice, Revenue } from '@/app/lib/definitions';
|
||||
import {
|
||||
fetchAllCustomers,
|
||||
fetchAllInvoices,
|
||||
fetchAllRevenue,
|
||||
} from '@/app/lib/data-fetches';
|
||||
|
||||
export default async function DashboardOverview() {
|
||||
const invoices = (await fetchAllInvoices()) as Invoice[];
|
||||
const customers = (await fetchAllCustomers()) as Customer[];
|
||||
const revenue = (await fetchAllRevenue()) as Revenue[];
|
||||
|
||||
const totalPaidInvoices = calculateAllInvoices(invoices, 'paid');
|
||||
const totalPendingInvoices = calculateAllInvoices(invoices, 'pending');
|
||||
const numberOfInvoices = invoices.length;
|
||||
const numberOfCustomers = customers.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Card title="Collected" value={totalPaidInvoices} type="collected" />
|
||||
<Card title="Pending" value={totalPendingInvoices} type="pending" />
|
||||
<Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
|
||||
<Card
|
||||
title="Total Customers"
|
||||
value={numberOfCustomers}
|
||||
type="customers"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
|
||||
<RevenueChart revenue={revenue} />
|
||||
<LatestInvoices invoices={invoices} customers={customers} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Revenue } from '@/app/lib/definitions';
|
||||
import { generateYAxis } from '@/app/lib/calculations';
|
||||
import { generateYAxis } from '@/app/lib/utils';
|
||||
import { fetchRevenueDelayed } from '@/app/lib/data';
|
||||
|
||||
// This component is representational only.
|
||||
// For data visualization UI, check out:
|
||||
// https://www.tremor.so/
|
||||
// https://www.chartjs.org/
|
||||
// https://airbnb.io/visx/
|
||||
// https://www.tremor.so/
|
||||
export default function RevenueChart({ revenue }: { revenue: Revenue[] }) {
|
||||
|
||||
export default async function RevenueChart() {
|
||||
const revenue = await fetchRevenueDelayed();
|
||||
|
||||
const chartHeight = 350;
|
||||
const { yAxisLabels, topLabel } = generateYAxis(revenue);
|
||||
|
||||
@@ -15,7 +18,7 @@ export default function RevenueChart({ revenue }: { revenue: Revenue[] }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border p-6 shadow-sm md:col-span-5">
|
||||
<div className="rounded-xl border bg-white p-6 shadow-sm md:col-span-5">
|
||||
<h2 className="font-semibold">Revenue</h2>
|
||||
<div className="sm:grid-cols-13 mt-4 grid grid-cols-12 items-end gap-2 md:gap-4">
|
||||
{/* y-axis */}
|
||||
|
||||
@@ -11,7 +11,7 @@ import PaginationButtons from './pagination';
|
||||
import {
|
||||
fetchFilteredInvoices,
|
||||
fetchInvoiceCountBySearchTerm,
|
||||
} from '@/app/lib/data-fetches';
|
||||
} from '@/app/lib/data';
|
||||
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
|
||||
|
||||
90
dashboard/15-final/app/ui/skeletons.tsx
Normal file
90
dashboard/15-final/app/ui/skeletons.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
// Loading animation
|
||||
const shimmer =
|
||||
'before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent';
|
||||
|
||||
export function CardSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className={`${shimmer} relative h-[114px] overflow-hidden rounded-xl border bg-white p-6 shadow-sm`}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<div className="h-6 w-20 rounded-lg bg-gray-100"></div>
|
||||
<div className="h-6 w-6 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="mt-4 h-8 w-1/2 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RevenueChartSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className={`${shimmer} relative h-[464px] overflow-hidden rounded-xl border bg-white p-6 shadow-sm md:col-span-5`}
|
||||
>
|
||||
<div className="h-8 w-32 rounded-lg bg-gray-100"></div>
|
||||
<div className="my-4 h-[370px] w-full rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LatestInvoicesSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className={`${shimmer} relative w-full overflow-hidden rounded-xl border bg-white p-6 shadow-sm md:col-span-4 lg:col-span-3`}
|
||||
>
|
||||
<div className="h-8 w-40 rounded-lg bg-gray-100"></div>
|
||||
<div className="mt-8 flex flex-row items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-gray-100"></div>
|
||||
<div className="ml-4 h-10 w-44 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="h-10 w-16 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-gray-100"></div>
|
||||
<div className="ml-4 h-10 w-44 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="h-10 w-16 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-gray-100"></div>
|
||||
<div className="ml-4 h-10 w-44 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="h-10 w-16 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-gray-100"></div>
|
||||
<div className="ml-4 h-10 w-44 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="h-10 w-16 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 rounded-lg bg-gray-100"></div>
|
||||
<div className="ml-4 h-10 w-44 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
<div className="h-10 w-16 rounded-lg bg-gray-100"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
|
||||
<RevenueChartSkeleton />
|
||||
<LatestInvoicesSkeleton />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,15 @@ const config: Config = {
|
||||
'13': 'repeat(13, minmax(0, 1fr))',
|
||||
},
|
||||
},
|
||||
keyframes: {
|
||||
shimmer: {
|
||||
'100%': {
|
||||
transform: 'translateX(100%)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
};
|
||||
export default config;
|
||||
|
||||
Reference in New Issue
Block a user