Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 34 additions & 40 deletions src/components/checkout/AddressFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import type { Country, State } from "@spree/sdk";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import type { AddressFormData } from "@/lib/utils/address";

interface AddressFormFieldsProps {
Expand Down Expand Up @@ -104,50 +101,47 @@ export function AddressFormFields({

<Field>
<FieldLabel htmlFor={`${idPrefix}-country`}>Country</FieldLabel>
<Select
value={address.country_iso || undefined}
onValueChange={(value) => onChange("country_iso", value)}
<NativeSelect
id={`${idPrefix}-country`}
className="w-full"
value={address.country_iso}
onChange={(e) => onChange("country_iso", e.target.value)}
required
>
<SelectTrigger id={`${idPrefix}-country`} className="w-full">
<SelectValue placeholder="Select a country" />
</SelectTrigger>
<SelectContent>
{countries.map((country) => (
<SelectItem key={country.iso} value={country.iso}>
{country.name}
</SelectItem>
))}
</SelectContent>
</Select>
<NativeSelectOption value="" disabled>
Select a country
</NativeSelectOption>
{countries.map((country) => (
<NativeSelectOption key={country.iso} value={country.iso}>
{country.name}
</NativeSelectOption>
))}
</NativeSelect>
</Field>

<Field>
<FieldLabel htmlFor={`${idPrefix}-state`}>State / Province</FieldLabel>
{loadingStates ? (
<Select disabled>
<SelectTrigger id={`${idPrefix}-state`} className="w-full">
<SelectValue placeholder="Loading..." />
</SelectTrigger>
<SelectContent />
</Select>
<NativeSelect id={`${idPrefix}-state`} className="w-full" disabled>
<NativeSelectOption value="">Loading...</NativeSelectOption>
</NativeSelect>
) : hasStates ? (
<Select
value={address.state_abbr || undefined}
onValueChange={(value) => onChange("state_abbr", value)}
<NativeSelect
id={`${idPrefix}-state`}
className="w-full"
value={address.state_abbr}
onChange={(e) => onChange("state_abbr", e.target.value)}
required
>
<SelectTrigger id={`${idPrefix}-state`} className="w-full">
<SelectValue placeholder="Select a state" />
</SelectTrigger>
<SelectContent>
{states.map((state) => (
<SelectItem key={state.abbr} value={state.abbr}>
{state.name}
</SelectItem>
))}
</SelectContent>
</Select>
<NativeSelectOption value="" disabled>
Select a state
</NativeSelectOption>
{states.map((state) => (
<NativeSelectOption key={state.abbr} value={state.abbr}>
{state.name}
</NativeSelectOption>
))}
</NativeSelect>
) : (
<Input
type="text"
Expand Down
23 changes: 11 additions & 12 deletions src/components/checkout/AddressStep.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"use client";

import type { Address, AddressParams, Country, Order, State } from "@spree/sdk";
import { InfoIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState, useTransition } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -122,18 +124,15 @@ export function AddressStep({
<form onSubmit={handleSubmit} className="space-y-8">
{/* Sign-in prompt for guests */}
{!isAuthenticated && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<p className="text-sm text-blue-800">
Already have an account?{" "}
<Link
href={signInUrl}
className="font-medium text-blue-600 hover:text-blue-700 underline"
>
Sign in
</Link>{" "}
to access your saved addresses and order history.
</p>
</div>
<Alert role="status">
<InfoIcon />
<AlertDescription>
Already have an account?
<br />
<Link href={signInUrl}>Sign in</Link> to access your saved
addresses and order history.
</AlertDescription>
</Alert>
)}

{/* Contact Information */}
Expand Down
29 changes: 13 additions & 16 deletions src/components/checkout/CouponCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { Order } from "@spree/sdk";
import { CheckCircle } from "lucide-react";
import { useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -63,30 +64,26 @@ export function CouponCode({ order, onApply, onRemove }: CouponCodeProps) {
{couponPromotions.length > 0 && (
<div className="space-y-2 mb-4">
{couponPromotions.map((promotion) => (
<div
key={promotion.id}
className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-xl"
>
<div className="flex items-center">
<CheckCircle className="w-5 h-5 text-green-600 mr-2" />
<div>
<span className="text-sm font-medium text-green-800">
{promotion.code || promotion.name}
</span>
<span className="text-sm text-green-600 ml-2">
{promotion.display_amount}
</span>
</div>
</div>
<Alert key={promotion.id} role="status">
<CheckCircle />
<AlertDescription>
<span className="font-medium">
{promotion.code || promotion.name}
</span>
<span className="text-muted-foreground ml-2">
{promotion.display_amount}
</span>
</AlertDescription>
<Button
variant="destructive"
size="sm"
className="absolute top-2 right-2"
onClick={() => handleRemove(promotion.id)}
disabled={removing === promotion.id}
>
{removing === promotion.id ? "..." : "Remove"}
</Button>
</div>
</Alert>
))}
</div>
)}
Expand Down
9 changes: 8 additions & 1 deletion src/components/checkout/StripePaymentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
useStripe,
} from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import { CircleAlert } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";

const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
Expand Down Expand Up @@ -72,7 +74,12 @@ function StripePaymentFormInner({
layout: "tabs",
}}
/>
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
{error && (
<Alert variant="destructive" className="mt-3">
<CircleAlert />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
);
}
Expand Down
56 changes: 56 additions & 0 deletions src/components/ui/native-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ChevronDownIcon } from "lucide-react";
import type * as React from "react";

import { cn } from "@/lib/utils";

type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
size?: "sm" | "default";
};

function NativeSelect({
className,
size = "default",
...props
}: NativeSelectProps) {
return (
<div
className={cn(
"group/native-select relative w-fit has-[select:disabled]:opacity-50",
className,
)}
data-slot="native-select-wrapper"
data-size={size}
>
<select
data-slot="native-select"
data-size={size}
className="h-10 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-base transition-colors outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus:border-black focus:[outline:1px_solid_black] disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive md:text-sm data-[size=sm]:h-8 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:focus:border-white dark:focus:[outline:1px_solid_white] dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50"
{...props}
/>
<ChevronDownIcon
className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
aria-hidden="true"
data-slot="native-select-icon"
/>
</div>
);
}

function NativeSelectOption({ ...props }: React.ComponentProps<"option">) {
return <option data-slot="native-select-option" {...props} />;
}

function NativeSelectOptGroup({
className,
...props
}: React.ComponentProps<"optgroup">) {
return (
<optgroup
data-slot="native-select-optgroup"
className={cn(className)}
{...props}
/>
);
}

export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };
Loading
Loading