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 DeveloperOptionsActionCreators from '@app/actions/DeveloperOptionsActionCreators';
|
||||||
import * as UserActionCreators from '@app/actions/UserActionCreators';
|
import * as UserActionCreators from '@app/actions/UserActionCreators';
|
||||||
|
import {Input} from '@app/components/form/Input';
|
||||||
import {Select} from '@app/components/form/Select';
|
import {Select} from '@app/components/form/Select';
|
||||||
import {Switch} from '@app/components/form/Switch';
|
import {Switch} from '@app/components/form/Switch';
|
||||||
import {SettingsTabSection} from '@app/components/modals/shared/SettingsTabLayout';
|
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 {Trans, useLingui} from '@lingui/react/macro';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import {useCallback} from 'react';
|
||||||
|
|
||||||
interface AccountPremiumTabContentProps {
|
interface AccountPremiumTabContentProps {
|
||||||
user: UserRecord;
|
user: UserRecord;
|
||||||
@ -132,6 +134,24 @@ export const AccountPremiumTabContent: React.FC<AccountPremiumTabContentProps> =
|
|||||||
/>
|
/>
|
||||||
</SettingsTabSection>
|
</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>}>
|
<SettingsTabSection title={<Trans>Premium Subscription Scenarios</Trans>}>
|
||||||
<Select<PremiumScenarioOption>
|
<Select<PremiumScenarioOption>
|
||||||
label={t`Test Subscription State`}
|
label={t`Test Subscription State`}
|
||||||
|
|||||||
@ -52,41 +52,37 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtualBadge {
|
.sequenceBadge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 1px;
|
color: var(--brand-primary-light);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
font-family: var(--font-primary);
|
||||||
color: #4641d9;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
letter-spacing: 0.05em;
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-decoration: none;
|
pointer-events: none;
|
||||||
touch-action: manipulation;
|
text-shadow:
|
||||||
user-select: none;
|
0 0 6px var(--glow-color),
|
||||||
-webkit-user-select: none;
|
0 0 14px color-mix(in srgb, var(--glow-color) 50%, transparent);
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
height: 28px;
|
||||||
min-width: 28px;
|
font-size: 24px;
|
||||||
font-size: 18px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtualBadgeDesktop {
|
.sequenceBadgeDesktop {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
min-width: 20px;
|
font-size: 17px;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtualBadge:hover,
|
|
||||||
.virtualBadge:active {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
|||||||
@ -33,12 +33,34 @@ import {observer} from 'mobx-react-lite';
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {useMemo} from 'react';
|
import {useMemo} from 'react';
|
||||||
|
|
||||||
interface Badge {
|
interface BaseBadge {
|
||||||
key: string;
|
key: string;
|
||||||
iconUrl: string;
|
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
url: 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 {
|
interface UserProfileBadgesProps {
|
||||||
user: UserRecord;
|
user: UserRecord;
|
||||||
@ -57,6 +79,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
|||||||
|
|
||||||
if (user.flags & PublicUserFlags.STAFF) {
|
if (user.flags & PublicUserFlags.STAFF) {
|
||||||
result.push({
|
result.push({
|
||||||
|
type: 'icon',
|
||||||
key: 'staff',
|
key: 'staff',
|
||||||
iconUrl: cdnUrl('badges/staff.svg'),
|
iconUrl: cdnUrl('badges/staff.svg'),
|
||||||
tooltip: t`Fluxer Staff`,
|
tooltip: t`Fluxer Staff`,
|
||||||
@ -66,6 +89,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
|||||||
|
|
||||||
if (!selfHosted && user.flags & PublicUserFlags.CTP_MEMBER) {
|
if (!selfHosted && user.flags & PublicUserFlags.CTP_MEMBER) {
|
||||||
result.push({
|
result.push({
|
||||||
|
type: 'icon',
|
||||||
key: 'ctp',
|
key: 'ctp',
|
||||||
iconUrl: cdnUrl('badges/ctp.svg'),
|
iconUrl: cdnUrl('badges/ctp.svg'),
|
||||||
tooltip: t`Fluxer Community Team`,
|
tooltip: t`Fluxer Community Team`,
|
||||||
@ -75,6 +99,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
|||||||
|
|
||||||
if (!selfHosted && user.flags & PublicUserFlags.PARTNER) {
|
if (!selfHosted && user.flags & PublicUserFlags.PARTNER) {
|
||||||
result.push({
|
result.push({
|
||||||
|
type: 'icon',
|
||||||
key: 'partner',
|
key: 'partner',
|
||||||
iconUrl: cdnUrl('badges/partner.svg'),
|
iconUrl: cdnUrl('badges/partner.svg'),
|
||||||
tooltip: t`Fluxer Partner`,
|
tooltip: t`Fluxer Partner`,
|
||||||
@ -84,6 +109,7 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
|||||||
|
|
||||||
if (!selfHosted && user.flags & PublicUserFlags.BUG_HUNTER) {
|
if (!selfHosted && user.flags & PublicUserFlags.BUG_HUNTER) {
|
||||||
result.push({
|
result.push({
|
||||||
|
type: 'icon',
|
||||||
key: 'bug_hunter',
|
key: 'bug_hunter',
|
||||||
iconUrl: cdnUrl('badges/bug-hunter.svg'),
|
iconUrl: cdnUrl('badges/bug-hunter.svg'),
|
||||||
tooltip: t`Fluxer Bug Hunter`,
|
tooltip: t`Fluxer Bug Hunter`,
|
||||||
@ -109,15 +135,27 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
|
type: 'icon',
|
||||||
key: 'premium',
|
key: 'premium',
|
||||||
iconUrl: cdnUrl('badges/plutonium.svg'),
|
iconUrl: cdnUrl('badges/plutonium.svg'),
|
||||||
tooltip: tooltipText,
|
tooltip: tooltipText,
|
||||||
url: badgeUrl,
|
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;
|
return result;
|
||||||
}, [selfHosted, user.flags, profile?.premiumType, profile?.premiumSince]);
|
}, [selfHosted, user.flags, profile?.premiumType, profile?.premiumSince, profile?.premiumLifetimeSequence]);
|
||||||
|
|
||||||
if (badges.length === 0) {
|
if (badges.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -145,7 +183,19 @@ export const UserProfileBadges: React.FC<UserProfileBadgesProps> = observer(
|
|||||||
return (
|
return (
|
||||||
<div className={containerClassName}>
|
<div className={containerClassName}>
|
||||||
{badges.map((badge) => {
|
{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 (
|
return (
|
||||||
<Tooltip key={badge.key} text={badge.tooltip} maxWidth="xl">
|
<Tooltip key={badge.key} text={badge.tooltip} maxWidth="xl">
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export class UserRecord {
|
|||||||
private readonly _premiumUntil?: Date | null;
|
private readonly _premiumUntil?: Date | null;
|
||||||
private readonly _premiumWillCancel?: boolean;
|
private readonly _premiumWillCancel?: boolean;
|
||||||
private readonly _premiumBillingCycle?: string | null;
|
private readonly _premiumBillingCycle?: string | null;
|
||||||
readonly premiumLifetimeSequence?: number | null;
|
private readonly _premiumLifetimeSequence?: number | null;
|
||||||
readonly premiumBadgeHidden?: boolean;
|
readonly premiumBadgeHidden?: boolean;
|
||||||
readonly premiumBadgeMasked?: boolean;
|
readonly premiumBadgeMasked?: boolean;
|
||||||
readonly premiumBadgeTimestampHidden?: 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_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_will_cancel' in user) this._premiumWillCancel = user.premium_will_cancel;
|
||||||
if ('premium_billing_cycle' in user) this._premiumBillingCycle = user.premium_billing_cycle;
|
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_hidden' in user) this.premiumBadgeHidden = user.premium_badge_hidden;
|
||||||
if ('premium_badge_masked' in user) this.premiumBadgeMasked = user.premium_badge_masked;
|
if ('premium_badge_masked' in user) this.premiumBadgeMasked = user.premium_badge_masked;
|
||||||
if ('premium_badge_timestamp_hidden' in user)
|
if ('premium_badge_timestamp_hidden' in user)
|
||||||
@ -197,6 +197,11 @@ export class UserRecord {
|
|||||||
return override != null ? override : this._premiumBillingCycle;
|
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 {
|
get premiumWillCancel(): boolean | undefined {
|
||||||
const override = DeveloperOptionsStore.premiumWillCancelOverride;
|
const override = DeveloperOptionsStore.premiumWillCancelOverride;
|
||||||
return override != null ? override : this._premiumWillCancel;
|
return override != null ? override : this._premiumWillCancel;
|
||||||
@ -339,8 +344,8 @@ export class UserRecord {
|
|||||||
...(this._premiumBillingCycle !== undefined || updates.premium_billing_cycle !== undefined
|
...(this._premiumBillingCycle !== undefined || updates.premium_billing_cycle !== undefined
|
||||||
? {premium_billing_cycle: updates.premium_billing_cycle ?? this._premiumBillingCycle}
|
? {premium_billing_cycle: updates.premium_billing_cycle ?? this._premiumBillingCycle}
|
||||||
: {}),
|
: {}),
|
||||||
...(this.premiumLifetimeSequence !== undefined || updates.premium_lifetime_sequence !== undefined
|
...(this._premiumLifetimeSequence !== undefined || updates.premium_lifetime_sequence !== undefined
|
||||||
? {premium_lifetime_sequence: updates.premium_lifetime_sequence ?? this.premiumLifetimeSequence}
|
? {premium_lifetime_sequence: updates.premium_lifetime_sequence ?? this._premiumLifetimeSequence}
|
||||||
: {}),
|
: {}),
|
||||||
...(this.premiumBadgeHidden !== undefined || updates.premium_badge_hidden !== undefined
|
...(this.premiumBadgeHidden !== undefined || updates.premium_badge_hidden !== undefined
|
||||||
? {premium_badge_hidden: updates.premium_badge_hidden ?? this.premiumBadgeHidden}
|
? {premium_badge_hidden: updates.premium_badge_hidden ?? this.premiumBadgeHidden}
|
||||||
@ -520,7 +525,7 @@ export class UserRecord {
|
|||||||
this.premiumUntil?.getTime() === other.premiumUntil?.getTime() &&
|
this.premiumUntil?.getTime() === other.premiumUntil?.getTime() &&
|
||||||
this.premiumWillCancel === other.premiumWillCancel &&
|
this.premiumWillCancel === other.premiumWillCancel &&
|
||||||
this._premiumBillingCycle === other._premiumBillingCycle &&
|
this._premiumBillingCycle === other._premiumBillingCycle &&
|
||||||
this.premiumLifetimeSequence === other.premiumLifetimeSequence &&
|
this._premiumLifetimeSequence === other._premiumLifetimeSequence &&
|
||||||
this.premiumBadgeHidden === other.premiumBadgeHidden &&
|
this.premiumBadgeHidden === other.premiumBadgeHidden &&
|
||||||
this.premiumBadgeMasked === other.premiumBadgeMasked &&
|
this.premiumBadgeMasked === other.premiumBadgeMasked &&
|
||||||
this.premiumBadgeTimestampHidden === other.premiumBadgeTimestampHidden &&
|
this.premiumBadgeTimestampHidden === other.premiumBadgeTimestampHidden &&
|
||||||
@ -582,7 +587,9 @@ export class UserRecord {
|
|||||||
...(this._premiumUntil !== undefined ? {premium_until: normalizeDate(this._premiumUntil)} : {}),
|
...(this._premiumUntil !== undefined ? {premium_until: normalizeDate(this._premiumUntil)} : {}),
|
||||||
...(this._premiumWillCancel !== undefined ? {premium_will_cancel: this._premiumWillCancel} : {}),
|
...(this._premiumWillCancel !== undefined ? {premium_will_cancel: this._premiumWillCancel} : {}),
|
||||||
...(this._premiumBillingCycle !== undefined ? {premium_billing_cycle: this._premiumBillingCycle} : {}),
|
...(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.premiumBadgeHidden !== undefined ? {premium_badge_hidden: this.premiumBadgeHidden} : {}),
|
||||||
...(this.premiumBadgeMasked !== undefined ? {premium_badge_masked: this.premiumBadgeMasked} : {}),
|
...(this.premiumBadgeMasked !== undefined ? {premium_badge_masked: this.premiumBadgeMasked} : {}),
|
||||||
...(this.premiumBadgeTimestampHidden !== undefined
|
...(this.premiumBadgeTimestampHidden !== undefined
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export type DeveloperOptionsState = Readonly<{
|
|||||||
forceShowVanityURLDisclaimer: boolean;
|
forceShowVanityURLDisclaimer: boolean;
|
||||||
forceShowVoiceConnection: boolean;
|
forceShowVoiceConnection: boolean;
|
||||||
premiumTypeOverride: number | null;
|
premiumTypeOverride: number | null;
|
||||||
|
premiumLifetimeSequenceOverride: number | null;
|
||||||
premiumSinceOverride: Date | null;
|
premiumSinceOverride: Date | null;
|
||||||
premiumUntilOverride: Date | null;
|
premiumUntilOverride: Date | null;
|
||||||
premiumBillingCycleOverride: string | null;
|
premiumBillingCycleOverride: string | null;
|
||||||
@ -122,6 +123,7 @@ class DeveloperOptionsStore implements DeveloperOptionsState {
|
|||||||
forceShowVanityURLDisclaimer = false;
|
forceShowVanityURLDisclaimer = false;
|
||||||
forceShowVoiceConnection = false;
|
forceShowVoiceConnection = false;
|
||||||
premiumTypeOverride: number | null = null;
|
premiumTypeOverride: number | null = null;
|
||||||
|
premiumLifetimeSequenceOverride: number | null = null;
|
||||||
premiumSinceOverride: Date | null = null;
|
premiumSinceOverride: Date | null = null;
|
||||||
premiumUntilOverride: Date | null = null;
|
premiumUntilOverride: Date | null = null;
|
||||||
premiumBillingCycleOverride: string | null = null;
|
premiumBillingCycleOverride: string | null = null;
|
||||||
@ -200,6 +202,7 @@ class DeveloperOptionsStore implements DeveloperOptionsState {
|
|||||||
'forceShowVanityURLDisclaimer',
|
'forceShowVanityURLDisclaimer',
|
||||||
'forceShowVoiceConnection',
|
'forceShowVoiceConnection',
|
||||||
'premiumTypeOverride',
|
'premiumTypeOverride',
|
||||||
|
'premiumLifetimeSequenceOverride',
|
||||||
'premiumSinceOverride',
|
'premiumSinceOverride',
|
||||||
'premiumUntilOverride',
|
'premiumUntilOverride',
|
||||||
'premiumBillingCycleOverride',
|
'premiumBillingCycleOverride',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user