add preloading to account's pages

This commit is contained in:
tedraykov 2024-06-21 16:24:45 +03:00
parent 19065c1d82
commit aa29b3071d
12 changed files with 285 additions and 186 deletions

View File

@ -38,7 +38,7 @@ export default async function Page({ params }: { params: { page: string } }) {
{page.title} {page.title}
</h1> </h1>
</div> </div>
<main> <div>
<div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<div className="flex flex-col space-y-16"> <div className="flex flex-col space-y-16">
{page.metaobjects?.map((content) => ( {page.metaobjects?.map((content) => (
@ -48,7 +48,7 @@ export default async function Page({ params }: { params: { page: string } }) {
))} ))}
</div> </div>
</div> </div>
</main> </div>
</> </>
); );
} }

3
app/account/layout.tsx Normal file
View File

@ -0,0 +1,3 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return <div className="mx-auto max-w-screen-2xl">{children}</div>;
}

34
app/account/loading.tsx Normal file
View File

@ -0,0 +1,34 @@
import Divider from 'components/divider';
import Heading from 'components/ui/heading';
import Skeleton from 'components/ui/skeleton';
export default function Loading() {
return (
<div className="p-6">
<Heading className="pb-4" as="h1">
Orders
</Heading>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="flex w-full flex-col rounded border bg-white p-6">
<div className="flex flex-col gap-4">
<div>
<div className="flex items-center gap-2">
<Skeleton className="h-20 w-20 flex-none" />
<Skeleton />
</div>
</div>
</div>
<Divider />
<div className="flex flex-col gap-4">
<div>
<Skeleton className="mb-2 h-5 w-14" />
<Skeleton className="h-4 w-24" />
</div>
<Skeleton className="w-20" />
</div>
<Skeleton className="mt-4 h-11" />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,29 @@
import Skeleton from 'components/ui/skeleton';
export default function Loading() {
return (
<div className="mx-auto max-w-6xl p-6">
<div className="mb-6 flex justify-between">
<div className="flex items-start gap-2">
<Skeleton className="mt-1 h-6 w-6" />
<div className="flex flex-col gap-2">
<Skeleton className="h-8 w-32" />
<Skeleton className="h-4 w-36" />
</div>
</div>
<div>
<Skeleton className="h-9 w-32" />
</div>
</div>
<div className="flex items-start gap-6">
<div className="flex flex-1 flex-col gap-6">
<Skeleton className="h-72" />
<Skeleton className="h-72" />
</div>
<div className="hidden md:block md:basis-5/12">
<Skeleton className="h-80" />
</div>
</div>
</div>
);
}

View File

@ -10,8 +10,9 @@ import Text from 'components/ui/text';
import Price from 'components/price'; import Price from 'components/price';
import Badge from 'components/ui/badge'; import Badge from 'components/ui/badge';
import Link from 'next/link'; import Link from 'next/link';
import OrderSummaryMobile from 'components/account/orders/order-summary-mobile';
import { Suspense } from 'react'; import { Suspense } from 'react';
import Skeleton from 'components/ui/skeleton'; import OrderSummary from 'components/account/orders/order-summary';
export const runtime = 'edge'; export const runtime = 'edge';
@ -236,86 +237,22 @@ function OrderDetails({ order }: { order: Order }) {
); );
} }
function OrderSummary({ order }: { order: Order }) {
return (
<div className="flex flex-col gap-6">
<Heading size="sm">Order Summary</Heading>
<div className="flex flex-col gap-6">
{order.lineItems.map((lineItem, index) => (
<div key={index} className="flex items-center gap-4">
<Badge content={lineItem.quantity!}>
<Image
src={lineItem.image.url}
alt={lineItem.image.altText}
width={lineItem.image.width}
height={lineItem.image.height}
className="rounded border"
/>
</Badge>
<div className="flex flex-col gap-2">
<Text>{lineItem.title}</Text>
<Label>{lineItem.sku}</Label>
</div>
<Price
className="text-sm"
amount={lineItem.price!.amount}
currencyCode={lineItem.price!.currencyCode}
/>
</div>
))}
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<div className="flex items-center justify-between">
<Text>Subtotal</Text>
<Price
className="text-sm font-semibold"
amount={order.totalPrice!.amount}
currencyCode={order.totalPrice!.currencyCode}
/>
</div>
<div className="flex items-center justify-between">
<Text>Shipping</Text>
{order.shippingMethod?.price.amount !== '0.0' ? (
<Price
className="text-sm font-semibold"
amount={order.shippingMethod!.price.amount}
currencyCode={order.shippingMethod!.price.currencyCode}
/>
) : (
<Text className="font-semibold">Free</Text>
)}
</div>
</div>
<div className="flex items-center justify-between">
<Heading as="span" size="sm">
Total
</Heading>
<Price
className="font-semibold"
amount={order.totalPrice!.amount}
currencyCode={order.totalPrice!.currencyCode}
/>
</div>
</div>
</div>
);
}
export default async function OrderPage({ params }: { params: { id: string } }) { export default async function OrderPage({ params }: { params: { id: string } }) {
const order = await getCustomerOrder(params.id); const order = await getCustomerOrder(params.id);
return ( return (
<main className="mx-auto max-w-6xl p-6"> <>
<Suspense>
<OrderSummaryMobile order={order} />
</Suspense>
<div className="mx-auto max-w-6xl p-6">
<div className="mb-6 flex justify-between"> <div className="mb-6 flex justify-between">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<Link href="/account"> <Link href="/account">
<ArrowLeftIcon className="mt-1 h-6 w-6" /> <ArrowLeftIcon className="mt-1 h-6 w-6" />
</Link> </Link>
<div> <div>
<Suspense fallback={<Skeleton />}>
<Heading as="h1">Order {order.name}</Heading> <Heading as="h1">Order {order.name}</Heading>
</Suspense>
<Label>Confirmed {toPrintDate(order.processedAt)}</Label> <Label>Confirmed {toPrintDate(order.processedAt)}</Label>
</div> </div>
</div> </div>
@ -325,16 +262,15 @@ export default async function OrderPage({ params }: { params: { id: string } })
</div> </div>
<div className="flex items-start gap-6"> <div className="flex items-start gap-6">
<div className="flex flex-1 flex-col gap-6"> <div className="flex flex-1 flex-col gap-6">
<Suspense fallback={<Skeleton />}>
<Fulfillments order={order} /> <Fulfillments order={order} />
</Suspense>
<Unfulfilled order={order} /> <Unfulfilled order={order} />
<OrderDetails order={order} /> <OrderDetails order={order} />
</div> </div>
<Card className="hidden md:block md:basis-5/12"> <Card className="hidden lg:block lg:basis-5/12">
<OrderSummary order={order} /> <OrderSummary order={order} />
</Card> </Card>
</div> </div>
</main> </div>
</>
); );
} }

View File

@ -1,104 +1,71 @@
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { getCustomerOrders } from 'lib/shopify'; import { getCustomerOrders } from 'lib/shopify';
import Text from 'components/ui/text';
import Price from 'components/price'; import Price from 'components/price';
import Divider from 'components/divider'; import Divider from 'components/divider';
import { Button } from 'components/button'; import { Button } from 'components/button';
import Heading from 'components/ui/heading';
import Label from 'components/ui/label';
import Badge from 'components/ui/badge';
import { Card } from 'components/ui/card';
export const runtime = 'edge'; export const runtime = 'edge';
export default async function AccountPage() { export default async function AccountPage() {
// if (!access) {
// redirect('/logout');
// }
// if (access === 'denied') {
// redirect('/logout');
// }
//
// const customerAccessToken = access;
//
// //this is needed b/c of strange way server components handle redirects etc.
// //see https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#redirecting
// //can only redirect outside of try/catch!
// let success = true;
// let errorMessage;
// let customerData;
// let orders;
//
// try {
// const responseCustomerDetails = await shopifyCustomerFetch<CustomerDetailsData>({
// customerToken: customerAccessToken,
// query: CUSTOMER_DETAILS_QUERY,
// tags: [TAGS.customer]
// });
// const userDetails = responseCustomerDetails.body;
// if (!userDetails) {
// throw new Error('Error getting actual user data Account page.');
// }
// customerData = userDetails?.data?.customer;
// orders = customerData?.orders?.edges;
// //console.log ("Details",orders)
// } catch (e) {
// //they don't recognize this error in TS!
// //@ts-ignore
// errorMessage = e?.error?.toString() ?? 'Unknown Error';
// console.log('error customer fetch account', e);
// if (errorMessage !== 'unauthorized') {
// throw new Error('Error getting actual user data Account page.');
// } else {
// console.log('Unauthorized access. Set to false and redirect');
// success = false;
// }
// }
// if (!success && errorMessage === 'unauthorized') redirect('/logout');
// // revalidateTag('posts') // Update cached posts //FIX
const orders = await getCustomerOrders(); const orders = await getCustomerOrders();
return ( return (
<div className="mx-auto mt-4 max-w-screen-2xl px-4"> <div className="p-6">
<h3 className="pb-4 text-2xl font-bold">Orders</h3> <Heading className="pb-4" as="h1">
Orders
</Heading>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{orders.map((order, index) => ( {orders.map((order, index) => (
<div className="relative" key={index}> <div className="relative" key={index}>
<Link <Link
className="absolute left-0 top-0 h-full w-full" className="peer absolute left-0 top-0 h-full w-full"
href={`/account/orders/${order.id}`} href={`/account/orders/${order.id}`}
></Link> />
<div className="flex w-full flex-col rounded border bg-white p-6 md:w-fit"> <Card className="flex h-full flex-col transition-shadow peer-hover:shadow-lg peer-active:shadow-lg">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-4">
{order.lineItems.slice(0, 2).map((lineItem, index) => ( {order.lineItems.map((lineItem, index) => (
<div key={index}> <div key={index}>
<div className="flex gap-2"> <div className="flex items-center gap-2">
<Badge content={lineItem.quantity!}>
<Image <Image
src={lineItem?.image?.url} src={lineItem?.image?.url}
alt={lineItem?.image?.altText} alt={lineItem?.image?.altText}
width={80} width={80}
height={80} height={80}
className="rounded border"
/> />
<div> </Badge>
<p>{lineItem.title}</p> <Text>{lineItem.title}</Text>
</div> </div>
</div> </div>
<Divider />
</div>
))} ))}
</div> </div>
<div className="flex flex-col gap-4"> <Divider />
<div className="flex flex-1 flex-col justify-end gap-4">
<div> <div>
<p className="font-bold"> <Text>
{order.lineItems.length} item{order.lineItems.length > 1 && 's'} {order.lineItems.length} item{order.lineItems.length > 1 && 's'}
</p> </Text>
<p className="text-gray-500">Order {order.name}</p> <Label>Order {order.name}</Label>
</div> </div>
<Price <Price
className="text-lg font-medium text-gray-900"
amount={order.totalPrice!.amount} amount={order.totalPrice!.amount}
currencyCode={order.totalPrice!.currencyCode} currencyCode={order.totalPrice!.currencyCode}
/> />
</div> </div>
<Button size="lg">Activate Warranty</Button> <Button size="lg" className="mt-4">
</div> Activate Warranty
</Button>
</Card>
</div> </div>
))} ))}
</div> </div>
</div>
); );
} }

View File

@ -36,15 +36,17 @@ export const metadata = {
export default async function RootLayout({ children }: { children: ReactNode }) { export default async function RootLayout({ children }: { children: ReactNode }) {
return ( return (
<html lang="en" className={GeistSans.variable}> <html lang="en" className={GeistSans.variable}>
<body className="bg-white text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white"> <body className="min-h-screen bg-white text-black selection:bg-primary-muted dark:bg-neutral-900 dark:text-white dark:selection:bg-primary-emphasis dark:selection:text-white">
<AuthProvider> <AuthProvider>
<div> {/* We need to have this wrapper div because the headless ui popover clickaway event is not working properly */}
{/* https://github.com/tailwindlabs/headlessui/issues/2752#issuecomment-1724096430 */}
<div className="flex h-screen flex-col">
<header> <header>
<Banner /> <Banner />
<Navbar /> <Navbar />
</header> </header>
<Suspense> <Suspense>
<main>{children}</main> <main className="main group flex-1">{children}</main>
</Suspense> </Suspense>
</div> </div>
</AuthProvider> </AuthProvider>

View File

@ -0,0 +1,46 @@
'use client';
import { ShoppingCartIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
import Text from 'components/ui/text';
import { Disclosure, DisclosureButton, DisclosurePanel, Transition } from '@headlessui/react';
import Divider from 'components/divider';
import Price from 'components/price';
import { Order } from 'lib/shopify/types';
import OrderSummary from './order-summary';
export default function OrderSummaryMobile({ order }: { order: Order }) {
return (
<div className="block lg:hidden">
<Disclosure>
{({ open }) => (
<>
<DisclosureButton className="flex w-full justify-between p-6">
<div className="flex items-center gap-2 text-primary">
<ShoppingCartIcon className="w-6" />
<Text>{open ? 'Hide order summary' : 'Show order summary'}</Text>
{open ? <ChevronUpIcon className="w-4" /> : <ChevronDownIcon className="w-4" />}
</div>
<Price
amount={order.totalPrice!.amount}
currencyCode={order.totalPrice!.currencyCode}
/>
</DisclosureButton>
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0 -translate-y-6"
enterTo="opacity-100 translate-y-0"
leave="duration-300 ease-out"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 -translate-y-6"
>
<DisclosurePanel className="origin-top p-6 text-gray-500 transition">
<OrderSummary order={order} />
</DisclosurePanel>
</Transition>
</>
)}
</Disclosure>
<Divider hasSpacing={false} />
</div>
);
}

View File

@ -0,0 +1,73 @@
import Image from 'next/image';
import Price from 'components/price';
import Badge from 'components/ui/badge';
import Heading from 'components/ui/heading';
import Label from 'components/ui/label';
import Text from 'components/ui/text';
import { Order } from 'lib/shopify/types';
export default function OrderSummary({ order }: { order: Order }) {
return (
<div className="flex flex-col gap-6">
<Heading size="sm">Order Summary</Heading>
<div className="flex flex-col gap-6">
{order.lineItems.map((lineItem, index) => (
<div key={index} className="flex items-center gap-4">
<Badge content={lineItem.quantity!}>
<Image
src={lineItem.image.url}
alt={lineItem.image.altText}
width={lineItem.image.width}
height={lineItem.image.height}
className="rounded border"
/>
</Badge>
<div className="flex flex-col gap-2">
<Text>{lineItem.title}</Text>
<Label>{lineItem.sku}</Label>
</div>
<Price
className="text-sm"
amount={lineItem.price!.amount}
currencyCode={lineItem.price!.currencyCode}
/>
</div>
))}
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<div className="flex items-center justify-between">
<Text>Subtotal</Text>
<Price
className="text-sm font-semibold"
amount={order.totalPrice!.amount}
currencyCode={order.totalPrice!.currencyCode}
/>
</div>
<div className="flex items-center justify-between">
<Text>Shipping</Text>
{order.shippingMethod?.price.amount !== '0.0' ? (
<Price
className="text-sm font-semibold"
amount={order.shippingMethod!.price.amount}
currencyCode={order.shippingMethod!.price.currencyCode}
/>
) : (
<Text className="font-semibold">Free</Text>
)}
</div>
</div>
<div className="flex items-center justify-between">
<Heading as="span" size="sm">
Total
</Heading>
<Price
className="font-semibold"
amount={order.totalPrice!.amount}
currencyCode={order.totalPrice!.currencyCode}
/>
</div>
</div>
</div>
);
}

View File

@ -43,7 +43,9 @@ const buttonVariants = tv({
// hover color // hover color
'hover:bg-primary-empahsis', 'hover:bg-primary-empahsis',
// disabled // disabled
'disabled:bg-primary-muted' 'disabled:bg-primary-muted',
'hover:bg-primary-emphasis',
'pressed:bg-primary-emphasis/80'
] ]
}, },
secondary: { secondary: {

View File

@ -8,15 +8,16 @@ const cardStyles = tv({
variants: { variants: {
outlined: { outlined: {
true: 'border bg-white', true: 'border bg-white',
false: {} false: ''
}, },
elevated: { elevated: {
true: 'shadow-lg shadow-content/10 bg-white', true: 'shadow-lg bg-white',
false: {} false: ''
} }
}, },
defaultVariants: { defaultVariants: {
outlined: true outlined: true,
elevated: false
} }
}); });
@ -27,8 +28,13 @@ interface CardProps extends React.ComponentPropsWithoutRef<'div'>, VariantProps<
const Card = React.forwardRef<HTMLDivElement, CardProps>( const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, asChild, outlined, elevated, ...props }, forwardedRef) => { ({ className, asChild, outlined, elevated, ...props }, forwardedRef) => {
const Component = asChild ? Slot : 'div'; const Component = asChild ? Slot : 'div';
return ( return (
<Component ref={forwardedRef} className={cardStyles({ outlined, className })} {...props} /> <Component
ref={forwardedRef}
className={cardStyles({ outlined, elevated, className })}
{...props}
/>
); );
} }
); );

View File

@ -20,6 +20,7 @@ const orderCard = /* GraphQL */ `
edges { edges {
node { node {
title title
quantity
image { image {
altText altText
height height