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
676 changes: 25 additions & 651 deletions src/App.tsx

Large diffs are not rendered by default.

64 changes: 25 additions & 39 deletions src/CheckoutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,12 @@ import { useState } from 'react'
import type { FormEvent } from 'react'
import { useMutation } from '@apollo/client/react'
import { CREATE_CHECKOUT } from './graphql/queries'
import type { Product } from './data/products'
import { ErrorMessage } from './components/ErrorMessage'
import { PageContainer } from './components/PageContainer'
import { EmptyCartState } from './components/checkout/EmptyCartState'
import { ShippingAddressForm } from './components/checkout/ShippingAddressForm'
import { PaymentMethodSelect } from './components/checkout/PaymentMethodSelect'
import { OrderSummary } from './components/checkout/OrderSummary'

type CartLineItem = {
product: Product
quantity: number
}

type CheckoutPageProps = {
cartItems: CartLineItem[]
subtotal: number
shipping: number
total: number
}
import { useCart } from './hooks/useCart'

type CreateCheckoutInput = {
items: Array<{
Expand Down Expand Up @@ -53,7 +40,8 @@ type CreateCheckoutMutationResult = {
}
}

export default function CheckoutPage({ cartItems, subtotal, shipping, total }: CheckoutPageProps) {
export default function CheckoutPage() {
const { cartItems, subtotal, shipping, total } = useCart()
const [createCheckout, { loading, error }] =
useMutation<CreateCheckoutMutationResult>(CREATE_CHECKOUT)
const [formData, setFormData] = useState({
Expand Down Expand Up @@ -131,29 +119,27 @@ export default function CheckoutPage({ cartItems, subtotal, shipping, total }: C
}

return (
<PageContainer backLink={{ to: '/products', label: '← Back to products' }}>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_400px] gap-12 mt-8">
<section>
<h1 className="text-4xl mb-8 m-0">Checkout</h1>

<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
<ShippingAddressForm formData={formData} onChange={handleChange} />
<PaymentMethodSelect value={formData.paymentMethod} onChange={handleChange} />

{error && <ErrorMessage message={`Error processing checkout: ${error.message}`} />}

<button
type="submit"
className="rounded-full px-6 py-3.5 text-sm font-semibold cursor-pointer transition-all bg-orange-500 text-white shadow-lg shadow-orange-500/25 hover:-translate-y-0.5 w-full text-center mt-4 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? 'Processing...' : 'Complete Order'}
</button>
</form>
</section>

<OrderSummary cartItems={cartItems} subtotal={subtotal} shipping={shipping} total={total} />
</div>
</PageContainer>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_400px] gap-12 mt-8">
<section>
<h1 className="text-4xl mb-8 m-0">Checkout</h1>

<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
<ShippingAddressForm formData={formData} onChange={handleChange} />
<PaymentMethodSelect value={formData.paymentMethod} onChange={handleChange} />

{error && <ErrorMessage message={`Error processing checkout: ${error.message}`} />}

<button
type="submit"
className="rounded-full px-6 py-3.5 text-sm font-semibold cursor-pointer transition-all bg-orange-500 text-white shadow-lg shadow-orange-500/25 hover:-translate-y-0.5 w-full text-center mt-4 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? 'Processing...' : 'Complete Order'}
</button>
</form>
</section>

<OrderSummary cartItems={cartItems} subtotal={subtotal} shipping={shipping} total={total} />
</div>
)
}
115 changes: 53 additions & 62 deletions src/CheckoutSuccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,8 @@ import { useQuery } from '@apollo/client/react'
import { GET_CHECKOUT } from './graphql/queries'
import { LoadingState } from './components/LoadingState'
import { EmptyState } from './components/EmptyState'
import { PageContainer } from './components/PageContainer'

const currency = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
})

type CheckoutSuccessPageProps = {
onClearCart?: () => void
}
import { useCart } from './hooks/useCart'
import { currency } from './utils/constants'

type CheckoutQueryResult = {
checkout: {
Expand All @@ -39,7 +31,8 @@ type CheckoutQueryResult = {
} | null
}

export default function CheckoutSuccessPage({ onClearCart }: CheckoutSuccessPageProps) {
export default function CheckoutSuccessPage() {
const { clearCart } = useCart()
const { id } = useParams<{ id: string }>()
const { loading, data } = useQuery<CheckoutQueryResult>(GET_CHECKOUT, {
variables: { id: id || '' },
Expand All @@ -48,10 +41,10 @@ export default function CheckoutSuccessPage({ onClearCart }: CheckoutSuccessPage

useEffect(() => {
// Clear cart when checkout is successful
if (data?.checkout && onClearCart) {
onClearCart()
if (data?.checkout) {
clearCart()
}
}, [data?.checkout, onClearCart])
}, [data?.checkout, clearCart])

// Scroll to top on mount
useEffect(() => {
Expand All @@ -78,57 +71,55 @@ export default function CheckoutSuccessPage({ onClearCart }: CheckoutSuccessPage
}

return (
<PageContainer>
<div className="text-center py-16 px-8 flex flex-col items-center gap-8 max-w-2xl mx-auto">
<div className="mb-4">
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="32" cy="32" r="32" fill="#22c55e" />
<path
d="M20 32L28 40L44 24"
stroke="white"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<div className="text-center py-16 px-8 flex flex-col items-center gap-8 max-w-2xl mx-auto">
<div className="mb-4">
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="32" cy="32" r="32" fill="#22c55e" />
<path
d="M20 32L28 40L44 24"
stroke="white"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<h1 className="text-4xl m-0 font-semibold">Order Confirmed!</h1>
<p className="text-lg text-slate-600 m-0 leading-relaxed">
Thank you for your order. We've received your payment and will begin processing your
shipment shortly.
</p>
<div className="bg-white border border-slate-200 rounded-3xl p-8 w-full flex flex-col gap-4">
<div className="flex justify-between items-center pb-4 border-b border-slate-200">
<span className="font-semibold text-slate-600">Order ID:</span>
<span className="font-semibold text-slate-900">{checkout.id}</span>
</div>
<h1 className="text-4xl m-0 font-semibold">Order Confirmed!</h1>
<p className="text-lg text-slate-600 m-0 leading-relaxed">
Thank you for your order. We've received your payment and will begin processing your
shipment shortly.
</p>
<div className="bg-white border border-slate-200 rounded-3xl p-8 w-full flex flex-col gap-4">
<div className="flex justify-between items-center pb-4 border-b border-slate-200">
<span className="font-semibold text-slate-600">Order ID:</span>
<span className="font-semibold text-slate-900">{checkout.id}</span>
</div>
<div className="flex justify-between items-center pb-4 border-b border-slate-200">
<span className="font-semibold text-slate-600">Total:</span>
<span className="font-semibold text-slate-900">{currency.format(checkout.total)}</span>
</div>
<div className="flex justify-between items-center">
<span className="font-semibold text-slate-600">Status:</span>
<span className="font-semibold text-slate-900">{checkout.status}</span>
</div>
<div className="flex justify-between items-center pb-4 border-b border-slate-200">
<span className="font-semibold text-slate-600">Total:</span>
<span className="font-semibold text-slate-900">{currency.format(checkout.total)}</span>
</div>
<div className="mt-4">
<button
type="button"
className="rounded-full px-6 py-3.5 text-sm font-semibold cursor-pointer transition-all bg-orange-500 text-white shadow-lg shadow-orange-500/25 hover:-translate-y-0.5"
onClick={() => {
window.location.href = '/products'
}}
>
Continue Shopping
</button>
<div className="flex justify-between items-center">
<span className="font-semibold text-slate-600">Status:</span>
<span className="font-semibold text-slate-900">{checkout.status}</span>
</div>
</div>
</PageContainer>
<div className="mt-4">
<button
type="button"
className="rounded-full px-6 py-3.5 text-sm font-semibold cursor-pointer transition-all bg-orange-500 text-white shadow-lg shadow-orange-500/25 hover:-translate-y-0.5"
onClick={() => {
window.location.href = '/products'
}}
>
Continue Shopping
</button>
</div>
</div>
)
}
83 changes: 40 additions & 43 deletions src/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useState, type FormEvent } from 'react'
import { useNavigate, Link, useLocation } from 'react-router-dom'
import { useAuth } from './contexts/AuthContext'
import { useAuth } from './hooks/useAuth'
import { FormField } from './components/FormField'
import { ErrorMessage } from './components/ErrorMessage'
import { PageContainer } from './components/PageContainer'

export default function LoginPage() {
const navigate = useNavigate()
Expand Down Expand Up @@ -32,51 +31,49 @@ export default function LoginPage() {
}

return (
<PageContainer>
<div className="max-w-[500px] mx-auto">
<h1 className="text-4xl mb-8 m-0">Login</h1>
<div className="max-w-[500px] mx-auto">
<h1 className="text-4xl mb-8 m-0">Login</h1>

<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
{error && <ErrorMessage message={error} />}
<form className="flex flex-col gap-8" onSubmit={handleSubmit}>
{error && <ErrorMessage message={error} />}

<FormField
id="email"
name="email"
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
/>
<FormField
id="email"
name="email"
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
/>

<FormField
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
/>
<FormField
id="password"
name="password"
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
/>

<button
type="submit"
className="rounded-full px-6 py-3.5 text-sm font-semibold cursor-pointer transition-all bg-orange-500 text-white shadow-lg shadow-orange-500/25 hover:-translate-y-0.5 w-full text-center mt-4 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</button>
<button
type="submit"
className="rounded-full px-6 py-3.5 text-sm font-semibold cursor-pointer transition-all bg-orange-500 text-white shadow-lg shadow-orange-500/25 hover:-translate-y-0.5 w-full text-center mt-4 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</button>

<p className="text-center mt-4">
Don't have an account?{' '}
<Link to="/register" className="text-inherit underline">
Register here
</Link>
</p>
</form>
</div>
</PageContainer>
<p className="text-center mt-4">
Don't have an account?{' '}
<Link to="/register" className="text-inherit underline">
Register here
</Link>
</p>
</form>
</div>
)
}
32 changes: 14 additions & 18 deletions src/ProductPage.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import { useState, useEffect, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { useQuery } from '@apollo/client/react'
import Reviews from './Reviews'
import { getAverageRating } from './utils/reviews'
import { GET_PRODUCT, GET_REVIEWS } from './graphql/queries'
import type { Product } from './data/products'
import { LoadingState } from './components/LoadingState'
import { EmptyState } from './components/EmptyState'
import { PageContainer } from './components/PageContainer'
import { ProductImage } from './components/product/ProductImage'
import { ProductDetails } from './components/product/ProductDetails'

type ProductPageProps = {
onAddToCart: (productId: string) => void
isHighlighted: (productId: string) => boolean
}
import Reviews from '#src/Reviews'
import { getAverageRating } from '#src/utils/reviews'
import { GET_PRODUCT, GET_REVIEWS } from '#src/graphql/queries'
import type { Product } from '#src/data/products'
import { LoadingState } from '#src/components/LoadingState'
import { EmptyState } from '#src/components/EmptyState'
import { ProductImage } from '#src/components/product/ProductImage'
import { ProductDetails } from '#src/components/product/ProductDetails'
import { useCart } from '#src/hooks/useCart'

type ProductQueryResult = {
product: Product | null
}

export default function ProductPage({ onAddToCart, isHighlighted }: ProductPageProps) {
export default function ProductPage() {
const { addToCart, isHighlighted } = useCart()
const { id } = useParams<{ id: string }>()
const [selectedColor, setSelectedColor] = useState<string | null>(null)

Expand Down Expand Up @@ -93,11 +89,11 @@ export default function ProductPage({ onAddToCart, isHighlighted }: ProductPageP
}

const handleAddToCart = () => {
onAddToCart(product.id)
addToCart(product.id)
}

return (
<PageContainer backLink={{ to: '/products', label: '← Back to products' }}>
<>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start">
<ProductImage product={product} />
<ProductDetails
Expand All @@ -111,6 +107,6 @@ export default function ProductPage({ onAddToCart, isHighlighted }: ProductPageP
</div>

<Reviews productId={product.id} />
</PageContainer>
</>
)
}
Loading