mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-10 23:56:22 +08:00
feat: indicate premium users (fehmer) (#5092)
* feat: indicate premium users * frontend * Test multiple userFlags, remove later * cleanup * fix flag alignment on profile and leaderboards * fix name auto scaling * update screenshot watermark * update header text * use userFlags for lbOptOut * use flex end * removeo unused code, increase margin --------- Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
7e957fb449
commit
c95e3b2fa8
23 changed files with 373 additions and 91 deletions
|
@ -51,6 +51,9 @@ describe("user controller test", () => {
|
|||
enabled: false,
|
||||
maxMail: 0,
|
||||
},
|
||||
premium: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import { ObjectId } from "mongodb";
|
|||
import * as UserDal from "../../src/dal/user";
|
||||
import * as LeaderboardsDal from "../../src/dal/leaderboards";
|
||||
import * as PublicDal from "../../src/dal/public";
|
||||
import * as Configuration from "../../src/init/configuration";
|
||||
const configuration = Configuration.getCachedConfiguration();
|
||||
|
||||
import * as DB from "../../src/init/db";
|
||||
|
||||
|
@ -50,10 +52,10 @@ describe("LeaderboardsDal", () => {
|
|||
const lb = result.map((it) => _.omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry(1, rank1, "15"),
|
||||
expectedLbEntry(2, rank2, "15"),
|
||||
expectedLbEntry(3, rank3, "15"),
|
||||
expectedLbEntry(4, rank4, "15"),
|
||||
expectedLbEntry("15", { rank: 1, user: rank1 }),
|
||||
expectedLbEntry("15", { rank: 2, user: rank2 }),
|
||||
expectedLbEntry("15", { rank: 3, user: rank3 }),
|
||||
expectedLbEntry("15", { rank: 4, user: rank4 }),
|
||||
]);
|
||||
});
|
||||
it("should create leaderboard time english 60", async () => {
|
||||
|
@ -76,10 +78,10 @@ describe("LeaderboardsDal", () => {
|
|||
const lb = result.map((it) => _.omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry(1, rank1, "60"),
|
||||
expectedLbEntry(2, rank2, "60"),
|
||||
expectedLbEntry(3, rank3, "60"),
|
||||
expectedLbEntry(4, rank4, "60"),
|
||||
expectedLbEntry("60", { rank: 1, user: rank1 }),
|
||||
expectedLbEntry("60", { rank: 2, user: rank2 }),
|
||||
expectedLbEntry("60", { rank: 3, user: rank3 }),
|
||||
expectedLbEntry("60", { rank: 4, user: rank4 }),
|
||||
]);
|
||||
});
|
||||
it("should not include discord properties for users without discord connection", async () => {
|
||||
|
@ -154,10 +156,113 @@ describe("LeaderboardsDal", () => {
|
|||
//THEN
|
||||
expect(result).toEqual({ "20": 2, "110": 2 });
|
||||
});
|
||||
|
||||
it("should create leaderboard with badges", async () => {
|
||||
//GIVEN
|
||||
const noBadge = await createUser(lbBests(pb(4)));
|
||||
const oneBadgeSelected = await createUser(lbBests(pb(3)), {
|
||||
inventory: { badges: [{ id: 1, selected: true }] },
|
||||
});
|
||||
const oneBadgeNotSelected = await createUser(lbBests(pb(2)), {
|
||||
inventory: { badges: [{ id: 1, selected: false }] },
|
||||
});
|
||||
const multipleBadges = await createUser(lbBests(pb(1)), {
|
||||
inventory: {
|
||||
badges: [
|
||||
{ id: 1, selected: false },
|
||||
{ id: 2, selected: true },
|
||||
{ id: 3, selected: true },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const result = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0
|
||||
)) as SharedTypes.LeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = result.map((it) => _.omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("15", { rank: 1, user: noBadge }),
|
||||
expectedLbEntry("15", {
|
||||
rank: 2,
|
||||
user: oneBadgeSelected,
|
||||
badgeId: 1,
|
||||
}),
|
||||
expectedLbEntry("15", { rank: 3, user: oneBadgeNotSelected }),
|
||||
expectedLbEntry("15", {
|
||||
rank: 4,
|
||||
user: multipleBadges,
|
||||
badgeId: 2,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create leaderboard with premium", async () => {
|
||||
await enablePremiumFeatures(true);
|
||||
//GIVEN
|
||||
const noPremium = await createUser(lbBests(pb(4)));
|
||||
const lifetime = await createUser(lbBests(pb(3)), premium(-1));
|
||||
const validPremium = await createUser(lbBests(pb(2)), premium(10));
|
||||
const expiredPremium = await createUser(lbBests(pb(1)), premium(-10));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const result = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0
|
||||
)) as SharedTypes.LeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
const lb = result.map((it) => _.omit(it, ["_id"]));
|
||||
|
||||
expect(lb).toEqual([
|
||||
expectedLbEntry("15", { rank: 1, user: noPremium }),
|
||||
expectedLbEntry("15", {
|
||||
rank: 2,
|
||||
user: lifetime,
|
||||
isPremium: true,
|
||||
}),
|
||||
expectedLbEntry("15", {
|
||||
rank: 3,
|
||||
user: validPremium,
|
||||
isPremium: true,
|
||||
}),
|
||||
expectedLbEntry("15", { rank: 4, user: expiredPremium }),
|
||||
]);
|
||||
});
|
||||
it("should create leaderboard without premium if feature disabled", async () => {
|
||||
await enablePremiumFeatures(false);
|
||||
//GIVEN
|
||||
const lifetime = await createUser(lbBests(pb(3)), premium(-1));
|
||||
|
||||
//WHEN
|
||||
await LeaderboardsDal.update("time", "15", "english");
|
||||
const result = (await LeaderboardsDal.get(
|
||||
"time",
|
||||
"15",
|
||||
"english",
|
||||
0
|
||||
)) as SharedTypes.LeaderboardEntry[];
|
||||
|
||||
//THEN
|
||||
expect(result[0]?.isPremium).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
|
||||
function expectedLbEntry(
|
||||
time: string,
|
||||
{ rank, user, badgeId, isPremium }: ExpectedLbEntry
|
||||
) {
|
||||
const lbBest: SharedTypes.PersonalBest =
|
||||
user.lbPersonalBests?.time[time].english;
|
||||
|
||||
|
@ -172,7 +277,8 @@ function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
|
|||
consistency: lbBest.consistency,
|
||||
discordId: user.discordId,
|
||||
discordAvatar: user.discordAvatar,
|
||||
badgeId: 2,
|
||||
badgeId,
|
||||
isPremium,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -192,12 +298,6 @@ async function createUser(
|
|||
timeTyping: 7200,
|
||||
discordId: "discord " + uid,
|
||||
discordAvatar: "avatar " + uid,
|
||||
inventory: {
|
||||
badges: [
|
||||
{ id: 1, selected: false },
|
||||
{ id: 2, selected: true },
|
||||
],
|
||||
},
|
||||
...userProperties,
|
||||
lbPersonalBests,
|
||||
},
|
||||
|
@ -234,3 +334,32 @@ function pb(
|
|||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
function premium(expirationDeltaSeconds) {
|
||||
return {
|
||||
premium: {
|
||||
startTimestamp: 0,
|
||||
expirationTimestamp:
|
||||
expirationDeltaSeconds === -1
|
||||
? -1
|
||||
: Date.now() + expirationDeltaSeconds * 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface ExpectedLbEntry {
|
||||
rank: number;
|
||||
user: MonkeyTypes.DBUser;
|
||||
badgeId?: number;
|
||||
isPremium?: boolean;
|
||||
}
|
||||
|
||||
async function enablePremiumFeatures(premium: boolean): Promise<void> {
|
||||
const mockConfig = _.merge(await configuration, {
|
||||
users: { premium: { enabled: premium } },
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(Configuration, "getCachedConfiguration")
|
||||
.mockResolvedValue(mockConfig);
|
||||
}
|
||||
|
|
|
@ -105,7 +105,8 @@ export async function getDailyLeaderboard(
|
|||
const topResults = await dailyLeaderboard.getResults(
|
||||
minRank,
|
||||
maxRank,
|
||||
req.ctx.configuration.dailyLeaderboards
|
||||
req.ctx.configuration.dailyLeaderboards,
|
||||
req.ctx.configuration.users.premium.enabled
|
||||
);
|
||||
|
||||
return new MonkeyResponse("Daily leaderboard retrieved", topResults);
|
||||
|
|
|
@ -489,6 +489,8 @@ export async function addResult(
|
|||
(isDevEnvironment() || (user.timeTyping ?? 0) > 7200);
|
||||
|
||||
const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
|
||||
const isPremium =
|
||||
(await UserDAL.checkIfUserIsPremium(user.uid, user)) || undefined;
|
||||
|
||||
if (dailyLeaderboard && validResultCriteria) {
|
||||
incrementDailyLeaderboard(
|
||||
|
@ -508,6 +510,7 @@ export async function addResult(
|
|||
discordAvatar: user.discordAvatar,
|
||||
discordId: user.discordId,
|
||||
badgeId: selectedBadgeId,
|
||||
isPremium,
|
||||
},
|
||||
dailyLeaderboardsConfig
|
||||
);
|
||||
|
|
|
@ -753,6 +753,7 @@ export async function getProfile(
|
|||
streak: streak?.length ?? 0,
|
||||
maxStreak: streak?.maxLength ?? 0,
|
||||
lbOptOut,
|
||||
isPremium: await UserDAL.checkIfUserIsPremium(user.uid, user),
|
||||
};
|
||||
|
||||
if (banned) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import Logger from "../utils/logger";
|
|||
import { performance } from "perf_hooks";
|
||||
import { setLeaderboard } from "../utils/prometheus";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { getCachedConfiguration } from "../init/configuration";
|
||||
|
||||
const leaderboardUpdating: Record<string, boolean> = {};
|
||||
|
||||
|
@ -27,6 +28,13 @@ export async function get(
|
|||
.skip(skip)
|
||||
.limit(limit)
|
||||
.toArray();
|
||||
|
||||
const premiumFeaturesEnabled = (await getCachedConfiguration(true)).users
|
||||
.premium.enabled;
|
||||
|
||||
if (!premiumFeaturesEnabled) {
|
||||
preset.forEach((it) => (it.isPremium = undefined));
|
||||
}
|
||||
return preset;
|
||||
} catch (e) {
|
||||
if (e.error === 175) {
|
||||
|
@ -77,7 +85,6 @@ export async function update(
|
|||
const key = `lbPersonalBests.${mode}.${mode2}.${language}`;
|
||||
const lbCollectionName = `leaderboards.${language}.${mode}.${mode2}`;
|
||||
leaderboardUpdating[`${language}_${mode}_${mode2}`] = true;
|
||||
const start1 = performance.now();
|
||||
const lb = db
|
||||
.collection<MonkeyTypes.DBUser>("users")
|
||||
.aggregate<SharedTypes.LeaderboardEntry>(
|
||||
|
@ -127,41 +134,43 @@ export async function update(
|
|||
discordId: 1,
|
||||
discordAvatar: 1,
|
||||
inventory: 1,
|
||||
premium: 1,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
$addFields: {
|
||||
[`${key}.uid`]: "$uid",
|
||||
[`${key}.name`]: "$name",
|
||||
[`${key}.discordId`]: {
|
||||
$ifNull: ["$discordId", "$$REMOVE"],
|
||||
},
|
||||
[`${key}.discordAvatar`]: {
|
||||
$ifNull: ["$discordAvatar", "$$REMOVE"],
|
||||
},
|
||||
"user.uid": "$uid",
|
||||
"user.name": "$name",
|
||||
"user.discordId": { $ifNull: ["$discordId", "$$REMOVE"] },
|
||||
"user.discordAvatar": { $ifNull: ["$discordAvatar", "$$REMOVE"] },
|
||||
[`${key}.consistency`]: {
|
||||
$ifNull: [`$${key}.consistency`, "$$REMOVE"],
|
||||
},
|
||||
[`${key}.rank`]: {
|
||||
calculated: {
|
||||
$function: {
|
||||
body: "function() {try {row_number+= 1;} catch (e) {row_number= 1;}return row_number;}",
|
||||
args: [],
|
||||
lang: "js",
|
||||
},
|
||||
},
|
||||
[`${key}.badgeId`]: {
|
||||
$function: {
|
||||
body: "function(badges) {if (!badges) return null; for(let i=0;i<badges.length;i++){ if(badges[i].selected) return badges[i].id;}return null;}",
|
||||
args: ["$inventory.badges"],
|
||||
lang: "js",
|
||||
args: [
|
||||
"$premium.expirationTimestamp",
|
||||
"$$NOW",
|
||||
"$inventory.badges",
|
||||
],
|
||||
body: `function(expiration, currentTime, badges) {
|
||||
try {row_number+= 1;} catch (e) {row_number= 1;}
|
||||
var badgeId = undefined;
|
||||
if(badges)for(let i=0; i<badges.length; i++){
|
||||
if(badges[i].selected){ badgeId = badges[i].id; break}
|
||||
}
|
||||
var isPremium = expiration !== undefined && (expiration === -1 || new Date(expiration)>currentTime) || undefined;
|
||||
return {rank:row_number,badgeId, isPremium};
|
||||
}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$replaceRoot: {
|
||||
newRoot: `$${key}`,
|
||||
$replaceWith: {
|
||||
$mergeObjects: [`$${key}`, "$user", "$calculated"],
|
||||
},
|
||||
},
|
||||
{ $out: lbCollectionName },
|
||||
|
@ -169,6 +178,7 @@ export async function update(
|
|||
{ allowDiskUse: true }
|
||||
);
|
||||
|
||||
const start1 = performance.now();
|
||||
await lb.toArray();
|
||||
const end1 = performance.now();
|
||||
|
||||
|
@ -179,7 +189,6 @@ export async function update(
|
|||
const end2 = performance.now();
|
||||
|
||||
//update speedStats
|
||||
const start3 = performance.now();
|
||||
const boundaries = [...Array(32).keys()].map((it) => it * 10);
|
||||
const statsKey = `${language}_${mode}_${mode2}`;
|
||||
const src = await db.collection(lbCollectionName);
|
||||
|
@ -218,6 +227,7 @@ export async function update(
|
|||
],
|
||||
{ allowDiskUse: true }
|
||||
);
|
||||
const start3 = performance.now();
|
||||
await histogram.toArray();
|
||||
const end3 = performance.now();
|
||||
|
||||
|
@ -258,6 +268,7 @@ async function createIndex(key: string): Promise<void> {
|
|||
discordId: 1,
|
||||
discordAvatar: 1,
|
||||
inventory: 1,
|
||||
premium: 1,
|
||||
};
|
||||
const partial = {
|
||||
partialFilterExpression: {
|
||||
|
@ -275,4 +286,10 @@ async function createIndex(key: string): Promise<void> {
|
|||
export async function createIndicies(): Promise<void> {
|
||||
await createIndex("lbPersonalBests.time.15.english");
|
||||
await createIndex("lbPersonalBests.time.60.english");
|
||||
|
||||
if (isDevEnvironment()) {
|
||||
Logger.info("Updating leaderboards in dev mode...");
|
||||
await update("time", "15", "english");
|
||||
await update("time", "60", "english");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import MonkeyError from "../utils/error";
|
|||
import { Collection, ObjectId, Long, UpdateFilter } from "mongodb";
|
||||
import Logger from "../utils/logger";
|
||||
import { flattenObjectDeep, isToday, isYesterday } from "../utils/misc";
|
||||
import { getCachedConfiguration } from "../init/configuration";
|
||||
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
|
||||
|
@ -1049,6 +1050,11 @@ export async function checkIfUserIsPremium(
|
|||
uid: string,
|
||||
userInfoOverride?: MonkeyTypes.DBUser
|
||||
): Promise<boolean> {
|
||||
const premiumFeaturesEnabled = (await getCachedConfiguration(true)).users
|
||||
.premium.enabled;
|
||||
if (!premiumFeaturesEnabled) {
|
||||
return false;
|
||||
}
|
||||
const user = userInfoOverride ?? (await getUser(uid, "checkIfUserIsPremium"));
|
||||
const expirationDate = user.premium?.expirationTimestamp;
|
||||
|
||||
|
|
2
backend/src/types/types.d.ts
vendored
2
backend/src/types/types.d.ts
vendored
|
@ -20,7 +20,7 @@ declare namespace MonkeyTypes {
|
|||
|
||||
type DBUser = Omit<
|
||||
SharedTypes.User,
|
||||
"resultFilterPresets" | "tags" | "customThemes"
|
||||
"resultFilterPresets" | "tags" | "customThemes" | "isPremium"
|
||||
> & {
|
||||
_id: ObjectId;
|
||||
resultFilterPresets?: WithObjectIdArray<SharedTypes.ResultFilters[]>;
|
||||
|
|
|
@ -14,6 +14,7 @@ type DailyLeaderboardEntry = {
|
|||
discordAvatar?: string;
|
||||
discordId?: string;
|
||||
badgeId?: number;
|
||||
isPremium?: boolean;
|
||||
};
|
||||
|
||||
type GetRankResponse = {
|
||||
|
@ -123,7 +124,8 @@ export class DailyLeaderboard {
|
|||
public async getResults(
|
||||
minRank: number,
|
||||
maxRank: number,
|
||||
dailyLeaderboardsConfig: SharedTypes.Configuration["dailyLeaderboards"]
|
||||
dailyLeaderboardsConfig: SharedTypes.Configuration["dailyLeaderboards"],
|
||||
premiumFeaturesEnabled: boolean
|
||||
): Promise<LbEntryWithRank[]> {
|
||||
const connection = RedisClient.getConnection();
|
||||
if (!connection || !dailyLeaderboardsConfig.enabled) {
|
||||
|
@ -156,6 +158,10 @@ export class DailyLeaderboard {
|
|||
})
|
||||
);
|
||||
|
||||
if (!premiumFeaturesEnabled) {
|
||||
resultsWithRanks.forEach((it) => (it.isPremium = undefined));
|
||||
}
|
||||
|
||||
return resultsWithRanks;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ async function handleDailyLeaderboardResults(
|
|||
const allResults = await dailyLeaderboard.getResults(
|
||||
0,
|
||||
-1,
|
||||
dailyLeaderboardsConfig
|
||||
dailyLeaderboardsConfig,
|
||||
false
|
||||
);
|
||||
|
||||
if (allResults.length === 0) {
|
||||
|
|
|
@ -29,7 +29,10 @@
|
|||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="name">-</div>
|
||||
<div class="user">
|
||||
<div class="name">-</div>
|
||||
<div class="userFlags"></div>
|
||||
</div>
|
||||
<div class="badges"></div>
|
||||
<div class="allBadges"></div>
|
||||
<div class="joined" data-balloon-pos="up">-</div>
|
||||
|
|
|
@ -27,7 +27,10 @@
|
|||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="name">-</div>
|
||||
<div class="user">
|
||||
<div class="name">-</div>
|
||||
<div class="userFlags"></div>
|
||||
</div>
|
||||
<div class="badges"></div>
|
||||
<div class="allBadges"></div>
|
||||
<div class="joined" data-balloon-pos="up">-</div>
|
||||
|
|
|
@ -159,6 +159,12 @@
|
|||
.badge {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
.flagsAndBadge {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
color: var(--sub-color);
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
tr td:first-child {
|
||||
|
|
|
@ -17,8 +17,11 @@ nav {
|
|||
|
||||
.textButton {
|
||||
.text {
|
||||
font-size: 0.65em;
|
||||
font-size: 0.75em;
|
||||
align-self: center;
|
||||
.fas {
|
||||
margin-left: 0.33em;
|
||||
}
|
||||
}
|
||||
|
||||
.icon,
|
||||
|
@ -34,7 +37,7 @@ nav {
|
|||
&.account {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
gap: 0.33em;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
.loading,
|
||||
|
@ -107,8 +110,13 @@ nav {
|
|||
}
|
||||
}
|
||||
}
|
||||
&:hover .level {
|
||||
background-color: var(--text-color);
|
||||
&:hover {
|
||||
.level {
|
||||
background-color: var(--text-color);
|
||||
}
|
||||
.userFlags {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -355,17 +355,20 @@
|
|||
grid-column: 1/2;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
// font-size: 3rem;
|
||||
// line-height: 2.5rem;
|
||||
.user {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1rem;
|
||||
line-height: 100%;
|
||||
// line-height: 100%;
|
||||
width: max-content;
|
||||
.bannedIcon {
|
||||
margin-left: 0.5em;
|
||||
display: inline-grid;
|
||||
font-size: 75%;
|
||||
color: var(--error-color);
|
||||
gap: 0.35rem;
|
||||
.userFlags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
// font-size: 0.75em;
|
||||
color: var(--sub-color);
|
||||
place-items: center left;
|
||||
}
|
||||
}
|
||||
.badge {
|
||||
|
|
|
@ -107,8 +107,14 @@
|
|||
.ssWatermark {
|
||||
font-size: 1.25rem;
|
||||
color: var(--sub-color);
|
||||
line-height: 1rem;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0 1em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.fas {
|
||||
margin-left: 0.33em;
|
||||
}
|
||||
}
|
||||
|
||||
#timerNumber {
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
} from "../test/test-config";
|
||||
import * as ConnectionState from "../states/connection";
|
||||
import { navigate } from "./route-controller";
|
||||
import { getHtmlByUserFlags } from "./user-flag-controller";
|
||||
|
||||
let signedOutThisSession = false;
|
||||
|
||||
|
@ -114,6 +115,9 @@ async function getDataAndInit(): Promise<boolean> {
|
|||
LoadingPage.updateText("Applying settings...");
|
||||
const snapshot = DB.getSnapshot() as MonkeyTypes.Snapshot;
|
||||
$("nav .textButton.account > .text").text(snapshot.name);
|
||||
$("nav .textButton.account > .text").append(
|
||||
getHtmlByUserFlags(snapshot, { iconsOnly: true })
|
||||
);
|
||||
showFavoriteQuoteLength();
|
||||
|
||||
ResultFilters.loadTags(snapshot.tags);
|
||||
|
|
83
frontend/src/ts/controllers/user-flag-controller.ts
Normal file
83
frontend/src/ts/controllers/user-flag-controller.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
const flags: UserFlag[] = [
|
||||
{
|
||||
name: "Prime Ape",
|
||||
description: "Paying for a monthly subscription",
|
||||
icon: "fa-dollar-sign",
|
||||
test: (it) => it.isPremium === true,
|
||||
},
|
||||
{
|
||||
name: "Banned",
|
||||
description: "This account is banned",
|
||||
icon: "fa-gavel",
|
||||
color: "var(--error-color)",
|
||||
test: (it) => it.banned === true,
|
||||
},
|
||||
{
|
||||
name: "LbOptOut",
|
||||
description: "This account has opted out of leaderboards",
|
||||
icon: "fa-crown",
|
||||
color: "var(--error-color)",
|
||||
test: (it) => it.lbOptOut === true,
|
||||
},
|
||||
];
|
||||
|
||||
type SupportsFlags = {
|
||||
isPremium?: boolean;
|
||||
banned?: boolean;
|
||||
lbOptOut?: boolean;
|
||||
};
|
||||
|
||||
type UserFlag = {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly icon: string;
|
||||
readonly color?: string;
|
||||
readonly background?: string;
|
||||
readonly customStyle?: string;
|
||||
test(source: SupportsFlags): boolean;
|
||||
};
|
||||
|
||||
type UserFlagOptions = {
|
||||
iconsOnly?: boolean;
|
||||
};
|
||||
|
||||
const USER_FLAG_OPTIONS_DEFAULT: UserFlagOptions = {
|
||||
iconsOnly: false,
|
||||
};
|
||||
|
||||
function getMatchingFlags(source: SupportsFlags): UserFlag[] {
|
||||
const result = flags.filter((it) => it.test(source));
|
||||
return result;
|
||||
}
|
||||
function toHtml(flag: UserFlag, formatOptions: UserFlagOptions): string {
|
||||
const icon = `<i class="fas ${flag.icon}"></i>`;
|
||||
|
||||
if (formatOptions.iconsOnly) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
const style = [];
|
||||
if (flag.background !== undefined) {
|
||||
style.push(`background: ${flag.background};`);
|
||||
}
|
||||
if (flag?.color !== undefined) {
|
||||
style.push(`color: ${flag.color};`);
|
||||
}
|
||||
if (flag?.customStyle !== undefined) {
|
||||
style.push(flag.customStyle);
|
||||
}
|
||||
|
||||
const balloon = `aria-label="${flag.description}" data-balloon-pos="right"`;
|
||||
|
||||
return `<div class="flag" ${balloon} style="${style.join("")}">${icon}</div>`;
|
||||
}
|
||||
|
||||
export function getHtmlByUserFlags(
|
||||
source: SupportsFlags,
|
||||
options?: UserFlagOptions
|
||||
): string {
|
||||
const formatOptions = { ...USER_FLAG_OPTIONS_DEFAULT, ...options };
|
||||
return getMatchingFlags(source)
|
||||
.map((it) => toHtml(it, formatOptions))
|
||||
.join("");
|
||||
}
|
|
@ -12,6 +12,7 @@ import * as Skeleton from "../utils/skeleton";
|
|||
import { debounce } from "throttle-debounce";
|
||||
import Format from "../utils/format";
|
||||
import SlimSelect from "slim-select";
|
||||
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
|
||||
|
||||
const wrapperId = "leaderboardsWrapper";
|
||||
|
||||
|
@ -336,7 +337,10 @@ async function fillTable(lb: LbKey): Promise<void> {
|
|||
<a href="${location.origin}/profile/${
|
||||
entry.uid
|
||||
}?isUid" class="entryName" uid=${entry.uid} router-link>${entry.name}</a>
|
||||
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
|
||||
<div class="flagsAndBadge">
|
||||
${getHtmlByUserFlags(entry)}
|
||||
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="alignRight">${Format.typingSpeed(entry.wpm, {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { throttle } from "throttle-debounce";
|
|||
import * as EditProfilePopup from "../popups/edit-profile-popup";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
|
||||
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
|
||||
|
||||
type ProfileViewPaths = "profile" | "account";
|
||||
type UserProfileOrSnapshot = SharedTypes.UserProfile | MonkeyTypes.Snapshot;
|
||||
|
@ -23,7 +24,6 @@ export async function update(
|
|||
|
||||
profileElement.attr("uid", profile.uid ?? "");
|
||||
profileElement.attr("name", profile.name ?? "");
|
||||
profileElement.attr("lbOptOut", `${profile.lbOptOut ?? false}`);
|
||||
|
||||
// ============================================================================
|
||||
// DO FREAKING NOT USE .HTML OR .APPEND HERE - USER INPUT!!!!!!
|
||||
|
@ -31,8 +31,6 @@ export async function update(
|
|||
|
||||
const banned = profile.banned === true;
|
||||
|
||||
const lbOptOut = profile.lbOptOut === true;
|
||||
|
||||
if (
|
||||
details === undefined ||
|
||||
profile === undefined ||
|
||||
|
@ -78,22 +76,9 @@ export async function update(
|
|||
}
|
||||
|
||||
details.find(".name").text(profile.name);
|
||||
details.find(".userFlags").html(getHtmlByUserFlags(profile));
|
||||
|
||||
if (banned) {
|
||||
details
|
||||
.find(".name")
|
||||
.append(
|
||||
`<div class="bannedIcon" aria-label="This account is banned" data-balloon-pos="up"><i class="fas fa-gavel"></i></div>`
|
||||
);
|
||||
}
|
||||
|
||||
if (lbOptOut) {
|
||||
details
|
||||
.find(".name")
|
||||
.append(
|
||||
`<div class="bannedIcon" aria-label="This account has opted out of leaderboards" data-balloon-pos="up"><i class="fas fa-crown"></i></div>`
|
||||
);
|
||||
|
||||
if (profile.lbOptOut === true) {
|
||||
if (where === "profile") {
|
||||
profileElement
|
||||
.find(".lbOptOutReminder")
|
||||
|
@ -413,7 +398,7 @@ export function updateNameFontSize(where: ProfileViewPaths): void {
|
|||
details = $(".pageProfile .profile .details");
|
||||
}
|
||||
if (!details) return;
|
||||
const nameFieldjQ = details.find(".name");
|
||||
const nameFieldjQ = details.find(".user");
|
||||
const nameFieldParent = nameFieldjQ.parent()[0];
|
||||
const nameField = nameFieldjQ[0];
|
||||
const upperLimit = Misc.convertRemToPixels(2);
|
||||
|
|
|
@ -19,7 +19,10 @@ function reset(): void {
|
|||
<div class="avatar"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="name">-</div>
|
||||
<div class="user">
|
||||
<div class="name">-</div>
|
||||
<div class="userFlags"></div>
|
||||
</div>
|
||||
<div class="badges"></div>
|
||||
<div class="allBadges"></div>
|
||||
<div class="joined" data-balloon-pos="up">-</div>
|
||||
|
|
|
@ -23,6 +23,7 @@ import * as ResultWordHighlight from "../elements/result-word-highlight";
|
|||
import * as ActivePage from "../states/active-page";
|
||||
import Format from "../utils/format";
|
||||
import * as Loader from "../elements/loader";
|
||||
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
|
||||
|
||||
async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
|
||||
return (await import("html2canvas")).default;
|
||||
|
@ -449,17 +450,20 @@ export async function screenshot(): Promise<void> {
|
|||
const dateNow = new Date(Date.now());
|
||||
$("#resultReplay").addClass("hidden");
|
||||
$(".pageTest .ssWatermark").removeClass("hidden");
|
||||
$(".pageTest .ssWatermark").text(
|
||||
format(dateNow, "dd MMM yyyy HH:mm") + " | monkeytype.com "
|
||||
);
|
||||
if (isAuthenticated()) {
|
||||
$(".pageTest .ssWatermark").text(
|
||||
DB.getSnapshot()?.name +
|
||||
" | " +
|
||||
format(dateNow, "dd MMM yyyy HH:mm") +
|
||||
" | monkeytype.com "
|
||||
);
|
||||
|
||||
const snapshot = DB.getSnapshot();
|
||||
const ssWatermark = [format(dateNow, "dd MMM yyyy HH:mm"), "monkeytype.com"];
|
||||
if (snapshot?.name !== undefined) {
|
||||
const userText = `${snapshot?.name}${getHtmlByUserFlags(snapshot, {
|
||||
iconsOnly: true,
|
||||
})}`;
|
||||
ssWatermark.unshift(userText);
|
||||
}
|
||||
$(".pageTest .ssWatermark").html(
|
||||
ssWatermark
|
||||
.map((el) => `<span>${el}</span>`)
|
||||
.join("<span class='pipe'>|</span>")
|
||||
);
|
||||
$(".pageTest .buttons").addClass("hidden");
|
||||
$("#notificationCenter").addClass("hidden");
|
||||
$("#commandLineMobileButton").addClass("hidden");
|
||||
|
|
5
shared-types/types.d.ts
vendored
5
shared-types/types.d.ts
vendored
|
@ -446,7 +446,8 @@ declare namespace SharedTypes {
|
|||
discordId?: string;
|
||||
discordAvatar?: string;
|
||||
rank: number;
|
||||
badgeId: number | null;
|
||||
badgeId?: number;
|
||||
isPremium?: boolean;
|
||||
}
|
||||
|
||||
type PostResultResponse = {
|
||||
|
@ -526,6 +527,7 @@ declare namespace SharedTypes {
|
|||
profileDetails?: UserProfileDetails;
|
||||
customThemes?: CustomTheme[];
|
||||
premium?: PremiumInfo;
|
||||
isPremium?: boolean;
|
||||
quoteRatings?: UserQuoteRatings;
|
||||
favoriteQuotes?: Record<string, string[]>;
|
||||
lbMemory?: UserLbMemory;
|
||||
|
@ -575,6 +577,7 @@ declare namespace SharedTypes {
|
|||
| "lbOptOut"
|
||||
| "inventory"
|
||||
| "uid"
|
||||
| "isPremium"
|
||||
> & {
|
||||
typingStats: {
|
||||
completedTests: User["completedTests"];
|
||||
|
|
Loading…
Add table
Reference in a new issue