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 } \n` : `Failed to complete the payment.${ pleaseRetryStr } \n` ;
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