vercel / next-learn

Learn Next.js Starter Code
https://next-learn-dashboard.vercel.sh/
MIT License
3.75k stars 1.92k forks source link

Missing updating Customer page #507

Open alliyya opened 11 months ago

alliyya commented 11 months ago

Just completed the course and I seem to be missing the step where the customer page is actually updated to be functional?

I feel like it should be here: https://nextjs.org/learn/dashboard-app/adding-search-and-pagination based on what's in the final file.

Please let me know what step I skipped out on. I went through all the Chapters and tried to ctrl-f through the pages for customers and didn't find it. Maybe I just missed it.

Otherwise, great course. Would've enjoyed a few more DIY activities for each chapter.

jwhits commented 10 months ago

I did the same thing. This was really annoying have to control F through every chapter.

ryota-murakami commented 10 months ago

@alliyya Same here. I've finished all chapters but http://localhost:3000/dashboard/customers page only showing customers text.
So I found correct implementation of app/dashboard/customers/page.tsx from final-example and then customers page working.

Screenshot 2023-12-23 at 18 36 00

Might be we should update Learn Next course, following @alliyya suggestion

I feel like it should be here: https://nextjs.org/learn/dashboard-app/adding-search-and-pagination based on what's in the final file.

adding customers page modify instruction like this.

+import { fetchFilteredCustomers } from '@/app/lib/data' +import CustomersTable from '@/app/ui/customers/table'

export const metadata: Metadata = { title: 'Customers', }

export default async function Page({

@delbaoliveira @balazsorban44 Has Vercel customer support been contacted about the unfinished customers page after completing the Learn Next course?

dexhowl commented 6 months ago

Just realized the same thing today and thought I forgot something. Glad to see I'm not the only one that was confused by this.

EvivNotrub commented 2 months ago

Hello,

based on @ryota-murakami 's model and the course guidelines globaly, below could be a version that uses Suspense and skeletons as well, and some pagination. Some code has been moved from CustomersTable.tsx to page.tsx in order to set the suspense boundary around the relevant component. This could be a good exercise to ask users of the tutorial to reproduce the previous steps on the Customers Page in order to practice and learn better. Let me know if you see some mistakes or bad practice as I am not experienced.

import CustomersTable from "@/app/ui/customers/table";
import { lusitana } from "@/app/ui/fonts";
import Search from "@/app/ui/search";
import { CustomersTableSkeleton } from "@/app/ui/skeletons";
import { Metadata } from "next";
import { Suspense } from "react";

export const metadata: Metadata = {
    title: "Customers",
};

export default function Page({
    searchParams
    } : {
        searchParams?:
        {
            query?: string,
            page?: string
        }
    }) {

    const query = searchParams?.query || '';
    const currentPage = Number(searchParams?.page) || 1;
    return (
        <main className="w-full">
            <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">
                            <Suspense key={query + currentPage} fallback={<CustomersTableSkeleton/>}>
                                <CustomersTable query={query} currentPage={currentPage}/>
                            </Suspense>
                        </div>
                    </div>
                </div>
            </div>
        </main>
    );
}
import Image from 'next/image';
import {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  CustomersTableType,
  FormattedCustomersTable,
} from '@/app/lib/definitions';
import { fetchCustomesPages, fetchFilteredCustomers } from '@/app/lib/data';
import Pagination from '../invoices/pagination';

export default async function CustomersTable({
  query,
  currentPage
} : {
  query: string; 
  currentPage: number;
 }) {

  let customers: FormattedCustomersTable[] = [];
  let totalPages: number = 1;
  [customers, totalPages] = await Promise.all([
    fetchFilteredCustomers(query, currentPage),
    fetchCustomesPages(query),
  ]);

  return (
    <>
      <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_invoices} invoices</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 Invoices
              </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_invoices}
                </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 className="mt-5 flex w-full justify-center">
        <Pagination totalPages={totalPages} />
      </div>
    </>
  );
}
export function CustomerMobileSkeleton() {
  return (
    <div className="mb-2 w-full rounded-md bg-white p-4">
      <div className="flex items-center justify-between border-b border-gray-100 pb-4">
        <div className="flex items-center">
          <div className="mr-2 h-8 w-8 rounded-full bg-gray-100"></div>
          <div className="h-6 w-16 rounded bg-gray-100"></div>
        </div>
        <div className="h-6 w-16 rounded bg-gray-100"></div>
      </div>
      <div className="flex w-full items-center justify-between border-b py-5">
        <div className="flex w-1/2 flex-col">
          <div className="h-4 w-16 rounded bg-gray-100"></div>
          <div className="mt-2 h-6 w-24 rounded bg-gray-100"></div>
        </div>
        <div className="flex w-1/2 flex-col">
          <div className="h-4 w-16 rounded bg-gray-100"></div>
          <div className="mt-2 h-6 w-24 rounded bg-gray-100"></div>
        </div>
      </div>
      <div className="pt-4 text-sm">
        <div className="h-6 w-24 rounded bg-gray-100"></div>
      </div>
    </div>
  );
}

export function CustomersMobileSkeleton() {
  return (
    <div className="md:hidden">
      <CustomerMobileSkeleton />
      <CustomerMobileSkeleton />
      <CustomerMobileSkeleton />
      <CustomerMobileSkeleton />
      <CustomerMobileSkeleton />
    </div>
  );
}

export function CustomerTableRowSkeleton() {
  return (
    <tr className="w-full border-b border-gray-100 last-of-type:border-none [&:first-child>td:first-child]:rounded-tl-lg [&:first-child>td:last-child]:rounded-tr-lg [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg">
      {/* Customer Name and Image */}
      <td className="relative overflow-hidden whitespace-nowrap py-3 pl-6 pr-3">
        <div className="flex items-center gap-3">
          <div className="h-8 w-8 rounded-full bg-gray-100"></div>
          <div className="h-6 w-24 rounded bg-gray-100"></div>
        </div>
      </td>
      {/* Email */}
      <td className="whitespace-nowrap px-3 py-3">
        <div className="h-6 w-32 rounded bg-gray-100"></div>
      </td>
      {/* Total Invoices */}
      <td className="whitespace-nowrap px-3 py-3">
        <div className="h-6 w-16 rounded bg-gray-100"></div>
      </td>
      {/* Total Pending */}
      <td className="whitespace-nowrap px-3 py-3">
        <div className="h-6 w-16 rounded bg-gray-100"></div>
      </td>
      {/* Total Paid */}
      <td className="whitespace-nowrap py-3 pl-6 pr-3">
        <div className="h-6 w-16 rounded bg-gray-100"></div>
      </td>
    </tr>
  );
}

export function PaginationSkeleton(){
  return (
    <>
      <div className="inline-flex">
        <div className={'flex h-10 w-10 items-center justify-center rounded-md border' + ' pointer-events-none text-gray-300' + ' mr-2 md:mr-4'}>
          <ArrowLeftIcon className="w-4"/>
        </div>
        <div className="flex -space-x-px">
          <div className={'z-10 bg-blue-600 border-blue-600 text-white ' + 'flex h-10 w-10 items-center justify-center text-sm border' + " rounded-l-md"}>1</div>
        </div>
        <div className="flex -space-x-px">
          <div className={'flex h-10 w-10 items-center justify-center text-sm border' + " rounded-r-md"}>2</div>
        </div>
        <div className={'flex h-10 w-10 items-center justify-center rounded-md border' + ' pointer-events-none text-gray-300' + ' ml-2 md:ml-4'}>
          <ArrowRightIcon className="w-4" />
        </div>
      </div>
    </>
  )
}

export function CustomersTableSkeleton() {
  return (
    <>
      <div className="overflow-hidden rounded-md bg-gray-50 p-2 md:pt-0">
          <CustomersMobileSkeleton />
        <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 Invoices
              </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">
            <CustomerTableRowSkeleton />
            <CustomerTableRowSkeleton />
            <CustomerTableRowSkeleton />
            <CustomerTableRowSkeleton />
            <CustomerTableRowSkeleton />
          </tbody>
        </table>
      </div>
      <div className="mt-5 flex w-full justify-center">
        <PaginationSkeleton />
      </div>
    </>
  );
}
export async function fetchFilteredCustomers(query: string, currentPage: number) {
  const offset = (currentPage - 1) * ITEMS_PER_PAGE;
  try {
    // Artificially delay a response for demo purposes.
    // await new Promise((resolve) => setTimeout(resolve, delays[0]));
    const data = await sql<CustomersTableType>`
        SELECT
          customers.id,
          customers.name,
          customers.email,
          customers.image_url,
          COUNT(invoices.id) AS total_invoices,
          SUM(CASE WHEN invoices.status = 'pending' THEN invoices.amount ELSE 0 END) AS total_pending,
          SUM(CASE WHEN invoices.status = 'paid' THEN invoices.amount ELSE 0 END) AS total_paid
        FROM customers
        LEFT JOIN invoices ON customers.id = invoices.customer_id
        WHERE
          customers.name ILIKE ${`%${query}%`} OR
        customers.email ILIKE ${`%${query}%`}
        GROUP BY customers.id, customers.name, customers.email, customers.image_url
        ORDER BY customers.name ASC
    LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
      `;

    const customers = data.rows.map((customer) => ({
      ...customer,
      total_pending: formatCurrency(customer.total_pending),
      total_paid: formatCurrency(customer.total_paid),
    }));

    return customers;
  } catch (err) {
    console.error('Database Error:', err);
    throw new Error('Failed to fetch customer table.');
  }
}

export async function fetchCustomesPages(query: string) {
  try {
    const count = await sql`
      SELECT COUNT(*)
      FROM customers
      WHERE
        name ILIKE ${`%${query}%`} OR
        email ILIKE ${`%${query}%`}
    `;

    const totalPages = Math.ceil(Number(count.rows[0].count) / ITEMS_PER_PAGE);
    return totalPages;
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch total number of customers.');
  }
}