mirror of
https://github.com/vercel/commerce.git
synced 2025-05-11 04:07:50 +00:00
Merge branch 'main' of github.com:Car-Part-Planet/storefront
This commit is contained in:
commit
d12c264a1c
@ -14,6 +14,9 @@ import { toPrintDate } from 'lib/utils';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import ActivateWarranty from 'components/orders/activate-warranty';
|
||||
import OrderStatuses from 'components/orders/order-statuses';
|
||||
import Divider from 'components/divider';
|
||||
import { CoreReturn } from 'components/orders/core-return';
|
||||
|
||||
function Unfulfilled({ order }: { order: Order }) {
|
||||
// Build a map of line item IDs to quantities fulfilled
|
||||
@ -222,10 +225,13 @@ export default async function OrderPage({ params }: { params: { id: string } })
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<OrderStatuses order={order} className="hidden flex-wrap gap-2 lg:flex" />
|
||||
<OrderConfirmation order={order} />
|
||||
<ActivateWarranty order={order} />
|
||||
<CoreReturn order={order} />
|
||||
</div>
|
||||
</div>
|
||||
<OrderStatuses order={order} className="my-6 flex flex-wrap gap-2 lg:hidden" />
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="flex flex-1 flex-col gap-6">
|
||||
<Fulfillments order={order} />
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/outline';
|
||||
import Divider from 'components/divider';
|
||||
import ActivateWarranty from 'components/orders/activate-warranty';
|
||||
import MobileOrderActions from 'components/orders/mobile-order-actions';
|
||||
import OrderConfirmation from 'components/orders/order-confirmation';
|
||||
import OrderStatuses from 'components/orders/order-statuses';
|
||||
import OrdersHeader from 'components/orders/orders-header';
|
||||
import Price from 'components/price';
|
||||
import { Button } from 'components/ui';
|
||||
@ -28,7 +30,7 @@ export default async function AccountPage() {
|
||||
<h3 className="sr-only">
|
||||
Order placed on <time dateTime={order.createdAt}>{order.createdAt}</time>
|
||||
</h3>
|
||||
<div className="flex items-center border-b border-gray-200 p-4 sm:grid sm:grid-cols-4 sm:gap-x-6 sm:p-6">
|
||||
<div className="flex items-center p-4 sm:grid sm:grid-cols-4 sm:gap-x-6 sm:p-6">
|
||||
<dl className="grid flex-1 grid-cols-2 gap-x-6 text-sm sm:col-span-3 sm:grid-cols-3 lg:col-span-2">
|
||||
<div>
|
||||
<dt className="font-medium text-gray-900">Order</dt>
|
||||
@ -66,6 +68,10 @@ export default async function AccountPage() {
|
||||
<OrderConfirmation order={order} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 pt-0 sm:p-6 sm:pt-0">
|
||||
<OrderStatuses order={order} className="flex flex-wrap gap-4" />
|
||||
</div>
|
||||
<Divider hasSpacing={false} />
|
||||
|
||||
<h4 className="sr-only">Items</h4>
|
||||
<ul role="list" className="divide-y divide-gray-200">
|
||||
|
@ -12,7 +12,7 @@ const divider = tv({
|
||||
element: 'w-full h-[1px] '
|
||||
},
|
||||
vertical: {
|
||||
root: 'flex justify-between items-stretch text-tremor-default text-tremor-content',
|
||||
root: 'flex justify-between items-stretch text-tremor-default text-tremor-content h-full',
|
||||
element: 'h-full w-[1px]'
|
||||
}
|
||||
},
|
||||
@ -42,12 +42,17 @@ const divider = tv({
|
||||
type DividerProps = {
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
hasSpacing?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
export default function Divider({ orientation = 'horizontal', hasSpacing = true }: DividerProps) {
|
||||
export default function Divider({
|
||||
orientation = 'horizontal',
|
||||
hasSpacing = true,
|
||||
className
|
||||
}: DividerProps) {
|
||||
const { root, element } = divider({ orientation, hasSpacing });
|
||||
|
||||
return (
|
||||
<div className={root()}>
|
||||
<div className={root({ className })}>
|
||||
<span className={element()} />
|
||||
</div>
|
||||
);
|
||||
|
18
components/form/combobox-button.tsx
Normal file
18
components/form/combobox-button.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { ComboboxButtonProps, ComboboxButton as HeadlessComboboxButton } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/16/solid';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export default function ComboboxButton({ className, ...props }: ComboboxButtonProps) {
|
||||
return (
|
||||
<HeadlessComboboxButton
|
||||
className={clsx(
|
||||
'group absolute inset-y-0 right-0 px-2.5',
|
||||
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="fill-black/60 group-data-[hover]:fill-black size-5" />
|
||||
</HeadlessComboboxButton>
|
||||
);
|
||||
}
|
21
components/form/combobox-input.tsx
Normal file
21
components/form/combobox-input.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { ComboboxInputProps } from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export default function ComboboxInput({ className, ...props }: ComboboxInputProps) {
|
||||
return (
|
||||
<ComboboxInput
|
||||
className={clsx(
|
||||
'w-full rounded border border-gray-200',
|
||||
'py-1.5 pl-3 pr-8 text-sm',
|
||||
'ring-2 ring-transparent',
|
||||
'focus:outline-none focus-visible:outline-none',
|
||||
'data-[disabled]:cursor-not-allowed data-[autofocus]:border-0',
|
||||
'data-[focus]:border-transparent data-[disabled]:opacity-50',
|
||||
'data-[focus]:ring-2 data-[autofocus]:ring-secondary',
|
||||
'data-[focus]:ring-secondary data-[focus]:ring-offset-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
16
components/form/combobox-option.tsx
Normal file
16
components/form/combobox-option.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { ComboboxOption as HeadlessCombobox, ComboboxOptionProps } from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export default function ComboboxOption({ className, ...props }: ComboboxOptionProps) {
|
||||
return (
|
||||
<HeadlessCombobox
|
||||
className={clsx(
|
||||
'flex cursor-default select-none items-center gap-2',
|
||||
'rounded-lg px-3 py-1.5 text-sm/6',
|
||||
'data-[focus]:bg-secondary/10',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
18
components/form/combobox-options.tsx
Normal file
18
components/form/combobox-options.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import {
|
||||
ComboboxOptionsProps,
|
||||
ComboboxOptions as HeadlessComboboxOptions
|
||||
} from '@headlessui/react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export default function ComboboxOption({ className, ...props }: ComboboxOptionsProps) {
|
||||
return (
|
||||
<HeadlessComboboxOptions
|
||||
className={clsx(
|
||||
'z-10 w-[var(--input-width)] rounded-xl',
|
||||
'border border-gray-200 bg-white p-1 [--anchor-gap:6px] empty:hidden',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -164,3 +164,63 @@ export const confirmOrder = async ({ order, content, formData }: ConfirmOrderOpt
|
||||
console.log('activateWarranty action', error);
|
||||
}
|
||||
};
|
||||
|
||||
export async function returnCore() {
|
||||
// const rawFormData = [
|
||||
// getMetafieldValue(
|
||||
// 'coreReturnZip',
|
||||
// {
|
||||
// key: '',
|
||||
// value: formData.get('name') as string | null,
|
||||
// type: 'file_reference'
|
||||
// },
|
||||
// order
|
||||
// ),
|
||||
// getMetafieldValue(
|
||||
// 'warrantyActivationInstallation',
|
||||
// {
|
||||
// key: 'warranty_activation_installation',
|
||||
// value: installationFileId,
|
||||
// type: 'file_reference'
|
||||
// },
|
||||
// order
|
||||
// ),
|
||||
// getMetafieldValue(
|
||||
// 'warrantyActivationSelfInstall',
|
||||
// {
|
||||
// key: 'warranty_activation_self_install',
|
||||
// value: formData.get('warranty_activation_self_install') === 'on' ? 'true' : 'false',
|
||||
// type: 'boolean'
|
||||
// },
|
||||
// order
|
||||
// ),
|
||||
// getMetafieldValue(
|
||||
// 'warrantyActivationMileage',
|
||||
// {
|
||||
// key: 'warranty_activation_mileage',
|
||||
// value: formData.get('warranty_activation_mileage') as string | null,
|
||||
// type: 'number_integer'
|
||||
// },
|
||||
// order
|
||||
// ),
|
||||
// getMetafieldValue(
|
||||
// 'warrantyActivationVIN',
|
||||
// {
|
||||
// key: 'warranty_activation_vin',
|
||||
// value: formData.get('warranty_activation_vin') as string | null,
|
||||
// type: 'single_line_text_field'
|
||||
// },
|
||||
// order
|
||||
// )
|
||||
// ];
|
||||
try {
|
||||
// await updateOrderMetafields({
|
||||
// orderId: order.id,
|
||||
// metafields: rawFormData
|
||||
// });
|
||||
|
||||
revalidateTag(TAGS.orderMetafields);
|
||||
} catch (error) {
|
||||
console.log('activateWarranty action', error);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { Order, WarrantyStatus } from 'lib/shopify/types';
|
||||
import { isBeforeToday } from 'lib/utils';
|
||||
import { useState } from 'react';
|
||||
import ActivateWarrantyModal from './activate-warranty-modal';
|
||||
import WarrantyActivatedBadge from './warranty-activated-badge';
|
||||
import { Button } from 'components/ui';
|
||||
|
||||
type ActivateWarrantyModalProps = {
|
||||
@ -13,7 +12,7 @@ type ActivateWarrantyModalProps = {
|
||||
|
||||
const ActivateWarranty = ({ order }: ActivateWarrantyModalProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isWarrantyActivated = order?.warrantyStatus?.value === WarrantyStatus.Activated;
|
||||
const isActivated = order?.warrantyStatus?.value === WarrantyStatus.Activated;
|
||||
const isPassDeadline = isBeforeToday(order?.warrantyActivationDeadline?.value);
|
||||
const isOrderConfirmed = order?.orderConfirmation?.value;
|
||||
|
||||
@ -21,11 +20,7 @@ const ActivateWarranty = ({ order }: ActivateWarrantyModalProps) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isWarrantyActivated) {
|
||||
return <WarrantyActivatedBadge />;
|
||||
}
|
||||
|
||||
if (isPassDeadline) {
|
||||
if (isPassDeadline || isActivated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
94
components/orders/core-return-modal.tsx
Normal file
94
components/orders/core-return-modal.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { Dialog, DialogBackdrop, DialogPanel, Fieldset, Legend } from '@headlessui/react';
|
||||
import { Order } from 'lib/shopify/types';
|
||||
import { Button, Heading, Input } from 'components/ui';
|
||||
import StatesCombobox from 'components/states-combobox';
|
||||
import { useTransition } from 'react';
|
||||
// import { returnCore } from './actions';
|
||||
|
||||
export function CoreReturnModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
order
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
order: Order;
|
||||
}) {
|
||||
const [submitting, startTransition] = useTransition();
|
||||
|
||||
async function submitCoreReturn(formData: FormData) {
|
||||
startTransition(async () => {
|
||||
// returnCore(order, formData);
|
||||
console.log(formData);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
|
||||
<DialogBackdrop
|
||||
transition
|
||||
className="bg-black/30 fixed inset-0 duration-300 ease-out data-[closed]:opacity-0"
|
||||
/>
|
||||
<div className="fixed inset-0 w-screen overflow-y-auto p-4">
|
||||
<div className="flex min-h-full items-center justify-center">
|
||||
<DialogPanel
|
||||
transition
|
||||
className="w-full max-w-xl space-y-4 rounded bg-white p-5 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<Heading>Core Return</Heading>
|
||||
<Heading size="sm" className="text-content">
|
||||
Order {order.name}
|
||||
</Heading>
|
||||
</div>
|
||||
<form action={submitCoreReturn} className="flex flex-col gap-4">
|
||||
<Fieldset className="grid grid-cols-2 gap-4" disabled={submitting}>
|
||||
<Heading as={Legend} size="sm" className="col-span-2">
|
||||
Core Pickup Address
|
||||
</Heading>
|
||||
<Input name="name" label="Name" required className="col-span-2" />
|
||||
<Input
|
||||
defaultValue={order.customer?.emailAddress}
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
defaultValue={order.shippingAddress.phone}
|
||||
label="Phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
defaultValue={order.shippingAddress.address1}
|
||||
label="Address"
|
||||
name="address"
|
||||
required
|
||||
className="col-span-2"
|
||||
/>
|
||||
<Input
|
||||
defaultValue={order.shippingAddress.city}
|
||||
label="City"
|
||||
name="city"
|
||||
required
|
||||
/>
|
||||
<Input defaultValue={order.shippingAddress.zip} label="Zip" name="zip" required />
|
||||
<StatesCombobox defaultStateCode={order.shippingAddress.provinceCode} />
|
||||
</Fieldset>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="text" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button color="primary" variant="solid" disabled={submitting}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
8
components/orders/core-return-status.tsx
Normal file
8
components/orders/core-return-status.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { Chip } from 'components/ui';
|
||||
import { Order } from 'lib/shopify/types';
|
||||
|
||||
export default function CoreReturnStatus({ order }: { order: Order }) {
|
||||
if (!order.coreReturnStatus?.value) return null;
|
||||
|
||||
return <Chip level="warn">Core Return: {order.coreReturnStatus.value}</Chip>;
|
||||
}
|
15
components/orders/core-return.tsx
Normal file
15
components/orders/core-return.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
import { Button } from 'components/ui';
|
||||
import { Order } from 'lib/shopify/types';
|
||||
import { useState } from 'react';
|
||||
import { CoreReturnModal } from './core-return-modal';
|
||||
|
||||
export function CoreReturn({ order }: { order: Order }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>Core Return</Button>
|
||||
<CoreReturnModal order={order} isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -8,7 +8,7 @@ import Markdown from 'markdown-to-jsx';
|
||||
import { Order, OrderConfirmationContent } from 'lib/shopify/types';
|
||||
import { FormEventHandler, useEffect, useRef, useState, useTransition } from 'react';
|
||||
import { confirmOrder, fetchOrderConfirmationContent } from 'components/orders/actions';
|
||||
import { Button, Heading, Text, Label, Input, Skeleton } from 'components/ui';
|
||||
import { Button, Heading, Text, Label, Input } from 'components/ui';
|
||||
import LoadingDots from 'components/loading-dots';
|
||||
|
||||
function OrderConfirmationDetails({
|
||||
@ -228,7 +228,7 @@ export default function OrderConfirmationModal({
|
||||
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
|
||||
<DialogBackdrop
|
||||
transition
|
||||
className="fixed inset-0 bg-black/30 duration-300 ease-out data-[closed]:opacity-0"
|
||||
className="bg-black/30 fixed inset-0 duration-300 ease-out data-[closed]:opacity-0"
|
||||
/>
|
||||
<div className="fixed inset-0 w-screen overflow-y-auto p-4">
|
||||
<div className="flex min-h-full items-center justify-center">
|
||||
|
@ -156,7 +156,9 @@ export default function OrderConfirmationPdf({
|
||||
<Text style={styles.label}>Payment</Text>
|
||||
<View>
|
||||
<Text style={styles.p}>
|
||||
Ending with {order.transactions[0]!.paymentDetails.last4} -
|
||||
{order.transactions[0]?.paymentDetails
|
||||
? `Ending with ${order.transactions[0]!.paymentDetails.last4} - `
|
||||
: 'Manual - '}
|
||||
<PDFPrice
|
||||
amount={order.transactions[0]!.transactionAmount.amount}
|
||||
currencyCode={order.transactions[0]!.transactionAmount.currencyCode}
|
||||
|
8
components/orders/order-confirmation-status.tsx
Normal file
8
components/orders/order-confirmation-status.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { Chip } from 'components/ui';
|
||||
import { Order } from 'lib/shopify/types';
|
||||
|
||||
export default function OrderConfirmedStatus({ order }: { order: Order }) {
|
||||
if (order.orderConfirmation?.value) return null;
|
||||
|
||||
return <Chip level="error">Order Not Confirmed</Chip>;
|
||||
}
|
14
components/orders/order-statuses.tsx
Normal file
14
components/orders/order-statuses.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { Order } from 'lib/shopify/types';
|
||||
import CoreReturnStatus from './core-return-status';
|
||||
import WarrantyActivatedStatus from './warranty-activated-status';
|
||||
import OrderConfirmedStatus from './order-confirmation-status';
|
||||
|
||||
export default function OrderStatuses({ order, className }: { order: Order; className?: string }) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<OrderConfirmedStatus order={order} />
|
||||
<CoreReturnStatus order={order} />
|
||||
<WarrantyActivatedStatus order={order} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -20,7 +20,9 @@ export default function PaymentsDetails({ order, hideIcon }: { order: Order; hid
|
||||
|
||||
<div>
|
||||
<Text>
|
||||
Ending with {transaction.paymentDetails.last4} -
|
||||
{transaction?.paymentDetails
|
||||
? `Ending with ${transaction.paymentDetails.last4} - `
|
||||
: 'Manual - '}
|
||||
<Price
|
||||
as="span"
|
||||
amount={transaction.transactionAmount.amount}
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
const WarrantyActivatedBadge = () => {
|
||||
return (
|
||||
<span className="inline-flex h-fit items-center gap-x-2 rounded-md bg-green-50 px-2.5 py-2 text-sm font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
|
||||
<CheckCircleIcon className="h-5 w-5 text-green-500" aria-hidden="true" />
|
||||
Warranty Activated
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarrantyActivatedBadge;
|
21
components/orders/warranty-activated-status.tsx
Normal file
21
components/orders/warranty-activated-status.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import Chip, { ChipProps } from 'components/ui/chip';
|
||||
import { Order, WarrantyStatus } from 'lib/shopify/types';
|
||||
|
||||
const WarrantyActivatedStatus = ({ order }: { order: Order }) => {
|
||||
const warrantyStatus = order?.warrantyStatus?.value;
|
||||
const isOrderConfirmed = order?.orderConfirmation?.value;
|
||||
|
||||
if (!isOrderConfirmed || !warrantyStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let level: ChipProps['level'] = 'success';
|
||||
|
||||
if (warrantyStatus === WarrantyStatus.NotActivated) {
|
||||
level = 'warn';
|
||||
}
|
||||
|
||||
return <Chip level={level}>Warranty: {warrantyStatus}</Chip>;
|
||||
};
|
||||
|
||||
export default WarrantyActivatedStatus;
|
73
components/states-combobox.tsx
Normal file
73
components/states-combobox.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import Combobox from './ui/combobox';
|
||||
|
||||
const states = [
|
||||
{ name: 'Alabama', code: 'AL' },
|
||||
{ name: 'Alaska', code: 'AK' },
|
||||
{ name: 'Arizona', code: 'AZ' },
|
||||
{ name: 'Arkansas', code: 'AR' },
|
||||
{ name: 'California', code: 'CA' },
|
||||
{ name: 'Colorado', code: 'CO' },
|
||||
{ name: 'Connecticut', code: 'CT' },
|
||||
{ name: 'Delaware', code: 'DE' },
|
||||
{ name: 'Florida', code: 'FL' },
|
||||
{ name: 'Georgia', code: 'GA' },
|
||||
{ name: 'Hawaii', code: 'HI' },
|
||||
{ name: 'Idaho', code: 'ID' },
|
||||
{ name: 'Illinois', code: 'IL' },
|
||||
{ name: 'Indiana', code: 'IN' },
|
||||
{ name: 'Iowa', code: 'IA' },
|
||||
{ name: 'Kansas', code: 'KS' },
|
||||
{ name: 'Kentucky', code: 'KY' },
|
||||
{ name: 'Louisiana', code: 'LA' },
|
||||
{ name: 'Maine', code: 'ME' },
|
||||
{ name: 'Maryland', code: 'MD' },
|
||||
{ name: 'Massachusetts', code: 'MA' },
|
||||
{ name: 'Michigan', code: 'MI' },
|
||||
{ name: 'Minnesota', code: 'MN' },
|
||||
{ name: 'Mississippi', code: 'MS' },
|
||||
{ name: 'Missouri', code: 'MO' },
|
||||
{ name: 'Montana', code: 'MT' },
|
||||
{ name: 'Nebraska', code: 'NE' },
|
||||
{ name: 'Nevada', code: 'NV' },
|
||||
{ name: 'New Hampshire', code: 'NH' },
|
||||
{ name: 'New Jersey', code: 'NJ' },
|
||||
{ name: 'New Mexico', code: 'NM' },
|
||||
{ name: 'New York', code: 'NY' },
|
||||
{ name: 'North Carolina', code: 'NC' },
|
||||
{ name: 'North Dakota', code: 'ND' },
|
||||
{ name: 'Ohio', code: 'OH' },
|
||||
{ name: 'Oklahoma', code: 'OK' },
|
||||
{ name: 'Oregon', code: 'OR' },
|
||||
{ name: 'Pennsylvania', code: 'PA' },
|
||||
{ name: 'Rhode Island', code: 'RI' },
|
||||
{ name: 'South Carolina', code: 'SC' },
|
||||
{ name: 'South Dakota', code: 'SD' },
|
||||
{ name: 'Tennessee', code: 'TN' },
|
||||
{ name: 'Texas', code: 'TX' },
|
||||
{ name: 'Utah', code: 'UT' },
|
||||
{ name: 'Vermont', code: 'VT' },
|
||||
{ name: 'Virginia', code: 'VA' },
|
||||
{ name: 'Washington', code: 'WA' },
|
||||
{ name: 'West Virginia', code: 'WV' },
|
||||
{ name: 'Wisconsin', code: 'WI' },
|
||||
{ name: 'Wyoming', code: 'WY' }
|
||||
];
|
||||
|
||||
function findState(code: string) {
|
||||
return states.find((state) => state.code === code);
|
||||
}
|
||||
|
||||
export default function StatesCombobox({ defaultStateCode }: { defaultStateCode: string }) {
|
||||
return (
|
||||
<Combobox
|
||||
defaultValue={findState(defaultStateCode)}
|
||||
label="State"
|
||||
name="state"
|
||||
required
|
||||
options={states}
|
||||
displayKey="name"
|
||||
by="code"
|
||||
className="col-span-2"
|
||||
/>
|
||||
);
|
||||
}
|
@ -7,7 +7,7 @@ const cardStyles = tv({
|
||||
base: 'rounded p-6 text-left w-full',
|
||||
variants: {
|
||||
outlined: {
|
||||
true: 'border bg-white',
|
||||
true: 'border bg-white shadow-sm',
|
||||
false: ''
|
||||
},
|
||||
elevated: {
|
||||
|
63
components/ui/chip.tsx
Normal file
63
components/ui/chip.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { CheckCircleIcon, ExclamationCircleIcon, XCircleIcon } from '@heroicons/react/24/solid';
|
||||
import clsx from 'clsx';
|
||||
import { VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
const chip = tv({
|
||||
slots: {
|
||||
root: 'inline-flex items-center gap-x-2 rounded-md px-2.5 py-2 text-sm font-medium ring-1 ring-inset',
|
||||
leadingIcon: 'h-5 w-5'
|
||||
},
|
||||
variants: {
|
||||
level: {
|
||||
success: {
|
||||
root: 'bg-green-50 text-green-700 ring-green-600/20'
|
||||
},
|
||||
warn: {
|
||||
root: 'bg-yellow-50 text-yellow-700 ring-yellow-600/20'
|
||||
},
|
||||
info: {
|
||||
root: 'bg-content/5 text-content-emphasis ring-content/20'
|
||||
},
|
||||
error: {
|
||||
root: 'bg-red-50 text-red-700 ring-red-600/20'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export interface LevelLeadingProps extends VariantProps<typeof chip> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ChipProps extends VariantProps<typeof chip> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function LevelLeadingIcon({ level, className }: LevelLeadingProps) {
|
||||
if (level === 'success') {
|
||||
return <CheckCircleIcon className={clsx(className, 'text-green-500')} aria-hidden="true" />;
|
||||
}
|
||||
|
||||
if (level === 'warn') {
|
||||
return (
|
||||
<ExclamationCircleIcon className={clsx(className, 'text-yellow-500')} aria-hidden="true" />
|
||||
);
|
||||
}
|
||||
|
||||
if (level === 'error') {
|
||||
return <XCircleIcon className={clsx(className, 'text-red-500')} aria-hidden="true" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Chip({ children, level, className }: ChipProps) {
|
||||
const { root, leadingIcon } = chip();
|
||||
return (
|
||||
<span className={root({ level, className })}>
|
||||
<LevelLeadingIcon level={level} className={leadingIcon()} />
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
163
components/ui/combobox.tsx
Normal file
163
components/ui/combobox.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Combobox as HeadlessCombobox,
|
||||
ComboboxProps as HeadlessComboboxProps,
|
||||
ComboboxButton,
|
||||
ComboboxInput,
|
||||
ComboboxOption,
|
||||
ComboboxOptions,
|
||||
Field,
|
||||
Label
|
||||
} from '@headlessui/react';
|
||||
import { AnchorProps } from '@headlessui/react/dist/internal/floating';
|
||||
import { ChevronDownIcon } from '@heroicons/react/16/solid';
|
||||
import { focusInput } from 'lib/utils';
|
||||
import get from 'lodash.get';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { tv } from 'tailwind-variants';
|
||||
|
||||
const combobox = tv({
|
||||
slots: {
|
||||
root: '',
|
||||
label: [
|
||||
'text-sm leading-none',
|
||||
'text-content-strong font-medium',
|
||||
'data-[disabled]:text-gray-400'
|
||||
],
|
||||
input: [
|
||||
// base
|
||||
'w-full relative block rounded-md border-0 shadow-sm outline-none transition sm:text-sm sm:leading-6',
|
||||
'mt-2 px-2.5 py-1.5',
|
||||
// border color
|
||||
'border-gray-300',
|
||||
// text color
|
||||
'text-gray-900',
|
||||
// ring
|
||||
'ring-1 ring-inset ring-gray-300',
|
||||
// placeholder color
|
||||
'placeholder-gray-400',
|
||||
// background color
|
||||
'bg-white',
|
||||
// disabled
|
||||
'data-[disabled]:border-gray-300 data-[disabled]:bg-gray-100 data-[disabled]:text-gray-400',
|
||||
// focus
|
||||
focusInput,
|
||||
// invalid
|
||||
'data-[invalid]:ring-2 data-[invalid]:ring-red-200 data-[invalid]:border-red-500'
|
||||
],
|
||||
button: [
|
||||
'group absolute inset-y-0 right-0 px-2.5 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50'
|
||||
],
|
||||
options: [
|
||||
'z-10 w-[var(--input-width)] rounded-xl border border-gray-200 bg-white p-1 [--anchor-gap:6px] empty:hidden'
|
||||
],
|
||||
option: [
|
||||
'flex cursor-default select-none items-center gap-2 rounded-lg px-3 py-1.5 text-sm/6 data-[focus]:bg-secondary/10'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
interface ComboboxProps<T extends Record<string, unknown>, TMultiple extends boolean | undefined>
|
||||
extends HeadlessComboboxProps<T, TMultiple> {
|
||||
options: T[];
|
||||
label: string;
|
||||
labelHidden?: boolean;
|
||||
autoFocus?: boolean;
|
||||
displayKey: keyof T;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
anchor?: AnchorProps;
|
||||
}
|
||||
|
||||
const Combobox = <T extends Record<string, unknown>, TMultiple extends boolean | undefined>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
labelHidden,
|
||||
displayKey,
|
||||
disabled,
|
||||
autoFocus,
|
||||
by,
|
||||
className,
|
||||
required,
|
||||
anchor,
|
||||
...props
|
||||
}: ComboboxProps<T, TMultiple>) => {
|
||||
const {
|
||||
root,
|
||||
label: labelStyles,
|
||||
input,
|
||||
button,
|
||||
options: optionsStyles,
|
||||
option: optionStyles
|
||||
} = combobox();
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const getDisplayValue = useCallback(
|
||||
(option: T | null) => {
|
||||
if (!option) return '';
|
||||
|
||||
if (typeof option[displayKey] === 'string') {
|
||||
return option[displayKey] as string;
|
||||
}
|
||||
|
||||
return get(option, `${displayKey as string}.value`) as string;
|
||||
},
|
||||
[displayKey]
|
||||
);
|
||||
|
||||
const filteredOptions =
|
||||
query === ''
|
||||
? options
|
||||
: options.filter((option) => {
|
||||
return getDisplayValue(option).toLocaleLowerCase().includes(query.toLowerCase());
|
||||
});
|
||||
|
||||
return (
|
||||
<Field disabled={disabled} className={root({ className })}>
|
||||
{!labelHidden && (
|
||||
<Label className={labelStyles()}>
|
||||
{label}
|
||||
{required && <span className="text-red-500"> *</span>}
|
||||
</Label>
|
||||
)}
|
||||
<HeadlessCombobox
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onClose={() => setQuery('')}
|
||||
disabled={disabled}
|
||||
by={by}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative">
|
||||
<ComboboxInput
|
||||
aria-label={label}
|
||||
displayValue={getDisplayValue}
|
||||
placeholder={`Select ${label}`}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
className={input()}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
<ComboboxButton className={button()}>
|
||||
<ChevronDownIcon className="fill-black/60 group-data-[hover]:fill-black size-5" />
|
||||
</ComboboxButton>
|
||||
</div>
|
||||
<ComboboxOptions anchor="bottom" className={optionsStyles()}>
|
||||
{filteredOptions.map((option) => (
|
||||
<ComboboxOption
|
||||
key={option[by as keyof T] as string}
|
||||
value={option}
|
||||
className={optionStyles()}
|
||||
>
|
||||
{getDisplayValue(option)}
|
||||
</ComboboxOption>
|
||||
))}
|
||||
</ComboboxOptions>
|
||||
</HeadlessCombobox>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default Combobox;
|
@ -22,7 +22,7 @@ const heading = tv(
|
||||
export interface HeadingProps extends VariantProps<typeof heading> {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export default function Heading({ children, className, size, as }: HeadingProps) {
|
||||
|
@ -12,3 +12,5 @@ export * from './skeleton';
|
||||
export { default as Skeleton } from './skeleton';
|
||||
export * from './text';
|
||||
export { default as Text } from './text';
|
||||
export { default as Chip } from './chip';
|
||||
export * from './chip';
|
||||
|
@ -70,7 +70,12 @@ function Input({
|
||||
const { root, label: labelStyles, input } = inputStyles({ hasError });
|
||||
return (
|
||||
<Field disabled={disabled} className={root({ className })}>
|
||||
{label && <Label className={labelStyles({ className: labelClassName })}>{label}</Label>}
|
||||
{label && (
|
||||
<Label className={labelStyles({ className: labelClassName })}>
|
||||
{label}
|
||||
{props.required && <span className="text-red-500"> *</span>}
|
||||
</Label>
|
||||
)}
|
||||
<HeadlessInput
|
||||
disabled={disabled}
|
||||
className={input({ className: inputClassName })}
|
||||
|
@ -8,10 +8,14 @@ const text = tv(
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm',
|
||||
lg: 'text-md'
|
||||
},
|
||||
bold: {
|
||||
true: 'font-medium'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md'
|
||||
size: 'md',
|
||||
bold: false
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -25,8 +29,8 @@ export interface TextProps extends VariantProps<typeof text> {
|
||||
as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||
}
|
||||
|
||||
export default function Text({ children, className, size, as }: TextProps) {
|
||||
export default function Text({ children, className, size, as, bold }: TextProps) {
|
||||
const Component = as || 'p';
|
||||
|
||||
return <Component className={text({ size, className })}>{children}</Component>;
|
||||
return <Component className={text({ size, bold, className })}>{children}</Component>;
|
||||
}
|
||||
|
@ -203,7 +203,6 @@ export async function refreshToken({ request, origin }: { request: NextRequest;
|
||||
return { success: false, message: `no_refresh_token` };
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log('data response from initial fetch to refresh', data);
|
||||
const { access_token, expires_in, refresh_token } = data;
|
||||
|
||||
const customerAccessToken = await exchangeAccessToken(
|
||||
@ -238,10 +237,7 @@ export async function checkExpires({
|
||||
let isExpired = false;
|
||||
if (parseInt(expiresAt, 10) - 1000 < new Date().getTime()) {
|
||||
isExpired = true;
|
||||
console.log('Isexpired is true, we are running refresh token!');
|
||||
const refresh = await refreshToken({ request, origin });
|
||||
console.log('refresh', refresh);
|
||||
//this will return success: true or success: false - depending on result of refresh
|
||||
return { ranRefresh: isExpired, refresh };
|
||||
}
|
||||
return { ranRefresh: isExpired, success: true };
|
||||
@ -364,8 +360,6 @@ export async function isLoggedIn(request: NextRequest, origin: string) {
|
||||
//return { success: false, message: `no_refresh_token` }
|
||||
} else {
|
||||
const refreshData = isExpired?.refresh?.data;
|
||||
//console.log ("refresh data", refreshData)
|
||||
console.log('We used the refresh token, so now going to reset the token and cookies');
|
||||
const newCustomerAccessToken = refreshData?.customerAccessToken;
|
||||
const expires_in = refreshData?.expires_in;
|
||||
//const test_expires_in = 180 //to test to see if it expires in 60 seconds!
|
||||
@ -468,7 +462,6 @@ export async function authorize(request: NextRequest, origin: string) {
|
||||
//sets an expires time 2 minutes before expiration which we can use in refresh strategy
|
||||
//const test_expires_in = 180 //to test to see if it expires in 60 seconds!
|
||||
const expiresAt = new Date(new Date().getTime() + (expires_in! - 120) * 1000).getTime() + '';
|
||||
console.log('expires at', expiresAt);
|
||||
|
||||
return await createAllCookies({
|
||||
response: authResponse,
|
||||
|
@ -50,6 +50,33 @@ const orderMetafieldsFragment = /* GraphQL */ `
|
||||
orderConfirmation: metafield(namespace: "custom", key: "customer_confirmation") {
|
||||
value
|
||||
}
|
||||
coreReturnStatus: metafield(namespace: "custom", key: "core_status") {
|
||||
value
|
||||
}
|
||||
coreReturnDeadline: metafield(namespace: "custom", key: "core_return_deadline") {
|
||||
value
|
||||
}
|
||||
coreReturnName: metafield(namespace: "custom", key: "core_return_name") {
|
||||
value
|
||||
}
|
||||
coreReturnEmail: metafield(namespace: "custom", key: "core_return_email") {
|
||||
value
|
||||
}
|
||||
coreReturnPhone: metafield(namespace: "custom", key: "core_return_phone") {
|
||||
value
|
||||
}
|
||||
coreReturnAddress: metafield(namespace: "custom", key: "core_return_address") {
|
||||
value
|
||||
}
|
||||
coreReturnCity: metafield(namespace: "custom", key: "core_return_city") {
|
||||
value
|
||||
}
|
||||
coreReturnState: metafield(namespace: "custom", key: "core_return_state") {
|
||||
value
|
||||
}
|
||||
coreReturnZip: metafield(namespace: "custom", key: "core_return_zip") {
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -136,6 +136,9 @@ const userAgent = '*';
|
||||
const placeholderProductImage =
|
||||
'https://cdn.shopify.com/shopifycloud/customer-account-web/production/assets/8bc6556601c510713d76.svg';
|
||||
|
||||
const placeholderPaymentIcon =
|
||||
'https://cdn.shopify.com/shopifycloud/customer-account-web/production/assets/7bea2f.svg';
|
||||
|
||||
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
|
||||
const adminAccessToken = process.env.SHOPIFY_ADMIN_API_ACCESS_TOKEN!;
|
||||
|
||||
@ -601,15 +604,17 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
|
||||
const orderTransactions: Transaction[] = shopifyOrder.transactions?.map((transaction) => ({
|
||||
processedAt: transaction.processedAt,
|
||||
paymentIcon: {
|
||||
url: transaction.paymentIcon.url,
|
||||
altText: transaction.paymentIcon.altText,
|
||||
url: transaction.paymentIcon?.url || placeholderPaymentIcon,
|
||||
altText: transaction.paymentIcon?.altText || 'Payment Icon',
|
||||
width: 100,
|
||||
height: 100
|
||||
},
|
||||
paymentDetails: {
|
||||
last4: transaction.paymentDetails.last4,
|
||||
cardBrand: transaction.paymentDetails.cardBrand
|
||||
},
|
||||
paymentDetails: transaction.paymentDetails
|
||||
? {
|
||||
last4: transaction.paymentDetails.last4,
|
||||
cardBrand: transaction.paymentDetails.cardBrand
|
||||
}
|
||||
: undefined,
|
||||
transactionAmount: reshapeMoney(transaction.transactionAmount.presentmentMoney)!
|
||||
}));
|
||||
|
||||
@ -646,7 +651,17 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
|
||||
warrantyActivationOdometer: shopifyOrder.warrantyActivationOdometer,
|
||||
warrantyActivationSelfInstall: shopifyOrder.warrantyActivationSelfInstall,
|
||||
warrantyActivationVIN: shopifyOrder.warrantyActivationVIN,
|
||||
orderConfirmation: shopifyOrder.orderConfirmation
|
||||
orderConfirmation: shopifyOrder.orderConfirmation,
|
||||
coreReturnStatus: shopifyOrder.coreReturnStatus,
|
||||
coreReturnDeadline: shopifyOrder.coreReturnDeadline,
|
||||
coreReturnName: shopifyOrder.coreReturnName,
|
||||
coreReturnAddress: shopifyOrder.coreReturnAddress,
|
||||
coreReturnEmail: shopifyOrder.coreReturnEmail,
|
||||
coreReturnPhone: shopifyOrder.coreReturnPhone,
|
||||
coreReturnCity: shopifyOrder.coreReturnCity,
|
||||
coreReturnState: shopifyOrder.coreReturnState,
|
||||
coreReturnZip: shopifyOrder.coreReturnZip,
|
||||
coreReturnDescription: shopifyOrder.coreReturnDescription
|
||||
};
|
||||
|
||||
if (shopifyOrder.customer) {
|
||||
|
@ -115,7 +115,7 @@ export type Fulfillment = {
|
||||
export type Transaction = {
|
||||
processedAt: string;
|
||||
paymentIcon: Image;
|
||||
paymentDetails: {
|
||||
paymentDetails?: {
|
||||
last4: string;
|
||||
cardBrand: string;
|
||||
};
|
||||
@ -206,8 +206,8 @@ type ShopifyShippingLine = {
|
||||
type ShopifyOrderTransaction = {
|
||||
id: string;
|
||||
processedAt: string;
|
||||
paymentIcon: ShopifyPaymentIconImage;
|
||||
paymentDetails: ShopifyCardPaymentDetails;
|
||||
paymentIcon: ShopifyPaymentIconImage | null;
|
||||
paymentDetails: ShopifyCardPaymentDetails | null;
|
||||
transactionAmount: ShopifyMoneyBag;
|
||||
giftCardDetails: ShopifyGiftCardDetails | null;
|
||||
status: string;
|
||||
@ -901,6 +901,14 @@ export enum WarrantyStatus {
|
||||
LimitedActivated = 'Limited Activation'
|
||||
}
|
||||
|
||||
export enum CoreReturnStatus {
|
||||
CoreNeeded = 'Core Needed',
|
||||
PickupRequested = 'Pickup Requested',
|
||||
BOLCreated = 'BOL Created',
|
||||
CoreReceived = 'Core Received',
|
||||
CoreRefunded = 'Core Refunded'
|
||||
}
|
||||
|
||||
export type ShopifyOrderMetafield = {
|
||||
orderConfirmation: ShopifyMetafield | null;
|
||||
warrantyStatus: ShopifyMetafield | null;
|
||||
@ -910,6 +918,16 @@ export type ShopifyOrderMetafield = {
|
||||
warrantyActivationSelfInstall: ShopifyMetafield | null;
|
||||
warrantyActivationVIN: ShopifyMetafield | null;
|
||||
warrantyActivationMileage: ShopifyMetafield | null;
|
||||
coreReturnStatus: ShopifyMetafield | null;
|
||||
coreReturnDeadline: ShopifyMetafield | null;
|
||||
coreReturnName: ShopifyMetafield | null;
|
||||
coreReturnAddress: ShopifyMetafield | null;
|
||||
coreReturnEmail: ShopifyMetafield | null;
|
||||
coreReturnPhone: ShopifyMetafield | null;
|
||||
coreReturnCity: ShopifyMetafield | null;
|
||||
coreReturnState: ShopifyMetafield | null;
|
||||
coreReturnZip: ShopifyMetafield | null;
|
||||
coreReturnDescription: ShopifyMetafield | null;
|
||||
};
|
||||
|
||||
export type File = {
|
||||
|
@ -8,11 +8,7 @@ export function cx(...args: ClassValue[]) {
|
||||
}
|
||||
export const focusInput = [
|
||||
// base
|
||||
'focus:ring-2',
|
||||
// ring color
|
||||
'focus:ring-blue-200 focus:dark:ring-blue-700/30',
|
||||
// border color
|
||||
'focus:border-blue-500 focus:dark:border-blue-700'
|
||||
'focus:ring-2 focus:ring-offset-4'
|
||||
];
|
||||
|
||||
export const hasErrorInput = [
|
||||
|
Loading…
x
Reference in New Issue
Block a user