learn next

This commit is contained in:
linhdevtran99 committed 2024-02-26 14:22:09 +07:00
1 parent 4da604097d
commit 0194cf3054
27 files changed
+712 -64

No files matched your search

+9 -2
View File
@@ -5,6 +5,7 @@ import {
InboxIcon,
} from '@heroicons/react/24/outline';
import { lusitana } from '@/app/ui/fonts';
import { fetchCardData } from '@/app/lib/data';
const iconMap = {
collected: BanknotesIcon,
@@ -14,18 +15,24 @@ const iconMap = {
};
export default async function CardWrapper() {
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData();
return (
<>
{/* NOTE: comment in this code when you get to this point in the course */}
{/* <Card title="Collected" value={totalPaidInvoices} type="collected" />
<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"
/> */}
/>
</>
);
}
+6 -8
View File
@@ -2,12 +2,10 @@ 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';
export default async function LatestInvoices({
latestInvoices,
}: {
latestInvoices: LatestInvoice[];
}) {
import { fetchLatestInvoices } from '@/app/lib/data';
export default async function LatestInvoices() {
const latestInvoices = await fetchLatestInvoices();
return (
<div className="flex w-full flex-col md:col-span-4">
<h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
@@ -16,7 +14,7 @@ export default async function LatestInvoices({
<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">
<div className="bg-white px-6">
{latestInvoices.map((invoice, i) => {
return (
<div
@@ -53,7 +51,7 @@ export default async function LatestInvoices({
</div>
);
})}
</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>
+9 -12
View File
@@ -1,7 +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';
// This component is representational only.
// For data visualization UI, check out:
@@ -9,19 +9,16 @@ import { Revenue } from '@/app/lib/definitions';
// https://www.chartjs.org/
// https://airbnb.io/visx/
export default async function RevenueChart({
revenue,
}: {
revenue: Revenue[];
}) {
export default async function RevenueChart() {
const revenue = await fetchRevenue();
const chartHeight = 350;
// NOTE: comment in this code when you get to this point in the course
// const { yAxisLabels, topLabel } = generateYAxis(revenue);
const { yAxisLabels, topLabel } = generateYAxis(revenue);
// if (!revenue || revenue.length === 0) {
// return <p className="mt-4 text-gray-400">No data available.</p>;
// }
if (!revenue || revenue.length === 0) {
return <p className="mt-4 text-gray-400">No data available.</p>;
}
return (
<div className="w-full md:col-span-4">
@@ -30,7 +27,7 @@ export default async function RevenueChart({
</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="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"
@@ -59,7 +56,7 @@ export default async function RevenueChart({
<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>
</div>
);
}
+8 -1
View File
@@ -2,6 +2,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';
export default function SideNav() {
return (
@@ -17,7 +19,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>
<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>
+6 -3
View File
@@ -1,5 +1,6 @@
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { deleteInvoice } from '@/app/lib/actions';
export function CreateInvoice() {
return (
@@ -16,7 +17,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 +26,14 @@ 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>
);
}
+45 -1
View File
@@ -1,4 +1,6 @@
'use client';
import { CustomerField } from '@/app/lib/definitions';
import { useFormState } from 'react-dom';
import Link from 'next/link';
import {
CheckIcon,
@@ -7,10 +9,16 @@ import {
UserCircleIcon,
} from '@heroicons/react/24/outline';
import { Button } from '@/app/ui/button';
import { createInvoice } from '@/app/lib/actions';
import { date } from 'zod';
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState = { message: null, errors: {} };
const [state, dispatch] = useFormState(createInvoice, initialState);
console.log(state)
return (
<form>
<form action={dispatch}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
@@ -23,6 +31,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
name="customerId"
className="peer block w-full cursor-pointer 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
@@ -35,6 +44,14 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
</select>
<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}>
{error}
</p>
))}
</div>
</div>
{/* Invoice Amount */}
@@ -51,9 +68,18 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
step="0.01"
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"
/>
<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 id="amount-error" aria-live="polite" aria-atomic="true">
{state.errors?.amount &&
state.errors.amount.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
</div>
@@ -71,6 +97,7 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
type="radio"
value="pending"
className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
aria-describedby="status-error"
/>
<label
htmlFor="pending"
@@ -96,6 +123,23 @@ export default function Form({ customers }: { customers: CustomerField[] }) {
</div>
</div>
</div>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.status &&
state.errors.status.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.message !== 'null' &&
(
<p className="mt-2 text-sm text-red-500" key={state.message}>
{state.message}
</p>
)
}
</div>
</fieldset>
</div>
<div className="mt-6 flex justify-end gap-4">
+7 -1
View File
@@ -8,7 +8,9 @@ import {
UserCircleIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { useFormState } from 'react-dom';
import { Button } from '@/app/ui/button';
import { updateInvoice } from '@/app/lib/actions';
export default function EditInvoiceForm({
invoice,
@@ -17,8 +19,12 @@ export default function EditInvoiceForm({
invoice: InvoiceForm;
customers: CustomerField[];
}) {
const initialState = { message: null, errors: {} };
const updateInvoiceWithId = updateInvoice.bind(null, invoice.id);
const [state, dispatch] = useFormState(updateInvoiceWithId, initialState);
return (
<form>
<form action={dispatch} >
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
+13 -3
View File
@@ -4,17 +4,27 @@ 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';
export default function Pagination({ totalPages }: { totalPages: number }) {
// NOTE: comment in this code when you get to this point in the course
// const allPages = generatePagination(currentPage, totalPages);
const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const allPages = generatePagination(currentPage, totalPages);
const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString());
console.log(pathname + "?" + params.toString())
return `${pathname}?${params.toString()}`;
};
return (
<>
{/* NOTE: comment in this code when you get to this point in the course */}
{/* <div className="inline-flex">
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
@@ -47,7 +57,7 @@ export default function Pagination({ totalPages }: { totalPages: number }) {
href={createPageURL(currentPage + 1)}
isDisabled={currentPage >= totalPages}
/>
</div> */}
</div>
</>
);
}
+20 -4
View File
@@ -1,3 +1,4 @@
'use client'
import { lusitana } from '@/app/ui/fonts';
import {
AtSymbolIcon,
@@ -6,10 +7,14 @@ import {
} 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';
export default function LoginForm() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined);
return (
<form className="space-y-3">
<form action={dispatch} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
Please log in to continue.
@@ -56,8 +61,17 @@ export default function LoginForm() {
</div>
</div>
<LoginButton />
<div className="flex h-8 items-end space-x-1">
{/* Add form errors here */}
<div
className="flex h-8 items-end space-x-1"
aria-live="polite"
aria-atomic="true"
>
{errorMessage && (
<>
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
<p className="text-sm text-red-500">{errorMessage}</p>
</>
)}
</div>
</div>
</form>
@@ -65,8 +79,10 @@ export default function LoginForm() {
}
function LoginButton() {
const { pending } = useFormStatus();
return (
<Button className="mt-4 w-full">
<Button className="mt-4 w-full" aria-disabled={pending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
);
+18
View File
@@ -1,8 +1,24 @@
'use client';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';
export default function Search({ placeholder }: { placeholder: string }) {
const searchParam = useSearchParams();
const pathName = usePathname();
const { replace } = useRouter();
const handlerSearch = useDebouncedCallback((term: string) => {
const params = new URLSearchParams(searchParam);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathName}?${params.toString()}`);
}, 300);
return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
@@ -11,6 +27,8 @@ export default function Search({ placeholder }: { placeholder: string }) {
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => handlerSearch(e.target.value)}
defaultValue={searchParam.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>