Skip to content

Commit

Permalink
Feature/checkout stage (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-kwiatkowski authored Jun 15, 2023
2 parents d5a4137 + 196b563 commit 16c5c20
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 26 deletions.
5 changes: 3 additions & 2 deletions frontend/client/src/components/CartItemPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export default function CartItemPanel({item, removeItem, updateItem}: CartItemPa

return (<li className="py-8 flex odd:border-t odd:border-b last:border-b border-gray-200">
<div className="shrink-0">
<img className="h-48 w-48 rounded-md object-cover" src={item.product.image} alt={item.product.name}/>
<img className="h-32 w-32 md:h-48 md:w-48 rounded-md object-cover" src={item.product.image}
alt={item.product.name}/>
</div>
<div className="flex flex-col ml-8 justify-between flex-1">
<div className="flex flex-col h-full">
Expand All @@ -28,7 +29,7 @@ export default function CartItemPanel({item, removeItem, updateItem}: CartItemPa
<p className="mt-2 text-sm text-gray-500">
{currencyFormat(item.product.price)}
</p>
<div className="my-auto flex">
<div className="mt-auto justify-end flex">
<CounterInput value={item.quantity} onChange={
(e) => {
const value = parseInt(e.target.value);
Expand Down
6 changes: 3 additions & 3 deletions frontend/client/src/components/CartPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default function CartPopover() {
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel
className="absolute -right-56 z-10 mt-3 w-screen max-w-sm -translate-x-1/2 transform px-4 sm:px-0 lg:max-w-sm ">
className="fixed right-0 z-10 mt-3 w-screen sm:max-w-sm sm:right-4 transform px-4 sm:px-0 lg:max-w-sm ">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
{items.length > 0 ? (<>
<div className="relative grid gap-8 bg-white p-7 overflow-y-auto max-h-96">
Expand Down Expand Up @@ -65,13 +65,13 @@ export default function CartPopover() {
</a>))}
</div>
<div className="bg-gray-50 p-4">
<Link to={'#'}>
<Popover.Button as={Link} to={'checkout'}>
<span className="flex items-center justify-center bg-gray-950 p-2 rounded-lg my-4 mx-2">
<span className="text-sm font-medium text-white">
Checkout
</span>
</span>
</Link>
</Popover.Button>
<Popover.Button as={Link} to={'cart'}>
<span className="flex items-center justify-center">
<span className="text-sm font-medium text-gray-900 my-2">
Expand Down
4 changes: 2 additions & 2 deletions frontend/client/src/components/CategoryFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function CategoryFilters({children}: { children: React.ReactNode }) {
<div className="px-4">
<button
type="submit"
className="mt-8 w-full bg-gray-900 border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 transition-all"
className="mt-8 w-full border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900transition-all"
>
Apply Filters
</button>
Expand Down Expand Up @@ -242,7 +242,7 @@ export function CategoryFilters({children}: { children: React.ReactNode }) {

<button
type="submit"
className="mt-8 w-full bg-gray-900 border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 transition-all"
className="mt-8 w-full border border-transparent rounded-md py-3 px-8 flex items-center justify-center text-base font-medium text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 transition-all"
>
Apply Filters
</button>
Expand Down
8 changes: 4 additions & 4 deletions frontend/client/src/components/OrderSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import {Link} from "react-router-dom";
import {currencyFormat} from "../utilities/currencyFormat";

export default function OrderSummary({items}: { items: CartItem[] }) {
const subTotal = items.reduce((acc, item) => acc + item.product.price * item.quantity, 0)
const subtotal = items.reduce((acc, item) => acc + item.product.price * item.quantity, 0)
const shipping = items.length > 0 ? 5 : 0
const total = subTotal + shipping
const total = subtotal + shipping

return (<div className="w-full p-8 bg-gray-50 rounded-xl">
<h2 className="text-lg font-medium text-gray-900">Order summary</h2>
<dl className="mt-8">
<div className="flex justify-between items-center">
<dt className="text-gray-600 text-sm">Subtotal</dt>
<dd className="text-gray-900 text-sm font-medium">
{currencyFormat(subTotal)}
{currencyFormat(subtotal)}
</dd>
</div>
<div className="flex justify-between items-center mt-5 pt-5 border-t border-gray-200">
Expand All @@ -31,7 +31,7 @@ export default function OrderSummary({items}: { items: CartItem[] }) {
</dl>
<div className="mt-8">
<Link to="/checkout"
className="inline-flex items-center px-6 py-3 justify-center border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-neutral-950 hover:bg-neutral-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 w-full">
className="inline-flex items-center px-6 py-3 justify-center border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-neutral-950 hover:bg-neutral-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 w-full transition-all">
Checkout
</Link>
</div>
Expand Down
11 changes: 11 additions & 0 deletions frontend/client/src/context/ShoppingCartContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ type ShoppingCart = {

const ShoppingCartContext = createContext({} as ShoppingCart);

async function sendItemUpdate(item: CartItem) {
await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/basket/${item.product.productID}`, {
method: 'PUT', headers: {
'Content-Type': 'application/json'
}, body: JSON.stringify(item)
})
}

const debouncedUpdateItem = debounce(sendItemUpdate, 500)

export function useShoppingCart() {
return useContext(ShoppingCartContext);
}
Expand Down Expand Up @@ -68,6 +78,7 @@ export function ShoppingCartProvider({children}: ShoppingCartProviderProps) {
}
}


async function updateItem(item: CartItem) {
try {
await updateItemDebounced(item);
Expand Down
10 changes: 5 additions & 5 deletions frontend/client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ProductsPage from "./pages/ProductsPage";
import LoginPage from "./pages/LoginPage";
import ProductDetailsPage, {loader as productLoader} from "./pages/ProductDetailsPage";
import CartOverviewPage from "./pages/CartOverviewPage";
// import CheckoutPage from "./pages/CheckoutPage";
import CheckoutPage from "./pages/CheckoutPage";
import {AuthProvider} from "./context/AuthContext";
import {ProtectedRoute} from "./pages/ProtectedRoute";
import LogoutPage from "./pages/LogoutPage";
Expand All @@ -32,10 +32,10 @@ const router = createBrowserRouter([
path: "cart",
element: <CartOverviewPage/>
},
// {
// path: "checkout",
// element: <CheckoutPage/>
// }
{
path: "checkout",
element: <CheckoutPage/>
}
]
},
{
Expand Down
4 changes: 2 additions & 2 deletions frontend/client/src/pages/CartOverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export default function CartOverviewPage() {
const {items, removeItem, updateItem} = useShoppingCart();

return (
<div className="bg-white">
<div>
<main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex items-baseline justify-between border-b border-gray-200 pb-6 pt-12">
<h1 className="text-2xl sm:text-4xl font-bold tracking-tight text-gray-900">Shopping Cart</h1>
</div>

<div className="my-12 grid grid-cols-12 gap-8">
<div className="my-12 sm:grid sm:grid-cols-12 gap-8">
<div className="col-span-12 sm:col-span-7 -mt-4">
<ul>
{items.map((item, index) =>
Expand Down
178 changes: 178 additions & 0 deletions frontend/client/src/pages/CheckoutPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from "react";
import {useShoppingCart} from "../context/ShoppingCartContext";
import {Form, Link} from "react-router-dom";
import {currencyFormat} from "../utilities/currencyFormat";

export default function CheckoutPage() {
const {items} = useShoppingCart();
const subtotal = items.reduce((total, item) => total + item.product.price * item.quantity, 0);

return (<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 w-full">
<div className="flex items-baseline justify-between border-b border-gray-200 pb-6 pt-12">
<h1 className="text-2xl sm:text-4xl font-bold tracking-tight text-gray-900">Checkout</h1>
<Link to="/cart"
className="text-sm font-medium text-neutral-900 hover:text-neutral-700 transition-all"
>
Return to cart
</Link>
</div>
<div className="mb-8 lg:grid lg:grid-cols-2 gap-8">
{/* payment and shipping details */}
<Form className="py-8 px-4">
<div className="mx-auto max-w-none">
<section>
<h2 className="text-gray-900 text-lg font-medium">
Contact information
</h2>
<div className="mt-4">
<label htmlFor="email-address" className="text-gray-500 font-medium text-sm">Email address</label>
<div className="mt-1">
<input type="email"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="email-address" name="email-address" autoComplete="email"/>
</div>
</div>
</section>
<section className="mt-8">
<h2 className="text-gray-900 text-lg font-medium">
Shipping address
</h2>
<div className="mt-4">
<label htmlFor="country" className="text-gray-500 font-medium text-sm">Country</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="country" name="country" autoComplete="country"/>
</div>
</div>
<div className="mt-4 grid grid-cols-7 gap-4">
<div className="col-span-5">
<label htmlFor="street-address" className="text-gray-500 font-medium text-sm">Street</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="street-address" name="street-address" autoComplete="street-address"/>
</div>
</div>
<div className="col-span-1">
<label htmlFor="street-number" className="text-gray-500 font-medium text-sm">Number</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="street-number" name="street-number" autoComplete="street-number"/>
</div>
</div>
<div className="col-span-1">
<label htmlFor="apt-number" className="text-gray-500 font-medium text-sm">Apt.</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="apt-number" name="apt-number" autoComplete="apt-number"/>
</div>
</div>
</div>
<div className="mt-4 flex gap-4">
<div>
<label htmlFor="city" className="text-gray-500 font-medium text-sm">City</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="city" name="city" autoComplete="city"/>
</div>
</div>
<div>
<label htmlFor="state" className="text-gray-500 font-medium text-sm">State/Province</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="state" name="state" autoComplete="state"/>
</div>
</div>
<div>
<label htmlFor="postal-code" className="text-gray-500 font-medium text-sm">Postal code</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="postal-code" name="postal-code" autoComplete="postal-code"/>
</div>
</div>
</div>
</section>
<section className="mt-8">
<h2 className="text-gray-900 text-lg font-medium">
Payment details
</h2>
<div className="mt-4">
<label htmlFor="card-number" className="text-gray-500 font-medium text-sm">Card number</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="card-number" name="card-number" autoComplete="card-number"/>
</div>
</div>
<div className="mt-4 grid grid-cols-9 gap-4">
<div className="col-span-5">
<label htmlFor="card-holder" className="text-gray-500 font-medium text-sm">Card holder</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="card-holder" name="card-holder" autoComplete="card-holder"/>
</div>
</div>
<div className="col-span-2">
<label htmlFor="card-expiration" className="text-gray-500 font-medium text-sm">Expiration</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="card-expiration" name="card-expiration" autoComplete="card-expiration"/>
</div>
</div>
<div className="col-span-2">
<label htmlFor="card-cvc" className="text-gray-500 font-medium text-sm">CVC</label>
<div className="mt-1">
<input type="text"
className="text-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 block w-full border-gray-300 rounded-md transition-colors"
id="card-cvc" name="card-cvc" autoComplete="card-cvc"/>
</div>
</div>
</div>
</section>
<div className="mt-8">
<button type="submit"
className="w-full flex justify-center py-4 px-6 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-neutral-900 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-900 transition-all">
Continue
</button>
</div>
</div>
</Form>
<div className="py-8 px-4">
<div className="p-4 flex flex-col bg-gray-50 rounded-lg">
<ul className="p-4">
{items.map((item, index) => (<li key={index}
className="border-b last:border-b-0 border-gray-200 py-4 px-4 flex items-center justify-between">
<div className="shrink-0">
<img src={item.product.image} alt={item.product.name}
className="w-20 h-20 rounded-lg object-cover"/>
</div>
<div className="flex-1 min-w-0 ml-4">
<h3 className="text-sm font-medium text-gray-900 truncate">{item.product.name}</h3>
<p className="text-sm text-gray-500 truncate">{item.product.description}</p>
<p
className="text-sm font-medium text-gray-900 truncate">{currencyFormat(item.product.price)} x {item.quantity}</p>
</div>
</li>))}
</ul>
</div>
{/* subtotal */}
<div className="mt-8 px-4">
<div className="flex justify-between items-center">
<h2 className="text-gray-900 text-lg font-medium">
Subtotal
</h2>
<p className="text-gray-900 font-medium">{currencyFormat(subtotal)}</p>
</div>
</div>
</div>
</div>
</div>);
}
9 changes: 1 addition & 8 deletions frontend/client/src/pages/ProductsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@ import {CategoryFilters} from "../components/CategoryFilters";
import {useLocation} from "react-router-dom";
import SpinnerIcon from "../resources/SpinnerIcon";


export function loader({request}: { request: Request }) {
let url = new URL(request.url);
console.log(url)
return null;
}

function useQuery() {
const {search} = useLocation();

Expand Down Expand Up @@ -57,7 +50,7 @@ export default function ProductsPage() {
url.searchParams.append(key, value);
}

fetch(url).then(res => res.json()).then((data) => {
fetch(url).then(res => res.ok ? res.json() : Promise.reject(res)).then((data) => {
setProducts((prevProducts) => [...prevProducts, ...data]);
setHasMore(data.length > 0);
setLoading(false);
Expand Down

0 comments on commit 16c5c20

Please sign in to comment.