mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆕 Added New catalogue page
This commit is contained in:
@@ -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: "{ prefixText[selectedLetterIndex] || '' }"
|
||||
{ LocalizeText('catalog.prefix.color.selected') } "{ prefixText[selectedLetterIndex] || '' }"
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user