Revert "Merge branch 'main' into furnisettingeditor-pr"

This reverts commit dfbfb1c2c1, reversing
changes made to 07702c44d0.
This commit is contained in:
duckietm
2026-03-23 11:49:05 +01:00
parent ac8da5827b
commit a0d0b5c4a4
50 changed files with 4746 additions and 3327 deletions
@@ -1,6 +1,7 @@
import { PurchasePrefixComposer } from '@nitrots/nitro-renderer';
import { createPortal } from 'react-dom';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { LocalizeText, SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api';
import { 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';
@@ -31,6 +32,32 @@ 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(() =>
{
@@ -77,7 +104,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: color }));
setCustomColorInput(color);
// Auto-avanza alla lettera successiva
// Auto-advance to next letter
if(selectedLetterIndex < prefixText.length - 1)
{
const nextIdx = selectedLetterIndex + 1;
@@ -167,12 +194,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">{ LocalizeText('catalog.prefix.text') }</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">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={ LocalizeText('catalog.prefix.text.placeholder') }
placeholder="Enter text..."
style={ {
background: 'rgba(0,0,0,0.15)',
border: '1px solid rgba(0,0,0,0.15)',
@@ -187,7 +214,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">{ LocalizeText('catalog.prefix.icon') }</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">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]"
@@ -205,7 +232,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={ LocalizeText('catalog.prefix.icon.remove') }
title="Remove icon"
onClick={ () => setSelectedIcon('') }>
</button>
@@ -214,14 +241,14 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
</div>
</div>
{ /* Emoji Picker (emoji-mart) - fixed overlay */ }
{ showIconPicker && (
{ /* Emoji Picker (emoji-mart) - portaled to body, no backdrop */ }
{ showIconPicker && createPortal(
<>
<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)' } }>
<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' } }>
<Picker
data={ data }
locale="it"
locale="en"
onEmojiSelect={ (emoji: { native: string }) => { setSelectedIcon(emoji.native); setShowIconPicker(false); } }
theme="dark"
previewPosition="none"
@@ -234,12 +261,13 @@ 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">{ LocalizeText('catalog.prefix.effect') }</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">Effect</label>
<div className="flex flex-wrap gap-1">
{ PRESET_PREFIX_EFFECTS.map(fx => (
<button
@@ -259,7 +287,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">{ LocalizeText('catalog.prefix.color') }</label>
<label className="text-[11px] font-bold uppercase tracking-wider opacity-60">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"
@@ -269,7 +297,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
opacity: colorMode === 'single' ? 1 : 0.6
} }
onClick={ () => { setColorMode('single'); setSelectedLetterIndex(null); } }>
{ LocalizeText('catalog.prefix.color.single') }
🎨 Single
</button>
<button
className="flex-1 px-2 py-1.5 text-xs font-bold transition-all"
@@ -278,7 +306,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
opacity: colorMode === 'perLetter' ? 1 : 0.6
} }
onClick={ () => { setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0); } }>
{ LocalizeText('catalog.prefix.color.per.letter') }
🌈 Per Letter
</button>
</div>
</div>
@@ -288,7 +316,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">
{ LocalizeText('catalog.prefix.color.hint') }
Select a letter, then choose a color. Auto-advances.
</span>
<button
className="text-[10px] px-1.5 py-0.5 rounded transition-all"
@@ -296,9 +324,9 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
background: 'rgba(0,0,0,0.1)',
border: '1px solid rgba(0,0,0,0.1)'
} }
title={ LocalizeText('catalog.prefix.color.apply.all.title') }
title="Apply current color to all letters"
onClick={ applyColorToAll }>
{ LocalizeText('catalog.prefix.color.apply.all') }
Apply to all
</button>
</div>
<div className="flex flex-wrap gap-1 p-2 rounded-lg"
@@ -351,20 +379,25 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
<div className="flex flex-col gap-1">
{ colorMode === 'perLetter' && selectedLetterIndex !== null &&
<span className="text-[10px] opacity-50 italic">
{ LocalizeText('catalog.prefix.color.selected') } &quot;{ prefixText[selectedLetterIndex] || '' }&quot;
Selected letter: &quot;{ prefixText[selectedLetterIndex] || '' }&quot;
</span>
}
<div className="grid gap-1" style={ { gridTemplateColumns: 'repeat(auto-fill, minmax(34px, 1fr))' } }>
<div className="grid grid-cols-10 gap-[3px]">
{ PRESET_COLORS.map((color, idx) =>
{
const isActive = currentActiveColor === color;
return (
<div
key={ idx }
className={ `aspect-square rounded cursor-pointer transition-all duration-100 border-2 ${ isActive ? 'scale-110 border-white shadow-lg' : 'border-transparent hover:scale-105' }` }
className="cursor-pointer transition-all"
style={ {
width: '100%',
aspectRatio: '1',
borderRadius: '5px',
backgroundColor: color,
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)',
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)',
zIndex: isActive ? 5 : 1
} }
onClick={ () => handleColorSelect(color) } />
@@ -410,8 +443,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">{ LocalizeText('catalog.prefix.price') }</span>
<span className="text-sm font-bold">{ LocalizeText('catalog.prefix.price.amount') }</span>
<span className="text-xs opacity-60">Price:</span>
<span className="text-sm font-bold">5 Credits</span>
</div>
<button
className="px-5 py-1.5 rounded-md text-sm font-bold transition-all"
@@ -429,7 +462,7 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
borderRadius: '6px'
} }
onClick={ handlePurchase }>
{ purchased ? LocalizeText('catalog.prefix.purchased') : LocalizeText('catalog.prefix.purchase') }
{ purchased ? '✓ Purchased!' : 'Purchase' }
</button>
</div>
</div>
@@ -1,9 +1,7 @@
import { FC } from 'react';
import { FaEdit, FaPlus } from 'react-icons/fa';
import { GetConfigurationValue, LocalizeText, ProductTypeEnum } from '../../../../../api';
import { Text } from '../../../../../common';
import { GetConfigurationValue, ProductTypeEnum } from '../../../../../api';
import { Column, Flex, Grid, LayoutImage, 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';
@@ -18,87 +16,46 @@ 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 (
<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>
<>
<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>
</>
);
};
@@ -1,5 +1,5 @@
import { FC } from 'react';
import { FaPaw } from 'react-icons/fa';
import { Column } from '../../../../../common';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutPets3View: FC<CatalogLayoutProps> = props =>
@@ -9,28 +9,17 @@ export const CatalogLayoutPets3View: FC<CatalogLayoutProps> = props =>
const imageUrl = page.localization.getImage(1);
return (
<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>
<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>
{ /* 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) } } />
<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) } } />
</div>
{ /* 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>
</Column>
);
};
@@ -1,10 +1,6 @@
import { FC, useEffect, useState } from 'react';
import { FaEdit, FaPen, FaPlus, FaTrophy } from 'react-icons/fa';
import { LocalizeText, ProductTypeEnum } from '../../../../../api';
import { Text } from '../../../../../common';
import { Column, Grid, 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';
@@ -16,8 +12,6 @@ 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(() =>
{
@@ -33,104 +27,30 @@ export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
});
}, [ currentOffer, trophyText, setPurchaseOptions ]);
const canPurchase = currentOffer && trophyText.trim().length > 0;
return (
<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">
<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>
<CatalogPurchaseWidgetView />
</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>
</Column>
</> }
</Column>
</Grid>
);
};
@@ -1,12 +1,12 @@
import { ApproveNameMessageComposer, ApproveNameMessageEvent, ColorConverter, GetSellablePetPalettesComposer, PurchaseFromCatalogComposer, SellablePetPaletteData } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaCheck, FaEdit, FaFillDrip, FaPaw, FaPlus, FaTimes } from 'react-icons/fa';
import { FaFillDrip } from 'react-icons/fa';
import { DispatchUiEvent, GetPetAvailableColors, GetPetIndexFromLocalization, LocalizeText, SendMessageComposer } from '../../../../../../api';
import { LayoutGridItem, LayoutPetImageView } from '../../../../../../common';
import { AutoGrid, Button, Column, Grid, LayoutGridItem, LayoutPetImageView, Text } 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,8 +24,6 @@ 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(() =>
@@ -196,131 +194,50 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
if(!currentOffer) return null;
return (
<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) }
/> }
<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>
{ 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>
<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>
);
};