UNPKG

wass-rct-ui

Version:

A lightweight and customizable WASS Rct UI component library for modern web applications.

848 lines (820 loc) 121 kB
import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import * as React from 'react'; import { useMemo, memo, forwardRef, useState, useRef, useEffect, useImperativeHandle, useCallback, useLayoutEffect, createContext, useContext, useReducer } from 'react'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; import { Link, useLocation } from 'react-router-dom'; const Block = React.memo(({ className = "", bgColor, children }) => { const dynamicClasses = [className, bgColor].filter(Boolean).join(" "); return jsx("div", { className: `block ${dynamicClasses}`.trim(), children: children }); }); const Buttons = React.memo(({ children, className }) => { const classNames = useMemo(() => { const baseClasses = ["buttons"]; if (className) baseClasses.push(className); return baseClasses.join(" "); }, [className]); return jsx("div", { className: classNames, children: children }); }); const spriteUrl = "/svg/icons.svg"; const getIconStyle = (style, color) => ({ ...style, fill: color, cursor: "pointer", userSelect: "none", }); const Icon = React.memo(({ name, width = 20, height = 20, style = {}, color = "white", onClick }) => { const computedStyle = useMemo(() => getIconStyle(style, color), [style, color]); return (jsx("svg", { width: width, height: height, style: computedStyle, role: onClick ? "button" : "presentation", "aria-hidden": !onClick, onClick: onClick, children: jsx("use", { xlinkHref: `${spriteUrl}#${name}` }) })); }); const Button = React.memo(({ keyId, label, iconAlignment = "right", iconName, iconSize = 20, iconColor = "white", colorVariant, sizeVariant, fullWidth, outlined, inverted, rounded, hovered, focused, isStatic, active, loading, disabled, dark, light, skeleton, responsive, onClick, type = "button", as: Component = "button", href, className = "", }) => { const classNames = [ "button", colorVariant, sizeVariant, fullWidth && "fullwidth", outlined && "outlined", inverted && "inverted", rounded && "rounded", hovered && "hovered", focused && "focused", active && "active", loading && "loading", disabled && "disabled", isStatic && "static", dark && "dark", light && "light", responsive && "responsive", skeleton && "skeleton", className, ] .filter(Boolean) .join(" "); const handleClick = (event) => { if (disabled || loading || skeleton) { event.preventDefault(); return; } onClick?.(event); }; const content = (jsxs(Fragment, { children: [iconAlignment === "right" && label, iconName && (jsx("span", { className: `icon ${sizeVariant}`, children: jsx(Icon, { name: iconName, width: iconSize, height: iconSize, color: skeleton ? "transparent" : iconColor }) })), iconAlignment === "left" && label] })); if (Component === "a" && href) { return (jsx("a", { className: classNames, href: href, onClick: handleClick, "aria-disabled": disabled, role: "button", children: content }, keyId)); } if (Component === "input") { return (jsx("input", { className: classNames, type: type, value: label, onClick: handleClick, disabled: disabled }, keyId)); } return (jsx("button", { className: classNames, type: type, onClick: handleClick, disabled: disabled, role: "button", children: content }, keyId)); }); const Field = ({ label, sideLabel = false, rightIcon = false, leftIcon = false, isExpanded = false, isGrouped = false, isLoading = false, isAddon = false, rightButton, buttonDisabled = false, leftButton, groupAlignment = "", labelSizeVariant = "normal", iconSizeVariant = "normal", leftIconSize = 16, leftIconColor = "grey", leftIconName = "default-icon", rightIconSize = 16, rightIconColor = "grey", rightIconName = "default-icon", rightButtonColor = "link", leftButtonColor = "link", className = "", children, onLeftIconClick, onRightIconClick, }) => { const classNames = ["field", className]; if (sideLabel) classNames.push("horizontal"); if (isGrouped) classNames.push("grouped"); if (isAddon) classNames.push("addons"); if (groupAlignment) classNames.push(groupAlignment); const controlClassNames = ["control"]; if (!rightButton && rightIcon) controlClassNames.push("icons-right"); if (!leftButton && leftIcon) controlClassNames.push("icons-left"); if (isLoading) controlClassNames.push("loading"); if (isExpanded) controlClassNames.push("expanded"); const labelClassNames = ["label", labelSizeVariant, isAddon ? "pr-3" : ""]; return (jsxs("div", { className: classNames.join(" "), children: [sideLabel ? (jsx("div", { className: "field-label", children: jsx("label", { className: labelClassNames.join(" "), children: label }) })) : (jsx("label", { className: labelClassNames.join(" "), children: label })), jsx("div", { className: sideLabel ? "field-body" : "field-width", children: jsxs("div", { className: `field ${leftButton || rightButton ? "addons" : ""}`, children: [leftButton && (jsx("div", { className: controlClassNames.join(" "), children: jsx(Button, { colorVariant: leftButtonColor, iconName: leftIconName, disabled: buttonDisabled, iconColor: leftIconColor, iconSize: leftIconSize, onClick: onLeftIconClick }) })), jsxs("div", { className: controlClassNames.join(" "), children: [leftIcon && !leftButton && (jsx("span", { className: `icon ${iconSizeVariant} left`, children: jsx(Icon, { name: leftIconName, color: leftIconColor, height: leftIconSize, width: leftIconSize, onClick: onLeftIconClick }) })), children, rightIcon && !rightButton && (jsx("span", { className: `icon ${iconSizeVariant} right`, onClick: onRightIconClick, children: jsx(Icon, { name: rightIconName, color: rightIconColor, height: rightIconSize, width: rightIconSize, onClick: onRightIconClick }) }))] }), rightButton && (jsx("div", { className: controlClassNames.join(" "), children: jsx(Button, { colorVariant: rightButtonColor, iconName: rightIconName, disabled: buttonDisabled, iconColor: rightIconColor, iconSize: rightIconSize, onClick: onRightIconClick }) }))] }) })] })); }; const InputFactory = memo((props) => { const { colorVariant, sizeVariant, isReadonly, fixedSize, rows, disabled, id, type, name, placeholder, value, defaultValue, options = [], checked, required, minLength, maxLength, pattern, accept, multiple, step, readOnly, onChange, onBlur, onKeyDown, onKeyUp, onFocus, onClick, } = props; let className = ""; if (type === "text" || type === "color" || type === "password" || type === "number" || type === "tel") className = "input"; if (type === "textarea") className = "textarea"; const classNames = [ className, colorVariant, sizeVariant, isReadonly, fixedSize, ] .filter(Boolean) .join(" "); const commonProps = { className: classNames, rows, disabled, id, name, placeholder, value, defaultValue, required, readOnly, minLength, maxLength, pattern, accept, multiple, step, onChange, onBlur, onKeyDown, onKeyUp, onFocus, onClick, }; switch (type) { case "text": case "password": case "color": case "number": case "tel": return jsx("input", { type: type, ...commonProps }); case "textarea": return jsx("textarea", { ...commonProps }); case "select": return (jsx("div", { className: "select", children: jsx("select", { ...commonProps, children: options.map((option) => (jsx("option", { value: option.value, children: option.label }, option.value))) }) })); case "checkbox": return (jsxs("label", { className: "checkbox", children: [jsx("input", { type: "checkbox", checked: checked, ...commonProps }), props.label] })); case "radio": return (jsx("div", { children: options.map((option) => (jsxs("label", { className: "radio", children: [jsx("input", { type: "radio", checked: value === option.value, ...commonProps }), option.label] }, option.value))) })); default: return null; } }); const FormInput = memo((props) => { const { fieldProps = {}, validationMessage } = props; return (jsxs(Field, { ...fieldProps, children: [jsx(InputFactory, { ...props }), validationMessage && (jsx("p", { className: "help text-danger", children: validationMessage }))] })); }); const GridBox = memo(({ minCol = 4, gap = 0, columnGap = 0, rowGap = 0, mobileCols, tabletCols, desktopCols, widescreenCols, fullhdCols, allDeviceCols, autoCount = false, fixedGrid = false, className, children, }) => { const gridClasses = useMemo(() => { const classes = ["grid"]; classes.push(`col-min-${Math.min(Math.max(minCol, 1), 32)}`); if (gap > 0) classes.push(`gap-${Math.min(Math.max(gap, 0), 8)}`); if (columnGap > 0) classes.push(`column-gap-${Math.min(Math.max(columnGap, 0), 8)}`); if (rowGap > 0) classes.push(`row-gap-${Math.min(Math.max(rowGap, 0), 8)}`); if (className) classes.push(className); return classes.join(" "); }, [minCol, gap, columnGap, rowGap, className]); const fixedGridClasses = useMemo(() => { const classes = ["fixed-grid"]; if (mobileCols) classes.push(`cols-${mobileCols}-mobile`); if (tabletCols) classes.push(`cols-${tabletCols}-tablet`); if (desktopCols) classes.push(`cols-${desktopCols}-desktop`); if (widescreenCols) classes.push(`cols-${widescreenCols}-widescreen`); if (fullhdCols) classes.push(`cols-${fullhdCols}-fullhd`); if (allDeviceCols) classes.push(`cols-${allDeviceCols}`); if (autoCount) classes.push("auto-count"); if (className) classes.push(className); return classes.join(" "); }, [ mobileCols, tabletCols, desktopCols, widescreenCols, fullhdCols, allDeviceCols, autoCount, className, ]); return fixedGrid ? (jsx("div", { className: fixedGridClasses, children: jsx("div", { className: gridClasses, children: children }) })) : (jsx("div", { className: gridClasses, children: children })); }); const GridBoxCell = memo(({ className, children, colStart, colFromEnd, colSpan, rowStart, rowFromEnd, rowSpan, }) => { const gridCellClasses = useMemo(() => { const classes = ["cell"]; if (colStart) classes.push(`col-start-${colStart}`); if (colFromEnd) classes.push(`col-from-end-${colFromEnd}`); if (colSpan) classes.push(`col-span-${colSpan}`); if (rowStart) classes.push(`row-start-${rowStart}`); if (rowFromEnd) classes.push(`row-from-end-${rowFromEnd}`); if (rowSpan) classes.push(`row-span-${rowSpan}`); if (className) classes.push(className); return classes.join(" "); }, [ colStart, colFromEnd, colSpan, rowStart, rowFromEnd, rowSpan, className, ]); return jsx("div", { className: gridCellClasses, children: children }); }); const SubTitle = memo(({ sizeLevel, skeleton = false, colorVariant = "text-black", children, className }) => { const Tag = `h${sizeLevel}`; const classNames = [ `subtitle s-${sizeLevel}`, skeleton && "skeleton", colorVariant, className ] .filter(Boolean) .join(" "); return jsx(Tag, { className: classNames, children: children }); }); const getPositionStyles = (position) => { const positions = { "bottom-right": { bottom: "20px", right: "20px" }, "bottom-left": { bottom: "20px", left: "20px" }, "top-right": { top: "20px", right: "20px" }, "top-left": { top: "20px", left: "20px" }, "top-center": { top: "20px", left: "50%", transform: "translateX(-50%)" }, "middle-left": { top: "50%", left: "20px", transform: "translateY(-50%)" }, "middle-right": { top: "50%", right: "20px", transform: "translateY(-50%)", }, "bottom-center": { bottom: "20px", left: "50%", transform: "translateX(-50%)", }, }; return positions[position] || positions["bottom-right"]; }; const getChatBoxPosition = (position) => { const positions = { "bottom-right": { bottom: "60px", right: "20px" }, "bottom-left": { bottom: "60px", left: "20px" }, "top-right": { top: "60px", right: "20px" }, "top-left": { top: "60px", left: "20px" }, "top-center": { top: "60px", left: "50%", transform: "translateX(-50%)" }, "middle-left": { top: "50%", left: "20px", transform: "translateY(-50%)" }, "middle-right": { top: "50%", right: "20px", transform: "translateY(-50%)", }, "bottom-center": { bottom: "60px", left: "50%", transform: "translateX(-50%)", }, }; return positions[position] || positions["bottom-right"]; }; const ChatBot = forwardRef(({ position = "bottom-right", iconName = "solid-message-rounded", iconColor = "white", iconSize = 30, buttonColor = "link", chatTheme = "background-link", defaultMessages = [], chatBotTitle = "Chat Assistant", welcomeMessage = "👋 Hi! I'm your chat assistant. How can I help you today?", botResponses = {}, placeholderText = "Type a message...", quickReplies = [], onAskQuestion, }, ref) => { const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState(defaultMessages); const [inputValue, setInputValue] = useState(""); const [showTypingIndicator, setShowTypingIndicator] = useState(false); const messagesContainerRef = useRef(null); useEffect(() => { if (isOpen && messages.length === 0) { setTimeout(() => { addMessage(welcomeMessage, "bot"); }, 1000); } }, [isOpen]); const addMessage = (text, sender) => { setMessages((prevMessages) => [...prevMessages, { text, sender }]); scrollToBottom(); }; const getBotResponse = (message) => { return botResponses[message]; }; const handleSendMessage = () => { if (!inputValue.trim()) return; onAskQuestion?.(inputValue); addMessage(inputValue, "user"); setInputValue(""); setShowTypingIndicator(true); setTimeout(() => { setShowTypingIndicator(false); addMessage(getBotResponse(inputValue), "bot"); }, 1500); }; const handleQuickReply = (text) => { addMessage(text, "user"); setShowTypingIndicator(true); setTimeout(() => { setShowTypingIndicator(false); const botReply = getBotResponse(text); addMessage(botReply, "bot"); }, 1500); }; const scrollToBottom = () => { if (messagesContainerRef.current) { setTimeout(() => { messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight - 20; }, 100); } }; useImperativeHandle(ref, () => ({ addNewMessage: (message, sender = "bot") => { addMessage(message, sender); }, })); return (jsxs("div", { className: "chat-widget", style: getPositionStyles(position), children: [jsxs("div", { className: `chat-toggle ${chatTheme}`, onClick: () => setIsOpen(!isOpen), children: [jsx("span", { className: "notification-badge", children: "1" }), jsx(Icon, { name: iconName, height: iconSize, width: iconSize, color: iconColor })] }), jsxs("div", { className: `chat-box ${isOpen ? "active" : ""}`, style: getChatBoxPosition(position), children: [jsxs(GridBox, { fixedGrid: true, minCol: 2, className: `p-1 mb-0 ${chatTheme}`, children: [jsx(GridBoxCell, { colSpan: 1, children: jsx(SubTitle, { sizeLevel: 6, colorVariant: "text-white", children: chatBotTitle }) }), jsx(GridBoxCell, { colSpan: 1, className: "icons-controll", children: jsxs(Buttons, { children: [jsx(Button, { colorVariant: "black", iconName: "minus", sizeVariant: "small", iconColor: "white", iconSize: 16, onClick: () => setIsOpen(false) }), jsx(Button, { colorVariant: "black", iconName: "x", sizeVariant: "small", iconColor: "white", iconSize: 16, onClick: () => setIsOpen(false) })] }) })] }), jsxs("div", { className: "chat-messages", ref: messagesContainerRef, children: [messages.map((message, index) => (jsxs("div", { className: `message ${message.sender}-message`, children: [message.text, jsx("span", { className: "message-time", children: new Date().toLocaleTimeString() }), message.sender === "user" && (jsx("div", { className: "message-status", children: "\u2713\u2713" }))] }, index))), showTypingIndicator && (jsxs("div", { className: "typing-indicator background-link", children: [jsx("span", {}), jsx("span", {}), jsx("span", {})] }))] }), jsx(Buttons, { className: "mb-0 p-2", children: quickReplies.map((reply, index) => (jsx(Button, { rounded: true, sizeVariant: "small", label: reply, onClick: () => handleQuickReply(reply) }, index))) }), jsxs(GridBox, { fixedGrid: true, allDeviceCols: 4, className: "p-1 mb-0", children: [jsx(GridBoxCell, { colSpan: 3, children: jsx(FormInput, { type: "text", name: "chat-input", id: "chat-input", value: inputValue, placeholder: placeholderText, onChange: (e) => setInputValue(e.target.value) }) }), jsx(GridBoxCell, { colSpan: 1, className: "pt-2", children: jsx(Buttons, { children: jsx(Button, { colorVariant: buttonColor, label: "Send", onClick: handleSendMessage }) }) })] })] })] })); }); const Collapsible = ({ title, children, isOpen, onToggle, bodyBGColor = "background-danger", headerBGColor = "background-success", titleColor = "text-black", iconColor = "#000", iconSize = 16, padding = 2, }) => { return (jsxs("div", { className: "card mb-3", children: [jsxs("header", { className: `card-header ${headerBGColor}`, onClick: onToggle, children: [jsx("p", { className: `card-header-title ${titleColor} p-${padding}`, children: title }), jsx("button", { className: "card-header-icon", "aria-label": "more options", children: jsx("span", { className: "icon", children: jsx(Icon, { name: isOpen ? "solid-up-arrow" : "solid-down-arrow", color: iconColor, width: iconSize, height: iconSize }) }) })] }), isOpen && (jsx("div", { className: `card-footer p-${padding} ${bodyBGColor}`, children: children }))] })); }; const CollapsibleGroup = ({ iconColor = "#000", iconSize = 16, padding = 2, className = "", items, }) => { const [openIndex, setOpenIndex] = useState(null); const handleToggle = useCallback((index) => { setOpenIndex((prevIndex) => (prevIndex === index ? null : index)); }, []); const collapsibles = useMemo(() => items.map((item, index) => (jsx(Collapsible, { title: item.title, isOpen: openIndex === index, onToggle: () => handleToggle(index), iconColor: iconColor, padding: padding, iconSize: iconSize, bodyBGColor: item.bodyBGColor, headerBGColor: item.headerBGColor, titleColor: item.titleColor, children: item.content }, index))), [items, openIndex, iconColor, padding, iconSize, handleToggle]); const classNames = []; if (className) classNames.push(className); return jsx("div", { className: classNames.join(" "), children: collapsibles }); }; const Column = memo(({ size, offset, isNarrow, isNarrowMobile, isNarrowTablet, isNarrowTouch, isNarrowDesktop, isNarrowWidescreen, isNarrowFullhd, children, clasName, }) => { const columnClassNames = [ "column", size ? size : "", offset ? offset : "", isNarrow ? "narrow" : "", isNarrowMobile ? "narrow-mobile" : "", isNarrowTablet ? "narrow-tablet" : "", isNarrowTouch ? "narrow-touch" : "", isNarrowDesktop ? "narrow-desktop" : "", isNarrowWidescreen ? "narrow-widescreen" : "", isNarrowFullhd ? "narrow-fullhd" : "", clasName, ] .filter(Boolean) .join(" "); return jsx("div", { className: columnClassNames, children: children }); }); const Columns = memo(({ isMobile, isTablet, isTouch, isDesktop, isWidescreen, isFullhd, isGapLess, isMultiLine, isCentered, isVCentered, children, className, }) => { const columnsClassNames = [ "columns", isCentered ? "centered" : "", isVCentered ? "vcentered" : "", isGapLess ? "gapless" : "", isMultiLine ? "multiline" : "", isMobile ? "mobile" : "", isTablet ? "tablet" : "", isTouch ? "touch" : "", isDesktop ? "desktop" : "", isWidescreen ? "widescreen" : "", isFullhd ? "fullhd" : "", className || "", ] .filter(Boolean) .join(" "); return jsx("div", { className: columnsClassNames, children: children }); }); const Container = memo(({ containerVariant, className = "", children }) => { const classNames = ["container"]; if (containerVariant) classNames.push(containerVariant); if (className) classNames.push(className); return jsx("section", { className: classNames.join(" "), children: children }); }); const Content = memo(({ sizeVariant, className = "", children }) => { const classNames = ["content"]; if (sizeVariant) classNames.push(sizeVariant); if (className) classNames.push(className); return jsx("section", { className: classNames.join(" "), children: children }); }); // Extend dayjs with plugins dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(isBetween); const DatePicker = memo(({ value = "", onChange = () => { }, timeZone = "Asia/Kolkata", pickerType = "date", placeholder = "Pick Date", minDate, maxDate, dateRange = false, colorVariant = "link", }) => { const now = dayjs().tz(timeZone); const datePickerRef = useRef(null); const [selectedDate, setSelectedDate] = useState(value || ""); const [showCalendar, setShowCalendar] = useState(false); const [showMonthPicker, setShowMonthPicker] = useState(false); const [showYearPicker, setShowYearPicker] = useState(false); const [currentMonth, setCurrentMonth] = useState(now.month()); const [currentYear, setCurrentYear] = useState(now.year()); const [startYear, setStartYear] = useState(Math.floor(now.year() / 12) * 12); const [rangeStart, setRangeStart] = useState(null); const [rangeEnd, setRangeEnd] = useState(null); const prevYearRange = () => setStartYear(startYear - 12); const nextYearRange = () => setStartYear(startYear + 12); const firstDayOfMonth = (month, year) => dayjs(new Date(year, month)).startOf("month").day(); const parseDateString = (dateString) => { const [day, month, year] = dateString.split("/").map(Number); return dayjs(new Date(year, month - 1, day)).tz(timeZone); }; const [validMinDate, setValidMinDate] = useState(minDate ? parseDateString(minDate) : null); const [validMaxDate, setValidMaxDate] = useState(maxDate ? parseDateString(maxDate) : null); useEffect(() => { if (value) { if (dateRange && value.includes(" - ")) { const [startDateStr, endDateStr] = value.split(" - "); const startDate = parseDateString(startDateStr); const endDate = parseDateString(endDateStr); setRangeStart(startDate); setRangeEnd(endDate); setSelectedDate(`${startDate.format("DD/MM/YYYY")} - ${endDate.format("DD/MM/YYYY")}`); } else { const date = parseDateString(value); setSelectedDate(date.format("DD/MM/YYYY")); if (dateRange) { setRangeStart(date); setRangeEnd(null); } } } else { setSelectedDate(""); setRangeStart(null); setRangeEnd(null); } }, [value, dateRange]); useEffect(() => { if (minDate) { const minDateObj = parseDateString(minDate); setValidMinDate(minDateObj); } else { setValidMinDate(null); } if (maxDate) { const maxDateObj = parseDateString(maxDate); setValidMaxDate(maxDateObj); } else { setValidMaxDate(null); } }, [minDate, maxDate]); const handleDateClick = (day) => { const date = dayjs(new Date(currentYear, currentMonth, day)).tz(timeZone); if (dateRange) { if (!rangeStart || (rangeStart && rangeEnd)) { setRangeStart(date); setRangeEnd(null); } else if (rangeStart && !rangeEnd) { if (date.isBefore(rangeStart)) { setRangeStart(date); setRangeEnd(rangeStart); } else { setRangeEnd(date); } const fromDate = rangeStart.format("DD/MM/YYYY"); const toDate = date.format("DD/MM/YYYY"); setSelectedDate(`${fromDate} - ${toDate}`); onChange([fromDate, toDate]); setShowCalendar(false); setRangeStart(null); setRangeEnd(null); } } else { const selectedDate = date.format("DD/MM/YYYY"); setSelectedDate(selectedDate); onChange(selectedDate); setShowCalendar(false); } }; const toggleCalendar = (type) => { if (type === "date") setShowCalendar(true); if (type === "month") setShowMonthPicker(true); if (type === "year") setShowYearPicker(true); }; const handleMonthClick = (month) => { setCurrentMonth(month); setShowMonthPicker(false); setShowCalendar(true); if (pickerType === "month") { const formattedMonth = dayjs(new Date(currentYear, month)).format("MM/YYYY"); setSelectedDate(formattedMonth); onChange(formattedMonth); } }; const handleYearClick = (year) => { setCurrentYear(year); setShowYearPicker(false); setShowMonthPicker(true); if (pickerType === "year") { const formattedYear = dayjs(new Date(year, currentMonth)).format("YYYY"); setSelectedDate(formattedYear); onChange(formattedYear); } }; const prevMonth = () => { const newDate = dayjs(new Date(currentYear, currentMonth)).subtract(1, "month"); setCurrentMonth(newDate.month()); setCurrentYear(newDate.year()); }; const nextMonth = () => { const newDate = dayjs(new Date(currentYear, currentMonth)).add(1, "month"); setCurrentMonth(newDate.month()); setCurrentYear(newDate.year()); }; useEffect(() => { const handleClickOutside = (event) => { if (datePickerRef.current && !datePickerRef.current.contains(event.target)) { setShowCalendar(false); setShowMonthPicker(false); setShowYearPicker(false); } }; if (showCalendar || showMonthPicker || showYearPicker) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [showCalendar, showMonthPicker, showYearPicker]); const isDateDisabled = (day) => { const date = dayjs(new Date(currentYear, currentMonth, day)).tz(timeZone); if (validMinDate && date.isBefore(validMinDate)) { return true; } if (validMaxDate && date.isAfter(validMaxDate)) { return true; } if (rangeStart && date.isBefore(rangeStart)) { return true; } return false; }; const isDateInRange = (day) => { const date = dayjs(new Date(currentYear, currentMonth, day)).tz(timeZone); if (rangeStart && rangeEnd) { return date.isBetween(rangeStart, rangeEnd, null, "[]"); } return false; }; const renderDatePicker = () => { const daysInMonth = dayjs(new Date(currentYear, currentMonth)).daysInMonth(); const firstDayOfWeek = firstDayOfMonth(currentMonth, currentYear); const lastDayOfPrevMonth = dayjs(new Date(currentYear, currentMonth - 1)).daysInMonth(); const daysFromPrevMonth = firstDayOfWeek; const daysFromNextMonth = 6 * 7 - (daysInMonth + daysFromPrevMonth); return (jsx("div", { className: "fixed-grid cols-7 date-picker-calendar animate-fade-in", children: jsxs("div", { className: "grid column-gap-1 row-gap-1", children: [jsx("div", { className: "cell", children: jsx(Button, { iconName: "solid-left-arrow", iconSize: 14, iconColor: "black", sizeVariant: "small", onClick: prevMonth }, "month-next") }), jsx("div", { className: "cell col-span-5", children: jsx("span", { className: "month-year", onClick: () => setShowMonthPicker(true), children: dayjs(new Date(currentYear, currentMonth)).format("MMMM YYYY") }) }), jsx("div", { className: "cell", children: jsx(Button, { iconName: "solid-right-arrow", iconSize: 14, iconColor: "black", sizeVariant: "small", onClick: nextMonth }, "month-next") }), ["S", "M", "T", "W", "T", "F", "S"].map((d, index) => (jsx("div", { className: "cell", children: jsx("b", { children: d }) }, index))), [...Array(daysFromPrevMonth)].map((_, i) => (jsx(Button, { label: lastDayOfPrevMonth - daysFromPrevMonth + i + 1, sizeVariant: "small", disabled: true }, `prev-month-day-${i}`))), [...Array(daysInMonth)].map((_, i) => (jsx(Button, { label: i + 1, sizeVariant: "small", colorVariant: colorVariant, className: `${isDateDisabled(i + 1) ? "disabled" : ""} ${isDateInRange(i + 1) || (rangeStart && rangeStart.format("DD/MM/YYYY") === dayjs(new Date(currentYear, currentMonth, i + 1)).format("DD/MM/YYYY")) ? "in-range" : ""}`, onClick: () => handleDateClick(i + 1), disabled: isDateDisabled(i + 1) }, `day-${i + 1}`))), [...Array(daysFromNextMonth)].map((_, i) => (jsx(Button, { label: i + 1, sizeVariant: "small", disabled: true }, `next-month-day-${i}`)))] }) })); }; const renderMonthPicker = () => (jsx("div", { className: "fixed-grid cols-3 month-picker animate-fade-in", children: jsxs("div", { className: "grid column-gap-1 row-gap-1", children: [jsx("div", { className: "cell", children: "\u00A0" }), jsx("div", { className: "cell", children: jsx(Button, { label: currentYear, sizeVariant: "small", onClick: () => { setShowMonthPicker(false); setShowYearPicker(true); } }, "year") }), jsx("div", { className: "cell", children: "\u00A0" }), [...Array(12)].map((_, i) => (jsx(Button, { label: dayjs(new Date(currentYear, i)).format("MMM"), sizeVariant: "small", colorVariant: colorVariant, onClick: () => handleMonthClick(i) }, `month-${i}`)))] }) })); const renderYearPicker = () => (jsx("div", { className: "fixed-grid cols-4 year-picker animate-fade-in", children: jsxs("div", { className: "grid column-gap-1 row-gap-1", children: [jsx("div", { className: "cell", children: jsx(Button, { iconName: "solid-left-arrow", iconSize: 14, iconColor: "black", sizeVariant: "small", onClick: prevYearRange }, "year-prev") }), jsxs("div", { className: "cell col-span-2", children: [startYear, " - ", startYear + 11] }), jsx("div", { className: "cell", children: jsx(Button, { iconName: "solid-right-arrow", iconSize: 14, iconColor: "black", sizeVariant: "small", onClick: nextYearRange }, "year-next") }), [...Array(12)].map((_, i) => (jsx(Button, { label: startYear + i, sizeVariant: "small", colorVariant: colorVariant, onClick: () => handleYearClick(startYear + i) }, startYear + i)))] }) })); return (jsxs("div", { className: "date-picker", ref: datePickerRef, children: [jsx(FormInput, { type: "text", name: "date-picker", placeholder: placeholder, readOnly: true, value: selectedDate, onClick: () => toggleCalendar(pickerType), fieldProps: { leftIcon: true, leftIconColor: "grey", leftIconName: "calendar", leftIconSize: 20, rightIcon: true, rightIconColor: "grey", rightIconName: "refresh", rightIconSize: 20, onRightIconClick() { setSelectedDate(""); }, } }, Date.now().toString()), pickerType === "date" && showCalendar && !showMonthPicker && !showYearPicker && renderDatePicker(), (pickerType === "date" || pickerType === "month") && showMonthPicker && renderMonthPicker(), (pickerType === "year" || pickerType === "month" || pickerType === "date") && showYearPicker && renderYearPicker()] })); }); const Figure = React.memo(({ width = 1, height = 1, className = "", children, caption }) => { const classNames = useMemo(() => { const baseClasses = ["image", `s-${width}by${height}`]; if (className) baseClasses.push(className); return baseClasses.join(" "); }, [width, height, className]); return (jsxs("figure", { className: classNames, children: [children, caption && jsx("figcaption", { children: caption })] })); }); const FlexContainer = ({ direction, wrap, justify, alignContent, alignItems, alignSelf, flexGrow, flexShrink, className = "", children, }) => { const flexClasses = useMemo(() => [ "flex", direction && `flex-direction-${direction}`, wrap && `flex-wrap-${wrap}`, justify && `justify-content-${justify}`, alignContent && `align-content-${alignContent}`, alignItems && `align-items-${alignItems}`, alignSelf && `align-self-${alignSelf}`, flexGrow !== undefined && `flex-grow-${flexGrow}`, flexShrink !== undefined && `flex-shrink-${flexShrink}`, className, ] .filter(Boolean) .join(" "), [ direction, wrap, justify, alignContent, alignItems, alignSelf, flexGrow, flexShrink, className, ]); return jsx("div", { className: flexClasses, children: children }); }; const FlexBox = React.memo(FlexContainer); const Footer = memo(({ className = "", children }) => { return jsx("footer", { className: `footer ${className}`.trim(), children: children }); }); const Form = forwardRef(({ className = "", children, onSubmit, ...props }, ref) => { return (jsx("form", { ref: ref, className: `form ${className}`.trim(), onSubmit: (event) => { event.preventDefault(); onSubmit?.(event); }, ...props, children: children })); }); Form.displayName = "Form"; const MemoizedForm = React.memo(Form); const getClassNames = ({ className, colorVariant, sizeVariant, }) => { return ["hero", colorVariant, sizeVariant, className] .filter(Boolean) .join(" "); }; const Hero = React.memo(({ className = "", colorVariant, sizeVariant, children }) => { const classNames = useMemo(() => getClassNames({ className, colorVariant, sizeVariant }), [className, colorVariant, sizeVariant]); return (jsx("section", { className: classNames, children: jsx("div", { className: "hero-body", children: children }) })); }); const ImageViewer = ({ isOpen, onClose, imageUrl, altText, }) => { if (!isOpen) return null; return (jsxs("div", { className: "modal active", children: [jsx("div", { className: "modal-background", onClick: onClose }), jsx("div", { className: "modal-content", children: jsx("p", { className: "image", children: jsx("img", { src: imageUrl || "https://bulma.io/assets/images/placeholders/1280x960.png", alt: altText || "Placeholder" }) }) }), jsx("button", { className: "modal-close large", "aria-label": "close", onClick: onClose })] })); }; const Media = memo(({ className = "", mediaLeft, mediaRight, mediaContent }) => { const mediaClass = `media ${className}`.trim(); return (jsxs("section", { className: mediaClass, children: [mediaLeft && jsx("div", { className: "media-left", children: mediaLeft }), jsx("div", { className: "media-content", children: mediaContent }), mediaRight && jsx("div", { className: "media-right", children: mediaRight })] })); }); const Navbar = ({ logo, leftLinks, rightLinks, logoWidth = 120, logoHeight = 40, navColor, isSideBarEnabled, sideBarIcon = "menu-alt-left", sideBarIconColor = "white", sideBarIconSize = 32, toggleSidebar, className, }) => { const [isActive, setIsActive] = useState(false); const [activeDropdown, setActiveDropdown] = useState(null); const [isTouchDevice, setIsTouchDevice] = useState(false); const [isScreenLarge, setIsScreenLarge] = useState(window.innerWidth > 1028); useEffect(() => { const handleResize = () => { setIsScreenLarge(window.innerWidth > 1028); }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); useEffect(() => { setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0); }, []); const toggleMenu = () => setIsActive((prev) => !prev); const handleDropdownClick = (index) => setActiveDropdown((prev) => (prev === index ? null : index)); const closeDropdown = () => { setActiveDropdown(null); setIsActive(false); }; return (jsxs("nav", { className: `navbar ${className} ${navColor || ""}`.trim(), children: [jsxs("div", { className: "navbar-brand", children: [isSideBarEnabled && !isScreenLarge && (jsx(Icon, { style: { paddingLeft: "0.5em" }, name: sideBarIcon, color: sideBarIconColor, height: sideBarIconSize, width: sideBarIconSize, onClick: () => toggleSidebar(!isSideBarEnabled) })), logo && (jsx(Link, { className: "navbar-item", to: "/", children: jsx("img", { src: logo, alt: "Logo", width: logoWidth, height: logoHeight }) })), jsxs("button", { className: `navbar-burger ${isActive ? "active" : ""}`, onClick: toggleMenu, children: [jsx("span", {}), jsx("span", {}), jsx("span", {}), jsx("span", {})] })] }), jsxs("div", { className: `navbar-menu ${isActive ? "active" : ""}`, children: [jsx("div", { className: "navbar-start", children: leftLinks.map((link, index) => link.type === "dropdown" && link.dropdownList ? (jsxs("div", { className: `navbar-item dropdown ${activeDropdown === index ? "active" : ""}`, onClick: () => handleDropdownClick(index), onMouseEnter: () => !isTouchDevice && setActiveDropdown(index), onMouseLeave: () => !isTouchDevice && setActiveDropdown(null), children: [jsx("span", { className: "navbar-link", children: link.label }), jsx("div", { className: `navbar-dropdown radius-sm ${link.dropdownRight ? "right" : ""}`, children: link.dropdownList && link.dropdownList.map((subLink, subIndex) => (jsx(Link, { className: "navbar-item", to: subLink.link, onClick: closeDropdown, children: subLink.label }, subIndex))) })] }, index)) : (jsx(Link, { className: `navbar-item ${link.className}`, to: link.link, onClick: closeDropdown, children: link.label }, index))) }), rightLinks && (jsx("div", { className: "navbar-end", children: jsx("div", { className: "navbar-item", children: jsx("div", { className: "buttons", children: rightLinks.map((link, index) => link.type === "dropdown" && link.dropdownList ? (jsxs("div", { className: `navbar-item dropdown ${activeDropdown === index ? "active" : ""}`, onClick: () => handleDropdownClick(index), onMouseEnter: () => !isTouchDevice && setActiveDropdown(index), onMouseLeave: () => !isTouchDevice && setActiveDropdown(null), children: [jsx("span", { className: "navbar-link", children: link.label }), jsx("div", { className: `navbar-dropdown radius-sm ${link.dropdownRight ? "right" : ""}`, children: link.dropdownList.map((subLink, subIndex) => (jsx(Link, { className: "navbar-item", to: subLink.link, onClick: closeDropdown, children: subLink.label }, subIndex))) })] }, index)) : link.type === "icon-button" ? (jsx(Link, { className: `button ${link.className} ${link.colorVariant}`, to: link.link, onClick: closeDropdown, children: link.iconName && (jsx(Icon, { name: link.iconName, color: link.iconColor, height: link.iconSize, width: link.iconSize })) }, index)) : link.type === "icon" ? (jsx(Link, { to: link.link, children: link.iconName && (jsx(Icon, { name: link.iconName, color: link.iconColor, height: link.iconSize, width: link.iconSize })) }, index)) : link.type === "button" ? (jsx(Link, { className: `button ${link.className} ${link.colorVariant}`, to: link.link, onClick: closeDropdown, children: jsx("strong", { children: link.label }) }, index)) : (jsx(Link, { className: `navbar-item ${link.className}`, to: link.link, onClick: closeDropdown, children: link.label }, index))) }) }) }))] })] })); }; const Card = memo(({ header, children, footer, className = "", skeleton = false, bgColorVariant = "background-white", }) => { const classNames = useMemo(() => { const baseClasses = ["card", "rounded-sm", "overflow-hidden"]; if (skeleton) baseClasses.push("skeleton"); if (!skeleton && bgColorVariant) baseClasses.push(bgColorVariant); if (className) baseClasses.push(className); return baseClasses.join(" "); }, [bgColorVariant, className, skeleton]); return (jsxs("section", { className: classNames, role: "region", "aria-label": "Card", children: [header && (jsx("header", { className: "card-header p-4", role: "heading", children: header })), jsx("main", { className: "card-content p-4", role: "main", children: jsx("div", { className: "content", children: children }) }), footer && (jsx("footer", { className: "card-footer p-4", role: "contentinfo", children: footer }))] })); }); const NoData = memo(({ className = "", title = "No Data", paddingLevel = 4 }) => { const classNames = ["no-data", "text-centered", className] .filter(Boolean) .join(" "); return (jsx("section", { className: classNames, role: "alert", "aria-live": "polite", children: jsx(Card, { children: jsx(SubTitle, { sizeLevel: 6, children: jsx("p", { className: `p-${paddingLevel}`, children: title }) }) }) })); }); const areEqual = (prevProps, nextProps) => { return (prevProps.rows === nextProps.rows && prevProps.viewMode === nextProps.viewMode && prevProps.min === nextProps.min && prevProps.max === nextProps.max && prevProps.onCellClick === nextProps.onCellClick); }; const OddsTableTable = memo(({ rows: initialRows, onCellClick, viewMode = "standard", tableHeader = "Odds", headerBGColor = "background-warning", headerTextColor = "text-black", min, max, }) => { const [rows, setRows] = useState(initialRows); const [changedCells, setChangedCells] = useState({}); const prevRows = useRef(initialRows); const handleCellClick = useCallback((rowId, cellType, teamId, odds, size) => { if (onCellClick) { onCellClick(rowId, cellType, teamId, odds, size); } }, [onCellClick]); const renderMinMaxLabel = useCallback(() => { if (min !== undefined && max !== undefined) { return `Min / Max: ${min} / ${max}`; } return null; }, [min, max]); useEffect(() => { const newChangedCells = {}; initialRows.forEach((row, rowIndex) => { const prevRow = prevRows.current[rowIndex]; if (!prevRow) return; if (row.suspended) return; row.backOdds.forEach((oddItem, oddIndex) => { const prevOddItem = prevRow.backOdds[oddIndex]; if (!prevOddItem) return; if (oddItem.odd !== prevOddItem.odd || oddItem.size !== prevOddItem.size) { const cellId = viewMode === "fancy" ? `${row.id}-back1` : `${row.id}-back${oddIndex + 1}`; newChangedCells[cellId] = true; } }); row.layOdds.forEach((oddItem, oddIndex) => { const prevOddItem = prevRow.layOdds[oddIndex]; if (!prevOddItem) return; if (oddItem.odd !== prevOddItem.odd || oddItem.size !== prevOddItem.size) { const cellId = viewMode === "fancy" ? `${row.id}-lay1` : `${row.id}-lay${oddIndex + 1}`; newChangedCells[cellId] = true; } }); }); setChangedCells(newChangedCells); prevRows.current = initialRows; setRows(initialRows); const timeout = setTimeout(() => { setChangedCells({}); }, 1200); return () => clearTimeout(timeout); }, [initialRows, viewMode]); const renderStandardRow = useCallback((row) => (jsxs("tr", { className: row.suspended ? "suspended" : "", "data-suspended-message": row.suspendedMessage || "SUSPENDED", children: [jsxs("td", { className: "text-weight-bold team-name", children: [jsx("span", { className: "team-name-text", children: row.teamName }), row.profitLoss !== null && (jsx("span", { className: `text-weight-bold profit-loss ${row.profitLoss >= 0 ? "profit" : "loss"}`, children: row.profitLoss >= 0 ? `+${row.profitLoss}` : row.profitLoss }))] }), row.backOdds.map((oddsItem, index) => { const cellId = `${row.id}-back${index + 1}`; return (jsx("td", { className: `text-centered back back-${index + 1} ${changedCells[cellId] ? "changed" : ""} ${row.suspended ? "suspended-cell" : ""}`, id: cellId, onClick: !row.suspended && onCellClick ? () => handleCellClick(row.id, "back", row.teamId, oddsItem.odd, oddsItem.size) : undefined, style: { cursor: !row.suspended && onCellClick ? "pointer" : "default", }, children: jsxs(Fragment, { children: [jsx("span", { className: "odds text-weight-bold", children: oddsItem.odd }), jsx("span", { className: "size", children: oddsItem.size })] }) }, `back-${index}`)); }), row.layOdds.map((oddsItem, index) => { const cellId = `${row.id}-lay${index + 1}`; return (jsx("td", { className: `text-centered lay lay-${index + 1} ${changedCells[cellId] ? "changed" : ""} ${row.suspended ? "suspended-cell" : ""}`, id: cellId, onClick: !row.suspended && onCellClick ? () => handleCellClick(row.id, "lay", row.teamId, oddsItem.odd, oddsItem.size) : undefined, style: { cursor: !row.suspended && onCellClick ? "pointer" : "default", }, children: jsxs(Fragment, { children: [jsx("span", { className: "odds text-weight-bold", children: oddsItem.odd }), jsx("span", { className: "size", children: oddsItem.size })] }) }, `lay-${index}`)); })] }, row.id)), [changedCells, handleCellClick, onCellClick]); const renderFancyRow = useCallback((row) => (jsxs("tr", { className: row.suspended ? "suspended" : "", "data-suspended-message": row.suspendedMessage || "SUSPENDED", children: [jsxs("td", { className: "team-name text-weight-bold", children: [jsx("span", { className: "team-name-text", children: row.teamName }), row.profitLoss !== null && (jsx("span", { className: `profit-loss ${row.profitLoss >= 0 ? "profit" : "loss"}`, children: row.profitLoss >= 0 ? `+${row.profitLoss}` : row.profitLoss }))] }), jsx("td", { className: `text-centered lay-cell ${changedCells[`${row.id}-lay1`] ? "changed" : ""} ${row.suspended ? "suspended-cell" : ""}`, id: `${row.id}-lay1`, onClick: !row.suspended && onCellClick ? () => handleCellClick(row.id, "lay", row.teamId, row.layOdds[0]?.odd || "", row.layOdds[0]?.size || "") : undefined, style: { cursor: !row.suspended && onCellClick ? "pointer" : "default", }, children: jsxs(Fragment, { children: [jsx("span", { className: "odds text-weight-bold", children: row.layOdds[0]?.odd || "-" }), jsx("span", { className: "size", children: row.layOdds[0]?.size || "-" })] }) }), jsx("td", { className: `text-centered back-cell ${changedCells[`${row.id}-back1`] ? "changed" : ""} ${row.suspended ? "suspended-cell" : ""}`, id: `${row.id}-back1`, onClick: !row.suspended && onCellClick ? () => handleCellClick(row.id, "back", row.teamId, row.backOdds[0]?.odd || "", row.backOdds[0]?.size || "") : undefined, style: { cursor: !row.suspended && onCellCli