Remove nextjs scaffolding contents #113
| @ -1,23 +1,5 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| // This file contains type definitions for your data.
 | ||||
| // It describes the shape of the data, and what data type each property should accept.
 | ||||
| // For simplicity of teaching, we're manually defining these types.
 | ||||
| // However, these types are generated automatically if you're using an ORM such as Prisma.
 | ||||
| export type User = { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   email: string; | ||||
|   password: string; | ||||
| }; | ||||
| 
 | ||||
| export type Customer = { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   email: string; | ||||
|   image_url: string; | ||||
| }; | ||||
| 
 | ||||
| export type GuestStatus = 'considered' | 'invited' | 'confirmed' | 'declined' | 'tentative'; | ||||
| export type Guest = { | ||||
|   id?: string; | ||||
| @ -62,34 +44,6 @@ export type AttendanceSummary = { | ||||
|   total: number; | ||||
| } | ||||
| 
 | ||||
| export type Invoice = { | ||||
|   id: string; | ||||
|   customer_id: string; | ||||
|   amount: number; | ||||
|   date: string; | ||||
|   // In TypeScript, this is called a string union type.
 | ||||
|   // It means that the "status" property can only be one of the two strings: 'pending' or 'paid'.
 | ||||
|   status: 'pending' | 'paid'; | ||||
| }; | ||||
| 
 | ||||
| export type Revenue = { | ||||
|   month: string; | ||||
|   revenue: number; | ||||
| }; | ||||
| 
 | ||||
| export type LatestInvoice = { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   image_url: string; | ||||
|   email: string; | ||||
|   amount: string; | ||||
| }; | ||||
| 
 | ||||
| // The database returns a number for amount, but we later format it to a string with the formatCurrency function
 | ||||
| export type LatestInvoiceRaw = Omit<LatestInvoice, 'amount'> & { | ||||
|   amount: number; | ||||
| }; | ||||
| 
 | ||||
| export type guestsTable = { | ||||
|   id: string; | ||||
|   customer_id: string; | ||||
| @ -100,35 +54,3 @@ export type guestsTable = { | ||||
|   amount: number; | ||||
|   status: 'pending' | 'paid'; | ||||
| }; | ||||
| 
 | ||||
| export type CustomersTableType = { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   email: string; | ||||
|   image_url: string; | ||||
|   total_guests: number; | ||||
|   total_pending: number; | ||||
|   total_paid: number; | ||||
| }; | ||||
| 
 | ||||
| export type FormattedCustomersTable = { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   email: string; | ||||
|   image_url: string; | ||||
|   total_guests: number; | ||||
|   total_pending: string; | ||||
|   total_paid: string; | ||||
| }; | ||||
| 
 | ||||
| export type CustomerField = { | ||||
|   id: string; | ||||
|   name: string; | ||||
| }; | ||||
| 
 | ||||
| export type InvoiceForm = { | ||||
|   id: string; | ||||
|   customer_id: string; | ||||
|   amount: number; | ||||
|   status: 'pending' | 'paid'; | ||||
| }; | ||||
|  | ||||
| @ -1,149 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| // This file contains placeholder data that you'll be replacing with real data in the Data Fetching chapter:
 | ||||
| // https://nextjs.org/learn/dashboard-app/fetching-data
 | ||||
| const users = [ | ||||
|   { | ||||
|     id: '410544b2-4001-4271-9855-fec4b6a6442a', | ||||
|     name: 'User', | ||||
|     email: 'user@nextmail.com', | ||||
|     password: '123456', | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const customers = [ | ||||
|   { | ||||
|     id: 'd6e15727-9fe1-4961-8c5b-ea44a9bd81aa', | ||||
|     name: 'Evil Rabbit', | ||||
|     email: 'evil@rabbit.com', | ||||
|     image_url: '/customers/evil-rabbit.png', | ||||
|   }, | ||||
|   { | ||||
|     id: '3958dc9e-712f-4377-85e9-fec4b6a6442a', | ||||
|     name: 'Delba de Oliveira', | ||||
|     email: 'delba@oliveira.com', | ||||
|     image_url: '/customers/delba-de-oliveira.png', | ||||
|   }, | ||||
|   { | ||||
|     id: '3958dc9e-742f-4377-85e9-fec4b6a6442a', | ||||
|     name: 'Lee Robinson', | ||||
|     email: 'lee@robinson.com', | ||||
|     image_url: '/customers/lee-robinson.png', | ||||
|   }, | ||||
|   { | ||||
|     id: '76d65c26-f784-44a2-ac19-586678f7c2f2', | ||||
|     name: 'Michael Novotny', | ||||
|     email: 'michael@novotny.com', | ||||
|     image_url: '/customers/michael-novotny.png', | ||||
|   }, | ||||
|   { | ||||
|     id: 'CC27C14A-0ACF-4F4A-A6C9-D45682C144B9', | ||||
|     name: 'Amy Burns', | ||||
|     email: 'amy@burns.com', | ||||
|     image_url: '/customers/amy-burns.png', | ||||
|   }, | ||||
|   { | ||||
|     id: '13D07535-C59E-4157-A011-F8D2EF4E0CBB', | ||||
|     name: 'Balazs Orban', | ||||
|     email: 'balazs@orban.com', | ||||
|     image_url: '/customers/balazs-orban.png', | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const guests = [ | ||||
|   { | ||||
|     customer_id: customers[0].id, | ||||
|     amount: 15795, | ||||
|     status: 'pending', | ||||
|     date: '2022-12-06', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[1].id, | ||||
|     amount: 20348, | ||||
|     status: 'pending', | ||||
|     date: '2022-11-14', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[4].id, | ||||
|     amount: 3040, | ||||
|     status: 'paid', | ||||
|     date: '2022-10-29', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[3].id, | ||||
|     amount: 44800, | ||||
|     status: 'paid', | ||||
|     date: '2023-09-10', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[5].id, | ||||
|     amount: 34577, | ||||
|     status: 'pending', | ||||
|     date: '2023-08-05', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[2].id, | ||||
|     amount: 54246, | ||||
|     status: 'pending', | ||||
|     date: '2023-07-16', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[0].id, | ||||
|     amount: 666, | ||||
|     status: 'pending', | ||||
|     date: '2023-06-27', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[3].id, | ||||
|     amount: 32545, | ||||
|     status: 'paid', | ||||
|     date: '2023-06-09', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[4].id, | ||||
|     amount: 1250, | ||||
|     status: 'paid', | ||||
|     date: '2023-06-17', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[5].id, | ||||
|     amount: 8546, | ||||
|     status: 'paid', | ||||
|     date: '2023-06-07', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[1].id, | ||||
|     amount: 500, | ||||
|     status: 'paid', | ||||
|     date: '2023-08-19', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[5].id, | ||||
|     amount: 8945, | ||||
|     status: 'paid', | ||||
|     date: '2023-06-03', | ||||
|   }, | ||||
|   { | ||||
|     customer_id: customers[2].id, | ||||
|     amount: 1000, | ||||
|     status: 'paid', | ||||
|     date: '2022-06-05', | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const revenue = [ | ||||
|   { month: 'Jan', revenue: 2000 }, | ||||
|   { month: 'Feb', revenue: 1800 }, | ||||
|   { month: 'Mar', revenue: 2200 }, | ||||
|   { month: 'Apr', revenue: 2500 }, | ||||
|   { month: 'May', revenue: 2300 }, | ||||
|   { month: 'Jun', revenue: 3200 }, | ||||
|   { month: 'Jul', revenue: 3500 }, | ||||
|   { month: 'Aug', revenue: 3700 }, | ||||
|   { month: 'Sep', revenue: 2500 }, | ||||
|   { month: 'Oct', revenue: 2800 }, | ||||
|   { month: 'Nov', revenue: 3000 }, | ||||
|   { month: 'Dec', revenue: 4800 }, | ||||
| ]; | ||||
| 
 | ||||
| export { users, customers, guests, revenue }; | ||||
| @ -1,124 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| // import bcrypt from 'bcrypt';
 | ||||
| // import { db } from '@vercel/postgres';
 | ||||
| // import { guests, customers, revenue, users } from '../lib/placeholder-data';
 | ||||
| 
 | ||||
| // const client = await db.connect();
 | ||||
| 
 | ||||
| // async function seedUsers() {
 | ||||
| //   await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
 | ||||
| //   await client.sql`
 | ||||
| //     CREATE TABLE IF NOT EXISTS users (
 | ||||
| //       id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
 | ||||
| //       name VARCHAR(255) NOT NULL,
 | ||||
| //       email TEXT NOT NULL UNIQUE,
 | ||||
| //       password TEXT NOT NULL
 | ||||
| //     );
 | ||||
| //   `;
 | ||||
| 
 | ||||
| //   const insertedUsers = await Promise.all(
 | ||||
| //     users.map(async (user) => {
 | ||||
| //       const hashedPassword = await bcrypt.hash(user.password, 10);
 | ||||
| //       return client.sql`
 | ||||
| //         INSERT INTO users (id, name, email, password)
 | ||||
| //         VALUES (${user.id}, ${user.name}, ${user.email}, ${hashedPassword})
 | ||||
| //         ON CONFLICT (id) DO NOTHING;
 | ||||
| //       `;
 | ||||
| //     }),
 | ||||
| //   );
 | ||||
| 
 | ||||
| //   return insertedUsers;
 | ||||
| // }
 | ||||
| 
 | ||||
| // async function seedguests() {
 | ||||
| //   await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
 | ||||
| 
 | ||||
| //   await client.sql`
 | ||||
| //     CREATE TABLE IF NOT EXISTS guests (
 | ||||
| //       id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
 | ||||
| //       customer_id UUID NOT NULL,
 | ||||
| //       amount INT NOT NULL,
 | ||||
| //       status VARCHAR(255) NOT NULL,
 | ||||
| //       date DATE NOT NULL
 | ||||
| //     );
 | ||||
| //   `;
 | ||||
| 
 | ||||
| //   const insertedguests = await Promise.all(
 | ||||
| //     guests.map(
 | ||||
| //       (invoice) => client.sql`
 | ||||
| //         INSERT INTO guests (customer_id, amount, status, date)
 | ||||
| //         VALUES (${invoice.customer_id}, ${invoice.amount}, ${invoice.status}, ${invoice.date})
 | ||||
| //         ON CONFLICT (id) DO NOTHING;
 | ||||
| //       `,
 | ||||
| //     ),
 | ||||
| //   );
 | ||||
| 
 | ||||
| //   return insertedguests;
 | ||||
| // }
 | ||||
| 
 | ||||
| // async function seedCustomers() {
 | ||||
| //   await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
 | ||||
| 
 | ||||
| //   await client.sql`
 | ||||
| //     CREATE TABLE IF NOT EXISTS customers (
 | ||||
| //       id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
 | ||||
| //       name VARCHAR(255) NOT NULL,
 | ||||
| //       email VARCHAR(255) NOT NULL,
 | ||||
| //       image_url VARCHAR(255) NOT NULL
 | ||||
| //     );
 | ||||
| //   `;
 | ||||
| 
 | ||||
| //   const insertedCustomers = await Promise.all(
 | ||||
| //     customers.map(
 | ||||
| //       (customer) => client.sql`
 | ||||
| //         INSERT INTO customers (id, name, email, image_url)
 | ||||
| //         VALUES (${customer.id}, ${customer.name}, ${customer.email}, ${customer.image_url})
 | ||||
| //         ON CONFLICT (id) DO NOTHING;
 | ||||
| //       `,
 | ||||
| //     ),
 | ||||
| //   );
 | ||||
| 
 | ||||
| //   return insertedCustomers;
 | ||||
| // }
 | ||||
| 
 | ||||
| // async function seedRevenue() {
 | ||||
| //   await client.sql`
 | ||||
| //     CREATE TABLE IF NOT EXISTS revenue (
 | ||||
| //       month VARCHAR(4) NOT NULL UNIQUE,
 | ||||
| //       revenue INT NOT NULL
 | ||||
| //     );
 | ||||
| //   `;
 | ||||
| 
 | ||||
| //   const insertedRevenue = await Promise.all(
 | ||||
| //     revenue.map(
 | ||||
| //       (rev) => client.sql`
 | ||||
| //         INSERT INTO revenue (month, revenue)
 | ||||
| //         VALUES (${rev.month}, ${rev.revenue})
 | ||||
| //         ON CONFLICT (month) DO NOTHING;
 | ||||
| //       `,
 | ||||
| //     ),
 | ||||
| //   );
 | ||||
| 
 | ||||
| //   return insertedRevenue;
 | ||||
| // }
 | ||||
| 
 | ||||
| export async function GET() { | ||||
|   return Response.json({ | ||||
|     message: | ||||
|       'Uncomment this file and remove this line. You can delete this file when you are finished.', | ||||
|   }); | ||||
|   // try {
 | ||||
|   //   await client.sql`BEGIN`;
 | ||||
|   //   await seedUsers();
 | ||||
|   //   await seedCustomers();
 | ||||
|   //   await seedguests();
 | ||||
|   //   await seedRevenue();
 | ||||
|   //   await client.sql`COMMIT`;
 | ||||
| 
 | ||||
|   //   return Response.json({ message: 'Database seeded successfully' });
 | ||||
|   // } catch (error) {
 | ||||
|   //   await client.sql`ROLLBACK`;
 | ||||
|   //   return Response.json({ error }, { status: 500 });
 | ||||
|   // }
 | ||||
| } | ||||
| @ -1,125 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| import Image from 'next/image'; | ||||
| import { lusitana } from '@/app/ui/fonts'; | ||||
| import Search from '@/app/ui/search'; | ||||
| import { | ||||
|   CustomersTableType, | ||||
|   FormattedCustomersTable, | ||||
| } from '@/app/lib/definitions'; | ||||
| 
 | ||||
| export default async function CustomersTable({ | ||||
|   customers, | ||||
| }: { | ||||
|   customers: FormattedCustomersTable[]; | ||||
| }) { | ||||
|   return ( | ||||
|     <div className="w-full"> | ||||
|       <h1 className={`${lusitana.className} mb-8 text-xl md:text-2xl`}> | ||||
|         Customers | ||||
|       </h1> | ||||
|       <Search placeholder="Search customers..." /> | ||||
|       <div className="mt-6 flow-root"> | ||||
|         <div className="overflow-x-auto"> | ||||
|           <div className="inline-block min-w-full align-middle"> | ||||
|             <div className="overflow-hidden rounded-md bg-gray-50 p-2 md:pt-0"> | ||||
|               <div className="md:hidden"> | ||||
|                 {customers?.map((customer) => ( | ||||
|                   <div | ||||
|                     key={customer.id} | ||||
|                     className="mb-2 w-full rounded-md bg-white p-4" | ||||
|                   > | ||||
|                     <div className="flex items-center justify-between border-b pb-4"> | ||||
|                       <div> | ||||
|                         <div className="mb-2 flex items-center"> | ||||
|                           <div className="flex items-center gap-3"> | ||||
|                             <Image | ||||
|                               src={customer.image_url} | ||||
|                               className="rounded-full" | ||||
|                               alt={`${customer.name}'s profile picture`} | ||||
|                               width={28} | ||||
|                               height={28} | ||||
|                             /> | ||||
|                             <p>{customer.name}</p> | ||||
|                           </div> | ||||
|                         </div> | ||||
|                         <p className="text-sm text-gray-500"> | ||||
|                           {customer.email} | ||||
|                         </p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                     <div className="flex w-full items-center justify-between border-b py-5"> | ||||
|                       <div className="flex w-1/2 flex-col"> | ||||
|                         <p className="text-xs">Pending</p> | ||||
|                         <p className="font-medium">{customer.total_pending}</p> | ||||
|                       </div> | ||||
|                       <div className="flex w-1/2 flex-col"> | ||||
|                         <p className="text-xs">Paid</p> | ||||
|                         <p className="font-medium">{customer.total_paid}</p> | ||||
|                       </div> | ||||
|                     </div> | ||||
|                     <div className="pt-4 text-sm"> | ||||
|                       <p>{customer.total_guests} guests</p> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 ))} | ||||
|               </div> | ||||
|               <table className="hidden min-w-full rounded-md text-gray-900 md:table"> | ||||
|                 <thead className="rounded-md bg-gray-50 text-left text-sm font-normal"> | ||||
|                   <tr> | ||||
|                     <th scope="col" className="px-4 py-5 font-medium sm:pl-6"> | ||||
|                       Name | ||||
|                     </th> | ||||
|                     <th scope="col" className="px-3 py-5 font-medium"> | ||||
|                       Email | ||||
|                     </th> | ||||
|                     <th scope="col" className="px-3 py-5 font-medium"> | ||||
|                       Total guests | ||||
|                     </th> | ||||
|                     <th scope="col" className="px-3 py-5 font-medium"> | ||||
|                       Total Pending | ||||
|                     </th> | ||||
|                     <th scope="col" className="px-4 py-5 font-medium"> | ||||
|                       Total Paid | ||||
|                     </th> | ||||
|                   </tr> | ||||
|                 </thead> | ||||
| 
 | ||||
|                 <tbody className="divide-y divide-gray-200 text-gray-900"> | ||||
|                   {customers.map((customer) => ( | ||||
|                     <tr key={customer.id} className="group"> | ||||
|                       <td className="whitespace-nowrap bg-white py-5 pl-4 pr-3 text-sm text-black group-first-of-type:rounded-md group-last-of-type:rounded-md sm:pl-6"> | ||||
|                         <div className="flex items-center gap-3"> | ||||
|                           <Image | ||||
|                             src={customer.image_url} | ||||
|                             className="rounded-full" | ||||
|                             alt={`${customer.name}'s profile picture`} | ||||
|                             width={28} | ||||
|                             height={28} | ||||
|                           /> | ||||
|                           <p>{customer.name}</p> | ||||
|                         </div> | ||||
|                       </td> | ||||
|                       <td className="whitespace-nowrap bg-white px-4 py-5 text-sm"> | ||||
|                         {customer.email} | ||||
|                       </td> | ||||
|                       <td className="whitespace-nowrap bg-white px-4 py-5 text-sm"> | ||||
|                         {customer.total_guests} | ||||
|                       </td> | ||||
|                       <td className="whitespace-nowrap bg-white px-4 py-5 text-sm"> | ||||
|                         {customer.total_pending} | ||||
|                       </td> | ||||
|                       <td className="whitespace-nowrap bg-white px-4 py-5 text-sm group-first-of-type:rounded-md group-last-of-type:rounded-md"> | ||||
|                         {customer.total_paid} | ||||
|                       </td> | ||||
|                     </tr> | ||||
|                   ))} | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -1,60 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| import { | ||||
|   BanknotesIcon, | ||||
|   ClockIcon, | ||||
|   UserGroupIcon, | ||||
|   InboxIcon, | ||||
| } from '@heroicons/react/24/outline'; | ||||
| import { lusitana } from '@/app/ui/fonts'; | ||||
| 
 | ||||
| const iconMap = { | ||||
|   collected: BanknotesIcon, | ||||
|   customers: UserGroupIcon, | ||||
|   pending: ClockIcon, | ||||
|   guests: InboxIcon, | ||||
| }; | ||||
| 
 | ||||
| export default async function CardWrapper() { | ||||
|   return ( | ||||
|     <> | ||||
|       {/* NOTE: Uncomment this code in Chapter 9 */} | ||||
| 
 | ||||
|       {/* <Card title="Collected" value={totalPaidguests} type="collected" /> | ||||
|       <Card title="Pending" value={totalPendingguests} type="pending" /> | ||||
|       <Card title="Total guests" value={numberOfguests} type="guests" /> | ||||
|       <Card | ||||
|         title="Total Customers" | ||||
|         value={numberOfCustomers} | ||||
|         type="customers" | ||||
|       /> */} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function Card({ | ||||
|   title, | ||||
|   value, | ||||
|   type, | ||||
| }: { | ||||
|   title: string; | ||||
|   value: number | string; | ||||
|   type: 'guests' | 'customers' | 'pending' | 'collected'; | ||||
| }) { | ||||
|   const Icon = iconMap[type]; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="rounded-xl bg-gray-50 p-2 shadow-sm"> | ||||
|       <div className="flex p-4"> | ||||
|         {Icon ? <Icon className="h-5 w-5 text-gray-700" /> : null} | ||||
|         <h3 className="ml-2 text-sm font-medium">{title}</h3> | ||||
|       </div> | ||||
|       <p | ||||
|         className={`${lusitana.className} | ||||
|           truncate rounded-xl bg-white px-4 py-8 text-center text-2xl`}
 | ||||
|       > | ||||
|         {value} | ||||
|       </p> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -1,66 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| 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 Latestguests({ | ||||
|   latestguests, | ||||
| }: { | ||||
|   latestguests: LatestInvoice[]; | ||||
| }) { | ||||
|   return ( | ||||
|     <div className="flex w-full flex-col md:col-span-4"> | ||||
|       <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | ||||
|         Latest guests | ||||
|       </h2> | ||||
|       <div className="flex grow flex-col justify-between rounded-xl bg-gray-50 p-4"> | ||||
|         {/* NOTE: Uncomment this code in Chapter 7 */} | ||||
| 
 | ||||
|         {/* <div className="bg-white px-6"> | ||||
|           {latestguests.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`} | ||||
|                 > | ||||
|                   {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> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -1,67 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| 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'; | ||||
| 
 | ||||
| // This component is representational only.
 | ||||
| // For data visualization UI, check out:
 | ||||
| // https://www.tremor.so/
 | ||||
| // https://www.chartjs.org/
 | ||||
| // https://airbnb.io/visx/
 | ||||
| 
 | ||||
| export default async function RevenueChart({ | ||||
|   revenue, | ||||
| }: { | ||||
|   revenue: Revenue[]; | ||||
| }) { | ||||
|   const chartHeight = 350; | ||||
|   // NOTE: Uncomment this code in Chapter 7
 | ||||
| 
 | ||||
|   // const { yAxisLabels, topLabel } = generateYAxis(revenue);
 | ||||
| 
 | ||||
|   // 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"> | ||||
|       <h2 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}> | ||||
|         Recent Revenue | ||||
|       </h2> | ||||
|       {/* NOTE: Uncomment this code in Chapter 7 */} | ||||
| 
 | ||||
|       {/* <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> | ||||
|         <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,38 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| import { clsx } from 'clsx'; | ||||
| import Link from 'next/link'; | ||||
| import { lusitana } from '@/app/ui/fonts'; | ||||
| 
 | ||||
| interface Breadcrumb { | ||||
|   label: string; | ||||
|   href: string; | ||||
|   active?: boolean; | ||||
| } | ||||
| 
 | ||||
| export default 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> | ||||
|   ); | ||||
| } | ||||
| @ -1,38 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; | ||||
| import Link from 'next/link'; | ||||
| 
 | ||||
| export function CreateInvoice() { | ||||
|   return ( | ||||
|     <Link | ||||
|       href="/dashboard/guests/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>{' '} | ||||
|       <PlusIcon className="h-5 md:ml-4" /> | ||||
|     </Link> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function UpdateInvoice({ id }: { id: string }) { | ||||
|   return ( | ||||
|     <Link | ||||
|       href="/dashboard/guests" | ||||
|       className="rounded-md border p-2 hover:bg-gray-100" | ||||
|     > | ||||
|       <PencilIcon className="w-5" /> | ||||
|     </Link> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function DeleteInvoice({ id }: { id: string }) { | ||||
|   return ( | ||||
|     <> | ||||
|       <button className="rounded-md border p-2 hover:bg-gray-100"> | ||||
|         <span className="sr-only">Delete</span> | ||||
|         <TrashIcon className="w-5" /> | ||||
|       </button> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @ -1,114 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| import { CustomerField } from '@/app/lib/definitions'; | ||||
| import Link from 'next/link'; | ||||
| import { | ||||
|   CheckIcon, | ||||
|   ClockIcon, | ||||
|   CurrencyDollarIcon, | ||||
|   UserCircleIcon, | ||||
| } from '@heroicons/react/24/outline'; | ||||
| import { Button } from '@/app/ui/button'; | ||||
| 
 | ||||
| export default function Form({ customers }: { customers: CustomerField[] }) { | ||||
|   return ( | ||||
|     <form> | ||||
|       <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"> | ||||
|             Choose customer | ||||
|           </label> | ||||
|           <div className="relative"> | ||||
|             <select | ||||
|               id="customer" | ||||
|               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="" | ||||
|             > | ||||
|               <option value="" disabled> | ||||
|                 Select a customer | ||||
|               </option> | ||||
|               {customers.map((customer) => ( | ||||
|                 <option key={customer.id} value={customer.id}> | ||||
|                   {customer.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> | ||||
|         </div> | ||||
| 
 | ||||
|         {/* 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" | ||||
|                 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" | ||||
|               /> | ||||
|               <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> | ||||
|         </div> | ||||
| 
 | ||||
|         {/* Invoice Status */} | ||||
|         <fieldset> | ||||
|           <legend className="mb-2 block text-sm font-medium"> | ||||
|             Set the invoice status | ||||
|           </legend> | ||||
|           <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 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2" | ||||
|                 /> | ||||
|                 <label | ||||
|                   htmlFor="pending" | ||||
|                   className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600" | ||||
|                 > | ||||
|                   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 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2" | ||||
|                 /> | ||||
|                 <label | ||||
|                   htmlFor="paid" | ||||
|                   className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white" | ||||
|                 > | ||||
|                   Paid <CheckIcon className="h-4 w-4" /> | ||||
|                 </label> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </fieldset> | ||||
|       </div> | ||||
|       <div className="mt-6 flex justify-end gap-4"> | ||||
|         <Link | ||||
|           href="/dashboard/guests" | ||||
|           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> | ||||
|   ); | ||||
| } | ||||
| @ -1,125 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| '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'; | ||||
| 
 | ||||
| export default function EditInvoiceForm({ | ||||
|   invoice, | ||||
|   customers, | ||||
| }: { | ||||
|   invoice: InvoiceForm; | ||||
|   customers: CustomerField[]; | ||||
| }) { | ||||
|   return ( | ||||
|     <form> | ||||
|       <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"> | ||||
|             Choose customer | ||||
|           </label> | ||||
|           <div className="relative"> | ||||
|             <select | ||||
|               id="customer" | ||||
|               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={invoice.customer_id} | ||||
|             > | ||||
|               <option value="" disabled> | ||||
|                 Select a customer | ||||
|               </option> | ||||
|               {customers.map((customer) => ( | ||||
|                 <option key={customer.id} value={customer.id}> | ||||
|                   {customer.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> | ||||
|         </div> | ||||
| 
 | ||||
|         {/* 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" | ||||
|                 step="0.01" | ||||
|                 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" | ||||
|               /> | ||||
|               <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> | ||||
|         </div> | ||||
| 
 | ||||
|         {/* Invoice Status */} | ||||
|         <fieldset> | ||||
|           <legend className="mb-2 block text-sm font-medium"> | ||||
|             Set the invoice status | ||||
|           </legend> | ||||
|           <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 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2" | ||||
|                 /> | ||||
|                 <label | ||||
|                   htmlFor="pending" | ||||
|                   className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-600" | ||||
|                 > | ||||
|                   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 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2" | ||||
|                 /> | ||||
|                 <label | ||||
|                   htmlFor="paid" | ||||
|                   className="ml-2 flex cursor-pointer items-center gap-1.5 rounded-full bg-green-500 px-3 py-1.5 text-xs font-medium text-white" | ||||
|                 > | ||||
|                   Paid <CheckIcon className="h-4 w-4" /> | ||||
|                 </label> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </fieldset> | ||||
|       </div> | ||||
|       <div className="mt-6 flex justify-end gap-4"> | ||||
|         <Link | ||||
|           href="/dashboard/guests" | ||||
|           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> | ||||
|   ); | ||||
| } | ||||
| @ -1,121 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| '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'; | ||||
| 
 | ||||
| export default function Pagination({ totalPages }: { totalPages: number }) { | ||||
|   // NOTE: Uncomment this code in Chapter 11
 | ||||
| 
 | ||||
|   // const allPages = generatePagination(currentPage, totalPages);
 | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       {/*  NOTE: Uncomment this code in Chapter 11 */} | ||||
| 
 | ||||
|       {/* <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; | ||||
| 
 | ||||
|             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} | ||||
|         /> | ||||
|       </div> */} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function PaginationNumber({ | ||||
|   page, | ||||
|   href, | ||||
|   isActive, | ||||
|   position, | ||||
| }: { | ||||
|   page: number | string; | ||||
|   href: string; | ||||
|   position?: 'first' | 'last' | 'middle' | 'single'; | ||||
|   isActive: boolean; | ||||
| }) { | ||||
|   const className = clsx( | ||||
|     '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', | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   return isActive || position === 'middle' ? ( | ||||
|     <div className={className}>{page}</div> | ||||
|   ) : ( | ||||
|     <Link href={href} className={className}> | ||||
|       {page} | ||||
|     </Link> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function PaginationArrow({ | ||||
|   href, | ||||
|   direction, | ||||
|   isDisabled, | ||||
| }: { | ||||
|   href: string; | ||||
|   direction: 'left' | 'right'; | ||||
|   isDisabled?: boolean; | ||||
| }) { | ||||
|   const className = clsx( | ||||
|     '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', | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   const icon = | ||||
|     direction === 'left' ? ( | ||||
|       <ArrowLeftIcon className="w-4" /> | ||||
|     ) : ( | ||||
|       <ArrowRightIcon className="w-4" /> | ||||
|     ); | ||||
| 
 | ||||
|   return isDisabled ? ( | ||||
|     <div className={className}>{icon}</div> | ||||
|   ) : ( | ||||
|     <Link className={className} href={href}> | ||||
|       {icon} | ||||
|     </Link> | ||||
|   ); | ||||
| } | ||||
| @ -1,31 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline'; | ||||
| import clsx from 'clsx'; | ||||
| 
 | ||||
| export default function gueststatus({ status }: { status: string }) { | ||||
|   return ( | ||||
|     <span | ||||
|       className={clsx( | ||||
|         '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', | ||||
|         }, | ||||
|       )} | ||||
|     > | ||||
|       {status === 'pending' ? ( | ||||
|         <> | ||||
|           Pending | ||||
|           <ClockIcon className="ml-1 w-4 text-gray-500" /> | ||||
|         </> | ||||
|       ) : null} | ||||
|       {status === 'paid' ? ( | ||||
|         <> | ||||
|           Paid | ||||
|           <CheckIcon className="ml-1 w-4 text-white" /> | ||||
|         </> | ||||
|       ) : null} | ||||
|     </span> | ||||
|   ); | ||||
| } | ||||
| @ -1,69 +0,0 @@ | ||||
| /* Copyright (C) 2024 Manuel Bustillo*/ | ||||
| 
 | ||||
| import { lusitana } from '@/app/ui/fonts'; | ||||
| import { | ||||
|   AtSymbolIcon, | ||||
|   KeyIcon, | ||||
|   ExclamationCircleIcon, | ||||
| } from '@heroicons/react/24/outline'; | ||||
| import { ArrowRightIcon } from '@heroicons/react/20/solid'; | ||||
| import { Button } from './button'; | ||||
| 
 | ||||
| export default function LoginForm() { | ||||
|   return ( | ||||
|     <form 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. | ||||
|         </h1> | ||||
|         <div className="w-full"> | ||||
|           <div> | ||||
|             <label | ||||
|               className="mb-3 mt-5 block text-xs font-medium text-gray-900" | ||||
|               htmlFor="email" | ||||
|             > | ||||
|               Email | ||||
|             </label> | ||||
|             <div className="relative"> | ||||
|               <input | ||||
|                 className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500" | ||||
|                 id="email" | ||||
|                 type="email" | ||||
|                 name="email" | ||||
|                 placeholder="Enter your email address" | ||||
|                 required | ||||
|               /> | ||||
|               <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"> | ||||
|             <label | ||||
|               className="mb-3 mt-5 block text-xs font-medium text-gray-900" | ||||
|               htmlFor="password" | ||||
|             > | ||||
|               Password | ||||
|             </label> | ||||
|             <div className="relative"> | ||||
|               <input | ||||
|                 className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500" | ||||
|                 id="password" | ||||
|                 type="password" | ||||
|                 name="password" | ||||
|                 placeholder="Enter password" | ||||
|                 required | ||||
|                 minLength={6} | ||||
|               /> | ||||
|               <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> | ||||
|         </div> | ||||
|         <Button className="mt-4 w-full"> | ||||
|           Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" /> | ||||
|         </Button> | ||||
|         <div className="flex h-8 items-end space-x-1"> | ||||
|           {/* Add form errors here */} | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|   ); | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 7.8 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 7.8 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 5.7 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 1019 B | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 5.5 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 9.7 KiB | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user