update
This commit is contained in:
@@ -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
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
}: {
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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 (
|
||||
|
||||
Reference in New Issue
Block a user