mirror of
https://github.com/vercel/next-learn.git
synced 2026-06-24 13:15:48 +00:00
Update create invoice view (#192)
* Update create invoice view * Update edit view * Update lock file * Update lock file * Update sidebar styles * Update * Move breadcrumbs to a separate file * Polish sidebar/mobile * Add middleware back --------- Co-authored-by: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
<div className="w-full flex-none md:w-64">
|
||||
<SideNav />
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto p-8 md:p-12">{children}</div>
|
||||
<div className="flex-grow overflow-y-auto p-6 md:p-12">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export function Button({ children, className, ...rest }: ButtonProps) {
|
||||
<button
|
||||
{...rest}
|
||||
className={clsx(
|
||||
'flex h-12 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-gray-50 transition-colors hover:bg-blue-400',
|
||||
'flex h-10 items-center rounded-lg bg-blue-600 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-500',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function LogOutButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className="flex grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2"
|
||||
className="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"
|
||||
>
|
||||
<PowerIcon className="w-6" />
|
||||
<div className="hidden md:block">Sign Out</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import {
|
||||
UserGroupIcon,
|
||||
HomeIcon,
|
||||
InboxIcon,
|
||||
DocumentDuplicateIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
@@ -13,7 +13,11 @@ import clsx from 'clsx';
|
||||
// Depending on the size of the application, this would be stored in a database.
|
||||
const links = [
|
||||
{ name: 'Home', href: '/dashboard', icon: HomeIcon },
|
||||
{ name: 'Invoices', href: '/dashboard/invoices', icon: InboxIcon },
|
||||
{
|
||||
name: 'Invoices',
|
||||
href: '/dashboard/invoices',
|
||||
icon: DocumentDuplicateIcon,
|
||||
},
|
||||
{ name: 'Customers', href: '/dashboard/customers', icon: UserGroupIcon },
|
||||
];
|
||||
|
||||
@@ -29,7 +33,7 @@ export default function NavLinks() {
|
||||
key={link.name}
|
||||
href={link.href}
|
||||
className={clsx(
|
||||
'flex grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2',
|
||||
'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,
|
||||
},
|
||||
|
||||
@@ -5,9 +5,9 @@ import AcmeLogo from '../acme-logo';
|
||||
|
||||
export default function SideNav() {
|
||||
return (
|
||||
<div className="flex h-full flex-col border-r px-2 py-4">
|
||||
<div className="flex h-full flex-col px-3 py-4 md:px-2">
|
||||
<Link
|
||||
className="mb-2 flex h-20 items-end justify-center rounded-md bg-blue-500 p-4 md:h-40 md:justify-start"
|
||||
className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
|
||||
href="/"
|
||||
>
|
||||
<div className="w-32 text-white md:w-40">
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
32
dashboard/15-final/app/ui/invoices/breadcrumbs.tsx
Normal file
32
dashboard/15-final/app/ui/invoices/breadcrumbs.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { clsx } from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import { lusitana } from '@/app/ui/fonts';
|
||||
|
||||
interface Breadcrumb {
|
||||
label: string;
|
||||
href: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export function Breadcrumbs({ breadcrumbs }: { breadcrumbs: Breadcrumb[] }) {
|
||||
return (
|
||||
<nav aria-label="Breadcrumb" className="mb-6 block">
|
||||
<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',
|
||||
)}
|
||||
>
|
||||
<Link href={breadcrumb.href}>{breadcrumb.label}</Link>
|
||||
{index < breadcrumbs.length - 1 ? (
|
||||
<span className="mx-3 inline-block">/</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,17 @@
|
||||
|
||||
import { createInvoice } from '@/app/lib/actions';
|
||||
import { CustomerName } from '@/app/lib/definitions';
|
||||
import Link from 'next/link';
|
||||
// @ts-ignore React types do not yet include useFormState
|
||||
import { experimental_useFormState as useFormState } from 'react-dom';
|
||||
import {
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
CurrencyDollarIcon,
|
||||
UserCircleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Button } from '../button';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
|
||||
export default function Form({
|
||||
customerNames,
|
||||
@@ -14,122 +23,162 @@ export default function Form({
|
||||
const [state, dispatch] = useFormState(createInvoice, initialState);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-sm rounded-lg border px-6 py-8 shadow-sm">
|
||||
<h2 className="mb-6 text-xl font-semibold">Update Invoice</h2>
|
||||
<div>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Invoices', href: '/dashboard/invoices' },
|
||||
{
|
||||
label: 'Create Invoice',
|
||||
href: '/dashboard/invoices/create',
|
||||
active: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<form action={dispatch}>
|
||||
{/* Customer Name */}
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="customer"
|
||||
className="mb-2 block text-sm font-semibold"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<select
|
||||
id="customer"
|
||||
name="customerId"
|
||||
className="block w-full rounded-md border border-gray-200 py-2 pl-3 text-sm outline-2 placeholder:text-gray-200"
|
||||
defaultValue=""
|
||||
aria-describedby="customer-error"
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select a customer
|
||||
</option>
|
||||
{customerNames.map((name) => (
|
||||
<option key={name.id} value={name.id}>
|
||||
{name.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{state.errors?.customerId ? (
|
||||
<div
|
||||
id="customer-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
<div className="rounded-md bg-gray-50 p-4 md:p-6">
|
||||
{/* Customer Name */}
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="customer"
|
||||
className="mb-2 block text-sm font-medium"
|
||||
>
|
||||
{state.errors.customerId.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
Choose customer
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
id="customer"
|
||||
name="customerId"
|
||||
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
||||
defaultValue=""
|
||||
aria-describedby="customer-error"
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select a customer
|
||||
</option>
|
||||
{customerNames.map((name) => (
|
||||
<option key={name.id} value={name.id}>
|
||||
{name.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Invoice Amount */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="amount" className="mb-2 block text-sm font-semibold">
|
||||
Amount
|
||||
</label>
|
||||
<div className="relative mt-2 rounded-md">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span className="text-gray-600 sm:text-sm">$</span>
|
||||
</div>
|
||||
<input
|
||||
id="amount"
|
||||
name="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="00.00"
|
||||
className="block w-full rounded-md border border-gray-200 py-2 pl-6 text-sm outline-2 placeholder:text-gray-200"
|
||||
aria-describedby="amount-error"
|
||||
/>
|
||||
{state.errors?.customerId ? (
|
||||
<div
|
||||
id="customer-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.customerId.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{state.errors?.amount ? (
|
||||
<div
|
||||
id="amount-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.amount.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
{/* Invoice Amount */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="amount" className="mb-2 block text-sm font-medium">
|
||||
Choose an amount
|
||||
</label>
|
||||
<div className="relative mt-2 rounded-md">
|
||||
<div className="relative">
|
||||
<input
|
||||
id="amount"
|
||||
name="amount"
|
||||
type="number"
|
||||
placeholder="Enter USD amount"
|
||||
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
||||
aria-describedby="amount-error"
|
||||
style={
|
||||
{ '-moz-appearance': 'textfield' } as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
<CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Invoice Status */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="status" className="mb-2 block text-sm font-semibold">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="block w-full rounded-md border border-gray-200 py-2 pl-3 text-sm outline-2 placeholder:text-gray-200"
|
||||
defaultValue="pending"
|
||||
aria-describedby="status-error"
|
||||
>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="paid">Paid</option>
|
||||
</select>
|
||||
|
||||
{state.errors?.status ? (
|
||||
<div
|
||||
aria-describedby="status-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.status.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{state.message ? (
|
||||
<div aria-live="polite" className="my-2 text-sm text-red-500">
|
||||
<p>{state.message}</p>
|
||||
{state.errors?.amount ? (
|
||||
<div
|
||||
id="amount-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.amount.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-2 rounded-md bg-black px-4 py-2 text-center text-sm text-white outline-2 outline-offset-4 hover:bg-gray-800"
|
||||
>
|
||||
Create Invoice
|
||||
</button>
|
||||
{/* Invoice Status */}
|
||||
<div>
|
||||
<label htmlFor="status" className="mb-2 block text-sm font-medium">
|
||||
Set the invoice status
|
||||
</label>
|
||||
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="pending"
|
||||
name="status"
|
||||
type="radio"
|
||||
value="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
|
||||
htmlFor="pending"
|
||||
className="ml-2 flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
Pending <ClockIcon className="h-4 w-4" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="paid"
|
||||
name="status"
|
||||
type="radio"
|
||||
value="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
|
||||
htmlFor="paid"
|
||||
className="ml-2 flex items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white dark:text-gray-300"
|
||||
>
|
||||
Paid <CheckIcon className="h-4 w-4" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{state.errors?.status ? (
|
||||
<div
|
||||
aria-describedby="status-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.status.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{state.message ? (
|
||||
<div aria-live="polite" className="my-2 text-sm text-red-500">
|
||||
<p>{state.message}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-4">
|
||||
<Link
|
||||
href="/dashboard/invoices"
|
||||
className="flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<Button type="submit">Create Invoice</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,17 @@ import { CustomerName, InvoiceForm } from '@/app/lib/definitions';
|
||||
import { updateInvoice } from '@/app/lib/actions';
|
||||
// @ts-ignore React types do not yet include useFormState
|
||||
import { experimental_useFormState as useFormState } from 'react-dom';
|
||||
import Link from 'next/link';
|
||||
import { lusitana } from '@/app/ui/fonts';
|
||||
import { clsx } from 'clsx';
|
||||
import {
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
CurrencyDollarIcon,
|
||||
UserCircleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Button } from '../button';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
|
||||
export default function EditInvoiceForm({
|
||||
id,
|
||||
@@ -18,120 +29,165 @@ export default function EditInvoiceForm({
|
||||
const [state, dispatch] = useFormState(updateInvoice, initialState);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-sm rounded-lg border px-6 py-8 shadow-sm">
|
||||
<h2 className="mb-6 text-xl font-semibold">Update Invoice</h2>
|
||||
<div>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Invoices', href: '/dashboard/invoices' },
|
||||
{
|
||||
label: 'Edit Invoice',
|
||||
href: `/dashboard/invoices/${id}/edit`,
|
||||
active: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<form action={dispatch}>
|
||||
<input type="hidden" name="id" value={id} />
|
||||
{/* Customer selection */}
|
||||
<div className="mb-4" role="group">
|
||||
<label
|
||||
htmlFor="customer"
|
||||
className="mb-2 block text-sm font-semibold"
|
||||
>
|
||||
Customer
|
||||
</label>
|
||||
<select
|
||||
id="customer"
|
||||
name="customerId"
|
||||
className="block w-full rounded-md border border-gray-200 py-2 pl-3 text-sm outline-2 placeholder:text-gray-200"
|
||||
defaultValue={invoice.name}
|
||||
aria-describedby="customer-error"
|
||||
>
|
||||
{customerNames.map((name) => (
|
||||
<option key={name.id} value={name.id}>
|
||||
{name.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{state.errors?.customerId ? (
|
||||
<div
|
||||
id="customer-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
<div className="rounded-md bg-gray-50 p-4 md:p-6">
|
||||
{/* Customer Name */}
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="customer"
|
||||
className="mb-2 block text-sm font-medium"
|
||||
>
|
||||
{state.errors.customerId.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
Choose customer
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
id="customer"
|
||||
name="customerId"
|
||||
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
||||
defaultValue={invoice.name}
|
||||
aria-describedby="customer-error"
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select a customer
|
||||
</option>
|
||||
{customerNames.map((name) => (
|
||||
<option key={name.id} value={name.id}>
|
||||
{name.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Invoice amount */}
|
||||
<div className="mb-4" role="group">
|
||||
<label className="mb-2 block text-sm font-semibold">Amount</label>
|
||||
<div className="relative mt-2 rounded-md shadow-sm">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span className="text-gray-600 sm:text-sm" aria-hidden="true">
|
||||
$
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
name="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
defaultValue={invoice.amount}
|
||||
placeholder="00.00"
|
||||
className="block w-full rounded-md border border-gray-200 py-2 pl-3 text-sm outline-2 placeholder:text-gray-200"
|
||||
aria-describedby="amount-error"
|
||||
/>
|
||||
{state.errors?.customerId ? (
|
||||
<div
|
||||
id="customer-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.customerId.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{state.errors?.amount ? (
|
||||
<div
|
||||
id="amount-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.amount.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
{/* Invoice Amount */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="amount" className="mb-2 block text-sm font-medium">
|
||||
Choose an amount
|
||||
</label>
|
||||
<div className="relative mt-2 rounded-md">
|
||||
<div className="relative">
|
||||
<input
|
||||
id="amount"
|
||||
name="amount"
|
||||
type="number"
|
||||
defaultValue={invoice.amount}
|
||||
placeholder="Enter USD amount"
|
||||
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
|
||||
aria-describedby="amount-error"
|
||||
style={
|
||||
{ '-moz-appearance': 'textfield' } as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
<CurrencyDollarIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Invoice status */}
|
||||
<div className="mb-4" role="group">
|
||||
<label className="mb-2 block text-sm font-semibold" htmlFor="status">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="block w-full rounded-md border border-gray-200 py-2 pl-3 text-sm outline-2 placeholder:text-gray-200"
|
||||
defaultValue={invoice.status}
|
||||
aria-describedby="status-error"
|
||||
>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="paid">Paid</option>
|
||||
</select>
|
||||
|
||||
{state.errors?.status ? (
|
||||
<div
|
||||
aria-describedby="status-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.status.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{state.message ? (
|
||||
<div aria-live="polite" className="my-2 text-sm text-red-500">
|
||||
<p>{state.message}</p>
|
||||
{state.errors?.amount ? (
|
||||
<div
|
||||
id="amount-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.amount.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Submit button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-2 rounded-md bg-black px-4 py-2 text-center text-sm text-white outline-2 outline-offset-4 hover:bg-gray-800"
|
||||
>
|
||||
Update Invoice
|
||||
</button>
|
||||
{/* Invoice Status */}
|
||||
<div>
|
||||
<label htmlFor="status" className="mb-2 block text-sm font-medium">
|
||||
Set the invoice status
|
||||
</label>
|
||||
<div className="rounded-md border border-gray-200 bg-white px-[14px] py-3">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="pending"
|
||||
name="status"
|
||||
type="radio"
|
||||
value="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
|
||||
htmlFor="pending"
|
||||
className="ml-2 flex items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
Pending <ClockIcon className="h-4 w-4" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="paid"
|
||||
name="status"
|
||||
type="radio"
|
||||
value="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
|
||||
htmlFor="paid"
|
||||
className="ml-2 flex items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white dark:text-gray-300"
|
||||
>
|
||||
Paid <CheckIcon className="h-4 w-4" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{state.errors?.status ? (
|
||||
<div
|
||||
aria-describedby="status-error"
|
||||
aria-live="polite"
|
||||
className="mt-2 text-sm text-red-500"
|
||||
>
|
||||
{state.errors.status.map((error: string) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{state.message ? (
|
||||
<div aria-live="polite" className="my-2 text-sm text-red-500">
|
||||
<p>{state.message}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-4">
|
||||
<Link
|
||||
href="/dashboard/invoices"
|
||||
className="flex h-10 items-center rounded-lg bg-gray-100 px-4 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-200"
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<Button type="submit">Edit Invoice</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function LoginForm() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<AtSymbolIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
||||
<AtSymbolIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
@@ -83,7 +83,7 @@ export default function LoginForm() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<KeyIcon className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
||||
<KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user