11import  *  as  Common  from  "@frontend/common" ; 
2- import  {  AccordionProps ,  Button ,  ButtonProps ,  CircularProgress ,  Divider ,  Stack ,  TextField ,  Typography  }  from  "@mui/material" ; 
2+ import  {  Close  }  from  "@mui/icons-material" ; 
3+ import  { 
4+   AccordionProps , 
5+   Box , 
6+   Button , 
7+   ButtonProps , 
8+   Card , 
9+   CardActions , 
10+   CardContent , 
11+   CardMedia , 
12+   CircularProgress , 
13+   Dialog , 
14+   DialogContent , 
15+   DialogProps , 
16+   DialogTitle , 
17+   Divider , 
18+   Grid , 
19+   IconButton , 
20+   Stack , 
21+   styled , 
22+   TextField , 
23+   Typography , 
24+ }  from  "@mui/material" ; 
325import  {  ErrorBoundary ,  Suspense  }  from  "@suspensive/react" ; 
426import  {  useQueryClient  }  from  "@tanstack/react-query" ; 
527import  {  enqueueSnackbar ,  OptionsObject  }  from  "notistack" ; 
@@ -68,14 +90,15 @@ const NotPurchasable: React.FC<React.PropsWithChildren> = ({ children }) => {
6890  ) ; 
6991} ; 
7092
71- type  ProductItemPropType  =  Omit < AccordionProps ,   "children" >   &   { 
93+ type  ProductItemPropType  =  { 
7294  disabled ?: boolean ; 
7395  language : "ko"  |  "en" ; 
7496  product : ShopSchemas . Product ; 
97+   onAddToCartSuccess ?: ( )  =>  void ; 
7598  startPurchaseProcess : ( oneItemOrderData : ShopSchemas . CartItemAppendRequest )  =>  void ; 
7699} ; 
77100
78- const  ProductItem : React . FC < ProductItemPropType >  =  ( {  disabled : rootDisabled ,  language,  product,  startPurchaseProcess,  ... props  } )  =>  { 
101+ const  ProductItem : React . FC < ProductItemPropType >  =  ( {  disabled : rootDisabled ,  language,  product,  startPurchaseProcess,  onAddToCartSuccess  } )  =>  { 
79102  const  navigate  =  useNavigate ( ) ; 
80103  const  [ ,  forceRender ]  =  React . useReducer ( ( x )  =>  x  +  1 ,  0 ) ; 
81104  const  [ donationPrice ,  setDonationPrice ]  =  React . useState < string > ( product . donation_min_price ?. toString ( )  ||  "0" ) ; 
@@ -168,7 +191,7 @@ const ProductItem: React.FC<ProductItemPropType> = ({ disabled: rootDisabled, la
168191    if  ( ! formData )  return ; 
169192
170193    addItemToCartMutation . mutate ( formData ,  { 
171-       onSuccess : ( )  => 
194+       onSuccess : ( )  =>   { 
172195        addSnackbar ( 
173196          < Stack  spacing = { 2 }  justifyContent = "center"  alignItems = "center"  sx = { {  width : "100%" ,  flexGrow : 1  } } > 
174197            < div > { succeededToAddOneItemToCartStr } </ div > 
@@ -182,7 +205,9 @@ const ProductItem: React.FC<ProductItemPropType> = ({ disabled: rootDisabled, la
182205            /> 
183206          </ Stack > , 
184207          "success" 
185-         ) , 
208+         ) ; 
209+         onAddToCartSuccess ?.( ) ; 
210+       } , 
186211      onError : ( )  =>  alert ( failedToAddOneItemToCartStr ) , 
187212    } ) ; 
188213  } ; 
@@ -202,15 +227,8 @@ const ProductItem: React.FC<ProductItemPropType> = ({ disabled: rootDisabled, la
202227    return  totalPrice ; 
203228  } ; 
204229
205-   const  actionButton  =  R . isNullish ( notPurchasableReason )  &&  ( 
206-     < CommonComponents . SignInGuard  fallback = { < NotPurchasable > { requiresSignInStr } </ NotPurchasable > } > 
207-       < Button  { ...actionButtonProps }  onClick = { addItemToCart }  children = { addToCartStr }  /> 
208-       < Button  { ...actionButtonProps }  onClick = { onOrderOneItemButtonClick }  children = { orderOneItemStr }  /> 
209-     </ CommonComponents . SignInGuard > 
210-   ) ; 
211- 
212230  return  ( 
213-     < Common . Components . MDX . PrimaryStyledDetails   { ... props }   summary = { product . name }   actions = { actionButton } > 
231+     < > 
214232      < Common . Components . MDXRenderer  text = { product . description  ||  "" }  /> 
215233      < br  /> 
216234      < Divider  /> 
@@ -283,11 +301,108 @@ const ProductItem: React.FC<ProductItemPropType> = ({ disabled: rootDisabled, la
283301      )  : ( 
284302        < NotPurchasable > { notPurchasableReason } </ NotPurchasable > 
285303      ) } 
304+       { R . isNullish ( notPurchasableReason )  &&  ( 
305+         < CommonComponents . SignInGuard  fallback = { < NotPurchasable > { requiresSignInStr } </ NotPurchasable > } > 
306+           < Stack  direction = "row"  spacing = { 1 }  sx = { {  justifyContent : "flex-end" ,  mt : 2  } } > 
307+             < Button  { ...actionButtonProps }  onClick = { addItemToCart }  children = { addToCartStr }  /> 
308+             < Button  { ...actionButtonProps }  onClick = { onOrderOneItemButtonClick }  children = { orderOneItemStr }  /> 
309+           </ Stack > 
310+         </ CommonComponents . SignInGuard > 
311+       ) } 
312+     </ > 
313+   ) ; 
314+ } ; 
315+ 
316+ type  FoldableProductItemPropType  =  Omit < AccordionProps ,  "children" >  &  ProductItemPropType ; 
317+ 
318+ const  FoldableProductItem : React . FC < FoldableProductItemPropType >  =  ( {  disabled,  language,  product,  startPurchaseProcess,  ...props  } )  =>  { 
319+   return  ( 
320+     < Common . Components . MDX . PrimaryStyledDetails  { ...props }  summary = { product . name } > 
321+       < ProductItem  disabled = { disabled }  language = { language }  product = { product }  startPurchaseProcess = { startPurchaseProcess }  /> 
286322    </ Common . Components . MDX . PrimaryStyledDetails > 
287323  ) ; 
288324} ; 
289325
290- type  ProductStateType  =  { 
326+ const  CloseButton  =  styled ( IconButton ) ( ( {  theme } )  =>  ( { 
327+   position : "absolute" , 
328+   right : theme . spacing ( 1 ) , 
329+   top : theme . spacing ( 1 ) , 
330+   color : theme . palette . grey [ 500 ] , 
331+ } ) ) ; 
332+ 
333+ type  DialogedProductItemPropType  =  Omit < DialogProps ,  "children" >  & 
334+   Omit < ProductItemPropType ,  "product" >  &  { 
335+     product ?: ShopSchemas . Product ; 
336+   } ; 
337+ 
338+ const  DialogedProductItem : React . FC < DialogedProductItemPropType >  =  ( {  disabled,  language,  product,  startPurchaseProcess,  ...props  } )  =>  { 
339+   const  dialogTitle  =  language  ===  "ko"  ? "상품 상세 정보"  : "Product Details" ; 
340+   const  onCloseClick  =  ( props . onClose  as  ( )  =>  void )  ||  ( ( )  =>  { } ) ; 
341+   return  ( 
342+     < Dialog  maxWidth = "md"  fullWidth  { ...props } > 
343+       < DialogTitle > { dialogTitle } </ DialogTitle > 
344+       < CloseButton  onClick = { onCloseClick }  children = { < Close  /> }  /> 
345+       < DialogContent > 
346+         { product  &&  ( 
347+           < ProductItem 
348+             disabled = { disabled } 
349+             language = { language } 
350+             product = { product } 
351+             startPurchaseProcess = { startPurchaseProcess } 
352+             onAddToCartSuccess = { onCloseClick } 
353+           /> 
354+         ) } 
355+       </ DialogContent > 
356+     </ Dialog > 
357+   ) ; 
358+ } ; 
359+ 
360+ type  ProductImageCardPropType  =  { 
361+   language : "ko"  |  "en" ; 
362+   product : ShopSchemas . Product ; 
363+   disabled ?: boolean ; 
364+   showDetail : ( product : ShopSchemas . Product )  =>  void ; 
365+ } ; 
366+ 
367+ const  StyledProductImageCard  =  styled ( Card ) ( ( {  theme } )  =>  ( { 
368+   cursor : "pointer" , 
369+   maxWidth : "300px" , 
370+   borderRadius : "0.5rem" , 
371+   border : `1px solid ${ theme . palette . primary . light }  , 
372+   transition : "all 0.2s ease" , 
373+ 
374+   "&:hover" : { 
375+     boxShadow : theme . shadows [ 3 ] , 
376+     borderColor : theme . palette . primary . main , 
377+   } , 
378+ } ) ) ; 
379+ 
380+ const  ProductImageCard : React . FC < ProductImageCardPropType >  =  ( {  language,  product,  disabled,  showDetail } )  =>  { 
381+   const  showDetailStr  =  language  ===  "ko"  ? "상품 상세 정보 보기"  : "View Product Details" ; 
382+   return  ( 
383+     < StyledProductImageCard  onClick = { ( )  =>  showDetail ( product ) }  elevation = { 0 } > 
384+       < CardMedia  sx = { {  height : "200px" ,  objectFit : "contain" ,  borderRadius : "0 0 0.5rem 0.5rem"  } } > 
385+         < Common . Components . FallbackImage 
386+           src = { product . image  ||  "" } 
387+           alt = "Product Image" 
388+           loading = "lazy" 
389+           errorFallback = { < Box  sx = { {  width : "100%" ,  height : "100%" ,  flexGrow : 1 ,  backgroundColor : "#bbb" ,  borderRadius : "0 0 0.5rem 0.5rem"  } }  /> } 
390+         /> 
391+       </ CardMedia > 
392+       < CardContent  sx = { {  py : 1  } } > 
393+         < Stack  spacing = { 1 } > 
394+           < Typography  variant = "h6"  sx = { {  textAlign : "center"  } }  children = { product . name }  /> 
395+           < Typography  variant = "body1"  sx = { {  textAlign : "right"  } }  children = { < CommonComponents . PriceDisplay  price = { product . price }  /> }  /> 
396+         </ Stack > 
397+       </ CardContent > 
398+       < CardActions > 
399+         < Button  variant = "outlined"  color = "primary"  disabled = { disabled }  children = { showDetailStr }  fullWidth  /> 
400+       </ CardActions > 
401+     </ StyledProductImageCard > 
402+   ) ; 
403+ } ; 
404+ 
405+ type  ProductListStateType  =  { 
291406  openDialog : boolean ; 
292407  openBackdrop : boolean ; 
293408  product ?: ShopSchemas . Product ; 
@@ -303,7 +418,7 @@ export const ProductList: React.FC<ShopSchemas.ProductListQueryParams> = (qs) =>
303418    const  oneItemOrderStartMutation  =  ShopHooks . usePrepareOneItemOrderMutation ( shopAPIClient ) ; 
304419    const  {  data }  =  ShopHooks . useProducts ( shopAPIClient ,  qs ) ; 
305420
306-     const  [ state ,  setState ]  =  React . useState < ProductStateType > ( { 
421+     const  [ state ,  setState ]  =  React . useState < ProductListStateType > ( { 
307422      openDialog : false , 
308423      openBackdrop : false , 
309424    } ) ; 
@@ -353,7 +468,7 @@ export const ProductList: React.FC<ShopSchemas.ProductListQueryParams> = (qs) =>
353468        < CommonComponents . CustomerInfoFormDialog  open = { state . openDialog }  closeFunc = { closeDialog }  onSubmit = { onFormSubmit }  /> 
354469        < Common . Components . MDX . OneDetailsOpener > 
355470          { data . map ( ( p )  =>  ( 
356-             < ProductItem 
471+             < FoldableProductItem 
357472              disabled = { oneItemOrderStartMutation . isPending } 
358473              language = { language } 
359474              key = { p . id } 
@@ -376,3 +491,105 @@ export const ProductList: React.FC<ShopSchemas.ProductListQueryParams> = (qs) =>
376491    </ ErrorBoundary > 
377492  ) ; 
378493} ; 
494+ 
495+ type  ProductImageCardListStateType  =  { 
496+   openProductDialog : boolean ; 
497+   openCustomerInfoDialog : boolean ; 
498+   openBackdrop : boolean ; 
499+   product ?: ShopSchemas . Product ; 
500+   oneItemOrderData ?: ShopSchemas . CartItemAppendRequest ; 
501+ } ; 
502+ 
503+ export  const  ProductImageCardList : React . FC < ShopSchemas . ProductListQueryParams >  =  ( qs )  =>  { 
504+   const  WrappedProductImageCardList : React . FC  =  ( )  =>  { 
505+     const  queryClient  =  useQueryClient ( ) ; 
506+     const  navigate  =  useNavigate ( ) ; 
507+     const  {  language,  shopImpAccountId }  =  ShopHooks . useShopContext ( ) ; 
508+     const  shopAPIClient  =  ShopHooks . useShopClient ( ) ; 
509+     const  oneItemOrderStartMutation  =  ShopHooks . usePrepareOneItemOrderMutation ( shopAPIClient ) ; 
510+     const  {  data }  =  ShopHooks . useProducts ( shopAPIClient ,  qs ) ; 
511+ 
512+     const  [ state ,  setState ]  =  React . useState < ProductImageCardListStateType > ( { 
513+       openProductDialog : false , 
514+       openCustomerInfoDialog : false , 
515+       openBackdrop : false , 
516+     } ) ; 
517+ 
518+     const  openProductDialog  =  ( product : ShopSchemas . Product )  =>  setState ( ( ps )  =>  ( {  ...ps ,  product,  openProductDialog : true  } ) ) ; 
519+     const  closeProductDialog  =  ( )  =>  setState ( ( ps )  =>  ( {  ...ps ,  openProductDialog : false  } ) ) ; 
520+     const  openCustomerInfoDialog  =  ( )  =>  setState ( ( ps )  =>  ( {  ...ps ,  openCustomerInfoDialog : true  } ) ) ; 
521+     const  closeCustomerInfoDialog  =  ( )  =>  setState ( ( ps )  =>  ( {  ...ps ,  openCustomerInfoDialog : false  } ) ) ; 
522+     const  openBackdrop  =  ( )  =>  setState ( ( ps )  =>  ( {  ...ps ,  openBackdrop : true  } ) ) ; 
523+     const  closeBackdrop  =  ( )  =>  setState ( ( ps )  =>  ( {  ...ps ,  openBackdrop : false  } ) ) ; 
524+     const  setProductDataAndOpenDialog  =  ( oneItemOrderData : ShopSchemas . CartItemAppendRequest )  =>  { 
525+       closeProductDialog ( ) ; 
526+       setState ( ( ps )  =>  ( {  ...ps ,  oneItemOrderData } ) ) ; 
527+       openCustomerInfoDialog ( ) ; 
528+     } ; 
529+ 
530+     const  pleaseRetryStr  =  language  ===  "ko"  ? "\n잠시 후 다시 시도해주세요."  : "\nPlease try again later." ; 
531+     const  failedToOrderStr  =  language  ===  "ko"  ? `결제에 실패했습니다.${ pleaseRetryStr }   : `Failed to complete the payment.${ pleaseRetryStr }  ; 
532+     const  orderErrorStr  = 
533+       language  ===  "ko"  ? `결제 준비 중 문제가 발생했습니다,${ pleaseRetryStr }   : `An error occurred while preparing the payment,${ pleaseRetryStr }  ; 
534+ 
535+     const  onFormSubmit  =  ( customer_info : ShopSchemas . CustomerInfo )  =>  { 
536+       if  ( ! state . oneItemOrderData )  return ; 
537+ 
538+       closeCustomerInfoDialog ( ) ; 
539+       openBackdrop ( ) ; 
540+       oneItemOrderStartMutation . mutate ( 
541+         {  ...state . oneItemOrderData ,  customer_info : customer_info  } , 
542+         { 
543+           onSuccess : ( order : ShopSchemas . Order )  =>  { 
544+             ShopUtils . startPortOnePurchase ( 
545+               shopImpAccountId , 
546+               order , 
547+               ( )  =>  { 
548+                 queryClient . invalidateQueries ( ) ; 
549+                 queryClient . resetQueries ( ) ; 
550+                 navigate ( "/store/thank-you-for-your-purchase" ) ; 
551+               } , 
552+               ( response )  =>  alert ( failedToOrderStr  +  response . error_msg ) , 
553+               closeBackdrop 
554+             ) ; 
555+           } , 
556+           onError : ( error )  =>  alert ( error . message  ||  orderErrorStr ) , 
557+         } 
558+       ) ; 
559+     } ; 
560+ 
561+     return  ( 
562+       < > 
563+         < CommonComponents . CustomerInfoFormDialog  open = { state . openCustomerInfoDialog }  closeFunc = { closeCustomerInfoDialog }  onSubmit = { onFormSubmit }  /> 
564+         < DialogedProductItem 
565+           open = { state . openProductDialog } 
566+           onClose = { closeProductDialog } 
567+           language = { language } 
568+           product = { state . product } 
569+           startPurchaseProcess = { setProductDataAndOpenDialog } 
570+         /> 
571+         < Grid > 
572+           { data . map ( ( p )  =>  ( 
573+             < ProductImageCard 
574+               disabled = { oneItemOrderStartMutation . isPending } 
575+               language = { language } 
576+               key = { p . id } 
577+               product = { p } 
578+               showDetail = { openProductDialog } 
579+             /> 
580+           ) ) } 
581+         </ Grid > 
582+       </ > 
583+     ) ; 
584+   } ; 
585+ 
586+   return  ( 
587+     < ErrorBoundary  fallback = { < div > 상품 목록을 불러오는 중 문제가 발생했습니다.</ div > } > 
588+       < Suspense  fallback = { < CircularProgress  /> } > 
589+         < Stack  spacing = { 2 } > 
590+           < WrappedProductImageCardList  /> 
591+         </ Stack > 
592+       </ Suspense > 
593+     </ ErrorBoundary > 
594+   ) ; 
595+ } ; 
0 commit comments