mirror of
https://github.com/vercel/commerce.git
synced 2025-05-13 05:07:51 +00:00
commit
eec6518905
@ -1,18 +1,19 @@
|
|||||||
import { ArrowLeftIcon, CheckCircleIcon, TruckIcon } from '@heroicons/react/24/outline';
|
import { ArrowLeftIcon, CheckCircleIcon, TruckIcon } from '@heroicons/react/24/outline';
|
||||||
import ActivateWarranty from 'components/orders/activate-warranty';
|
import OrderConfirmation from 'components/orders/order-confirmation';
|
||||||
|
import PaymentsDetails from 'components/orders/payment-details';
|
||||||
import OrderSummary from 'components/orders/order-summary';
|
import OrderSummary from 'components/orders/order-summary';
|
||||||
import OrderSummaryMobile from 'components/orders/order-summary-mobile';
|
import OrderSummaryMobile from 'components/orders/order-summary-mobile';
|
||||||
import Price from 'components/price';
|
|
||||||
import Badge from 'components/ui/badge';
|
import Badge from 'components/ui/badge';
|
||||||
import { Card } from 'components/ui/card';
|
import { Card } from 'components/ui';
|
||||||
import Heading from 'components/ui/heading';
|
import Heading from 'components/ui/heading';
|
||||||
import Label from 'components/ui/label';
|
import Label from 'components/ui/label';
|
||||||
import Text from 'components/ui/text';
|
import Text from 'components/ui/text';
|
||||||
import { getCustomerOrder, getOrderMetafields } from 'lib/shopify';
|
import { getCustomerOrder } from 'lib/shopify';
|
||||||
import { Fulfillment, Order } from 'lib/shopify/types';
|
import { Fulfillment, Order } from 'lib/shopify/types';
|
||||||
import { toPrintDate } from 'lib/utils';
|
import { toPrintDate } from 'lib/utils';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import ActivateWarranty from 'components/orders/activate-warranty';
|
||||||
|
|
||||||
function Unfulfilled({ order }: { order: Order }) {
|
function Unfulfilled({ order }: { order: Order }) {
|
||||||
// Build a map of line item IDs to quantities fulfilled
|
// Build a map of line item IDs to quantities fulfilled
|
||||||
@ -144,30 +145,6 @@ function Fulfillments({ order }: { order: Order }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaymentsDetails({ order }: { order: Order }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{order.transactions.map((transaction, index) => (
|
|
||||||
<div key={index} className="flex items-start gap-2">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img src={transaction.paymentIcon.url} alt={transaction.paymentIcon.altText} width={36} />
|
|
||||||
<div>
|
|
||||||
<Text>
|
|
||||||
Ending with {transaction.paymentDetails.last4} -
|
|
||||||
<Price
|
|
||||||
as="span"
|
|
||||||
amount={transaction.transactionAmount.amount}
|
|
||||||
currencyCode={transaction.transactionAmount.currencyCode}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
<Label>{toPrintDate(transaction.processedAt)}</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function OrderDetails({ order }: { order: Order }) {
|
function OrderDetails({ order }: { order: Order }) {
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-col gap-4">
|
<Card className="flex flex-col gap-4">
|
||||||
@ -228,10 +205,7 @@ function OrderDetails({ order }: { order: Order }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function OrderPage({ params }: { params: { id: string } }) {
|
export default async function OrderPage({ params }: { params: { id: string } }) {
|
||||||
const [order, orderMetafields] = await Promise.all([
|
const order = await getCustomerOrder(params.id);
|
||||||
getCustomerOrder(params.id),
|
|
||||||
getOrderMetafields(params.id)
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -247,7 +221,10 @@ export default async function OrderPage({ params }: { params: { id: string } })
|
|||||||
<Label>Confirmed {toPrintDate(order.processedAt)}</Label>
|
<Label>Confirmed {toPrintDate(order.processedAt)}</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ActivateWarranty order={order} orderMetafields={orderMetafields} />
|
<div className="flex items-start gap-2">
|
||||||
|
<OrderConfirmation order={order} />
|
||||||
|
<ActivateWarranty order={order} />
|
||||||
|
</div>
|
||||||
</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">
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import { InformationCircleIcon } from '@heroicons/react/24/outline';
|
import { InformationCircleIcon } from '@heroicons/react/24/outline';
|
||||||
import ActivateWarranty from 'components/orders/activate-warranty';
|
|
||||||
import MobileOrderActions from 'components/orders/mobile-order-actions';
|
import MobileOrderActions from 'components/orders/mobile-order-actions';
|
||||||
import OrdersHeader from 'components/orders/orders-header';
|
import OrdersHeader from 'components/orders/orders-header';
|
||||||
import Price from 'components/price';
|
import Price from 'components/price';
|
||||||
import { getCustomerOrders, getOrdersMetafields } from 'lib/shopify';
|
import { getCustomerOrders } from 'lib/shopify';
|
||||||
import { toPrintDate } from 'lib/utils';
|
import { isBeforeToday, toPrintDate } from 'lib/utils';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { Button } from 'components/ui';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const OrderConfirmation = dynamic(() => import('components/orders/order-confirmation'));
|
||||||
|
const ActivateWarranty = dynamic(() => import('components/orders/activate-warranty'));
|
||||||
|
|
||||||
export default async function AccountPage() {
|
export default async function AccountPage() {
|
||||||
const [orders, ordersMetafields] = await Promise.all([
|
const orders = await getCustomerOrders();
|
||||||
getCustomerOrders(),
|
|
||||||
getOrdersMetafields()
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-5 sm:py-10">
|
<div className="py-5 sm:py-10">
|
||||||
@ -54,17 +55,19 @@ export default async function AccountPage() {
|
|||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<MobileOrderActions order={order} orderMetafields={ordersMetafields[order.id]} />
|
<MobileOrderActions order={order} />
|
||||||
|
|
||||||
<div className="hidden lg:col-span-2 lg:flex lg:items-center lg:justify-end lg:space-x-4">
|
<div className="hidden lg:col-span-2 lg:flex lg:items-center lg:justify-end lg:space-x-4">
|
||||||
<Link
|
<Link href={`/account/orders/${order.normalizedId}`} passHref legacyBehavior>
|
||||||
href={`/account/orders/${order.normalizedId}`}
|
<Button as="a">
|
||||||
className="flex items-center justify-center rounded-md border border-gray-300 bg-white px-2.5 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
View Order
|
||||||
>
|
|
||||||
<span>View Order</span>
|
|
||||||
<span className="sr-only">{order.normalizedId}</span>
|
<span className="sr-only">{order.normalizedId}</span>
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<ActivateWarranty order={order} orderMetafields={ordersMetafields[order.id]} />
|
{!isBeforeToday(order?.warrantyActivationDeadline?.value) && (
|
||||||
|
<ActivateWarranty order={order} />
|
||||||
|
)}
|
||||||
|
{!order.orderConfirmation && <OrderConfirmation order={order} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
7
app/api/orders/confirmation/route.ts
Normal file
7
app/api/orders/confirmation/route.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { getOrderConfirmationContent } from 'lib/shopify';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const data = await getOrderConfirmationContent();
|
||||||
|
|
||||||
|
return Response.json({ ...data });
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { getMenu, getMetaobjects } from 'lib/shopify';
|
import { getAllMetaobjects, getMenu } from 'lib/shopify';
|
||||||
import FiltersList from './filters-list';
|
import FiltersList from './filters-list';
|
||||||
|
|
||||||
const title: Record<string, string> = {
|
const title: Record<string, string> = {
|
||||||
@ -12,9 +12,9 @@ const title: Record<string, string> = {
|
|||||||
const { STORE_PREFIX } = process.env;
|
const { STORE_PREFIX } = process.env;
|
||||||
|
|
||||||
const HomePageFilters = async () => {
|
const HomePageFilters = async () => {
|
||||||
const yearsData = getMetaobjects('make_model_year_composite');
|
const yearsData = getAllMetaobjects('make_model_year_composite');
|
||||||
const modelsData = getMetaobjects('make_model_composite');
|
const modelsData = getAllMetaobjects('make_model_composite');
|
||||||
const makesData = getMetaobjects('make');
|
const makesData = getAllMetaobjects('make');
|
||||||
|
|
||||||
const [years, models, makes] = await Promise.all([yearsData, modelsData, makesData]);
|
const [years, models, makes] = await Promise.all([yearsData, modelsData, makesData]);
|
||||||
const menu = await getMenu('main-menu');
|
const menu = await getMenu('main-menu');
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { renderToBuffer } from '@react-pdf/renderer';
|
||||||
|
import OrderConfirmationPdf from 'components/orders/order-confirmation-pdf';
|
||||||
import { handleUploadFile } from 'components/form/file-input/actions';
|
import { handleUploadFile } from 'components/form/file-input/actions';
|
||||||
import { TAGS } from 'lib/constants';
|
import { TAGS } from 'lib/constants';
|
||||||
import { updateOrderMetafields } from 'lib/shopify';
|
import { updateOrderMetafields } from 'lib/shopify';
|
||||||
import { ShopifyOrderMetafield, UpdateOrderMetafieldInput } from 'lib/shopify/types';
|
import {
|
||||||
|
Order,
|
||||||
|
OrderConfirmationContent,
|
||||||
|
ShopifyOrderMetafield,
|
||||||
|
UpdateOrderMetafieldInput
|
||||||
|
} from 'lib/shopify/types';
|
||||||
import { revalidateTag } from 'next/cache';
|
import { revalidateTag } from 'next/cache';
|
||||||
|
|
||||||
const getMetafieldValue = (
|
const getMetafieldValue = (
|
||||||
@ -16,11 +23,7 @@ const getMetafieldValue = (
|
|||||||
: { ...newValue, namespace: 'custom' };
|
: { ...newValue, namespace: 'custom' };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const activateWarranty = async (
|
export const activateWarranty = async (order: Order, formData: FormData) => {
|
||||||
orderId: string,
|
|
||||||
formData: FormData,
|
|
||||||
orderMetafields?: ShopifyOrderMetafield
|
|
||||||
) => {
|
|
||||||
let odometerFileId = null;
|
let odometerFileId = null;
|
||||||
let installationFileId = null;
|
let installationFileId = null;
|
||||||
const odometerFile = formData.get('warranty_activation_odometer');
|
const odometerFile = formData.get('warranty_activation_odometer');
|
||||||
@ -32,7 +35,7 @@ export const activateWarranty = async (
|
|||||||
if (installationFile) {
|
if (installationFile) {
|
||||||
installationFileId = await handleUploadFile({ file: installationFile as File });
|
installationFileId = await handleUploadFile({ file: installationFile as File });
|
||||||
}
|
}
|
||||||
console.log(formData.get('warranty_activation_self_install'));
|
|
||||||
// https://shopify.dev/docs/api/admin-graphql/2024-01/mutations/orderUpdate
|
// https://shopify.dev/docs/api/admin-graphql/2024-01/mutations/orderUpdate
|
||||||
const rawFormData = [
|
const rawFormData = [
|
||||||
getMetafieldValue(
|
getMetafieldValue(
|
||||||
@ -42,7 +45,7 @@ export const activateWarranty = async (
|
|||||||
value: odometerFileId,
|
value: odometerFileId,
|
||||||
type: 'file_reference'
|
type: 'file_reference'
|
||||||
},
|
},
|
||||||
orderMetafields
|
order
|
||||||
),
|
),
|
||||||
getMetafieldValue(
|
getMetafieldValue(
|
||||||
'warrantyActivationInstallation',
|
'warrantyActivationInstallation',
|
||||||
@ -51,7 +54,7 @@ export const activateWarranty = async (
|
|||||||
value: installationFileId,
|
value: installationFileId,
|
||||||
type: 'file_reference'
|
type: 'file_reference'
|
||||||
},
|
},
|
||||||
orderMetafields
|
order
|
||||||
),
|
),
|
||||||
getMetafieldValue(
|
getMetafieldValue(
|
||||||
'warrantyActivationSelfInstall',
|
'warrantyActivationSelfInstall',
|
||||||
@ -60,7 +63,7 @@ export const activateWarranty = async (
|
|||||||
value: formData.get('warranty_activation_self_install') === 'on' ? 'true' : 'false',
|
value: formData.get('warranty_activation_self_install') === 'on' ? 'true' : 'false',
|
||||||
type: 'boolean'
|
type: 'boolean'
|
||||||
},
|
},
|
||||||
orderMetafields
|
order
|
||||||
),
|
),
|
||||||
getMetafieldValue(
|
getMetafieldValue(
|
||||||
'warrantyActivationMileage',
|
'warrantyActivationMileage',
|
||||||
@ -69,7 +72,7 @@ export const activateWarranty = async (
|
|||||||
value: formData.get('warranty_activation_mileage') as string | null,
|
value: formData.get('warranty_activation_mileage') as string | null,
|
||||||
type: 'number_integer'
|
type: 'number_integer'
|
||||||
},
|
},
|
||||||
orderMetafields
|
order
|
||||||
),
|
),
|
||||||
getMetafieldValue(
|
getMetafieldValue(
|
||||||
'warrantyActivationVIN',
|
'warrantyActivationVIN',
|
||||||
@ -78,13 +81,76 @@ export const activateWarranty = async (
|
|||||||
value: formData.get('warranty_activation_vin') as string | null,
|
value: formData.get('warranty_activation_vin') as string | null,
|
||||||
type: 'single_line_text_field'
|
type: 'single_line_text_field'
|
||||||
},
|
},
|
||||||
orderMetafields
|
order
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateOrderMetafields({
|
await updateOrderMetafields({
|
||||||
orderId,
|
orderId: order.id,
|
||||||
|
metafields: rawFormData
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidateTag(TAGS.orderMetafields);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('activateWarranty action', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function generateOrderConfirmationPDF(
|
||||||
|
order: Order,
|
||||||
|
content: OrderConfirmationContent,
|
||||||
|
signature1: string,
|
||||||
|
signature2: string,
|
||||||
|
signDate: string
|
||||||
|
) {
|
||||||
|
return renderToBuffer(
|
||||||
|
<OrderConfirmationPdf
|
||||||
|
order={order}
|
||||||
|
content={content}
|
||||||
|
signature1={signature1}
|
||||||
|
signature2={signature2}
|
||||||
|
date={signDate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfirmOrderOptions = {
|
||||||
|
order: Order;
|
||||||
|
content: OrderConfirmationContent;
|
||||||
|
formData: FormData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const confirmOrder = async ({ order, content, formData }: ConfirmOrderOptions) => {
|
||||||
|
const signature1 = formData.get('signature1') as string;
|
||||||
|
const signature2 = formData.get('signature2') as string;
|
||||||
|
const signDate = formData.get('date') as string;
|
||||||
|
|
||||||
|
const pdfBuffer = await generateOrderConfirmationPDF(
|
||||||
|
order,
|
||||||
|
content,
|
||||||
|
signature1,
|
||||||
|
signature2,
|
||||||
|
signDate
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileName = `${new Date().getTime()}-${order.name}-signaturePdf.pdf`;
|
||||||
|
const file = new File([pdfBuffer], fileName, { type: 'application/pdf' });
|
||||||
|
|
||||||
|
const confirmationPDFId = await handleUploadFile({ file });
|
||||||
|
|
||||||
|
const rawFormData = [
|
||||||
|
{
|
||||||
|
key: 'customer_confirmation',
|
||||||
|
value: confirmationPDFId,
|
||||||
|
type: 'file_reference',
|
||||||
|
namespace: 'custom'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateOrderMetafields({
|
||||||
|
orderId: order.id,
|
||||||
metafields: rawFormData
|
metafields: rawFormData
|
||||||
});
|
});
|
||||||
|
|
@ -6,23 +6,17 @@ import CheckboxField from 'components/form/checkbox-field';
|
|||||||
import FileInput from 'components/form/file-input';
|
import FileInput from 'components/form/file-input';
|
||||||
import Input from 'components/form/input-field';
|
import Input from 'components/form/input-field';
|
||||||
import LoadingDots from 'components/loading-dots';
|
import LoadingDots from 'components/loading-dots';
|
||||||
import { ShopifyOrderMetafield } from 'lib/shopify/types';
|
import { Order } from 'lib/shopify/types';
|
||||||
import { FormEventHandler, useRef, useTransition } from 'react';
|
import { FormEventHandler, useRef, useTransition } from 'react';
|
||||||
import { activateWarranty } from './actions';
|
import { activateWarranty } from './actions';
|
||||||
|
|
||||||
type ActivateWarrantyModalProps = {
|
type ActivateWarrantyModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
orderId: string;
|
order: Order;
|
||||||
orderMetafields?: ShopifyOrderMetafield;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ActivateWarrantyModal({
|
function ActivateWarrantyModal({ onClose, isOpen, order }: ActivateWarrantyModalProps) {
|
||||||
onClose,
|
|
||||||
isOpen,
|
|
||||||
orderId,
|
|
||||||
orderMetafields
|
|
||||||
}: ActivateWarrantyModalProps) {
|
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
@ -33,7 +27,7 @@ function ActivateWarrantyModal({
|
|||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
await activateWarranty(orderId, formData, orderMetafields);
|
await activateWarranty(order, formData);
|
||||||
form.reset();
|
form.reset();
|
||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
@ -59,28 +53,28 @@ function ActivateWarrantyModal({
|
|||||||
<FileInput
|
<FileInput
|
||||||
label="Odometer"
|
label="Odometer"
|
||||||
name="warranty_activation_odometer"
|
name="warranty_activation_odometer"
|
||||||
fileId={orderMetafields?.warrantyActivationOdometer?.value}
|
fileId={order?.warrantyActivationOdometer?.value}
|
||||||
/>
|
/>
|
||||||
<FileInput
|
<FileInput
|
||||||
label="Installation Receipt"
|
label="Installation Receipt"
|
||||||
name="warranty_activation_installation"
|
name="warranty_activation_installation"
|
||||||
fileId={orderMetafields?.warrantyActivationInstallation?.value}
|
fileId={order?.warrantyActivationInstallation?.value}
|
||||||
/>
|
/>
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
label="Self Installed"
|
label="Self Installed"
|
||||||
name="warranty_activation_self_install"
|
name="warranty_activation_self_install"
|
||||||
defaultChecked={orderMetafields?.warrantyActivationSelfInstall?.value === 'true'}
|
defaultChecked={order?.warrantyActivationSelfInstall?.value === 'true'}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Customer Mileage"
|
label="Customer Mileage"
|
||||||
name="warranty_activation_mileage"
|
name="warranty_activation_mileage"
|
||||||
type="number"
|
type="number"
|
||||||
defaultValue={orderMetafields?.warrantyActivationMileage?.value}
|
defaultValue={order?.warrantyActivationMileage?.value}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Customer VIN"
|
label="Customer VIN"
|
||||||
name="warranty_activation_vin"
|
name="warranty_activation_vin"
|
||||||
defaultValue={orderMetafields?.warrantyActivationVIN?.value}
|
defaultValue={order?.warrantyActivationVIN?.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex w-full justify-end gap-4">
|
<div className="mt-4 flex w-full justify-end gap-4">
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Order, ShopifyOrderMetafield, WarrantyStatus } from 'lib/shopify/types';
|
import { Order, WarrantyStatus } from 'lib/shopify/types';
|
||||||
import { isBeforeToday } from 'lib/utils';
|
import { isBeforeToday } from 'lib/utils';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import ActivateWarrantyModal from './activate-warranty-modal';
|
import ActivateWarrantyModal from './activate-warranty-modal';
|
||||||
import WarrantyActivatedBadge from './warranty-activated-badge';
|
import WarrantyActivatedBadge from './warranty-activated-badge';
|
||||||
|
import { Button } from 'components/ui';
|
||||||
|
|
||||||
type ActivateWarrantyModalProps = {
|
type ActivateWarrantyModalProps = {
|
||||||
order: Order;
|
order: Order;
|
||||||
orderMetafields?: ShopifyOrderMetafield;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActivateWarranty = ({ order, orderMetafields }: ActivateWarrantyModalProps) => {
|
const ActivateWarranty = ({ order }: ActivateWarrantyModalProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const isWarrantyActivated = orderMetafields?.warrantyStatus?.value === WarrantyStatus.Activated;
|
const isWarrantyActivated = order?.warrantyStatus?.value === WarrantyStatus.Activated;
|
||||||
const isPassDeadline = isBeforeToday(orderMetafields?.warrantyActivationDeadline?.value);
|
const isPassDeadline = isBeforeToday(order?.warrantyActivationDeadline?.value);
|
||||||
|
|
||||||
if (isWarrantyActivated) {
|
if (isWarrantyActivated) {
|
||||||
return <WarrantyActivatedBadge />;
|
return <WarrantyActivatedBadge />;
|
||||||
@ -26,18 +26,8 @@ const ActivateWarranty = ({ order, orderMetafields }: ActivateWarrantyModalProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button onClick={() => setIsOpen(true)}>Activate Warranty</Button>
|
||||||
className="flex h-fit items-center justify-center rounded-md border border-gray-300 bg-white px-2.5 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} order={order} />
|
||||||
onClick={() => setIsOpen(true)}
|
|
||||||
>
|
|
||||||
Activate Warranty
|
|
||||||
</button>
|
|
||||||
<ActivateWarrantyModal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={() => setIsOpen(false)}
|
|
||||||
orderId={order.id}
|
|
||||||
orderMetafields={orderMetafields}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
import { Button, Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
||||||
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
|
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Order, ShopifyOrderMetafield, WarrantyStatus } from 'lib/shopify/types';
|
import { Order, WarrantyStatus } from 'lib/shopify/types';
|
||||||
import { isBeforeToday } from 'lib/utils';
|
import { isBeforeToday } from 'lib/utils';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import ActivateWarrantyModal from './activate-warranty-modal';
|
import ActivateWarrantyModal from './activate-warranty-modal';
|
||||||
|
|
||||||
const MobileOrderActions = ({
|
const OrderConfirmationModal = dynamic(() => import('./order-confirmation-modal'));
|
||||||
order,
|
|
||||||
orderMetafields
|
const MobileOrderActions = ({ order }: { order: Order }) => {
|
||||||
}: {
|
const [isWarrantyOpen, setIsWarrantyOpen] = useState(false);
|
||||||
order: Order;
|
const [isOrderConfirmaionOpen, setIsOrderConfirmationOpen] = useState(false);
|
||||||
orderMetafields?: ShopifyOrderMetafield;
|
|
||||||
}) => {
|
const isWarrantyActivated = order?.warrantyStatus?.value === WarrantyStatus.Activated;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const isPassDeadline = isBeforeToday(order?.warrantyActivationDeadline?.value);
|
||||||
const isWarrantyActivated = orderMetafields?.warrantyStatus?.value === WarrantyStatus.Activated;
|
const isOrderConfirmed = order?.orderConfirmation?.value;
|
||||||
const isPassDeadline = isBeforeToday(orderMetafields?.warrantyActivationDeadline?.value);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -56,22 +56,43 @@ const MobileOrderActions = ({
|
|||||||
focus ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
focus ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||||
'flex w-full px-4 py-2 text-sm'
|
'flex w-full px-4 py-2 text-sm'
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsWarrantyOpen(true)}
|
||||||
>
|
>
|
||||||
Activate Warranty
|
Activate Warranty
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{!isOrderConfirmed && (
|
||||||
|
<MenuItem>
|
||||||
|
{({ focus }) => (
|
||||||
|
<Button
|
||||||
|
className={clsx(
|
||||||
|
focus ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||||
|
'flex w-full px-4 py-2 text-sm'
|
||||||
|
)}
|
||||||
|
onClick={() => setIsOrderConfirmationOpen(true)}
|
||||||
|
>
|
||||||
|
Confirm Order
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MenuItems>
|
</MenuItems>
|
||||||
</Menu>
|
</Menu>
|
||||||
<ActivateWarrantyModal
|
<ActivateWarrantyModal
|
||||||
isOpen={isOpen}
|
isOpen={isWarrantyOpen}
|
||||||
onClose={() => setIsOpen(false)}
|
onClose={() => setIsWarrantyOpen(false)}
|
||||||
orderId={order.id}
|
order={order}
|
||||||
orderMetafields={orderMetafields}
|
|
||||||
/>
|
/>
|
||||||
|
{!isOrderConfirmed && (
|
||||||
|
<OrderConfirmationModal
|
||||||
|
isOpen={isOrderConfirmaionOpen}
|
||||||
|
onClose={() => setIsOrderConfirmationOpen(false)}
|
||||||
|
order={order}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
292
components/orders/order-confirmation-modal.tsx
Normal file
292
components/orders/order-confirmation-modal.tsx
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
|
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react';
|
||||||
|
import { toPrintDate } from 'lib/utils';
|
||||||
|
import PaymentsDetails from './payment-details';
|
||||||
|
import Price from 'components/price';
|
||||||
|
import Divider from 'components/divider';
|
||||||
|
import Markdown from 'markdown-to-jsx';
|
||||||
|
import { Order, OrderConfirmationContent } from 'lib/shopify/types';
|
||||||
|
import { FormEventHandler, useEffect, useRef, useState, useTransition } from 'react';
|
||||||
|
import { confirmOrder } from 'components/orders/actions';
|
||||||
|
import { Button, Heading, Text, Label, Skeleton, InputLabel, Input } from 'components/ui';
|
||||||
|
|
||||||
|
function OrderConfirmationDetails({
|
||||||
|
content,
|
||||||
|
order
|
||||||
|
}: {
|
||||||
|
content: OrderConfirmationContent;
|
||||||
|
order: Order;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<figure>
|
||||||
|
<Image
|
||||||
|
src={content?.logo?.url}
|
||||||
|
alt={content?.logo?.altText || 'Logo'}
|
||||||
|
width={content?.logo?.width || 400}
|
||||||
|
height={content?.logo?.height || 400}
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<Heading className="text-primary" size="sm">
|
||||||
|
ORDER INFORMATION:
|
||||||
|
</Heading>
|
||||||
|
<div>
|
||||||
|
<Text>Order number: {order.name}</Text>
|
||||||
|
<Text>Email: {order.customer?.emailAddress}</Text>
|
||||||
|
<Text>Date: {toPrintDate(order.processedAt)}</Text>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Label>Shipping Address</Label>
|
||||||
|
<div>
|
||||||
|
<Text>
|
||||||
|
{order.shippingAddress!.firstName} {order.shippingAddress!.lastName}
|
||||||
|
</Text>
|
||||||
|
<Text>{order.shippingAddress!.address1}</Text>
|
||||||
|
{order.shippingAddress!.address2 && <Text>{order.shippingAddress!.address2}</Text>}
|
||||||
|
<Text>
|
||||||
|
{order.shippingAddress!.city} {order.shippingAddress!.provinceCode}{' '}
|
||||||
|
{order.shippingAddress!.zip}
|
||||||
|
</Text>
|
||||||
|
<Text>{order.shippingAddress!.country}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<Label>Billing Address</Label>
|
||||||
|
<div>
|
||||||
|
<Text>
|
||||||
|
{order.billingAddress!.firstName} {order.billingAddress!.lastName}
|
||||||
|
</Text>
|
||||||
|
<Text>{order.billingAddress!.address1}</Text>
|
||||||
|
{order.billingAddress!.address2 && <Text>{order.billingAddress!.address2}</Text>}
|
||||||
|
<Text>
|
||||||
|
{order.billingAddress!.city} {order.billingAddress!.provinceCode}{' '}
|
||||||
|
{order.billingAddress!.zip}
|
||||||
|
</Text>
|
||||||
|
<Text>{order.billingAddress!.country}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Payment</Label>
|
||||||
|
<PaymentsDetails order={order} hideIcon />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<Heading size="sm">Products</Heading>
|
||||||
|
<Divider />
|
||||||
|
<table className="w-full table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-start">
|
||||||
|
<Label>Product</Label>
|
||||||
|
</th>
|
||||||
|
<th className="text-start">
|
||||||
|
<Label>Quantity</Label>
|
||||||
|
</th>
|
||||||
|
<th className="text-start">
|
||||||
|
<Label>Price</Label>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{order.lineItems.map((lineItem, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className="py-4 text-start">
|
||||||
|
<Text className="max-w-sm">{lineItem.title}</Text>
|
||||||
|
</td>
|
||||||
|
<td className="text-start">
|
||||||
|
<Text>{lineItem.quantity}</Text>
|
||||||
|
</td>
|
||||||
|
<td className="text-start">
|
||||||
|
<Price
|
||||||
|
className="text-sm"
|
||||||
|
amount={lineItem.totalPrice!.amount}
|
||||||
|
currencyCode={lineItem.totalPrice!.currencyCode}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<Divider />
|
||||||
|
<div className="ml-auto flex w-60 flex-col gap-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Text>Subtotal</Text>
|
||||||
|
<Price
|
||||||
|
className="text-sm font-semibold"
|
||||||
|
amount={order.subtotal!.amount}
|
||||||
|
currencyCode={order.subtotal!.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 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>
|
||||||
|
<Markdown
|
||||||
|
options={{
|
||||||
|
overrides: {
|
||||||
|
h1: {
|
||||||
|
props: {
|
||||||
|
className: 'text-primary font-semibold mt-4 mb-2 text-xl'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
props: {
|
||||||
|
className: 'text-primary font-semibold mt-4 mb-2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
props: {
|
||||||
|
className: 'text-primary text-sm font-semibold mt-4 mb-2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
props: {
|
||||||
|
className: 'text-sm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
props: {
|
||||||
|
className: 'text-sm, text-primary underline'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content?.body || ''}
|
||||||
|
</Markdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default function OrderConfirmationModal({
|
||||||
|
order,
|
||||||
|
isOpen,
|
||||||
|
onClose
|
||||||
|
}: {
|
||||||
|
order: Order;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [orderConfirmationContent, setOrderConfirmationContent] =
|
||||||
|
useState<OrderConfirmationContent>();
|
||||||
|
const [, startTransition] = useTransition();
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOrderConfirmationContent = async () => {
|
||||||
|
const res = await fetch('/api/orders/confirmation');
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
setOrderConfirmationContent(data);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the order has already been confirmed, don't fetch the content
|
||||||
|
if (order.orderConfirmation) return;
|
||||||
|
|
||||||
|
fetchOrderConfirmationContent();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
const form = formRef.current;
|
||||||
|
if (!form) return;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
startTransition(async () => {
|
||||||
|
await confirmOrder({
|
||||||
|
order,
|
||||||
|
content: orderConfirmationContent!,
|
||||||
|
formData
|
||||||
|
});
|
||||||
|
form.reset();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!loading && !orderConfirmationContent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<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="max-w-3xl space-y-4 rounded bg-white p-12 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<Skeleton />
|
||||||
|
) : (
|
||||||
|
<OrderConfirmationDetails content={orderConfirmationContent!} order={order} />
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleSubmit} ref={formRef} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<InputLabel htmlFor="date">Today's date</InputLabel>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
id="date"
|
||||||
|
name="date"
|
||||||
|
readOnly
|
||||||
|
value={new Date().toLocaleDateString('en-CA', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<InputLabel htmlFor="signature1">Print your name to sign</InputLabel>
|
||||||
|
<Input id="signature1" name="signature1" required />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<InputLabel htmlFor="signature2">
|
||||||
|
Credit card holder's electronic signature
|
||||||
|
</InputLabel>
|
||||||
|
<Input id="signature2" name="signature2" required />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="text">Cancel</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
disabled={loading}
|
||||||
|
isLoading={loading}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
298
components/orders/order-confirmation-pdf.tsx
Normal file
298
components/orders/order-confirmation-pdf.tsx
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import Markdown from 'markdown-to-jsx';
|
||||||
|
import { Document, Image, Page, Text, StyleSheet, View, Link } from '@react-pdf/renderer';
|
||||||
|
import { Order, OrderConfirmationContent } from 'lib/shopify/types';
|
||||||
|
import { toPrintDate } from 'lib/utils';
|
||||||
|
|
||||||
|
const PDFPrice = ({
|
||||||
|
style,
|
||||||
|
amount,
|
||||||
|
currencyCode = 'USD'
|
||||||
|
}: {
|
||||||
|
style?: any;
|
||||||
|
amount: string;
|
||||||
|
currencyCode: string;
|
||||||
|
}) => {
|
||||||
|
const price = parseFloat(amount);
|
||||||
|
|
||||||
|
// Return 'Included' if price is 0
|
||||||
|
if (price === 0) {
|
||||||
|
return <Text style={style}>Free</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text style={style}>
|
||||||
|
{new Intl.NumberFormat(undefined, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currencyCode,
|
||||||
|
currencyDisplay: 'narrowSymbol'
|
||||||
|
}).format(price)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OrderConfirmationPdf({
|
||||||
|
content,
|
||||||
|
order,
|
||||||
|
signature1,
|
||||||
|
signature2,
|
||||||
|
date
|
||||||
|
}: {
|
||||||
|
content: OrderConfirmationContent;
|
||||||
|
order: Order;
|
||||||
|
signature1: string;
|
||||||
|
signature2: string;
|
||||||
|
date: string;
|
||||||
|
}) {
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
logo: {
|
||||||
|
width: 300,
|
||||||
|
marginHorizontal: 'auto',
|
||||||
|
marginBottom: 24
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
padding: 48,
|
||||||
|
paddingVertical: 64
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 700,
|
||||||
|
marginBottom: 12,
|
||||||
|
color: content.color
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 700,
|
||||||
|
marginBottom: 12,
|
||||||
|
color: content.color
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
marginBottom: 12,
|
||||||
|
color: content.color
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
fontSize: 10,
|
||||||
|
marginBottom: 12
|
||||||
|
},
|
||||||
|
span: {
|
||||||
|
fontSize: 10
|
||||||
|
},
|
||||||
|
strong: {
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: 10
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
color: content.color,
|
||||||
|
fontSize: 10,
|
||||||
|
textDecoration: 'underline'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#555'
|
||||||
|
},
|
||||||
|
tableRow: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8
|
||||||
|
},
|
||||||
|
tableCell: {
|
||||||
|
textAlign: 'left',
|
||||||
|
fontSize: 10,
|
||||||
|
paddingVertical: 12
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page size="A4" style={styles.page}>
|
||||||
|
<View>
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
||||||
|
<Image src={content?.logo?.url} style={styles.logo} />
|
||||||
|
<Text style={styles.h3}>ORDER INFORMATION:</Text>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.span}>Order number: {order.name}</Text>
|
||||||
|
<Text style={styles.span}>Email: {order.customer?.emailAddress}</Text>
|
||||||
|
<Text style={styles.p}>Date: {toPrintDate(order.processedAt)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={[styles.label, { marginBottom: 6 }]}>Shipping Address</Text>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.span}>
|
||||||
|
{order.shippingAddress!.firstName} {order.shippingAddress!.lastName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.span}>{order.shippingAddress!.address1}</Text>
|
||||||
|
{order.shippingAddress!.address2 && (
|
||||||
|
<Text style={styles.p}>{order.shippingAddress!.address2}</Text>
|
||||||
|
)}
|
||||||
|
<Text style={styles.span}>
|
||||||
|
{order.shippingAddress!.city} {order.shippingAddress!.provinceCode}{' '}
|
||||||
|
{order.shippingAddress!.zip}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.p}>{order.shippingAddress!.country}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={[styles.label, { marginBottom: 6 }]}>Billing Address</Text>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.span}>
|
||||||
|
{order.billingAddress!.firstName} {order.billingAddress!.lastName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.span}>{order.billingAddress!.address1}</Text>
|
||||||
|
{order.billingAddress!.address2 && (
|
||||||
|
<Text style={styles.span}>{order.billingAddress!.address2}</Text>
|
||||||
|
)}
|
||||||
|
<Text style={styles.span}>
|
||||||
|
{order.billingAddress!.city} {order.billingAddress!.provinceCode}{' '}
|
||||||
|
{order.billingAddress!.zip}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.p}>{order.billingAddress!.country}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.label}>Payment</Text>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.p}>
|
||||||
|
Ending with {order.transactions[0]!.paymentDetails.last4} -
|
||||||
|
<PDFPrice
|
||||||
|
amount={order.transactions[0]!.transactionAmount.amount}
|
||||||
|
currencyCode={order.transactions[0]!.transactionAmount.currencyCode}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<View style={styles.tableRow}>
|
||||||
|
<Text style={[styles.label, { width: '70%' }]}>Products</Text>
|
||||||
|
<Text style={[styles.label, { width: '15%' }]}>Quantity</Text>
|
||||||
|
<Text style={[styles.label, { width: '15%' }]}>Price</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
borderBottom: '1px solid #333',
|
||||||
|
marginTop: 6,
|
||||||
|
marginBottom: 8
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{order.lineItems.map((lineItem, index) => (
|
||||||
|
<View key={index} style={styles.tableRow}>
|
||||||
|
<Text style={[styles.tableCell, { width: '70%' }]}>{lineItem.title}</Text>
|
||||||
|
<Text style={[styles.tableCell, { width: '15%' }]}>{lineItem.quantity}</Text>
|
||||||
|
<PDFPrice
|
||||||
|
style={[styles.tableCell, { width: '15%' }]}
|
||||||
|
amount={lineItem.totalPrice!.amount}
|
||||||
|
currencyCode={lineItem.totalPrice!.currencyCode}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
borderBottom: '1px solid black',
|
||||||
|
marginTop: 6,
|
||||||
|
marginBottom: 8
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View style={{ width: '150px', marginLeft: 'auto', marginRight: '20' }}>
|
||||||
|
<View
|
||||||
|
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<Text style={styles.span}>Subtotal</Text>
|
||||||
|
<PDFPrice
|
||||||
|
style={styles.span}
|
||||||
|
amount={order.subtotal!.amount}
|
||||||
|
currencyCode={order.subtotal!.currencyCode}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<Text style={styles.span}>Shipping</Text>
|
||||||
|
<PDFPrice
|
||||||
|
style={styles.span}
|
||||||
|
amount={order.shippingMethod!.price.amount}
|
||||||
|
currencyCode={order.shippingMethod!.price.currencyCode}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}
|
||||||
|
>
|
||||||
|
<Text style={styles.span}>Total</Text>
|
||||||
|
<PDFPrice
|
||||||
|
style={styles.span}
|
||||||
|
amount={order.totalPrice!.amount}
|
||||||
|
currencyCode={order.totalPrice!.currencyCode}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
<Page size="A4" style={styles.page}>
|
||||||
|
<Markdown
|
||||||
|
options={{
|
||||||
|
wrapper: View,
|
||||||
|
overrides: {
|
||||||
|
h1: {
|
||||||
|
component: Text,
|
||||||
|
props: {
|
||||||
|
style: styles.h1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
component: Text,
|
||||||
|
props: {
|
||||||
|
style: styles.h2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
component: Text,
|
||||||
|
props: {
|
||||||
|
style: styles.h3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
component: Text,
|
||||||
|
props: {
|
||||||
|
style: styles.p
|
||||||
|
}
|
||||||
|
},
|
||||||
|
strong: {
|
||||||
|
component: Text,
|
||||||
|
props: {
|
||||||
|
style: styles.strong
|
||||||
|
}
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
component: Link,
|
||||||
|
props: {
|
||||||
|
style: styles.a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content.body}
|
||||||
|
</Markdown>
|
||||||
|
<View style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
<Text style={[styles.p, { flex: 1 }]}>Date:</Text>
|
||||||
|
<Text style={[styles.p, { flex: 1 }]}>{toPrintDate(date)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
<Text style={[styles.p, { flex: 1 }]}>Print your name to sign:</Text>
|
||||||
|
<Text style={[styles.p, { flex: 1 }]}>{signature1}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
|
<Text style={[styles.p, { flex: 1 }]}>
|
||||||
|
Credit card holder's electronic signature
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.p, { flex: 1 }]}>{signature2}</Text>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
}
|
24
components/orders/order-confirmation.tsx
Normal file
24
components/orders/order-confirmation.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'use client';
|
||||||
|
import { Button } from 'components/ui';
|
||||||
|
import { Order } from 'lib/shopify/types';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const OrderConfirmationModal = dynamic(() => import('./order-confirmation-modal'));
|
||||||
|
|
||||||
|
export default function OrderConfirmation({ order }: { order: Order }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
if (order.orderConfirmation) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="outlined" onClick={() => setIsOpen(true)}>
|
||||||
|
Confirm Order
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<OrderConfirmationModal isOpen={isOpen} onClose={() => setIsOpen(false)} order={order} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -54,8 +54,8 @@ export default function OrderSummary({ order }: { order: Order }) {
|
|||||||
<Text>Subtotal</Text>
|
<Text>Subtotal</Text>
|
||||||
<Price
|
<Price
|
||||||
className="text-sm font-semibold"
|
className="text-sm font-semibold"
|
||||||
amount={order.totalPrice!.amount}
|
amount={order.subtotal!.amount}
|
||||||
currencyCode={order.totalPrice!.currencyCode}
|
currencyCode={order.subtotal!.currencyCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
36
components/orders/payment-details.tsx
Normal file
36
components/orders/payment-details.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Text from 'components/ui/text';
|
||||||
|
import Label from 'components/ui/label';
|
||||||
|
import { Order } from 'lib/shopify/types';
|
||||||
|
import Price from 'components/price';
|
||||||
|
import { toPrintDate } from 'lib/utils';
|
||||||
|
|
||||||
|
export default function PaymentsDetails({ order, hideIcon }: { order: Order; hideIcon?: boolean }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{order.transactions.map((transaction, index) => (
|
||||||
|
<div key={index} className="flex items-start gap-2">
|
||||||
|
{!hideIcon && (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={transaction.paymentIcon.url}
|
||||||
|
alt={transaction.paymentIcon.altText}
|
||||||
|
width={36}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text>
|
||||||
|
Ending with {transaction.paymentDetails.last4} -
|
||||||
|
<Price
|
||||||
|
as="span"
|
||||||
|
amount={transaction.transactionAmount.amount}
|
||||||
|
currencyCode={transaction.transactionAmount.currencyCode}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
<Label>{toPrintDate(transaction.processedAt)}</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { CloseButton, Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react';
|
import { CloseButton, Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react';
|
||||||
import { ArrowRightIcon } from '@heroicons/react/16/solid';
|
import { ArrowRightIcon } from '@heroicons/react/16/solid';
|
||||||
import { Button } from 'components/button';
|
import { Button } from 'components/ui';
|
||||||
import useAuth from 'hooks/use-auth';
|
import useAuth from 'hooks/use-auth';
|
||||||
import { Menu } from 'lib/shopify/types';
|
import { Menu } from 'lib/shopify/types';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import clsx from 'clsx';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export default function Spinner({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={clsx('flex-1 animate-spin stroke-current stroke-[3]', className)}
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
|
|
||||||
className="stroke-current opacity-25"
|
|
||||||
/>
|
|
||||||
<path d="M12 2C6.47715 2 2 6.47715 2 12C2 14.7255 3.09032 17.1962 4.85857 19" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
@ -19,7 +19,7 @@ const badgeStyles = tv({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
interface BadgeProps extends VariantProps<typeof badgeStyles> {
|
export interface BadgeProps extends VariantProps<typeof badgeStyles> {
|
||||||
content: string | number;
|
content: string | number;
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
@ -3,7 +3,8 @@ import React from 'react';
|
|||||||
import { Button as ButtonBase, ButtonProps as ButtonBaseProps } from '@headlessui/react';
|
import { Button as ButtonBase, ButtonProps as ButtonBaseProps } from '@headlessui/react';
|
||||||
import { tv, type VariantProps } from 'tailwind-variants';
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Spinner from './spinner';
|
import LoadingDots from './loading-dots';
|
||||||
|
import { focusInput } from 'lib/utils';
|
||||||
|
|
||||||
const buttonVariants = tv({
|
const buttonVariants = tv({
|
||||||
slots: {
|
slots: {
|
||||||
@ -15,7 +16,8 @@ const buttonVariants = tv({
|
|||||||
// transition
|
// transition
|
||||||
'transition-all duration-100 ease-in-out',
|
'transition-all duration-100 ease-in-out',
|
||||||
// disabled
|
// disabled
|
||||||
'disabled:pointer-events-none disabled:shadow-none'
|
'disabled:pointer-events-none disabled:shadow-none',
|
||||||
|
focusInput
|
||||||
],
|
],
|
||||||
loading: 'pointer-events-none flex shrink-0 items-center justify-center gap-1.5'
|
loading: 'pointer-events-none flex shrink-0 items-center justify-center gap-1.5'
|
||||||
},
|
},
|
||||||
@ -36,11 +38,15 @@ const buttonVariants = tv({
|
|||||||
content: {}
|
content: {}
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
solid: {},
|
solid: {
|
||||||
outlined: {
|
root: 'border border-transparent shadow-sm'
|
||||||
root: 'border bg-white'
|
|
||||||
},
|
},
|
||||||
text: {}
|
outlined: {
|
||||||
|
root: 'border bg-white shadow-sm'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
root: 'border border-transparent'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
@ -49,20 +55,35 @@ const buttonVariants = tv({
|
|||||||
variant: 'solid',
|
variant: 'solid',
|
||||||
class: {
|
class: {
|
||||||
root: [
|
root: [
|
||||||
// border
|
|
||||||
'border-transparent',
|
|
||||||
// text color
|
// text color
|
||||||
'text-white',
|
'text-white',
|
||||||
// background color
|
// background color
|
||||||
'bg-primary',
|
'bg-primary',
|
||||||
// hover color
|
// hover color
|
||||||
'hover:bg-primary-empahsis',
|
'hover:bg-primary-emphasis',
|
||||||
// disabled
|
// disabled
|
||||||
'disabled:bg-primary-muted',
|
'disabled:bg-primary-muted',
|
||||||
'pressed:bg-primary-emphasis/80'
|
'pressed:bg-primary-emphasis/80'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
color: 'content',
|
||||||
|
variant: 'solid',
|
||||||
|
class: {
|
||||||
|
root: [
|
||||||
|
// text color
|
||||||
|
'text-white',
|
||||||
|
// background color
|
||||||
|
'bg-content',
|
||||||
|
// hover color
|
||||||
|
'hover:bg-content-emphasis',
|
||||||
|
// disabled
|
||||||
|
'disabled:bg-content-muted',
|
||||||
|
'pressed:bg-content-emphasis/80'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
variant: 'outlined',
|
variant: 'outlined',
|
||||||
@ -75,25 +96,62 @@ const buttonVariants = tv({
|
|||||||
// background color
|
// background color
|
||||||
'bg-white',
|
'bg-white',
|
||||||
// hover color
|
// hover color
|
||||||
'hover:bg-primary/10',
|
'hover:bg-primary/5',
|
||||||
// disabled
|
// disabled
|
||||||
'disabled:border-primary-muted disabled:text-primary-muted'
|
'disabled:border-primary-muted disabled:text-primary-muted'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'content',
|
||||||
|
variant: 'outlined',
|
||||||
|
class: {
|
||||||
|
root: [
|
||||||
|
// border
|
||||||
|
'border-content-subtle',
|
||||||
|
// text color
|
||||||
|
'text-content-emphasis',
|
||||||
|
// background color
|
||||||
|
'bg-white',
|
||||||
|
// hover color
|
||||||
|
'hover:bg-content/5',
|
||||||
|
// disabled
|
||||||
|
'disabled:border-content-muted disabled:text-content-muted'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'content',
|
||||||
|
variant: 'text',
|
||||||
|
class: {
|
||||||
|
root: [
|
||||||
|
// text color
|
||||||
|
'text-content-emphasis',
|
||||||
|
// background color
|
||||||
|
'bg-transparent',
|
||||||
|
// hover color
|
||||||
|
'hover:bg-content/5',
|
||||||
|
// disabled
|
||||||
|
'disabled:text-content-muted'
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: 'solid',
|
variant: 'outlined',
|
||||||
color: 'primary',
|
color: 'content',
|
||||||
size: 'md'
|
size: 'md'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ButtonProps extends Omit<ButtonBaseProps, 'color'>, VariantProps<typeof buttonVariants> {
|
export interface ButtonProps
|
||||||
|
extends Omit<ButtonBaseProps, 'color' | 'as'>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
loadingText?: string;
|
loadingText?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
as?: React.ElementType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
@ -105,14 +163,19 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
isLoading,
|
isLoading,
|
||||||
loadingText = 'Loading',
|
loadingText = 'Loading',
|
||||||
size,
|
size,
|
||||||
|
color,
|
||||||
variant,
|
variant,
|
||||||
|
as,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps,
|
}: ButtonProps,
|
||||||
forwardedRef
|
forwardedRef
|
||||||
) => {
|
) => {
|
||||||
const { loading, root } = buttonVariants({ variant, size });
|
const { loading, root } = buttonVariants({ variant, size, color });
|
||||||
|
|
||||||
|
const Component = as || 'button';
|
||||||
return (
|
return (
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
|
as={Component}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={clsx(root(), className)}
|
className={clsx(root(), className)}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
@ -120,7 +183,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<span className={loading()}>
|
<span className={loading()}>
|
||||||
<Spinner />
|
<LoadingDots />
|
||||||
<span className="sr-only">{loadingText}</span>
|
<span className="sr-only">{loadingText}</span>
|
||||||
<span>{loadingText}</span>
|
<span>{loadingText}</span>
|
||||||
</span>
|
</span>
|
||||||
@ -134,4 +197,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
|
|
||||||
Button.displayName = 'Button';
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
export { Button, buttonVariants, type ButtonProps };
|
export default Button;
|
@ -21,7 +21,9 @@ const cardStyles = tv({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
interface CardProps extends React.ComponentPropsWithoutRef<'div'>, VariantProps<typeof cardStyles> {
|
export interface CardProps
|
||||||
|
extends React.ComponentPropsWithoutRef<'div'>,
|
||||||
|
VariantProps<typeof cardStyles> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,4 +43,4 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|||||||
|
|
||||||
Card.displayName = 'Card';
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
export { Card, type CardProps };
|
export default Card;
|
||||||
|
28
components/ui/checkbox.tsx
Normal file
28
components/ui/checkbox.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CheckIcon } from '@heroicons/react/24/outline';
|
||||||
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
|
import { cn } from 'lib/utils';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
const Checkbox = forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'ring-offset-background focus-visible:ring-ring peer h-4 w-4 shrink-0 rounded-sm border border-dark focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-dark data-[state=checked]:text-white',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
));
|
||||||
|
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export default Checkbox;
|
@ -1,6 +1,7 @@
|
|||||||
import { VariantProps, tv } from 'tailwind-variants';
|
import { VariantProps, tv } from 'tailwind-variants';
|
||||||
|
|
||||||
const heading = tv({
|
const heading = tv(
|
||||||
|
{
|
||||||
base: [''],
|
base: [''],
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
@ -12,9 +13,13 @@ const heading = tv({
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
size: 'md'
|
size: 'md'
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
twMerge: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
interface HeadingProps extends VariantProps<typeof heading> {
|
export interface HeadingProps extends VariantProps<typeof heading> {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||||
|
18
components/ui/index.ts
Normal file
18
components/ui/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export { default as Badge } from './badge';
|
||||||
|
export * from './badge';
|
||||||
|
export { default as Button } from './button';
|
||||||
|
export * from './button';
|
||||||
|
export { default as Card } from './card';
|
||||||
|
export * from './card';
|
||||||
|
export { default as Checkbox } from './checkbox';
|
||||||
|
export { default as Heading } from './heading';
|
||||||
|
export { default as InputLabel } from './input-label';
|
||||||
|
export * from './input-label';
|
||||||
|
export { default as Input } from './input';
|
||||||
|
export * from './input';
|
||||||
|
export { default as Label } from './label';
|
||||||
|
export * from './label';
|
||||||
|
export { default as Skeleton } from './skeleton';
|
||||||
|
export * from './skeleton';
|
||||||
|
export { default as Text } from './text';
|
||||||
|
export * from './text';
|
33
components/ui/input-label.tsx
Normal file
33
components/ui/input-label.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as LabelPrimitives from '@radix-ui/react-label';
|
||||||
|
|
||||||
|
import { cx } from 'lib/utils';
|
||||||
|
|
||||||
|
export interface InputLabelProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof LabelPrimitives.Root> {
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputLabel = React.forwardRef<React.ElementRef<typeof LabelPrimitives.Root>, InputLabelProps>(
|
||||||
|
({ className, disabled, ...props }, forwardedRef) => (
|
||||||
|
<LabelPrimitives.Root
|
||||||
|
ref={forwardedRef}
|
||||||
|
className={cx(
|
||||||
|
// base
|
||||||
|
'text-sm leading-none',
|
||||||
|
// text color
|
||||||
|
'text-gray-900 dark:text-gray-50',
|
||||||
|
// disabled
|
||||||
|
{
|
||||||
|
'text-gray-400 dark:text-gray-600': disabled
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
InputLabel.displayName = 'InputLabel';
|
||||||
|
|
||||||
|
export default InputLabel;
|
70
components/ui/input.tsx
Normal file
70
components/ui/input.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
|
|
||||||
|
import { cx, focusInput, hasErrorInput } from 'lib/utils';
|
||||||
|
|
||||||
|
const inputStyles = tv({
|
||||||
|
base: [
|
||||||
|
// base
|
||||||
|
'relative block w-full appearance-none rounded-md border px-2.5 py-1.5 shadow-sm outline-none transition sm:text-sm',
|
||||||
|
// border color
|
||||||
|
'border-gray-300 dark:border-gray-800',
|
||||||
|
// text color
|
||||||
|
'text-gray-900 dark:text-gray-50',
|
||||||
|
// placeholder color
|
||||||
|
'placeholder-gray-400 dark:placeholder-gray-500',
|
||||||
|
// background color
|
||||||
|
'bg-white dark:bg-gray-950',
|
||||||
|
// disabled
|
||||||
|
'disabled:border-gray-300 disabled:bg-gray-100 disabled:text-gray-400',
|
||||||
|
'disabled:dark:border-gray-700 disabled:dark:bg-gray-800 disabled:dark:text-gray-500',
|
||||||
|
// file
|
||||||
|
[
|
||||||
|
'file:-my-1.5 file:-ml-2.5 file:h-[36px] file:cursor-pointer file:rounded-l-md file:rounded-r-none file:border-0 file:px-3 file:py-1.5 file:outline-none focus:outline-none disabled:pointer-events-none file:disabled:pointer-events-none',
|
||||||
|
'file:border-solid file:border-gray-300 file:bg-gray-50 file:text-gray-500 file:hover:bg-gray-100 file:dark:border-gray-800 file:dark:bg-gray-950 file:hover:dark:bg-gray-900/20 file:disabled:dark:border-gray-700',
|
||||||
|
'file:[border-inline-end-width:1px] file:[margin-inline-end:0.75rem]',
|
||||||
|
'file:disabled:bg-gray-100 file:disabled:text-gray-500 file:disabled:dark:bg-gray-800'
|
||||||
|
],
|
||||||
|
// focus
|
||||||
|
focusInput,
|
||||||
|
// invalid
|
||||||
|
'aria-[invalid=true]:dark:ring-red-400/20 aria-[invalid=true]:ring-2 aria-[invalid=true]:ring-red-200 aria-[invalid=true]:border-red-500 invalid:ring-2 invalid:ring-red-200 invalid:border-red-500'
|
||||||
|
],
|
||||||
|
variants: {
|
||||||
|
hasError: {
|
||||||
|
true: hasErrorInput
|
||||||
|
},
|
||||||
|
// number input
|
||||||
|
enableStepper: {
|
||||||
|
true: '[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
VariantProps<typeof inputStyles> {
|
||||||
|
inputClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
(
|
||||||
|
{ className, inputClassName, hasError, enableStepper, type, ...props }: InputProps,
|
||||||
|
forwardedRef
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div className={cx('relative w-full', className)}>
|
||||||
|
<input
|
||||||
|
ref={forwardedRef}
|
||||||
|
type={type}
|
||||||
|
className={cx(inputStyles({ hasError, enableStepper }), inputClassName)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Input.displayName = 'Input';
|
||||||
|
|
||||||
|
export default Input;
|
@ -19,7 +19,7 @@ const label = tv(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
interface LabelProps extends VariantProps<typeof label> {
|
export interface LabelProps extends VariantProps<typeof label> {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||||
|
15
components/ui/loading-dots.tsx
Normal file
15
components/ui/loading-dots.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
const dots = 'mx-[1px] inline-block h-1 w-1 animate-blink rounded-md';
|
||||||
|
|
||||||
|
const LoadingDots = ({ className }: { className?: string }) => {
|
||||||
|
return (
|
||||||
|
<span className="mx-2 inline-flex items-center">
|
||||||
|
<span className={clsx(dots, className)} />
|
||||||
|
<span className={clsx(dots, 'animation-delay-[200ms]', className)} />
|
||||||
|
<span className={clsx(dots, 'animation-delay-[400ms]', className)} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingDots;
|
@ -19,7 +19,7 @@ const text = tv(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
interface TextProps extends VariantProps<typeof text> {
|
export interface TextProps extends VariantProps<typeof text> {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'p';
|
||||||
|
@ -390,6 +390,7 @@ export async function isLoggedIn(request: NextRequest, origin: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newHeaders.set('x-shop-customer-token', `${customerTokenValue}`);
|
newHeaders.set('x-shop-customer-token', `${customerTokenValue}`);
|
||||||
|
console.log('Customer Token', customerTokenValue);
|
||||||
return NextResponse.next({
|
return NextResponse.next({
|
||||||
request: {
|
request: {
|
||||||
// New request headers
|
// New request headers
|
||||||
|
17
lib/shopify/fragments/address.ts
Normal file
17
lib/shopify/fragments/address.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const addressFragment = /* GraphQL */ `
|
||||||
|
fragment Address on CustomerAddress {
|
||||||
|
id
|
||||||
|
address1
|
||||||
|
address2
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
provinceCode: zoneCode
|
||||||
|
city
|
||||||
|
zip
|
||||||
|
countryCodeV2: territoryCode
|
||||||
|
company
|
||||||
|
phone: phoneNumber
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default addressFragment;
|
0
lib/shopify/fragments/order-card.ts
Normal file
0
lib/shopify/fragments/order-card.ts
Normal file
56
lib/shopify/fragments/order-metafields.ts
Normal file
56
lib/shopify/fragments/order-metafields.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
const orderMetafieldsFragment = /* GraphQL */ `
|
||||||
|
fragment OrderMetafields on Order {
|
||||||
|
warrantyStatus: metafield(namespace: "custom", key: "warranty_status") {
|
||||||
|
value
|
||||||
|
id
|
||||||
|
key
|
||||||
|
}
|
||||||
|
warrantyActivationDeadline: metafield(
|
||||||
|
namespace: "custom"
|
||||||
|
key: "warranty_activation_deadline"
|
||||||
|
) {
|
||||||
|
value
|
||||||
|
id
|
||||||
|
key
|
||||||
|
}
|
||||||
|
warrantyActivationOdometer: metafield(
|
||||||
|
namespace: "custom"
|
||||||
|
key: "warranty_activation_odometer"
|
||||||
|
) {
|
||||||
|
value
|
||||||
|
id
|
||||||
|
key
|
||||||
|
}
|
||||||
|
warrantyActivationInstallation: metafield(
|
||||||
|
namespace: "custom"
|
||||||
|
key: "warranty_activation_installation"
|
||||||
|
) {
|
||||||
|
value
|
||||||
|
id
|
||||||
|
key
|
||||||
|
}
|
||||||
|
warrantyActivationSelfInstall: metafield(
|
||||||
|
namespace: "custom"
|
||||||
|
key: "warranty_activation_self_install"
|
||||||
|
) {
|
||||||
|
value
|
||||||
|
id
|
||||||
|
key
|
||||||
|
}
|
||||||
|
warrantyActivationVIN: metafield(namespace: "custom", key: "warranty_activation_vin") {
|
||||||
|
value
|
||||||
|
id
|
||||||
|
key
|
||||||
|
}
|
||||||
|
warrantyActivationMileage: metafield(namespace: "custom", key: "warranty_activation_mileage") {
|
||||||
|
value
|
||||||
|
id
|
||||||
|
key
|
||||||
|
}
|
||||||
|
orderConfirmation: metafield(namespace: "custom", key: "customer_confirmation") {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default orderMetafieldsFragment;
|
38
lib/shopify/fragments/order-transaction.ts
Normal file
38
lib/shopify/fragments/order-transaction.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const orderTransactionFragment = /* GraphQL */ `
|
||||||
|
fragment OrderTransaction on OrderTransaction {
|
||||||
|
id
|
||||||
|
processedAt
|
||||||
|
paymentIcon {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
altText
|
||||||
|
}
|
||||||
|
paymentDetails {
|
||||||
|
... on CardPaymentDetails {
|
||||||
|
last4
|
||||||
|
cardBrand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transactionAmount {
|
||||||
|
presentmentMoney {
|
||||||
|
...Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
giftCardDetails {
|
||||||
|
last4
|
||||||
|
balance {
|
||||||
|
...Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status
|
||||||
|
kind
|
||||||
|
transactionParentId
|
||||||
|
type
|
||||||
|
typeDetails {
|
||||||
|
name
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default orderTransactionFragment;
|
@ -1,4 +1,8 @@
|
|||||||
|
import addressFragment from './address';
|
||||||
import lineItemFragment from './line-item';
|
import lineItemFragment from './line-item';
|
||||||
|
import orderMetafieldsFragment from './order-metafields';
|
||||||
|
import orderTrasactionFragment from './order-transaction';
|
||||||
|
import priceFragment from './price';
|
||||||
|
|
||||||
const orderCard = /* GraphQL */ `
|
const orderCard = /* GraphQL */ `
|
||||||
fragment OrderCard on Order {
|
fragment OrderCard on Order {
|
||||||
@ -16,69 +20,44 @@ const orderCard = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
totalPrice {
|
totalPrice {
|
||||||
amount
|
...Price
|
||||||
currencyCode
|
}
|
||||||
|
subtotal {
|
||||||
|
...Price
|
||||||
|
}
|
||||||
|
totalShipping {
|
||||||
|
...Price
|
||||||
|
}
|
||||||
|
totalTax {
|
||||||
|
...Price
|
||||||
|
}
|
||||||
|
shippingLine {
|
||||||
|
title
|
||||||
|
originalPrice {
|
||||||
|
...Price
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lineItems(first: 20) {
|
lineItems(first: 20) {
|
||||||
nodes {
|
nodes {
|
||||||
...LineItem
|
...LineItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
shippingAddress {
|
||||||
|
...Address
|
||||||
|
}
|
||||||
|
billingAddress {
|
||||||
|
...Address
|
||||||
|
}
|
||||||
|
transactions {
|
||||||
|
...OrderTransaction
|
||||||
|
}
|
||||||
|
...OrderMetafields
|
||||||
}
|
}
|
||||||
${lineItemFragment}
|
${lineItemFragment}
|
||||||
`;
|
${addressFragment}
|
||||||
|
${priceFragment}
|
||||||
export const orderMetafields = /* GraphQL */ `
|
${orderTrasactionFragment}
|
||||||
fragment OrderMetafield on Order {
|
${orderMetafieldsFragment}
|
||||||
id
|
|
||||||
warrantyStatus: metafield(namespace: "custom", key: "warranty_status") {
|
|
||||||
value
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
warrantyActivationDeadline: metafield(
|
|
||||||
namespace: "custom"
|
|
||||||
key: "warranty_activation_deadline"
|
|
||||||
) {
|
|
||||||
value
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
warrantyActivationOdometer: metafield(
|
|
||||||
namespace: "custom"
|
|
||||||
key: "warranty_activation_odometer"
|
|
||||||
) {
|
|
||||||
value
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
warrantyActivationInstallation: metafield(
|
|
||||||
namespace: "custom"
|
|
||||||
key: "warranty_activation_installation"
|
|
||||||
) {
|
|
||||||
value
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
warrantyActivationSelfInstall: metafield(
|
|
||||||
namespace: "custom"
|
|
||||||
key: "warranty_activation_self_install"
|
|
||||||
) {
|
|
||||||
value
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
warrantyActivationVIN: metafield(namespace: "custom", key: "warranty_activation_vin") {
|
|
||||||
value
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
warrantyActivationMileage: metafield(namespace: "custom", key: "warranty_activation_mileage") {
|
|
||||||
value
|
|
||||||
id
|
|
||||||
key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default orderCard;
|
export default orderCard;
|
||||||
|
8
lib/shopify/fragments/price.ts
Normal file
8
lib/shopify/fragments/price.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const priceFragment = /* GraphQL */ `
|
||||||
|
fragment Price on MoneyV2 {
|
||||||
|
amount
|
||||||
|
currencyCode
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default priceFragment;
|
@ -38,8 +38,7 @@ import { getCustomerQuery } from './queries/customer';
|
|||||||
import { getMenuQuery } from './queries/menu';
|
import { getMenuQuery } from './queries/menu';
|
||||||
import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject';
|
import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject';
|
||||||
import { getFileQuery, getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
|
import { getFileQuery, getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
|
||||||
import { getCustomerOrderQuery, getOrderMetafieldsQuery } from './queries/order';
|
import { getCustomerOrdersQuery } from './queries/orders';
|
||||||
import { getCustomerOrderMetafieldsQuery, getCustomerOrdersQuery } from './queries/orders';
|
|
||||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||||
import {
|
import {
|
||||||
getProductQuery,
|
getProductQuery,
|
||||||
@ -64,6 +63,7 @@ import {
|
|||||||
Metaobject,
|
Metaobject,
|
||||||
Money,
|
Money,
|
||||||
Order,
|
Order,
|
||||||
|
OrderConfirmationContent,
|
||||||
Page,
|
Page,
|
||||||
PageInfo,
|
PageInfo,
|
||||||
Product,
|
Product,
|
||||||
@ -86,10 +86,10 @@ import {
|
|||||||
ShopifyImageOperation,
|
ShopifyImageOperation,
|
||||||
ShopifyMenuOperation,
|
ShopifyMenuOperation,
|
||||||
ShopifyMetaobject,
|
ShopifyMetaobject,
|
||||||
|
ShopifyMetaobjectOperation,
|
||||||
ShopifyMetaobjectsOperation,
|
ShopifyMetaobjectsOperation,
|
||||||
ShopifyMoneyV2,
|
ShopifyMoneyV2,
|
||||||
ShopifyOrder,
|
ShopifyOrder,
|
||||||
ShopifyOrderMetafield,
|
|
||||||
ShopifyPage,
|
ShopifyPage,
|
||||||
ShopifyPageOperation,
|
ShopifyPageOperation,
|
||||||
ShopifyPagesOperation,
|
ShopifyPagesOperation,
|
||||||
@ -109,6 +109,7 @@ import {
|
|||||||
UploadInput,
|
UploadInput,
|
||||||
WarrantyStatus
|
WarrantyStatus
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import getCustomerOrderQuery from './queries/order';
|
||||||
|
|
||||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||||
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
|
? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, 'https://')
|
||||||
@ -185,7 +186,7 @@ export async function shopifyFetch<T>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function adminFetch<T>({
|
async function shopifyAdminFetch<T>({
|
||||||
headers,
|
headers,
|
||||||
query,
|
query,
|
||||||
variables,
|
variables,
|
||||||
@ -313,7 +314,7 @@ export async function shopifyCustomerFetch<T>({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeEdgesAndNodes = (array: Connection<any>) => {
|
const removeEdgesAndNodes = <T = any>(array: Connection<T>) => {
|
||||||
return array.edges.map((edge) => edge?.node);
|
return array.edges.map((edge) => edge?.node);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -439,7 +440,7 @@ const reshapeImages = (images: Connection<Image>, productTitle: string) => {
|
|||||||
const flattened = removeEdgesAndNodes(images);
|
const flattened = removeEdgesAndNodes(images);
|
||||||
|
|
||||||
return flattened.map((image) => {
|
return flattened.map((image) => {
|
||||||
const filename = image.url.match(/.*\/(.*)\..*/)[1];
|
const filename = (image.url.match(/.*\/(.*)\..*/) || [])[1];
|
||||||
return {
|
return {
|
||||||
...image,
|
...image,
|
||||||
altText: image.altText || `${productTitle} - ${filename}`
|
altText: image.altText || `${productTitle} - ${filename}`
|
||||||
@ -531,8 +532,7 @@ function reshapeOrders(orders: ShopifyOrder[]): any[] | Promise<Order[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
|
function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
|
||||||
const reshapeAddress = (address?: ShopifyAddress): Address | undefined => {
|
const reshapeAddress = (address: ShopifyAddress): Address => {
|
||||||
if (!address) return undefined;
|
|
||||||
return {
|
return {
|
||||||
address1: address.address1,
|
address1: address.address1,
|
||||||
address2: address.address2,
|
address2: address.address2,
|
||||||
@ -547,8 +547,7 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const reshapeMoney = (money?: ShopifyMoneyV2): Money | undefined => {
|
const reshapeMoney = (money: ShopifyMoneyV2): Money => {
|
||||||
if (!money) return undefined;
|
|
||||||
return {
|
return {
|
||||||
amount: money.amount || '0.00',
|
amount: money.amount || '0.00',
|
||||||
currencyCode: money.currencyCode || 'USD'
|
currencyCode: money.currencyCode || 'USD'
|
||||||
@ -619,23 +618,38 @@ function reshapeOrder(shopifyOrder: ShopifyOrder): Order {
|
|||||||
totalShipping: reshapeMoney(shopifyOrder.totalShipping),
|
totalShipping: reshapeMoney(shopifyOrder.totalShipping),
|
||||||
totalTax: reshapeMoney(shopifyOrder.totalTax),
|
totalTax: reshapeMoney(shopifyOrder.totalTax),
|
||||||
totalPrice: reshapeMoney(shopifyOrder.totalPrice),
|
totalPrice: reshapeMoney(shopifyOrder.totalPrice),
|
||||||
createdAt: shopifyOrder.createdAt
|
createdAt: shopifyOrder.createdAt,
|
||||||
|
shippingMethod: {
|
||||||
|
name: shopifyOrder.shippingLine?.title,
|
||||||
|
price: reshapeMoney(shopifyOrder.shippingLine.originalPrice)!
|
||||||
|
},
|
||||||
|
warrantyActivationDeadline: shopifyOrder.warrantyActivationDeadline,
|
||||||
|
warrantyStatus: shopifyOrder.warrantyStatus,
|
||||||
|
warrantyActivationInstallation: shopifyOrder.warrantyActivationInstallation,
|
||||||
|
warrantyActivationMileage: shopifyOrder.warrantyActivationMileage,
|
||||||
|
warrantyActivationOdometer: shopifyOrder.warrantyActivationOdometer,
|
||||||
|
warrantyActivationSelfInstall: shopifyOrder.warrantyActivationSelfInstall,
|
||||||
|
warrantyActivationVIN: shopifyOrder.warrantyActivationVIN,
|
||||||
|
orderConfirmation: shopifyOrder.orderConfirmation
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shopifyOrder.customer) {
|
if (shopifyOrder.customer) {
|
||||||
order.customer = reshapeCustomer(shopifyOrder.customer);
|
order.customer = reshapeCustomer(shopifyOrder.customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shopifyOrder.shippingLine) {
|
|
||||||
order.shippingMethod = {
|
|
||||||
name: shopifyOrder.shippingLine?.title,
|
|
||||||
price: reshapeMoney(shopifyOrder.shippingLine.originalPrice)!
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reshapeOrderConfirmationPdf(
|
||||||
|
metaobject: ShopifyMetaobject
|
||||||
|
): OrderConfirmationContent {
|
||||||
|
return {
|
||||||
|
body: metaobject.fields.find((field) => field.key === 'body')?.value || '',
|
||||||
|
logo: metaobject.fields.find((field) => field.key === 'logo')?.reference.image!,
|
||||||
|
color: metaobject.fields.find((field) => field.key === 'color')?.value || '#000000'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function createCart(): Promise<Cart> {
|
export async function createCart(): Promise<Cart> {
|
||||||
const res = await shopifyFetch<ShopifyCreateCartOperation>({
|
const res = await shopifyFetch<ShopifyCreateCartOperation>({
|
||||||
query: createCartMutation,
|
query: createCartMutation,
|
||||||
@ -874,6 +888,31 @@ export async function getMetaobjects(type: string) {
|
|||||||
return reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
|
return reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllMetaobjects(type: string) {
|
||||||
|
const allMetaobjects: Metaobject[] = [];
|
||||||
|
let hasNextPage = true;
|
||||||
|
let after: string | undefined;
|
||||||
|
|
||||||
|
while (hasNextPage) {
|
||||||
|
const res = await shopifyFetch<ShopifyMetaobjectsOperation>({
|
||||||
|
query: getMetaobjectsQuery,
|
||||||
|
tags: [TAGS.collections, TAGS.products],
|
||||||
|
variables: { type, after }
|
||||||
|
});
|
||||||
|
|
||||||
|
const metaobjects = reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
|
||||||
|
|
||||||
|
for (const metaobject of metaobjects) {
|
||||||
|
allMetaobjects.push(metaobject);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNextPage = res.body.data.metaobjects.pageInfo?.hasNextPage || false;
|
||||||
|
after = res.body.data.metaobjects.pageInfo?.endCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allMetaobjects;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMetaobjectsByIds(ids: string[]) {
|
export async function getMetaobjectsByIds(ids: string[]) {
|
||||||
if (!ids.length) return [];
|
if (!ids.length) return [];
|
||||||
|
|
||||||
@ -895,10 +934,7 @@ export async function getMetaobject({
|
|||||||
id?: string;
|
id?: string;
|
||||||
handle?: { handle: string; type: string };
|
handle?: { handle: string; type: string };
|
||||||
}) {
|
}) {
|
||||||
const res = await shopifyFetch<{
|
const res = await shopifyFetch<ShopifyMetaobjectOperation>({
|
||||||
data: { metaobject: ShopifyMetaobject };
|
|
||||||
variables: { id?: string; handle?: { handle: string; type: string } };
|
|
||||||
}>({
|
|
||||||
query: getMetaobjectQuery,
|
query: getMetaobjectQuery,
|
||||||
variables: { id, handle }
|
variables: { id, handle }
|
||||||
});
|
});
|
||||||
@ -906,6 +942,15 @@ export async function getMetaobject({
|
|||||||
return res.body.data.metaobject ? reshapeMetaobjects([res.body.data.metaobject])[0] : null;
|
return res.body.data.metaobject ? reshapeMetaobjects([res.body.data.metaobject])[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getOrderConfirmationContent(): Promise<OrderConfirmationContent> {
|
||||||
|
const res = await shopifyFetch<ShopifyMetaobjectOperation>({
|
||||||
|
query: getMetaobjectQuery,
|
||||||
|
variables: { handle: { handle: 'order-confirmation-pdf', type: 'order_confirmation_pdf' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
return reshapeOrderConfirmationPdf(res.body.data.metaobject);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPage(handle: string): Promise<Page> {
|
export async function getPage(handle: string): Promise<Page> {
|
||||||
const res = await shopifyFetch<ShopifyPageOperation>({
|
const res = await shopifyFetch<ShopifyPageOperation>({
|
||||||
query: getPageQuery,
|
query: getPageQuery,
|
||||||
@ -1064,7 +1109,7 @@ export const getImage = async (id: string): Promise<Image> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const stageUploadFile = async (params: UploadInput) => {
|
export const stageUploadFile = async (params: UploadInput) => {
|
||||||
const res = await adminFetch<ShopifyStagedUploadOperation>({
|
const res = await shopifyAdminFetch<ShopifyStagedUploadOperation>({
|
||||||
query: createStageUploads,
|
query: createStageUploads,
|
||||||
variables: { input: [params] }
|
variables: { input: [params] }
|
||||||
});
|
});
|
||||||
@ -1080,7 +1125,7 @@ export const uploadFile = async ({ url, formData }: { url: string; formData: For
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createFile = async (params: FileCreateInput) => {
|
export const createFile = async (params: FileCreateInput) => {
|
||||||
const res = await adminFetch<ShopifyCreateFileOperation>({
|
const res = await shopifyAdminFetch<ShopifyCreateFileOperation>({
|
||||||
query: createFileMutation,
|
query: createFileMutation,
|
||||||
variables: { files: [params] }
|
variables: { files: [params] }
|
||||||
});
|
});
|
||||||
@ -1103,7 +1148,7 @@ export const updateOrderMetafields = async ({
|
|||||||
validMetafields.find(({ key }) => (Array.isArray(field) ? field.includes(key) : key === field))
|
validMetafields.find(({ key }) => (Array.isArray(field) ? field.includes(key) : key === field))
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await adminFetch<ShopifyUpdateOrderMetafieldsOperation>({
|
const response = await shopifyAdminFetch<ShopifyUpdateOrderMetafieldsOperation>({
|
||||||
query: updateOrderMetafieldsMutation,
|
query: updateOrderMetafieldsMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
@ -1125,59 +1170,6 @@ export const updateOrderMetafields = async ({
|
|||||||
return response.body.data.orderUpdate.order.id;
|
return response.body.data.orderUpdate.order.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOrdersMetafields = async (): Promise<{ [key: string]: ShopifyOrderMetafield }> => {
|
|
||||||
const customer = await getCustomer();
|
|
||||||
const res = await adminFetch<{
|
|
||||||
data: {
|
|
||||||
customer: {
|
|
||||||
orders: {
|
|
||||||
nodes: Array<
|
|
||||||
{
|
|
||||||
id: string;
|
|
||||||
} & ShopifyOrderMetafield
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
}>({
|
|
||||||
query: getCustomerOrderMetafieldsQuery,
|
|
||||||
variables: { id: customer.id },
|
|
||||||
tags: [TAGS.orderMetafields]
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.body.data.customer.orders.nodes.reduce(
|
|
||||||
(acc, order) => ({
|
|
||||||
...acc,
|
|
||||||
[order.id]: order
|
|
||||||
}),
|
|
||||||
{} as { [key: string]: ShopifyOrderMetafield }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOrderMetafields = async (orderId: string): Promise<ShopifyOrderMetafield> => {
|
|
||||||
const res = await adminFetch<{
|
|
||||||
data: {
|
|
||||||
order: {
|
|
||||||
id: string;
|
|
||||||
} & ShopifyOrderMetafield;
|
|
||||||
};
|
|
||||||
variables: {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
}>({
|
|
||||||
query: getOrderMetafieldsQuery,
|
|
||||||
variables: { id: `gid://shopify/Order/${orderId}` },
|
|
||||||
tags: [TAGS.orderMetafields]
|
|
||||||
});
|
|
||||||
|
|
||||||
const order = res.body.data.order;
|
|
||||||
|
|
||||||
return order;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFile = async (id: string) => {
|
export const getFile = async (id: string) => {
|
||||||
const res = await shopifyFetch<{
|
const res = await shopifyFetch<{
|
||||||
data: {
|
data: {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export const getMetaobjectsQuery = /* GraphQL */ `
|
export const getMetaobjectsQuery = /* GraphQL */ `
|
||||||
query getMetaobjects($type: String!) {
|
query getMetaobjects($type: String!, $after: String) {
|
||||||
metaobjects(type: $type, first: 200) {
|
metaobjects(type: $type, first: 200, after: $after) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@ -16,6 +16,10 @@ export const getMetaobjectsQuery = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -30,6 +34,14 @@ export const getMetaobjectQuery = /* GraphQL */ `
|
|||||||
... on Metaobject {
|
... on Metaobject {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
... on MediaImage {
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
altText
|
||||||
|
height
|
||||||
|
width
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
key
|
key
|
||||||
value
|
value
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import addressFragment from '../fragments/address';
|
||||||
import lineItemFragment from '../fragments/line-item';
|
import lineItemFragment from '../fragments/line-item';
|
||||||
import { orderMetafields } from '../fragments/order';
|
import orderMetafieldsFragment from '../fragments/order-metafields';
|
||||||
|
import orderTrasactionFragment from '../fragments/order-transaction';
|
||||||
|
import priceFragment from '../fragments/price';
|
||||||
|
|
||||||
// NOTE: https://shopify.dev/docs/api/customer/latest/queries/customer
|
// NOTE: https://shopify.dev/docs/api/customer/latest/queries/customer
|
||||||
export const getCustomerOrderQuery = /* GraphQL */ `
|
const getCustomerOrderQuery = /* GraphQL */ `
|
||||||
query getCustomerOrderQuery($orderId: ID!) {
|
query getCustomerOrderQuery($orderId: ID!) {
|
||||||
customer {
|
customer {
|
||||||
emailAddress {
|
emailAddress {
|
||||||
@ -95,60 +98,7 @@ export const getCustomerOrderQuery = /* GraphQL */ `
|
|||||||
...Price
|
...Price
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
...OrderMetafields
|
||||||
|
|
||||||
fragment OrderTransaction on OrderTransaction {
|
|
||||||
id
|
|
||||||
processedAt
|
|
||||||
paymentIcon {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
altText
|
|
||||||
}
|
|
||||||
paymentDetails {
|
|
||||||
... on CardPaymentDetails {
|
|
||||||
last4
|
|
||||||
cardBrand
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transactionAmount {
|
|
||||||
presentmentMoney {
|
|
||||||
...Price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
giftCardDetails {
|
|
||||||
last4
|
|
||||||
balance {
|
|
||||||
...Price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
status
|
|
||||||
kind
|
|
||||||
transactionParentId
|
|
||||||
type
|
|
||||||
typeDetails {
|
|
||||||
name
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment Price on MoneyV2 {
|
|
||||||
amount
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment Address on CustomerAddress {
|
|
||||||
id
|
|
||||||
address1
|
|
||||||
address2
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
provinceCode: zoneCode
|
|
||||||
city
|
|
||||||
zip
|
|
||||||
countryCodeV2: territoryCode
|
|
||||||
company
|
|
||||||
phone: phoneNumber
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment Fulfillment on Fulfillment {
|
fragment Fulfillment on Fulfillment {
|
||||||
@ -220,13 +170,10 @@ export const getCustomerOrderQuery = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
${lineItemFragment}
|
${lineItemFragment}
|
||||||
|
${addressFragment}
|
||||||
|
${priceFragment}
|
||||||
|
${orderTrasactionFragment}
|
||||||
|
${orderMetafieldsFragment}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const getOrderMetafieldsQuery = /* GraphQL */ `
|
export default getCustomerOrderQuery;
|
||||||
query getOrderMetafields($id: ID!) {
|
|
||||||
order(id: $id) {
|
|
||||||
...OrderMetafield
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${orderMetafields}
|
|
||||||
`;
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import customerDetailsFragment from '../fragments/customer-details';
|
import customerDetailsFragment from '../fragments/customer-details';
|
||||||
import { orderMetafields } from '../fragments/order';
|
|
||||||
|
|
||||||
const customerFragment = `#graphql
|
const customerFragment = `#graphql
|
||||||
`;
|
`;
|
||||||
@ -14,16 +13,3 @@ export const getCustomerOrdersQuery = `#graphql
|
|||||||
${customerFragment}
|
${customerFragment}
|
||||||
${customerDetailsFragment}
|
${customerDetailsFragment}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const getCustomerOrderMetafieldsQuery = /* GraphQL */ `
|
|
||||||
query getCustomerOrderMetafields($id: ID!) {
|
|
||||||
customer(id: $id) {
|
|
||||||
orders(first: 20, sortKey: PROCESSED_AT, reverse: true) {
|
|
||||||
nodes {
|
|
||||||
...OrderMetafield
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${orderMetafields}
|
|
||||||
`;
|
|
||||||
|
@ -3,6 +3,7 @@ export type Maybe<T> = T | null;
|
|||||||
|
|
||||||
export type Connection<T> = {
|
export type Connection<T> = {
|
||||||
edges: Array<Edge<T>>;
|
edges: Array<Edge<T>>;
|
||||||
|
pageInfo?: PageInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Edge<T> = {
|
export type Edge<T> = {
|
||||||
@ -141,18 +142,18 @@ export type Order = {
|
|||||||
fulfillments: Fulfillment[];
|
fulfillments: Fulfillment[];
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
lineItems: LineItem[];
|
lineItems: LineItem[];
|
||||||
shippingAddress?: Address;
|
shippingAddress: Address;
|
||||||
billingAddress?: Address;
|
billingAddress: Address;
|
||||||
/** the price of all line items, excluding taxes and surcharges */
|
/** the price of all line items, excluding taxes and surcharges */
|
||||||
subtotal?: Money;
|
subtotal: Money;
|
||||||
totalShipping?: Money;
|
totalShipping: Money;
|
||||||
totalTax?: Money;
|
totalTax: Money;
|
||||||
totalPrice?: Money;
|
totalPrice: Money;
|
||||||
shippingMethod?: {
|
shippingMethod: {
|
||||||
name: string;
|
name: string;
|
||||||
price: Money;
|
price: Money;
|
||||||
};
|
};
|
||||||
};
|
} & ShopifyOrderMetafield;
|
||||||
|
|
||||||
export type ShopifyOrder = {
|
export type ShopifyOrder = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -181,7 +182,7 @@ export type ShopifyOrder = {
|
|||||||
requiresShipping: boolean;
|
requiresShipping: boolean;
|
||||||
shippingLine: ShopifyShippingLine;
|
shippingLine: ShopifyShippingLine;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
};
|
} & ShopifyOrderMetafield;
|
||||||
|
|
||||||
type ShopifyShippingLine = {
|
type ShopifyShippingLine = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -372,16 +373,30 @@ export type ShopifyMetaobject = {
|
|||||||
value: string;
|
value: string;
|
||||||
reference: {
|
reference: {
|
||||||
id: string;
|
id: string;
|
||||||
|
image?: Image;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShopifyMetafield = {
|
||||||
|
id: string;
|
||||||
|
namespace: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Metaobject = {
|
export type Metaobject = {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OrderConfirmationContent = {
|
||||||
|
logo: Image;
|
||||||
|
body: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TransmissionType = 'Automatic' | 'Manual';
|
export type TransmissionType = 'Automatic' | 'Manual';
|
||||||
|
|
||||||
export type Product = Omit<
|
export type Product = Omit<
|
||||||
@ -665,7 +680,7 @@ export type ShopifyImageOperation = {
|
|||||||
|
|
||||||
export type ShopifyMetaobjectsOperation = {
|
export type ShopifyMetaobjectsOperation = {
|
||||||
data: { metaobjects: Connection<ShopifyMetaobject> };
|
data: { metaobjects: Connection<ShopifyMetaobject> };
|
||||||
variables: { type: string };
|
variables: { type: string; after?: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopifyPagesOperation = {
|
export type ShopifyPagesOperation = {
|
||||||
@ -675,8 +690,8 @@ export type ShopifyPagesOperation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ShopifyMetaobjectOperation = {
|
export type ShopifyMetaobjectOperation = {
|
||||||
data: { nodes: ShopifyMetaobject[] };
|
data: { metaobject: ShopifyMetaobject };
|
||||||
variables: { ids: string[] };
|
variables: { id?: string; handle?: { handle: string; type: string } };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopifyProductOperation = {
|
export type ShopifyProductOperation = {
|
||||||
@ -858,20 +873,15 @@ export enum WarrantyStatus {
|
|||||||
LimitedActivated = 'Limited Activation'
|
LimitedActivated = 'Limited Activation'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderMetafieldValue<T = string> = {
|
|
||||||
value: T;
|
|
||||||
id: string;
|
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ShopifyOrderMetafield = {
|
export type ShopifyOrderMetafield = {
|
||||||
warrantyStatus: OrderMetafieldValue | null;
|
orderConfirmation: ShopifyMetafield | null;
|
||||||
warrantyActivationDeadline: OrderMetafieldValue | null;
|
warrantyStatus: ShopifyMetafield | null;
|
||||||
warrantyActivationOdometer: OrderMetafieldValue | null;
|
warrantyActivationDeadline: ShopifyMetafield | null;
|
||||||
warrantyActivationInstallation: OrderMetafieldValue | null;
|
warrantyActivationOdometer: ShopifyMetafield | null;
|
||||||
warrantyActivationSelfInstall: OrderMetafieldValue | null;
|
warrantyActivationInstallation: ShopifyMetafield | null;
|
||||||
warrantyActivationVIN: OrderMetafieldValue | null;
|
warrantyActivationSelfInstall: ShopifyMetafield | null;
|
||||||
warrantyActivationMileage: OrderMetafieldValue | null;
|
warrantyActivationVIN: ShopifyMetafield | null;
|
||||||
|
warrantyActivationMileage: ShopifyMetafield | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type File = {
|
export type File = {
|
||||||
|
@ -5,7 +5,7 @@ export const carPartPlanetColor = {
|
|||||||
muted: '#E6CCB7'
|
muted: '#E6CCB7'
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
subtle: '#9ca3af', // gray-400
|
subtle: '#d1d5db', // gray-300
|
||||||
DEFAULT: '#6b7280', // gray-500
|
DEFAULT: '#6b7280', // gray-500
|
||||||
emphasis: '#374151', // gray-700
|
emphasis: '#374151', // gray-700
|
||||||
strong: '#111827', // gray-900
|
strong: '#111827', // gray-900
|
||||||
|
28
lib/utils.ts
28
lib/utils.ts
@ -3,6 +3,34 @@ import { ReadonlyURLSearchParams } from 'next/navigation';
|
|||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { Menu } from './shopify/types';
|
import { Menu } from './shopify/types';
|
||||||
|
|
||||||
|
export function cx(...args: ClassValue[]) {
|
||||||
|
return twMerge(clsx(...args));
|
||||||
|
}
|
||||||
|
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'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const hasErrorInput = [
|
||||||
|
// base
|
||||||
|
'ring-2',
|
||||||
|
// border color
|
||||||
|
'border-red-500 dark:border-red-700',
|
||||||
|
// ring color
|
||||||
|
'ring-red-200 dark:ring-red-700/30'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const focusRing = [
|
||||||
|
// base
|
||||||
|
'outline outline-offset-2 outline-0 focus-visible:outline-2',
|
||||||
|
// outline color
|
||||||
|
'outline-blue-500 dark:outline-blue-500'
|
||||||
|
];
|
||||||
|
|
||||||
export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => {
|
export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => {
|
||||||
const paramsString = params.toString();
|
const paramsString = params.toString();
|
||||||
const queryString = `${paramsString.length ? '?' : ''}${paramsString}`;
|
const queryString = `${paramsString.length ? '?' : ''}${paramsString}`;
|
||||||
|
@ -25,12 +25,17 @@
|
|||||||
"@headlessui/react": "^2.1.0",
|
"@headlessui/react": "^2.1.0",
|
||||||
"@heroicons/react": "^2.1.3",
|
"@heroicons/react": "^2.1.3",
|
||||||
"@hookform/resolvers": "^3.6.0",
|
"@hookform/resolvers": "^3.6.0",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@react-pdf/renderer": "^3.4.4",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.kebabcase": "^4.1.1",
|
"lodash.kebabcase": "^4.1.1",
|
||||||
"lodash.startcase": "^4.4.0",
|
"lodash.startcase": "^4.4.0",
|
||||||
|
"markdown-to-jsx": "^7.4.7",
|
||||||
"next": "14.2.4",
|
"next": "14.2.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
1021
pnpm-lock.yaml
generated
1021
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user