WIP preserve local changes before duckie merge

This commit is contained in:
Lorenzune
2026-04-21 11:13:32 +02:00
parent e0174e450c
commit 9b36513def
74 changed files with 4419 additions and 408 deletions
@@ -1,24 +1,29 @@
import { FC, useEffect, useState } from 'react';
import { FC, useEffect, useMemo, useState } from 'react';
import { FaTrashAlt } from 'react-icons/fa';
import { IPrefixItem, LocalizeText, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
import { useInventoryPrefixes, useNotification } from '../../../../hooks';
import { IPrefixItem, LocalizeText, parsePrefixColors, getPrefixEffectStyle, getPrefixFontStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api';
import { Button } from '../../../../common';
import { GetNickIconUrl } from '../../../../assets/images/user_custom/nick_icons';
import { useInventoryNickIcons, useInventoryPrefixes, useNotification } from '../../../../hooks';
import { NitroButton } from '../../../../layout';
const PrefixPreview: FC<{ text: string; color: string; icon: string; effect?: string; className?: string; textSize?: string }> = ({ text, color, icon, effect = '', className = '', textSize = 'text-sm' }) =>
type InventoryIdentityTab = 'prefixes' | 'icons';
const PrefixPreview: FC<{ text: string; color: string; icon: string; effect?: string; font?: string; className?: string; textSize?: string }> = ({ text, color, icon, effect = '', font = '', className = '', textSize = 'text-sm' }) =>
{
const colors = parsePrefixColors(text, color);
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
const fxStyle = getPrefixEffectStyle(effect, colors[0] || '#FFFFFF');
const fontStyle = getPrefixFontStyle(font);
return (
<span className={ `font-bold ${ textSize } ${ className }` } style={ fxStyle }>
{ effect === 'pulse' && <style>{ PREFIX_EFFECT_KEYFRAMES }</style> }
<span className={ `font-bold ${ textSize } ${ className }` } style={ { ...fontStyle, ...fxStyle } }>
{ !!effect && <style>{ PREFIX_EFFECT_KEYFRAMES }</style> }
{ icon && <span className="mr-0.5">{ icon }</span> }
<span style={ hasMultiColor ? fxStyle : { ...fxStyle, color: colors[0] || '#FFFFFF' } }>
<span style={ hasMultiColor ? { ...fontStyle, ...fxStyle } : { ...fontStyle, ...fxStyle, color: colors[0] || '#FFFFFF' } }>
{'{'}
{ hasMultiColor
? [ ...text ].map((char, i) => (
<span key={ i } style={ { color: colors[i] || colors[colors.length - 1], ...getPrefixEffectStyle(effect, colors[i]) } }>{ char }</span>
<span key={ i } style={ { ...fontStyle, color: colors[i] || colors[colors.length - 1], ...getPrefixEffectStyle(effect, colors[i]) } }>{ char }</span>
))
: text
}
@@ -40,7 +45,30 @@ const PrefixItemView: FC<{
${ isSelected ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
${ prefix.active ? 'ring-2 ring-green-400' : '' }` }
onClick={ onClick }>
<PrefixPreview className="truncate" color={ prefix.color } effect={ prefix.effect } icon={ prefix.icon } text={ prefix.text } />
<PrefixPreview className="truncate" color={ prefix.color } effect={ prefix.effect } font={ prefix.font } icon={ prefix.icon } text={ prefix.text } />
</div>
);
};
const NickIconItemView: FC<{
iconKey: string;
displayName: string;
isSelected: boolean;
isActive: boolean;
onClick: () => void;
}> = ({ iconKey, displayName, isSelected, isActive, onClick }) =>
{
return (
<div
className={ `relative flex cursor-pointer items-center justify-center rounded-md border-2 p-2 transition-colors
${ isSelected ? 'border-card-grid-item-active bg-card-grid-item-active' : 'border-card-grid-item-border bg-card-grid-item' }
${ isActive ? 'ring-2 ring-green-400' : '' }` }
onClick={ onClick }>
{ isActive && <span className="absolute right-1 top-1 rounded bg-[#15954c] px-1 py-0.5 text-[8px] font-bold uppercase text-white">Active</span> }
<div className="flex flex-col items-center gap-1">
<img className="h-auto max-h-[28px] w-auto object-contain" src={ GetNickIconUrl(iconKey) } alt={ displayName || iconKey } />
<span className="max-w-[90px] truncate text-center text-[11px] font-bold">{ displayName || iconKey }</span>
</div>
</div>
);
};
@@ -48,8 +76,13 @@ const PrefixItemView: FC<{
export const InventoryPrefixView: FC<{}> = () =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ activeTab, setActiveTab ] = useState<InventoryIdentityTab>('prefixes');
const { prefixes = [], activePrefix = null, selectedPrefix = null, setSelectedPrefix = null, activatePrefix = null, deactivatePrefix = null, deletePrefix = null, activate = null, deactivate = null } = useInventoryPrefixes();
const { nickIcons = [], activeNickIcon = null, selectedNickIcon = null, setSelectedNickIcon = null, activateNickIcon = null, deactivateNickIcon = null, activate: activateNickIcons = null, deactivate: deactivateNickIcons = null } = useInventoryNickIcons();
const { showConfirm = null } = useNotification();
const hasPrefixes = prefixes && (prefixes.length > 0);
const hasNickIcons = nickIcons && (nickIcons.length > 0);
const selectedIconUrl = useMemo(() => selectedNickIcon ? GetNickIconUrl(selectedNickIcon.iconKey) : '', [ selectedNickIcon ]);
const attemptDeletePrefix = () =>
{
@@ -69,10 +102,15 @@ export const InventoryPrefixView: FC<{}> = () =>
{
if(!isVisible) return;
const id = activate();
const prefixVisibilityId = activate();
const iconVisibilityId = activateNickIcons();
return () => deactivate(id);
}, [ isVisible, activate, deactivate ]);
return () =>
{
deactivate(prefixVisibilityId);
deactivateNickIcons(iconVisibilityId);
};
}, [ isVisible, activate, activateNickIcons, deactivate, deactivateNickIcons ]);
useEffect(() =>
{
@@ -82,55 +120,115 @@ export const InventoryPrefixView: FC<{}> = () =>
}, []);
return (
<div className="grid h-full grid-cols-12 gap-2">
<div className="flex flex-col col-span-7 gap-1 overflow-auto">
<div className="grid grid-cols-3 gap-1">
{ prefixes.map(prefix => (
<PrefixItemView
key={ prefix.id }
isSelected={ selectedPrefix?.id === prefix.id }
prefix={ prefix }
onClick={ () => setSelectedPrefix(prefix) } />
)) }
<div className="flex h-full flex-col gap-2">
<div className="shrink-0 rounded border border-black/10 bg-[#C9C9C9] p-1">
<div className="flex items-center gap-2">
<button
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activeTab === 'prefixes' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
type="button"
onClick={ () => setActiveTab('prefixes') }>
Prefixes
</button>
<button
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activeTab === 'icons' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
type="button"
onClick={ () => setActiveTab('icons') }>
Icons
</button>
</div>
{ (!prefixes || prefixes.length === 0) &&
<div className="flex items-center justify-center h-full text-sm opacity-50">
{ LocalizeText('inventory.empty.title') }
</div> }
</div>
<div className="flex flex-col justify-between col-span-5 overflow-auto">
{ activePrefix &&
<div className="flex flex-col gap-1">
<span className="text-sm truncate min-h-[1.25rem] leading-5">Active prefix</span>
<div className="flex items-center justify-center p-3 rounded-md border-2 border-green-400 bg-card-grid-item">
<PrefixPreview color={ activePrefix.color } effect={ activePrefix.effect } icon={ activePrefix.icon } text={ activePrefix.text } textSize="text-lg" />
{ activeTab === 'prefixes' &&
<div className="grid h-full grid-cols-12 gap-2">
<div className="col-span-7 flex flex-col gap-1 overflow-auto pr-1">
<div className="grid grid-cols-3 gap-1">
{ prefixes.map(prefix => (
<PrefixItemView
key={ prefix.id }
isSelected={ selectedPrefix?.id === prefix.id }
prefix={ prefix }
onClick={ () => setSelectedPrefix(prefix) } />
)) }
</div>
</div> }
{ !activePrefix &&
<div className="flex flex-col gap-1">
<span className="text-sm truncate min-h-[1.25rem] leading-5">Active prefix</span>
<div className="flex items-center justify-center p-3 rounded-md border-2 border-dashed border-card-grid-item-border bg-card-grid-item opacity-50">
<span className="text-sm">No active prefix</span>
{ !hasPrefixes &&
<div className="flex h-full items-center justify-center text-sm opacity-50">
{ LocalizeText('inventory.empty.title') }
</div> }
</div>
<div className="col-span-5 flex flex-col justify-between overflow-auto">
{ activePrefix &&
<div className="flex flex-col gap-1">
<span className="min-h-[1.25rem] truncate text-sm leading-5">Active prefix</span>
<div className="flex items-center justify-center rounded-md border-2 border-green-400 bg-card-grid-item p-3">
<PrefixPreview color={ activePrefix.color } effect={ activePrefix.effect } font={ activePrefix.font } icon={ activePrefix.icon } text={ activePrefix.text } textSize="text-lg" />
</div>
</div> }
{ !activePrefix &&
<div className="flex flex-col gap-1">
<span className="min-h-[1.25rem] truncate text-sm leading-5">Active prefix</span>
<div className="flex items-center justify-center rounded-md border-2 border-dashed border-card-grid-item-border bg-card-grid-item p-3 opacity-50">
<span className="text-sm">No active prefix</span>
</div>
</div> }
{ !!selectedPrefix &&
<div className="mt-2 flex flex-col gap-2">
<div className="flex items-center justify-center gap-2 rounded bg-card-grid-item p-2">
<PrefixPreview color={ selectedPrefix.color } effect={ selectedPrefix.effect } font={ selectedPrefix.font } icon={ selectedPrefix.icon } text={ selectedPrefix.text } textSize="text-lg" />
</div>
<div className="flex items-center gap-2">
<NitroButton
className="grow"
onClick={ () => selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }>
{ selectedPrefix.active ? 'Deactivate' : 'Activate' }
</NitroButton>
{ !selectedPrefix.active &&
<NitroButton className="bg-danger! hover:bg-danger/80! p-1" onClick={ attemptDeletePrefix }>
<FaTrashAlt className="fa-icon" />
</NitroButton> }
</div>
</div> }
</div>
</div> }
{ activeTab === 'icons' &&
<div className="grid h-full grid-cols-12 gap-2">
<div className="col-span-7 flex flex-col gap-1 overflow-auto pr-1">
<div className="grid grid-cols-3 gap-1">
{ nickIcons.map(icon => (
<NickIconItemView
key={ icon.id }
displayName={ icon.displayName }
iconKey={ icon.iconKey }
isActive={ !!icon.active }
isSelected={ selectedNickIcon?.id === icon.id }
onClick={ () => setSelectedNickIcon(icon) } />
)) }
</div>
</div> }
{ !!selectedPrefix &&
<div className="flex flex-col gap-2 mt-2">
<div className="flex items-center justify-center gap-2 p-2 rounded bg-card-grid-item">
<PrefixPreview color={ selectedPrefix.color } effect={ selectedPrefix.effect } icon={ selectedPrefix.icon } text={ selectedPrefix.text } textSize="text-lg" />
{ !hasNickIcons &&
<div className="flex h-full items-center justify-center text-sm opacity-50">
No purchased icons yet
</div> }
</div>
<div className="col-span-5 flex flex-col justify-between overflow-auto">
<div className="flex flex-col gap-1">
<span className="min-h-[1.25rem] truncate text-sm leading-5">Active icon</span>
<div className={ `flex min-h-[88px] items-center justify-center rounded-md border-2 bg-card-grid-item p-3 ${ activeNickIcon ? 'border-green-400' : 'border-dashed border-card-grid-item-border opacity-50' }` }>
{ activeNickIcon && <img className="h-auto max-h-[36px] w-auto object-contain" src={ GetNickIconUrl(activeNickIcon.iconKey) } alt={ activeNickIcon.displayName || activeNickIcon.iconKey } /> }
{ !activeNickIcon && <span className="text-sm">No active icon</span> }
</div>
</div>
<div className="flex items-center gap-2">
<NitroButton
className="grow"
onClick={ () => selectedPrefix.active ? deactivatePrefix() : activatePrefix(selectedPrefix.id) }>
{ selectedPrefix.active ? 'Deactivate' : 'Activate' }
</NitroButton>
{ !selectedPrefix.active &&
<NitroButton className="bg-danger! hover:bg-danger/80! p-1" onClick={ attemptDeletePrefix }>
<FaTrashAlt className="fa-icon" />
</NitroButton> }
</div>
</div> }
</div>
{ !!selectedNickIcon &&
<div className="mt-2 flex flex-col gap-2">
<div className="flex min-h-[100px] flex-col items-center justify-center gap-2 rounded bg-card-grid-item p-3 text-center">
<img className="h-auto max-h-[40px] w-auto object-contain" src={ selectedIconUrl } alt={ selectedNickIcon.displayName || selectedNickIcon.iconKey } />
<span className="text-sm font-bold">{ selectedNickIcon.displayName || selectedNickIcon.iconKey }</span>
</div>
<Button disabled={ false } onClick={ () => selectedNickIcon.active ? deactivateNickIcon() : activateNickIcon(selectedNickIcon.id) }>
{ selectedNickIcon.active ? 'Deactivate' : 'Activate' }
</Button>
</div> }
</div>
</div> }
</div>
);
};