mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
4d81fe7c82
@emoji-mart/data (~430 KB / ~82 KB gzip) was pulled into the initial load by three always-mounted views (catalog custom-prefix, nick-icon, chat-input emoji selector) importing it statically. Add LazyEmojiPicker — a Suspense wrapper that dynamically imports the data + Picker on first open — and use it in those three sites. Verified via the build manifest: the entry chunk no longer imports vendor-emoji statically or dynamically; vendor-emoji is now a dynamicImport of src/index.tsx, loaded on first picker open. No behaviour change. typecheck 0, tests 545/545.
592 lines
34 KiB
TypeScript
592 lines
34 KiB
TypeScript
import { AddLinkEventTracker, CustomPrefixPurchaseFailedEvent, ILinkEventTracker, PurchaseCatalogPrefixComposer, PurchaseNickIconComposer, PurchasePrefixComposer, RemoveLinkEventTracker, RequestNickIconsComposer, SetActiveNickIconComposer, SetActivePrefixComposer, SetDisplayOrderComposer, UserNickIconsEvent } from '@nitrots/nitro-renderer';
|
|
import { LazyEmojiPicker } from '../../common/LazyEmojiPicker';
|
|
import { FC, useEffect, useMemo, useState } from 'react';
|
|
import { INickIconItem, IPrefixItem, PRESET_PREFIX_EFFECTS, PRESET_PREFIX_FONTS, SendMessageComposer, getPrefixEffectStyle, getPrefixFontStyle, parsePrefixColors } from '../../api';
|
|
import { GetNickIconUrl } from '../../assets/images/user_custom/nick_icons';
|
|
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text, UserIdentityView } from '../../common';
|
|
import { LayoutCurrencyIcon } from '../../common/layout/LayoutCurrencyIcon';
|
|
import { useMessageEvent } from '../../hooks';
|
|
|
|
type CustomizeTab = 'icons' | 'prefix' | 'settings';
|
|
type PrefixSubTab = 'library' | 'custom';
|
|
|
|
interface ICatalogPrefixItem extends IPrefixItem
|
|
{
|
|
points: number;
|
|
pointsType: number;
|
|
owned: boolean;
|
|
ownedPrefixId: number;
|
|
}
|
|
|
|
interface ICombinedPrefixItem extends IPrefixItem
|
|
{
|
|
points: number;
|
|
pointsType: number;
|
|
owned: boolean;
|
|
ownedPrefixId: number;
|
|
}
|
|
|
|
const ORDER_LABELS: Record<string, string> = {
|
|
'icon-prefix-name': 'Icon / Prefix / Name',
|
|
'prefix-icon-name': 'Prefix / Icon / Name',
|
|
'name-icon-prefix': 'Name / Icon / Prefix',
|
|
'name-prefix-icon': 'Name / Prefix / Icon',
|
|
'icon-name-prefix': 'Icon / Name / Prefix',
|
|
'prefix-name-icon': 'Prefix / Name / Icon'
|
|
};
|
|
|
|
const PRESET_COLORS: string[] = [
|
|
'#D62828', '#E85D04', '#F77F00', '#2A9D8F',
|
|
'#0077B6', '#4361EE', '#6A4C93', '#C1121F',
|
|
'#B5179E', '#3A86FF', '#3F8E00', '#8D5524'
|
|
];
|
|
|
|
export const CustomizeNickIconView: FC<{}> = () =>
|
|
{
|
|
const [ isVisible, setIsVisible ] = useState(false);
|
|
const [ isLoading, setIsLoading ] = useState(false);
|
|
const [ activeTab, setActiveTab ] = useState<CustomizeTab>('icons');
|
|
const [ activePrefixSubTab, setActivePrefixSubTab ] = useState<PrefixSubTab>('library');
|
|
const [ iconItems, setIconItems ] = useState<INickIconItem[]>([]);
|
|
const [ prefixItems, setPrefixItems ] = useState<IPrefixItem[]>([]);
|
|
const [ catalogPrefixes, setCatalogPrefixes ] = useState<ICatalogPrefixItem[]>([]);
|
|
const [ displayOrder, setDisplayOrder ] = useState('icon-prefix-name');
|
|
const [ customPrefixMaxLength, setCustomPrefixMaxLength ] = useState(15);
|
|
const [ customPrefixPriceCredits, setCustomPrefixPriceCredits ] = useState(0);
|
|
const [ customPrefixPricePoints, setCustomPrefixPricePoints ] = useState(0);
|
|
const [ customPrefixPointsType, setCustomPrefixPointsType ] = useState(0);
|
|
const [ customPrefixFontPriceCredits, setCustomPrefixFontPriceCredits ] = useState(0);
|
|
const [ customPrefixFontPricePoints, setCustomPrefixFontPricePoints ] = useState(0);
|
|
const [ customPrefixFontPointsType, setCustomPrefixFontPointsType ] = useState(0);
|
|
const [ customPrefixText, setCustomPrefixText ] = useState('');
|
|
const [ customPrefixColor, setCustomPrefixColor ] = useState('#FFFFFF');
|
|
const [ customPrefixIcon, setCustomPrefixIcon ] = useState('');
|
|
const [ customPrefixEffect, setCustomPrefixEffect ] = useState('');
|
|
const [ customPrefixFont, setCustomPrefixFont ] = useState('');
|
|
const [ showEmojiPicker, setShowEmojiPicker ] = useState(false);
|
|
|
|
useMessageEvent<CustomPrefixPurchaseFailedEvent>(CustomPrefixPurchaseFailedEvent, () =>
|
|
{
|
|
setIsLoading(false);
|
|
setIsVisible(false);
|
|
});
|
|
|
|
useMessageEvent<UserNickIconsEvent>(UserNickIconsEvent, event =>
|
|
{
|
|
const parser = event.getParser();
|
|
|
|
setIconItems(parser.nickIcons.map(icon => ({
|
|
id: icon.id,
|
|
iconKey: icon.iconKey,
|
|
displayName: icon.displayName,
|
|
points: icon.points,
|
|
pointsType: icon.pointsType,
|
|
owned: icon.owned,
|
|
active: icon.active
|
|
})));
|
|
setPrefixItems(parser.ownedPrefixes.map(prefix => ({
|
|
id: prefix.id,
|
|
displayName: prefix.displayName,
|
|
text: prefix.text,
|
|
color: prefix.color,
|
|
icon: prefix.icon || '',
|
|
effect: prefix.effect || '',
|
|
font: prefix.font || '',
|
|
active: prefix.active,
|
|
isCustom: prefix.isCustom,
|
|
points: prefix.points,
|
|
pointsType: prefix.pointsType,
|
|
catalogPrefixId: prefix.catalogPrefixId
|
|
})));
|
|
setCatalogPrefixes(parser.prefixCatalog.map(prefix => ({
|
|
id: prefix.id,
|
|
displayName: prefix.displayName,
|
|
text: prefix.text,
|
|
color: prefix.color,
|
|
icon: prefix.icon || '',
|
|
effect: prefix.effect || '',
|
|
font: prefix.font || '',
|
|
active: prefix.active,
|
|
points: prefix.points,
|
|
pointsType: prefix.pointsType,
|
|
owned: prefix.owned,
|
|
ownedPrefixId: prefix.ownedPrefixId
|
|
})));
|
|
setDisplayOrder(parser.displayOrder || 'icon-prefix-name');
|
|
setCustomPrefixMaxLength(parser.customPrefixMaxLength || 15);
|
|
setCustomPrefixPriceCredits(parser.customPrefixPriceCredits || 0);
|
|
setCustomPrefixPricePoints(parser.customPrefixPricePoints || 0);
|
|
setCustomPrefixPointsType(parser.customPrefixPointsType || 0);
|
|
setCustomPrefixFontPriceCredits(parser.customPrefixFontPriceCredits || 0);
|
|
setCustomPrefixFontPricePoints(parser.customPrefixFontPricePoints || 0);
|
|
setCustomPrefixFontPointsType(parser.customPrefixFontPointsType || 0);
|
|
setIsLoading(false);
|
|
});
|
|
|
|
useEffect(() =>
|
|
{
|
|
const linkTracker: ILinkEventTracker = {
|
|
linkReceived: (url: string) =>
|
|
{
|
|
const parts = url.split('/');
|
|
|
|
if(parts.length < 2) return;
|
|
|
|
switch(parts[1])
|
|
{
|
|
case 'show':
|
|
setIsVisible(true);
|
|
return;
|
|
case 'hide':
|
|
setIsVisible(false);
|
|
return;
|
|
case 'toggle':
|
|
setIsVisible(previousValue => !previousValue);
|
|
return;
|
|
}
|
|
},
|
|
eventUrlPrefix: 'customize/'
|
|
};
|
|
|
|
AddLinkEventTracker(linkTracker);
|
|
|
|
return () => RemoveLinkEventTracker(linkTracker);
|
|
}, []);
|
|
|
|
useEffect(() =>
|
|
{
|
|
if(!isVisible) return;
|
|
|
|
setIsLoading(true);
|
|
SendMessageComposer(new RequestNickIconsComposer());
|
|
}, [ isVisible ]);
|
|
|
|
const activeIcon = useMemo(() => iconItems.find(item => item.active) || null, [ iconItems ]);
|
|
const activePrefix = useMemo(() => prefixItems.find(item => item.active) || null, [ prefixItems ]);
|
|
const combinedPrefixes = useMemo(() =>
|
|
{
|
|
const ownedByCatalogId = new Map<number, IPrefixItem>();
|
|
|
|
for(const prefix of prefixItems)
|
|
{
|
|
if(prefix.catalogPrefixId && (prefix.catalogPrefixId > 0)) ownedByCatalogId.set(prefix.catalogPrefixId, prefix);
|
|
}
|
|
|
|
const catalogEntries: ICombinedPrefixItem[] = catalogPrefixes.map(prefix =>
|
|
{
|
|
const ownedPrefix = ownedByCatalogId.get(prefix.id);
|
|
|
|
return {
|
|
id: ownedPrefix?.id || prefix.id,
|
|
displayName: ownedPrefix?.displayName || prefix.displayName,
|
|
text: ownedPrefix?.text || prefix.text,
|
|
color: ownedPrefix?.color || prefix.color,
|
|
icon: ownedPrefix?.icon || prefix.icon,
|
|
effect: ownedPrefix?.effect || prefix.effect,
|
|
font: ownedPrefix?.font || prefix.font,
|
|
active: ownedPrefix?.active || prefix.active,
|
|
isCustom: false,
|
|
points: prefix.points,
|
|
pointsType: prefix.pointsType,
|
|
catalogPrefixId: prefix.id,
|
|
owned: prefix.owned || !!ownedPrefix,
|
|
ownedPrefixId: prefix.ownedPrefixId || ownedPrefix?.id || 0
|
|
};
|
|
});
|
|
|
|
const customEntries: ICombinedPrefixItem[] = prefixItems
|
|
.filter(prefix => !prefix.catalogPrefixId || (prefix.catalogPrefixId <= 0))
|
|
.map(prefix => ({
|
|
id: prefix.id,
|
|
displayName: prefix.displayName,
|
|
text: prefix.text,
|
|
color: prefix.color,
|
|
icon: prefix.icon,
|
|
effect: prefix.effect,
|
|
font: prefix.font || '',
|
|
active: prefix.active,
|
|
isCustom: true,
|
|
points: prefix.points || customPrefixPricePoints,
|
|
pointsType: prefix.pointsType || customPrefixPointsType,
|
|
catalogPrefixId: 0,
|
|
owned: true,
|
|
ownedPrefixId: prefix.id
|
|
}));
|
|
|
|
return [ ...catalogEntries, ...customEntries ];
|
|
}, [ catalogPrefixes, customPrefixPointsType, customPrefixPricePoints, prefixItems ]);
|
|
const selectedEffectOption = useMemo(() => PRESET_PREFIX_EFFECTS.find(effect => effect.id === customPrefixEffect) || PRESET_PREFIX_EFFECTS[0], [ customPrefixEffect ]);
|
|
const selectedFontOption = useMemo(() => PRESET_PREFIX_FONTS.find(font => font.id === customPrefixFont) || PRESET_PREFIX_FONTS[0], [ customPrefixFont ]);
|
|
const basicEffects = useMemo(() => PRESET_PREFIX_EFFECTS.filter(effect => effect.tier === 'basic'), []);
|
|
const premiumEffects = useMemo(() => PRESET_PREFIX_EFFECTS.filter(effect => effect.tier === 'premium'), []);
|
|
const basicFonts = useMemo(() => PRESET_PREFIX_FONTS.filter(font => font.tier === 'basic'), []);
|
|
const premiumFonts = useMemo(() => PRESET_PREFIX_FONTS.filter(font => font.tier === 'premium'), []);
|
|
const prefixPreviewColors = useMemo(() => parsePrefixColors(customPrefixText || 'Preview', customPrefixColor || '#FFFFFF'), [ customPrefixText, customPrefixColor ]);
|
|
const customPrefixPreviewStyle = useMemo(() => getPrefixEffectStyle(customPrefixEffect, prefixPreviewColors[0] || '#FFFFFF'), [ customPrefixEffect, prefixPreviewColors ]);
|
|
const customPrefixFontStyle = useMemo(() => getPrefixFontStyle(customPrefixFont), [ customPrefixFont ]);
|
|
const customPrefixTotalCredits = useMemo(() => customPrefixPriceCredits + (customPrefixFont ? customPrefixFontPriceCredits : 0), [ customPrefixFont, customPrefixFontPriceCredits, customPrefixPriceCredits ]);
|
|
const customPrefixTotalPoints = useMemo(() => customPrefixPricePoints + ((customPrefixFont && (customPrefixFontPointsType === customPrefixPointsType)) ? customPrefixFontPricePoints : 0), [ customPrefixFont, customPrefixFontPointsType, customPrefixFontPricePoints, customPrefixPointsType, customPrefixPricePoints ]);
|
|
const customPrefixIsValid = useMemo(() =>
|
|
{
|
|
const trimmed = customPrefixText.trim();
|
|
|
|
if(!trimmed.length || (trimmed.length > customPrefixMaxLength)) return false;
|
|
|
|
return customPrefixColor.split(',').every(color => /^#[0-9A-Fa-f]{6}$/.test(color));
|
|
}, [ customPrefixColor, customPrefixMaxLength, customPrefixText ]);
|
|
|
|
const refreshCustomizeData = () =>
|
|
{
|
|
setIsLoading(true);
|
|
SendMessageComposer(new RequestNickIconsComposer());
|
|
};
|
|
|
|
const handleIconAction = (item: INickIconItem) =>
|
|
{
|
|
setIsLoading(true);
|
|
|
|
if(!item.owned)
|
|
{
|
|
SendMessageComposer(new PurchaseNickIconComposer(item.iconKey));
|
|
return;
|
|
}
|
|
|
|
SendMessageComposer(new SetActiveNickIconComposer(item.active ? 0 : item.id));
|
|
};
|
|
|
|
const handleCombinedPrefixAction = (item: ICombinedPrefixItem) =>
|
|
{
|
|
setIsLoading(true);
|
|
|
|
if(item.owned)
|
|
{
|
|
SendMessageComposer(new SetActivePrefixComposer(item.active ? 0 : item.ownedPrefixId));
|
|
return;
|
|
}
|
|
|
|
SendMessageComposer(new PurchaseCatalogPrefixComposer(item.catalogPrefixId || item.id));
|
|
};
|
|
|
|
const handleCustomPrefixPurchase = () =>
|
|
{
|
|
if(!customPrefixIsValid) return;
|
|
|
|
setIsLoading(true);
|
|
SendMessageComposer(new PurchasePrefixComposer(customPrefixText.trim(), customPrefixColor, customPrefixIcon, customPrefixEffect, customPrefixFont));
|
|
};
|
|
|
|
const handleDisplayOrderChange = (nextDisplayOrder: string) =>
|
|
{
|
|
if(nextDisplayOrder === displayOrder) return;
|
|
|
|
setDisplayOrder(nextDisplayOrder);
|
|
setIsLoading(true);
|
|
SendMessageComposer(new SetDisplayOrderComposer(nextDisplayOrder));
|
|
};
|
|
|
|
if(!isVisible) return null;
|
|
|
|
return (
|
|
<NitroCardView className="customize-nick-icon-window w-[680px] max-w-[95vw]" theme="primary-slim" uniqueKey="customize-nick-icons">
|
|
<NitroCardHeaderView headerText="Customize Bubble Identity" onCloseClick={ () => setIsVisible(false) } />
|
|
<NitroCardTabsView>
|
|
<NitroCardTabsItemView isActive={ activeTab === 'icons' } onClick={ () => setActiveTab('icons') }>
|
|
<Text>Icons</Text>
|
|
</NitroCardTabsItemView>
|
|
<NitroCardTabsItemView isActive={ activeTab === 'prefix' } onClick={ () => setActiveTab('prefix') }>
|
|
<Text>Prefix</Text>
|
|
</NitroCardTabsItemView>
|
|
<NitroCardTabsItemView isActive={ activeTab === 'settings' } onClick={ () => setActiveTab('settings') }>
|
|
<Text>Settings</Text>
|
|
</NitroCardTabsItemView>
|
|
</NitroCardTabsView>
|
|
<NitroCardContentView className="flex max-h-[78vh] flex-col gap-3 overflow-y-auto text-black">
|
|
<div className="rounded border border-black/10 bg-black/5 p-3">
|
|
<Text bold>Live preview</Text>
|
|
<div className="mt-2 flex min-h-[54px] items-center justify-center rounded border border-black/10 bg-[#cfe8fb] px-3 py-2 text-[#1f2937]">
|
|
<UserIdentityView
|
|
displayOrder={ displayOrder }
|
|
nickIcon={ activeIcon?.iconKey || '' }
|
|
prefixColor={ activePrefix?.color || customPrefixColor }
|
|
prefixEffect={ activePrefix?.effect || customPrefixEffect }
|
|
prefixFont={ activePrefix?.font || customPrefixFont }
|
|
prefixIcon={ activePrefix?.icon || customPrefixIcon }
|
|
prefixText={ activePrefix?.text || customPrefixText }
|
|
username="Username" />
|
|
</div>
|
|
</div>
|
|
|
|
{ activeTab === 'icons' &&
|
|
<>
|
|
<div className="rounded border border-black/10 bg-black/5 p-2 text-[11px] leading-4">
|
|
Choose the icon shown in your bubble identity.
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{ iconItems.map(item =>
|
|
{
|
|
const iconUrl = GetNickIconUrl(item.iconKey);
|
|
|
|
return (
|
|
<div
|
|
key={ item.iconKey }
|
|
className={ `relative flex min-h-[126px] flex-col items-center justify-between gap-2 rounded border p-3 transition-colors ${ item.active ? 'border-[#1e7295] bg-[#dff3fb]' : 'border-black/10 bg-black/5' }` }>
|
|
{ item.active && <span className="absolute right-1 top-1 rounded bg-[#1e7295] px-1.5 py-0.5 text-[9px] font-bold uppercase text-white">Active</span> }
|
|
<img className="h-auto max-h-[28px] w-auto object-contain" src={ iconUrl } alt={ item.iconKey } />
|
|
<div className="flex flex-col items-center gap-1 text-center text-[11px]">
|
|
<span>{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' }</span>
|
|
<span className="max-w-[140px] truncate">{ item.displayName || `Icon #${ item.iconKey }` }</span>
|
|
<span className="inline-flex items-center gap-1">
|
|
<LayoutCurrencyIcon type={ item.pointsType } />
|
|
{ item.points }
|
|
</span>
|
|
</div>
|
|
<Button disabled={ isLoading } onClick={ () => handleIconAction(item) }>
|
|
{ !item.owned && 'Buy' }
|
|
{ item.owned && !item.active && 'Activate' }
|
|
{ item.owned && item.active && 'Deactivate' }
|
|
</Button>
|
|
</div>
|
|
);
|
|
}) }
|
|
</div>
|
|
</> }
|
|
|
|
{ activeTab === 'prefix' &&
|
|
<div className="flex flex-col gap-3">
|
|
<div className="rounded border border-black/10 bg-black/5 p-1">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activePrefixSubTab === 'library' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
|
|
type="button"
|
|
onClick={ () => setActivePrefixSubTab('library') }>
|
|
Library
|
|
</button>
|
|
<button
|
|
className={ `rounded px-3 py-1.5 text-[11px] font-bold transition-colors ${ activePrefixSubTab === 'custom' ? 'bg-[#1e7295] text-white' : 'bg-white text-black' }` }
|
|
type="button"
|
|
onClick={ () => setActivePrefixSubTab('custom') }>
|
|
Custom
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{ activePrefixSubTab === 'library' &&
|
|
<>
|
|
<div className="rounded border border-black/10 bg-black/5 p-2 text-[11px] leading-4">
|
|
Choose a preset or custom prefix for your bubble identity.
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{ combinedPrefixes.map(item => (
|
|
<div key={ `${ item.catalogPrefixId || 'custom' }-${ item.ownedPrefixId || item.id }` } className={ `relative flex min-h-[96px] flex-col gap-2 rounded border p-2.5 ${ item.active ? 'border-[#1e7295] bg-[#dff3fb]' : 'border-black/10 bg-black/5' }` }>
|
|
{ item.active && <span className="absolute right-1 top-1 rounded bg-[#1e7295] px-1.5 py-0.5 text-[9px] font-bold uppercase text-white">Active</span> }
|
|
<UserIdentityView
|
|
displayOrder={ displayOrder }
|
|
nickIcon={ activeIcon?.iconKey || '' }
|
|
prefixColor={ item.color }
|
|
prefixEffect={ item.effect }
|
|
prefixFont={ item.font || '' }
|
|
prefixIcon={ item.icon }
|
|
prefixText={ item.text }
|
|
username="Username" />
|
|
<div className="flex flex-col gap-1 text-[11px]">
|
|
<span>{ item.owned ? (item.active ? 'Owned - Active' : 'Owned') : 'Locked' }</span>
|
|
<span className="truncate">{ item.displayName || item.text }{ item.isCustom ? ' - Custom' : '' }</span>
|
|
<span className="inline-flex items-center gap-1">
|
|
<LayoutCurrencyIcon type={ item.pointsType } />
|
|
{ item.points }
|
|
</span>
|
|
</div>
|
|
<Button disabled={ isLoading } onClick={ () => handleCombinedPrefixAction(item) }>
|
|
{ !item.owned && 'Buy' }
|
|
{ item.owned && !item.active && 'Activate' }
|
|
{ item.owned && item.active && 'Deactivate' }
|
|
</Button>
|
|
</div>
|
|
)) }
|
|
</div>
|
|
</> }
|
|
|
|
{ activePrefixSubTab === 'custom' &&
|
|
<div className="rounded border border-black/10 bg-black/5 p-3">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<Text bold>Custom prefix</Text>
|
|
<Button disabled={ isLoading } onClick={ refreshCustomizeData }>Refresh</Button>
|
|
</div>
|
|
<div className="mt-2 flex flex-col gap-2">
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
className="flex-1 rounded border border-black/10 bg-white px-3 py-2 text-sm"
|
|
maxLength={ customPrefixMaxLength }
|
|
placeholder="Enter your prefix"
|
|
type="text"
|
|
value={ customPrefixText }
|
|
onChange={ event => setCustomPrefixText(event.target.value) } />
|
|
<span className="text-[11px] text-black/60">{ customPrefixText.length }/{ customPrefixMaxLength }</span>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<button className="rounded border border-black/10 bg-white px-3 py-2 text-sm" type="button" onClick={ () => setShowEmojiPicker(true) }>
|
|
{ customPrefixIcon || 'Emoji' }
|
|
</button>
|
|
{ !!customPrefixIcon && <Button onClick={ () => setCustomPrefixIcon('') }>Clear</Button> }
|
|
</div>
|
|
<div className="rounded border border-black/10 bg-white p-2">
|
|
<div className="mb-2 text-[11px] leading-4 text-black/70">
|
|
Safe colors only, chosen to stay readable on both light and dark backgrounds.
|
|
</div>
|
|
<div className="grid grid-cols-6 gap-2">
|
|
{ PRESET_COLORS.map(color => (
|
|
<button
|
|
key={ color }
|
|
className={ `flex h-[28px] items-center justify-center rounded border text-[10px] font-bold uppercase ${ customPrefixColor === color ? 'border-[#1e7295] ring-1 ring-[#1e7295]' : 'border-black/10' }` }
|
|
style={ { backgroundColor: color } }
|
|
type="button"
|
|
onClick={ () => setCustomPrefixColor(color) }>
|
|
{ customPrefixColor === color ? 'ON' : '' }
|
|
</button>
|
|
)) }
|
|
</div>
|
|
</div>
|
|
<div className="rounded border border-black/10 bg-white p-2">
|
|
<div className="mb-2 text-[11px] leading-4 text-black/70">
|
|
Effect
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
className="flex-1 rounded border border-black/10 bg-white px-2 py-2 text-sm"
|
|
value={ customPrefixEffect }
|
|
onChange={ event => setCustomPrefixEffect(event.target.value) }>
|
|
<optgroup label="Basic">
|
|
{ basicEffects.map(effect => (
|
|
<option key={ effect.id || 'none' } value={ effect.id }>
|
|
{ effect.label }
|
|
</option>
|
|
)) }
|
|
</optgroup>
|
|
<optgroup label="Premium">
|
|
{ premiumEffects.map(effect => (
|
|
<option key={ effect.id } value={ effect.id }>
|
|
{ effect.label }
|
|
</option>
|
|
)) }
|
|
</optgroup>
|
|
</select>
|
|
<div className="min-w-[130px] rounded border border-black/10 bg-black/5 px-2 py-2 text-center text-[11px] font-bold">
|
|
{ selectedEffectOption.icon } { selectedEffectOption.label }
|
|
<div className="mt-1 text-[9px] uppercase text-black/60">
|
|
{ selectedEffectOption.tier }
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="rounded border border-black/10 bg-white p-2">
|
|
<div className="mb-2 text-[11px] leading-4 text-black/70">
|
|
Font
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
className="flex-1 rounded border border-black/10 bg-white px-2 py-2 text-sm"
|
|
value={ customPrefixFont }
|
|
onChange={ event => setCustomPrefixFont(event.target.value) }>
|
|
<optgroup label="Basic">
|
|
{ basicFonts.map(font => (
|
|
<option key={ font.id || 'default' } value={ font.id }>
|
|
{ font.label }
|
|
</option>
|
|
)) }
|
|
</optgroup>
|
|
<optgroup label="Premium">
|
|
{ premiumFonts.map(font => (
|
|
<option key={ font.id } value={ font.id }>
|
|
{ font.label }
|
|
</option>
|
|
)) }
|
|
</optgroup>
|
|
</select>
|
|
<div className="min-w-[130px] rounded border border-black/10 bg-black/5 px-2 py-2 text-center text-[11px] font-bold">
|
|
<span style={ customPrefixFontStyle }>{ selectedFontOption.label }</span>
|
|
<div className="mt-1 text-[9px] uppercase text-black/60">
|
|
{ selectedFontOption.tier }
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{ !!customPrefixFont &&
|
|
<div className="mt-2 text-[10px] leading-4 text-black/60">
|
|
Premium fonts add an extra price on top of the custom prefix.
|
|
</div> }
|
|
</div>
|
|
<div className="rounded border border-black/10 bg-[#cfe8fb] px-3 py-2 text-[#1f2937]" style={ customPrefixPreviewStyle }>
|
|
<UserIdentityView
|
|
displayOrder={ displayOrder }
|
|
nickIcon={ activeIcon?.iconKey || '' }
|
|
prefixColor={ customPrefixColor }
|
|
prefixEffect={ customPrefixEffect }
|
|
prefixFont={ customPrefixFont }
|
|
prefixIcon={ customPrefixIcon }
|
|
prefixText={ customPrefixText || 'Preview' }
|
|
username="Username" />
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2 text-[12px]">
|
|
{ customPrefixTotalCredits > 0 && <span>{ customPrefixTotalCredits } credits</span> }
|
|
{ customPrefixTotalPoints > 0 &&
|
|
<span className="inline-flex items-center gap-1">
|
|
<LayoutCurrencyIcon type={ customPrefixPointsType } />
|
|
{ customPrefixTotalPoints }
|
|
</span> }
|
|
{ !!customPrefixFont && (customPrefixFontPointsType !== customPrefixPointsType) && (customPrefixFontPricePoints > 0) &&
|
|
<span className="inline-flex items-center gap-1">
|
|
<LayoutCurrencyIcon type={ customPrefixFontPointsType } />
|
|
{ customPrefixFontPricePoints }
|
|
</span> }
|
|
</div>
|
|
<Button disabled={ !customPrefixIsValid || isLoading } onClick={ handleCustomPrefixPurchase }>
|
|
Buy custom prefix
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div> }
|
|
</div> }
|
|
|
|
{ activeTab === 'settings' &&
|
|
<div className="flex flex-col gap-3">
|
|
<div className="rounded border border-black/10 bg-black/5 p-3">
|
|
<Text bold>Display order</Text>
|
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
|
{ Object.entries(ORDER_LABELS).map(([ key, label ]) => (
|
|
<Button key={ key } disabled={ isLoading && (displayOrder === key) } onClick={ () => handleDisplayOrderChange(key) }>
|
|
{ displayOrder === key ? '* ' : '' }{ label }
|
|
</Button>
|
|
)) }
|
|
</div>
|
|
</div>
|
|
<div className="rounded border border-black/10 bg-black/5 p-3">
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<Text bold>Refresh data</Text>
|
|
<Button disabled={ isLoading } onClick={ refreshCustomizeData }>Refresh</Button>
|
|
</div>
|
|
<div className="text-[11px] leading-4 text-black/70">
|
|
Use this tab to control how your icon, prefix and username are ordered in bubbles, profile and infostand.
|
|
</div>
|
|
</div>
|
|
</div> }
|
|
</NitroCardContentView>
|
|
{ showEmojiPicker &&
|
|
<>
|
|
<div className="fixed inset-0 z-[999]" onClick={ () => setShowEmojiPicker(false) } />
|
|
<div className="fixed left-1/2 top-1/2 z-[1000] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-xl shadow-2xl">
|
|
<LazyEmojiPicker
|
|
locale="en"
|
|
onEmojiSelect={ (emoji: { native: string }) =>
|
|
{
|
|
setCustomPrefixIcon(emoji.native); setShowEmojiPicker(false);
|
|
} }
|
|
previewPosition="none"
|
|
set="native"
|
|
theme="dark" />
|
|
</div>
|
|
</> }
|
|
</NitroCardView>
|
|
);
|
|
};
|
|
|