add core return to orders

This commit is contained in:
tedraykov 2024-07-08 17:48:27 +03:00
parent 49c52b0129
commit 7c9bc1f786
32 changed files with 722 additions and 57 deletions

View File

@ -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} />

View File

@ -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">

View File

@ -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>
);

View 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>
);
}

View 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}
/>
);
}

View 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}
/>
);
}

View 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}
/>
);
}

View File

@ -164,3 +164,63 @@ export const confirmOrder = async ({ order, content, formData }: ConfirmOrderOpt
console.log('activateWarranty action', error);
}
};
export async function returnCore(order: Order, formData: FormData) {
const rawFormData = [
getMetafieldValue(
'coreReturnZip',
{
key: 'warranty_activation_odometer',
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);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,93 @@
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);
});
}
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>
);
}

View 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>;
}

View 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)} />
</>
);
}

View File

@ -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">

View File

@ -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}

View 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>;
}

View 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>
);
}

View File

@ -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}

View File

@ -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;

View 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;

View 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"
/>
);
}

View File

@ -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
View 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
View 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;

View File

@ -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) {

View File

@ -14,3 +14,5 @@ export { default as Skeleton } from './skeleton';
export * from './skeleton';
export { default as Text } from './text';
export * from './text';
export { default as Chip } from './chip';
export * from './chip';

View File

@ -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 })}

View File

@ -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>;
}

View File

@ -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,

View File

@ -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
}
}
`;

View File

@ -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!;
@ -597,15 +600,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)!
}));
@ -642,7 +647,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) {
@ -1182,14 +1197,24 @@ export const getFile = async (id: string) => {
};
export async function getProductFilters(
{ collection }: { collection: string },
{ collection, make }: { collection: string; make?: string | string[] },
filterId: string
): Promise<Filter | null | undefined> {
const [namespace, metafieldKey] = MAKE_FILTER_ID.split('.').slice(-2);
const _make = Array.isArray(make) ? make : make ? [make] : undefined;
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
query: getProductFiltersQuery,
tags: [TAGS.collections, TAGS.products],
variables: {
handle: collection
handle: collection,
...(_make
? {
filters: _make.map((make) => ({
productMetafield: { namespace, key: metafieldKey, value: make }
}))
}
: {})
}
});

View File

@ -105,7 +105,7 @@ export type Fulfillment = {
export type Transaction = {
processedAt: string;
paymentIcon: Image;
paymentDetails: {
paymentDetails?: {
last4: string;
cardBrand: string;
};
@ -196,8 +196,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;
@ -879,6 +879,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;
@ -888,6 +896,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 = {

View 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 = [