feat: new lifetime badge design
This commit is contained in:
parent
e11f9bc52e
commit
76ec07da6e
@ -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`}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user