mirror of
https://github.com/vercel/commerce.git
synced 2025-07-25 11:11:24 +00:00
feat: add activate warranty form
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
33
components/form/file-input.tsx
Normal file
33
components/form/file-input.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { PhotoIcon } from '@heroicons/react/24/outline';
|
||||
import { useId } from 'react';
|
||||
|
||||
type FileInputProps = {
|
||||
name: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const FileInput = ({ name, label }: FileInputProps) => {
|
||||
const id = useId();
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium leading-6 text-gray-900">{label}</label>
|
||||
<div className="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-5">
|
||||
<div className="text-center">
|
||||
<PhotoIcon className="mx-auto h-12 w-12 text-gray-300" aria-hidden="true" />
|
||||
<div className="mt-2 flex text-sm leading-6 text-gray-600">
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="relative cursor-pointer rounded-md bg-white font-semibold text-primary focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2"
|
||||
>
|
||||
<span>Upload a file</span>
|
||||
<input id={id} name={name} type="file" className="sr-only" />
|
||||
</label>
|
||||
<p className="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileInput;
|
20
components/form/input.tsx
Normal file
20
components/form/input.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Field, Input as HeadlessInput, Label } from '@headlessui/react';
|
||||
import { InputHTMLAttributes } from 'react';
|
||||
|
||||
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label: string;
|
||||
};
|
||||
|
||||
const Input = ({ label, ...props }: InputProps) => {
|
||||
return (
|
||||
<Field>
|
||||
<Label className="block text-sm font-medium leading-6 text-gray-900">{label}</Label>
|
||||
<HeadlessInput
|
||||
className="mt-2 block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary/70 sm:text-sm sm:leading-6"
|
||||
{...props}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
57
components/orders/activate-warranty-modal.tsx
Normal file
57
components/orders/activate-warranty-modal.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
|
||||
import FileInput from 'components/form/file-input';
|
||||
import Input from 'components/form/input';
|
||||
|
||||
type ActivateWarrantyModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function ActivateWarrantyModal({ onClose, isOpen }: ActivateWarrantyModalProps) {
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
transition
|
||||
className="fixed inset-0 z-50 flex w-screen items-center justify-center bg-black/30 p-4 transition duration-300 ease-out data-[closed]:opacity-0"
|
||||
>
|
||||
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
||||
<DialogBackdrop className="fixed inset-0 bg-black/30" />
|
||||
|
||||
{/* Full-screen container to center the panel */}
|
||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||
{/* The actual dialog panel */}
|
||||
<DialogPanel className="w-full max-w-lg bg-white p-5 sm:w-[500px]">
|
||||
<DialogTitle className="mb-2 font-bold">Activate Warranty</DialogTitle>
|
||||
<form>
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<FileInput label="Odometer" name="odometer" />
|
||||
<FileInput label="Installation Receipt" name="installation-receipt" />
|
||||
<Input label="Customer Mileage" name="customer-mileage" type="number" />
|
||||
<Input label="Customer VIN" name="customer-vin" />
|
||||
</div>
|
||||
</form>
|
||||
<div className="mt-4 flex w-full justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-semibold leading-6 text-gray-900"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-primary px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Activate
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActivateWarrantyModal;
|
25
components/orders/activate-warranty.tsx
Normal file
25
components/orders/activate-warranty.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import ActivateWarrantyModal from './activate-warranty-modal';
|
||||
|
||||
type ActivateWarrantyModalProps = {
|
||||
orderId: string;
|
||||
};
|
||||
|
||||
const ActivateWarranty = ({ orderId }: ActivateWarrantyModalProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
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"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
Activate Warranty
|
||||
</button>
|
||||
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivateWarranty;
|
62
components/orders/mobile-order-actions.tsx
Normal file
62
components/orders/mobile-order-actions.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
|
||||
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
|
||||
import clsx from 'clsx';
|
||||
import { Order } from 'lib/shopify/types';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import ActivateWarrantyModal from './activate-warranty-modal';
|
||||
|
||||
const MobileOrderActions = ({ order }: { order: Order }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu as="div" className="relative flex justify-end lg:hidden">
|
||||
<div className="flex items-center">
|
||||
<MenuButton className="-m-2 flex items-center p-2 text-gray-400 hover:text-gray-500">
|
||||
<span className="sr-only">Options for order {order.name}</span>
|
||||
<EllipsisVerticalIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<MenuItems
|
||||
transition
|
||||
className="absolute right-0 z-10 mt-2 w-40 origin-bottom-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition focus:outline-none data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in"
|
||||
>
|
||||
<div className="py-1">
|
||||
<MenuItem>
|
||||
{({ focus }) => (
|
||||
<Link
|
||||
href={`/account/orders/${order.normalizedId}`}
|
||||
className={clsx(
|
||||
focus ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||
'block px-4 py-2 text-sm'
|
||||
)}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
{({ focus }) => (
|
||||
<Button
|
||||
className={clsx(
|
||||
focus ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||
'block px-4 py-2 text-sm'
|
||||
)}
|
||||
>
|
||||
Activate Warranty
|
||||
</Button>
|
||||
)}
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
<ActivateWarrantyModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileOrderActions;
|
@@ -1,10 +1,11 @@
|
||||
import Image from 'next/image';
|
||||
import { InformationCircleIcon } from '@heroicons/react/24/outline';
|
||||
import Price from 'components/price';
|
||||
import Badge from 'components/ui/badge';
|
||||
import Heading from 'components/ui/heading';
|
||||
import Label from 'components/ui/label';
|
||||
import Text from 'components/ui/text';
|
||||
import { Order } from 'lib/shopify/types';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function OrderSummary({ order }: { order: Order }) {
|
||||
return (
|
||||
@@ -12,19 +13,32 @@ export default function OrderSummary({ order }: { order: Order }) {
|
||||
<Heading size="sm">Order Summary</Heading>
|
||||
<div className="flex flex-col gap-6">
|
||||
{order.lineItems.map((lineItem, index) => (
|
||||
<div key={index} className="flex items-center gap-4">
|
||||
<Badge content={lineItem.quantity!}>
|
||||
<Image
|
||||
src={lineItem.image.url}
|
||||
alt={lineItem.image.altText}
|
||||
width={lineItem.image.width}
|
||||
height={lineItem.image.height}
|
||||
className="rounded border"
|
||||
/>
|
||||
</Badge>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text>{lineItem.title}</Text>
|
||||
<Label>{lineItem.sku}</Label>
|
||||
<div key={index} className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<Badge content={lineItem.quantity!}>
|
||||
<div className="h-20 w-20 flex-shrink-0 overflow-hidden rounded-lg bg-gray-100">
|
||||
{lineItem.image ? (
|
||||
<Image
|
||||
src={lineItem.image.url}
|
||||
alt={lineItem.image.altText}
|
||||
width={lineItem.image.width}
|
||||
height={lineItem.image.height}
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-center"
|
||||
title="Missing Product Image"
|
||||
>
|
||||
<InformationCircleIcon className="size-8 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Badge>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Text>{lineItem.title}</Text>
|
||||
<Label>{lineItem.sku}</Label>
|
||||
</div>
|
||||
</div>
|
||||
<Price
|
||||
className="text-sm"
|
||||
|
16
components/orders/orders-header.tsx
Normal file
16
components/orders/orders-header.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
const OrdersHeader = () => {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl sm:px-2 lg:px-8">
|
||||
<div className="mx-auto max-w-2xl px-4 lg:max-w-4xl lg:px-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">
|
||||
Order history
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Check the status of recent orders, manage returns, and discover similar products.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersHeader;
|
@@ -1,16 +1,17 @@
|
||||
import clsx from 'clsx';
|
||||
import { JSXElementConstructor } from 'react';
|
||||
|
||||
const Price = ({
|
||||
amount,
|
||||
className,
|
||||
as,
|
||||
as: Component = 'p',
|
||||
currencyCode = 'USD',
|
||||
currencyCodeClassName,
|
||||
showCurrency = false,
|
||||
prefix
|
||||
}: {
|
||||
amount: string;
|
||||
as?: 'p' | 'span';
|
||||
as?: keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
|
||||
className?: string;
|
||||
currencyCode: string;
|
||||
currencyCodeClassName?: string;
|
||||
@@ -25,7 +26,6 @@ const Price = ({
|
||||
return <p className={className}>Included</p>;
|
||||
}
|
||||
|
||||
const Component = as || 'p';
|
||||
// Otherwise, format and display the price
|
||||
return (
|
||||
<Component suppressHydrationWarning={true} className={className}>
|
||||
|
Reference in New Issue
Block a user