This commit is contained in:
2026-05-12 17:35:38 +03:00
parent d9ffcb4b92
commit e3f3e62482
51 changed files with 882 additions and 3413 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import { GlobeAltIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { GlobeAltIcon } from "@heroicons/react/24/outline";
import { lusitana } from "@/app/ui/fonts";
export default function AcmeLogo() {
return (
+2 -2
View File
@@ -1,4 +1,4 @@
import clsx from 'clsx';
import clsx from "clsx";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
@@ -9,7 +9,7 @@ export function Button({ children, className, ...rest }: ButtonProps) {
<button
{...rest}
className={clsx(
'flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:bg-blue-600 aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
"flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:bg-blue-600 aria-disabled:cursor-not-allowed aria-disabled:opacity-50",
className,
)}
>
+4 -4
View File
@@ -1,7 +1,7 @@
import Image from 'next/image';
import { lusitana } from '@/app/ui/fonts';
import Search from '../search';
import { CustomersTable, FormattedCustomersTable } from '@/app/lib/definitions';
import Image from "next/image";
import type { FormattedCustomersTable } from "@/app/lib/definitions";
import { lusitana } from "@/app/ui/fonts";
import Search from "../search";
export default async function customersTable({
customers,
}: {
+6 -6
View File
@@ -1,11 +1,11 @@
import {
BanknotesIcon,
ClockIcon,
UserGroupIcon,
InboxIcon,
} from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { fetchCardData } from '@/app/lib/data';
UserGroupIcon,
} from "@heroicons/react/24/outline";
import { fetchCardData } from "@/app/lib/data";
import { lusitana } from "@/app/ui/fonts";
const iconMap = {
collected: BanknotesIcon,
@@ -21,7 +21,7 @@ export default async function CardWrapper() {
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
return (
<>
<Card title="Collected" value={totalPaidInvoices} type="collected" />
@@ -43,7 +43,7 @@ export function Card({
}: {
title: string;
value: number | string;
type: 'invoices' | 'customers' | 'pending' | 'collected';
type: "invoices" | "customers" | "pending" | "collected";
}) {
const Icon = iconMap[type];
+46 -44
View File
@@ -1,11 +1,11 @@
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Image from 'next/image';
import { lusitana } from '@/app/ui/fonts';
import { LatestInvoice } from '@/app/lib/definitions';
import { fetchLatestInvoices } from '@/app/lib/data';
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import Image from "next/image";
import { fetchLatestInvoices } from "@/app/lib/data";
import { lusitana } from "@/app/ui/fonts";
export default async function LatestInvoices() { // Remove props
export default async function LatestInvoices() {
// Remove props
const latestInvoices = await fetchLatestInvoices();
return (
@@ -16,44 +16,46 @@ export default async function LatestInvoices() { // Remove props
<div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4">
{/* NOTE: comment in this code when you get to this point in the course */}
{ <div className="bg-white px-6">
{latestInvoices.map((invoice, i) => {
return (
<div
key={invoice.id}
className={clsx(
'flex flex-row items-center justify-between py-4',
{
'border-t': i !== 0,
},
)}
>
<div className="flex items-center">
<Image
src={invoice.image_url}
alt={`${invoice.name}'s profile picture`}
className="mr-4 rounded-full"
width={32}
height={32}
/>
<div className="min-w-0">
<p className="truncate text-sm font-semibold md:text-base">
{invoice.name}
</p>
<p className="hidden text-sm text-gray-500 sm:block">
{invoice.email}
</p>
</div>
</div>
<p
className={`${lusitana.className} truncate text-sm font-medium md:text-base`}
{
<div className="bg-white px-6">
{latestInvoices.map((invoice, i) => {
return (
<div
key={invoice.id}
className={clsx(
"flex flex-row items-center justify-between py-4",
{
"border-t": i !== 0,
},
)}
>
{invoice.amount}
</p>
</div>
);
})}
</div> }
<div className="flex items-center">
<Image
src={invoice.image_url}
alt={`${invoice.name}'s profile picture`}
className="mr-4 rounded-full"
width={32}
height={32}
/>
<div className="min-w-0">
<p className="truncate text-sm font-semibold md:text-base">
{invoice.name}
</p>
<p className="hidden text-sm text-gray-500 sm:block">
{invoice.email}
</p>
</div>
</div>
<p
className={`${lusitana.className} truncate text-sm font-medium md:text-base`}
>
{invoice.amount}
</p>
</div>
);
})}
</div>
}
<div className="flex items-center pb-2 pt-6">
<ArrowPathIcon className="h-5 w-5 text-gray-500" />
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
+15 -15
View File
@@ -1,29 +1,29 @@
'use client';
"use client";
import {
UserGroupIcon,
HomeIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
HomeIcon,
UserGroupIcon,
} from "@heroicons/react/24/outline";
import clsx from "clsx";
import Link from "next/link";
import { usePathname } from "next/navigation";
// Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database.
const links = [
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
{ name: "Home", href: "/dashboard", icon: HomeIcon },
{
name: 'Invoices',
href: '/dashboard/invoices',
name: "Invoices",
href: "/dashboard/invoices",
icon: DocumentDuplicateIcon,
},
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
{ name: "Customers", href: "/dashboard/customers", icon: UserGroupIcon },
];
export default function NavLinks() {
const pathname = usePathname();
return (
const pathname = usePathname();
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
@@ -32,9 +32,9 @@ return (
key={link.name}
href={link.href}
className={clsx(
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
"flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3",
{
'bg-sky-100 text-blue-600': pathname === link.href,
"bg-sky-100 text-blue-600": pathname === link.href,
},
)}
>
+38 -33
View File
@@ -1,8 +1,7 @@
import { generateYAxis } from '@/app/lib/utils';
import { CalendarIcon } from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { Revenue } from '@/app/lib/definitions';
import { fetchRevenue } from '@/app/lib/data';
import { CalendarIcon } from "@heroicons/react/24/outline";
import { fetchRevenue } from "@/app/lib/data";
import { generateYAxis } from "@/app/lib/utils";
import { lusitana } from "@/app/ui/fonts";
// This component is representational only.
// For data visualization UI, check out:
@@ -10,7 +9,8 @@ import { fetchRevenue } from '@/app/lib/data';
// https://www.chartjs.org/
// https://airbnb.io/visx/
export default async function RevenueChart() { // Make component async, remove the props
export default async function RevenueChart() {
// Make component async, remove the props
const revenue = await fetchRevenue(); // Fetch data inside the component
const chartHeight = 350;
@@ -29,36 +29,41 @@ export default async function RevenueChart() { // Make component async, remove t
</h2>
{/* NOTE: comment in this code when you get to this point in the course */}
{ <div className="rounded-xl bg-gray-50 p-4">
<div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
<div
className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
style={{ height: `${chartHeight}px` }}
>
{yAxisLabels.map((label) => (
<p key={label}>{label}</p>
{
<div className="rounded-xl bg-gray-50 p-4">
<div className="sm:grid-cols-13 mt-0 grid grid-cols-12 items-end gap-2 rounded-md bg-white p-4 md:gap-4">
<div
className="mb-6 hidden flex-col justify-between text-sm text-gray-400 sm:flex"
style={{ height: `${chartHeight}px` }}
>
{yAxisLabels.map((label) => (
<p key={label}>{label}</p>
))}
</div>
{revenue.map((month) => (
<div
key={month.month}
className="flex flex-col items-center gap-2"
>
<div
className="w-full rounded-md bg-blue-300"
style={{
height: `${(chartHeight / topLabel) * month.revenue}px`,
}}
></div>
<p className="-rotate-90 text-sm text-gray-400 sm:rotate-0">
{month.month}
</p>
</div>
))}
</div>
{revenue.map((month) => (
<div key={month.month} className="flex flex-col items-center gap-2">
<div
className="w-full rounded-md bg-blue-300"
style={{
height: `${(chartHeight / topLabel) * month.revenue}px`,
}}
></div>
<p className="-rotate-90 text-sm text-gray-400 sm:rotate-0">
{month.month}
</p>
</div>
))}
<div className="flex items-center pb-2 pt-6">
<CalendarIcon className="h-5 w-5 text-gray-500" />
<h3 className="ml-2 text-sm text-gray-500 ">Last 12 months</h3>
</div>
</div>
<div className="flex items-center pb-2 pt-6">
<CalendarIcon className="h-5 w-5 text-gray-500" />
<h3 className="ml-2 text-sm text-gray-500 ">Last 12 months</h3>
</div>
</div> }
}
</div>
);
}
+12 -11
View File
@@ -1,9 +1,8 @@
import Link from 'next/link';
import NavLinks from '@/app/ui/dashboard/nav-links';
import AcmeLogo from '@/app/ui/acme-logo';
import { PowerIcon } from '@heroicons/react/24/outline';
import { signOut } from '@/auth';
import { redirect } from 'next/navigation';
import { PowerIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import AcmeLogo from "@/app/ui/acme-logo";
import NavLinks from "@/app/ui/dashboard/nav-links";
import { signOut } from "@/auth";
export default function SideNav() {
return (
@@ -19,10 +18,12 @@ export default function SideNav() {
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<form action={async () => {
'use server';
await signOut();
}}>
<form
action={async () => {
"use server";
await signOut();
}}
>
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<PowerIcon className="w-6" />
<div className="hidden md:block">Sign Out</div>
@@ -31,4 +32,4 @@ export default function SideNav() {
</div>
</div>
);
}
}
+8 -8
View File
@@ -1,8 +1,8 @@
import { Inter, Lusitana } from 'next/font/google';
export const inter = Inter({ subsets: ['latin'] });
export const lusitana = Lusitana({
weight: ['400', '700'],
subsets: ['latin'],
});
import { Inter, Lusitana } from "next/font/google";
export const inter = Inter({ subsets: ["latin"] });
export const lusitana = Lusitana({
weight: ["400", "700"],
subsets: ["latin"],
});
+3 -3
View File
@@ -2,17 +2,17 @@
@tailwind components;
@tailwind utilities;
input[type='number'] {
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type='number']::-webkit-inner-spin-button {
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number']::-webkit-outer-spin-button {
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
+7 -7
View File
@@ -1,7 +1,7 @@
.shape {
height: 0;
width: 0;
border-bottom: 30px solid black;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
}
.shape {
height: 0;
width: 0;
border-bottom: 30px solid black;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
}
+5 -5
View File
@@ -1,6 +1,6 @@
import { clsx } from 'clsx';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
import { clsx } from "clsx";
import Link from "next/link";
import { lusitana } from "@/app/ui/fonts";
interface Breadcrumb {
label: string;
@@ -15,13 +15,13 @@ export default function Breadcrumbs({
}) {
return (
<nav aria-label="Breadcrumb" className="mb-6 block">
<ol className={clsx(lusitana.className, 'flex text-xl md:text-2xl')}>
<ol className={clsx(lusitana.className, "flex text-xl md:text-2xl")}>
{breadcrumbs.map((breadcrumb, index) => (
<li
key={breadcrumb.href}
aria-current={breadcrumb.active}
className={clsx(
breadcrumb.active ? 'text-gray-900' : 'text-gray-500',
breadcrumb.active ? "text-gray-900" : "text-gray-500",
)}
>
<Link href={breadcrumb.href}>{breadcrumb.label}</Link>
+9 -8
View File
@@ -1,13 +1,13 @@
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { PencilIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { deleteInvoice } from "@/app/lib/actions";
export function CreateInvoice() {
return (
<Link
href="/dashboard/invoices/create"
className="flex h-10 items-center rounded-lg bg-blue-600 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
<span className="hidden md:block">Create Invoice</span>{' '}
<span className="hidden md:block">Create Invoice</span>{" "}
<PlusIcon className="h-5 md:ml-4" />
</Link>
);
@@ -16,7 +16,7 @@ export function CreateInvoice() {
export function UpdateInvoice({ id }: { id: string }) {
return (
<Link
href="/dashboard/invoices"
href={`/dashboard/invoices/${id}/edit`}
className="rounded-md border p-2 hover:bg-gray-100"
>
<PencilIcon className="w-5" />
@@ -25,12 +25,13 @@ export function UpdateInvoice({ id }: { id: string }) {
}
export function DeleteInvoice({ id }: { id: string }) {
const deleteInvoiceWithId = deleteInvoice.bind(null, id);
return (
<>
<form action={deleteInvoiceWithId}>
<button className="rounded-md border p-2 hover:bg-gray-100">
<span className="sr-only">Delete</span>
<TrashIcon className="w-5" />
</button>
</>
</form>
);
}
}
+14 -17
View File
@@ -1,25 +1,23 @@
'use client';
"use client";
import { useFormState } from 'react-dom';
import { CustomerField } from '@/app/lib/definitions';
import Link from 'next/link';
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '../button';
import { createInvoice, State } from '@/app/lib/actions';
import { useActionState } from 'react';
} from "@heroicons/react/24/outline";
import Link from "next/link";
import { useFormState } from "react-dom";
import { createInvoice } from "@/app/lib/actions";
import type { CustomerField } from "@/app/lib/definitions";
import { Button } from "../button";
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { message: '', error: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
const initialState = { message: "", error: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
return (
<form action={dispatch}>
<form action={dispatch}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
@@ -46,11 +44,10 @@ const [state, dispatch] = useFormState(createInvoice, initialState);
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
</div>
<div id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{state.errors?.customerId?.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
</p>
))}
</div>
</div>
+7 -7
View File
@@ -1,14 +1,14 @@
'use client';
"use client";
import { CustomerField, InvoiceForm } from '@/app/lib/definitions';
import {
CheckIcon,
ClockIcon,
CurrencyDollarIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { Button } from '@/app/ui/button';
} from "@heroicons/react/24/outline";
import Link from "next/link";
import type { CustomerField, InvoiceForm } from "@/app/lib/definitions";
import { Button } from "@/app/ui/button";
export default function EditInvoiceForm({
invoice,
@@ -80,7 +80,7 @@ export default function EditInvoiceForm({
name="status"
type="radio"
value="pending"
defaultChecked={invoice.status === 'pending'}
defaultChecked={invoice.status === "pending"}
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
/>
<label
@@ -96,7 +96,7 @@ export default function EditInvoiceForm({
name="status"
type="radio"
value="paid"
defaultChecked={invoice.status === 'paid'}
defaultChecked={invoice.status === "paid"}
className="h-4 w-4 border-gray-300 bg-gray-100 text-gray-600 focus:ring-2 focus:ring-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-gray-600"
/>
<label
+54 -57
View File
@@ -1,62 +1,59 @@
'use client';
"use client";
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { generatePagination } from "@/app/lib/utils";
export default function Pagination({ totalPages }: { totalPages: number }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const currentPage = Number(searchParams.get("page")) || 1;
const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString());
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
};
const allPages = generatePagination(currentPage, totalPages);
return (
<>
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>
<div className="flex -space-x-px">
{allPages.map((page, index) => {
let position: "first" | "last" | "single" | "middle" | undefined;
<div className="flex -space-x-px">
{allPages.map((page, index) => {
let position: 'first' | 'last' | 'single' | 'middle' | undefined;
if (index === 0) position = "first";
if (index === allPages.length - 1) position = "last";
if (allPages.length === 1) position = "single";
if (page === "...") position = "middle";
if (index === 0) position = 'first';
if (index === allPages.length - 1) position = 'last';
if (allPages.length === 1) position = 'single';
if (page === '...') position = 'middle';
return (
<PaginationNumber
key={page}
href={createPageURL(page)}
page={page}
position={position}
isActive={currentPage === page}
/>
);
})}
</div>
<PaginationArrow
direction="right"
href={createPageURL(currentPage + 1)}
isDisabled={currentPage >= totalPages}
/>
return (
<PaginationNumber
key={page}
href={createPageURL(page)}
page={page}
position={position}
isActive={currentPage === page}
/>
);
})}
</div>
</>
<PaginationArrow
direction="right"
href={createPageURL(currentPage + 1)}
isDisabled={currentPage >= totalPages}
/>
</div>
);
}
@@ -68,21 +65,21 @@ function PaginationNumber({
}: {
page: number | string;
href: string;
position?: 'first' | 'last' | 'middle' | 'single';
position?: "first" | "last" | "middle" | "single";
isActive: boolean;
}) {
const className = clsx(
'flex h-10 w-10 items-center justify-center text-sm border',
"flex h-10 w-10 items-center justify-center text-sm border",
{
'rounded-l-md': position === 'first' || position === 'single',
'rounded-r-md': position === 'last' || position === 'single',
'z-10 bg-blue-600 border-blue-600 text-white': isActive,
'hover:bg-gray-100': !isActive && position !== 'middle',
'text-gray-300': position === 'middle',
"rounded-l-md": position === "first" || position === "single",
"rounded-r-md": position === "last" || position === "single",
"z-10 bg-blue-600 border-blue-600 text-white": isActive,
"hover:bg-gray-100": !isActive && position !== "middle",
"text-gray-300": position === "middle",
},
);
return isActive || position === 'middle' ? (
return isActive || position === "middle" ? (
<div className={className}>{page}</div>
) : (
<Link href={href} className={className}>
@@ -97,21 +94,21 @@ function PaginationArrow({
isDisabled,
}: {
href: string;
direction: 'left' | 'right';
direction: "left" | "right";
isDisabled?: boolean;
}) {
const className = clsx(
'flex h-10 w-10 items-center justify-center rounded-md border',
"flex h-10 w-10 items-center justify-center rounded-md border",
{
'pointer-events-none text-gray-300': isDisabled,
'hover:bg-gray-100': !isDisabled,
'mr-2 md:mr-4': direction === 'left',
'ml-2 md:ml-4': direction === 'right',
"pointer-events-none text-gray-300": isDisabled,
"hover:bg-gray-100": !isDisabled,
"mr-2 md:mr-4": direction === "left",
"ml-2 md:ml-4": direction === "right",
},
);
const icon =
direction === 'left' ? (
direction === "left" ? (
<ArrowLeftIcon className="w-4" />
) : (
<ArrowRightIcon className="w-4" />
+7 -7
View File
@@ -1,24 +1,24 @@
import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { CheckIcon, ClockIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
export default function InvoiceStatus({ status }: { status: string }) {
return (
<span
className={clsx(
'inline-flex items-center rounded-full px-2 py-1 text-xs',
"inline-flex items-center rounded-full px-2 py-1 text-xs",
{
'bg-gray-100 text-gray-500': status === 'pending',
'bg-green-500 text-white': status === 'paid',
"bg-gray-100 text-gray-500": status === "pending",
"bg-green-500 text-white": status === "paid",
},
)}
>
{status === 'pending' ? (
{status === "pending" ? (
<>
Pending
<ClockIcon className="ml-1 w-4 text-gray-500" />
</>
) : null}
{status === 'paid' ? (
{status === "paid" ? (
<>
Paid
<CheckIcon className="ml-1 w-4 text-white" />
+5 -5
View File
@@ -1,8 +1,8 @@
import Image from 'next/image';
import { UpdateInvoice, DeleteInvoice } from '@/app/ui/invoices/buttons';
import InvoiceStatus from '@/app/ui/invoices/status';
import { formatDateToLocal, formatCurrency } from '@/app/lib/utils';
import { fetchFilteredInvoices } from '@/app/lib/data';
import Image from "next/image";
import { fetchFilteredInvoices } from "@/app/lib/data";
import { formatCurrency, formatDateToLocal } from "@/app/lib/utils";
import { DeleteInvoice, UpdateInvoice } from "@/app/ui/invoices/buttons";
import InvoiceStatus from "@/app/ui/invoices/status";
export default async function InvoicesTable({
query,
+18 -16
View File
@@ -1,19 +1,18 @@
'use client';
"use client";
import { lusitana } from '@/app/ui/fonts';
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import {
AtSymbolIcon,
KeyIcon,
ExclamationCircleIcon,
} from '@heroicons/react/24/outline';
import { ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button } from './button';
import { useFormState, useFormStatus } from 'react-dom';
import { authenticate } from '../lib/actions';
KeyIcon,
} from "@heroicons/react/24/outline";
import { useFormState, useFormStatus } from "react-dom";
import { lusitana } from "@/app/ui/fonts";
import { authenticate } from "../lib/actions";
import { Button } from "./button";
export default function LoginForm() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined)
const [errorMessage, dispatch] = useFormState(authenticate, undefined);
return (
<form action={dispatch} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
@@ -62,15 +61,18 @@ export default function LoginForm() {
</div>
</div>
<LoginButton />
<div className="flex h-8 items-end space-x-1" aria-live='polite' aria-atomic='true'>
<div
className="flex h-8 items-end space-x-1"
aria-live="polite"
aria-atomic="true"
>
{/* Add form errors here */}
{errorMessage && (
<>
<ExclamationCircleIcon className='h-5 w-5 text-red-500' />
<p className='text-sm text-red-500'>{errorMessage}</p>
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
<p className="text-sm text-red-500">{errorMessage}</p>
</>
)}
</div>
</div>
</form>
@@ -78,10 +80,10 @@ export default function LoginForm() {
}
function LoginButton() {
const {pending} = useFormStatus()
const { pending } = useFormStatus();
return (
<Button className="mt-4 w-full" aria-disabled={pending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
);
}
}
+11 -12
View File
@@ -1,25 +1,24 @@
'use client';
"use client";
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams, usePathname, useRouter } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useDebouncedCallback } from "use-debounce";
var placeholder = '';
var placeholder = "";
export default function Search() {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
const handleSearch = useDebouncedCallback((term) => {
console.log(`Searching... ${term}`);
const params = new URLSearchParams(searchParams);
params.set('page', '1');
params.set("page", "1");
if (term) {
params.set('query', term);
params.set("query", term);
} else {
params.delete('query');
params.delete("query");
}
replace(`${pathname}?${params.toString()}`);
}, 300);
@@ -34,8 +33,8 @@ export default function Search() {
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get('query')?.toString()}
/>
defaultValue={searchParams.get("query")?.toString()}
/>
<MagnifyingGlassIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
);
+1 -1
View File
@@ -1,6 +1,6 @@
// 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';
"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 (