🆕 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
@@ -0,0 +1,225 @@
import { FC, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa';
import { LocalizeText } from '../../../../api';
import { useCatalog } from '../../../../hooks';
import { IOfferEditData, useCatalogAdmin } from '../../CatalogAdminContext';
export const CatalogAdminOfferEditView: FC<{}> = () =>
{
const { currentPage = null } = useCatalog();
const catalogAdmin = useCatalogAdmin();
const editingOffer = catalogAdmin?.editingOffer ?? null;
const setEditingOffer = catalogAdmin?.setEditingOffer;
const saveOffer = catalogAdmin?.saveOffer;
const deleteOffer = catalogAdmin?.deleteOffer;
const createOffer = catalogAdmin?.createOffer;
const loading = catalogAdmin?.loading ?? false;
const [ itemIds, setItemIds ] = useState('0');
const [ catalogName, setCatalogName ] = useState('');
const [ costCredits, setCostCredits ] = useState(0);
const [ costPoints, setCostPoints ] = useState(0);
const [ pointsType, setPointsType ] = useState(0);
const [ amount, setAmount ] = useState(1);
const [ clubOnly, setClubOnly ] = useState('0');
const [ extradata, setExtradata ] = useState('');
const [ haveOffer, setHaveOffer ] = useState('1');
const [ offerId, setOfferIdGroup ] = useState(-1);
const [ limitedStack, setLimitedStack ] = useState(0);
const [ orderNumber, setOrderNumber ] = useState(0);
const [ isNew, setIsNew ] = useState(false);
useEffect(() =>
{
if(!editingOffer) return;
if(editingOffer.offerId === -1)
{
setIsNew(true);
setItemIds('0');
setCatalogName('');
setCostCredits(0);
setCostPoints(0);
setPointsType(0);
setAmount(1);
setClubOnly('0');
setExtradata('');
setHaveOffer('1');
setOfferIdGroup(-1);
setLimitedStack(0);
setOrderNumber(0);
}
else
{
setIsNew(false);
setItemIds(String(editingOffer.product?.productClassId || 0));
setCatalogName(editingOffer.localizationName || '');
setCostCredits(editingOffer.priceInCredits);
setCostPoints(editingOffer.priceInActivityPoints);
setPointsType(editingOffer.activityPointType);
setAmount(editingOffer.product?.productCount || 1);
setClubOnly(editingOffer.clubLevel > 0 ? '1' : '0');
setExtradata(editingOffer.product?.extraParam || '');
setHaveOffer('1');
setOfferIdGroup(editingOffer.offerId || -1);
setLimitedStack(0);
setOrderNumber(0);
}
}, [ editingOffer ]);
if(!editingOffer) return null;
const handleSave = async () =>
{
if(!saveOffer || !createOffer) return;
const data: IOfferEditData = {
offerId: isNew ? undefined : editingOffer.offerId,
pageId: currentPage?.pageId || 0,
itemIds,
catalogName,
costCredits,
costPoints,
pointsType,
amount,
clubOnly,
extradata,
haveOffer,
offerId_group: offerId,
limitedStack,
orderNumber
};
const success = isNew ? await createOffer(data) : await saveOffer(data);
if(success && setEditingOffer) setEditingOffer(null);
};
const handleDelete = () =>
{
if(isNew || !deleteOffer || !confirm(LocalizeText('catalog.admin.delete.offer.confirm'))) return;
deleteOffer(editingOffer.offerId);
if(setEditingOffer) setEditingOffer(null);
};
const inputClass = 'text-[11px] border-2 border-card-grid-item-border rounded px-2 py-1 bg-white focus:outline-none focus:border-primary transition-colors';
return createPortal(
<div className="fixed inset-0 flex items-center justify-center" style={ { zIndex: 1000 } } onClick={ () => setEditingOffer(null) }>
<div className="absolute inset-0 bg-black/30 backdrop-blur-[1px]" />
<div className="relative bg-light rounded-lg w-[420px] border-2 border-card-border overflow-hidden shadow-lg" onClick={ e => e.stopPropagation() }>
{ /* Header */ }
<div className="flex items-center justify-between px-3 py-2 bg-card-header">
<span className="text-sm font-bold text-white">
{ isNew ? LocalizeText('catalog.admin.offer.new') : `${ LocalizeText('catalog.admin.offer.edit') } #${ editingOffer.offerId }` }
</span>
<div className="cursor-pointer" onClick={ () => setEditingOffer(null) }>
<FaTimes className="text-white/70 hover:text-white text-xs" />
</div>
</div>
<div className="p-3 flex flex-col gap-2.5">
{ /* Current name */ }
{ !isNew &&
<div className="text-[10px] text-muted bg-card-grid-item rounded px-2.5 py-1 font-mono border border-card-grid-item-border">
{ editingOffer.localizationName }
</div> }
{ /* Catalog Name */ }
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-primary uppercase font-bold">{ LocalizeText('catalog.admin.offer.name') }</label>
<input className={ inputClass } placeholder="es. rare_dragon_lamp" type="text" value={ catalogName } onChange={ e => setCatalogName(e.target.value) } />
</div>
{ /* Generale */ }
<div className="bg-white rounded border-2 border-card-grid-item-border p-2.5">
<div className="text-[9px] text-primary uppercase font-bold mb-1.5">{ LocalizeText('catalog.admin.offer.general') }</div>
<div className="grid grid-cols-3 gap-1.5">
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">Item IDs</label>
<input className={ inputClass } placeholder="1234" type="text" value={ itemIds } onChange={ e => setItemIds(e.target.value) } />
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">{ LocalizeText('catalog.admin.offer.quantity') }</label>
<input className={ inputClass } min={ 1 } type="number" value={ amount } onChange={ e => setAmount(parseInt(e.target.value) || 1) } />
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">{ LocalizeText('catalog.admin.order') }</label>
<input className={ inputClass } min={ 0 } type="number" value={ orderNumber } onChange={ e => setOrderNumber(parseInt(e.target.value) || 0) } />
</div>
</div>
</div>
{ /* Prezzi */ }
<div className="bg-white rounded border-2 border-card-grid-item-border p-2.5">
<div className="text-[9px] text-primary uppercase font-bold mb-1.5">{ LocalizeText('catalog.admin.offer.prices') }</div>
<div className="grid grid-cols-3 gap-1.5">
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">{ LocalizeText('catalog.admin.offer.credits') }</label>
<input className={ inputClass } min={ 0 } type="number" value={ costCredits } onChange={ e => setCostCredits(parseInt(e.target.value) || 0) } />
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">{ LocalizeText('catalog.admin.offer.points') }</label>
<input className={ inputClass } min={ 0 } type="number" value={ costPoints } onChange={ e => setCostPoints(parseInt(e.target.value) || 0) } />
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">{ LocalizeText('catalog.admin.offer.points.type') }</label>
<select className={ inputClass } value={ pointsType } onChange={ e => setPointsType(parseInt(e.target.value)) }>
<option value={ 0 }>Duckets</option>
<option value={ 5 }>Diamonds</option>
<option value={ 101 }>Seasonal</option>
</select>
</div>
</div>
</div>
{ /* Opzioni */ }
<div className="bg-white rounded border-2 border-card-grid-item-border p-2.5">
<div className="text-[9px] text-primary uppercase font-bold mb-1.5">{ LocalizeText('catalog.admin.offer.options') }</div>
<div className="grid grid-cols-3 gap-1.5 mb-1.5">
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">{ LocalizeText('catalog.admin.offer.club.only') }</label>
<select className={ inputClass } value={ clubOnly } onChange={ e => setClubOnly(e.target.value) }>
<option value="0">No</option>
<option value="1">Si</option>
</select>
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">Limited Stack</label>
<input className={ inputClass } min={ 0 } type="number" value={ limitedStack } onChange={ e => setLimitedStack(parseInt(e.target.value) || 0) } />
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">Offer ID</label>
<input className={ inputClass } type="number" value={ offerId } onChange={ e => setOfferIdGroup(parseInt(e.target.value) || -1) } />
</div>
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted">{ LocalizeText('catalog.admin.offer.extradata') }</label>
<input className={ inputClass } placeholder="dati extra (opzionale)" type="text" value={ extradata } onChange={ e => setExtradata(e.target.value) } />
</div>
<div className="flex items-center gap-1.5 mt-1.5">
<input className="accent-primary" checked={ haveOffer === '1' } id="haveOffer" type="checkbox" onChange={ e => setHaveOffer(e.target.checked ? '1' : '0') } />
<label className="text-[10px] cursor-pointer" htmlFor="haveOffer">{ LocalizeText('catalog.admin.offer.have.offer') }</label>
</div>
</div>
{ /* Actions */ }
<div className="flex justify-between">
{ !isNew
? <button className="flex items-center gap-1 px-2 py-1 rounded text-[10px] font-bold bg-danger/10 text-danger border border-danger/30 hover:bg-danger/20 transition-colors cursor-pointer" onClick={ handleDelete }>
<FaTrash className="text-[8px]" /> { LocalizeText('catalog.admin.delete') }
</button>
: <div /> }
<button className="flex items-center gap-1 px-3 py-1 rounded text-[10px] font-bold bg-primary text-white hover:bg-secondary transition-colors cursor-pointer disabled:opacity-50" disabled={ loading } onClick={ handleSave }>
{ loading ? <FaSpinner className="text-[8px] animate-spin" /> : <FaSave className="text-[8px]" /> } { isNew ? LocalizeText('catalog.admin.create') : LocalizeText('catalog.admin.save') }
</button>
</div>
</div>
</div>
</div>,
document.body
);
};
@@ -0,0 +1,153 @@
import { FC, useEffect, useState } from 'react';
import { FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa';
import { LocalizeText } from '../../../../api';
import { useCatalog } from '../../../../hooks';
import { IPageEditData, useCatalogAdmin } from '../../CatalogAdminContext';
const LAYOUT_OPTIONS = [
'default_3x3', 'frontpage4', 'pets', 'pets2', 'pets3',
'spaces_new', 'soundmachine', 'trophies', 'roomads',
'guild_frontpage', 'guild_forum', 'guild_custom_furni',
'vip_buy', 'marketplace', 'marketplace_own_items',
'recycler', 'recycler_info', 'recycler_prizes',
'info_loyalty', 'badge_display', 'bots', 'single_bundle',
'color_grouping', 'recent_purchases', 'custom_prefix'
];
export const CatalogAdminPageEditView: FC<{}> = () =>
{
const { currentPage = null, activeNodes = [], rootNode = null } = useCatalog();
const catalogAdmin = useCatalogAdmin();
const editingPageData = catalogAdmin?.editingPageData ?? false;
const editingRootPage = catalogAdmin?.editingRootPage ?? false;
const editingPageNode = catalogAdmin?.editingPageNode ?? null;
const loading = catalogAdmin?.loading ?? false;
const [ caption, setCaption ] = useState('');
const [ pageLayout, setPageLayout ] = useState('default_3x3');
const [ minRank, setMinRank ] = useState(1);
const [ visible, setVisible ] = useState('1');
const [ enabled, setEnabled ] = useState('1');
const [ orderNum, setOrderNum ] = useState(0);
// Resolve what we're editing:
// 1. editingPageNode (explicit node from sidebar click)
// 2. editingRootPage (root button)
// 3. current active page (from "Modifica Pagina" in layout)
const targetNode = editingPageNode
? editingPageNode
: editingRootPage
? rootNode
: (activeNodes.length > 0 ? activeNodes[activeNodes.length - 1] : null);
const targetPageId = targetNode?.pageId ?? currentPage?.pageId;
const isRoot = editingRootPage;
const closeForm = () =>
{
catalogAdmin?.setEditingPageData(false);
catalogAdmin?.setEditingRootPage(false);
catalogAdmin?.setEditingPageNode(null);
};
useEffect(() =>
{
if(!editingPageData || !targetNode) return;
setCaption(targetNode.localization || '');
setPageLayout(currentPage?.layoutCode || 'default_3x3');
setVisible(targetNode.isVisible ? '1' : '0');
setEnabled('1');
setMinRank(1);
setOrderNum(0);
}, [ editingPageData, targetNode, currentPage ]);
if(!editingPageData || !targetNode) return null;
const inputClass = 'text-[11px] border-2 border-card-grid-item-border rounded px-2 py-1 bg-white focus:outline-none focus:border-primary transition-colors';
const handleSave = async () =>
{
if(!catalogAdmin?.savePage) return;
const parentNode = targetNode.parent;
const data: IPageEditData = {
pageId: targetPageId,
caption,
pageLayout,
minRank,
visible,
enabled,
orderNum,
parentId: parentNode ? parentNode.pageId : -1,
};
const success = await catalogAdmin.savePage(data);
if(success) closeForm();
};
const handleDelete = async () =>
{
if(!catalogAdmin?.deletePage || isRoot) return;
if(!confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ targetNode.localization ]))) return;
const success = await catalogAdmin.deletePage(targetPageId);
if(success) closeForm();
};
return (
<div className="bg-white rounded border-2 border-card-grid-item-border p-2.5 mb-2">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold text-primary uppercase tracking-wide">
{ isRoot ? LocalizeText('catalog.admin.edit.root') : `${ LocalizeText('catalog.admin.edit') } ${ targetNode.localization } (#${ targetPageId })` }
</span>
<FaTimes className="text-muted cursor-pointer hover:text-danger text-[10px]" onClick={ closeForm } />
</div>
<div className="grid grid-cols-3 gap-1.5">
<div className="flex flex-col gap-0.5 col-span-2">
<label className="text-[9px] text-muted uppercase font-bold">Caption</label>
<input className={ inputClass } value={ caption } onChange={ e => setCaption(e.target.value) } />
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted uppercase font-bold">Min Rank</label>
<input className={ inputClass } min={ 1 } type="number" value={ minRank } onChange={ e => setMinRank(parseInt(e.target.value) || 1) } />
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted uppercase font-bold">Layout</label>
<select className={ inputClass } value={ pageLayout } onChange={ e => setPageLayout(e.target.value) }>
{ LAYOUT_OPTIONS.map(l => <option key={ l } value={ l }>{ l }</option>) }
</select>
</div>
<div className="flex flex-col gap-0.5">
<label className="text-[9px] text-muted uppercase font-bold">{ LocalizeText('catalog.admin.order') }</label>
<input className={ inputClass } min={ 0 } type="number" value={ orderNum } onChange={ e => setOrderNum(parseInt(e.target.value) || 0) } />
</div>
<div className="flex items-end gap-2 pb-0.5">
<label className="flex items-center gap-1 text-[10px] cursor-pointer">
<input className="accent-primary" checked={ visible === '1' } type="checkbox" onChange={ e => setVisible(e.target.checked ? '1' : '0') } />
{ LocalizeText('catalog.admin.visible') }
</label>
<label className="flex items-center gap-1 text-[10px] cursor-pointer">
<input className="accent-primary" checked={ enabled === '1' } type="checkbox" onChange={ e => setEnabled(e.target.checked ? '1' : '0') } />
{ LocalizeText('catalog.admin.enabled') }
</label>
</div>
</div>
<div className="flex justify-between mt-2">
{ !isRoot
? <button className="flex items-center gap-1 px-2 py-1 rounded text-[10px] font-bold bg-danger/10 text-danger border border-danger/30 hover:bg-danger/20 transition-colors cursor-pointer" onClick={ handleDelete }>
<FaTrash className="text-[8px]" /> { LocalizeText('catalog.admin.delete') }
</button>
: <div /> }
<button className="flex items-center gap-1 px-3 py-1 rounded text-[10px] font-bold bg-primary text-white hover:bg-secondary transition-colors cursor-pointer disabled:opacity-50" disabled={ loading } onClick={ handleSave }>
{ loading ? <FaSpinner className="text-[8px] animate-spin" /> : <FaSave className="text-[8px]" /> } { LocalizeText('catalog.admin.save') }
</button>
</div>
</div>
);
};