mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
Merge branch 'Dev' into merge-duckie-main-2026-05-06
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import { CatalogAdminCreateOfferComposer, CatalogAdminCreatePageComposer, CatalogAdminDeleteOfferComposer, CatalogAdminDeletePageComposer, CatalogAdminMoveOfferComposer, CatalogAdminMovePageComposer, CatalogAdminPublishComposer, CatalogAdminResultEvent, CatalogAdminSaveOfferComposer, CatalogAdminSavePageComposer } from '@nitrots/nitro-renderer';
|
||||
import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { ICatalogNode, IPurchasableOffer, NotificationAlertType, SendMessageComposer } from '../../api';
|
||||
import { useCatalog, useMessageEvent, useNotification } from '../../hooks';
|
||||
import { useCatalogUiState, useMessageEvent, useNotification } from '../../hooks';
|
||||
|
||||
export interface IPageEditData
|
||||
{
|
||||
pageId?: number;
|
||||
caption: string;
|
||||
captionSave: string;
|
||||
parentId: number;
|
||||
catalogMode: string;
|
||||
pageLayout: string;
|
||||
iconImage: number;
|
||||
enabled: string;
|
||||
visible: string;
|
||||
minRank: number;
|
||||
@@ -76,7 +78,7 @@ export const useCatalogAdmin = () => useContext(CatalogAdminContext);
|
||||
|
||||
export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) =>
|
||||
{
|
||||
const { currentType } = useCatalog();
|
||||
const { currentType } = useCatalogUiState();
|
||||
const [ adminMode, setAdminMode ] = useState(false);
|
||||
const [ editingOffer, setEditingOffer ] = useState<IPurchasableOffer | null>(null);
|
||||
const [ editingPageData, setEditingPageData ] = useState(false);
|
||||
@@ -88,7 +90,6 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
const pendingActionRef = useRef<string | null>(null);
|
||||
const { simpleAlert = null } = useNotification();
|
||||
|
||||
// Keyboard shortcuts: Esc to close edit panels
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!adminMode) return;
|
||||
@@ -97,7 +98,10 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
{
|
||||
if(e.key === 'Escape')
|
||||
{
|
||||
if(editingOffer) { setEditingOffer(null); e.preventDefault(); return; }
|
||||
if(editingOffer)
|
||||
{
|
||||
setEditingOffer(null); e.preventDefault(); return;
|
||||
}
|
||||
if(editingPageData || editingRootPage || editingPageNode)
|
||||
{
|
||||
setEditingPageData(false);
|
||||
@@ -173,11 +177,13 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
setLoading(true);
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'savePage';
|
||||
|
||||
SendMessageComposer(new CatalogAdminSavePageComposer(
|
||||
data.pageId || 0, data.caption, data.caption, data.pageLayout, 0,
|
||||
data.pageId || 0, data.caption, data.captionSave, data.pageLayout, data.iconImage,
|
||||
data.minRank, data.visible === '1', data.enabled === '1',
|
||||
data.orderNum, data.parentId,
|
||||
data.pageHeadline || '', data.pageTeaser || '', data.pageTextDetails || '', currentType, data.catalogMode
|
||||
data.pageHeadline || '', data.pageTeaser || '', data.pageTextDetails || '', currentType, data.catalogMode,
|
||||
data.pageText1 || ''
|
||||
));
|
||||
}, [ currentType ]);
|
||||
|
||||
@@ -187,7 +193,7 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
setLastError(null);
|
||||
pendingActionRef.current = 'createPage';
|
||||
SendMessageComposer(new CatalogAdminCreatePageComposer(
|
||||
data.caption, data.caption, data.pageLayout, 0,
|
||||
data.caption, data.captionSave, data.pageLayout, data.iconImage,
|
||||
data.minRank, data.visible === '1', data.enabled === '1',
|
||||
data.orderNum, data.parentId, currentType, data.catalogMode
|
||||
));
|
||||
@@ -280,7 +286,7 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CatalogAdminContext.Provider value={ {
|
||||
<CatalogAdminContext value={ {
|
||||
adminMode, setAdminMode,
|
||||
editingOffer, setEditingOffer,
|
||||
editingPageData, setEditingPageData,
|
||||
@@ -293,6 +299,6 @@ export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children })
|
||||
publishCatalog
|
||||
} }>
|
||||
{ children }
|
||||
</CatalogAdminContext.Provider>
|
||||
</CatalogAdminContext>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa';
|
||||
import { CatalogType, LocalizeText } from '../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useCatalog } from '../../hooks';
|
||||
import { CatalogType, GetConfigurationValue, LocalizeText } from '../../api';
|
||||
import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common';
|
||||
import { useCatalogActions, useCatalogData, useCatalogUiState, useHasPermission } from '../../hooks';
|
||||
import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext';
|
||||
import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView';
|
||||
import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView';
|
||||
@@ -18,14 +18,19 @@ import { MarketplacePostOfferView } from './views/page/layout/marketplace/Market
|
||||
|
||||
const CatalogClassicViewInner: FC<{}> = () =>
|
||||
{
|
||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const { rootNode = null, currentPage = null, searchResult = null } = useCatalogData();
|
||||
const { isVisible = false, setIsVisible = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], setSearchResult = null, currentType = CatalogType.NORMAL } = useCatalogUiState();
|
||||
const { openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null } = useCatalogActions();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {});
|
||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
|
||||
{});
|
||||
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
|
||||
const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {});
|
||||
const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
|
||||
{});
|
||||
const loading = catalogAdmin?.loading ?? false;
|
||||
const isMod = GetSessionDataManager().isModerator;
|
||||
|
||||
const isMod = useHasPermission('acc_catalogfurni');
|
||||
const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER)
|
||||
? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' }
|
||||
: undefined;
|
||||
@@ -148,13 +153,19 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
||||
{ adminMode &&
|
||||
<div className="flex items-center gap-0.5 ml-1" onClick={ e => e.stopPropagation() }>
|
||||
<FaEdit className="text-[8px] text-primary cursor-pointer hover:text-dark" title={ LocalizeText('catalog.admin.edit.title') }
|
||||
onClick={ () => { catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } } />
|
||||
onClick={ () =>
|
||||
{
|
||||
catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||
} } />
|
||||
<span className="cursor-pointer" title={ isHidden ? LocalizeText('catalog.admin.show') : LocalizeText('catalog.admin.hide') }
|
||||
onClick={ () => catalogAdmin.togglePageVisible(child.pageId) }>
|
||||
{ isHidden ? <FaEye className="text-[8px] text-success" /> : <FaEyeSlash className="text-[8px] text-muted" /> }
|
||||
</span>
|
||||
<FaTrash className="text-[8px] text-danger cursor-pointer hover:text-red-800" title={ LocalizeText('catalog.admin.delete.title') }
|
||||
onClick={ () => { if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId); } } />
|
||||
onClick={ () =>
|
||||
{
|
||||
if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId);
|
||||
} } />
|
||||
</div> }
|
||||
</div>
|
||||
</NitroCardTabsItemView>
|
||||
@@ -171,14 +182,17 @@ const CatalogClassicViewInner: FC<{}> = () =>
|
||||
<div className="flex items-center gap-2 mb-1 nitro-catalog-classic-admin-actions">
|
||||
<button
|
||||
className="flex items-center gap-1 text-[9px] text-success hover:text-green-800 cursor-pointer transition-colors"
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', catalogMode: currentType, pageLayout: 'default_3x3', minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', captionSave: 'New Category', catalogMode: currentType, pageLayout: 'default_3x3', iconImage: 0, minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
>
|
||||
<FaPlus className="text-[8px]" />
|
||||
<span>{ LocalizeText('catalog.admin.new') }</span>
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
|
||||
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true);
|
||||
} }
|
||||
>
|
||||
<FaEdit className="text-[8px]" />
|
||||
<span>{ LocalizeText('catalog.admin.root') }</span>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaCog, FaEdit, FaEye, FaEyeSlash, FaHeart, FaPlus, FaStar, FaTrash } from 'react-icons/fa';
|
||||
import { CatalogType, LocalizeText } from '../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
|
||||
import { useCatalog, useCatalogFavorites } from '../../hooks';
|
||||
import { useCatalogActions, useCatalogData, useCatalogFavorites, useCatalogUiState, useHasPermission } from '../../hooks';
|
||||
import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext';
|
||||
import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView';
|
||||
import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView';
|
||||
@@ -18,17 +18,21 @@ import { MarketplacePostOfferView } from './views/page/layout/marketplace/Market
|
||||
|
||||
const CatalogModernViewInner: FC<{}> = () =>
|
||||
{
|
||||
const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const { rootNode = null, currentPage = null, searchResult = null } = useCatalogData();
|
||||
const { isVisible = false, setIsVisible = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], setSearchResult = null, currentType = CatalogType.NORMAL } = useCatalogUiState();
|
||||
const { openPageByName = null, openPageByOfferId = null, activateNode = null, openCatalogByType = null, toggleCatalogByType = null } = useCatalogActions();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {});
|
||||
const setAdminMode = catalogAdmin?.setAdminMode ?? (() =>
|
||||
{});
|
||||
const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false;
|
||||
const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {});
|
||||
const publishCatalog = catalogAdmin?.publishCatalog ?? (() =>
|
||||
{});
|
||||
const loading = catalogAdmin?.loading ?? false;
|
||||
const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites();
|
||||
const [ showFavorites, setShowFavorites ] = useState(false);
|
||||
|
||||
const isMod = GetSessionDataManager().isModerator;
|
||||
const isMod = useHasPermission('acc_catalogfurni');
|
||||
const totalFavs = favoriteOfferIds.length + favoritePageIds.length;
|
||||
const buildersClubHeaderStyle = (currentType === CatalogType.BUILDER)
|
||||
? { borderColor: '#d79d2e', borderBottomColor: '#000', background: 'linear-gradient(180deg, #d89f2d 0%, #c68515 100%)' }
|
||||
@@ -162,7 +166,7 @@ const CatalogModernViewInner: FC<{}> = () =>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[9px] text-success hover:text-green-800 cursor-pointer transition-colors"
|
||||
title={ LocalizeText('catalog.admin.new.root.category') }
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', catalogMode: currentType, pageLayout: 'default_3x3', minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
onClick={ () => catalogAdmin.createPage({ caption: 'New Category', captionSave: 'New Category', catalogMode: currentType, pageLayout: 'default_3x3', iconImage: 0, minRank: 1, visible: '1', enabled: '1', orderNum: 99, parentId: rootNode.pageId }) }
|
||||
>
|
||||
<FaPlus className="text-[8px]" />
|
||||
<span className="whitespace-nowrap">{ LocalizeText('catalog.admin.new') }</span>
|
||||
@@ -170,7 +174,10 @@ const CatalogModernViewInner: FC<{}> = () =>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[9px] text-primary hover:text-dark cursor-pointer transition-colors"
|
||||
title={ LocalizeText('catalog.admin.edit.root') }
|
||||
onClick={ () => { catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(true); catalogAdmin.setEditingPageData(true);
|
||||
} }
|
||||
>
|
||||
<FaEdit className="text-[8px]" />
|
||||
<span className="whitespace-nowrap">{ LocalizeText('catalog.admin.root') }</span>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { FC } from 'react';
|
||||
import { GetConfigurationValue } from '../../api';
|
||||
import { useCatalog } from '../../hooks';
|
||||
import { useCatalogData } from '../../hooks';
|
||||
import { CatalogClassicView } from './CatalogClassicView';
|
||||
import { CatalogModernView } from './CatalogModernView';
|
||||
|
||||
export const CatalogView: FC<{}> = () =>
|
||||
{
|
||||
const { catalogLocalizationVersion = 0 } = useCatalog();
|
||||
const { catalogLocalizationVersion = 0 } = useCatalogData();
|
||||
const useNewStyle = GetConfigurationValue<boolean>('catalog.style.new', false);
|
||||
|
||||
if(useNewStyle) return (
|
||||
|
||||
@@ -2,12 +2,12 @@ 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 { useCatalogData } from '../../../../hooks';
|
||||
import { IOfferEditData, useCatalogAdmin } from '../../CatalogAdminContext';
|
||||
|
||||
export const CatalogAdminOfferEditView: FC<{}> = () =>
|
||||
{
|
||||
const { currentPage = null } = useCatalog();
|
||||
const { currentPage = null } = useCatalogData();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const editingOffer = catalogAdmin?.editingOffer ?? null;
|
||||
const setEditingOffer = catalogAdmin?.setEditingOffer;
|
||||
@@ -91,9 +91,10 @@ export const CatalogAdminOfferEditView: FC<{}> = () =>
|
||||
orderNumber
|
||||
};
|
||||
|
||||
const success = isNew ? await createOffer(data) : await saveOffer(data);
|
||||
if(isNew) createOffer(data);
|
||||
else saveOffer(data);
|
||||
|
||||
if(success && setEditingOffer) setEditingOffer(null);
|
||||
if(setEditingOffer) setEditingOffer(null);
|
||||
};
|
||||
|
||||
const handleDelete = () =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa';
|
||||
import { FaLanguage, FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa';
|
||||
import { CatalogType, LocalizeText } from '../../../../api';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState, useTranslationActions, useTranslationState } from '../../../../hooks';
|
||||
import { IPageEditData, useCatalogAdmin } from '../../CatalogAdminContext';
|
||||
|
||||
const LAYOUT_OPTIONS = [
|
||||
@@ -15,13 +15,15 @@ const LAYOUT_OPTIONS = [
|
||||
];
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ value: CatalogType.NORMAL, label: 'Normale' },
|
||||
{ value: 'BOTH', label: 'Entrambi' }
|
||||
{ value: 'NORMAL', label: 'Normal' },
|
||||
{ value: 'BUILDER', label: 'Builder' },
|
||||
{ value: 'BOTH', label: 'Both' }
|
||||
];
|
||||
|
||||
export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
{
|
||||
const { currentPage = null, activeNodes = [], rootNode = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const { currentPage = null, rootNode = null } = useCatalogData();
|
||||
const { activeNodes = [], currentType = CatalogType.NORMAL } = useCatalogUiState();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const editingPageData = catalogAdmin?.editingPageData ?? false;
|
||||
const editingRootPage = catalogAdmin?.editingRootPage ?? false;
|
||||
@@ -29,17 +31,22 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
const loading = catalogAdmin?.loading ?? false;
|
||||
|
||||
const [ caption, setCaption ] = useState('');
|
||||
const [ catalogMode, setCatalogMode ] = useState(CatalogType.NORMAL);
|
||||
const [ captionSave, setCaptionSave ] = useState('');
|
||||
const [ catalogMode, setCatalogMode ] = useState<string>('NORMAL');
|
||||
const [ pageLayout, setPageLayout ] = useState('default_3x3');
|
||||
const [ iconImage, setIconImage ] = useState(0);
|
||||
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 [ parentId, setParentId ] = useState(-1);
|
||||
const [ pageText1, setPageText1 ] = useState('');
|
||||
const [ showTranslate, setShowTranslate ] = useState(false);
|
||||
const [ translateTargetLanguage, setTranslateTargetLanguage ] = useState('en');
|
||||
const [ isTranslating, setIsTranslating ] = useState(false);
|
||||
const [ translateError, setTranslateError ] = useState<string | null>(null);
|
||||
const { supportedLanguages = [], languagesLoading = false } = useTranslationState();
|
||||
const { translateText, ensureSupportedLanguagesLoaded } = useTranslationActions();
|
||||
const targetNode = editingPageNode
|
||||
? editingPageNode
|
||||
: editingRootPage
|
||||
@@ -61,12 +68,26 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
if(!editingPageData || !targetNode) return;
|
||||
|
||||
setCaption(targetNode.localization || '');
|
||||
setCatalogMode(currentType === CatalogType.BUILDER ? CatalogType.BUILDER : (currentType || CatalogType.NORMAL));
|
||||
setCaptionSave(targetNode.pageName || targetNode.localization || '');
|
||||
setCatalogMode(currentType === CatalogType.BUILDER ? 'BUILDER' : 'NORMAL');
|
||||
setPageLayout(currentPage?.layoutCode || 'default_3x3');
|
||||
setIconImage(targetNode.iconId ?? 0);
|
||||
setVisible(targetNode.isVisible ? '1' : '0');
|
||||
setEnabled('1');
|
||||
setMinRank(1);
|
||||
setOrderNum(0);
|
||||
const matchesLoadedPage = currentPage && targetPageId === currentPage.pageId;
|
||||
const existingText1 = matchesLoadedPage && currentPage.localization
|
||||
? currentPage.localization.getText(0)
|
||||
: '';
|
||||
setPageText1(existingText1 || '');
|
||||
setShowTranslate(false);
|
||||
setIsTranslating(false);
|
||||
setTranslateError(null);
|
||||
const wireParentId = targetNode.parentId;
|
||||
setParentId(typeof wireParentId === 'number' && wireParentId !== -1
|
||||
? wireParentId
|
||||
: (targetNode.parent ? targetNode.parent.pageId : -1));
|
||||
}, [ editingPageData, targetNode, currentPage, currentType ]);
|
||||
|
||||
if(!editingPageData || !targetNode) return null;
|
||||
@@ -77,23 +98,65 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
{
|
||||
if(!catalogAdmin?.savePage) return;
|
||||
|
||||
const parentNode = targetNode.parent;
|
||||
|
||||
const data: IPageEditData = {
|
||||
pageId: targetPageId,
|
||||
caption,
|
||||
captionSave,
|
||||
catalogMode,
|
||||
pageLayout,
|
||||
iconImage,
|
||||
minRank,
|
||||
visible,
|
||||
enabled,
|
||||
orderNum,
|
||||
parentId: parentNode ? parentNode.pageId : -1,
|
||||
parentId,
|
||||
pageText1,
|
||||
};
|
||||
|
||||
const success = await catalogAdmin.savePage(data);
|
||||
catalogAdmin.savePage(data);
|
||||
|
||||
if(success) closeForm();
|
||||
closeForm();
|
||||
};
|
||||
|
||||
const openTranslate = () =>
|
||||
{
|
||||
const next = !showTranslate;
|
||||
setShowTranslate(next);
|
||||
setTranslateError(null);
|
||||
if(next) ensureSupportedLanguagesLoaded();
|
||||
};
|
||||
|
||||
const runTranslate = async () =>
|
||||
{
|
||||
if(!pageText1.trim().length)
|
||||
{
|
||||
setTranslateError('Nothing to translate yet.');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!translateTargetLanguage)
|
||||
{
|
||||
setTranslateError('Pick a language first.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTranslating(true);
|
||||
setTranslateError(null);
|
||||
|
||||
try
|
||||
{
|
||||
const result = await translateText(pageText1, translateTargetLanguage);
|
||||
setPageText1(result?.translatedText || pageText1);
|
||||
setShowTranslate(false);
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
setTranslateError((error as Error)?.message || 'Translation failed.');
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsTranslating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () =>
|
||||
@@ -101,16 +164,16 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
if(!catalogAdmin?.deletePage || isRoot) return;
|
||||
if(!confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ targetNode.localization ]))) return;
|
||||
|
||||
const success = await catalogAdmin.deletePage(targetPageId);
|
||||
catalogAdmin.deletePage(targetPageId);
|
||||
|
||||
if(success) closeForm();
|
||||
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 })` }
|
||||
{ isRoot ? LocalizeText('catalog.admin.edit.root') : `${ LocalizeText('catalog.admin.edit') } ${ targetNode.localization }` }
|
||||
</span>
|
||||
<FaTimes className="text-muted cursor-pointer hover:text-danger text-[10px]" onClick={ closeForm } />
|
||||
</div>
|
||||
@@ -124,13 +187,19 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
<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 col-span-2">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Caption Save (Localisation Key)</label>
|
||||
<input className={ inputClass } value={ captionSave } onChange={ e => setCaptionSave(e.target.value) } />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Icon Image</label>
|
||||
<input className={ inputClass } min={ 0 } type="number" value={ iconImage } onChange={ e => setIconImage(parseInt(e.target.value) || 0) } />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Mode</label>
|
||||
{ currentType === CatalogType.BUILDER
|
||||
? <div className={ `${ inputClass } flex items-center min-h-[28px] bg-gray-100 text-muted` }>Builders Club</div>
|
||||
: <select className={ inputClass } value={ catalogMode } onChange={ e => setCatalogMode(e.target.value) }>
|
||||
{ MODE_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ option.label }</option>) }
|
||||
</select> }
|
||||
<select className={ inputClass } value={ catalogMode } onChange={ e => setCatalogMode(e.target.value) }>
|
||||
{ MODE_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ option.label }</option>) }
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Layout</label>
|
||||
@@ -142,6 +211,10 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
<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 flex-col gap-0.5">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Parent ID</label>
|
||||
<input className={ inputClass } disabled={ isRoot } type="number" value={ parentId } onChange={ e => setParentId(parseInt(e.target.value) || -1) } />
|
||||
</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') } />
|
||||
@@ -152,6 +225,50 @@ export const CatalogAdminPageEditView: FC<{}> = () =>
|
||||
{ LocalizeText('catalog.admin.enabled') }
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 col-span-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[9px] text-muted uppercase font-bold">Page Text 1 <span className="text-muted normal-case font-normal opacity-70">(leave blank to keep current)</span></label>
|
||||
<button
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold text-primary border border-primary/40 hover:bg-primary/10 transition-colors cursor-pointer disabled:opacity-50"
|
||||
disabled={ isTranslating || !pageText1.trim().length }
|
||||
title="Translate via Google Translate"
|
||||
type="button"
|
||||
onClick={ openTranslate }>
|
||||
{ isTranslating ? <FaSpinner className="text-[8px] animate-spin" /> : <FaLanguage className="text-[10px]" /> }
|
||||
Translate
|
||||
</button>
|
||||
</div>
|
||||
{ showTranslate &&
|
||||
<div className="flex items-center gap-1 mb-1 p-1 bg-gray-50 border border-card-grid-item-border rounded">
|
||||
<select
|
||||
className={ `${ inputClass } flex-1` }
|
||||
disabled={ isTranslating || languagesLoading }
|
||||
value={ translateTargetLanguage }
|
||||
onChange={ e => setTranslateTargetLanguage(e.target.value) }>
|
||||
{ languagesLoading && !supportedLanguages.length &&
|
||||
<option value="">Loading languages…</option> }
|
||||
{ supportedLanguages.map(lang => (
|
||||
<option key={ lang.code } value={ lang.code }>{ lang.name } ({ lang.code })</option>
|
||||
)) }
|
||||
</select>
|
||||
<button
|
||||
className="px-2 py-1 rounded text-[10px] font-bold bg-primary text-white hover:bg-secondary transition-colors cursor-pointer disabled:opacity-50"
|
||||
disabled={ isTranslating || !translateTargetLanguage || !pageText1.trim().length }
|
||||
type="button"
|
||||
onClick={ runTranslate }>
|
||||
{ isTranslating ? <FaSpinner className="text-[8px] animate-spin" /> : 'Apply' }
|
||||
</button>
|
||||
<button
|
||||
className="px-2 py-1 rounded text-[10px] font-bold text-muted border border-card-grid-item-border hover:bg-gray-100 transition-colors cursor-pointer"
|
||||
disabled={ isTranslating }
|
||||
type="button"
|
||||
onClick={ () => { setShowTranslate(false); setTranslateError(null); } }>
|
||||
Cancel
|
||||
</button>
|
||||
</div> }
|
||||
{ translateError && <span className="text-[9px] text-danger">{ translateError }</span> }
|
||||
<textarea className={ `${ inputClass } min-h-[60px] resize-y` } value={ pageText1 } onChange={ e => setPageText1(e.target.value) } />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-2">
|
||||
|
||||
@@ -2,11 +2,12 @@ import { GetTickerTime } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { CatalogType, FriendlyTime, LocalizeText } from '../../../../api';
|
||||
import buildersClubIcon from '../../../../assets/images/toolbar/icons/buildersclub.png';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../hooks';
|
||||
|
||||
export const CatalogBuildersClubStatusView: FC = () =>
|
||||
{
|
||||
const { currentType = CatalogType.NORMAL, furniCount = 0, furniLimit = 0, secondsLeft = 0, secondsLeftWithGrace = 0, updateTime = 0 } = useCatalog();
|
||||
const { furniCount = 0, furniLimit = 0, secondsLeft = 0, secondsLeftWithGrace = 0, updateTime = 0 } = useCatalogData();
|
||||
const { currentType = CatalogType.NORMAL } = useCatalogUiState();
|
||||
const [ ticker, setTicker ] = useState(() => GetTickerTime());
|
||||
|
||||
useEffect(() =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { GetConfigurationValue } from '../../../../api';
|
||||
|
||||
export interface CatalogHeaderViewProps
|
||||
@@ -9,12 +9,7 @@ export interface CatalogHeaderViewProps
|
||||
export const CatalogHeaderView: FC<CatalogHeaderViewProps> = props =>
|
||||
{
|
||||
const { imageUrl = null } = props;
|
||||
const [ displayImageUrl, setDisplayImageUrl ] = useState('');
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setDisplayImageUrl(imageUrl ?? GetConfigurationValue<string>('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder'));
|
||||
}, [ imageUrl ]);
|
||||
const displayImageUrl = imageUrl ?? GetConfigurationValue<string>('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder');
|
||||
|
||||
return <div className="flex justify-center items-center w-full nitro-catalog-header">
|
||||
<img src={ displayImageUrl } onError={ ({ currentTarget }) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useMemo } from 'react';
|
||||
import { FaHeart, FaStar, FaTimes } from 'react-icons/fa';
|
||||
import { ICatalogNode, LocalizeText } from '../../../../api';
|
||||
import { useCatalog, useCatalogFavorites } from '../../../../hooks';
|
||||
import { useCatalogActions, useCatalogData, useCatalogFavorites } from '../../../../hooks';
|
||||
import { CatalogIconView } from '../catalog-icon/CatalogIconView';
|
||||
|
||||
interface CatalogFavoritesViewProps
|
||||
@@ -13,7 +13,8 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
|
||||
{
|
||||
const { onClose } = props;
|
||||
const { favoriteOffers, favoritePageIds, toggleFavoritePage, toggleFavoriteOffer } = useCatalogFavorites();
|
||||
const { offersToNodes, activateNode, openPageByOfferId, rootNode } = useCatalog();
|
||||
const { offersToNodes, rootNode } = useCatalogData();
|
||||
const { activateNode, openPageByOfferId } = useCatalogActions();
|
||||
|
||||
const favoritePages = useMemo(() =>
|
||||
{
|
||||
@@ -93,13 +94,19 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
|
||||
<div
|
||||
key={ page.pageId }
|
||||
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
|
||||
onClick={ () => { activateNode(page.node); onClose(); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
activateNode(page.node); onClose();
|
||||
} }
|
||||
>
|
||||
<CatalogIconView icon={ page.iconId } />
|
||||
<span className="text-[11px] flex-1 truncate font-medium">{ page.name }</span>
|
||||
<FaTimes
|
||||
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
|
||||
onClick={ e => { e.stopPropagation(); toggleFavoritePage(page.pageId); } }
|
||||
onClick={ e =>
|
||||
{
|
||||
e.stopPropagation(); toggleFavoritePage(page.pageId);
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
)) }
|
||||
@@ -118,7 +125,10 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
|
||||
<div
|
||||
key={ fav.offerId }
|
||||
className="group/fav flex items-center gap-2 px-1.5 py-1 bg-card-grid-item rounded border border-card-grid-item-border hover:bg-card-grid-item-active cursor-pointer transition-all duration-100"
|
||||
onClick={ () => { openPageByOfferId(fav.offerId); onClose(); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
openPageByOfferId(fav.offerId); onClose();
|
||||
} }
|
||||
>
|
||||
{ /* Furni icon */ }
|
||||
<div className="w-7 h-7 flex items-center justify-center shrink-0 bg-white rounded border border-card-grid-item-border overflow-hidden">
|
||||
@@ -132,7 +142,10 @@ export const CatalogFavoritesView: FC<CatalogFavoritesViewProps> = props =>
|
||||
<span className="text-[11px] flex-1 truncate font-medium">{ fav.displayName }</span>
|
||||
<FaTimes
|
||||
className="text-[7px] text-muted opacity-0 group-hover/fav:opacity-100 hover:text-danger transition-all cursor-pointer"
|
||||
onClick={ e => { e.stopPropagation(); toggleFavoriteOffer(fav.offerId); } }
|
||||
onClick={ e =>
|
||||
{
|
||||
e.stopPropagation(); toggleFavoriteOffer(fav.offerId);
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
)) }
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import { ColorUtils, LocalizeText, MessengerFriend, ProductTypeEnum, SendMessageComposer } from '../../../../api';
|
||||
import { Button, Column, Flex, FormGroup, LayoutCurrencyIcon, LayoutFurniImageView, LayoutGiftTagView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
|
||||
import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchasedEvent } from '../../../../events';
|
||||
import { useCatalog, useFriends, useMessageEvent, useUiEvent } from '../../../../hooks';
|
||||
import { useFriends, useGiftConfiguration, useMessageEvent, useUiEvent } from '../../../../hooks';
|
||||
import { classNames } from '../../../../layout';
|
||||
|
||||
let isBuyingGift = false;
|
||||
@@ -25,9 +25,8 @@ export const CatalogGiftView: FC<{}> = props =>
|
||||
const [ maxBoxIndex, setMaxBoxIndex ] = useState<number>(0);
|
||||
const [ maxRibbonIndex, setMaxRibbonIndex ] = useState<number>(0);
|
||||
const [ receiverNotFound, setReceiverNotFound ] = useState<boolean>(false);
|
||||
const { catalogOptions = null } = useCatalog();
|
||||
const { friends } = useFriends();
|
||||
const { giftConfiguration = null } = catalogOptions;
|
||||
const { data: giftConfiguration = null } = useGiftConfiguration();
|
||||
const [ boxTypes, setBoxTypes ] = useState<number[]>([]);
|
||||
const [ suggestions, setSuggestions ] = useState([]);
|
||||
const [ isAutocompleteVisible, setIsAutocompleteVisible ] = useState(true);
|
||||
@@ -133,7 +132,10 @@ export const CatalogGiftView: FC<{}> = props =>
|
||||
if(isBuyingGift) return;
|
||||
|
||||
isBuyingGift = true;
|
||||
setTimeout(() => { isBuyingGift = false; }, 10000);
|
||||
setTimeout(() =>
|
||||
{
|
||||
isBuyingGift = false;
|
||||
}, 10000);
|
||||
|
||||
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace));
|
||||
return;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { FC } from 'react';
|
||||
import { LocalizeText } from '../../../../api';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
import { useCatalogActions, useCatalogUiState } from '../../../../hooks';
|
||||
|
||||
export const CatalogBreadcrumbView: FC<{}> = () =>
|
||||
{
|
||||
const { activeNodes = [], activateNode } = useCatalog();
|
||||
const { activeNodes = [] } = useCatalogUiState();
|
||||
const { activateNode } = useCatalogActions();
|
||||
|
||||
if(!activeNodes || activeNodes.length === 0)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useCallback, useRef, useState } from 'react';
|
||||
import { FaArrowsAlt, FaCaretDown, FaCaretUp, FaPlus, FaStar, FaTrash } from 'react-icons/fa';
|
||||
import { CatalogType, ICatalogNode, LocalizeText } from '../../../../api';
|
||||
import { useCatalog, useCatalogFavorites } from '../../../../hooks';
|
||||
import { useCatalogActions, useCatalogFavorites, useCatalogUiState } from '../../../../hooks';
|
||||
import { useCatalogAdmin } from '../../CatalogAdminContext';
|
||||
import { CatalogIconView } from '../catalog-icon/CatalogIconView';
|
||||
import { CatalogNavigationSetView } from './CatalogNavigationSetView';
|
||||
@@ -15,7 +15,8 @@ export interface CatalogNavigationItemViewProps
|
||||
export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = props =>
|
||||
{
|
||||
const { node = null, child = false } = props;
|
||||
const { activateNode = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const { activateNode = null } = useCatalogActions();
|
||||
const { currentType = CatalogType.NORMAL } = useCatalogUiState();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const { isFavoritePage, toggleFavoritePage } = useCatalogFavorites();
|
||||
@@ -100,8 +101,10 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
|
||||
e.stopPropagation();
|
||||
catalogAdmin.createPage({
|
||||
caption: 'New Page',
|
||||
captionSave: 'New Page',
|
||||
catalogMode: currentType,
|
||||
pageLayout: 'default_3x3',
|
||||
iconImage: 0,
|
||||
minRank: 1,
|
||||
visible: '1',
|
||||
enabled: '1',
|
||||
@@ -125,8 +128,11 @@ export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = pro
|
||||
</div> }
|
||||
{ !adminMode && node.pageId > 0 &&
|
||||
<FaStar
|
||||
className={ `nitro-catalog-classic-navigation-favorite text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` }
|
||||
onClick={ e => { e.stopPropagation(); toggleFavoritePage(node.pageId); } }
|
||||
className={ `text-[8px] transition-all duration-100 cursor-pointer shrink-0 ${ isFav ? 'text-warning opacity-100' : 'text-muted opacity-0 group-hover/nav:opacity-100 hover:text-warning' }` }
|
||||
onClick={ e =>
|
||||
{
|
||||
e.stopPropagation(); toggleFavoritePage(node.pageId);
|
||||
} }
|
||||
/> }
|
||||
{ node.isBranch &&
|
||||
<span className="nitro-catalog-classic-navigation-caret text-[9px] text-muted shrink-0">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC } from 'react';
|
||||
import { ICatalogNode } from '../../../../api';
|
||||
import { useCatalog } from '../../../../hooks';
|
||||
import { useCatalogData } from '../../../../hooks';
|
||||
import { CatalogNavigationItemView } from './CatalogNavigationItemView';
|
||||
import { CatalogNavigationSetView } from './CatalogNavigationSetView';
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface CatalogNavigationViewProps
|
||||
export const CatalogNavigationView: FC<CatalogNavigationViewProps> = props =>
|
||||
{
|
||||
const { node = null } = props;
|
||||
const { searchResult = null } = useCatalog();
|
||||
const { searchResult = null } = useCatalogData();
|
||||
|
||||
return (
|
||||
<div className="nitro-catalog-classic-navigation-list">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FC, MouseEvent, useMemo, useState } from 'react';
|
||||
import { FaHeart } from 'react-icons/fa';
|
||||
import { CatalogType, IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common';
|
||||
import { useCatalog, useCatalogFavorites, useInventoryFurni } from '../../../../../hooks';
|
||||
import { useCatalogActions, useCatalogFavorites, useCatalogUiState, useInventoryFurni } from '../../../../../hooks';
|
||||
|
||||
interface CatalogGridOfferViewProps extends LayoutGridItemProps
|
||||
{
|
||||
@@ -15,7 +15,8 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
||||
{
|
||||
const { offer = null, selectOffer = null, itemActive = false, ...rest } = props;
|
||||
const [ isMouseDown, setMouseDown ] = useState(false);
|
||||
const { requestOfferToMover = null, currentType = CatalogType.NORMAL } = useCatalog();
|
||||
const { requestOfferToMover = null } = useCatalogActions();
|
||||
const { currentType = CatalogType.NORMAL } = useCatalogUiState();
|
||||
const { isVisible = false } = useInventoryFurni();
|
||||
const { isFavoriteOffer, toggleFavoriteOffer } = useCatalogFavorites();
|
||||
const isFav = offer ? isFavoriteOffer(offer.offerId) : false;
|
||||
@@ -78,7 +79,10 @@ export const CatalogGridOfferView: FC<CatalogGridOfferViewProps> = props =>
|
||||
<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); } }
|
||||
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' }` } />
|
||||
|
||||
@@ -2,12 +2,13 @@ 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, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
|
||||
export const CatalogSearchView: FC<{}> = () =>
|
||||
{
|
||||
const [ searchValue, setSearchValue ] = useState('');
|
||||
const { currentType = null, rootNode = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog();
|
||||
const { rootNode = null, searchResult = null } = useCatalogData();
|
||||
const { currentType = null, setSearchResult = null, setCurrentPage = null } = useCatalogUiState();
|
||||
|
||||
const normalizeSearchText = (value: string) => (value || '')
|
||||
.toLocaleLowerCase()
|
||||
@@ -48,6 +49,7 @@ export const CatalogSearchView: FC<{}> = () =>
|
||||
|
||||
const name = normalizeSearchText(furniture.name || '');
|
||||
const matchesSearch = name.includes(search);
|
||||
const isBuyable = (furniture.purchaseOfferId > -1) || (furniture.rentOfferId > -1);
|
||||
|
||||
if((currentType === CatalogType.BUILDER) && (furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1))
|
||||
{
|
||||
@@ -56,7 +58,7 @@ export const CatalogSearchView: FC<{}> = () =>
|
||||
if(matchesSearch) foundFurniLines.push(furniture.furniLine);
|
||||
}
|
||||
}
|
||||
else if(matchesSearch)
|
||||
else if(matchesSearch && isBuyable)
|
||||
{
|
||||
foundFurniture.push(furniture);
|
||||
|
||||
@@ -67,6 +69,10 @@ export const CatalogSearchView: FC<{}> = () =>
|
||||
|
||||
if(foundFurniture.length === 250) break;
|
||||
}
|
||||
else if(matchesSearch && furniture.furniLine && furniture.furniLine.length && (foundFurniLines.indexOf(furniture.furniLine) < 0))
|
||||
{
|
||||
foundFurniLines.push(furniture.furniLine);
|
||||
}
|
||||
}
|
||||
|
||||
const offers: IPurchasableOffer[] = [];
|
||||
@@ -81,7 +87,7 @@ export const CatalogSearchView: FC<{}> = () =>
|
||||
FilterCatalogNode(search, foundFurniLines, rootNode, nodes);
|
||||
|
||||
setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible))));
|
||||
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1) as ICatalogPage));
|
||||
setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1)));
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
import { LocalizeText, SanitizeHtml } from '../../../../../api';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
import { CatalogBadgeSelectorWidgetView } from '../widgets/CatalogBadgeSelectorWidgetView';
|
||||
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
@@ -14,7 +14,7 @@ import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
export const CatalogLayoutBadgeDisplayView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { ClubOfferData, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CatalogPurchaseState, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
import { useCatalogData, useClubOffers, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
@@ -14,12 +14,12 @@ export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
|
||||
{
|
||||
const [ pendingOffer, setPendingOffer ] = useState<ClubOfferData>(null);
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const { currentPage = null, catalogOptions = null } = useCatalog();
|
||||
const { currentPage = null } = useCatalogData();
|
||||
const { getCurrencyAmount = null } = usePurse();
|
||||
const isPurchasingRef = useRef(false);
|
||||
const isAddonLayout = (currentPage?.layoutCode === 'builders_club_addons');
|
||||
const windowId = (isAddonLayout ? BUILDERS_CLUB_ADDONS_WINDOW_ID : BUILDERS_CLUB_WINDOW_ID);
|
||||
const offers = catalogOptions?.clubOffersByWindowId?.[windowId] || null;
|
||||
const { data: offers = null } = useClubOffers(windowId);
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
{
|
||||
@@ -120,11 +120,6 @@ export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
|
||||
return currentPage.localization.getText(1) || currentPage.localization.getText(2) || currentPage.localization.getText(0) || '';
|
||||
}, [ currentPage ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!offers) SendMessageComposer(new GetClubOffersMessageComposer(windowId));
|
||||
}, [ offers, windowId ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!offers || !offers.length) return;
|
||||
@@ -142,44 +137,45 @@ export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
|
||||
{ currentPage?.localization?.getImage(0) &&
|
||||
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
|
||||
<Grid>
|
||||
<Column fullHeight justifyContent="between" overflow="hidden" size={ 7 }>
|
||||
<Column gap={ 1 } overflow="auto">
|
||||
{ offers && (offers.length > 0) && offers.map((offer, index) =>
|
||||
{
|
||||
const meta = getOfferMeta(offer);
|
||||
<Column fullHeight justifyContent="between" overflow="hidden" size={ 7 }>
|
||||
<Column gap={ 1 } overflow="auto">
|
||||
{ offers && (offers.length > 0) && offers.map((offer, index) =>
|
||||
{
|
||||
const meta = getOfferMeta(offer);
|
||||
|
||||
return (
|
||||
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-2" column={ false } itemActive={ pendingOffer?.offerId === offer.offerId } justifyContent="between" onClick={ () => {
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
setPendingOffer(offer);
|
||||
} }>
|
||||
<Column gap={ 0 }>
|
||||
<Text fontWeight="bold">{ getOfferName(offer) }</Text>
|
||||
{ meta.length > 0 && <Text small>{ meta }</Text> }
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
{ (offer.priceCredits > 0) &&
|
||||
return (
|
||||
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-2" column={ false } itemActive={ pendingOffer?.offerId === offer.offerId } justifyContent="between" onClick={ () =>
|
||||
{
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
setPendingOffer(offer);
|
||||
} }>
|
||||
<Column gap={ 0 }>
|
||||
<Text fontWeight="bold">{ getOfferName(offer) }</Text>
|
||||
{ meta.length > 0 && <Text small>{ meta }</Text> }
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
{ (offer.priceCredits > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ offer.priceCredits }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</Flex> }
|
||||
{ (offer.priceActivityPoints > 0) &&
|
||||
{ (offer.priceActivityPoints > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ offer.priceActivityPoints }</Text>
|
||||
<LayoutCurrencyIcon type={ offer.priceActivityPointsType } />
|
||||
</Flex> }
|
||||
</div>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
</LayoutGridItem>
|
||||
);
|
||||
}) }
|
||||
</Column>
|
||||
</Column>
|
||||
</Column>
|
||||
<Column gap={ 2 } overflow="hidden" size={ 5 }>
|
||||
<Column center grow overflow="hidden">
|
||||
{ currentPage?.localization.getImage(1) && <img alt="" src={ currentPage.localization.getImage(1) } /> }
|
||||
{ pageDescription.length > 0 && <Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(pageDescription) } } overflow="auto" /> }
|
||||
</Column>
|
||||
{ pendingOffer &&
|
||||
<Column gap={ 2 } overflow="hidden" size={ 5 }>
|
||||
<Column center grow overflow="hidden">
|
||||
{ currentPage?.localization.getImage(1) && <img alt="" src={ currentPage.localization.getImage(1) } /> }
|
||||
{ pageDescription.length > 0 && <Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(pageDescription) } } overflow="auto" /> }
|
||||
</Column>
|
||||
{ pendingOffer &&
|
||||
<Column fullWidth gap={ 1 }>
|
||||
<Text fontWeight="bold">{ getOfferName(pendingOffer) }</Text>
|
||||
{ getOfferMeta(pendingOffer).length > 0 && <Text>{ getOfferMeta(pendingOffer) }</Text> }
|
||||
@@ -202,7 +198,7 @@ export const CatalogLayoutBuildersClubBuyView: FC<CatalogLayoutProps> = () =>
|
||||
</Flex>
|
||||
{ getPurchaseButton() }
|
||||
</Column> }
|
||||
</Column>
|
||||
</Column>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FC, useMemo, useState } from 'react';
|
||||
import { FaFillDrip } from 'react-icons/fa';
|
||||
import { IPurchasableOffer, SanitizeHtml } from '../../../../../api';
|
||||
import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
|
||||
@@ -22,7 +22,8 @@ export const CatalogLayoutColorGroupingView: FC<CatalogLayoutColorGroupViewProps
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ colorableItems, setColorableItems ] = useState<Map<string, number[]>>(new Map<string, number[]>());
|
||||
const { currentOffer = null, setCurrentOffer = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { setCurrentOffer = null } = useCatalogUiState();
|
||||
const [ colorsShowing, setColorsShowing ] = useState<boolean>(false);
|
||||
|
||||
const sortByColorIndex = (a: IPurchasableOffer, b: IPurchasableOffer) =>
|
||||
|
||||
@@ -117,7 +117,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
||||
if(!prefixText.length) return;
|
||||
|
||||
const newColors: Record<number, string> = {};
|
||||
[ ...prefixText ].forEach((_, i) => { newColors[i] = customColorInput; });
|
||||
[ ...prefixText ].forEach((_, i) =>
|
||||
{
|
||||
newColors[i] = customColorInput;
|
||||
});
|
||||
setLetterColors(newColors);
|
||||
};
|
||||
|
||||
@@ -222,7 +225,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
||||
<Picker
|
||||
data={ data }
|
||||
locale="it"
|
||||
onEmojiSelect={ (emoji: { native: string }) => { setSelectedIcon(emoji.native); setShowIconPicker(false); } }
|
||||
onEmojiSelect={ (emoji: { native: string }) =>
|
||||
{
|
||||
setSelectedIcon(emoji.native); setShowIconPicker(false);
|
||||
} }
|
||||
theme="dark"
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
@@ -268,7 +274,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
||||
borderRight: '1px solid rgba(0,0,0,0.1)',
|
||||
opacity: colorMode === 'single' ? 1 : 0.6
|
||||
} }
|
||||
onClick={ () => { setColorMode('single'); setSelectedLetterIndex(null); } }>
|
||||
onClick={ () =>
|
||||
{
|
||||
setColorMode('single'); setSelectedLetterIndex(null);
|
||||
} }>
|
||||
{ LocalizeText('catalog.prefix.color.single') }
|
||||
</button>
|
||||
<button
|
||||
@@ -277,7 +286,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
||||
background: colorMode === 'perLetter' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)',
|
||||
opacity: colorMode === 'perLetter' ? 1 : 0.6
|
||||
} }
|
||||
onClick={ () => { setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0); } }>
|
||||
onClick={ () =>
|
||||
{
|
||||
setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0);
|
||||
} }>
|
||||
{ LocalizeText('catalog.prefix.color.per.letter') }
|
||||
</button>
|
||||
</div>
|
||||
@@ -328,7 +340,10 @@ export const CatalogLayoutCustomPrefixView: FC<CatalogLayoutProps> = props =>
|
||||
zIndex: isSelected ? 10 : 1,
|
||||
boxShadow: isSelected ? '0 0 8px rgba(59,130,246,0.3)' : 'none'
|
||||
} }
|
||||
onClick={ () => { setSelectedLetterIndex(i); setCustomColorInput(charColor); } }>
|
||||
onClick={ () =>
|
||||
{
|
||||
setSelectedLetterIndex(i); setCustomColorInput(charColor);
|
||||
} }>
|
||||
<span className="text-sm font-black" style={ { color: charColor } }>
|
||||
{ char }
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC } from 'react';
|
||||
import { FaEdit, FaPlus } from 'react-icons/fa';
|
||||
import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api';
|
||||
import { Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
import { useCatalogAdmin } from '../../../CatalogAdminContext';
|
||||
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
@@ -17,7 +17,7 @@ import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null, currentPage = null } = useCatalog();
|
||||
const { currentOffer = null, currentPage = null } = useCatalogData();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
|
||||
@@ -28,7 +28,10 @@ export const CatalogLayoutDefaultView: FC<CatalogLayoutProps> = props =>
|
||||
<div className="flex gap-2 nitro-catalog-classic-default-admin">
|
||||
<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); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||
} }
|
||||
>
|
||||
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
import { SanitizeHtml } from '../../../../../api';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView';
|
||||
import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
@@ -13,7 +13,7 @@ import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
export const CatalogLayouGuildCustomFurniView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { CatalogGroupsComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { SanitizeHtml, SendMessageComposer } from '../../../../../api';
|
||||
import { FC, useState } from 'react';
|
||||
import { SanitizeHtml } from '../../../../../api';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState, useUserGroups } from '../../../../../hooks';
|
||||
import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView';
|
||||
import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
@@ -13,13 +12,9 @@ export const CatalogLayouGuildForumView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
|
||||
const { currentOffer = null, setCurrentOffer = null, catalogOptions = null } = useCatalog();
|
||||
const { groups = null } = catalogOptions;
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new CatalogGroupsComposer());
|
||||
}, [ page ]);
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { setCurrentOffer = null } = useCatalogUiState();
|
||||
const { data: groups = null } = useUserGroups();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GetRoomAdPurchaseInfoComposer, GetUserEventCatsMessageComposer, PurchaseRoomAdMessageComposer, RoomAdPurchaseInfoEvent, RoomEntryData } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { useNitroQuery } from '../../../../../api/nitro-query';
|
||||
import { Button, Column, Text } from '../../../../../common';
|
||||
import { useCatalog, useMessageEvent, useNavigator, useRoomPromote } from '../../../../../hooks';
|
||||
import { useCatalogUiState, useNavigator, useRoomPromote } from '../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../layout';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
@@ -14,13 +15,20 @@ export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
|
||||
const [ eventName, setEventName ] = useState<string>('');
|
||||
const [ eventDesc, setEventDesc ] = useState<string>('');
|
||||
const [ roomId, setRoomId ] = useState<number>(-1);
|
||||
const [ availableRooms, setAvailableRooms ] = useState<RoomEntryData[]>([]);
|
||||
const [ extended, setExtended ] = useState<boolean>(false);
|
||||
const [ categoryId, setCategoryId ] = useState<number>(1);
|
||||
const { categories = null } = useNavigator();
|
||||
const { setIsVisible = null } = useCatalog();
|
||||
const { setIsVisible = null } = useCatalogUiState();
|
||||
const { promoteInformation, isExtended, setIsExtended } = useRoomPromote();
|
||||
|
||||
const { data: availableRooms = [] } = useNitroQuery<RoomAdPurchaseInfoEvent, RoomEntryData[]>({
|
||||
key: [ 'nitro', 'catalog', 'room-ad-purchase-info' ],
|
||||
request: () => new GetRoomAdPurchaseInfoComposer(),
|
||||
parser: RoomAdPurchaseInfoEvent,
|
||||
select: e => e.getParser()?.rooms ?? [],
|
||||
staleTime: 60_000
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isExtended)
|
||||
@@ -62,18 +70,8 @@ export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
|
||||
resetData();
|
||||
};
|
||||
|
||||
useMessageEvent<RoomAdPurchaseInfoEvent>(RoomAdPurchaseInfoEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
setAvailableRooms(parser.rooms);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new GetRoomAdPurchaseInfoComposer());
|
||||
// TODO: someone needs to fix this for morningstar
|
||||
SendMessageComposer(new GetUserEventCatsMessageComposer());
|
||||
}, []);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GetOfficialSongIdMessageComposer, GetSoundManager, MusicPriorities, Off
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Grid, LayoutImage, Text } from '../../../../../common';
|
||||
import { useCatalog, useMessageEvent } from '../../../../../hooks';
|
||||
import { useCatalogData, useMessageEvent } from '../../../../../hooks';
|
||||
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
@@ -18,7 +18,7 @@ export const CatalogLayoutSoundMachineView: FC<CatalogLayoutProps> = props =>
|
||||
const { page = null } = props;
|
||||
const [ songId, setSongId ] = useState(-1);
|
||||
const [ officialSongId, setOfficialSongId ] = useState('');
|
||||
const { currentOffer = null, currentPage = null } = useCatalog();
|
||||
const { currentOffer = null, currentPage = null } = useCatalogData();
|
||||
|
||||
const previewSong = (previewSongId: number) => GetSoundManager().musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_PURCHASE_PREVIEW, 15, 0, 0, 0);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { SanitizeHtml } from '../../../../../api';
|
||||
import { Column, Grid, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
|
||||
import { CatalogSpacesWidgetView } from '../widgets/CatalogSpacesWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
|
||||
@@ -11,7 +11,7 @@ import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
export const CatalogLayoutSpacesView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const { currentOffer = null, roomPreviewer = null } = useCatalog();
|
||||
const { currentOffer = null, roomPreviewer = null } = useCatalogData();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { FaEdit, FaPen, FaPlus, FaTrophy } from 'react-icons/fa';
|
||||
import { LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api';
|
||||
import { Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
import { useCatalogAdmin } from '../../../CatalogAdminContext';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
|
||||
@@ -15,7 +15,8 @@ export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null } = props;
|
||||
const [ trophyText, setTrophyText ] = useState<string>('');
|
||||
const { currentOffer = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { setPurchaseOptions = null } = useCatalogUiState();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
|
||||
@@ -42,7 +43,10 @@ export const CatalogLayoutTrophiesView: FC<CatalogLayoutProps> = props =>
|
||||
<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); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||
} }
|
||||
>
|
||||
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
||||
</button>
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { ClubOfferData, GiftReceiverNotFoundEvent, PurchaseFromCatalogAsGiftComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CatalogPurchaseState, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../api';
|
||||
import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
import { useCatalogData, useClubOffers, useMessageEvent, usePurse, useUiEvent, useUserDataSnapshot } from '../../../../../hooks';
|
||||
import { CatalogLayoutProps } from './CatalogLayout.types';
|
||||
|
||||
const VIP_WINDOW_ID = 1;
|
||||
|
||||
export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const [ pendingOffer, setPendingOffer ] = useState<ClubOfferData>(null);
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const { currentPage = null, catalogOptions = null } = useCatalog();
|
||||
const [ giftMode, setGiftMode ] = useState(false);
|
||||
const [ giftRecipient, setGiftRecipient ] = useState('');
|
||||
const [ giftError, setGiftError ] = useState<string | null>(null);
|
||||
const [ giftSuccess, setGiftSuccess ] = useState(false);
|
||||
const { currentPage = null } = useCatalogData();
|
||||
const { purse = null, getCurrencyAmount = null } = usePurse();
|
||||
const { clubOffers = null, clubOffersByWindowId = null } = (catalogOptions || {});
|
||||
const offers = clubOffersByWindowId?.[1] || clubOffers;
|
||||
const { data: offers = null } = useClubOffers(VIP_WINDOW_ID);
|
||||
const { userName: ownUserName = '' } = useUserDataSnapshot();
|
||||
const isPurchasingRef = useRef<boolean>(false);
|
||||
const wasGiftPurchaseRef = useRef<boolean>(false);
|
||||
const giftSuccessTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const isSelfGift = giftMode && !!ownUserName && giftRecipient.trim().toLowerCase() === ownUserName.toLowerCase();
|
||||
|
||||
const onCatalogEvent = useCallback((event: CatalogEvent) =>
|
||||
{
|
||||
@@ -23,9 +33,20 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
|
||||
isPurchasingRef.current = false;
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
setGiftError(null);
|
||||
if(wasGiftPurchaseRef.current)
|
||||
{
|
||||
wasGiftPurchaseRef.current = false;
|
||||
setGiftRecipient('');
|
||||
setGiftMode(false);
|
||||
setGiftSuccess(true);
|
||||
if(giftSuccessTimerRef.current) clearTimeout(giftSuccessTimerRef.current);
|
||||
giftSuccessTimerRef.current = setTimeout(() => setGiftSuccess(false), 3500);
|
||||
}
|
||||
return;
|
||||
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
|
||||
isPurchasingRef.current = false;
|
||||
wasGiftPurchaseRef.current = false;
|
||||
setPurchaseState(CatalogPurchaseState.FAILED);
|
||||
return;
|
||||
}
|
||||
@@ -34,6 +55,21 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, onCatalogEvent);
|
||||
useUiEvent(CatalogPurchaseFailureEvent.PURCHASE_FAILED, onCatalogEvent);
|
||||
|
||||
useEffect(() => () =>
|
||||
{
|
||||
if(giftSuccessTimerRef.current) clearTimeout(giftSuccessTimerRef.current);
|
||||
}, []);
|
||||
|
||||
const handleGiftReceiverNotFound = useCallback(() =>
|
||||
{
|
||||
if(!isPurchasingRef.current) return;
|
||||
isPurchasingRef.current = false;
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
setGiftError(LocalizeText('catalog.gift_wrapping.receiver_not_found.title'));
|
||||
}, []);
|
||||
|
||||
useMessageEvent<GiftReceiverNotFoundEvent>(GiftReceiverNotFoundEvent, handleGiftReceiverNotFound);
|
||||
|
||||
const getOfferText = useCallback((offer: ClubOfferData) =>
|
||||
{
|
||||
let offerText = '';
|
||||
@@ -88,16 +124,39 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
const purchaseSubscription = useCallback(() =>
|
||||
{
|
||||
if(!pendingOffer || isPurchasingRef.current) return;
|
||||
if(giftMode && !giftRecipient.trim()) return;
|
||||
if(isSelfGift) return;
|
||||
|
||||
isPurchasingRef.current = true;
|
||||
wasGiftPurchaseRef.current = giftMode;
|
||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(currentPage.pageId, pendingOffer.offerId, null, 1));
|
||||
}, [ pendingOffer, currentPage ]);
|
||||
setGiftError(null);
|
||||
setGiftSuccess(false);
|
||||
|
||||
if(giftMode)
|
||||
{
|
||||
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(currentPage.pageId, pendingOffer.offerId, '', giftRecipient.trim(), '', 0, 0, 0, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(currentPage.pageId, pendingOffer.offerId, null, 1));
|
||||
}
|
||||
}, [ pendingOffer, currentPage, giftMode, giftRecipient, isSelfGift ]);
|
||||
|
||||
const setOffer = useCallback((offer: ClubOfferData) =>
|
||||
{
|
||||
setPurchaseState(CatalogPurchaseState.NONE);
|
||||
setPendingOffer(offer);
|
||||
setGiftError(null);
|
||||
setGiftSuccess(false);
|
||||
if(!offer?.giftable) setGiftMode(false);
|
||||
}, []);
|
||||
|
||||
const onGiftRecipientChange = useCallback((value: string) =>
|
||||
{
|
||||
setGiftRecipient(value);
|
||||
setGiftError(null);
|
||||
setGiftSuccess(false);
|
||||
}, []);
|
||||
|
||||
const getPurchaseButton = useCallback(() =>
|
||||
@@ -114,24 +173,22 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
return <Button fullWidth variant="danger">{ LocalizeText('catalog.alert.notenough.activitypoints.title.' + pendingOffer.priceActivityPointsType) }</Button>;
|
||||
}
|
||||
|
||||
const giftBlocked = giftMode && (!giftRecipient.trim() || isSelfGift);
|
||||
const buyLabel = giftMode ? LocalizeText('catalog.gift_wrapping.give_gift') : LocalizeText('buy');
|
||||
|
||||
switch(purchaseState)
|
||||
{
|
||||
case CatalogPurchaseState.CONFIRM:
|
||||
return <Button fullWidth variant="warning" onClick={ purchaseSubscription }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
|
||||
return <Button disabled={ giftBlocked } fullWidth variant="warning" onClick={ purchaseSubscription }>{ LocalizeText('catalog.marketplace.confirm_title') }</Button>;
|
||||
case CatalogPurchaseState.PURCHASE:
|
||||
return <Button disabled fullWidth variant="primary"><LayoutLoadingSpinnerView /></Button>;
|
||||
case CatalogPurchaseState.FAILED:
|
||||
return <Button disabled fullWidth variant="danger">{ LocalizeText('generic.failed') }</Button>;
|
||||
case CatalogPurchaseState.NONE:
|
||||
default:
|
||||
return <Button fullWidth variant="success" onClick={ () => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ LocalizeText('buy') }</Button>;
|
||||
return <Button disabled={ giftBlocked } fullWidth variant="success" onClick={ () => setPurchaseState(CatalogPurchaseState.CONFIRM) }>{ buyLabel }</Button>;
|
||||
}
|
||||
}, [ pendingOffer, purchaseState, purchaseSubscription, getCurrencyAmount ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!offers) SendMessageComposer(new GetClubOffersMessageComposer(1));
|
||||
}, [ offers ]);
|
||||
}, [ pendingOffer, purchaseState, purchaseSubscription, getCurrencyAmount, giftMode, giftRecipient, isSelfGift ]);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
@@ -139,25 +196,29 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
<AutoGrid className="nitro-catalog-layout-vip-buy-grid" columnCount={ 1 }>
|
||||
{ offers && (offers.length > 0) && offers.map((offer, index) =>
|
||||
{
|
||||
const isActive = (pendingOffer === offer);
|
||||
|
||||
return (
|
||||
<LayoutGridItem key={ index } alignItems="center" center={ false } className="p-1" column={ false } itemActive={ pendingOffer === offer } justifyContent="between" onClick={ () => setOffer(offer) }>
|
||||
<i className="icon-hc-banner" />
|
||||
<Column gap={ 0 } justifyContent="end">
|
||||
<Text textEnd>{ getOfferText(offer) }</Text>
|
||||
<Flex gap={ 1 } justifyContent="end">
|
||||
{ (offer.priceCredits > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ offer.priceCredits }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</Flex> }
|
||||
{ (offer.priceActivityPoints > 0) &&
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="end">
|
||||
<Text>{ offer.priceActivityPoints }</Text>
|
||||
<LayoutCurrencyIcon type={ offer.priceActivityPointsType } />
|
||||
</Flex> }
|
||||
</Flex>
|
||||
</Column>
|
||||
</LayoutGridItem>
|
||||
<div key={ index } className={ 'nitro-vip-buy-offer flex flex-col gap-1.5 p-2 rounded-md border-2 cursor-pointer ' + (isActive ? 'active border-[#7a5500] bg-[#ffe066]' : 'border-[#b48a18] bg-[#fffbe7] hover:bg-[#fff5c4] hover:border-[#9c7610]') } onClick={ () => setOffer(offer) }>
|
||||
<div className="vip-offer-header flex items-center gap-2 pb-1.5 border-b border-dashed border-[#b48a18]">
|
||||
<span className="vip-offer-banner inline-flex items-center justify-center shrink-0 w-[34px] h-[20px]">
|
||||
<i className="nitro-icon icon-hc-banner" style={ { width: '34px', height: '20px', backgroundSize: 'contain', backgroundRepeat: 'no-repeat', backgroundPosition: 'center' } } />
|
||||
</span>
|
||||
<span className="vip-offer-title flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap font-bold text-[1.05rem] leading-tight text-[#2c2a25]">{ getOfferText(offer) }</span>
|
||||
</div>
|
||||
<div className="vip-offer-prices flex flex-col gap-1">
|
||||
{ (offer.priceCredits > 0) &&
|
||||
<span className="vip-offer-price flex items-center gap-1.5 font-bold text-[0.95rem] leading-tight text-[#4a473e] whitespace-nowrap">
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
<span>{ offer.priceCredits }</span>
|
||||
</span> }
|
||||
{ (offer.priceActivityPoints > 0) &&
|
||||
<span className="vip-offer-price flex items-center gap-1.5 font-bold text-[0.95rem] leading-tight text-[#4a473e] whitespace-nowrap">
|
||||
<LayoutCurrencyIcon type={ offer.priceActivityPointsType } />
|
||||
<span>{ offer.priceActivityPoints }</span>
|
||||
</span> }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}) }
|
||||
</AutoGrid>
|
||||
@@ -172,7 +233,7 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
<Column fullWidth grow justifyContent="end">
|
||||
<Flex alignItems="end">
|
||||
<Column grow gap={ 0 }>
|
||||
<Text fontWeight="bold">{ getPurchaseHeader() }</Text>
|
||||
<Text fontWeight="bold">{ giftMode ? LocalizeText('catalog.purchase_confirmation.gift') : getPurchaseHeader() }</Text>
|
||||
<Text>{ getPurchaseValidUntil() }</Text>
|
||||
</Column>
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -188,6 +249,28 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
|
||||
</Flex> }
|
||||
</div>
|
||||
</Flex>
|
||||
{ pendingOffer.giftable &&
|
||||
<Column className="mt-1" gap={ 1 }>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<label className="flex items-center gap-1 cursor-pointer text-sm">
|
||||
<input checked={ giftMode } className="cursor-pointer" type="checkbox" onChange={ event => { setGiftMode(event.target.checked); setGiftError(null); setGiftSuccess(false); } } />
|
||||
<span>{ LocalizeText('catalog.purchase_confirmation.gift') }</span>
|
||||
</label>
|
||||
{ giftMode &&
|
||||
<input
|
||||
className="flex-1 min-w-0 border border-[#b48a18] bg-white rounded px-2 py-1 text-sm"
|
||||
placeholder={ LocalizeText('catalog.gift_wrapping.receiver') }
|
||||
type="text"
|
||||
value={ giftRecipient }
|
||||
onChange={ event => onGiftRecipientChange(event.target.value) } /> }
|
||||
</Flex>
|
||||
{ giftMode && isSelfGift &&
|
||||
<Text className="text-[#b00020] text-xs">{ LocalizeText('catalog.gift_wrapping.cannot_send_to_self') }</Text> }
|
||||
{ giftMode && giftError && !isSelfGift &&
|
||||
<Text className="text-[#b00020] text-xs">{ giftError }</Text> }
|
||||
{ giftSuccess &&
|
||||
<Text className="text-[#1f7a1f] text-sm font-bold">{ LocalizeText('catalog.gift_wrapping.gift_sent') }</Text> }
|
||||
</Column> }
|
||||
{ getPurchaseButton() }
|
||||
</Column> }
|
||||
</Column>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CreateLinkEvent, FrontPageItem } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect } from 'react';
|
||||
import { Column, Grid } from '../../../../../../common';
|
||||
import { useCatalog } from '../../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../../hooks';
|
||||
import { CatalogRedeemVoucherView } from '../../common/CatalogRedeemVoucherView';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { CatalogLayoutFrontPageItemView } from './CatalogLayoutFrontPageItemView';
|
||||
@@ -9,7 +9,7 @@ import { CatalogLayoutFrontPageItemView } from './CatalogLayoutFrontPageItemView
|
||||
export const CatalogLayoutFrontpage4View: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { page = null, hideNavigation = null } = props;
|
||||
const { frontPageItems = [] } = useCatalog();
|
||||
const { frontPageItems = [] } = useCatalogData();
|
||||
|
||||
const selectItem = useCallback((item: FrontPageItem) =>
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { GetMarketplaceConfigurationMessageComposer, MakeOfferMessageComposer, MarketplaceConfigurationEvent } from '@nitrots/nitro-renderer';
|
||||
import { MakeOfferMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FurnitureItem, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../../api';
|
||||
import { Button, Column, Grid, LayoutFurniImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../../common';
|
||||
import { CatalogPostMarketplaceOfferEvent } from '../../../../../../events';
|
||||
import { useCatalog, useMessageEvent, useNotification, useUiEvent } from '../../../../../../hooks';
|
||||
import { useMarketplaceConfiguration, useNotification, useUiEvent } from '../../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../../layout';
|
||||
|
||||
let isPostingMarketplaceOffer = false;
|
||||
@@ -13,8 +13,7 @@ export const MarketplacePostOfferView: FC<{}> = props =>
|
||||
const [ item, setItem ] = useState<FurnitureItem>(null);
|
||||
const [ askingPrice, setAskingPrice ] = useState(0);
|
||||
const [ tempAskingPrice, setTempAskingPrice ] = useState('0');
|
||||
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
|
||||
const { marketplaceConfiguration = null } = catalogOptions;
|
||||
const { data: marketplaceConfiguration = null } = useMarketplaceConfiguration({ enabled: !!item });
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const updateAskingPrice = (price: string) =>
|
||||
@@ -28,29 +27,8 @@ export const MarketplacePostOfferView: FC<{}> = props =>
|
||||
setAskingPrice(parseInt(price));
|
||||
};
|
||||
|
||||
useMessageEvent<MarketplaceConfigurationEvent>(MarketplaceConfigurationEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
const newValue = { ...prevValue };
|
||||
|
||||
newValue.marketplaceConfiguration = parser;
|
||||
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useUiEvent<CatalogPostMarketplaceOfferEvent>(CatalogPostMarketplaceOfferEvent.POST_MARKETPLACE, event => setItem(event.item));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!item || marketplaceConfiguration) return;
|
||||
|
||||
SendMessageComposer(new GetMarketplaceConfigurationMessageComposer());
|
||||
}, [ item, marketplaceConfiguration ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!item) return;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ApproveNameMessageComposer, ApproveNameMessageEvent, ColorConverter, GetSellablePetPalettesComposer, PurchaseFromCatalogComposer, SellablePetPaletteData } from '@nitrots/nitro-renderer';
|
||||
import { ApproveNameMessageComposer, ApproveNameMessageEvent, ColorConverter, 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 { DispatchUiEvent, GetPetAvailableColors, GetPetIndexFromLocalization, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../../api';
|
||||
import { LayoutGridItem, LayoutPetImageView } from '../../../../../../common';
|
||||
import { CatalogPurchaseFailureEvent } from '../../../../../../events';
|
||||
import { useCatalog, useMessageEvent } from '../../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState, useMessageEvent, useSellablePetPalette } from '../../../../../../hooks';
|
||||
import { useCatalogAdmin } from '../../../../CatalogAdminContext';
|
||||
import { CatalogAddOnBadgeWidgetView } from '../../widgets/CatalogAddOnBadgeWidgetView';
|
||||
import { CatalogTotalPriceWidget } from '../../widgets/CatalogTotalPriceWidget';
|
||||
@@ -23,10 +23,12 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
|
||||
const [ petName, setPetName ] = useState('');
|
||||
const [ approvalPending, setApprovalPending ] = useState(true);
|
||||
const [ approvalResult, setApprovalResult ] = useState(-1);
|
||||
const { currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null, catalogOptions = null, roomPreviewer = null } = useCatalog();
|
||||
const { currentOffer = null, roomPreviewer = null } = useCatalogData();
|
||||
const { setCurrentOffer = null, setPurchaseOptions = null } = useCatalogUiState();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const { petPalettes = null } = catalogOptions;
|
||||
const breed: string = (currentOffer?.product?.productData?.type as unknown as string) ?? '';
|
||||
const { data: petPalette = null } = useSellablePetPalette(breed);
|
||||
|
||||
const getColor = useMemo(() =>
|
||||
{
|
||||
@@ -129,39 +131,25 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!currentOffer) return;
|
||||
|
||||
const productData = currentOffer.product.productData;
|
||||
|
||||
if(!productData) return;
|
||||
|
||||
if(petPalettes)
|
||||
if(!currentOffer || !petPalette)
|
||||
{
|
||||
for(const paletteData of petPalettes)
|
||||
{
|
||||
if(paletteData.breed !== productData.type) continue;
|
||||
|
||||
const palettes: SellablePetPaletteData[] = [];
|
||||
|
||||
for(const palette of paletteData.palettes)
|
||||
{
|
||||
if(!palette.sellable) continue;
|
||||
|
||||
palettes.push(palette);
|
||||
}
|
||||
|
||||
setSelectedPaletteIndex((palettes.length ? 0 : -1));
|
||||
setSellablePalettes(palettes);
|
||||
|
||||
return;
|
||||
}
|
||||
setSelectedPaletteIndex(-1);
|
||||
setSellablePalettes([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedPaletteIndex(-1);
|
||||
setSellablePalettes([]);
|
||||
const palettes: SellablePetPaletteData[] = [];
|
||||
|
||||
SendMessageComposer(new GetSellablePetPalettesComposer(productData.type));
|
||||
}, [ currentOffer, petPalettes ]);
|
||||
for(const palette of petPalette.palettes)
|
||||
{
|
||||
if(!palette.sellable) continue;
|
||||
|
||||
palettes.push(palette);
|
||||
}
|
||||
|
||||
setSelectedPaletteIndex(palettes.length ? 0 : -1);
|
||||
setSellablePalettes(palettes);
|
||||
}, [ currentOffer, petPalette ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@@ -202,7 +190,10 @@ export const CatalogLayoutPetView: FC<CatalogLayoutProps> = props =>
|
||||
<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); } }
|
||||
onClick={ () =>
|
||||
{
|
||||
catalogAdmin.setEditingPageNode(null); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true);
|
||||
} }
|
||||
>
|
||||
<FaEdit className="text-[10px]" /> { LocalizeText('catalog.admin.edit.page') }
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SelectClubGiftComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../../api';
|
||||
import { AutoGrid, Text } from '../../../../../../common';
|
||||
import { useCatalog, useNotification, usePurse } from '../../../../../../hooks';
|
||||
import { useClubGifts, useNotification, usePurse } from '../../../../../../hooks';
|
||||
import { CatalogLayoutProps } from '../CatalogLayout.types';
|
||||
import { VipGiftItem } from './VipGiftItemView';
|
||||
|
||||
@@ -11,8 +11,7 @@ let isSelectingGift = false;
|
||||
export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
{
|
||||
const { purse = null } = usePurse();
|
||||
const { catalogOptions = null, setCatalogOptions = null } = useCatalog();
|
||||
const { clubGifts = null } = catalogOptions;
|
||||
const { data: clubGifts = null } = useClubGifts();
|
||||
const { showConfirm = null } = useNotification();
|
||||
|
||||
const giftsAvailable = useCallback(() =>
|
||||
@@ -36,34 +35,32 @@ export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
|
||||
|
||||
isSelectingGift = true;
|
||||
|
||||
// The server replies with a fresh ClubGiftInfoEvent after
|
||||
// accepting the selection; useClubGifts subscribes to that
|
||||
// event via useNitroEventInvalidator, so giftsAvailable
|
||||
// refreshes from the authoritative source — no need to
|
||||
// mutate the parser locally.
|
||||
SendMessageComposer(new SelectClubGiftComposer(localizationId));
|
||||
|
||||
setCatalogOptions(prevValue =>
|
||||
{
|
||||
prevValue.clubGifts.giftsAvailable--;
|
||||
|
||||
return { ...prevValue };
|
||||
});
|
||||
|
||||
setTimeout(() => isSelectingGift = false, 5000);
|
||||
}, null);
|
||||
}, [ setCatalogOptions, showConfirm ]);
|
||||
}, [ showConfirm ]);
|
||||
|
||||
const sortGifts = useMemo(() =>
|
||||
{
|
||||
let gifts = clubGifts.offers.sort((a,b) =>
|
||||
{
|
||||
return clubGifts.getOfferExtraData(a.offerId).daysRequired - clubGifts.getOfferExtraData(b.offerId).daysRequired;
|
||||
});
|
||||
return gifts;
|
||||
},[ clubGifts ]);
|
||||
if(!clubGifts) return [];
|
||||
|
||||
return [ ...clubGifts.offers ].sort((a, b) =>
|
||||
(clubGifts.getOfferExtraData(a.offerId).daysRequired - clubGifts.getOfferExtraData(b.offerId).daysRequired)
|
||||
);
|
||||
}, [ clubGifts ]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text shrink truncate fontWeight="bold">{ giftsAvailable() }</Text>
|
||||
<AutoGrid className="nitro-catalog-layout-vip-gifts-grid" columnCount={ 1 }>
|
||||
{ (clubGifts.offers.length > 0) && sortGifts.map(offer => <VipGiftItem key={ offer.offerId } daysRequired={ clubGifts.getOfferExtraData(offer.offerId).daysRequired } isAvailable={ (clubGifts.getOfferExtraData(offer.offerId).isSelectable && (clubGifts.giftsAvailable > 0)) } offer={ offer } onSelect={ selectGift }/>) }
|
||||
{ clubGifts && (clubGifts.offers.length > 0) && sortGifts.map(offer => <VipGiftItem key={ offer.offerId } daysRequired={ clubGifts.getOfferExtraData(offer.offerId).daysRequired } isAvailable={ (clubGifts.getOfferExtraData(offer.offerId).isSelectable && (clubGifts.giftsAvailable > 0)) } offer={ offer } onSelect={ selectGift }/>) }
|
||||
</AutoGrid>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC } from 'react';
|
||||
import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
|
||||
interface CatalogAddOnBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
@@ -10,7 +10,7 @@ interface CatalogAddOnBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
|
||||
export const CatalogAddOnBadgeWidgetView: FC<CatalogAddOnBadgeWidgetViewProps> = props =>
|
||||
{
|
||||
const { ...rest } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
|
||||
if(!currentOffer || !currentOffer.badgeCode || !currentOffer.badgeCode.length) return null;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { AutoGrid, AutoGridProps, LayoutBadgeImageView, LayoutGridItem } from '../../../../../common';
|
||||
import { useCatalog, useInventoryBadges } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState, useInventoryBadges } from '../../../../../hooks';
|
||||
|
||||
const EXCLUDED_BADGE_CODES: string[] = [];
|
||||
|
||||
@@ -15,7 +15,8 @@ export const CatalogBadgeSelectorWidgetView: FC<CatalogBadgeSelectorWidgetViewPr
|
||||
const { columnCount = 5, ...rest } = props;
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const [ currentBadgeCode, setCurrentBadgeCode ] = useState<string>(null);
|
||||
const { currentOffer = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { setPurchaseOptions = null } = useCatalogUiState();
|
||||
const { badgeCodes = [], activate = null, deactivate = null } = useInventoryBadges();
|
||||
|
||||
const previewStuffData = useMemo(() =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { AutoGrid, AutoGridProps, LayoutGridItem } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
|
||||
interface CatalogBundleGridWidgetViewProps extends AutoGridProps
|
||||
{
|
||||
@@ -10,8 +10,8 @@ interface CatalogBundleGridWidgetViewProps extends AutoGridProps
|
||||
export const CatalogBundleGridWidgetView: FC<CatalogBundleGridWidgetViewProps> = props =>
|
||||
{
|
||||
const { columnCount = 5, children = null, ...rest } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
|
||||
export const CatalogFirstProductSelectorWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentPage = null, setCurrentOffer = null } = useCatalog();
|
||||
const { currentPage = null } = useCatalogData();
|
||||
const { setCurrentOffer = null } = useCatalogUiState();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { BaseProps, LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
|
||||
interface CatalogGuildBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
|
||||
{
|
||||
@@ -11,7 +11,8 @@ interface CatalogGuildBadgeWidgetViewProps extends BaseProps<HTMLDivElement>
|
||||
export const CatalogGuildBadgeWidgetView: FC<CatalogGuildBadgeWidgetViewProps> = props =>
|
||||
{
|
||||
const { ...rest } = props;
|
||||
const { currentOffer = null, purchaseOptions = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { purchaseOptions = null } = useCatalogUiState();
|
||||
const { previewStuffData = null } = purchaseOptions;
|
||||
|
||||
const badgeCode = useMemo(() =>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { CatalogGroupsComposer, StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { Button, Flex } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState, useUserGroups } from '../../../../../hooks';
|
||||
|
||||
export const CatalogGuildSelectorWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState<number>(0);
|
||||
const { currentOffer = null, catalogOptions = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { groups = null } = catalogOptions;
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { setPurchaseOptions = null } = useCatalogUiState();
|
||||
const { data: groups = null } = useUserGroups();
|
||||
|
||||
const previewStuffData = useMemo(() =>
|
||||
{
|
||||
@@ -41,11 +42,6 @@ export const CatalogGuildSelectorWidgetView: FC<{}> = props =>
|
||||
});
|
||||
}, [ currentOffer, previewStuffData, setPurchaseOptions ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new CatalogGroupsComposer());
|
||||
}, []);
|
||||
|
||||
if(!groups || !groups.length)
|
||||
{
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { IPurchasableOffer } from '../../../../../api';
|
||||
import { AutoGrid, AutoGridProps } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogActions, useCatalogData } from '../../../../../hooks';
|
||||
import { useCatalogAdmin } from '../../../CatalogAdminContext';
|
||||
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
|
||||
|
||||
@@ -13,10 +13,11 @@ interface CatalogItemGridWidgetViewProps extends AutoGridProps
|
||||
export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = props =>
|
||||
{
|
||||
const { columnCount = 5, children = null, ...rest } = props;
|
||||
const { currentOffer = null, currentPage = null, selectCatalogOffer = null } = useCatalog();
|
||||
const { currentOffer = null, currentPage = null } = useCatalogData();
|
||||
const { selectCatalogOffer = null } = useCatalogActions();
|
||||
const catalogAdmin = useCatalogAdmin();
|
||||
const adminMode = catalogAdmin?.adminMode ?? false;
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const [ dragIndex, setDragIndex ] = useState<number | null>(null);
|
||||
const [ dropIndex, setDropIndex ] = useState<number | null>(null);
|
||||
|
||||
@@ -25,13 +26,13 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
|
||||
if(elementRef && elementRef.current) elementRef.current.scrollTop = 0;
|
||||
}, [ currentPage ]);
|
||||
|
||||
if(!currentPage) return null;
|
||||
|
||||
const selectOffer = (offer: IPurchasableOffer) =>
|
||||
{
|
||||
selectCatalogOffer(offer);
|
||||
};
|
||||
|
||||
// Drag-and-drop handlers — hooks MUST run unconditionally so the
|
||||
// hook order stays stable when currentPage flips from null to a
|
||||
// real value (the `if(!currentPage) return null` below would
|
||||
// otherwise hide these from the first render and React would flag
|
||||
// "Rendered more hooks than during the previous render"). Bodies
|
||||
// are safe to evaluate pre-load: currentPage? optional chaining
|
||||
// already guards the only access inside handleDrop.
|
||||
const handleDragStart = useCallback((index: number) =>
|
||||
{
|
||||
setDragIndex(index);
|
||||
@@ -67,6 +68,13 @@ export const CatalogItemGridWidgetView: FC<CatalogItemGridWidgetViewProps> = pro
|
||||
setDropIndex(null);
|
||||
}, []);
|
||||
|
||||
if(!currentPage) return null;
|
||||
|
||||
const selectOffer = (offer: IPurchasableOffer) =>
|
||||
{
|
||||
selectCatalogOffer(offer);
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoGrid columnCount={ columnCount } innerRef={ elementRef } { ...rest }>
|
||||
{ currentPage.offers && (currentPage.offers.length > 0) && currentPage.offers.map((offer, index) =>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FC } from 'react';
|
||||
import { Offer } from '../../../../../api';
|
||||
import { LayoutLimitedEditionCompletePlateView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
|
||||
export const CatalogLimitedItemWidgetView: FC = props =>
|
||||
{
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
|
||||
if(!currentOffer || (currentOffer.pricingModel !== Offer.PRICING_MODEL_SINGLE) || !currentOffer.product.isUniqueLimitedItem) return null;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { IPurchasableOffer } from '../../../../../api';
|
||||
import { LayoutCurrencyIcon, Text } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogUiState } from '../../../../../hooks';
|
||||
|
||||
interface CatalogPriceDisplayWidgetViewProps
|
||||
{
|
||||
@@ -13,7 +13,7 @@ interface CatalogPriceDisplayWidgetViewProps
|
||||
export const CatalogPriceDisplayWidgetView: FC<CatalogPriceDisplayWidgetViewProps> = props =>
|
||||
{
|
||||
const { offer = null, separator = false } = props;
|
||||
const { purchaseOptions = null } = useCatalog();
|
||||
const { purchaseOptions = null } = useCatalogUiState();
|
||||
const { quantity = 1 } = purchaseOptions;
|
||||
|
||||
if(!offer) return null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { BuilderFurniPlaceableStatus, CatalogPurchaseState, CatalogType, DispatchUiEvent, GetClubMemberLevel, LocalStorageKeys, LocalizeText, NotificationBubbleType, Offer, ProductTypeEnum, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, LayoutLoadingSpinnerView, Text } from '../../../../../common';
|
||||
import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, CatalogPurchasedEvent } from '../../../../../events';
|
||||
import { useCatalog, useLocalStorage, useNotification, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
import { useCatalogActions, useCatalogData, useCatalogUiState, useLocalStorage, useNotification, usePurse, useUiEvent } from '../../../../../hooks';
|
||||
|
||||
interface CatalogPurchaseWidgetViewProps
|
||||
{
|
||||
@@ -20,7 +20,9 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
const [ purchaseWillBeGift, setPurchaseWillBeGift ] = useState(false);
|
||||
const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE);
|
||||
const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useLocalStorage(LocalStorageKeys.CATALOG_SKIP_PURCHASE_CONFIRMATION, false);
|
||||
const { currentOffer = null, currentPage = null, currentType = CatalogType.NORMAL, purchaseOptions = null, setPurchaseOptions = null, requestOfferToMover = null, setCatalogPlaceMultipleObjects = null, getBuilderFurniPlaceableStatus = null } = useCatalog();
|
||||
const { currentOffer = null, currentPage = null } = useCatalogData();
|
||||
const { currentType = CatalogType.NORMAL, purchaseOptions = null, setPurchaseOptions = null, setCatalogPlaceMultipleObjects = null } = useCatalogUiState();
|
||||
const { requestOfferToMover = null, getBuilderFurniPlaceableStatus = null, getNodesByOfferId = null } = useCatalogActions();
|
||||
const { getCurrencyAmount = null } = usePurse();
|
||||
const { showSingleBubble = null } = useNotification();
|
||||
|
||||
@@ -89,7 +91,10 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
isPurchasingCatalogItem = true;
|
||||
setPurchaseState(CatalogPurchaseState.PURCHASE);
|
||||
|
||||
setTimeout(() => { isPurchasingCatalogItem = false; }, 10000);
|
||||
setTimeout(() =>
|
||||
{
|
||||
isPurchasingCatalogItem = false;
|
||||
}, 10000);
|
||||
|
||||
if(purchaseCallback)
|
||||
{
|
||||
@@ -100,12 +105,11 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
|
||||
let pageId = currentOffer.page.pageId;
|
||||
|
||||
// if(pageId === -1)
|
||||
// {
|
||||
// const nodes = getNodesByOfferId(currentOffer.offerId);
|
||||
|
||||
// if(nodes) pageId = nodes[0].pageId;
|
||||
// }
|
||||
if(pageId === -1 && getNodesByOfferId)
|
||||
{
|
||||
const nodes = getNodesByOfferId(currentOffer.offerId);
|
||||
if(nodes && nodes.length) pageId = nodes[0].pageId;
|
||||
}
|
||||
|
||||
SendMessageComposer(new PurchaseFromCatalogComposer(pageId, currentOffer.offerId, purchaseOptions.extraData, purchaseOptions.quantity));
|
||||
};
|
||||
@@ -132,15 +136,19 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
};
|
||||
}, [ purchaseState ]);
|
||||
|
||||
if(!currentOffer) return null;
|
||||
|
||||
// Builders-club state — derived + hooks MUST run unconditionally on
|
||||
// every render so the hook order stays stable even when currentOffer
|
||||
// is null (the `if(!currentOffer) return null` below would otherwise
|
||||
// hide the useMemo/useEffect block from the first render and React
|
||||
// would flag "Rendered more hooks than during the previous render").
|
||||
const isBuildersClubOffer = (currentType === CatalogType.BUILDER);
|
||||
const isBuildersClubPlaceable = isBuildersClubOffer
|
||||
&& !!currentOffer
|
||||
&& !!currentOffer.product
|
||||
&& ((currentOffer.product.productType === ProductTypeEnum.FLOOR) || (currentOffer.product.productType === ProductTypeEnum.WALL));
|
||||
const builderPlaceableStatus = useMemo(() =>
|
||||
{
|
||||
if(!isBuildersClubPlaceable || !getBuilderFurniPlaceableStatus) return BuilderFurniPlaceableStatus.OKAY;
|
||||
if(!isBuildersClubPlaceable || !getBuilderFurniPlaceableStatus || !currentOffer) return BuilderFurniPlaceableStatus.OKAY;
|
||||
|
||||
return getBuilderFurniPlaceableStatus(currentOffer);
|
||||
}, [ currentOffer, getBuilderFurniPlaceableStatus, isBuildersClubPlaceable, builderPlaceableRefreshTick ]);
|
||||
@@ -159,6 +167,8 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
|
||||
return () => clearInterval(interval);
|
||||
}, [ isBuildersClubPlaceable ]);
|
||||
|
||||
if(!currentOffer) return null;
|
||||
|
||||
const PurchaseButton = () =>
|
||||
{
|
||||
if(isBuildersClubPlaceable)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC } from 'react';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
|
||||
|
||||
export const CatalogSimplePriceWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
|
||||
return (
|
||||
<div className="flex items-center bg-muted p-1 rounded gap-1">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { IPurchasableOffer, LocalizeText, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { AutoGrid, AutoGridProps, Button } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
import { CatalogGridOfferView } from '../common/CatalogGridOfferView';
|
||||
|
||||
interface CatalogSpacesWidgetViewProps extends AutoGridProps
|
||||
@@ -17,8 +17,9 @@ export const CatalogSpacesWidgetView: FC<CatalogSpacesWidgetViewProps> = props =
|
||||
const [ groupedOffers, setGroupedOffers ] = useState<IPurchasableOffer[][]>(null);
|
||||
const [ selectedGroupIndex, setSelectedGroupIndex ] = useState(-1);
|
||||
const [ selectedOfferForGroup, setSelectedOfferForGroup ] = useState<IPurchasableOffer[]>(null);
|
||||
const { currentPage = null, currentOffer = null, setCurrentOffer = null, setPurchaseOptions = null } = useCatalog();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
const { currentPage = null, currentOffer = null } = useCatalogData();
|
||||
const { setCurrentOffer = null, setPurchaseOptions = null } = useCatalogUiState();
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const setSelectedOffer = (offer: IPurchasableOffer) =>
|
||||
{
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { FC } from 'react';
|
||||
import { FaMinus, FaPlus } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
|
||||
const MIN_VALUE: number = 1;
|
||||
const MAX_VALUE: number = 99;
|
||||
|
||||
export const CatalogSpinnerWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentOffer = null, purchaseOptions = null, setPurchaseOptions = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
const { purchaseOptions = null, setPurchaseOptions = null } = useCatalogUiState();
|
||||
const { quantity = 1 } = purchaseOptions;
|
||||
|
||||
const updateQuantity = (value: number) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC } from 'react';
|
||||
import { Column, ColumnProps } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData } from '../../../../../hooks';
|
||||
import { CatalogPriceDisplayWidgetView } from './CatalogPriceDisplayWidgetView';
|
||||
|
||||
interface CatalogSimplePriceWidgetViewProps extends ColumnProps
|
||||
@@ -10,7 +10,7 @@ interface CatalogSimplePriceWidgetViewProps extends ColumnProps
|
||||
export const CatalogTotalPriceWidget: FC<CatalogSimplePriceWidgetViewProps> = props =>
|
||||
{
|
||||
const { gap = 1, ...rest } = props;
|
||||
const { currentOffer = null } = useCatalog();
|
||||
const { currentOffer = null } = useCatalogData();
|
||||
|
||||
return (
|
||||
<Column gap={ gap } { ...rest }>
|
||||
|
||||
@@ -2,11 +2,12 @@ import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrot
|
||||
import { FC, useEffect } from 'react';
|
||||
import { BuildPurchasableClothingFigure, FurniCategory, Offer, ProductTypeEnum } from '../../../../../api';
|
||||
import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common';
|
||||
import { useCatalog } from '../../../../../hooks';
|
||||
import { useCatalogData, useCatalogUiState } from '../../../../../hooks';
|
||||
|
||||
export const CatalogViewProductWidgetView: FC<{}> = props =>
|
||||
{
|
||||
const { currentOffer = null, roomPreviewer = null, purchaseOptions = null } = useCatalog();
|
||||
const { currentOffer = null, roomPreviewer = null } = useCatalogData();
|
||||
const { purchaseOptions = null } = useCatalogUiState();
|
||||
const { previewStuffData = null } = purchaseOptions;
|
||||
|
||||
useEffect(() =>
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import { GetTargetedOfferComposer, TargetedOfferData, TargetedOfferEvent } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SendMessageComposer } from '../../../../api';
|
||||
import { useMessageEvent } from '../../../../hooks';
|
||||
import { useState } from 'react';
|
||||
import { useNitroQuery } from '../../../../api/nitro-query';
|
||||
import { OfferBubbleView } from './OfferBubbleView';
|
||||
import { OfferWindowView } from './OfferWindowView';
|
||||
|
||||
export const OfferView = () =>
|
||||
{
|
||||
const [ offer, setOffer ] = useState<TargetedOfferData>(null);
|
||||
const [ opened, setOpened ] = useState<boolean>(false);
|
||||
|
||||
useMessageEvent<TargetedOfferEvent>(TargetedOfferEvent, evt =>
|
||||
{
|
||||
let parser = evt.getParser();
|
||||
|
||||
if(!parser) return;
|
||||
|
||||
setOffer(parser.data);
|
||||
const { data: offer } = useNitroQuery<TargetedOfferEvent, TargetedOfferData>({
|
||||
key: [ 'nitro', 'catalog', 'targeted-offer' ],
|
||||
request: () => new GetTargetedOfferComposer(),
|
||||
parser: TargetedOfferEvent,
|
||||
select: evt => evt.getParser()?.data ?? null,
|
||||
staleTime: Infinity
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
SendMessageComposer(new GetTargetedOfferComposer());
|
||||
}, []);
|
||||
const [ opened, setOpened ] = useState<boolean>(false);
|
||||
|
||||
if(!offer) return;
|
||||
if(!offer) return null;
|
||||
|
||||
return <>
|
||||
{ opened ? <OfferWindowView offer={ offer } setOpen={ setOpened } /> : <OfferBubbleView offer={ offer } setOpen={ setOpened } /> }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{ opened
|
||||
? <OfferWindowView offer={ offer } setOpen={ setOpened } />
|
||||
: <OfferBubbleView offer={ offer } setOpen={ setOpened } /> }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user