🆙 Update Purse

This commit is contained in:
duckietm
2026-06-09 13:55:33 +02:00
parent 3e92e718e5
commit 3ea6ec70a4
8 changed files with 841 additions and 446 deletions
+164
View File
@@ -0,0 +1,164 @@
import { CreateLinkEvent, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaChevronDown, FaLanguage, FaQuestionCircle, FaSignOutAlt } from 'react-icons/fa';
import { ClearRememberLogin, FriendlyTime, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api';
import { Column, Flex, LayoutCurrencyIcon, Text } from '../../common';
import { usePurse } from '../../hooks';
import purseIcon from '../../assets/images/rightside/purse.gif';
import { CurrencyView } from './views/CurrencyView';
import { SeasonalView } from './views/SeasonalView';
export const PurseClassicView: FC<{}> = props =>
{
const { purse = null, hcDisabled = false } = usePurse();
const [ isOpen, setIsOpen ] = useState(true);
const [ isCompact, setIsCompact ] = useState(false);
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
const getClubText = (() =>
{
if (!purse) return null;
const totalDays = ((purse.clubPeriods * 31) + purse.clubDays);
const minutesUntilExpiration = purse.minutesUntilExpiration;
if (purse.clubLevel === HabboClubLevelEnum.NO_CLUB) return LocalizeText('purse.clubdays.zero.amount.text');
else if ((minutesUntilExpiration > -1) && (minutesUntilExpiration < (60 * 24))) return FriendlyTime.shortFormat(minutesUntilExpiration * 60);
else return FriendlyTime.shortFormat(totalDays * 86400);
})();
const currencyTypes = useMemo(() =>
{
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return [];
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
types.sort((a, b) =>
{
if (a === 0) return -1;
if (b === 0) return 1;
if (a === 5) return -1;
if (b === 5) return 1;
return a - b;
});
return types;
}, [ displayedCurrencies, purse ]);
const primaryCurrencies = currencyTypes.slice(0, 2);
const seasonalCurrencies = currencyTypes.slice(2);
useEffect(() =>
{
if(isOpen)
{
setIsCompact(false);
return;
}
const timeout = window.setTimeout(() => setIsCompact(true), 220);
return () => window.clearTimeout(timeout);
}, [ isOpen ]);
const handleLogout = useCallback(async (event: React.MouseEvent) =>
{
event.stopPropagation();
const logoutUrl = GetConfigurationValue<string>('login.logout.endpoint', '/api/auth/logout');
const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? '';
const rememberToken = GetRememberLogin()?.token || '';
try
{
await fetch(logoutUrl, {
method: 'POST',
credentials: 'include',
keepalive: true,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'NitroPurseLogout'
},
body: JSON.stringify({ ssoTicket, rememberToken })
});
}
catch {}
ClearRememberLogin();
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
window.location.reload();
}, []);
if (!purse) return null;
return (
<Column alignItems="end" className="nitro-purse-container" gap={ 0 }>
<div className="nitro-purse-classic">
<div className={ `nitro-purse-shell ${ isCompact ? 'is-closed' : '' }` }>
<div className={ `nitro-purse ${ isCompact ? 'is-closed' : '' }` }>
<div className={ `nitro-purse__header ${ isCompact ? 'is-closed' : '' }` } onClick={ () => setIsOpen(value => !value) }>
<Flex alignItems="center" gap={ 1 } className={ isCompact ? 'nitro-purse__header-main is-closed' : 'nitro-purse__header-main' }>
<div className="nitro-purse__header-icon">
<img src={ purseIcon } alt="" className="nitro-purse__header-image" />
</div>
</Flex>
<div className={ `nitro-purse__header-toggle ${ isOpen ? 'is-open' : '' }` }>
<FaChevronDown className="fa-icon text-[10px]" />
</div>
</div>
<div className={ `nitro-purse__content ${ isOpen ? 'is-open' : 'is-closed' }` }>
<div className={ `nitro-purse__summary nitro-purse__summary--compact ${ hcDisabled ? 'is-no-hc' : '' }` }>
<div className="nitro-purse__primary">
<CurrencyView type={ -1 } amount={ purse.credits } short={ currencyDisplayNumberShort } />
{ primaryCurrencies.map(type => <CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } short={ currencyDisplayNumberShort } />) }
</div>
{ !hcDisabled &&
<div className="nitro-purse-subscription" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('catalog/open/' + GetConfigurationValue<Record<string, string>>('catalog.links')?.['hc.buy_hc']);
} }>
<div className="nitro-purse-subscription__icon">
<LayoutCurrencyIcon type="hc" />
</div>
<div className="nitro-purse-subscription__copy">
<Text variant="white" className="nitro-purse-subscription__label">HC</Text>
<Text variant="white" className="nitro-purse-subscription__value">{ getClubText }</Text>
</div>
</div> }
<div className="nitro-purse__actions">
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--translate" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('translation-settings/toggle');
} } title="Google Translate">
<FaLanguage />
</button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--help" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('help/show');
} } title={ LocalizeText('help.button.name') }>
<FaQuestionCircle />
</button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--settings" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('user-settings/toggle');
} } title={ LocalizeText('widget.memenu.settings.title') }>
<i className="nitro-icon icon-cog" />
</button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--logout" onClick={ handleLogout } title="Log out">
<FaSignOutAlt />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{ seasonalCurrencies.length > 0 &&
<div className="nitro-purse__other nitro-purse__other--classic">
{ seasonalCurrencies.map(type => <SeasonalView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } />) }
</div> }
</Column>
);
};
+138
View File
@@ -0,0 +1,138 @@
import { CreateLinkEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo } from 'react';
import { FaChartBar, FaCog, FaSignOutAlt } from 'react-icons/fa';
import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api';
import { Column, LayoutCurrencyIcon } from '../../common';
import { usePurse } from '../../hooks';
import { CurrencyView } from './views/CurrencyView';
import { SeasonalView } from './views/SeasonalView';
const localizeWithFallback = (key: string, fallback: string) =>
{
const text = LocalizeText(key);
return (text && text !== key) ? text : fallback;
};
export const PurseModernView: FC<{}> = props =>
{
const { purse = null, hcDisabled = false } = usePurse();
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
const currencyTypes = useMemo(() =>
{
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return [];
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
types.sort((a, b) =>
{
if (a === 0) return -1;
if (b === 0) return 1;
if (a === 5) return -1;
if (b === 5) return 1;
return a - b;
});
return types;
}, [ displayedCurrencies, purse ]);
const hasDiamonds = currencyTypes.indexOf(5) >= 0;
const hasDuckets = currencyTypes.indexOf(0) >= 0;
const otherCurrencies = currencyTypes.filter(type => (type !== 0 && type !== 5));
const joinLabel = useMemo(() => localizeWithFallback('purse.join', 'Join'), []);
const earningsLabel = useMemo(() => localizeWithFallback('earnings.title', 'Earnings'), []);
const helpLabel = useMemo(() => localizeWithFallback('help.button.name', 'Help'), []);
const openClub = useCallback((event: React.MouseEvent) =>
{
event.stopPropagation();
const page = GetConfigurationValue<string>('hc.buy_hc', 'habbo_club');
CreateLinkEvent('catalog/open/' + page);
}, []);
const openEarnings = useCallback((event: React.MouseEvent) =>
{
event.stopPropagation();
CreateLinkEvent('habboUI/open/vault');
}, []);
const handleLogout = useCallback(async (event: React.MouseEvent) =>
{
event.stopPropagation();
const logoutUrl = GetConfigurationValue<string>('login.logout.endpoint', '/api/auth/logout');
const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? '';
const rememberToken = GetRememberLogin()?.token || '';
try
{
await fetch(logoutUrl, {
method: 'POST',
credentials: 'include',
keepalive: true,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'NitroPurseLogout'
},
body: JSON.stringify({ ssoTicket, rememberToken })
});
}
catch { }
ClearRememberLogin();
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
window.location.reload();
}, []);
if (!purse) return null;
return (
<Column alignItems="end" className="nitro-purse-container" gap={ 0 }>
<div className="nitro-purse">
<div className="nitro-purse__body">
<div className="nitro-purse__currencies">
{ hasDiamonds && <CurrencyView type={ 5 } amount={ purse.activityPoints.get(5) || 0 } short={ currencyDisplayNumberShort } /> }
<CurrencyView type={ -1 } amount={ purse.credits } short={ currencyDisplayNumberShort } />
{ hasDuckets && <CurrencyView type={ 0 } amount={ purse.activityPoints.get(0) || 0 } short={ currencyDisplayNumberShort } /> }
</div>
<div className="nitro-purse__col nitro-purse__col--primary">
{ !hcDisabled &&
<button type="button" className="nitro-purse__btn nitro-purse__btn--join" onClick={ openClub } title={ joinLabel }>
<LayoutCurrencyIcon type="hc" />
<span>{ joinLabel }</span>
</button> }
<button type="button" className="nitro-purse__btn nitro-purse__btn--earnings" onClick={ openEarnings } title={ earningsLabel }>
<FaChartBar className="nitro-purse__btn-icon" />
<span>{ earningsLabel }</span>
</button>
</div>
<div className="nitro-purse__col nitro-purse__col--actions">
<button type="button" className="nitro-purse__btn nitro-purse__btn--help" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('help/show');
} } title={ helpLabel }>
<span>{ helpLabel }</span>
</button>
<button type="button" className="nitro-purse__btn nitro-purse__btn--icon nitro-purse__btn--logout" onClick={ handleLogout } title="Log out">
<FaSignOutAlt />
</button>
<button type="button" className="nitro-purse__btn nitro-purse__btn--icon nitro-purse__btn--settings" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('user-settings/toggle');
} } title={ LocalizeText('widget.memenu.settings.title') }>
<FaCog />
</button>
</div>
</div>
</div>
{ otherCurrencies.length > 0 &&
<div className="nitro-purse__other">
{ otherCurrencies.map(type => <SeasonalView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } />) }
</div> }
</Column>
);
};
+6 -158
View File
@@ -1,163 +1,11 @@
import { CreateLinkEvent, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaChevronDown, FaLanguage, FaQuestionCircle, FaSignOutAlt } from 'react-icons/fa';
import { ClearRememberLogin, FriendlyTime, GetConfigurationValue, GetRememberLogin, LocalizeText } from '../../api';
import { Column, Flex, LayoutCurrencyIcon, Text } from '../../common';
import { usePurse } from '../../hooks';
import purseIcon from '../../assets/images/rightside/purse.gif';
import { CurrencyView } from './views/CurrencyView';
import { SeasonalView } from './views/SeasonalView';
import { FC } from 'react';
import { useCatalogClassicStyle } from '../../hooks';
import { PurseClassicView } from './PurseClassicView';
import { PurseModernView } from './PurseModernView';
export const PurseView: FC<{}> = props =>
{
const { purse = null, hcDisabled = false } = usePurse();
const [ isOpen, setIsOpen ] = useState(true);
const [ isCompact, setIsCompact ] = useState(false);
const [ classicStyle ] = useCatalogClassicStyle();
const displayedCurrencies = useMemo(() => GetConfigurationValue<number[]>('system.currency.types', []), []);
const currencyDisplayNumberShort = useMemo(() => GetConfigurationValue<boolean>('currency.display.number.short', false), []);
const getClubText = (() =>
{
if (!purse) return null;
const totalDays = ((purse.clubPeriods * 31) + purse.clubDays);
const minutesUntilExpiration = purse.minutesUntilExpiration;
if (purse.clubLevel === HabboClubLevelEnum.NO_CLUB) return LocalizeText('purse.clubdays.zero.amount.text');
else if ((minutesUntilExpiration > -1) && (minutesUntilExpiration < (60 * 24))) return FriendlyTime.shortFormat(minutesUntilExpiration * 60);
else return FriendlyTime.shortFormat(totalDays * 86400);
})();
const currencyTypes = useMemo(() =>
{
if (!purse || !purse.activityPoints || !purse.activityPoints.size) return [];
const types = Array.from(purse.activityPoints.keys()).filter(type => (displayedCurrencies.indexOf(type) >= 0));
types.sort((a, b) =>
{
if (a === 0) return -1;
if (b === 0) return 1;
if (a === 5) return -1;
if (b === 5) return 1;
return a - b;
});
return types;
}, [ displayedCurrencies, purse ]);
const primaryCurrencies = currencyTypes.slice(0, 2);
const seasonalCurrencies = currencyTypes.slice(2);
useEffect(() =>
{
if(isOpen)
{
setIsCompact(false);
return;
}
const timeout = window.setTimeout(() => setIsCompact(true), 220);
return () => window.clearTimeout(timeout);
}, [ isOpen ]);
const handleLogout = useCallback(async (event: React.MouseEvent) =>
{
event.stopPropagation();
const logoutUrl = GetConfigurationValue<string>('login.logout.endpoint', '/api/auth/logout');
const ssoTicket = (window.NitroConfig?.['sso.ticket'] as string) ?? '';
const rememberToken = GetRememberLogin()?.token || '';
try
{
await fetch(logoutUrl, {
method: 'POST',
credentials: 'include',
keepalive: true,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'NitroPurseLogout'
},
body: JSON.stringify({ ssoTicket, rememberToken })
});
}
catch
{ /* best-effort — proceed with local logout regardless */ }
ClearRememberLogin();
if(window.NitroConfig) window.NitroConfig['sso.ticket'] = '';
window.location.reload();
}, []);
if (!purse) return null;
return (
<Column alignItems="end" className="nitro-purse-container" gap={ 0 }>
<div className={ `nitro-purse-shell ${ isCompact ? 'is-closed' : '' }` }>
<div className={ `nitro-purse ${ isCompact ? 'is-closed' : '' }` }>
<div className={ `nitro-purse__header ${ isCompact ? 'is-closed' : '' }` } onClick={ () => setIsOpen(value => !value) }>
<Flex alignItems="center" gap={ 1 } className={ isCompact ? 'nitro-purse__header-main is-closed' : 'nitro-purse__header-main' }>
<div className="nitro-purse__header-icon">
<img src={ purseIcon } alt="" className="nitro-purse__header-image" />
</div>
</Flex>
<div className={ `nitro-purse__header-toggle ${ isOpen ? 'is-open' : '' }` }>
<FaChevronDown className="fa-icon text-[10px]" />
</div>
</div>
<div className={ `nitro-purse__content ${ isOpen ? 'is-open' : 'is-closed' }` }>
<div className={ `nitro-purse__summary nitro-purse__summary--compact ${ hcDisabled ? 'is-no-hc' : '' }` }>
<div className="nitro-purse__primary">
<CurrencyView type={ -1 } amount={ purse.credits } short={ currencyDisplayNumberShort } />
{ primaryCurrencies.map(type => <CurrencyView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } short={ currencyDisplayNumberShort } />) }
</div>
{ !hcDisabled &&
<div className="nitro-purse-subscription" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('habboUI/open/hccenter');
} }>
<div className="nitro-purse-subscription__icon">
<LayoutCurrencyIcon type="hc" />
</div>
<div className="nitro-purse-subscription__copy">
<Text variant="white" className="nitro-purse-subscription__label">HC</Text>
<Text variant="white" className="nitro-purse-subscription__value">{ getClubText }</Text>
</div>
</div> }
<div className="nitro-purse__actions">
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--translate" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('translation-settings/toggle');
} } title="Google Translate">
<FaLanguage />
</button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--help" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('help/show');
} } title={ LocalizeText('help.button.name') }>
<FaQuestionCircle />
</button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--settings" onClick={ event =>
{
event.stopPropagation(); CreateLinkEvent('user-settings/toggle');
} } title={ LocalizeText('widget.memenu.settings.title') }>
<i className="nitro-icon icon-cog" />
</button>
<button type="button" className="nitro-purse__action-button nitro-purse__action-button--logout" onClick={ handleLogout } title="Log out">
<FaSignOutAlt />
</button>
</div>
</div>
{ seasonalCurrencies.length > 0 &&
<div className="nitro-purse__seasonal">
{ seasonalCurrencies.map(type => <SeasonalView key={ type } type={ type } amount={ purse.activityPoints.get(type) || 0 } />) }
</div> }
</div>
</div>
</div>
</Column>
);
return classicStyle ? <PurseClassicView /> : <PurseModernView />;
};
+3 -3
View File
@@ -21,9 +21,6 @@ export const SeasonalView: FC<SeasonalViewProps> = props =>
className={`nitro-purse-seasonal-currency nitro-notification ${seasonalColor}`}
>
<Flex fullWidth className="seasonal-row">
<Flex className="nitro-seasonal-box seasonal-image-padding">
<img src={ iconUrl } alt="" className="seasonal-image" />
</Flex>
<Text truncate fullWidth variant="white" className="seasonal-text-padding seasonal-text">
{LocalizeText(`purse.seasonal.currency.${type}`)}
</Text>
@@ -34,6 +31,9 @@ export const SeasonalView: FC<SeasonalViewProps> = props =>
>
{formattedAmount}
</Text>
<Flex className="nitro-seasonal-box seasonal-image-padding">
<img src={ iconUrl } alt="" className="seasonal-image" />
</Flex>
</Flex>
</Flex>
);