feat: new lifetime badge design

This commit is contained in:
Hampus Kraft 2026-02-20 23:32:31 +00:00
parent e11f9bc52e
commit 76ec07da6e
No known key found for this signature in database
GPG Key ID: 6090864C465A454D
5 changed files with 109 additions and 33 deletions

View File

@ -19,6 +19,7 @@
import * as DeveloperOptionsActionCreators from '@app/actions/DeveloperOptionsActionCreators';
import * as UserActionCreators from '@app/actions/UserActionCreators';
import {Input} from '@app/components/form/Input';
import {Select} from '@app/components/form/Select';
import {Switch} from '@app/components/form/Switch';
import {SettingsTabSection} from '@app/components/modals/shared/SettingsTabLayout';
@ -36,6 +37,7 @@ import type {MessageDescriptor} from '@lingui/core';
import {Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback} from 'react';
interface AccountPremiumTabContentProps {
user: UserRecord;
@ -132,6 +134,24 @@ export const AccountPremiumTabContent: React.FC<AccountPremiumTabContentProps> =
/>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Visionary Badge Override</Trans>}>
<Input
label={t`Visionary ID Number`}
type="number"
min={1}
placeholder={t`Leave empty for actual value`}
value={DeveloperOptionsStore.premiumLifetimeSequenceOverride?.toString() ?? ''}
onChange={useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const parsed = value === '' ? null : Number.parseInt(value, 10);
DeveloperOptionsActionCreators.updateOption(
'premiumLifetimeSequenceOverride',
parsed != null && !Number.isNaN(parsed) ? parsed : null,
);
}, [])}
/>
</SettingsTabSection>
<SettingsTabSection title={<Trans>Premium Subscription Scenarios</Trans>}>
<Select<PremiumScenarioOption>
label={t`Test Subscription State`}

View File

@ -52,41 +52,37 @@
width: 20px;
}
.virtualBadge {
.sequenceBadge {
display: inline-flex;
align-items: center;
justify-content: center;
padding-top: 1px;
color: var(--brand-primary-light);
font-weight: 700;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
color: #4641d9;
font-family: var(--font-primary);
line-height: 1;
letter-spacing: 0.05em;
font-variant-numeric: tabular-nums;
white-space: nowrap;
text-decoration: none;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
cursor: pointer;
pointer-events: none;
text-shadow:
0 0 6px var(--glow-color),
0 0 14px color-mix(in srgb, var(--glow-color) 50%, transparent);
}
.virtualBadgeMobile {
:global(.theme-light) .sequenceBadge {
color: var(--brand-primary);
text-shadow:
0 0 5px color-mix(in srgb, var(--glow-color) 60%, transparent),
0 0 12px color-mix(in srgb, var(--glow-color) 30%, transparent);
}
.sequenceBadgeMobile {
height: 28px;
min-width: 28px;
font-size: 18px;
line-height: 24px;
font-size: 24px;
}
.virtualBadgeDesktop {
.sequenceBadgeDesktop {
height: 20px;
min-width: 20px;
font-size: 14px;
line-height: 20px;
}
.virtualBadge:hover,
.virtualBadge:active {
text-decoration: none;
font-size: 17px;
}
.link {

View File

@ -33,12 +33,34 @@ import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useMemo} from 'react';
interface Badge {
interface BaseBadge {
key: string;
iconUrl: string;
tooltip: string;
url: string;
}
interface IconBadge extends BaseBadge {
type: 'icon';
iconUrl: string;
}
interface TextBadge extends BaseBadge {
type: 'text';
text: string;
glowColor: string;
}
type Badge = IconBadge | TextBadge;
const GLOW_COLORS = [
'#ff6b9d', // rose
'#c084fc', // violet
'#60a5fa', // sky blue
'#34d399', // emerald
'#fbbf24', // amber
'#f472b6', // pink
'#818cf8', // indigo
'#2dd4bf', // teal
'#fb923c', // orange
'#a78bfa', // purple
] as const;
interface UserProfileBadgesProps {
user: UserRecord;
@ -57,6 +79,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
if (user.flags & PublicUserFlags.STAFF) {
result.push({
type: 'icon',
key: 'staff',
iconUrl: cdnUrl('badges/staff.svg'),
tooltip: t`Fluxer Staff`,
@ -66,6 +89,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
if (!selfHosted && user.flags & PublicUserFlags.CTP_MEMBER) {
result.push({
type: 'icon',
key: 'ctp',
iconUrl: cdnUrl('badges/ctp.svg'),
tooltip: t`Fluxer Community Team`,
@ -75,6 +99,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
if (!selfHosted && user.flags & PublicUserFlags.PARTNER) {
result.push({
type: 'icon',
key: 'partner',
iconUrl: cdnUrl('badges/partner.svg'),
tooltip: t`Fluxer Partner`,
@ -84,6 +109,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
if (!selfHosted && user.flags & PublicUserFlags.BUG_HUNTER) {
result.push({
type: 'icon',
key: 'bug_hunter',
iconUrl: cdnUrl('badges/bug-hunter.svg'),
tooltip: t`Fluxer Bug Hunter`,
@ -109,15 +135,27 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
}
result.push({
type: 'icon',
key: 'premium',
iconUrl: cdnUrl('badges/plutonium.svg'),
tooltip: tooltipText,
url: badgeUrl,
});
if (profile.premiumType === UserPremiumTypes.LIFETIME && profile.premiumLifetimeSequence != null) {
result.push({
type: 'text',
key: 'premium_sequence',
text: `#${profile.premiumLifetimeSequence}`,
tooltip: t`Visionary ID #${profile.premiumLifetimeSequence}`,
url: badgeUrl,
glowColor: GLOW_COLORS[profile.premiumLifetimeSequence % GLOW_COLORS.length],
});
}
}
return result;
}, [selfHosted, user.flags, profile?.premiumType, profile?.premiumSince]);
}, [selfHosted, user.flags, profile?.premiumType, profile?.premiumSince, profile?.premiumLifetimeSequence]);
if (badges.length === 0) {
return null;
@ -145,7 +183,19 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
return (
<div className={containerClassName}>
{badges.map((badge) => {
const badgeContent = <img src={badge.iconUrl} alt={badge.tooltip} className={badgeClassName} />;
const sequenceClassName = isModal && isMobile ? styles.sequenceBadgeMobile : styles.sequenceBadgeDesktop;
const badgeContent =
badge.type === 'icon' ? (
<img src={badge.iconUrl} alt={badge.tooltip} className={badgeClassName} />
) : (
<span
className={clsx(styles.sequenceBadge, sequenceClassName)}
style={{'--glow-color': badge.glowColor} as React.CSSProperties}
aria-hidden="true"
>
{badge.text}
</span>
);
return (
<Tooltip key={badge.key} text={badge.tooltip} maxWidth="xl">

View File

@ -75,7 +75,7 @@ export class UserRecord {
private readonly _premiumUntil?: Date | null;
private readonly _premiumWillCancel?: boolean;
private readonly _premiumBillingCycle?: string | null;
readonly premiumLifetimeSequence?: number | null;
private readonly _premiumLifetimeSequence?: number | null;
readonly premiumBadgeHidden?: boolean;
readonly premiumBadgeMasked?: boolean;
readonly premiumBadgeTimestampHidden?: boolean;
@ -121,7 +121,7 @@ export class UserRecord {
if ('premium_until' in user) this._premiumUntil = user.premium_until ? new Date(user.premium_until) : null;
if ('premium_will_cancel' in user) this._premiumWillCancel = user.premium_will_cancel;
if ('premium_billing_cycle' in user) this._premiumBillingCycle = user.premium_billing_cycle;
if ('premium_lifetime_sequence' in user) this.premiumLifetimeSequence = user.premium_lifetime_sequence;
if ('premium_lifetime_sequence' in user) this._premiumLifetimeSequence = user.premium_lifetime_sequence;
if ('premium_badge_hidden' in user) this.premiumBadgeHidden = user.premium_badge_hidden;
if ('premium_badge_masked' in user) this.premiumBadgeMasked = user.premium_badge_masked;
if ('premium_badge_timestamp_hidden' in user)
@ -197,6 +197,11 @@ export class UserRecord {
return override != null ? override : this._premiumBillingCycle;
}
get premiumLifetimeSequence(): number | null | undefined {
const override = DeveloperOptionsStore.premiumLifetimeSequenceOverride;
return override != null ? override : this._premiumLifetimeSequence;
}
get premiumWillCancel(): boolean | undefined {
const override = DeveloperOptionsStore.premiumWillCancelOverride;
return override != null ? override : this._premiumWillCancel;
@ -339,8 +344,8 @@ export class UserRecord {
...(this._premiumBillingCycle !== undefined || updates.premium_billing_cycle !== undefined
? {premium_billing_cycle: updates.premium_billing_cycle ?? this._premiumBillingCycle}
: {}),
...(this.premiumLifetimeSequence !== undefined || updates.premium_lifetime_sequence !== undefined
? {premium_lifetime_sequence: updates.premium_lifetime_sequence ?? this.premiumLifetimeSequence}
...(this._premiumLifetimeSequence !== undefined || updates.premium_lifetime_sequence !== undefined
? {premium_lifetime_sequence: updates.premium_lifetime_sequence ?? this._premiumLifetimeSequence}
: {}),
...(this.premiumBadgeHidden !== undefined || updates.premium_badge_hidden !== undefined
? {premium_badge_hidden: updates.premium_badge_hidden ?? this.premiumBadgeHidden}
@ -520,7 +525,7 @@ export class UserRecord {
this.premiumUntil?.getTime() === other.premiumUntil?.getTime() &&
this.premiumWillCancel === other.premiumWillCancel &&
this._premiumBillingCycle === other._premiumBillingCycle &&
this.premiumLifetimeSequence === other.premiumLifetimeSequence &&
this._premiumLifetimeSequence === other._premiumLifetimeSequence &&
this.premiumBadgeHidden === other.premiumBadgeHidden &&
this.premiumBadgeMasked === other.premiumBadgeMasked &&
this.premiumBadgeTimestampHidden === other.premiumBadgeTimestampHidden &&
@ -582,7 +587,9 @@ export class UserRecord {
...(this._premiumUntil !== undefined ? {premium_until: normalizeDate(this._premiumUntil)} : {}),
...(this._premiumWillCancel !== undefined ? {premium_will_cancel: this._premiumWillCancel} : {}),
...(this._premiumBillingCycle !== undefined ? {premium_billing_cycle: this._premiumBillingCycle} : {}),
...(this.premiumLifetimeSequence !== undefined ? {premium_lifetime_sequence: this.premiumLifetimeSequence} : {}),
...(this._premiumLifetimeSequence !== undefined
? {premium_lifetime_sequence: this._premiumLifetimeSequence}
: {}),
...(this.premiumBadgeHidden !== undefined ? {premium_badge_hidden: this.premiumBadgeHidden} : {}),
...(this.premiumBadgeMasked !== undefined ? {premium_badge_masked: this.premiumBadgeMasked} : {}),
...(this.premiumBadgeTimestampHidden !== undefined

View File

@ -47,6 +47,7 @@ export type DeveloperOptionsState = Readonly<{
forceShowVanityURLDisclaimer: boolean;
forceShowVoiceConnection: boolean;
premiumTypeOverride: number | null;
premiumLifetimeSequenceOverride: number | null;
premiumSinceOverride: Date | null;
premiumUntilOverride: Date | null;
premiumBillingCycleOverride: string | null;
@ -122,6 +123,7 @@ class DeveloperOptionsStore implements DeveloperOptionsState {
forceShowVanityURLDisclaimer = false;
forceShowVoiceConnection = false;
premiumTypeOverride: number | null = null;
premiumLifetimeSequenceOverride: number | null = null;
premiumSinceOverride: Date | null = null;
premiumUntilOverride: Date | null = null;
premiumBillingCycleOverride: string | null = null;
@ -200,6 +202,7 @@ class DeveloperOptionsStore implements DeveloperOptionsState {
'forceShowVanityURLDisclaimer',
'forceShowVoiceConnection',
'premiumTypeOverride',
'premiumLifetimeSequenceOverride',
'premiumSinceOverride',
'premiumUntilOverride',
'premiumBillingCycleOverride',