🆕 Added New catalogue page

This commit is contained in:
duckietm
2026-03-23 15:02:20 +01:00
parent 19fd0e0809
commit 33c31fe07d
29 changed files with 2746 additions and 474 deletions
@@ -1,8 +1,9 @@
import { MouseEventType } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useMemo, useState } from 'react';
import { FaHeart } from 'react-icons/fa';
import { IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api';
import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common';
import { useCatalog, useInventoryFurni } from '../../../../../hooks';
import { useCatalog, useCatalogFavorites, useInventoryFurni } from '../../../../../hooks';
interface CatalogGridOfferViewProps extends LayoutGridItemProps
{
@@ -16,6 +17,8 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
const [ isMouseDown, setMouseDown ] = useState(false);
const { requestOfferToMover = null } = useCatalog();
const { isVisible = false } = useInventoryFurni();
const { isFavoriteOffer, toggleFavoriteOffer } = useCatalogFavorites();
const isFav = isFavoriteOffer(offer.offerId);
const iconUrl = useMemo(() =>
{
@@ -51,9 +54,28 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
if(!product) return null;
return (
<LayoutGridItem itemActive={ itemActive } itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) } itemImage={ iconUrl } itemUniqueNumber={ product.uniqueLimitedItemSeriesSize } itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) } onMouseDown={ onMouseEvent } onMouseOut={ onMouseEvent } onMouseUp={ onMouseEvent } { ...rest }>
<LayoutGridItem
className="group/tile relative"
itemActive={ itemActive }
itemCount={ ((offer.pricingModel === Offer.PRICING_MODEL_MULTI) ? product.productCount : 1) }
itemImage={ iconUrl }
itemUniqueNumber={ product.uniqueLimitedItemSeriesSize }
itemUniqueSoldout={ (product.uniqueLimitedItemSeriesSize && !product.uniqueLimitedItemsLeft) }
title={ `ID: ${ product.productClassId } | Offer: ${ offer.offerId }` }
onMouseDown={ onMouseEvent }
onMouseOut={ onMouseEvent }
onMouseUp={ onMouseEvent }
{ ...rest }
>
{ (offer.product.productType === ProductTypeEnum.ROBOT) &&
<LayoutAvatarImageView direction={ 3 } figure={ offer.product.extraParam } headOnly={ true } /> }
<div
className={ `absolute top-0 right-0 z-10 p-0.5 cursor-pointer transition-opacity duration-100 ${ isFav ? 'opacity-100' : 'opacity-0 group-hover/tile:opacity-100' }` }
onClick={ e => { e.stopPropagation(); e.preventDefault(); toggleFavoriteOffer(offer.offerId, offer.localizationName, iconUrl); } }
onMouseDown={ e => e.stopPropagation() }
>
<FaHeart className={ `text-[10px] drop-shadow transition-colors duration-100 ${ isFav ? 'text-danger' : 'text-muted hover:text-danger' }` } />
</div>
</LayoutGridItem>
);
};
@@ -2,11 +2,9 @@ import { GetSessionDataManager, IFurnitureData } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FaSearch, FaTimes } from 'react-icons/fa';
import { CatalogPage, CatalogType, FilterCatalogNode, FurnitureOffer, GetOfferNodes, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api';
import { Button, Flex } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { NitroInput } from '../../../../../layout';
export const CatalogSearchView: FC<{}> = props =>
export const CatalogSearchView: FC<{}> = () =>
{
const [ searchValue, setSearchValue ] = useState('');
const { currentType = null, rootNode = null, offersToNodes = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog();
@@ -78,29 +76,22 @@ export const CatalogSearchView: FC<{}> = props =>
}, [ offersToNodes, currentType, rootNode, searchValue, setCurrentPage, setSearchResult ]);
return (
<div className="flex gap-1">
<Flex fullWidth alignItems="center" position="relative">
<NitroInput
placeholder={ LocalizeText('generic.search') }
value={ searchValue }
onChange={ event => setSearchValue(event.target.value) } />
</Flex>
{ (!searchValue || !searchValue.length) &&
<Button className="catalog-search-button" variant="primary">
<FaSearch className="fa-icon" />
</Button> }
{ searchValue && !!searchValue.length &&
<Button className="catalog-search-button" variant="primary" onClick={ event => setSearchValue('') }>
<FaTimes className="fa-icon" />
</Button> }
<div className="relative w-full">
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-[9px] text-muted pointer-events-none" />
<input
className="w-full pl-6 pr-6 py-[3px] text-[11px] rounded border-2 border-card-grid-item-border bg-white text-dark placeholder-muted focus:outline-none focus:border-primary transition-colors"
placeholder={ LocalizeText('generic.search') }
type="text"
value={ searchValue }
onChange={ e => setSearchValue(e.target.value) }
/>
{ searchValue && searchValue.length > 0 &&
<button
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[9px] text-muted hover:text-danger cursor-pointer transition-colors"
onClick={ () => setSearchValue('') }
>
<FaTimes />
</button> }
</div>
);
};
@@ -1,7 +1,6 @@
import { PurchasePrefixComposer } from '@nitrots/nitro-renderer';
import { createPortal } from 'react-dom';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api';
import { LocalizeText, SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api';
import { CatalogLayoutProps } from './CatalogLayout.types';
import data from '@emoji-mart/data';
import Picker from '@emoji-mart/react';
@@ -32,32 +31,6 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
const [ showIconPicker, setShowIconPicker ] = useState(false);
const [ selectedEffect, setSelectedEffect ] = useState('');
const [ purchased, setPurchased ] = useState(false);
const pickerContainerRef = useRef<HTMLDivElement>(null);
// Inject style into emoji-mart Shadow DOM to remove backdrop-filter blur
useEffect(() =>
{
if(!showIconPicker) return;
const timer = setTimeout(() =>
{
const container = pickerContainerRef.current;
if(!container) return;
const emPicker = container.querySelector('em-emoji-picker');
if(!emPicker?.shadowRoot) return;
const existing = emPicker.shadowRoot.querySelector('#no-blur-fix');
if(existing) return;
const style = document.createElement('style');
style.id = 'no-blur-fix';
style.textContent = `.sticky { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; } .menu { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; }`;
emPicker.shadowRoot.appendChild(style);
}, 50);
return () => clearTimeout(timer);
}, [ showIconPicker ]);
const colorString = useMemo(() =>
{
@@ -104,7 +77,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: color }));
setCustomColorInput(color);
// Auto-advance to next letter
// Auto-avanza alla lettera successiva
if(selectedLetterIndex < prefixText.length - 1)
{
const nextIdx = selectedLetterIndex + 1;
@@ -194,12 +167,12 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
{ /* Text + Icon Row */ }
<div className="flex gap-2">
<div className="flex flex-col gap-0.5 flex-1">
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Text</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">{ LocalizeText('catalog.prefix.text') }</label>
<div className="relative">
<input
className="w-full px-3 py-1.5 rounded-md text-sm focus:outline-none transition-all"
maxLength={ 15 }
placeholder="Enter text..."
placeholder={ LocalizeText('catalog.prefix.text.placeholder') }
style={ {
background: 'rgba(0,0,0,0.15)',
border: '1px solid rgba(0,0,0,0.15)',
@@ -214,7 +187,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
</div>
</div>
<div className="flex flex-col gap-0.5 relative">
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Icon</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">{ LocalizeText('catalog.prefix.icon') }</label>
<div className="flex gap-1">
<button
className="flex items-center justify-center gap-1 px-3 py-1.5 rounded-md text-sm transition-all min-w-[70px]"
@@ -232,7 +205,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
<button
className="flex items-center justify-center px-1.5 rounded-md text-xs transition-all"
style={ { background: 'rgba(239,68,68,0.15)', border: '1px solid rgba(239,68,68,0.3)' } }
title="Remove icon"
title={ LocalizeText('catalog.prefix.icon.remove') }
onClick={ () => setSelectedIcon('') }>
</button>
@@ -241,14 +214,14 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
</div>
</div>
{ /* Emoji Picker (emoji-mart) - portaled to body, no backdrop */ }
{ showIconPicker && createPortal(
{ /* Emoji Picker (emoji-mart) - fixed overlay */ }
{ showIconPicker && (
<>
<div className="fixed inset-0" style={ { zIndex: 9998 } } onClick={ () => setShowIconPicker(false) } />
<div ref={ pickerContainerRef } className="fixed rounded-xl overflow-hidden" style={ { zIndex: 9999, top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: '#2b2f35' } }>
<div className="fixed inset-0" style={ { zIndex: 999, background: 'rgba(0,0,0,0.5)' } } onClick={ () => setShowIconPicker(false) } />
<div className="fixed rounded-xl overflow-hidden" style={ { zIndex: 1000, top: '50%', left: '50%', transform: 'translate(-50%, -50%)', boxShadow: '0 8px 32px rgba(0,0,0,0.6)' } }>
<Picker
data={ data }
locale="en"
locale="it"
onEmojiSelect={ (emoji: { native: string }) => { setSelectedIcon(emoji.native); setShowIconPicker(false); } }
theme="dark"
previewPosition="none"
@@ -261,13 +234,12 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
set="native"
/>
</div>
</>,
document.body
</>
) }
{ /* Effect Selector */ }
<div className="flex flex-col gap-1">
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Effect</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">{ LocalizeText('catalog.prefix.effect') }</label>
<div className="flex flex-wrap gap-1">
{ PRESET_PREFIX_EFFECTS.map(fx => (
<button
@@ -287,7 +259,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
{ /* Color Mode Toggle */ }
<div className="flex flex-col gap-1">
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Color</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">{ LocalizeText('catalog.prefix.color') }</label>
<div className="flex rounded-md overflow-hidden" style={ { border: '1px solid rgba(0,0,0,0.15)' } }>
<button
className="flex-1 px-2 py-1.5 text-xs font-bold transition-all"
@@ -297,7 +269,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
opacity: colorMode === 'single' ? 1 : 0.6
} }
onClick={ () => { setColorMode('single'); setSelectedLetterIndex(null); } }>
🎨 Single
{ LocalizeText('catalog.prefix.color.single') }
</button>
<button
className="flex-1 px-2 py-1.5 text-xs font-bold transition-all"
@@ -306,7 +278,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
opacity: colorMode === 'perLetter' ? 1 : 0.6
} }
onClick={ () => { setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0); } }>
🌈 Per Letter
{ LocalizeText('catalog.prefix.color.per.letter') }
</button>
</div>
</div>
@@ -316,7 +288,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<span className="text-[10px] opacity-50">
Select a letter, then choose a color. Auto-advances.
{ LocalizeText('catalog.prefix.color.hint') }
</span>
<button
className="text-[10px] px-1.5 py-0.5 rounded transition-all"
@@ -324,9 +296,9 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
background: 'rgba(0,0,0,0.1)',
border: '1px solid rgba(0,0,0,0.1)'
} }
title="Apply current color to all letters"
title={ LocalizeText('catalog.prefix.color.apply.all.title') }
onClick={ applyColorToAll }>
Apply to all
{ LocalizeText('catalog.prefix.color.apply.all') }
</button>
</div>
<div className="flex flex-wrap gap-1 p-2 rounded-lg"
@@ -379,25 +351,20 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
<div className="flex flex-col gap-1">
{ colorMode === 'perLetter' && selectedLetterIndex !== null &&
<span className="text-[10px] opacity-50 italic">
Selected letter: &quot;{ prefixText[selectedLetterIndex] || '' }&quot;
{ LocalizeText('catalog.prefix.color.selected') } &quot;{ prefixText[selectedLetterIndex] || '' }&quot;
</span>
}
<div className="grid grid-cols-10 gap-[3px]">
<div className="grid gap-1" style={ { gridTemplateColumns: 'repeat(auto-fill, minmax(34px, 1fr))' } }>
{ PRESET_COLORS.map((color, idx) =>
{
const isActive = currentActiveColor === color;
return (
<div
key={ idx }
className="cursor-pointer transition-all"
className={ `aspect-square rounded cursor-pointer transition-all duration-100 border-2 ${ isActive ? 'scale-110 border-white shadow-lg' : 'border-transparent hover:scale-105' }` }
style={ {
width: '100%',
aspectRatio: '1',
borderRadius: '5px',
backgroundColor: color,
border: isActive ? '2px solid #fff' : '1px solid rgba(0,0,0,0.15)',
boxShadow: isActive ? `0 0 6px ${ color }, 0 0 0 1px rgba(0,0,0,0.2)` : 'inset 0 1px 0 rgba(255,255,255,0.2)',
transform: isActive ? 'scale(1.2)' : 'scale(1)',
boxShadow: isActive ? `0 0 8px ${ color }, 0 0 0 1px rgba(0,0,0,0.3)` : 'inset 0 1px 0 rgba(255,255,255,0.25), 0 1px 2px rgba(0,0,0,0.15)',
zIndex: isActive ? 5 : 1
} }
onClick={ () => handleColorSelect(color) } />
@@ -443,8 +410,8 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
<div className="flex items-center justify-between mt-auto pt-2"
style={ { borderTop: '1px solid rgba(0,0,0,0.1)' } }>
<div className="flex items-center gap-1">
<span className="text-xs opacity-60">Price:</span>
<span className="text-sm font-bold">5 Credits</span>
<span className="text-xs opacity-60">{ LocalizeText('catalog.prefix.price') }</span>
<span className="text-sm font-bold">{ LocalizeText('catalog.prefix.price.amount') }</span>
</div>
<button
className="px-5 py-1.5 rounded-md text-sm font-bold transition-all"
@@ -462,7 +429,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
borderRadius: '6px'
} }
onClick={ handlePurchase }>
{ purchased ? '✓ Purchased!' : 'Purchase' }
{ purchased ? LocalizeText('catalog.prefix.purchased') : LocalizeText('catalog.prefix.purchase') }
</button>
</div>
</div>
@@ -1,7 +1,9 @@
import { FC } from 'react';
import { GetConfigurationValue, ProductTypeEnum } from '../../../../../api';
import { Column, Flex, Grid, LayoutImage, Text } from '../../../../../common';
import { FaEdit, FaPlus } from 'react-icons/fa';
import { GetConfigurationValue, LocalizeText, ProductTypeEnum } from '../../../../../api';
import { Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { useCatalogAdmin } from '../../../CatalogAdminContext';
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
@@ -16,46 +18,87 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const { currentOffer = null, currentPage = null } = useCatalog();
const catalogAdmin = useCatalogAdmin();
const adminMode = catalogAdmin?.adminMode ?? false;
return (
<>
<Grid>
<Column overflow="hidden" size={ 7 }>
{ GetConfigurationValue('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) &&
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<Flex center overflow="hidden" style={ { height: 140 } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 inset-e-1" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</Flex>
<Column grow gap={ 1 }>
<CatalogLimitedItemWidgetView />
<Text grow truncate>{ currentOffer.localizationName }</Text>
<div className="flex justify-between">
<div className="flex flex-col gap-1">
<CatalogSpinnerWidgetView />
</div>
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
<div className="flex flex-col h-full gap-2">
{ /* Admin: quick actions */ }
{ adminMode && !catalogAdmin.editingPageData &&
<div className="flex gap-2">
<button
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } }
>
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
</button>
<button
className="flex items-center gap-1 text-[10px] text-success hover:text-green-800 transition-colors cursor-pointer"
onClick={ () => catalogAdmin.setEditingOffer({ offerId: -1, product: { productClassId: 0, productType: 'i', productCount: 1, extraParam: '' } } as any) }
>
<FaPlus className="text-[10px]" /> { LocalizeText('catalog.admin.offer.new') }
</button>
</div> }
{ /* Product detail card */ }
{ currentOffer &&
<div className="flex gap-0 bg-white rounded border-2 border-card-grid-item-border overflow-hidden">
{ /* Preview area */ }
<div className="w-[140px] min-w-[140px] bg-card-grid-item relative flex items-center justify-center border-r-2 border-card-grid-item-border">
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 right-1 absolute" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) &&
<CatalogAddOnBadgeWidgetView className="scale-2" /> }
</div>
{ /* Product info + purchase */ }
<div className="flex flex-col flex-1 min-w-0 p-2.5 gap-2">
{ /* Title row */ }
<div>
<div className="flex items-start justify-between gap-2">
<Text className="text-[13px]! font-bold text-dark leading-tight">{ currentOffer.localizationName }</Text>
{ adminMode &&
<FaEdit
className="text-primary text-[11px] cursor-pointer hover:text-dark transition-colors shrink-0 mt-0.5"
title={ LocalizeText('catalog.admin.offer.edit') }
onClick={ () => catalogAdmin.setEditingOffer(currentOffer) }
/> }
</div>
{ adminMode &&
<div className="flex items-center gap-1 mt-1 flex-wrap">
<span className="text-[8px] font-mono text-white bg-gray-600 px-1 py-px rounded">ID: { currentOffer.product.productClassId }</span>
<span className="text-[8px] font-mono text-white bg-primary px-1 py-px rounded">Offer: { currentOffer.offerId }</span>
<span className="text-[8px] font-mono text-white bg-secondary px-1 py-px rounded">{ currentOffer.product.productType.toUpperCase() }</span>
</div> }
<CatalogLimitedItemWidgetView />
</div>
{ /* Price */ }
<CatalogTotalPriceWidget />
{ /* Spinner */ }
<CatalogSpinnerWidgetView />
{ /* Actions */ }
<div className="flex gap-1.5 mt-auto">
<CatalogPurchaseWidgetView />
</div>
</div>
</div> }
{ /* Welcome/description card */ }
{ !currentOffer &&
<div className="flex items-center gap-3 p-2.5 bg-white rounded border-2 border-card-grid-item-border">
{ !!page.localization.getImage(1) &&
<img className="w-[70px] h-[70px] object-contain rounded shrink-0" src={ page.localization.getImage(1) } /> }
<Text className="text-[11px]! text-muted" dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</div> }
{ /* Item grid */ }
<div className="flex-1 overflow-auto min-h-0">
{ GetConfigurationValue('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
<CatalogItemGridWidgetView columnCount={ 7 } columnMinHeight={ 50 } columnMinWidth={ 50 } />
</div>
</div>
);
};
@@ -1,5 +1,5 @@
import { FC } from 'react';
import { Column } from '../../../../../common';
import { FaPaw } from 'react-icons/fa';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutPets3View: FC<CatalogLayoutProps> = props =>
@@ -9,17 +9,28 @@ export const CatalogLayoutPets3View: FC<CatalogLayoutProps> = props =>
const imageUrl = page.localization.getImage(1);
return (
<Column grow className="bg-muted rounded text-black p-2" overflow="hidden">
<div className="items-center gap-2">
{ imageUrl && <img alt="" src={ imageUrl } /> }
<div className="fs-5" dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
<div className="flex flex-col h-full gap-2">
{ /* Header card */ }
<div className="flex items-center gap-3 p-2.5 bg-white rounded border-2 border-card-grid-item-border">
{ imageUrl && <img alt="" className="w-[60px] h-[60px] object-contain shrink-0" src={ imageUrl } /> }
<div>
<div className="flex items-center gap-1.5 mb-0.5">
<FaPaw className="text-primary text-xs" />
<span className="text-sm font-bold" dangerouslySetInnerHTML={ { __html: page.localization.getText(1) } } />
</div>
</div>
</div>
<Column grow alignItems="center" overflow="auto">
<div dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } />
</Column>
<div className="flex items-center">
<div className="font-bold " dangerouslySetInnerHTML={ { __html: page.localization.getText(3) } } />
{ /* Content */ }
<div className="flex-1 overflow-auto bg-white rounded border-2 border-card-grid-item-border p-3">
<div className="text-[11px] leading-relaxed" dangerouslySetInnerHTML={ { __html: page.localization.getText(2) } } />
</div>
</Column>
{ /* Footer */ }
{ !!page.localization.getText(3) &&
<div className="p-2 bg-card-grid-item rounded border border-card-grid-item-border">
<span className="text-[11px] font-bold" dangerouslySetInnerHTML={ { __html: page.localization.getText(3) } } />
</div> }
</div>
);
};
@@ -1,6 +1,10 @@
import { FC, useEffect, useState } from 'react';
import { Column, Grid, Text } from '../../../../../common';
import { FaEdit, FaPen, FaPlus, FaTrophy } from 'react-icons/fa';
import { LocalizeText, ProductTypeEnum } from '../../../../../api';
import { Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { useCatalogAdmin } from '../../../CatalogAdminContext';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
@@ -12,6 +16,8 @@ export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
const { page = null } = props;
const [ trophyText, setTrophyText ] = useState<string>('');
const { currentOffer = null, setPurchaseOptions = null } = useCatalog();
const catalogAdmin = useCatalogAdmin();
const adminMode = catalogAdmin?.adminMode ?? false;
useEffect(() =>
{
@@ -27,30 +33,104 @@ export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
});
}, [ currentOffer, trophyText, setPurchaseOptions ]);
const canPurchase = currentOffer && trophyText.trim().length > 0;
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<CatalogItemGridWidgetView />
<textarea className="grow! form-control w-full" defaultValue={ trophyText || '' } onChange={ event => setTrophyText(event.target.value) } />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<CatalogViewProductWidgetView />
<Column grow gap={ 1 }>
<Text grow truncate>{ currentOffer.localizationName }</Text>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" />
</div>
<div className="flex flex-col h-full gap-2">
{ /* Admin: quick actions */ }
{ adminMode && !catalogAdmin.editingPageData &&
<div className="flex gap-2">
<button
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } }
>
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
</button>
<button
className="flex items-center gap-1 text-[10px] text-success hover:text-green-800 transition-colors cursor-pointer"
onClick={ () => catalogAdmin.setEditingOffer({ offerId: -1, product: { productClassId: 0, productType: 'i', productCount: 1, extraParam: '' } } as any) }
>
<FaPlus className="text-[10px]" /> { LocalizeText('catalog.admin.offer.new') }
</button>
</div> }
{ /* Selected trophy card */ }
{ currentOffer
? <div className="flex gap-0 bg-white rounded border-2 border-warning/40 overflow-hidden" style={ { boxShadow: '0 0 8px rgba(255,193,7,0.15)' } }>
{ /* Preview */ }
<div className="w-[120px] min-w-[120px] relative flex items-center justify-center border-r-2 border-warning/30" style={ { background: 'linear-gradient(180deg, #fff9e6 0%, #fff3cc 100%)' } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE)
? <>
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 right-1 absolute" />
</>
: <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</div>
{ /* Info */ }
<div className="flex flex-col flex-1 min-w-0 p-2 gap-1.5">
<div className="flex items-center gap-1.5">
<FaTrophy className="text-warning text-[11px]" />
<Text className="text-[12px]! font-bold text-dark leading-tight">{ currentOffer.localizationName }</Text>
{ adminMode &&
<FaEdit
className="text-primary text-[11px] cursor-pointer hover:text-dark transition-colors shrink-0"
title={ LocalizeText('catalog.admin.offer.edit') }
onClick={ () => catalogAdmin.setEditingOffer(currentOffer) }
/> }
</div>
{ adminMode &&
<div className="flex items-center gap-1 flex-wrap">
<span className="text-[8px] font-mono text-white bg-gray-600 px-1 py-px rounded">ID: { currentOffer.product.productClassId }</span>
<span className="text-[8px] font-mono text-white bg-primary px-1 py-px rounded">Offer: { currentOffer.offerId }</span>
</div> }
<CatalogTotalPriceWidget />
{ !canPurchase &&
<span className="text-[9px] text-warning italic">{ LocalizeText('catalog.trophies.write.hint') }</span> }
<div className="flex gap-1.5 mt-auto">
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</div>
</div>
</div>
: <div className="flex items-start gap-3 p-2.5 bg-white rounded border-2 border-card-grid-item-border">
{ !!page.localization.getImage(1) &&
<img className="w-[50px] h-[50px] object-contain rounded shrink-0 mt-0.5" src={ page.localization.getImage(1) } /> }
<div className="min-w-0">
<div className="flex items-center gap-1.5 mb-1">
<FaTrophy className="text-warning text-[11px]" />
<span className="text-[12px] font-bold">{ LocalizeText('catalog.trophies.title') }</span>
</div>
<Text className="text-[10px]! text-muted leading-relaxed" dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</div>
</div> }
{ /* Trophy inscription */ }
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1.5">
<FaPen className="text-[8px] text-warning" />
<span className="text-[9px] font-bold text-muted uppercase tracking-wider">{ LocalizeText('catalog.trophies.inscription') }</span>
<span className={ `text-[9px] ml-auto ${ trophyText.length > 180 ? 'text-danger font-bold' : 'text-muted' }` }>{ trophyText.length }/200</span>
</div>
<div className="relative">
<textarea
className="w-full h-[60px] text-[11px] rounded p-2 pr-3 resize-none focus:outline-none transition-all border-2"
maxLength={ 200 }
placeholder={ LocalizeText('catalog.trophies.inscription.placeholder') }
style={ {
background: trophyText.length > 0 ? 'linear-gradient(180deg, #fffdf5 0%, #fff8e8 100%)' : '#fff',
borderColor: trophyText.length > 0 ? 'rgba(255,193,7,0.4)' : undefined
} }
value={ trophyText }
onChange={ event => setTrophyText(event.target.value) }
/>
{ trophyText.length > 0 &&
<FaTrophy className="absolute top-2 right-2 text-[10px] text-warning/30" /> }
</div>
</div>
{ /* Trophy grid */ }
<div className="flex-1 overflow-auto min-h-0">
<CatalogItemGridWidgetView columnCount={ 7 } columnMinHeight={ 50 } columnMinWidth={ 50 } />
</div>
</div>
);
};
@@ -1,12 +1,12 @@
import { ApproveNameMessageComposer, ApproveNameMessageEvent, ColorConverter, GetSellablePetPalettesComposer, PurchaseFromCatalogComposer, SellablePetPaletteData } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaFillDrip } from 'react-icons/fa';
import { FaCheck, FaEdit, FaFillDrip, FaPaw, FaPlus, FaTimes } from 'react-icons/fa';
import { DispatchUiEvent, GetPetAvailableColors, GetPetIndexFromLocalization, LocalizeText, SendMessageComposer } from '../../../../../../api';
import { AutoGrid, Button, Column, Grid, LayoutGridItem, LayoutPetImageView, Text } from '../../../../../../common';
import { LayoutGridItem, LayoutPetImageView } from '../../../../../../common';
import { CatalogPurchaseFailureEvent } from '../../../../../../events';
import { useCatalog, useMessageEvent } from '../../../../../../hooks';
import { useCatalogAdmin } from '../../../../CatalogAdminContext';
import { CatalogAddOnBadgeWidgetView } from '../../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogPurchaseWidgetView } from '../../widgets/CatalogPurchaseWidgetView';
import { CatalogTotalPriceWidget } from '../../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from '../CatalogLayout.types';
@@ -24,6 +24,8 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
const [ approvalPending, setApprovalPending ] = useState(true);
const [ approvalResult, setApprovalResult ] = useState(-1);
const { currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null, catalogOptions = null, roomPreviewer = null } = useCatalog();
const catalogAdmin = useCatalogAdmin();
const adminMode = catalogAdmin?.adminMode ?? false;
const { petPalettes = null } = catalogOptions;
const getColor = useMemo(() =>
@@ -194,50 +196,131 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
if(!currentOffer) return null;
return (
<Grid>
<Column overflow="hidden" size={ 7 }>
<AutoGrid columnCount={ 5 }>
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) =>
{
return (
<LayoutGridItem key={ index } itemActive={ (selectedPaletteIndex === index) } onClick={ event => setSelectedPaletteIndex(index) }>
<LayoutPetImageView direction={ 2 } headOnly={ true } paletteId={ palette.paletteId } typeId={ petIndex } />
</LayoutGridItem>
);
}) }
{ colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => <LayoutGridItem key={ index } itemHighlight className="clear-bg" itemActive={ (selectedColorIndex === index) } itemColor={ ColorConverter.int2rgb(colorSet[0]) } onClick={ event => setSelectedColorIndex(index) } />) }
</AutoGrid>
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) && <img alt="" src={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } />
</> }
{ currentOffer &&
<>
<div className="relative overflow-hidden">
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 inset-e-1" position="absolute" />
{ ((petIndex > -1) && (petIndex <= 7)) &&
<Button className="bottom-1 inset-s-1" position="absolute" onClick={ event => setColorsShowing(!colorsShowing) }>
<FaFillDrip className="fa-icon" />
</Button> }
<div className="flex flex-col h-full gap-2">
{ /* Admin: quick actions */ }
{ adminMode && !catalogAdmin.editingPageData &&
<div className="flex gap-2">
<button
className="flex items-center gap-1 text-[10px] text-primary hover:text-dark transition-colors cursor-pointer"
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } }
>
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
</button>
<button
className="flex items-center gap-1 text-[10px] text-success hover:text-green-800 transition-colors cursor-pointer"
onClick={ () => catalogAdmin.setEditingOffer({ offerId: -1, product: { productClassId: 0, productType: 'i', productCount: 1, extraParam: '' } } as any) }
>
<FaPlus className="text-[10px]" /> { LocalizeText('catalog.admin.offer.new') }
</button>
</div> }
{ /* Top card: preview + name + purchase */ }
<div className="flex gap-3 p-2.5 bg-white rounded border-2 border-card-grid-item-border">
{ /* Pet preview */ }
<div className="w-[160px] min-w-[160px] h-[140px] rounded overflow-hidden bg-card-grid-item relative flex items-center justify-center border border-card-grid-item-border">
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded absolute bottom-1 right-1" />
{ ((petIndex > -1) && (petIndex <= 7)) &&
<button
className={ `absolute bottom-1 left-1 w-[28px] h-[28px] rounded flex items-center justify-center cursor-pointer transition-all border ${ colorsShowing ? 'bg-primary text-white border-primary' : 'bg-white text-dark border-card-grid-item-border hover:bg-card-grid-item-active' }` }
title={ LocalizeText('catalog.pets.show.colors') }
onClick={ () => setColorsShowing(!colorsShowing) }
>
<FaFillDrip className="text-[10px]" />
</button> }
</div>
{ /* Pet info */ }
<div className="flex flex-col flex-1 justify-between min-w-0">
<div>
<div className="flex items-center gap-1.5">
<FaPaw className="text-primary text-xs" />
<span className="text-sm font-bold">{ petBreedName || LocalizeText('catalog.pet.breed') }</span>
{ adminMode && currentOffer &&
<FaEdit
className="text-primary text-[11px] cursor-pointer hover:text-dark transition-colors shrink-0"
title={ LocalizeText('catalog.admin.offer.edit') }
onClick={ () => catalogAdmin.setEditingOffer(currentOffer) }
/> }
</div>
<Column grow gap={ 1 }>
<Text truncate>{ petBreedName }</Text>
<Column grow gap={ 1 }>
<input className="min-h-[calc(1.5em+ .5rem+2px)] px-[.5rem] py-[.25rem] rounded-[.2rem] form-control-sm w-full" placeholder={ LocalizeText('widgets.petpackage.name.title') } type="text" value={ petName } onChange={ event => setPetName(event.target.value) } />
{ (approvalResult > 0) &&
<div className="invalid-feedback d-block m-0">{ validationErrorMessage }</div> }
</Column>
<div className="flex justify-end">
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView purchaseCallback={ purchasePet } />
</Column>
</> }
</Column>
</Grid>
{ adminMode && currentOffer &&
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
<span className="text-[8px] font-mono text-white bg-gray-600 px-1 py-px rounded">ID: { currentOffer.product.productClassId }</span>
<span className="text-[8px] font-mono text-white bg-primary px-1 py-px rounded">Offer: { currentOffer.offerId }</span>
</div> }
{ !!page.localization.getText(0) &&
<p className="text-[10px] text-muted mt-0.5" dangerouslySetInnerHTML={ { __html: page.localization.getText(0) } } /> }
</div>
{ /* Name input */ }
<div className="flex flex-col gap-1 mt-2">
<label className="text-[9px] text-muted uppercase font-bold">{ LocalizeText('widgets.petpackage.name.title') }</label>
<div className="relative">
<input
className={ `w-full text-[11px] border-2 rounded px-2 py-1.5 focus:outline-none transition-colors ${ approvalResult > 0 ? 'border-danger bg-danger/5' : approvalResult === 0 ? 'border-success bg-success/5' : 'border-card-grid-item-border focus:border-primary bg-white' }` }
placeholder={ LocalizeText('widgets.petpackage.name.title') }
type="text"
value={ petName }
onChange={ event => setPetName(event.target.value) }
/>
{ approvalResult === 0 &&
<FaCheck className="absolute right-2 top-1/2 -translate-y-1/2 text-success text-[10px]" /> }
{ approvalResult > 0 &&
<FaTimes className="absolute right-2 top-1/2 -translate-y-1/2 text-danger text-[10px]" /> }
</div>
{ (approvalResult > 0) &&
<span className="text-[10px] text-danger font-medium">{ validationErrorMessage }</span> }
</div>
{ /* Price + buy */ }
<div className="flex items-center justify-between mt-2">
<CatalogTotalPriceWidget />
<button
className="px-3 py-1 rounded text-[11px] font-bold bg-primary text-white hover:bg-secondary transition-colors cursor-pointer disabled:opacity-50"
disabled={ !petName.length || (approvalResult > 0) }
onClick={ purchasePet }
>
{ approvalResult === -1 ? LocalizeText('catalog.purchase_confirmation.buy') : LocalizeText('catalog.marketplace.confirm_title') }
</button>
</div>
</div>
</div>
{ /* Breed/Color grid */ }
<div className="flex-1 overflow-auto min-h-0">
<div className="flex items-center gap-1.5 mb-1.5">
<span className="text-[10px] font-bold text-muted uppercase tracking-wide">
{ colorsShowing ? LocalizeText('catalog.pets.choose.color') : LocalizeText('catalog.pets.choose.breed') }
</span>
{ colorsShowing &&
<button
className="text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
onClick={ () => setColorsShowing(false) }
>
{ LocalizeText('catalog.pets.back.breeds') }
</button> }
</div>
<div className="grid grid-cols-6 gap-1">
{ !colorsShowing && (sellablePalettes.length > 0) && sellablePalettes.map((palette, index) => (
<LayoutGridItem
key={ index }
className="group/pet"
itemActive={ (selectedPaletteIndex === index) }
onClick={ () => setSelectedPaletteIndex(index) }
>
<LayoutPetImageView direction={ 2 } headOnly={ true } paletteId={ palette.paletteId } typeId={ petIndex } />
</LayoutGridItem>
)) }
{ colorsShowing && (sellableColors.length > 0) && sellableColors.map((colorSet, index) => (
<div
key={ index }
className={ `w-full aspect-square rounded border-2 cursor-pointer transition-all ${ selectedColorIndex === index ? 'border-primary scale-110 shadow-md' : 'border-card-grid-item-border hover:border-primary/50' }` }
style={ { backgroundColor: `#${ ColorConverter.int2rgb(colorSet[0]) }` } }
onClick={ () => setSelectedColorIndex(index) }
/>
)) }
</div>
</div>
</div>
);
};
@@ -1,7 +1,8 @@
import { FC, useEffect, useRef } from 'react';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { IPurchasableOffer, ProductTypeEnum } from '../../../../../api';
import { AutoGrid, AutoGridProps } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
import { useCatalogAdmin } from '../../../CatalogAdminContext';
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
interface CatalogItemGridWidgetViewProps extends AutoGridProps
@@ -13,7 +14,11 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
{
const { columnCount = 5, children = null, ...rest } = props;
const { currentOffer = null, setCurrentOffer = null, currentPage = null, setPurchaseOptions = null } = useCatalog();
const catalogAdmin = useCatalogAdmin();
const adminMode = catalogAdmin?.adminMode ?? false;
const elementRef = useRef<HTMLDivElement>();
const [ dragIndex, setDragIndex ] = useState<number | null>(null);
const [ dropIndex, setDropIndex ] = useState<number | null>(null);
useEffect(() =>
{
@@ -43,9 +48,66 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
}
};
const handleDragStart = useCallback((index: number) =>
{
setDragIndex(index);
}, []);
const handleDragOver = useCallback((e: React.DragEvent, index: number) =>
{
e.preventDefault();
setDropIndex(index);
}, []);
const handleDrop = useCallback((index: number) =>
{
if(dragIndex !== null && dragIndex !== index && currentPage?.offers)
{
const offers = [ ...currentPage.offers ];
const [ moved ] = offers.splice(dragIndex, 1);
offers.splice(index, 0, moved);
const orders = offers.map((o, i) => ({ id: o.offerId, orderNumber: i }));
catalogAdmin?.reorderOffers(orders);
}
setDragIndex(null);
setDropIndex(null);
}, [ dragIndex, currentPage, catalogAdmin ]);
const handleDragEnd = useCallback(() =>
{
setDragIndex(null);
setDropIndex(null);
}, []);
return (
<AutoGrid columnCount={ columnCount } innerRef={ elementRef } { ...rest }>
{ currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) => <CatalogGridOfferView key={ index } itemActive={ (currentOffer && (currentOffer.offerId === offer.offerId)) } offer={ offer } selectOffer={ selectOffer } />) }
{ currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) =>
{
const isDragging = dragIndex === index;
const isDropTarget = dropIndex === index && dragIndex !== index;
return (
<div
key={ index }
className={ `${ isDragging ? 'opacity-40' : '' } ${ isDropTarget ? 'ring-2 ring-primary ring-offset-1 rounded' : '' }` }
draggable={ adminMode }
onDragEnd={ adminMode ? handleDragEnd : undefined }
onDragOver={ adminMode ? (e) => handleDragOver(e, index) : undefined }
onDragStart={ adminMode ? () => handleDragStart(index) : undefined }
onDrop={ adminMode ? () => handleDrop(index) : undefined }
>
<CatalogGridOfferView
itemActive={ (currentOffer && (currentOffer.offerId === offer.offerId)) }
offer={ offer }
selectOffer={ selectOffer }
/>
</div>
);
}) }
{ children }
</AutoGrid>
);
@@ -19,19 +19,19 @@ export const CatalogPriceDisplayWidgetView: FC<CatalogPriceDisplayWidgetViewProp
if(!offer) return null;
return (
<>
<div className="flex items-center gap-1.5">
{ (offer.priceInCredits > 0) &&
<div className="flex items-center gap-1">
<Text bold>{ (offer.priceInCredits * quantity) }</Text>
<div className="flex items-center gap-1 bg-warning/15 border border-warning/40 rounded-full px-2 py-0.5">
<Text className="text-[11px]! font-bold text-dark">{ (offer.priceInCredits * quantity) }</Text>
<LayoutCurrencyIcon type={ -1 } />
</div> }
{ separator && (offer.priceInCredits > 0) && (offer.priceInActivityPoints > 0) &&
<FaPlus className="fa-icon" color="black" size="xs" /> }
<FaPlus className="text-[7px] text-muted" /> }
{ (offer.priceInActivityPoints > 0) &&
<div className="flex items-center gap-1">
<Text bold>{ (offer.priceInActivityPoints * quantity) }</Text>
<div className="flex items-center gap-1 bg-purple/15 border border-purple/40 rounded-full px-2 py-0.5">
<Text className="text-[11px]! font-bold text-dark">{ (offer.priceInActivityPoints * quantity) }</Text>
<LayoutCurrencyIcon type={ offer.activityPointType } />
</div> }
</>
</div>
);
};
@@ -1,11 +1,10 @@
import { FC } from 'react';
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { FaMinus, FaPlus } from 'react-icons/fa';
import { LocalizeText } from '../../../../../api';
import { Text } from '../../../../../common';
import { useCatalog } from '../../../../../hooks';
const MIN_VALUE: number = 1;
const MAX_VALUE: number = 100;
const MAX_VALUE: number = 99;
export const CatalogSpinnerWidgetView: FC<{}> = props =>
{
@@ -34,13 +33,28 @@ export const CatalogSpinnerWidgetView: FC<{}> = props =>
if(!currentOffer || !currentOffer.bundlePurchaseAllowed) return null;
return (
<>
<Text>{ LocalizeText('catalog.bundlewidget.spinner.select.amount') }</Text>
<div className="flex items-center gap-1">
<FaCaretLeft className="text-black cursor-pointer fa-icon" onClick={ event => updateQuantity(quantity - 1) } />
<input className="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none min-h-[17px] h-[17px] w-[28px] px-[4px] py-0 text-right rounded-[.2rem]" type="number" value={ quantity } onChange={ event => updateQuantity(event.target.valueAsNumber) } />
<FaCaretRight className="text-black cursor-pointer fa-icon" onClick={ event => updateQuantity(quantity + 1) } />
<div className="flex items-center gap-1.5">
<span className="text-[10px] text-muted whitespace-nowrap">{ LocalizeText('catalog.bundlewidget.spinner.select.amount') }</span>
<div className="flex items-center rounded overflow-hidden border-2 border-card-grid-item-border">
<button
className="w-[24px] h-[24px] flex items-center justify-center bg-card-grid-item hover:bg-card-grid-item-active transition-colors cursor-pointer border-r border-card-grid-item-border"
onClick={ event => updateQuantity(quantity - 1) }
>
<FaMinus className="text-[7px] text-dark" />
</button>
<input
className="w-[40px] h-[24px] text-center text-[11px] font-bold bg-white border-x border-card-grid-item-border [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none focus:outline-none"
type="number"
value={ quantity }
onChange={ event => updateQuantity(event.target.valueAsNumber) }
/>
<button
className="w-[24px] h-[24px] flex items-center justify-center bg-card-grid-item hover:bg-card-grid-item-active transition-colors cursor-pointer border-l border-card-grid-item-border"
onClick={ event => updateQuantity(quantity + 1) }
>
<FaPlus className="text-[7px] text-dark" />
</button>
</div>
</>
</div>
);
};