[251209] feat: NBCU-ShopByShow-1

🕐 커밋 시간: 2025. 12. 09. 14:33:20

📊 변경 통계:
  • 총 파일: 15개
  • 추가: +128줄
  • 삭제: -11줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShow.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowContents/ShopByShowContents.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowList.module.less
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowNav/ShopByShowNav.jsx
  + com.twin.app.shoptime/src/views/FeaturedBrandsPanel/ShopByShow/ShopByShowList/ShopByShowNav/ShopByShowNav.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/brandActions.js
  ~ com.twin.app.shoptime/src/api/apiConfig.js
  ~ com.twin.app.shoptime/src/reducers/brandReducer.js
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBestSeller/FeaturedBestSellerList/FeaturedBestSellerList.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/FeaturedBrandsPanel.jsx
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/QuickMenu/QuickMenuItem/QuickMenuItem.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • API 서비스 레이어 개선
  • 중간 규모 기능 개선
  • 모듈 구조 개선
This commit is contained in:
2025-12-09 14:33:21 +09:00
parent 439e5f46e3
commit 4a6473e1e5
15 changed files with 732 additions and 11 deletions

View File

@@ -130,6 +130,7 @@ export const types = {
GET_BRAND_CREATORS_INFO: 'GET_BRAND_CREATORS_INFO',
GET_BRAND_SHOWROOM: 'GET_BRAND_SHOWROOM',
GET_BRAND_RECENTLY_AIRED: 'GET_BRAND_RECENTLY_AIRED',
GET_BRAND_SHOP_BY_SHOW: 'GET_BRAND_SHOP_BY_SHOW',
SET_BRAND_LIVE_CHANNEL_UPCOMING: 'SET_BRAND_LIVE_CHANNEL_UPCOMING',
SET_BRAND_CHAN_INFO: 'SET_BRAND_CHAN_INFO',
RESET_BRAND_STATE: 'RESET_BRAND_STATE',

View File

@@ -37,10 +37,12 @@ export const getBrandList = () => (dispatch, getState) => {
export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
const { patnrId } = props;
console.log("[getBrandLayoutInfo] Called - patnrId:", patnrId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => {
// dlog("getBrandLayoutInfo onSuccess ", response.data);
console.log("[getBrandLayoutInfo] onSuccess - patnrId:", patnrId, "data:", response.data.data);
dispatch({
type: types.GET_BRAND_LAYOUT_INFO,
@@ -53,6 +55,7 @@ export const getBrandLayoutInfo = (props) => (dispatch, getState) => {
};
const onFail = (error) => {
console.log("[getBrandLayoutInfo] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandLayoutInfo onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
@@ -336,10 +339,12 @@ export const getBrandCategoryProductInfo = (props) => (dispatch, getState) => {
export const getBrandBestSeller = (props) => (dispatch, getState) => {
const { patnrId } = props;
console.log("[getBrandBestSeller] Called - patnrId:", patnrId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => {
// dlog("getBrandBestSeller onSuccess ", response.data);
console.log("[getBrandBestSeller] onSuccess - patnrId:", patnrId, "data:", response.data.data);
dispatch({
type: types.GET_BRAND_BEST_SELLER,
@@ -352,6 +357,7 @@ export const getBrandBestSeller = (props) => (dispatch, getState) => {
};
const onFail = (error) => {
console.log("[getBrandBestSeller] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandBestSeller onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
@@ -386,6 +392,39 @@ export const getBrandShowroom = (props) => (dispatch, getState) => {
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_SHOWROOM, { patnrId }, {}, onSuccess, onFail);
};
// Featured Brands SHOP BY SHOW 정보 조회 IF-LGSP-376
export const getBrandShopByShow = (props) => (dispatch, getState) => {
const { patnrId, contsId } = props;
console.log("[getBrandShopByShow] Called - patnrId:", patnrId, "contsId:", contsId);
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } }));
const onSuccess = (response) => {
console.log("[getBrandShopByShow] onSuccess - patnrId:", patnrId, "data:", response.data.data);
dispatch({
type: types.GET_BRAND_SHOP_BY_SHOW,
payload: {
data: response.data.data,
},
});
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
const onFail = (error) => {
console.log("[getBrandShopByShow] onFail - patnrId:", patnrId, "error:", error);
derror('getBrandShopByShow onFail ', error);
dispatch(changeAppStatus({ showLoadingPanel: { show: false } }));
};
// patnrId: 필수, contsId: 선택
const params = contsId ? { patnrId, contsId } : { patnrId };
TAxios(dispatch, getState, 'get', URLS.GET_BRAND_SHOP_BY_SHOW, params, {}, onSuccess, onFail);
};
// Featured Brands Recently Aired 조회 IF-LGSP-373
export const getBrandRecentlyAired = (props) => (dispatch, getState) => {
const { patnrId } = props;

View File

@@ -55,6 +55,7 @@ export const URLS = {
GET_BRAND_CREATORS_INFO: "/lgsp/v1/brand/creators.lge",
GET_BRAND_SHOWROOM: "/lgsp/v1/brand/showroom.lge",
GET_BRAND_RECENTLY_AIRED: "/lgsp/v1/brand/recently/aired.lge",
GET_BRAND_SHOP_BY_SHOW: "/lgsp/v1/brand/shopByShow.lge",
//on-sale controller
GET_ON_SALE_INFO: "/lgsp/v1/onsale/onsale.lge",

View File

@@ -44,6 +44,10 @@ const initialState = {
brandRecentlyAiredData: {
data: {},
},
brandShopByShowData: {
data: {},
},
};
export const brandReducer = (state = initialState, action) => {
@@ -155,6 +159,12 @@ export const brandReducer = (state = initialState, action) => {
brandRecentlyAiredData: action.payload,
};
case types.GET_BRAND_SHOP_BY_SHOW:
return {
...state,
brandShopByShowData: action.payload,
};
case types.SET_BRAND_LIVE_CHANNEL_UPCOMING:
return {
...state,

View File

@@ -60,6 +60,10 @@ export default function FeaturedBestSellerList({
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
console.log("[FeaturedBestSellerList] Mounted - selectedPatnrId:", selectedPatnrId);
console.log("[FeaturedBestSellerList] brandBestSellerInfo:", brandBestSellerInfo);
console.log("[FeaturedBestSellerList] Data count:", brandBestSellerInfo?.length || 0);
const cursorVisible = useSelector(
(state) => state.common.appStatus.cursorVisible
);
@@ -155,6 +159,9 @@ export default function FeaturedBestSellerList({
lgCatNm,
euEnrgLblInfos,
} = brandBestSellerInfo[index];
console.log("[FeaturedBestSellerList] renderItem - index:", index, "patnrId:", patnrId, "rankOrd:", rankOrd, "prdtNm:", prdtNm);
const rankText =
rankOrd === 1
? rankOrd + "st,"

View File

@@ -23,6 +23,7 @@ import {
getBrandLiveChannelInfo,
getBrandRecommendedShowInfo,
getBrandSeriesInfo,
getBrandShopByShow,
getBrandShowroom,
getBrandTSVInfo,
} from "../../actions/brandActions";
@@ -63,6 +64,7 @@ import LiveChannels from "./LiveChannels/LiveChannels";
import QuickMenu from "./QuickMenu/QuickMenu";
import RecommendedShows from "./RecommendedShows/RecommendedShows";
import Series from "./Series/Series";
import ShopByShow from "./ShopByShow/ShopByShow";
import Showroom from "./Showroom/Showroom";
import TodaysDeals from "./TodaysDeals/TodaysDeals";
import UpComing from "./UpComing/UpComing";
@@ -90,6 +92,7 @@ const TEMPLATE_CODE_CONF = {
SERIES: "BRD00107",
CATEGORY: "BRD00108",
SHOWROOM: "BRD00109",
NBCU: "BRD00110",
};
const DISPATCH_MAP = Object.freeze({
@@ -101,6 +104,7 @@ const DISPATCH_MAP = Object.freeze({
[TEMPLATE_CODE_CONF.SERIES]: getBrandSeriesInfo,
[TEMPLATE_CODE_CONF.CATEGORY]: getBrandCategoryInfo,
[TEMPLATE_CODE_CONF.SHOWROOM]: getBrandShowroom,
[TEMPLATE_CODE_CONF.NBCU]: getBrandShopByShow,
});
const TOP_MARGIN = 36;
@@ -263,6 +267,12 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
const brandShowroomInfo = useSelector(
(state) => state.brand.brandShowroomData.data.brandShowroomInfo
);
const brandShopByShowContsList = useSelector(
(state) => state.brand.brandShopByShowData.data.brandShopByShowContsList
);
const brandShopByShowContsInfo = useSelector(
(state) => state.brand.brandShopByShowData.data.brandShopByShowContsInfo
);
const [displayTopButton, setDisplayTopButton] = useState(false);
const [focusedContainerId, setFocusedContainerId] = useState(null);
@@ -412,9 +422,12 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
);
const renderPageItem = useCallback(() => {
console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo length:", sortedBrandLayoutInfo.length);
console.log("[FeaturedBrandsPanel] renderPageItem - sortedBrandLayoutInfo items:", sortedBrandLayoutInfo.map(el => el.shptmBrndOptTpCd));
return (
<>
{sortedBrandLayoutInfo.map((el, idx) => {
console.log("[FeaturedBrandsPanel] Processing template code:", el.shptmBrndOptTpCd);
switch (el.shptmBrndOptTpCd) {
case TEMPLATE_CODE_CONF.LIVE_CHANNELS: {
return (
@@ -485,6 +498,10 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
}
case TEMPLATE_CODE_CONF.BEST_SELLER: {
console.log("[FeaturedBrandsPanel] BEST_SELLER - patnrId:", selectedPatnrId);
console.log("[FeaturedBrandsPanel] BEST_SELLER - hasTemplateCode:", hasTemplateCodeWithValue(sortedBrandLayoutInfo, TEMPLATE_CODE_CONF.BEST_SELLER));
console.log("[FeaturedBrandsPanel] BEST_SELLER - shouldRender:", shouldRenderComponent(brandBestSellerInfo));
console.log("[FeaturedBrandsPanel] BEST_SELLER - data:", brandBestSellerInfo);
return (
<React.Fragment key={el.shptmBrndOptTpCd}>
{hasTemplateCodeWithValue(
@@ -492,6 +509,8 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
TEMPLATE_CODE_CONF.BEST_SELLER
) &&
shouldRenderComponent(brandBestSellerInfo) && (
<>
{console.log("[FeaturedBrandsPanel] Rendering FeaturedBestSeller for patnrId:", selectedPatnrId)}
<FeaturedBestSeller
brandBestSellerInfo={brandBestSellerInfo}
handleItemFocus={handleItemFocus}
@@ -501,6 +520,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
spotlightId={TEMPLATE_CODE_CONF.BEST_SELLER}
selectedPatnrId={selectedPatnrId}
/>
</>
)}
</React.Fragment>
);
@@ -650,6 +670,36 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
</React.Fragment>
);
}
case TEMPLATE_CODE_CONF.NBCU: {
console.log("[FeaturedBrandsPanel] NBCU - patnrId:", selectedPatnrId);
console.log("[FeaturedBrandsPanel] NBCU - hasTemplateCode:", hasTemplateCodeWithValue(sortedBrandLayoutInfo, TEMPLATE_CODE_CONF.NBCU));
console.log("[FeaturedBrandsPanel] NBCU - shouldRender:", shouldRenderComponent(brandShopByShowContsList));
console.log("[FeaturedBrandsPanel] NBCU - data:", brandShopByShowContsList);
return (
<React.Fragment key={el.shptmBrndOptTpCd}>
{hasTemplateCodeWithValue(
sortedBrandLayoutInfo,
TEMPLATE_CODE_CONF.NBCU
) &&
shouldRenderComponent(brandShopByShowContsList) && (
<>
{console.log("[FeaturedBrandsPanel] Rendering ShopByShow for patnrId:", selectedPatnrId)}
<ShopByShow
brandShopByShowContsList={brandShopByShowContsList}
brandShopByShowContsInfo={brandShopByShowContsInfo}
handleItemFocus={handleItemFocus}
order={idx + 1}
shelfOrder={el.expsOrd}
shelfTitle={el.shptmBrndOptTpNm}
spotlightId={TEMPLATE_CODE_CONF.NBCU}
selectedPatnrId={selectedPatnrId}
/>
</>
)}
</React.Fragment>
);
}
}
})}
</>
@@ -668,6 +718,8 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
brandSeriesGroupInfo,
brandSeriesInfo,
brandShowroomInfo,
brandShopByShowContsList,
brandShopByShowContsInfo,
brandTsvInfo,
fromGNB,
fromQuickMenu,
@@ -711,6 +763,7 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
// effect: layout information fetching due to partner id change
useEffect(() => {
if (!fromDetail) {
console.log("[FeaturedBrandsPanel] Layout Info Effect - patnrId:", panelInfo?.patnrId);
dispatch({ type: types.RESET_BRAND_LAYOUT_INFO });
dispatch(getBrandLayoutInfo({ patnrId: panelInfo?.patnrId }));
setIsInitialFocusOccurred(false);
@@ -733,9 +786,12 @@ const FeaturedBrandsPanel = ({ isOnTop, panelInfo, spotlightId }) => {
// effect: data fetching based on brandLayoutInfo and selectedPatnrId
useEffect(() => {
if (sortedBrandLayoutInfo && selectedPatnrId) {
console.log("[FeaturedBrandsPanel] Data Fetching Effect - selectedPatnrId:", selectedPatnrId);
console.log("[FeaturedBrandsPanel] sortedBrandLayoutInfo:", sortedBrandLayoutInfo);
Object.entries(DISPATCH_MAP) //
.forEach(([templateCode, action]) => {
if (hasTemplateCodeWithValue(sortedBrandLayoutInfo, templateCode)) {
console.log("[FeaturedBrandsPanel] Fetching data for template:", templateCode, "patnrId:", selectedPatnrId);
dispatch(action({ patnrId: selectedPatnrId }));
}
});

View File

@@ -49,10 +49,13 @@ const QuickMenuItem = ({
}, [handleStopScrolling, itemIndex]);
const handleClick = useCallback(() => {
console.log("[QuickMenuItem] Click - patnrId:", patnrId, "currentPatnrId:", selectedPatnrId ?? panelInfo?.patnrId);
if (patnrId === (selectedPatnrId ?? panelInfo?.patnrId)) {
console.log("[QuickMenuItem] Already selected, returning");
return;
}
console.log("[QuickMenuItem] Switching to patnrId:", patnrId);
const from = "menu";
const name = panel_names.FEATURED_BRANDS_PANEL;

View File

@@ -0,0 +1,86 @@
import React, { memo, useCallback, useState } from "react";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import SectionTitle from "../../../components/SectionTitle/SectionTitle";
import { $L } from "../../../utils/helperMethods";
import css from "./ShopByShow.module.less";
import ShopByShowList from "./ShopByShowList/ShopByShowList";
const STRING_CONF = {
SHOP_BY_SHOW: "SHOP BY SHOW",
};
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
"div"
);
const ShopByShow = ({
brandShopByShowContsList,
brandShopByShowContsInfo,
handleItemFocus,
order,
shelfOrder,
spotlightId,
selectedPatnrId,
shelfTitle,
}) => {
const [firstChk, setFirstChk] = useState(0);
const _handleItemFocus = useCallback(() => {
if (handleItemFocus) handleItemFocus(spotlightId, shelfOrder);
const c = Spotlight.getCurrent();
if (firstChk === 0) {
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
if (cAriaLabel) {
cAriaLabel = "shop-by-show, Heading1," + cAriaLabel;
c.setAttribute("aria-label", cAriaLabel);
}
}
setFirstChk(1);
} else if (firstChk === 1) {
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace(
"shop-by-show, Heading1,",
""
);
c.setAttribute("aria-label", newcAriaLabel);
}
}
} else {
return;
}
}, [handleItemFocus, firstChk]);
return (
<Container
className={css.container}
data-shelf-order={order}
data-wheel-point
spotlightId={spotlightId}
>
<SectionTitle
title={$L(STRING_CONF.SHOP_BY_SHOW)}
data-title="shop-by-show"
label="shop-by-show Heading 1"
/>
<ShopByShowList
brandShopByShowContsList={brandShopByShowContsList}
brandShopByShowContsInfo={brandShopByShowContsInfo}
handleItemFocus={_handleItemFocus}
selectedPatnrId={selectedPatnrId}
spotlightId={spotlightId}
shelfOrder={shelfOrder}
shelfTitle={shelfTitle}
/>
</Container>
);
};
export default memo(ShopByShow);

View File

@@ -0,0 +1,12 @@
@import "../../../style/CommonStyle.module.less";
@import "../../../style/utils.module.less";
.container {
width: 100%;
margin-bottom: 58px;
> h2 {
margin-bottom: 24px;
padding-left: 60px;
}
}

View File

@@ -0,0 +1,144 @@
import React, { memo, useCallback } from "react";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import TItemCardNew, { removeDotAndColon } from "../../../../../components/TItemCard/TItemCard.new";
import TVirtualGridList from "../../../../../components/TVirtualGridList/TVirtualGridList";
import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from "../../../../../utils/Config";
import { getTranslate3dValueByDirection } from "../../../../../utils/helperMethods";
import Spotlight from "@enact/spotlight";
import { pushPanel, updatePanel } from "../../../../../actions/panelActions";
import { useDispatch } from "react-redux";
import css from "./ShopByShowContents.module.less";
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: null },
"div"
);
const ShopByShowContents = memo(({
clctId,
clctNm,
brandProductInfos,
contentsIndex,
handleItemFocus,
selectedPatnrId,
spotlightId,
shelfOrder,
shelfTitle,
getScrollTo,
}) => {
const dispatch = useDispatch();
const handleClick = useCallback(
(patnrId, prdtId) => (e) => {
const tItemCard = e.currentTarget;
const lastFocusedTarget = Spotlight.getCurrent();
const lastFocusedTargetId = lastFocusedTarget?.getAttribute("data-spotlight-id");
const xContainer = tItemCard?.parentNode?.parentNode;
if (lastFocusedTargetId && xContainer) {
const section = "shop-by-show";
const x = getTranslate3dValueByDirection(xContainer);
dispatch(
updatePanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: {
lastFocusedTargetId,
patnrId,
section,
x,
},
})
);
}
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: { patnrId, prdtId },
})
);
},
[dispatch]
);
const handleFocus = useCallback(() => {
if (handleItemFocus) {
handleItemFocus();
}
}, [handleItemFocus]);
return (
<Container
className={css.container}
data-wheel-point
spotlightId={`${spotlightId}-${contentsIndex}`}
>
<h3 data-collection-subtitle={clctNm}>{clctNm}</h3>
{brandProductInfos && brandProductInfos.length > 0 && (
<TVirtualGridList
cbScrollTo={getScrollTo}
className={css.tVirtualGridList}
dataSize={brandProductInfos.length}
direction="horizontal"
itemHeight={438}
itemWidth={324}
spacing={18}
renderItem={({ index, ...rest }) => {
if (!brandProductInfos || !brandProductInfos[index]) {
return null;
}
const product = brandProductInfos[index];
const {
prdtImgUrl,
prdtOfferId,
patnrId = "21",
prdtNm,
prdtId,
prdtPrice,
patncNm,
brndNm,
lgCatNm,
euEnrgLblInfos,
} = product;
return (
<TItemCardNew
catNm={lgCatNm}
contextName={LOG_CONTEXT_NAME.FEATURED_BRANDS}
messageId={LOG_MESSAGE_ID.SHELF_CLICK}
patnerName={patncNm || "Peacock | Shop The Moment"}
brandName={brndNm}
shelfId={spotlightId}
shelfLocation={shelfOrder}
shelfTitle={shelfTitle}
imageAlt={prdtNm}
imageSource={prdtImgUrl}
onClick={handleClick(patnrId, prdtId)}
onFocus={handleFocus}
offerInfo={prdtOfferId}
priceInfo={prdtPrice}
productId={prdtId}
productName={prdtNm}
spotlightId={"shop-by-show-spotlightId-" + removeDotAndColon(prdtId)}
label={index * 1 + 1 + " of " + brandProductInfos.length}
lastLabel=" go to detail, button"
euEnrgLblInfos={euEnrgLblInfos}
{...rest}
/>
);
}}
/>
)}
</Container>
);
});
export default ShopByShowContents;

View File

@@ -0,0 +1,32 @@
@import "../../../../../style/CommonStyle.module.less";
@import "../../../../../style/utils.module.less";
.container {
padding-left: 60px;
margin-bottom: 12px;
> h3 {
position: relative;
.font(@fontFamily: @arialFontBold, @fontSize: 36px);
color: @COLOR_GRAY08;
margin-bottom: 22px;
}
> div:nth-child(2) {
.flex(@justifyCenter: flex-start);
.size(@w: 100%, @h: auto);
}
}
.container:last-child {
margin-bottom: 0;
}
.tVirtualGridList {
padding-right: 18px;
.size(@h: 438px);
> div:nth-child(3) {
right: -18px;
}
}

View File

@@ -0,0 +1,134 @@
import React, {
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { Job } from '@enact/core/util';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import {
getContainerId,
setContainerLastFocusedElement,
} from '@enact/spotlight/src/container';
import { getBrandShopByShow } from '../../../../actions/brandActions';
import useScrollTo from '../../../../hooks/useScrollTo';
import css from './ShopByShowList.module.less';
import ShopByShowNav from './ShopByShowNav/ShopByShowNav';
import ShopByShowContents from './ShopByShowContents/ShopByShowContents';
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
"div"
);
export default function ShopByShowList({
brandShopByShowContsList,
brandShopByShowContsInfo,
handleItemFocus,
selectedPatnrId,
spotlightId,
shelfTitle,
shelfOrder,
}) {
const { getScrollTo, scrollLeft } = useScrollTo();
const dispatch = useDispatch();
const panelInfo = useSelector((state) => state.panels.panels[0]?.panelInfo);
const scrollLeftJob = useRef(new Job((func) => func(), 0));
const [selectedClctId, setSelectedClctId] = useState(null);
const brandShopByShowClctInfos = brandShopByShowContsInfo?.brandShopByShowClctInfos || [];
const filteredCollections = selectedClctId
? brandShopByShowClctInfos.filter(({ clctId }) => clctId === selectedClctId)
: brandShopByShowClctInfos;
useEffect(() => {
if (panelInfo?.section !== "shop-by-show" || !panelInfo?.x) {
return;
}
const scrollLeftJobValue = scrollLeftJob.current;
const { x } = panelInfo;
scrollLeftJobValue.start(() => scrollLeft({ x }));
return () => scrollLeftJobValue.stop();
}, [panelInfo, scrollLeft]);
useEffect(() => {
const containerId = "shop-by-show-list-id";
const container = document.getElementById(containerId);
if (container) {
const childrenId = getContainerId(container?.children[0].children[0]);
if (childrenId) {
setContainerLastFocusedElement(null, [containerId, childrenId]);
}
}
scrollLeft();
}, [scrollLeft, selectedPatnrId]);
const handleTabClick = useCallback((contsId) => {
dispatch(getBrandShopByShow({ patnrId: selectedPatnrId, contsId }));
}, [selectedPatnrId, dispatch]);
const _handleItemFocus = useCallback(() => {
if (handleItemFocus) {
handleItemFocus();
}
}, [handleItemFocus]);
return (
<Container
className={css.container}
id={"shop-by-show-list-id"}
spotlightId={"shop-by-show-list-id"}
>
<div className={css.tabsContainer}>
{brandShopByShowContsList && brandShopByShowContsList.map((item) => (
<button
key={item.contsId}
className={`${css.tabButton} ${brandShopByShowContsInfo?.contsId === item.contsId ? css.active : ''}`}
onClick={() => handleTabClick(item.contsId)}
onMouseDown={(e) => e.preventDefault()}
>
{item.contsNm}
</button>
))}
</div>
<ShopByShowNav
brandShopByShowClctInfos={brandShopByShowClctInfos}
handleItemFocus={_handleItemFocus}
selectedClctId={selectedClctId}
setSelectedClctId={setSelectedClctId}
/>
{filteredCollections.map((collection, collIdx) => (
<ShopByShowContents
key={`${spotlightId}-${collIdx}`}
clctId={collection.clctId}
clctNm={collection.clctNm}
brandProductInfos={collection.brandProductInfos}
contentsIndex={collIdx}
handleItemFocus={_handleItemFocus}
selectedPatnrId={selectedPatnrId}
spotlightId={spotlightId}
shelfOrder={shelfOrder}
shelfTitle={shelfTitle}
getScrollTo={getScrollTo}
/>
))}
</Container>
);
}

View File

@@ -0,0 +1,59 @@
@import "../../../../style/CommonStyle.module.less";
@import "../../../../style/utils.module.less";
.container {
display: flex;
flex-direction: column;
position: relative;
width: 100%;
}
.tabsContainer {
display: flex;
gap: 12px;
padding: 0 60px 24px 60px;
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 12px;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 2px;
}
}
.tabButton {
flex-shrink: 0;
padding: 8px 16px;
background-color: transparent;
border: 1px solid #ccc;
border-radius: 4px;
color: #666;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
&:hover {
border-color: #000;
color: #000;
}
&.active {
background-color: #000;
border-color: #000;
color: #fff;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.2);
}
}

View File

@@ -0,0 +1,82 @@
import React, { memo, useCallback } from "react";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import TButton, { TYPES } from "../../../../../components/TButton/TButton";
import TScroller from "../../../../../components/TScroller/TScroller";
import useScrollTo from "../../../../../hooks/useScrollTo";
import css from "./ShopByShowNav.module.less";
const Container = SpotlightContainerDecorator(
{ leaveFor: { right: "" }, enterTo: "last-focused" },
"nav"
);
const STRING_CONF = {
ALL: "ALL",
};
export default memo(function ShopByShowNav({
brandShopByShowClctInfos,
handleItemFocus,
selectedClctId,
setSelectedClctId,
}) {
const { getScrollTo, scrollLeft } = useScrollTo();
const handleClick = useCallback(
(clctId, clctNm) => () => {
setSelectedClctId(clctId);
},
[setSelectedClctId]
);
const handleFocus = useCallback(() => {
if (handleItemFocus) {
handleItemFocus();
}
}, [handleItemFocus]);
const selectedText = !selectedClctId ? "Selected " : "";
const allLabelText = selectedText + "ALL 1 of " + (brandShopByShowClctInfos?.length + 1 || 1);
return (
<Container className={css.nav} id="shop-by-show-nav-id" spotlightId="shop-by-show-nav-id">
<TScroller cbScrollTo={getScrollTo} direction="horizontal" noScrollByWheel>
<ul>
<li>
<TButton
className={!selectedClctId && css.selected}
onClick={handleClick(null, null)}
onFocus={handleFocus}
selected={!selectedClctId}
type={TYPES.oneDepthCategory}
ariaLabel={allLabelText}
>
ALL
</TButton>
</li>
{brandShopByShowClctInfos &&
brandShopByShowClctInfos.map(({ clctId, clctNm }, index) => (
<li key={"shop-by-show-clct-" + index}>
<TButton
className={selectedClctId && selectedClctId === clctId && css.selected}
onClick={handleClick(clctId, clctNm)}
onFocus={handleFocus}
selected={selectedClctId && selectedClctId === clctId}
type={TYPES.oneDepthCategory}
ariaLabel={
selectedClctId && selectedClctId === clctId
? "Selected " + clctNm + " " + (index * 1 + 2) + " of " + (brandShopByShowClctInfos.length + 1)
: "" + clctNm + " " + (index * 1 + 2) + " of " + (brandShopByShowClctInfos.length + 1)
}
>
{clctNm}
</TButton>
</li>
))}
</ul>
</TScroller>
</Container>
);
});

View File

@@ -0,0 +1,55 @@
@import "../../../../../style/CommonStyle.module.less";
@import "../../../../../style/utils.module.less";
.nav {
position: relative;
.size(@w: 100%, @h: 162px);
margin-bottom: 30px;
padding-right: 1px;
z-index: 2;
> div:nth-child(1) {
.size(@w: inherit, @h: inherit);
}
&::before {
position: absolute;
top: 0;
left: 0;
.size(@w: 100%, @h: 144px);
background-color: #ddd;
content: "";
}
ul {
display: flex;
align-items: center;
height: inherit;
padding-left: 60px;
border-bottom: 18px solid transparent;
li {
flex: none;
margin-right: 12px;
> div {
position: relative;
&.selected {
&::before {
position: absolute;
bottom: -62px;
left: 50%;
transform: translateX(-50%);
.size(@w: 0, @h: 0);
border-top: 18px solid #ddd;
border-right: 18px solid transparent;
border-bottom: 18px solid transparent;
border-left: 18px solid transparent;
content: "";
}
}
}
}
}
}