@wface/pixel-cli
Advanced tools
| .entityCard { | ||
| max-width: 320px; | ||
| padding: 8px; | ||
| border-radius: var(--sizing-16); | ||
| background: var(--purple-600); | ||
| .entityHeader { | ||
| background: var(--background-accent-purple-subtlest-default); | ||
| border-radius: var(--sizing-12); | ||
| padding: 24px 20px; | ||
| .plan { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| margin-bottom: 44px; | ||
| .planTitle { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 4px; | ||
| .icon { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| } | ||
| .title { | ||
| color: var(--text-default); | ||
| } | ||
| } | ||
| .tag { | ||
| } | ||
| } | ||
| .entityPrice { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 8px; | ||
| margin-bottom: 20px; | ||
| .discount { | ||
| color: var(--text-subtlest); | ||
| } | ||
| } | ||
| .desc { | ||
| margin-bottom: 20px; | ||
| p { | ||
| color: var(--text-subtlest); | ||
| } | ||
| } | ||
| } | ||
| .includes { | ||
| padding: 24px 20px; | ||
| ul { | ||
| li { | ||
| display: flex; | ||
| gap: 12px; | ||
| color: var(--text-white); | ||
| font-size: 14px; | ||
| line-height: 20px; | ||
| margin-bottom: 12px; | ||
| .icon { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 24px; | ||
| height: 24px; | ||
| } | ||
| &:last-child { | ||
| margin-bottom: 0; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| export type JsonType = { | ||
| TAG?: string; | ||
| GORUNEN_ISIM: string | ||
| OZEL_ALAN?: string | ||
| ACIKLAMA: string | ||
| Title: string | ||
| Billing_Period: string | ||
| BUTTON_LABEL: string | ||
| FOOTER_TEXT: string | ||
| SORT_ORDER: string | ||
| OFFER_TYPE_NAME: string | ||
| OFFER_GEO_LOCATION_ID: string | ||
| OFFER_CUSTOMER_STATU: string | ||
| OFFER_IS_VIP: string | ||
| OFFER_IS_DISCOUNT_PACKAGE: string | ||
| OFFER_IS_FREE_PACKAGE: string | ||
| OFFER_APPLICATION_ID: string | ||
| OFFER_GATEWAY: string | ||
| UCRETSIZ_PAKET_MI: string | ||
| } | ||
| export type Entity = { | ||
| OfferSrelId: number | ||
| EntityJsonTypeList: string | ||
| OfferCd: string | ||
| OfferType: string | ||
| OfferDescription: string | ||
| PriceAmount: number | ||
| CurrencyTypeCd: string | ||
| } | ||
| export type Props = { | ||
| entity: Entity | ||
| } |
| import {useMemo} from "react"; | ||
| import styles from './EntityCard.module.scss'; | ||
| import type { JsonType, Props } from './EntityCard.types'; | ||
| import {PiBadge, PiBody, PiButton, PiHeading} from "@wface/pixel-ui"; | ||
| import {FiCheck, FiCreditCard} from "react-icons/fi"; | ||
| const EntityCard = (props: Props) => { | ||
| const { entity } = props; | ||
| const getParsedData = (data: string) => { | ||
| if (!data) return null; | ||
| return JSON.parse(data); | ||
| } | ||
| const parsedEntity = useMemo<JsonType>(() => getParsedData(entity.EntityJsonTypeList), [entity.EntityJsonTypeList]); | ||
| const includes = useMemo<string[]>(() => { | ||
| if (!parsedEntity) return []; | ||
| return parsedEntity.FOOTER_TEXT.split(';').map((item) => item.trim()); | ||
| }, [parsedEntity]); | ||
| return ( | ||
| <div className={styles.entityCard}> | ||
| <div className={styles.entityHeader}> | ||
| <div className={styles.plan}> | ||
| <div className={styles.planTitle}> | ||
| <span className={styles.icon}><FiCreditCard /></span> | ||
| <PiBody className={styles.title} as="span" size="lg" weight="medium">{parsedEntity.Title}</PiBody> | ||
| </div> | ||
| { | ||
| parsedEntity?.TAG && <PiBadge filled variant="orange" content={parsedEntity.TAG}></PiBadge> | ||
| } | ||
| </div> | ||
| <div className={styles.entityPrice}> | ||
| <PiHeading variant="h2" as="span" className={styles.price}> | ||
| {`${entity.PriceAmount} ${entity.CurrencyTypeCd}`} | ||
| </PiHeading> | ||
| { | ||
| parsedEntity?.OZEL_ALAN && ( | ||
| <PiBody size="md" as="span" weight="regular" className={styles.discount}> | ||
| / {`${parsedEntity?.OZEL_ALAN} ${entity.CurrencyTypeCd}`} | ||
| </PiBody> | ||
| ) | ||
| } | ||
| </div> | ||
| <div className={styles.desc}> | ||
| <PiBody size="sm" weight="regular">{parsedEntity.ACIKLAMA}</PiBody> | ||
| </div> | ||
| <div className={styles.button}> | ||
| <PiButton fullWidth>{parsedEntity.BUTTON_LABEL}</PiButton> | ||
| </div> | ||
| </div> | ||
| <div className={styles.includes}> | ||
| <ul> | ||
| {includes.map(i => ( | ||
| <li key={i} className={styles.entityItem}> | ||
| <span className={styles.icon}><FiCheck size={24} /></span> | ||
| {i} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| export default EntityCard; |
| export const entity = { | ||
| OfferSrelId: 118868, | ||
| EntityJsonTypeList: "{\"GORUNEN_ISIM\":\"1 week FREE trial for new customers\",\"ACIKLAMA\":\"From 11th August, recurring monthly plan after free trial period renews at $15.99. Card fees may apply.\",\"Title\":\"MONTHLY PLAN\",\"Billing_Period\":\"Billed Monthly\",\"BUTTON_LABEL\":\"Subscribe\",\"FOOTER_TEXT\":\"Watch LaLiga, Serie A, Bundesliga, Carabao Cup, EFL Championship, SPFL and more;ATP and WTA Tour, Davis Cup;Live, On-Demand, Highlights & Mini-Matches;Available on iOS, Android mobile and tablet, Samsung TVs, LG TVs, AndroidTV, Web browser, No lock-in contract\",\"SORT_ORDER\":\"999\",\"OFFER_TYPE_NAME\":\"Subscription\",\"OFFER_GEO_LOCATION_ID\":\"5110\",\"OFFER_CUSTOMER_STATU\":\"POTANSIYEL\",\"OFFER_IS_VIP\":\"false\",\"OFFER_IS_DISCOUNT_PACKAGE\":\"false\",\"OFFER_IS_FREE_PACKAGE\":\"true\",\"OFFER_APPLICATION_ID\":\"4\",\"OFFER_GATEWAY\":\"INGENICO\",\"UCRETSIZ_PAKET_MI\":\"true\"}", | ||
| OfferCd: "AIMAN1", | ||
| OfferType: "Subscription", | ||
| OfferDescription: "Australia Monthly Pass (w Free Trial)", | ||
| PriceAmount: 15.99, | ||
| CurrencyTypeCd: "AUD" | ||
| } |
| .exampleList { | ||
| .title { | ||
| display: flex; | ||
| align-items: center; | ||
| cursor: pointer; | ||
| .expand { | ||
| width: 24px; | ||
| height: 24px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| transition: .3s; | ||
| &.active { | ||
| transform: rotate(90deg); | ||
| } | ||
| } | ||
| } | ||
| } |
| export interface Content { | ||
| ContentId: number | ||
| ContentSpecId: number | ||
| CmsContentId: string | ||
| ContentName: string | ||
| LongName: string | ||
| DisplayName: string | ||
| Description: string | ||
| CreationDate: string | ||
| CreatedBy: any | ||
| ContentSpecCd: string | ||
| ContentSpecName: string | ||
| ChannelName: string | ||
| CategoryName: string | ||
| IsLiveToVod: boolean | ||
| LiveToVodStatus: string | ||
| Type: string | ||
| HasWarning: boolean | ||
| contentUsageClass: ContentUsageClass[] | ||
| } | ||
| export interface ContentUsageClass { | ||
| CmsContentId: string | ||
| ContentId: number | ||
| DisplayName: string | ||
| UsageSpecCodeCd: string | ||
| UsageSpecId: number | ||
| EventStartTime: string | ||
| EventEndTime: string | ||
| EventShowTime: string | ||
| UsageSpecStatusTypeCd: string | ||
| UsageSpecStatusTypeName: string | ||
| ContentCategoryCd: string | ||
| OfferType: string | ||
| Type: string | ||
| HasWarning: boolean | ||
| contentUsageItemModel: ContentUsageItemModel[] | ||
| BroadcastStatu: boolean | ||
| } | ||
| export interface ContentUsageItemModel { | ||
| HasWarning: boolean | ||
| Type: string | ||
| DisplayName: string | ||
| IbmsServiceSpecCd: string | ||
| State: number | ||
| Status: string | ||
| LastPublishDate: any | ||
| } |
| import {PiBadge, PiButton, PiSkeleton, PiSwitch, PiTable, PiCountryFlag, PiDropdown } from "@wface/pixel-ui"; | ||
| import {FiChevronRight, FiMoreVertical, FiEdit, FiArrowRightCircle, FiPlus, FiTrash} from "react-icons/fi"; | ||
| import {useEffect, useState} from "react"; | ||
| import {fetchData} from "./mockServer"; | ||
| import styles from './ExampleList.module.scss'; | ||
| import type { Content } from "./ExampleList.types.ts"; | ||
| const SubTable = () => { | ||
| return ( | ||
| <PiTable subTable> | ||
| <PiTable.TableHeader> | ||
| <PiTable.TableRow> | ||
| <PiTable.TableCell>Region</PiTable.TableCell> | ||
| <PiTable.TableCell>License Starts</PiTable.TableCell> | ||
| <PiTable.TableCell>License Ends</PiTable.TableCell> | ||
| <PiTable.TableCell>Broadcast</PiTable.TableCell> | ||
| <PiTable.TableCell>Status</PiTable.TableCell> | ||
| <PiTable.TableCell textAlign="right">Actions</PiTable.TableCell> | ||
| </PiTable.TableRow> | ||
| </PiTable.TableHeader> | ||
| <PiTable.TableBody> | ||
| <PiTable.TableRow> | ||
| <PiTable.TableCell> | ||
| <div style={{display: "flex", alignItems: "center", gap: '8px'}}> | ||
| {PiCountryFlag(221, '24px', '24px')} | ||
| Hong Kong | ||
| </div> | ||
| </PiTable.TableCell> | ||
| <PiTable.TableCell>2020-05-17 10:55</PiTable.TableCell> | ||
| <PiTable.TableCell>2020-05-17 10:55</PiTable.TableCell> | ||
| <PiTable.TableCell><PiSwitch checked onChange={() => null} label="On"/></PiTable.TableCell> | ||
| <PiTable.TableCell><PiBadge variant="success" content="Valid"/></PiTable.TableCell> | ||
| <PiTable.TableCell> | ||
| <div style={{display: 'flex', justifyContent: 'flex-end'}}> | ||
| <PiButton iconButton variant="secondary"><FiMoreVertical/></PiButton> | ||
| </div> | ||
| </PiTable.TableCell> | ||
| </PiTable.TableRow> | ||
| </PiTable.TableBody> | ||
| </PiTable> | ||
| ) | ||
| } | ||
| const TableRowItem = (props: { row: Content, activeRowId: number | null, handleSubTable: (value: number) => void }) => { | ||
| const {row, activeRowId, handleSubTable} = props; | ||
| const [dropdown, setDropdown] = useState<boolean>(false); | ||
| const dropdownMenuList = () => { | ||
| return [ | ||
| { | ||
| label: "Edit Content", | ||
| icon: <FiEdit />, | ||
| onClick: () => null, | ||
| }, | ||
| { | ||
| label: "Update Content Metadata", | ||
| icon: <FiArrowRightCircle />, | ||
| onClick: () => null, | ||
| }, | ||
| { | ||
| line: true | ||
| }, | ||
| { | ||
| title: 'MANAGE COUNTRIES', | ||
| }, | ||
| { | ||
| label: "Edit Countries", | ||
| icon: <FiPlus />, | ||
| onClick: () => null | ||
| }, | ||
| { | ||
| label: "Delete Content", | ||
| icon: <FiTrash />, | ||
| onClick: () => null, | ||
| }, | ||
| ]; | ||
| }; | ||
| return ( | ||
| <PiTable.TableRow subTable={<SubTable/>} showSubTable={row.ContentId === activeRowId}> | ||
| <PiTable.TableCell width={300}> | ||
| <div className={styles.title} onClick={() => handleSubTable(row.ContentId)}> | ||
| <span className={`${styles.expand} ${activeRowId === row?.ContentId ? styles.active : ""}`}> | ||
| <FiChevronRight size={16}/> | ||
| </span> | ||
| <span className={styles.title}> | ||
| {row.CmsContentId} | ||
| </span> | ||
| </div> | ||
| </PiTable.TableCell> | ||
| <PiTable.TableCell width={300}>{row.Description}</PiTable.TableCell> | ||
| <PiTable.TableCell width={200}>{row.ContentName}</PiTable.TableCell> | ||
| <PiTable.TableCell>{row.CategoryName}</PiTable.TableCell> | ||
| <PiTable.TableCell>{row.ChannelName}</PiTable.TableCell> | ||
| <PiTable.TableCell><PiBadge filled variant="success" content={row.LiveToVodStatus}/></PiTable.TableCell> | ||
| <PiTable.TableCell width={80} textAlign="right"> | ||
| <PiDropdown minWidth={280} menuList={dropdownMenuList()} show={dropdown} setShow={setDropdown}/> | ||
| </PiTable.TableCell> | ||
| </PiTable.TableRow> | ||
| ) | ||
| } | ||
| const ExampleList = () => { | ||
| const [activeRowId, setActiveRowId] = useState<number | null>(null); | ||
| const [data, setData] = useState<Content[]>([]); | ||
| const [loading, setLoading] = useState<boolean>(false) | ||
| const handleSubTable = (id: number) => { | ||
| if (id !== activeRowId) { | ||
| setActiveRowId(id) | ||
| } else { | ||
| setActiveRowId(null) | ||
| } | ||
| } | ||
| const getData = async () => { | ||
| setLoading(true); | ||
| try { | ||
| const res = await fetchData(); | ||
| setData(res); | ||
| } catch (e) { | ||
| console.log(e) | ||
| setData([]); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| useEffect(() => { | ||
| getData(); | ||
| }, []); | ||
| return ( | ||
| <div className={styles.exampleList}> | ||
| { | ||
| loading ? ( | ||
| <> | ||
| { | ||
| Array.from({length: 5}).map(() => ( | ||
| <PiSkeleton | ||
| height={60} | ||
| variant="rectangular" | ||
| animation="wave" | ||
| style={{borderRadius: 12, marginBottom: 5}} | ||
| /> | ||
| )) | ||
| } | ||
| </> | ||
| ) : ( | ||
| <PiTable> | ||
| <PiTable.TableHeader> | ||
| <PiTable.TableRow> | ||
| <PiTable.TableCell width={300}>CMS CONTENT ID</PiTable.TableCell> | ||
| <PiTable.TableCell width={300}>EPISODE TITLE</PiTable.TableCell> | ||
| <PiTable.TableCell width={200}>PLANNING TITLE</PiTable.TableCell> | ||
| <PiTable.TableCell>CATEGORY</PiTable.TableCell> | ||
| <PiTable.TableCell>CHANNEL</PiTable.TableCell> | ||
| <PiTable.TableCell> LIVE TO VOD STATUS</PiTable.TableCell> | ||
| <PiTable.TableCell width={80}> </PiTable.TableCell> | ||
| </PiTable.TableRow> | ||
| </PiTable.TableHeader> | ||
| <PiTable.TableBody> | ||
| { | ||
| data.map((row) => { | ||
| return ( | ||
| <TableRowItem key={row.ContentId} row={row} handleSubTable={handleSubTable} activeRowId={activeRowId} /> | ||
| ) | ||
| }) | ||
| } | ||
| </PiTable.TableBody> | ||
| </PiTable> | ||
| ) | ||
| } | ||
| </div> | ||
| ); | ||
| }; | ||
| export default ExampleList; |
| import type {Content} from "./ExampleList.types.ts"; | ||
| const data: Content[] = [ | ||
| { | ||
| ContentId: 362099, | ||
| ContentSpecId: 42, | ||
| CmsContentId: "164_MPV000149869", | ||
| ContentName: "LaLiga", | ||
| LongName: "LaLiga", | ||
| DisplayName: "LaLiga", | ||
| Description: "Real Sociedad vs Atletico Madrid", | ||
| CreationDate: "2025-12-22T06:31:59", | ||
| CreatedBy: null, | ||
| ContentSpecCd: "SINGLE_EVENT", | ||
| ContentSpecName: "SINGLE_EVENT", | ||
| ChannelName: "beIN SPORTS MAX 4", | ||
| CategoryName: "FOOTBALL", | ||
| IsLiveToVod: true, | ||
| LiveToVodStatus: "Waiting", | ||
| Type: "CONTENT", | ||
| HasWarning: false, | ||
| contentUsageClass: [ | ||
| { | ||
| CmsContentId: "164_MPV000149869", | ||
| ContentId: 362099, | ||
| DisplayName: "VOD_BS_NZ", | ||
| UsageSpecCodeCd: "VOD_BS_NZ", | ||
| UsageSpecId: 1287994, | ||
| EventStartTime: "2026-01-04T22:27:00", | ||
| EventEndTime: "2026-01-18T19:47:00", | ||
| EventShowTime: "1/4/2026 10:27 PM - 1/18/2026 7:47 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_MOBILE", | ||
| IbmsServiceSpecCd: "BS_NZ_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_WEB", | ||
| IbmsServiceSpecCd: "BS_NZ_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| ContentId: 362121, | ||
| ContentSpecId: 42, | ||
| CmsContentId: "166_MP000150154", | ||
| ContentName: "LaLiga 2", | ||
| LongName: "LaLiga 2", | ||
| DisplayName: "LaLiga 2", | ||
| Description: "Deportivo vs Cadiz", | ||
| CreationDate: "2025-12-22T06:32:03", | ||
| CreatedBy: null, | ||
| ContentSpecCd: "SINGLE_EVENT", | ||
| ContentSpecName: "SINGLE_EVENT", | ||
| ChannelName: "beIN SPORTS MAX 6", | ||
| CategoryName: "FOOTBALL", | ||
| IsLiveToVod: true, | ||
| LiveToVodStatus: "Waiting", | ||
| Type: "CONTENT", | ||
| HasWarning: false, | ||
| contentUsageClass: [ | ||
| { | ||
| CmsContentId: "166_MP000150154", | ||
| ContentId: 362121, | ||
| DisplayName: "VOD_BS_AU", | ||
| UsageSpecCodeCd: "VOD_BS_AU", | ||
| UsageSpecId: 1288024, | ||
| EventStartTime: "2026-01-04T22:22:00", | ||
| EventEndTime: "2026-01-18T19:52:00", | ||
| EventShowTime: "1/4/2026 10:22 PM - 1/18/2026 7:52 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_AU_MOBILE", | ||
| IbmsServiceSpecCd: "BS_AU_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_AU_WEB", | ||
| IbmsServiceSpecCd: "BS_AU_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| }, | ||
| { | ||
| CmsContentId: "166_MP000150154", | ||
| ContentId: 362121, | ||
| DisplayName: "VOD_BS_NZ", | ||
| UsageSpecCodeCd: "VOD_BS_NZ", | ||
| UsageSpecId: 1288023, | ||
| EventStartTime: "2026-01-04T22:22:00", | ||
| EventEndTime: "2026-01-18T19:52:00", | ||
| EventShowTime: "1/4/2026 10:22 PM - 1/18/2026 7:52 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_MOBILE", | ||
| IbmsServiceSpecCd: "BS_NZ_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_WEB", | ||
| IbmsServiceSpecCd: "BS_NZ_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| ContentId: 362084, | ||
| ContentSpecId: 42, | ||
| CmsContentId: "162_MPV000153422", | ||
| ContentName: "Serie A", | ||
| LongName: "Serie A", | ||
| DisplayName: "Serie A", | ||
| Description: "Inter vs Bologna", | ||
| CreationDate: "2025-12-22T06:31:38", | ||
| CreatedBy: null, | ||
| ContentSpecCd: "SINGLE_EVENT", | ||
| ContentSpecName: "SINGLE_EVENT", | ||
| ChannelName: "beIN SPORTS MAX 2", | ||
| CategoryName: "FOOTBALL", | ||
| IsLiveToVod: true, | ||
| LiveToVodStatus: "Waiting", | ||
| Type: "CONTENT", | ||
| HasWarning: false, | ||
| contentUsageClass: [ | ||
| { | ||
| CmsContentId: "162_MPV000153422", | ||
| ContentId: 362084, | ||
| DisplayName: "VOD_BS_NZ", | ||
| UsageSpecCodeCd: "VOD_BS_NZ", | ||
| UsageSpecId: 1287977, | ||
| EventStartTime: "2026-01-04T22:10:00", | ||
| EventEndTime: "2026-01-18T19:30:00", | ||
| EventShowTime: "1/4/2026 10:10 PM - 1/18/2026 7:30 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_WEB", | ||
| IbmsServiceSpecCd: "BS_NZ_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_MOBILE", | ||
| IbmsServiceSpecCd: "BS_NZ_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| ContentId: 362111, | ||
| ContentSpecId: 42, | ||
| CmsContentId: "167_MP000138387", | ||
| ContentName: "AFCON 2025", | ||
| LongName: "AFCON 2025", | ||
| DisplayName: "AFCON 2025", | ||
| Description: "Episode 40 - Rd of 16", | ||
| CreationDate: "2025-12-22T06:32:45", | ||
| CreatedBy: null, | ||
| ContentSpecCd: "SINGLE_EVENT", | ||
| ContentSpecName: "SINGLE_EVENT", | ||
| ChannelName: "beIN SPORTS MAX 7", | ||
| CategoryName: "FOOTBALL", | ||
| IsLiveToVod: true, | ||
| LiveToVodStatus: "Waiting", | ||
| Type: "CONTENT", | ||
| HasWarning: false, | ||
| contentUsageClass: [ | ||
| { | ||
| CmsContentId: "167_MP000138387", | ||
| ContentId: 362111, | ||
| DisplayName: "VOD_BS_MY", | ||
| UsageSpecCodeCd: "VOD_BS_MY", | ||
| UsageSpecId: 1288179, | ||
| EventStartTime: "2026-01-04T22:03:00", | ||
| EventEndTime: "2026-01-18T18:48:00", | ||
| EventShowTime: "1/4/2026 10:03 PM - 1/18/2026 6:48 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_MY_WEB", | ||
| IbmsServiceSpecCd: "BS_MY_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_MY_MOBILE", | ||
| IbmsServiceSpecCd: "BS_MY_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| }, | ||
| { | ||
| CmsContentId: "167_MP000138387", | ||
| ContentId: 362111, | ||
| DisplayName: "VOD_BS_SG", | ||
| UsageSpecCodeCd: "VOD_BS_SG", | ||
| UsageSpecId: 1288177, | ||
| EventStartTime: "2026-01-04T22:03:00", | ||
| EventEndTime: "2026-01-18T18:48:00", | ||
| EventShowTime: "1/4/2026 10:03 PM - 1/18/2026 6:48 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_SG_MOBILE", | ||
| IbmsServiceSpecCd: "BS_SG_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_SG_WEB", | ||
| IbmsServiceSpecCd: "BS_SG_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| }, | ||
| { | ||
| CmsContentId: "167_MP000138387", | ||
| ContentId: 362111, | ||
| DisplayName: "VOD_BS_PH", | ||
| UsageSpecCodeCd: "VOD_BS_PH", | ||
| UsageSpecId: 1288178, | ||
| EventStartTime: "2026-01-04T22:03:00", | ||
| EventEndTime: "2026-01-18T18:48:00", | ||
| EventShowTime: "1/4/2026 10:03 PM - 1/18/2026 6:48 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_PH_WEB", | ||
| IbmsServiceSpecCd: "BS_PH_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_PH_MOBILE", | ||
| IbmsServiceSpecCd: "BS_PH_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| }, | ||
| { | ||
| CmsContentId: "167_MP000138387", | ||
| ContentId: 362111, | ||
| DisplayName: "VOD_BS_NZ", | ||
| UsageSpecCodeCd: "VOD_BS_NZ", | ||
| UsageSpecId: 1288012, | ||
| EventStartTime: "2026-01-04T22:03:00", | ||
| EventEndTime: "2026-01-18T18:48:00", | ||
| EventShowTime: "1/4/2026 10:03 PM - 1/18/2026 6:48 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_MOBILE", | ||
| IbmsServiceSpecCd: "BS_NZ_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_NZ_WEB", | ||
| IbmsServiceSpecCd: "BS_NZ_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| }, | ||
| { | ||
| CmsContentId: "167_MP000138387", | ||
| ContentId: 362111, | ||
| DisplayName: "VOD_BS_HK", | ||
| UsageSpecCodeCd: "VOD_BS_HK", | ||
| UsageSpecId: 1288181, | ||
| EventStartTime: "2026-01-04T22:03:00", | ||
| EventEndTime: "2026-01-18T18:48:00", | ||
| EventShowTime: "1/4/2026 10:03 PM - 1/18/2026 6:48 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_HK_MOBILE", | ||
| IbmsServiceSpecCd: "BS_HK_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_HK_WEB", | ||
| IbmsServiceSpecCd: "BS_HK_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| }, | ||
| { | ||
| CmsContentId: "167_MP000138387", | ||
| ContentId: 362111, | ||
| DisplayName: "VOD_BS_TH", | ||
| UsageSpecCodeCd: "VOD_BS_TH", | ||
| UsageSpecId: 1288176, | ||
| EventStartTime: "2026-01-04T22:03:00", | ||
| EventEndTime: "2026-01-18T18:48:00", | ||
| EventShowTime: "1/4/2026 10:03 PM - 1/18/2026 6:48 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_TH_WEB", | ||
| IbmsServiceSpecCd: "BS_TH_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_TH_MOBILE", | ||
| IbmsServiceSpecCd: "BS_TH_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| }, | ||
| { | ||
| CmsContentId: "167_MP000138387", | ||
| ContentId: 362111, | ||
| DisplayName: "VOD_BS_ID", | ||
| UsageSpecCodeCd: "VOD_BS_ID", | ||
| UsageSpecId: 1288180, | ||
| EventStartTime: "2026-01-04T22:03:00", | ||
| EventEndTime: "2026-01-18T18:48:00", | ||
| EventShowTime: "1/4/2026 10:03 PM - 1/18/2026 6:48 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_ID_MOBILE", | ||
| IbmsServiceSpecCd: "BS_ID_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_ID_WEB", | ||
| IbmsServiceSpecCd: "BS_ID_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| ContentId: 361588, | ||
| ContentSpecId: 42, | ||
| CmsContentId: "29_MP000149869", | ||
| ContentName: "LaLiga", | ||
| LongName: "LaLiga", | ||
| DisplayName: "LaLiga", | ||
| Description: "Real Sociedad vs Atletico Madrid", | ||
| CreationDate: "2025-12-21T00:22:21", | ||
| CreatedBy: null, | ||
| ContentSpecCd: "SINGLE_EVENT", | ||
| ContentSpecName: "SINGLE_EVENT", | ||
| ChannelName: "beIN Sports 1", | ||
| CategoryName: "FOOTBALL", | ||
| IsLiveToVod: true, | ||
| LiveToVodStatus: "Waiting", | ||
| Type: "CONTENT", | ||
| HasWarning: false, | ||
| contentUsageClass: [ | ||
| { | ||
| CmsContentId: "29_MP000149869", | ||
| ContentId: 361588, | ||
| DisplayName: "VOD_BS_SG", | ||
| UsageSpecCodeCd: "VOD_BS_SG", | ||
| UsageSpecId: 1287091, | ||
| EventStartTime: "2026-01-04T22:00:00", | ||
| EventEndTime: "2026-01-18T19:55:00", | ||
| EventShowTime: "1/4/2026 10:00 PM - 1/18/2026 7:55 PM", | ||
| UsageSpecStatusTypeCd: "GECERLI", | ||
| UsageSpecStatusTypeName: "Valid", | ||
| ContentCategoryCd: "FOOTBALL", | ||
| OfferType: "VOD", | ||
| Type: "USAGE_SPEC", | ||
| HasWarning: false, | ||
| contentUsageItemModel: [ | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_SG_WEB", | ||
| IbmsServiceSpecCd: "BS_SG_WEB", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| }, | ||
| { | ||
| HasWarning: false, | ||
| Type: "USAGE_SPEC_ITEM", | ||
| DisplayName: "BS_SG_MOBILE", | ||
| IbmsServiceSpecCd: "BS_SG_MOBILE", | ||
| State: 2, | ||
| Status: "Puplished", | ||
| LastPublishDate: null | ||
| } | ||
| ], | ||
| BroadcastStatu: true | ||
| } | ||
| ] | ||
| }, | ||
| ]; | ||
| export const fetchData = (): Promise<Content[]> => { | ||
| return new Promise(resolve => { | ||
| setTimeout(() => { | ||
| resolve(data); | ||
| }, 300) | ||
| }) | ||
| } |
| import styles from '../EpgCalendar.module.scss' | ||
| import type {EpgData} from "../../../programmingGuide.types.ts"; | ||
| type Props = { | ||
| data: EpgData | ||
| } | ||
| const Channel = (props: Props) => { | ||
| const { data } = props; | ||
| return ( | ||
| <div className={styles.channel}> | ||
| <span className={styles.code}>{ data.CmsChannelId }</span> | ||
| <span className={styles.img}> | ||
| <img src={data.ChannelImagePath} alt={data.DisplayName} /> | ||
| </span> | ||
| </div> | ||
| ); | ||
| }; | ||
| export default Channel; |
| .epgCard { | ||
| border-right: 1px solid var(--border-default); | ||
| height: 100%; | ||
| min-height: 90px; | ||
| padding: 6px; | ||
| background: var(--elevation-surface); | ||
| .header { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| margin-bottom: 8px; | ||
| .tags { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 4px; | ||
| .tag { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 0 2px; | ||
| border-radius: 4px; | ||
| .icon { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 16px; | ||
| height: 16px; | ||
| } | ||
| &.liveToVod { | ||
| background: var(--yellow-100); | ||
| color: var(--yellow-700); | ||
| } | ||
| &.videoTag { | ||
| background: var(--purple-100); | ||
| color: var(--purple-700);; | ||
| } | ||
| &.live { | ||
| background: var(--red-100); | ||
| .icon { | ||
| span { | ||
| display: block; | ||
| width: 8px; | ||
| height: 8px; | ||
| border-radius: 50%; | ||
| background: var(--red-700); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| .actions { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: flex-end; | ||
| gap: 4px; | ||
| } | ||
| } | ||
| .info { | ||
| .time, .name { | ||
| color: var(--text-default); | ||
| } | ||
| .program { | ||
| color: var(--text-subtlest); | ||
| margin-top: 8px; | ||
| } | ||
| } | ||
| } |
| import styles from './EpgCard.module.scss' | ||
| import type {Epg} from "../../../../programmingGuide.types.ts"; | ||
| import {PiBody} from "@wface/pixel-ui"; | ||
| import {FiEye, FiHeart, FiLock, FiMoreVertical} from "react-icons/fi"; | ||
| type Props = { | ||
| epg: Epg | ||
| } | ||
| const getTime = (timeValue: string, ampm: boolean = false) => { | ||
| const time = new Date(timeValue); | ||
| const hours = time.getUTCHours().toString().padStart(2, "0"); | ||
| const minutes = time.getUTCMinutes().toString().padStart(2, "0"); | ||
| let ampmValue: string = ''; | ||
| if (ampm) { | ||
| ampmValue = Number(hours) >= 12 ? "pm" : "am"; | ||
| } | ||
| return `${hours}:${minutes} ${ampmValue}`; | ||
| }; | ||
| const EpgCard = (props: Props) => { | ||
| const { epg } = props; | ||
| return ( | ||
| <div className={styles.epgCard}> | ||
| <div className={styles.header}> | ||
| <div className={styles.tags}> | ||
| {!epg?.IsLive ? <span className={`${styles.tag} ${styles.live}`}><span className={styles.icon}><span/></span></span> : null} | ||
| {!epg?.LiveToVod ? <span className={`${styles.tag} ${styles.liveToVod}`}><span className={styles.icon}><FiEye size={12} /></span> </span> : null} | ||
| {epg?.VideoTag ? <span className={`${styles.tag} ${styles.videoTag}`}><span className={styles.icon}><FiHeart size={12} /></span> </span> : null} | ||
| </div> | ||
| <div className={styles.actions}> | ||
| {!epg?.EpgLock ? <span className={styles.epgLock}><FiLock size={12}/></span> : null} | ||
| <span className={styles.dropdown}> | ||
| <FiMoreVertical size={12} /> | ||
| </span> | ||
| </div> | ||
| </div> | ||
| <div className={styles.info}> | ||
| <PiBody className={styles.time} size="sm" weight="medium">{`${getTime(epg.StartTimeUtc)} - ${getTime(epg.EndTimeUtc)}`}</PiBody> | ||
| <PiBody className={styles.name} size="sm" weight="regular" truncate>{epg.EventName}</PiBody> | ||
| <PiBody className={styles.program} size="sm" weight="light" truncate>{epg.ProgramName}</PiBody> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| export default EpgCard; |
| import styles from '../EpgCalendar.module.scss' | ||
| import type {Epg} from "../../../programmingGuide.types.ts"; | ||
| import {useState} from "react"; | ||
| import {Swiper, SwiperSlide} from "swiper/react"; | ||
| import "swiper/swiper.css"; | ||
| import EpgCard from "./epg-card"; | ||
| import {FiChevronLeft, FiChevronRight} from "react-icons/fi"; | ||
| import {useTheme} from "@wface/pixel-ui"; | ||
| type Props = { | ||
| data: Epg[] | ||
| } | ||
| const EpgList = (props: Props) => { | ||
| const { data } = props; | ||
| const [swiper, setSwiper] = useState<any>(null); | ||
| const { expanded } = useTheme(); | ||
| const handleNext = () => { | ||
| swiper.slideNext(); | ||
| }; | ||
| const handlePrev = () => { | ||
| swiper.slidePrev(); | ||
| }; | ||
| return ( | ||
| <div className={`${styles.epgList} ${!expanded ? styles.larger : ''}`}> | ||
| <span className={styles.prev} onClick={handlePrev}> | ||
| <FiChevronLeft /> | ||
| </span> | ||
| <Swiper | ||
| slidesPerView="auto" | ||
| onSwiper={setSwiper} | ||
| className={styles.swiper} | ||
| > | ||
| { | ||
| data?.map((epg) => { | ||
| return ( | ||
| <SwiperSlide className={`${styles.sliderItem}`} key={epg.EpgId}> | ||
| <EpgCard epg={epg} /> | ||
| </SwiperSlide> | ||
| ); | ||
| }) | ||
| } | ||
| </Swiper> | ||
| <span className={styles.next} onClick={handleNext}> | ||
| <FiChevronRight /> | ||
| </span> | ||
| </div> | ||
| ); | ||
| }; | ||
| export default EpgList; |
| .epgCalendar { | ||
| .row { | ||
| display: flex; | ||
| align-items: stretch; | ||
| border-bottom: 1px solid var(--border-default); | ||
| } | ||
| .channel { | ||
| display: flex; | ||
| align-items: center; | ||
| padding: 20px 32px; | ||
| width: 250px; | ||
| border-right: 1px solid var(--border-default); | ||
| background: var(--elevation-surface-hovered); | ||
| .code { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 0 4px; | ||
| border-radius: 4px; | ||
| border: 1px solid var(--background-accent-gray-default); | ||
| height: 16px; | ||
| font-size: 12px; | ||
| line-height: 16px; | ||
| color: var(--text-subtle); | ||
| font-weight: 500; | ||
| } | ||
| .img { | ||
| padding-left: 16px; | ||
| margin-left: 16px; | ||
| position: relative; | ||
| &:after { | ||
| content: ""; | ||
| position: absolute; | ||
| top: 50%; | ||
| left: 0; | ||
| transform: translateY(-50%); | ||
| width: 1px; | ||
| height: 32px; | ||
| background: var(--border-default); | ||
| } | ||
| img { | ||
| max-height: 40px; | ||
| } | ||
| } | ||
| } | ||
| .epgList { | ||
| flex: 1; | ||
| max-width: calc(100vw - 490px); | ||
| position: relative; | ||
| .swiper { | ||
| .sliderItem { | ||
| width: 168px; | ||
| } | ||
| } | ||
| .prev, .next { | ||
| position: absolute; | ||
| top: 50%; | ||
| transform: translateY(-50%); | ||
| width: 32px; | ||
| height: 32px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| background: var(--background-accent-gray-subtlest-default); | ||
| border-radius: 4px; | ||
| cursor: pointer; | ||
| z-index: 10; | ||
| opacity: 0; | ||
| visibility: hidden; | ||
| pointer-events: none; | ||
| transition: .3s; | ||
| &:hover { | ||
| background: var(--background-accent-purple-subtlest-default); | ||
| } | ||
| } | ||
| .prev { | ||
| left: -16px; | ||
| } | ||
| .next { | ||
| right: 16px; | ||
| } | ||
| &:hover { | ||
| .prev, .next { | ||
| opacity: 1; | ||
| visibility: visible; | ||
| pointer-events: all; | ||
| } | ||
| } | ||
| &.larger { | ||
| max-width: calc(100vw - 302px); | ||
| } | ||
| } | ||
| } |
| import styles from './EpgCalendar.module.scss' | ||
| import {useProgrammingGuide} from "../../programmingGuideProvider.tsx"; | ||
| import Channel from "./channel"; | ||
| import EpgList from "./epg-list"; | ||
| const EpgCalendar = () => { | ||
| const { data } = useProgrammingGuide(); | ||
| return ( | ||
| <div className={styles.epgCalendar}> | ||
| { | ||
| data.map((item, index: number) => ( | ||
| <div className={styles.row} key={index}> | ||
| <Channel data={item}/> | ||
| <EpgList data={item.Epgs}/> | ||
| </div> | ||
| )) | ||
| } | ||
| </div> | ||
| ); | ||
| }; | ||
| export default EpgCalendar; |
| import {useMemo} from 'react'; | ||
| import styles from "../EpgHeader.module.scss"; | ||
| import {PiBody, PiHeading} from "@wface/pixel-ui"; | ||
| import {getImageForCountry} from "../../../../../../helpers"; | ||
| import {useProgrammingGuide} from "../../../programmingGuideProvider.tsx"; | ||
| import type {Country} from "../../../programmingGuide.types.ts"; | ||
| const EpgChannel = () => { | ||
| const { filter, countries } = useProgrammingGuide(); | ||
| const country = useMemo<Country | undefined>(() => countries.find(i => i.ApplicationId === filter.applicationId), [countries, filter]); | ||
| return ( | ||
| <div className={styles.epgChannel}> | ||
| { | ||
| country && | ||
| ( | ||
| <> | ||
| <div className={styles.country}> | ||
| <PiHeading variant="h4">{country?.ApplicationName}</PiHeading> | ||
| <span className={styles.flag}> | ||
| {getImageForCountry(Number(filter.applicationId), '24px', '24px')} | ||
| </span> | ||
| </div> | ||
| <div className={styles.applicationInfo}> | ||
| <PiBody size="sm" weight="regular">Timezone: <PiBody as="span" weight="light" size="sm">UTC +10</PiBody></PiBody> | ||
| <PiBody size="sm" weight="regular">Both linear & overflow.</PiBody> | ||
| </div> | ||
| </> | ||
| ) | ||
| } | ||
| </div> | ||
| ); | ||
| }; | ||
| export default EpgChannel; |
| import styles from '../EpgHeader.module.scss' | ||
| import {PiBody, PiHeading, useTheme} from "@wface/pixel-ui"; | ||
| import { Swiper, SwiperSlide } from "swiper/react"; | ||
| import { useState} from "react"; | ||
| import "swiper/swiper.css"; | ||
| import {FiChevronLeft, FiChevronRight} from "react-icons/fi"; | ||
| const generateTimeSlots = (start: number, end: number) => { | ||
| const times: { isActive: boolean, value: string }[] = []; | ||
| const currentHour = new Date().getHours(); | ||
| for (let hour = start; hour <= end; hour++) { | ||
| const formattedHour = hour.toString().padStart(2, "0"); | ||
| if (hour === currentHour) { | ||
| times.push({ isActive: true, value: `${formattedHour}:00` }); | ||
| } else { | ||
| times.push({ isActive: false, value: `${formattedHour}:00` }); | ||
| } | ||
| } | ||
| return times; | ||
| }; | ||
| const EpgDate = () => { | ||
| const times = generateTimeSlots(0, 23); | ||
| const { expanded } = useTheme(); | ||
| const [swiper, setSwiper] = useState<any>(null); | ||
| const handleNext = () => { | ||
| swiper.slideNext(); | ||
| }; | ||
| const handlePrev = () => { | ||
| swiper.slidePrev(); | ||
| }; | ||
| return ( | ||
| <div className={`${styles.epgDate} ${!expanded ? styles.larger : ''}`}> | ||
| <div className={styles.date}> | ||
| <PiBody size="md" weight="light">TODAY</PiBody> | ||
| <PiHeading variant="h4">20 November 2025</PiHeading> | ||
| </div> | ||
| <div className={styles.hours}> | ||
| <span className={styles.prev} onClick={handlePrev}> | ||
| <FiChevronLeft /> | ||
| </span> | ||
| <div className={styles.hourSlider}> | ||
| <Swiper | ||
| slidesPerView={14} | ||
| onSwiper={setSwiper} | ||
| className={styles.swiper} | ||
| > | ||
| { | ||
| times?.map((time) => { | ||
| return ( | ||
| <SwiperSlide className={`${styles.sliderItem}`} key={time.value}> | ||
| <div className={`${styles.hour} ${time.isActive ? styles.active : ''}`}> | ||
| <PiBody size="sm" weight="light" as="span">{time.value}</PiBody> | ||
| </div> | ||
| </SwiperSlide> | ||
| ); | ||
| }) | ||
| } | ||
| </Swiper> | ||
| </div> | ||
| <span className={styles.next} onClick={handleNext}> | ||
| <FiChevronRight /> | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| export default EpgDate; |
| .epgHeader { | ||
| display: flex; | ||
| align-items: stretch; | ||
| border-bottom: 1px solid var(--border-default); | ||
| } | ||
| .epgChannel { | ||
| width: 250px; | ||
| display: flex; | ||
| flex-direction: column; | ||
| justify-content: space-between; | ||
| padding: 22px 32px; | ||
| border-right: 1px solid var(--border-default); | ||
| .country { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 20px; | ||
| margin-bottom: 32px; | ||
| .flag { | ||
| padding-left: 20px; | ||
| position: relative; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| &:before { | ||
| content: ""; | ||
| position: absolute; | ||
| top: 50%; | ||
| transform: translateY(-50%); | ||
| left: 0; | ||
| width: 1px; | ||
| height: 32px; | ||
| background: var(--border-default); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| .epgDate { | ||
| width: 100%; | ||
| max-width: calc(100vw - 490px); | ||
| .date { | ||
| padding: 16px; | ||
| border-bottom: 1px solid var(--border-default); | ||
| } | ||
| .hours { | ||
| display: flex; | ||
| align-items: center; | ||
| padding: 12px; | ||
| width: 100%; | ||
| .hourSlider { | ||
| width: calc(100% - 64px); | ||
| .swiper { | ||
| width: 100%; | ||
| .sliderItem { | ||
| .hour { | ||
| cursor: pointer; | ||
| text-align: center; | ||
| height: 28px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| span { | ||
| color: var(--text-subtlest) !important; | ||
| } | ||
| &.active { | ||
| background: var(--elevation-surface-overlay); | ||
| border-radius: 4px; | ||
| border: 1px solid var(--border-default); | ||
| span { | ||
| font-weight: 400 !important; | ||
| color: var(--text-default) !important; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| .prev, .next { | ||
| width: 32px; | ||
| height: 32px; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| background: var(--background-accent-gray-subtlest-default); | ||
| color: var(--icon-default); | ||
| cursor: pointer; | ||
| } | ||
| } | ||
| &.larger { | ||
| max-width: calc(100vw - 302px); | ||
| } | ||
| } |
| import styles from './EpgHeader.module.scss' | ||
| import EpgChannel from "./epg-channel"; | ||
| import EpgDate from "./epg-date"; | ||
| const EpgHeader = () => { | ||
| return ( | ||
| <div className={styles.epgHeader}> | ||
| <EpgChannel /> | ||
| <EpgDate /> | ||
| </div> | ||
| ); | ||
| }; | ||
| export default EpgHeader; |
| const FilterModal = () => { | ||
| return ( | ||
| <div> | ||
| FilterModal | ||
| </div> | ||
| ); | ||
| }; | ||
| export default FilterModal; |
| .filter { | ||
| .filterWrapper { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 10px; | ||
| .select { | ||
| min-width: 200px; | ||
| } | ||
| } | ||
| } |
| import styles from './Filter.module.scss' | ||
| import {useProgrammingGuide} from "../../programmingGuideProvider.tsx"; | ||
| import {PiSelect, PiSkeleton} from "@wface/pixel-ui"; | ||
| import {useMemo} from "react"; | ||
| type Option = { | ||
| value: string | number | ||
| label: string | ||
| } | ||
| const Filter = () => { | ||
| const { filter, setFilter, countries, countryLoading } = useProgrammingGuide(); | ||
| const countryOptions = useMemo<Option[]>(() => { | ||
| return countries.map(i => ({ | ||
| label: i.ApplicationName, | ||
| value: i.ApplicationId, | ||
| })); | ||
| }, [countries]); | ||
| return ( | ||
| <div className={styles.filter}> | ||
| <div className={styles.filterWrapper}> | ||
| { | ||
| countryLoading ? <PiSkeleton height={38} width={200} sx={{ transform: 'scale(1)' }} /> : ( | ||
| <div className={styles.select}> | ||
| <PiSelect | ||
| options={countryOptions} | ||
| value={countryOptions?.find(i => i.value === filter.applicationId)} | ||
| onChange={(option: Option) => setFilter({...filter, applicationId: option.value})} | ||
| placeholder="Filter Country" | ||
| /> | ||
| </div> | ||
| ) | ||
| } | ||
| { | ||
| countryLoading ? <PiSkeleton height={38} width={200} sx={{ transform: 'scale(1)' }} /> : ( | ||
| <div className={styles.select}> | ||
| <PiSelect | ||
| options={[]} | ||
| placeholder="Filter Channels" | ||
| /> | ||
| </div> | ||
| ) | ||
| } | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| export default Filter; |
| import {PiButton, PiModal} from "@wface/pixel-ui"; | ||
| import DashboardLayout from "../../layouts/dashboard-layout"; | ||
| import Filter from './components/filter' | ||
| import {FiCalendar} from "react-icons/fi"; | ||
| import EpgHeader from "./components/epg-header"; | ||
| import EpgCalendar from "./components/epg-calendar"; | ||
| import styles from './ProgrammingGuide.module.scss'; | ||
| import {ProgrammingGuideProvider} from "./programmingGuideProvider.tsx"; | ||
| import React from "react"; | ||
| import FilterModal from "./components/filter-modal"; | ||
| const ProgrammingGuide = () => { | ||
| const [filterOpen, setFilterOpen] = React.useState(false); | ||
| const topbar = { | ||
| pageTitle: "Programming Guide", | ||
| breadcrumb: [], | ||
| primary: | ||
| <PiButton variant="secondary" prefixIcon={<FiCalendar />} onClick={() => setFilterOpen(true)}> | ||
| Go To | ||
| </PiButton>, | ||
| secondary: <PiButton variant="primary">Today</PiButton> | ||
| } | ||
| return ( | ||
| <ProgrammingGuideProvider> | ||
| <DashboardLayout topbar={topbar} filterComponent={<Filter />} mainCn={styles.main}> | ||
| <EpgHeader /> | ||
| <EpgCalendar /> | ||
| </DashboardLayout> | ||
| <PiModal | ||
| open={filterOpen} | ||
| onClose={() => setFilterOpen(false)} | ||
| modalOptions={{ | ||
| title: 'Filter Options', | ||
| subtitle: 'Adjust your filter settings', | ||
| size: 'md', | ||
| icon: <FiCalendar size={24} /> | ||
| }} | ||
| > | ||
| <FilterModal /> | ||
| </PiModal> | ||
| </ProgrammingGuideProvider> | ||
| ); | ||
| }; | ||
| export default ProgrammingGuide; |
Sorry, the diff of this file is too big to display
| .main { | ||
| padding: 0 !important; | ||
| } |
| export interface Country { | ||
| ApplicationId: number | string, | ||
| ApplicationName: string | ||
| } | ||
| export interface Filter { | ||
| applicationId: string | number | null | ||
| channelContentIds: number[] | ||
| rangeStartDate: string | ||
| rangeEndDate: string, | ||
| epgDate: string | ||
| } | ||
| export interface EpgData { | ||
| ContentId: number | ||
| CmsChannelId: string | ||
| Name: string | ||
| DisplayName: string | ||
| IsOverflow: boolean | ||
| DisplayChannelOrder: number | ||
| ChannelImagePath: string | ||
| Epgs: Epg[] | ||
| } | ||
| export interface Epg { | ||
| StartTimeUtc: string | ||
| EndTimeUtc: string | ||
| ProgramName: string | ||
| EventName: string | ||
| EpgId: string | ||
| EpgElementKey: string | ||
| IsLive: boolean | ||
| LiveToVod: boolean | ||
| EpgLock: boolean | ||
| HasPoster: boolean | ||
| ChannelId: string | ||
| DocumentId: string | ||
| Duration: number | ||
| OptaId?: number | null | ||
| EventId?: number | null | ||
| VideoTag?: string | null | ||
| LeagueId?: number | null | ||
| LeagueName?: string | null | ||
| HomeTeamId?: number | null | ||
| HomeTeamName?: string | null | ||
| VisitorTeamId?: number | null | ||
| VisitorTeamName?: string | null | ||
| PosterPath: any | ||
| EpgMetadataId: number | ||
| EpgKey: number | ||
| AvailableApplications: string[] | ||
| ApplicationIds: number[] | ||
| } | ||
| import React, {createContext, useContext, useEffect, useState} from "react"; | ||
| import type {Country, EpgData, Filter} from "./programmingGuide.types.ts"; | ||
| import {fetchCountries, fetchEpgData} from "./mockServer.ts"; | ||
| interface Props { | ||
| children: React.ReactNode | ||
| } | ||
| interface ProgrammingGuideContextValue { | ||
| loading: boolean | ||
| filter: Filter, | ||
| setFilter: (value: Filter) => void | ||
| data: EpgData[], | ||
| countryLoading: boolean, | ||
| countries: Country[], | ||
| refresh: () => void; | ||
| } | ||
| const ProgrammingGuideContext = createContext<ProgrammingGuideContextValue | undefined>(undefined); | ||
| export const ProgrammingGuideProvider = (props: Props) => { | ||
| const { children } = props; | ||
| const [loading, setLoading] = useState<boolean>(false); | ||
| const [data, setData] = useState([]); | ||
| const [filter, setFilter] = useState<Filter>({ | ||
| applicationId: null, | ||
| rangeStartDate: "", | ||
| rangeEndDate: "", | ||
| channelContentIds: [], | ||
| epgDate: "" | ||
| }); | ||
| const [countryLoading, setCountryLoading] = useState<boolean>(false); | ||
| const [countries, setCountries] = useState<Country[]>([]); | ||
| const getCountries = async () => { | ||
| setCountryLoading(true); | ||
| try { | ||
| const res = await fetchCountries() as any; | ||
| setCountries(res); | ||
| } | ||
| catch (error) { | ||
| console.log(error) | ||
| setCountries([]) | ||
| } | ||
| finally { | ||
| setCountryLoading(false) | ||
| } | ||
| } | ||
| useEffect(() => { | ||
| getCountries(); | ||
| }, []); | ||
| const fetchData = async () => { | ||
| setLoading(true); | ||
| try { | ||
| const res = await fetchEpgData(filter) as any; | ||
| setData(res) | ||
| setLoading(false); | ||
| } | ||
| catch (error) { | ||
| console.log(error) | ||
| setData([]); | ||
| } | ||
| finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| useEffect(() => { | ||
| if (countries.length) setFilter({...filter, applicationId: countries[0].ApplicationId}); | ||
| }, [countries]); | ||
| useEffect(() => { | ||
| fetchData(); | ||
| }, [filter]); | ||
| const value = { | ||
| loading, | ||
| filter, | ||
| setFilter, | ||
| data, | ||
| countryLoading, | ||
| countries, | ||
| refresh: fetchData | ||
| }; | ||
| return ( | ||
| <ProgrammingGuideContext.Provider value={value}> | ||
| { children } | ||
| </ProgrammingGuideContext.Provider> | ||
| ) | ||
| } | ||
| /* eslint-disable react-refresh/only-export-components */ | ||
| export const useProgrammingGuide = (): ProgrammingGuideContextValue => { | ||
| const ctx = useContext(ProgrammingGuideContext); | ||
| if (!ctx) { | ||
| throw new Error("useProgrammingGuide must be used within ProgrammingGuideProvider"); | ||
| } | ||
| return ctx; | ||
| } |
+5
-5
@@ -102,3 +102,3 @@ #!/usr/bin/env node | ||
| function resolveTemplatePath(category, name) { | ||
| if (category === 'blocks' || category === 'layouts') { | ||
| if (category === 'blocks' || category === 'layouts' || category === 'views') { | ||
| return path.join(SRC_TEMPLATES, 'components', category, name) | ||
@@ -144,3 +144,3 @@ } | ||
| let depDir = dir | ||
| if (depCat === 'blocks' || depCat === 'layouts') depDir = depCat | ||
| if (depCat === 'blocks' || depCat === 'layouts' || depCat === 'views') depDir = depCat | ||
@@ -234,3 +234,3 @@ await addTemplateRecursive({ | ||
| const program = new Command() | ||
| program.name('pixel-ui').description('Pixel UI CLI').version('0.2.0') | ||
| program.name('pixel-ui').description('Pixel UI CLI').version('0.3.0') | ||
@@ -260,3 +260,3 @@ program | ||
| .command('add') | ||
| .argument('<category>', 'blocks | layouts | wface | components') | ||
| .argument('<category>', 'blocks | layouts | views') | ||
| .argument('<name>', 'template name') | ||
@@ -277,5 +277,5 @@ .option('--base <path>', 'base components directory', 'src/components') | ||
| .description('List available component templates') | ||
| .option('--category <name>', 'filter by subcategory (e.g. blocks, layouts)') | ||
| .option('--category <name>', 'filter by subcategory (e.g. blocks, layouts, views)') | ||
| .action(({ category }) => listTemplates(category)) | ||
| program.parse(process.argv) |
+9
-10
| { | ||
| "name": "@wface/pixel-cli", | ||
| "version": "0.3.0", | ||
| "version": "0.3.1", | ||
| "type": "module", | ||
@@ -19,9 +19,2 @@ "bin": { | ||
| }, | ||
| "dependencies": { | ||
| "commander": "^12.1.0", | ||
| "fs-extra": "^11.2.0", | ||
| "kleur": "^4.1.5", | ||
| "open": "^10.2.0", | ||
| "serve-handler": "^6.1.6" | ||
| }, | ||
| "engines": { | ||
@@ -34,4 +27,10 @@ "node": ">=18" | ||
| "devDependencies": { | ||
| "@types/serve-handler": "^6" | ||
| } | ||
| "@types/serve-handler": "^6", | ||
| "commander": "^12.1.0", | ||
| "fs-extra": "^11.2.0", | ||
| "kleur": "^4.1.5", | ||
| "open": "^10.2.0", | ||
| "serve-handler": "^6.1.6" | ||
| }, | ||
| "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" | ||
| } |
@@ -6,2 +6,5 @@ .sidebar { | ||
| border-right: 1px solid var(--border-default); | ||
| position: sticky; | ||
| top: 65px; | ||
| z-index: 2; | ||
@@ -182,2 +185,5 @@ .sidebarWrapper { | ||
| } | ||
| &:last-child { | ||
| border-bottom: none; | ||
| } | ||
| } | ||
@@ -184,0 +190,0 @@ } |
| .topbar { | ||
| border-bottom: 2px solid var(--border-default); | ||
| border-bottom: 1px solid var(--border-default); | ||
| background: var(--elevation-surface); | ||
| position: sticky; | ||
| top: 65px; | ||
| z-index: 2; | ||
| .topbarWrapper { | ||
@@ -4,0 +9,0 @@ display: flex; |
| import styles from './Topnav.module.scss' | ||
| import settings from "../../../constants/settings.ts"; | ||
| import settings from "../../../constants/settings"; | ||
| import {FiLogOut, FiMoon, FiSun} from "react-icons/fi"; | ||
@@ -4,0 +4,0 @@ import {PiAvatar, useTheme} from "@wface/pixel-ui"; |
| .topnav { | ||
| border-bottom: 1px solid var(--border-default); | ||
| background: var(--elevation-surface); | ||
| position: sticky; | ||
| top: 0; | ||
| z-index: 2; | ||
| .topnavWrapper { | ||
@@ -4,0 +9,0 @@ display: flex; |
| { | ||
| "dependencies": [ | ||
| { "category": "blocks", "name": "sidebar"}, | ||
| { "category": "blocks", "name": "topbar"}, | ||
| { "category": "blocks", "name": "topnav"} | ||
| { "category": "blocks", "name": "topnav"}, | ||
| { "category": "blocks", "name": "login-form"} | ||
| ] | ||
| } |
@@ -13,6 +13,7 @@ import styles from './DashboardLayout.module.scss'; | ||
| className?: string; | ||
| mainCn?: string | ||
| } | ||
| const DashboardLayout = (props: Props) => { | ||
| const { children, className, ...rest } = props; | ||
| const { children, className, mainCn, ...rest } = props; | ||
@@ -31,3 +32,3 @@ const auth = { | ||
| <Topbar {...rest} /> | ||
| <div className={styles.main}> | ||
| <div className={`${mainCn ? mainCn : ''} ${styles.main}`}> | ||
| { children } | ||
@@ -34,0 +35,0 @@ </div> |
| { | ||
| "dependencies": [ | ||
| { "category": "blocks", "name": "sidebar"}, | ||
| { "category": "blocks", "name": "topbar"} | ||
| { "category": "blocks", "name": "topbar"}, | ||
| { "category": "blocks", "name": "topnav"} | ||
| ] | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
13696316
1.45%0
-100%125
26.26%106498
5.29%6
500%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed