Skip to content

Commit e996420

Browse files
Part 4 : Checkout/ProductDetail Page Added
1 parent 8a78d9f commit e996420

File tree

10 files changed

+766
-11
lines changed

10 files changed

+766
-11
lines changed

src/App.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
} from 'react-router-dom';
1313
import Cart from './features/cart/Cart';
1414
import CartPage from './pages/CartPage';
15-
15+
import Checkout from './pages/Checkout';
16+
import ProductDetailPage from './pages/ProductDetailPage';
1617
const router = createBrowserRouter([
1718
{
1819
path: '/',
@@ -26,10 +27,18 @@ const router = createBrowserRouter([
2627
path: '/signup',
2728
element: <SignupPage></SignupPage>,
2829
},
29-
{ // only for testing - then page will be added
30+
{
3031
path: '/cart',
3132
element: <CartPage></CartPage>,
3233
},
34+
{
35+
path: '/checkout',
36+
element: <Checkout></Checkout>,
37+
},
38+
{
39+
path: '/product-detail',
40+
element: <ProductDetailPage></ProductDetailPage>,
41+
},
3342
]);
3443

3544
function App() {

src/features/cart/Cart.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,12 @@ export default function Cart() {
109109
Shipping and taxes calculated at checkout.
110110
</p>
111111
<div className="mt-6">
112-
<a
113-
href="#"
112+
<Link
113+
to="/checkout"
114114
className="flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-6 py-3 text-base font-medium text-white shadow-sm hover:bg-indigo-700"
115115
>
116116
Checkout
117-
</a>
117+
</Link>
118118
</div>
119119
<div className="mt-6 flex justify-center text-center text-sm text-gray-500">
120120
<p>
@@ -123,7 +123,6 @@ export default function Cart() {
123123
<button
124124
type="button"
125125
className="font-medium text-indigo-600 hover:text-indigo-500"
126-
onClick={() => setOpen(false)}
127126
>
128127
Continue Shopping
129128
<span aria-hidden="true"> &rarr;</span>
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
2+
import { useState } from 'react'
3+
import { StarIcon } from '@heroicons/react/20/solid'
4+
import { RadioGroup } from '@headlessui/react'
5+
6+
const product = {
7+
name: 'Basic Tee 6-Pack',
8+
price: '$192',
9+
href: '#',
10+
breadcrumbs: [
11+
{ id: 1, name: 'Men', href: '#' },
12+
{ id: 2, name: 'Clothing', href: '#' },
13+
],
14+
images: [
15+
{
16+
src: 'https://tailwindui.com/img/ecommerce-images/product-page-02-secondary-product-shot.jpg',
17+
alt: 'Two each of gray, white, and black shirts laying flat.',
18+
},
19+
{
20+
src: 'https://tailwindui.com/img/ecommerce-images/product-page-02-tertiary-product-shot-01.jpg',
21+
alt: 'Model wearing plain black basic tee.',
22+
},
23+
{
24+
src: 'https://tailwindui.com/img/ecommerce-images/product-page-02-tertiary-product-shot-02.jpg',
25+
alt: 'Model wearing plain gray basic tee.',
26+
},
27+
{
28+
src: 'https://tailwindui.com/img/ecommerce-images/product-page-02-featured-product-shot.jpg',
29+
alt: 'Model wearing plain white basic tee.',
30+
},
31+
],
32+
colors: [
33+
{ name: 'White', class: 'bg-white', selectedClass: 'ring-gray-400' },
34+
{ name: 'Gray', class: 'bg-gray-200', selectedClass: 'ring-gray-400' },
35+
{ name: 'Black', class: 'bg-gray-900', selectedClass: 'ring-gray-900' },
36+
],
37+
sizes: [
38+
{ name: 'XXS', inStock: false },
39+
{ name: 'XS', inStock: true },
40+
{ name: 'S', inStock: true },
41+
{ name: 'M', inStock: true },
42+
{ name: 'L', inStock: true },
43+
{ name: 'XL', inStock: true },
44+
{ name: '2XL', inStock: true },
45+
{ name: '3XL', inStock: true },
46+
],
47+
description:
48+
'The Basic Tee 6-Pack allows you to fully express your vibrant personality with three grayscale options. Feeling adventurous? Put on a heather gray tee. Want to be a trendsetter? Try our exclusive colorway: "Black". Need to add an extra pop of color to your outfit? Our white tee has you covered.',
49+
highlights: [
50+
'Hand cut and sewn locally',
51+
'Dyed with our proprietary colors',
52+
'Pre-washed & pre-shrunk',
53+
'Ultra-soft 100% cotton',
54+
],
55+
details:
56+
'The 6-Pack includes two black, two white, and two heather gray Basic Tees. Sign up for our subscription service and be the first to get new, exciting colors, like our upcoming "Charcoal Gray" limited release.',
57+
}
58+
const reviews = { href: '#', average: 4, totalCount: 117 }
59+
60+
function classNames(...classes) {
61+
return classes.filter(Boolean).join(' ')
62+
}
63+
64+
export default function ProductDetail() {
65+
const [selectedColor, setSelectedColor] = useState(product.colors[0])
66+
const [selectedSize, setSelectedSize] = useState(product.sizes[2])
67+
68+
return (
69+
<div className="bg-white">
70+
<div className="pt-6">
71+
<nav aria-label="Breadcrumb">
72+
<ol role="list" className="mx-auto flex max-w-2xl items-center space-x-2 px-4 sm:px-6 lg:max-w-7xl lg:px-8">
73+
{product.breadcrumbs.map((breadcrumb) => (
74+
<li key={breadcrumb.id}>
75+
<div className="flex items-center">
76+
<a href={breadcrumb.href} className="mr-2 text-sm font-medium text-gray-900">
77+
{breadcrumb.name}
78+
</a>
79+
<svg
80+
width={16}
81+
height={20}
82+
viewBox="0 0 16 20"
83+
fill="currentColor"
84+
aria-hidden="true"
85+
className="h-5 w-4 text-gray-300"
86+
>
87+
<path d="M5.697 4.34L8.98 16.532h1.327L7.025 4.341H5.697z" />
88+
</svg>
89+
</div>
90+
</li>
91+
))}
92+
<li className="text-sm">
93+
<a href={product.href} aria-current="page" className="font-medium text-gray-500 hover:text-gray-600">
94+
{product.name}
95+
</a>
96+
</li>
97+
</ol>
98+
</nav>
99+
100+
{/* Image gallery */}
101+
<div className="mx-auto mt-6 max-w-2xl sm:px-6 lg:grid lg:max-w-7xl lg:grid-cols-3 lg:gap-x-8 lg:px-8">
102+
<div className="aspect-h-4 aspect-w-3 hidden overflow-hidden rounded-lg lg:block">
103+
<img
104+
src={product.images[0].src}
105+
alt={product.images[0].alt}
106+
className="h-full w-full object-cover object-center"
107+
/>
108+
</div>
109+
<div className="hidden lg:grid lg:grid-cols-1 lg:gap-y-8">
110+
<div className="aspect-h-2 aspect-w-3 overflow-hidden rounded-lg">
111+
<img
112+
src={product.images[1].src}
113+
alt={product.images[1].alt}
114+
className="h-full w-full object-cover object-center"
115+
/>
116+
</div>
117+
<div className="aspect-h-2 aspect-w-3 overflow-hidden rounded-lg">
118+
<img
119+
src={product.images[2].src}
120+
alt={product.images[2].alt}
121+
className="h-full w-full object-cover object-center"
122+
/>
123+
</div>
124+
</div>
125+
<div className="aspect-h-5 aspect-w-4 lg:aspect-h-4 lg:aspect-w-3 sm:overflow-hidden sm:rounded-lg">
126+
<img
127+
src={product.images[3].src}
128+
alt={product.images[3].alt}
129+
className="h-full w-full object-cover object-center"
130+
/>
131+
</div>
132+
</div>
133+
134+
{/* Product info */}
135+
<div className="mx-auto max-w-2xl px-4 pb-16 pt-10 sm:px-6 lg:grid lg:max-w-7xl lg:grid-cols-3 lg:grid-rows-[auto,auto,1fr] lg:gap-x-8 lg:px-8 lg:pb-24 lg:pt-16">
136+
<div className="lg:col-span-2 lg:border-r lg:border-gray-200 lg:pr-8">
137+
<h1 className="text-2xl font-bold tracking-tight text-gray-900 sm:text-3xl">{product.name}</h1>
138+
</div>
139+
140+
{/* Options */}
141+
<div className="mt-4 lg:row-span-3 lg:mt-0">
142+
<h2 className="sr-only">Product information</h2>
143+
<p className="text-3xl tracking-tight text-gray-900">{product.price}</p>
144+
145+
{/* Reviews */}
146+
<div className="mt-6">
147+
<h3 className="sr-only">Reviews</h3>
148+
<div className="flex items-center">
149+
<div className="flex items-center">
150+
{[0, 1, 2, 3, 4].map((rating) => (
151+
<StarIcon
152+
key={rating}
153+
className={classNames(
154+
reviews.average > rating ? 'text-gray-900' : 'text-gray-200',
155+
'h-5 w-5 flex-shrink-0'
156+
)}
157+
aria-hidden="true"
158+
/>
159+
))}
160+
</div>
161+
<p className="sr-only">{reviews.average} out of 5 stars</p>
162+
<a href={reviews.href} className="ml-3 text-sm font-medium text-indigo-600 hover:text-indigo-500">
163+
{reviews.totalCount} reviews
164+
</a>
165+
</div>
166+
</div>
167+
168+
<form className="mt-10">
169+
{/* Colors */}
170+
<div>
171+
<h3 className="text-sm font-medium text-gray-900">Color</h3>
172+
173+
<RadioGroup value={selectedColor} onChange={setSelectedColor} className="mt-4">
174+
<RadioGroup.Label className="sr-only">Choose a color</RadioGroup.Label>
175+
<div className="flex items-center space-x-3">
176+
{product.colors.map((color) => (
177+
<RadioGroup.Option
178+
key={color.name}
179+
value={color}
180+
className={({ active, checked }) =>
181+
classNames(
182+
color.selectedClass,
183+
active && checked ? 'ring ring-offset-1' : '',
184+
!active && checked ? 'ring-2' : '',
185+
'relative -m-0.5 flex cursor-pointer items-center justify-center rounded-full p-0.5 focus:outline-none'
186+
)
187+
}
188+
>
189+
<RadioGroup.Label as="span" className="sr-only">
190+
{color.name}
191+
</RadioGroup.Label>
192+
<span
193+
aria-hidden="true"
194+
className={classNames(
195+
color.class,
196+
'h-8 w-8 rounded-full border border-black border-opacity-10'
197+
)}
198+
/>
199+
</RadioGroup.Option>
200+
))}
201+
</div>
202+
</RadioGroup>
203+
</div>
204+
205+
{/* Sizes */}
206+
<div className="mt-10">
207+
<div className="flex items-center justify-between">
208+
<h3 className="text-sm font-medium text-gray-900">Size</h3>
209+
<a href="#" className="text-sm font-medium text-indigo-600 hover:text-indigo-500">
210+
Size guide
211+
</a>
212+
</div>
213+
214+
<RadioGroup value={selectedSize} onChange={setSelectedSize} className="mt-4">
215+
<RadioGroup.Label className="sr-only">Choose a size</RadioGroup.Label>
216+
<div className="grid grid-cols-4 gap-4 sm:grid-cols-8 lg:grid-cols-4">
217+
{product.sizes.map((size) => (
218+
<RadioGroup.Option
219+
key={size.name}
220+
value={size}
221+
disabled={!size.inStock}
222+
className={({ active }) =>
223+
classNames(
224+
size.inStock
225+
? 'cursor-pointer bg-white text-gray-900 shadow-sm'
226+
: 'cursor-not-allowed bg-gray-50 text-gray-200',
227+
active ? 'ring-2 ring-indigo-500' : '',
228+
'group relative flex items-center justify-center rounded-md border py-3 px-4 text-sm font-medium uppercase hover:bg-gray-50 focus:outline-none sm:flex-1 sm:py-6'
229+
)
230+
}
231+
>
232+
{({ active, checked }) => (
233+
<>
234+
<RadioGroup.Label as="span">{size.name}</RadioGroup.Label>
235+
{size.inStock ? (
236+
<span
237+
className={classNames(
238+
active ? 'border' : 'border-2',
239+
checked ? 'border-indigo-500' : 'border-transparent',
240+
'pointer-events-none absolute -inset-px rounded-md'
241+
)}
242+
aria-hidden="true"
243+
/>
244+
) : (
245+
<span
246+
aria-hidden="true"
247+
className="pointer-events-none absolute -inset-px rounded-md border-2 border-gray-200"
248+
>
249+
<svg
250+
className="absolute inset-0 h-full w-full stroke-2 text-gray-200"
251+
viewBox="0 0 100 100"
252+
preserveAspectRatio="none"
253+
stroke="currentColor"
254+
>
255+
<line x1={0} y1={100} x2={100} y2={0} vectorEffect="non-scaling-stroke" />
256+
</svg>
257+
</span>
258+
)}
259+
</>
260+
)}
261+
</RadioGroup.Option>
262+
))}
263+
</div>
264+
</RadioGroup>
265+
</div>
266+
267+
<button
268+
type="submit"
269+
className="mt-10 flex w-full items-center justify-center rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
270+
>
271+
Add to Cart
272+
</button>
273+
</form>
274+
</div>
275+
276+
<div className="py-10 lg:col-span-2 lg:col-start-1 lg:border-r lg:border-gray-200 lg:pb-16 lg:pr-8 lg:pt-6">
277+
{/* Description and details */}
278+
<div>
279+
<h3 className="sr-only">Description</h3>
280+
281+
<div className="space-y-6">
282+
<p className="text-base text-gray-900">{product.description}</p>
283+
</div>
284+
</div>
285+
286+
<div className="mt-10">
287+
<h3 className="text-sm font-medium text-gray-900">Highlights</h3>
288+
289+
<div className="mt-4">
290+
<ul role="list" className="list-disc space-y-2 pl-4 text-sm">
291+
{product.highlights.map((highlight) => (
292+
<li key={highlight} className="text-gray-400">
293+
<span className="text-gray-600">{highlight}</span>
294+
</li>
295+
))}
296+
</ul>
297+
</div>
298+
</div>
299+
300+
<div className="mt-10">
301+
<h2 className="text-sm font-medium text-gray-900">Details</h2>
302+
303+
<div className="mt-4 space-y-6">
304+
<p className="text-sm text-gray-600">{product.details}</p>
305+
</div>
306+
</div>
307+
</div>
308+
</div>
309+
</div>
310+
</div>
311+
)
312+
}

src/features/product-list/ProductList.js renamed to src/features/product-list/components/ProductList.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { useState, Fragment } from 'react';
22
import { useSelector, useDispatch } from 'react-redux';
3-
import { increment, incrementAsync, selectCount } from './productListSlice';
3+
import { increment, incrementAsync, selectCount } from '../productSlice';
44
import { Dialog, Disclosure, Menu, Transition } from '@headlessui/react';
55
import { XMarkIcon } from '@heroicons/react/24/outline';
66
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
7-
7+
import { Link } from 'react-router-dom';
88
import {
99
ChevronDownIcon,
1010
FunnelIcon,
@@ -358,6 +358,7 @@ export default function ProductList() {
358358
<div className="mx-auto max-w-2xl px-4 py-0 sm:px-6 sm:py-0 lg:max-w-7xl lg:px-8">
359359
<div className="mt-6 grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:gap-x-8">
360360
{products.map((product) => (
361+
<Link to="/product-detail">
361362
<div key={product.id} className="group relative">
362363
<div className="min-h-80 aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-md bg-gray-200 lg:aspect-none group-hover:opacity-75 lg:h-80">
363364
<img
@@ -386,6 +387,7 @@ export default function ProductList() {
386387
</p>
387388
</div>
388389
</div>
390+
</Link>
389391
))}
390392
</div>
391393
</div>

0 commit comments

Comments
 (0)