@@ -9,12 +9,12 @@ import {
99 Minus ,
1010} from 'lucide-react'
1111import { useState } from 'react'
12- import { Link } from 'react-router'
12+ import { Link , useSearchParams } from 'react-router'
1313import { ProductCard } from '#app/components/product-card.js'
1414import {
1515 getProductById ,
1616 getRelatedProducts ,
17- } from '#app/domain/products.server.ts '
17+ } from '#app/domain/products.server.js '
1818import {
1919 getMetaFromMatches ,
2020 getMetaTitle ,
@@ -25,37 +25,79 @@ import { type Route } from './+types/_landing.products.$productId'
2525export const meta : Route . MetaFunction = ( { matches } ) => {
2626 const rootMeta = getMetaFromMatches ( matches , 'root' )
2727 const prefix = getMetaTitle ( rootMeta )
28+
2829 return [
2930 {
3031 title : constructPrefixedTitle ( 'Product overview' , prefix ) ,
3132 } ,
3233 ]
3334}
3435
35- export const loader = async ( { params } : Route . LoaderArgs ) => {
36+ export const loader = async ( { params, request } : Route . LoaderArgs ) => {
3637 const { productId } = params
3738 const product = await getProductById ( productId )
3839 const relatedProducts = await getRelatedProducts (
3940 productId ,
4041 product ?. category . id ,
4142 product ?. brand . id ,
4243 )
44+ const uniqueSizes = Array . from (
45+ new Set ( product ?. variations . map ( ( v ) => v . size ) || [ ] ) ,
46+ )
47+ const uniqueColors = Array . from (
48+ new Set ( product ?. variations . map ( ( v ) => v . color ) || [ ] ) ,
49+ )
50+
51+ const url = new URL ( request . url )
52+ const searchParams = url . searchParams
53+ // Get the selected size and color from the search params
54+ const selectedSize = searchParams . get ( 'size' ) || ''
55+ const selectedColor = searchParams . get ( 'color' ) || ''
56+ // Determine available sizes for the currently selected color
57+ const availableSizes = selectedColor
58+ ? product ?. variations
59+ . filter ( ( v ) => v . color === selectedColor && v . quantity > 0 )
60+ . map ( ( v ) => v . size ) || [ ]
61+ : uniqueSizes
62+ // Determine available colors for the currently selected size
63+ const availableColors = selectedSize
64+ ? product ?. variations
65+ . filter ( ( v ) => v . size === selectedSize && v . quantity > 0 )
66+ . map ( ( v ) => v . color ) || [ ]
67+ : uniqueColors
68+
69+ const selectedVariation = product ?. variations . find (
70+ ( v ) => v . size === selectedSize && v . color === selectedColor ,
71+ )
4372
4473 return {
4574 product,
75+ uniqueSizes,
76+ uniqueColors,
77+ availableSizes,
78+ availableColors,
79+ selectedVariation,
4680 relatedProducts,
4781 }
4882}
4983
5084export default function ProductDetailPage ( {
5185 loaderData,
5286} : Route . ComponentProps ) {
53- const { product, relatedProducts } = loaderData
54- const [ selectedSize ] = useState ( '' )
55- const [ selectedColor ] = useState ( '' )
87+ const {
88+ product,
89+ selectedVariation,
90+ uniqueColors,
91+ uniqueSizes,
92+ availableColors,
93+ availableSizes,
94+ relatedProducts,
95+ } = loaderData
96+ const [ searchParams , setSearchParams ] = useSearchParams ( )
5697 const [ quantity , setQuantity ] = useState ( 1 )
5798 const [ activeImage , setActiveImage ] = useState ( 0 )
58-
99+ const selectedSize = searchParams . get ( 'size' ) || ''
100+ const selectedColor = searchParams . get ( 'color' ) || ''
59101 if ( ! product ) {
60102 return (
61103 < div className = "flex min-h-screen items-center justify-center bg-stone-50 dark:bg-gray-900" >
@@ -83,6 +125,53 @@ export default function ProductDetailPage({
83125 alert ( 'Added to cart!' )
84126 }
85127
128+ const handleSizeChange = ( size : string ) => ( ) => {
129+ setSearchParams ( ( prev ) => {
130+ const newParams = new URLSearchParams ( prev )
131+ if ( size === selectedSize ) {
132+ newParams . delete ( 'size' )
133+ } else {
134+ newParams . set ( 'size' , size )
135+ }
136+ const color = newParams . get ( 'color' )
137+ // If the selected color is not available for the new size, remove it
138+ const noQuantityForColor =
139+ color &&
140+ product . variations . some (
141+ ( v ) => v . size === size && v . color === color && v . quantity === 0 ,
142+ )
143+
144+ if ( noQuantityForColor ) {
145+ newParams . delete ( 'color' )
146+ }
147+ return newParams
148+ } )
149+ setQuantity ( 1 )
150+ }
151+
152+ const handleColorChange = ( color : string ) => ( ) => {
153+ setSearchParams ( ( prev ) => {
154+ const newParams = new URLSearchParams ( prev )
155+ if ( color === selectedColor ) {
156+ newParams . delete ( 'color' )
157+ } else {
158+ newParams . set ( 'color' , color )
159+ }
160+ const size = newParams . get ( 'size' )
161+ // If the selected size is not available for the new color, remove it
162+ const noQuantityForSize =
163+ size &&
164+ product . variations . some (
165+ ( v ) => v . color === color && v . size === size && v . quantity === 0 ,
166+ )
167+ if ( noQuantityForSize ) {
168+ newParams . delete ( 'size' )
169+ }
170+ return newParams
171+ } )
172+ setQuantity ( 1 )
173+ }
174+
86175 // Mock additional images for gallery
87176 const productImages = [
88177 product . imageUrl ,
@@ -179,19 +268,21 @@ export default function ProductDetailPage({
179268 Size
180269 </ h3 >
181270 < div className = "grid grid-cols-6 gap-3" >
182- { /* {product.sizes .map((size) => (
271+ { uniqueSizes . map ( ( size ) => (
183272 < button
184273 key = { size }
185- onClick={() => setSelectedSize(size)}
186- className={`rounded-lg border px-4 py-3 text-center transition-colors duration-200 ${
274+ disabled = { ! availableSizes . includes ( size ) }
275+ title = { ! availableSizes . includes ( size ) ? 'Out of stock' : '' }
276+ onClick = { handleSizeChange ( size ) }
277+ className = { `rounded-lg border px-4 py-3 text-center transition-colors duration-200 disabled:opacity-20 ${
187278 selectedSize === size
188279 ? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'
189280 : 'border-gray-300 text-gray-700 hover:border-amber-300 dark:border-gray-600 dark:text-gray-300 dark:hover:border-amber-700'
190281 } `}
191282 >
192283 { size }
193284 </ button >
194- ))} */ }
285+ ) ) }
195286 </ div >
196287 </ div >
197288
@@ -200,53 +291,64 @@ export default function ProductDetailPage({
200291 < h3 className = "mb-4 text-lg font-medium text-gray-900 dark:text-white" >
201292 Color
202293 </ h3 >
203- < div className = "flex space-x-3" >
204- { /* {product.colors .map((color) => (
294+ < div className = "flex flex-wrap space-x-3" >
295+ { uniqueColors . map ( ( color ) => (
205296 < button
206297 key = { color }
207- onClick={() => setSelectedColor(color)}
208- className={`rounded-lg border px-4 py-2 transition-colors duration-200 ${
298+ disabled = { ! availableColors . includes ( color ) }
299+ title = {
300+ ! availableColors . includes ( color ) ? 'Out of stock' : ''
301+ }
302+ onClick = { handleColorChange ( color ) }
303+ className = { `rounded-lg border px-4 py-2 transition-colors duration-200 disabled:opacity-20 ${
209304 selectedColor === color
210305 ? 'border-amber-500 bg-amber-50 text-amber-800 dark:bg-amber-900/30 dark:text-amber-200'
211306 : 'border-gray-300 text-gray-700 hover:border-amber-300 dark:border-gray-600 dark:text-gray-300 dark:hover:border-amber-700'
212307 } `}
213308 >
214309 { color }
215310 </ button >
216- ))} */ }
311+ ) ) }
217312 </ div >
218313 </ div >
219314
220315 { /* Quantity */ }
221- < div >
222- < h3 className = "mb-4 text-lg font-medium text-gray-900 dark:text-white" >
223- Quantity
224- </ h3 >
225- < div className = "flex items-center space-x-4" >
226- < div className = "flex items-center rounded-lg border border-gray-300 dark:border-gray-600" >
227- < button
228- onClick = { ( ) => setQuantity ( Math . max ( 1 , quantity - 1 ) ) }
229- className = "p-2 transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-700"
230- >
231- < Minus className = "h-4 w-4" />
232- </ button >
233- < span className = "px-4 py-2 font-medium text-gray-900 dark:text-white" >
234- { quantity }
235- </ span >
236- < button
237- onClick = { ( ) => setQuantity ( quantity + 1 ) }
238- className = "p-2 transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-700"
239- >
240- < Plus className = "h-4 w-4" />
241- </ button >
316+ { selectedVariation ? (
317+ < div >
318+ < h3 className = "mb-4 text-lg font-medium text-gray-900 dark:text-white" >
319+ Quantity
320+ </ h3 >
321+ < div className = "flex items-center space-x-4" >
322+ < div className = "flex items-center rounded-lg border border-gray-300 dark:border-gray-600" >
323+ < button
324+ onClick = { ( ) => setQuantity ( Math . max ( 1 , quantity - 1 ) ) }
325+ disabled = { quantity === 1 }
326+ className = "p-2 transition-colors duration-200 hover:bg-gray-100 disabled:opacity-20 disabled:hover:bg-transparent dark:hover:bg-gray-700"
327+ >
328+ < Minus className = "h-4 w-4" />
329+ </ button >
330+ < span className = "px-4 py-2 font-medium text-gray-900 dark:text-white" >
331+ { quantity }
332+ </ span >
333+ < button
334+ onClick = { ( ) => setQuantity ( quantity + 1 ) }
335+ disabled = { quantity === selectedVariation . quantity }
336+ className = "p-2 transition-colors duration-200 hover:bg-gray-100 disabled:opacity-20 disabled:hover:bg-transparent dark:hover:bg-gray-700"
337+ >
338+ < Plus className = "h-4 w-4" />
339+ </ button >
340+ </ div >
242341 </ div >
243342 </ div >
244- </ div >
343+ ) : null }
245344
246345 { /* Add to Cart */ }
247346 < div className = "flex space-x-4" >
248347 < button
249348 onClick = { handleAddToCart }
349+ disabled = {
350+ ! selectedVariation || selectedVariation . quantity === 0
351+ }
250352 className = "flex-1 rounded-lg bg-amber-600 px-6 py-4 font-medium text-white transition-colors duration-300 hover:bg-amber-700 hover:shadow-lg"
251353 >
252354 Add to Cart
0 commit comments