mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-20 07:16:17 +08:00
refactor: shared user types (#5080)
* move user to shared definitions this includes whatever user can have on it, tags, presets and so on * profiles, mail and others * fix logic * yeet * same as master for now * tsc fixes * remove comment * fix tests * chore: omit ips * fix(language): remove some unnecessarily capitalised words in german 1k * fix(typing): first space sometimes soft locking the website * perf: speed up settings page loading * fix: use selected typing speed unit on personal best popup (fehmer) (#5070) * fix: Use selected typing speed unit on personal best popup * refactor * refactor * test coverage * use Format in more places * Make config mockable * dependency injection * wip * fix * test * touch * fix(language): typos in russian_10k.json (kae) (#5082) * Update russian_10k.json - fixed typos - removed duplicates * - fixed extra typos * remove duplicates * fix(language): typos in russian_10k.json * feat: add copy missed words to result screen (fehmer) (#5086) * feat: Add copy missed words to result screen * remove margin * update icons --------- Co-authored-by: Miodec <jack@monkeytype.com> * impr(funbox): add 46 group languages to wikipedia funbox (RealCyGuy) (#5078) * impr: provide all-time LB results during LB update (fehmer) (#5074) Try to provide LB results during the LB update. There is a very small time-frame where already running queries might fail during the update. For now we keep the 503 error in this cases and monitor how often this happens on production. * impr(funbox): add ` (grave accent, 96) and ~ (tilde, 126) to specials (#5073) * impr: add testWords and wordsHistory to copy result stats (#5085) * feat: add testWords and wordsHistory to copy result stats * fix * add fe ts dep --------- Co-authored-by: Christian Fehmer <fehmer@users.noreply.github.com> Co-authored-by: Andrey Kuznetsov <akuznetsov@outlook.com> Co-authored-by: Cyrus Yip <cyruscmyip1@gmail.com> Co-authored-by: fitzsim <fitzsim@fitzsim.org>
This commit is contained in:
parent
06c50deb3a
commit
01790d8a3a
|
@ -157,7 +157,7 @@ describe("LeaderboardsDal", () => {
|
|||
});
|
||||
});
|
||||
|
||||
function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) {
|
||||
function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
|
||||
const lbBest: SharedTypes.PersonalBest =
|
||||
user.lbPersonalBests?.time[time].english;
|
||||
|
||||
|
@ -178,13 +178,13 @@ function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) {
|
|||
|
||||
async function createUser(
|
||||
lbPersonalBests?: MonkeyTypes.LbPersonalBests,
|
||||
userProperties?: Partial<MonkeyTypes.User>
|
||||
): Promise<MonkeyTypes.User> {
|
||||
userProperties?: Partial<MonkeyTypes.DBUser>
|
||||
): Promise<MonkeyTypes.DBUser> {
|
||||
const uid = new ObjectId().toHexString();
|
||||
await UserDal.addUser("User " + uid, uid + "@example.com", uid);
|
||||
|
||||
await DB.getDb()
|
||||
?.collection<MonkeyTypes.User>("users")
|
||||
?.collection<MonkeyTypes.DBUser>("users")
|
||||
.updateOne(
|
||||
{ uid },
|
||||
{
|
||||
|
|
|
@ -15,7 +15,8 @@ async function createDummyData(
|
|||
timestamp: number,
|
||||
tag?: string
|
||||
): Promise<void> {
|
||||
const dummyUser: MonkeyTypes.User = {
|
||||
const dummyUser: MonkeyTypes.DBUser = {
|
||||
_id: new ObjectId(),
|
||||
uid,
|
||||
addedAt: 0,
|
||||
email: "test@example.com",
|
||||
|
|
|
@ -316,8 +316,8 @@ export async function updateEmail(
|
|||
}
|
||||
|
||||
function getRelevantUserInfo(
|
||||
user: MonkeyTypes.User
|
||||
): Partial<MonkeyTypes.User> {
|
||||
user: MonkeyTypes.DBUser
|
||||
): Partial<MonkeyTypes.DBUser> {
|
||||
return _.omit(user, [
|
||||
"bananas",
|
||||
"lbPersonalBests",
|
||||
|
@ -336,7 +336,7 @@ export async function getUser(
|
|||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
let userInfo: MonkeyTypes.User;
|
||||
let userInfo: MonkeyTypes.DBUser;
|
||||
try {
|
||||
userInfo = await UserDAL.getUser(uid, "get user");
|
||||
} catch (e) {
|
||||
|
@ -786,7 +786,7 @@ export async function getProfile(
|
|||
details: profileDetails,
|
||||
allTimeLbs: alltimelbs,
|
||||
uid: user.uid,
|
||||
};
|
||||
} as SharedTypes.UserProfile;
|
||||
|
||||
return new MonkeyResponse("Profile retrieved", profileData);
|
||||
}
|
||||
|
@ -811,10 +811,13 @@ export async function updateProfile(
|
|||
}
|
||||
});
|
||||
|
||||
const profileDetailsUpdates: Partial<MonkeyTypes.UserProfileDetails> = {
|
||||
const profileDetailsUpdates: Partial<SharedTypes.UserProfileDetails> = {
|
||||
bio: sanitizeString(bio),
|
||||
keyboard: sanitizeString(keyboard),
|
||||
socialProfiles: _.mapValues(socialProfiles, sanitizeString),
|
||||
socialProfiles: _.mapValues(
|
||||
socialProfiles,
|
||||
sanitizeString
|
||||
) as SharedTypes.UserProfileDetails["socialProfiles"],
|
||||
};
|
||||
|
||||
await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory);
|
||||
|
|
|
@ -14,7 +14,10 @@ const router = Router();
|
|||
|
||||
const checkIfUserIsQuoteMod = checkUserPermissions({
|
||||
criteria: (user) => {
|
||||
return !!user.quoteMod;
|
||||
return (
|
||||
user.quoteMod === true ||
|
||||
(typeof user.quoteMod === "string" && user.quoteMod !== "")
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ export async function update(
|
|||
leaderboardUpdating[`${language}_${mode}_${mode2}`] = true;
|
||||
const start1 = performance.now();
|
||||
const lb = db
|
||||
.collection<MonkeyTypes.User>("users")
|
||||
.collection<MonkeyTypes.DBUser>("users")
|
||||
.aggregate<SharedTypes.LeaderboardEntry>(
|
||||
[
|
||||
{
|
||||
|
|
|
@ -13,7 +13,7 @@ export async function addResult(
|
|||
uid: string,
|
||||
result: DBResult
|
||||
): Promise<{ insertedId: ObjectId }> {
|
||||
let user: MonkeyTypes.User | null = null;
|
||||
let user: MonkeyTypes.DBUser | null = null;
|
||||
try {
|
||||
user = await getUser(uid, "add result");
|
||||
} catch (e) {
|
||||
|
|
|
@ -16,15 +16,15 @@ type Result = Omit<
|
|||
>;
|
||||
|
||||
// Export for use in tests
|
||||
export const getUsersCollection = (): Collection<WithId<MonkeyTypes.User>> =>
|
||||
db.collection<MonkeyTypes.User>("users");
|
||||
export const getUsersCollection = (): Collection<MonkeyTypes.DBUser> =>
|
||||
db.collection<MonkeyTypes.DBUser>("users");
|
||||
|
||||
export async function addUser(
|
||||
name: string,
|
||||
email: string,
|
||||
uid: string
|
||||
): Promise<void> {
|
||||
const newUserDocument: Partial<MonkeyTypes.User> = {
|
||||
const newUserDocument: Partial<MonkeyTypes.DBUser> = {
|
||||
name,
|
||||
email,
|
||||
uid,
|
||||
|
@ -170,7 +170,7 @@ export async function optOutOfLeaderboards(uid: string): Promise<void> {
|
|||
|
||||
export async function updateQuoteRatings(
|
||||
uid: string,
|
||||
quoteRatings: MonkeyTypes.UserQuoteRatings
|
||||
quoteRatings: SharedTypes.UserQuoteRatings
|
||||
): Promise<boolean> {
|
||||
await getUser(uid, "update quote ratings");
|
||||
|
||||
|
@ -191,13 +191,15 @@ export async function updateEmail(
|
|||
export async function getUser(
|
||||
uid: string,
|
||||
stack: string
|
||||
): Promise<MonkeyTypes.User> {
|
||||
): Promise<MonkeyTypes.DBUser> {
|
||||
const user = await getUsersCollection().findOne({ uid });
|
||||
if (!user) throw new MonkeyError(404, "User not found", stack);
|
||||
return user;
|
||||
}
|
||||
|
||||
async function findByName(name: string): Promise<MonkeyTypes.User | undefined> {
|
||||
async function findByName(
|
||||
name: string
|
||||
): Promise<MonkeyTypes.DBUser | undefined> {
|
||||
return (
|
||||
await getUsersCollection()
|
||||
.find({ name })
|
||||
|
@ -220,7 +222,7 @@ export async function isNameAvailable(
|
|||
export async function getUserByName(
|
||||
name: string,
|
||||
stack: string
|
||||
): Promise<MonkeyTypes.User> {
|
||||
): Promise<MonkeyTypes.DBUser> {
|
||||
const user = await findByName(name);
|
||||
if (!user) throw new MonkeyError(404, "User not found", stack);
|
||||
return user;
|
||||
|
@ -284,7 +286,7 @@ export async function removeResultFilterPreset(
|
|||
export async function addTag(
|
||||
uid: string,
|
||||
name: string
|
||||
): Promise<MonkeyTypes.UserTag> {
|
||||
): Promise<MonkeyTypes.DBUserTag> {
|
||||
const user = await getUser(uid, "add tag");
|
||||
|
||||
if ((user?.tags?.length ?? 0) >= 15) {
|
||||
|
@ -315,7 +317,7 @@ export async function addTag(
|
|||
return toPush;
|
||||
}
|
||||
|
||||
export async function getTags(uid: string): Promise<MonkeyTypes.UserTag[]> {
|
||||
export async function getTags(uid: string): Promise<MonkeyTypes.DBUserTag[]> {
|
||||
const user = await getUser(uid, "get tags");
|
||||
|
||||
return user.tags ?? [];
|
||||
|
@ -396,9 +398,11 @@ export async function updateLbMemory(
|
|||
const user = await getUser(uid, "update lb memory");
|
||||
if (user.lbMemory === undefined) user.lbMemory = {};
|
||||
if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {};
|
||||
if (user.lbMemory[mode][mode2] === undefined) {
|
||||
if (user.lbMemory[mode]?.[mode2] === undefined) {
|
||||
//@ts-expect-error guarded above
|
||||
user.lbMemory[mode][mode2] = {};
|
||||
}
|
||||
//@ts-expect-error guarded above
|
||||
user.lbMemory[mode][mode2][language] = rank;
|
||||
await getUsersCollection().updateOne(
|
||||
{ uid },
|
||||
|
@ -410,7 +414,7 @@ export async function updateLbMemory(
|
|||
|
||||
export async function checkIfPb(
|
||||
uid: string,
|
||||
user: MonkeyTypes.User,
|
||||
user: MonkeyTypes.DBUser,
|
||||
result: Result
|
||||
): Promise<boolean> {
|
||||
const { mode } = result;
|
||||
|
@ -452,7 +456,7 @@ export async function checkIfPb(
|
|||
|
||||
export async function checkIfTagPb(
|
||||
uid: string,
|
||||
user: MonkeyTypes.User,
|
||||
user: MonkeyTypes.DBUser,
|
||||
result: Result
|
||||
): Promise<string[]> {
|
||||
if (user.tags === undefined || user.tags.length === 0) {
|
||||
|
@ -466,7 +470,7 @@ export async function checkIfTagPb(
|
|||
return [];
|
||||
}
|
||||
|
||||
const tagsToCheck: MonkeyTypes.UserTag[] = [];
|
||||
const tagsToCheck: MonkeyTypes.DBUserTag[] = [];
|
||||
user.tags.forEach((userTag) => {
|
||||
for (const resultTag of resultTags ?? []) {
|
||||
if (resultTag === userTag._id.toHexString()) {
|
||||
|
@ -553,7 +557,7 @@ export async function linkDiscord(
|
|||
discordId: string,
|
||||
discordAvatar?: string
|
||||
): Promise<void> {
|
||||
const updates: Partial<MonkeyTypes.User> = _.pickBy(
|
||||
const updates: Partial<MonkeyTypes.DBUser> = _.pickBy(
|
||||
{ discordId, discordAvatar },
|
||||
_.identity
|
||||
);
|
||||
|
@ -672,7 +676,7 @@ export async function editTheme(uid: string, _id, theme): Promise<void> {
|
|||
|
||||
export async function getThemes(
|
||||
uid: string
|
||||
): Promise<MonkeyTypes.CustomTheme[]> {
|
||||
): Promise<MonkeyTypes.DBCustomTheme[]> {
|
||||
const user = await getUser(uid, "get themes");
|
||||
return user.customThemes ?? [];
|
||||
}
|
||||
|
@ -705,7 +709,7 @@ export async function getStats(
|
|||
|
||||
export async function getFavoriteQuotes(
|
||||
uid
|
||||
): Promise<MonkeyTypes.User["favoriteQuotes"]> {
|
||||
): Promise<MonkeyTypes.DBUser["favoriteQuotes"]> {
|
||||
const user = await getUser(uid, "get favorite quotes");
|
||||
|
||||
return user.favoriteQuotes ?? {};
|
||||
|
@ -789,7 +793,7 @@ export async function recordAutoBanEvent(
|
|||
recentAutoBanTimestamps.push(now);
|
||||
|
||||
//update user, ban if needed
|
||||
const updateObj: Partial<MonkeyTypes.User> = {
|
||||
const updateObj: Partial<MonkeyTypes.DBUser> = {
|
||||
autoBanTimestamps: recentAutoBanTimestamps,
|
||||
};
|
||||
let banningUser = false;
|
||||
|
@ -810,8 +814,8 @@ export async function recordAutoBanEvent(
|
|||
|
||||
export async function updateProfile(
|
||||
uid: string,
|
||||
profileDetailUpdates: Partial<MonkeyTypes.UserProfileDetails>,
|
||||
inventory?: MonkeyTypes.UserInventory
|
||||
profileDetailUpdates: Partial<SharedTypes.UserProfileDetails>,
|
||||
inventory?: SharedTypes.UserInventory
|
||||
): Promise<void> {
|
||||
const profileUpdates = _.omitBy(
|
||||
flattenObjectDeep(profileDetailUpdates, "profileDetails"),
|
||||
|
@ -837,14 +841,14 @@ export async function updateProfile(
|
|||
|
||||
export async function getInbox(
|
||||
uid: string
|
||||
): Promise<MonkeyTypes.User["inbox"]> {
|
||||
): Promise<MonkeyTypes.DBUser["inbox"]> {
|
||||
const user = await getUser(uid, "get inventory");
|
||||
return user.inbox ?? [];
|
||||
}
|
||||
|
||||
type AddToInboxBulkEntry = {
|
||||
uid: string;
|
||||
mail: MonkeyTypes.MonkeyMail[];
|
||||
mail: SharedTypes.MonkeyMail[];
|
||||
};
|
||||
|
||||
export async function addToInboxBulk(
|
||||
|
@ -876,7 +880,7 @@ export async function addToInboxBulk(
|
|||
|
||||
export async function addToInbox(
|
||||
uid: string,
|
||||
mail: MonkeyTypes.MonkeyMail[],
|
||||
mail: SharedTypes.MonkeyMail[],
|
||||
inboxConfig: SharedTypes.Configuration["users"]["inbox"]
|
||||
): Promise<void> {
|
||||
const { enabled, maxMail } = inboxConfig;
|
||||
|
@ -902,11 +906,11 @@ export async function addToInbox(
|
|||
}
|
||||
|
||||
function buildRewardUpdates(
|
||||
rewards: MonkeyTypes.AllRewards[],
|
||||
rewards: SharedTypes.AllRewards[],
|
||||
inventoryIsNull = false
|
||||
): UpdateFilter<WithId<MonkeyTypes.User>> {
|
||||
): UpdateFilter<MonkeyTypes.DBUser> {
|
||||
let totalXp = 0;
|
||||
const newBadges: MonkeyTypes.Badge[] = [];
|
||||
const newBadges: SharedTypes.Badge[] = [];
|
||||
|
||||
rewards.forEach((reward) => {
|
||||
if (reward.type === "xp") {
|
||||
|
@ -954,7 +958,7 @@ export async function updateInbox(
|
|||
const mailToReadSet = new Set(mailToRead);
|
||||
const mailToDeleteSet = new Set(mailToDelete);
|
||||
|
||||
const allRewards: MonkeyTypes.AllRewards[] = [];
|
||||
const allRewards: SharedTypes.AllRewards[] = [];
|
||||
|
||||
const newInbox = inbox
|
||||
.filter((mail) => {
|
||||
|
@ -988,7 +992,7 @@ export async function updateStreak(
|
|||
timestamp: number
|
||||
): Promise<number> {
|
||||
const user = await getUser(uid, "calculate streak");
|
||||
const streak: MonkeyTypes.UserStreak = {
|
||||
const streak: SharedTypes.UserStreak = {
|
||||
lastResultTimestamp: user.streak?.lastResultTimestamp ?? 0,
|
||||
length: user.streak?.length ?? 0,
|
||||
maxLength: user.streak?.maxLength ?? 0,
|
||||
|
@ -1043,7 +1047,7 @@ export async function setBanned(uid: string, banned: boolean): Promise<void> {
|
|||
|
||||
export async function checkIfUserIsPremium(
|
||||
uid: string,
|
||||
userInfoOverride?: MonkeyTypes.User
|
||||
userInfoOverride?: MonkeyTypes.DBUser
|
||||
): Promise<boolean> {
|
||||
const user = userInfoOverride ?? (await getUser(uid, "checkIfUserIsPremium"));
|
||||
const expirationDate = user.premium?.expirationTimestamp;
|
||||
|
@ -1056,7 +1060,7 @@ export async function checkIfUserIsPremium(
|
|||
export async function logIpAddress(
|
||||
uid: string,
|
||||
ip: string,
|
||||
userInfoOverride?: MonkeyTypes.User
|
||||
userInfoOverride?: MonkeyTypes.DBUser
|
||||
): Promise<void> {
|
||||
const user = userInfoOverride ?? (await getUser(uid, "logIpAddress"));
|
||||
const currentIps = user.ips ?? [];
|
||||
|
|
|
@ -72,7 +72,7 @@ function checkIfUserIsAdmin(): RequestHandler {
|
|||
* Note that this middleware must be used after authentication in the middleware stack.
|
||||
*/
|
||||
function checkUserPermissions(
|
||||
options: ValidationOptions<MonkeyTypes.User>
|
||||
options: ValidationOptions<MonkeyTypes.DBUser>
|
||||
): RequestHandler {
|
||||
const { criteria, invalidMessage = "You don't have permission to do this." } =
|
||||
options;
|
||||
|
|
127
backend/src/types/types.d.ts
vendored
127
backend/src/types/types.d.ts
vendored
|
@ -18,103 +18,27 @@ declare namespace MonkeyTypes {
|
|||
ctx: Readonly<Context>;
|
||||
} & ExpressRequest;
|
||||
|
||||
// Data Model
|
||||
|
||||
type UserProfileDetails = {
|
||||
bio?: string;
|
||||
keyboard?: string;
|
||||
socialProfiles: {
|
||||
twitter?: string;
|
||||
github?: string;
|
||||
website?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Reward<T> = {
|
||||
type: string;
|
||||
item: T;
|
||||
};
|
||||
|
||||
type XpReward = {
|
||||
type: "xp";
|
||||
item: number;
|
||||
} & Reward<number>;
|
||||
|
||||
type BadgeReward = {
|
||||
type: "badge";
|
||||
item: Badge;
|
||||
} & Reward<Badge>;
|
||||
|
||||
type AllRewards = XpReward | BadgeReward;
|
||||
|
||||
type MonkeyMail = {
|
||||
id: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
timestamp: number;
|
||||
read: boolean;
|
||||
rewards: AllRewards[];
|
||||
};
|
||||
|
||||
type UserIpHistory = string[];
|
||||
|
||||
type User = {
|
||||
autoBanTimestamps?: number[];
|
||||
addedAt: number;
|
||||
verified?: boolean;
|
||||
bananas?: number;
|
||||
completedTests?: number;
|
||||
discordId?: string;
|
||||
email: string;
|
||||
lastNameChange?: number;
|
||||
lbMemory?: object;
|
||||
lbPersonalBests?: LbPersonalBests;
|
||||
name: string;
|
||||
customThemes?: CustomTheme[];
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
quoteRatings?: UserQuoteRatings;
|
||||
startedTests?: number;
|
||||
tags?: UserTag[];
|
||||
timeTyping?: number;
|
||||
uid: string;
|
||||
quoteMod?: boolean;
|
||||
configurationMod?: boolean;
|
||||
admin?: boolean;
|
||||
canReport?: boolean;
|
||||
banned?: boolean;
|
||||
canManageApeKeys?: boolean;
|
||||
favoriteQuotes?: Record<string, string[]>;
|
||||
needsToChangeName?: boolean;
|
||||
discordAvatar?: string;
|
||||
type DBUser = Omit<
|
||||
SharedTypes.User,
|
||||
"resultFilterPresets" | "tags" | "customThemes"
|
||||
> & {
|
||||
_id: ObjectId;
|
||||
resultFilterPresets?: WithObjectIdArray<SharedTypes.ResultFilters[]>;
|
||||
profileDetails?: UserProfileDetails;
|
||||
inventory?: UserInventory;
|
||||
xp?: number;
|
||||
inbox?: MonkeyMail[];
|
||||
streak?: UserStreak;
|
||||
lastReultHashes?: string[];
|
||||
lbOptOut?: boolean;
|
||||
premium?: PremiumInfo;
|
||||
ips?: UserIpHistory;
|
||||
tags?: DBUserTag[];
|
||||
lbPersonalBests?: LbPersonalBests;
|
||||
customThemes?: DBCustomTheme[];
|
||||
autoBanTimestamps?: number[];
|
||||
inbox?: SharedTypes.MonkeyMail[];
|
||||
ips?: string[];
|
||||
canReport?: boolean;
|
||||
lastNameChange?: number;
|
||||
canManageApeKeys?: boolean;
|
||||
bananas?: number;
|
||||
};
|
||||
|
||||
type UserStreak = {
|
||||
lastResultTimestamp: number;
|
||||
length: number;
|
||||
maxLength: number;
|
||||
hourOffset?: number;
|
||||
};
|
||||
type DBCustomTheme = WithObjectId<SharedTypes.CustomTheme>;
|
||||
|
||||
type UserInventory = {
|
||||
badges: Badge[];
|
||||
};
|
||||
|
||||
type Badge = {
|
||||
id: number;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
type UserQuoteRatings = Record<string, Record<string, number>>;
|
||||
type DBUserTag = WithObjectId<SharedTypes.UserTag>;
|
||||
|
||||
type LbPersonalBests = {
|
||||
time: Record<number, Record<string, SharedTypes.PersonalBest>>;
|
||||
|
@ -129,18 +53,6 @@ declare namespace MonkeyTypes {
|
|||
_id: ObjectId;
|
||||
}[];
|
||||
|
||||
type UserTag = {
|
||||
_id: ObjectId;
|
||||
name: string;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
};
|
||||
|
||||
type CustomTheme = {
|
||||
_id: ObjectId;
|
||||
name: string;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
type ApeKeyDB = SharedTypes.ApeKey & {
|
||||
_id: ObjectId;
|
||||
uid: string;
|
||||
|
@ -188,9 +100,4 @@ declare namespace MonkeyTypes {
|
|||
frontendForcedConfig?: Record<string, string[] | boolean[]>;
|
||||
frontendFunctions?: string[];
|
||||
};
|
||||
|
||||
type PremiumInfo = {
|
||||
startTimestamp: number;
|
||||
expirationTimestamp: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { v4 } from "uuid";
|
||||
|
||||
type MonkeyMailOptions = Partial<Omit<MonkeyTypes.MonkeyMail, "id" | "read">>;
|
||||
type MonkeyMailOptions = Partial<Omit<SharedTypes.MonkeyMail, "id" | "read">>;
|
||||
|
||||
export function buildMonkeyMail(
|
||||
options: MonkeyMailOptions
|
||||
): MonkeyTypes.MonkeyMail {
|
||||
): SharedTypes.MonkeyMail {
|
||||
return {
|
||||
id: v4(),
|
||||
subject: options.subject ?? "",
|
||||
|
|
|
@ -43,7 +43,7 @@ async function handleDailyLeaderboardResults(
|
|||
if (inboxConfig.enabled && xpRewardBrackets.length > 0) {
|
||||
const mailEntries: {
|
||||
uid: string;
|
||||
mail: MonkeyTypes.MonkeyMail[];
|
||||
mail: SharedTypes.MonkeyMail[];
|
||||
}[] = [];
|
||||
|
||||
allResults.forEach((entry) => {
|
||||
|
@ -132,7 +132,7 @@ async function handleWeeklyXpLeaderboardResults(
|
|||
|
||||
const mailEntries: {
|
||||
uid: string;
|
||||
mail: MonkeyTypes.MonkeyMail[];
|
||||
mail: SharedTypes.MonkeyMail[];
|
||||
}[] = [];
|
||||
|
||||
allResults.forEach((entry) => {
|
||||
|
|
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
|
@ -80,6 +80,7 @@
|
|||
"ts-jest": "29.1.2",
|
||||
"ts-loader": "9.2.6",
|
||||
"ts-node-dev": "2.0.0",
|
||||
"typescript": "5.3.3",
|
||||
"webpack": "5.72.0",
|
||||
"webpack-bundle-analyzer": "4.5.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
|
@ -21347,17 +21348,16 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.5.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
|
||||
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
"ts-jest": "29.1.2",
|
||||
"ts-loader": "9.2.6",
|
||||
"ts-node-dev": "2.0.0",
|
||||
"typescript": "5.3.3",
|
||||
"webpack": "5.72.0",
|
||||
"webpack-bundle-analyzer": "4.5.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
|
|
|
@ -217,7 +217,7 @@ async function createFilterPresetCallback(name: string): Promise<void> {
|
|||
const result = await Ape.users.addResultFilterPreset({ ...filters, name });
|
||||
Loader.hide();
|
||||
if (result.status === 200) {
|
||||
addFilterPresetToSnapshot({ ...filters, name, _id: result.data });
|
||||
addFilterPresetToSnapshot({ ...filters, name, _id: result.data as string });
|
||||
void updateFilterPresets();
|
||||
Notifications.add("Filter preset created", 1);
|
||||
} else {
|
||||
|
@ -299,7 +299,7 @@ function setAllFilters(
|
|||
});
|
||||
}
|
||||
|
||||
export function loadTags(tags: MonkeyTypes.Tag[]): void {
|
||||
export function loadTags(tags: MonkeyTypes.UserTag[]): void {
|
||||
tags.forEach((tag) => {
|
||||
defaultResultFilters.tags[tag._id] = true;
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ export default class Users {
|
|||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
async getData(): Ape.EndpointResponse {
|
||||
async getData(): Ape.EndpointResponse<Ape.Users.GetUser> {
|
||||
return await this.httpClient.get(BASE_PATH);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ export default class Users {
|
|||
captcha: string,
|
||||
email?: string,
|
||||
uid?: string
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
email,
|
||||
name,
|
||||
|
@ -25,23 +25,23 @@ export default class Users {
|
|||
return await this.httpClient.post(`${BASE_PATH}/signup`, { payload });
|
||||
}
|
||||
|
||||
async getNameAvailability(name: string): Ape.EndpointResponse {
|
||||
async getNameAvailability(name: string): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.get(`${BASE_PATH}/checkName/${name}`);
|
||||
}
|
||||
|
||||
async delete(): Ape.EndpointResponse {
|
||||
async delete(): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.delete(BASE_PATH);
|
||||
}
|
||||
|
||||
async reset(): Ape.EndpointResponse {
|
||||
async reset(): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.patch(`${BASE_PATH}/reset`);
|
||||
}
|
||||
|
||||
async optOutOfLeaderboards(): Ape.EndpointResponse {
|
||||
async optOutOfLeaderboards(): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/optOutOfLeaderboards`);
|
||||
}
|
||||
|
||||
async updateName(name: string): Ape.EndpointResponse {
|
||||
async updateName(name: string): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.patch(`${BASE_PATH}/name`, {
|
||||
payload: { name },
|
||||
});
|
||||
|
@ -52,7 +52,7 @@ export default class Users {
|
|||
mode2: SharedTypes.Config.Mode2<M>,
|
||||
language: string,
|
||||
rank: number
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
mode,
|
||||
mode2,
|
||||
|
@ -68,7 +68,7 @@ export default class Users {
|
|||
async updateEmail(
|
||||
newEmail: string,
|
||||
previousEmail: string
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
newEmail,
|
||||
previousEmail,
|
||||
|
@ -77,31 +77,31 @@ export default class Users {
|
|||
return await this.httpClient.patch(`${BASE_PATH}/email`, { payload });
|
||||
}
|
||||
|
||||
async deletePersonalBests(): Ape.EndpointResponse {
|
||||
async deletePersonalBests(): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.delete(`${BASE_PATH}/personalBests`);
|
||||
}
|
||||
|
||||
async addResultFilterPreset(
|
||||
filter: SharedTypes.ResultFilters
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<string> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/resultFilterPresets`, {
|
||||
payload: filter,
|
||||
});
|
||||
}
|
||||
|
||||
async removeResultFilterPreset(id: string): Ape.EndpointResponse {
|
||||
async removeResultFilterPreset(id: string): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.delete(
|
||||
`${BASE_PATH}/resultFilterPresets/${id}`
|
||||
);
|
||||
}
|
||||
|
||||
async createTag(tagName: string): Ape.EndpointResponse {
|
||||
async createTag(tagName: string): Ape.EndpointResponse<SharedTypes.UserTag> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/tags`, {
|
||||
payload: { tagName },
|
||||
});
|
||||
}
|
||||
|
||||
async editTag(tagId: string, newName: string): Ape.EndpointResponse {
|
||||
async editTag(tagId: string, newName: string): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
tagId,
|
||||
newName,
|
||||
|
@ -110,24 +110,24 @@ export default class Users {
|
|||
return await this.httpClient.patch(`${BASE_PATH}/tags`, { payload });
|
||||
}
|
||||
|
||||
async deleteTag(tagId: string): Ape.EndpointResponse {
|
||||
async deleteTag(tagId: string): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.delete(`${BASE_PATH}/tags/${tagId}`);
|
||||
}
|
||||
|
||||
async deleteTagPersonalBest(tagId: string): Ape.EndpointResponse {
|
||||
async deleteTagPersonalBest(tagId: string): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.delete(
|
||||
`${BASE_PATH}/tags/${tagId}/personalBest`
|
||||
);
|
||||
}
|
||||
|
||||
async getCustomThemes(): Ape.EndpointResponse {
|
||||
async getCustomThemes(): Ape.EndpointResponse<SharedTypes.CustomTheme[]> {
|
||||
return await this.httpClient.get(`${BASE_PATH}/customThemes`);
|
||||
}
|
||||
|
||||
async editCustomTheme(
|
||||
themeId: string,
|
||||
newTheme: Partial<MonkeyTypes.CustomTheme>
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
themeId: themeId,
|
||||
theme: {
|
||||
|
@ -140,7 +140,7 @@ export default class Users {
|
|||
});
|
||||
}
|
||||
|
||||
async deleteCustomTheme(themeId: string): Ape.EndpointResponse {
|
||||
async deleteCustomTheme(themeId: string): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
themeId: themeId,
|
||||
};
|
||||
|
@ -151,12 +151,12 @@ export default class Users {
|
|||
|
||||
async addCustomTheme(
|
||||
newTheme: Partial<MonkeyTypes.CustomTheme>
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<SharedTypes.CustomTheme> {
|
||||
const payload = { name: newTheme.name, colors: newTheme.colors };
|
||||
return await this.httpClient.post(`${BASE_PATH}/customThemes`, { payload });
|
||||
}
|
||||
|
||||
async getOauthLink(): Ape.EndpointResponse {
|
||||
async getOauthLink(): Ape.EndpointResponse<Ape.Users.GetOauthLink> {
|
||||
return await this.httpClient.get(`${BASE_PATH}/discord/oauth`);
|
||||
}
|
||||
|
||||
|
@ -164,20 +164,20 @@ export default class Users {
|
|||
tokenType: string,
|
||||
accessToken: string,
|
||||
state: string
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<Ape.Users.LinkDiscord> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/discord/link`, {
|
||||
payload: { tokenType, accessToken, state },
|
||||
});
|
||||
}
|
||||
|
||||
async unlinkDiscord(): Ape.EndpointResponse {
|
||||
async unlinkDiscord(): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/discord/unlink`);
|
||||
}
|
||||
|
||||
async addQuoteToFavorites(
|
||||
language: string,
|
||||
quoteId: string
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<null> {
|
||||
const payload = { language, quoteId };
|
||||
return await this.httpClient.post(`${BASE_PATH}/favoriteQuotes`, {
|
||||
payload,
|
||||
|
@ -187,25 +187,29 @@ export default class Users {
|
|||
async removeQuoteFromFavorites(
|
||||
language: string,
|
||||
quoteId: string
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<null> {
|
||||
const payload = { language, quoteId };
|
||||
return await this.httpClient.delete(`${BASE_PATH}/favoriteQuotes`, {
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
async getProfileByUid(uid: string): Promise<Ape.EndpointResponse> {
|
||||
async getProfileByUid(
|
||||
uid: string
|
||||
): Ape.EndpointResponse<SharedTypes.UserProfile> {
|
||||
return await this.httpClient.get(`${BASE_PATH}/${uid}/profile?isUid`);
|
||||
}
|
||||
|
||||
async getProfileByName(name: string): Promise<Ape.EndpointResponse> {
|
||||
async getProfileByName(
|
||||
name: string
|
||||
): Ape.EndpointResponse<SharedTypes.UserProfile> {
|
||||
return await this.httpClient.get(`${BASE_PATH}/${name}/profile`);
|
||||
}
|
||||
|
||||
async updateProfile(
|
||||
profileUpdates: Partial<MonkeyTypes.UserDetails>,
|
||||
profileUpdates: Partial<SharedTypes.UserProfileDetails>,
|
||||
selectedBadgeId?: number
|
||||
): Promise<Ape.EndpointResponse> {
|
||||
): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.patch(`${BASE_PATH}/profile`, {
|
||||
payload: {
|
||||
...profileUpdates,
|
||||
|
@ -214,14 +218,14 @@ export default class Users {
|
|||
});
|
||||
}
|
||||
|
||||
async getInbox(): Promise<Ape.EndpointResponse> {
|
||||
async getInbox(): Ape.EndpointResponse<Ape.Users.GetInbox> {
|
||||
return await this.httpClient.get(`${BASE_PATH}/inbox`);
|
||||
}
|
||||
|
||||
async updateInbox(options: {
|
||||
mailIdsToDelete?: string[];
|
||||
mailIdsToMarkRead?: string[];
|
||||
}): Promise<Ape.EndpointResponse> {
|
||||
}): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
mailIdsToDelete: options.mailIdsToDelete,
|
||||
mailIdsToMarkRead: options.mailIdsToMarkRead,
|
||||
|
@ -234,7 +238,7 @@ export default class Users {
|
|||
reason: string,
|
||||
comment: string,
|
||||
captcha: string
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<null> {
|
||||
const payload = {
|
||||
uid,
|
||||
reason,
|
||||
|
@ -245,23 +249,23 @@ export default class Users {
|
|||
return await this.httpClient.post(`${BASE_PATH}/report`, { payload });
|
||||
}
|
||||
|
||||
async verificationEmail(): Ape.EndpointResponse {
|
||||
async verificationEmail(): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.get(`${BASE_PATH}/verificationEmail`);
|
||||
}
|
||||
|
||||
async forgotPasswordEmail(email: string): Ape.EndpointResponse {
|
||||
async forgotPasswordEmail(email: string): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/forgotPasswordEmail`, {
|
||||
payload: { email },
|
||||
});
|
||||
}
|
||||
|
||||
async setStreakHourOffset(hourOffset: number): Ape.EndpointResponse {
|
||||
async setStreakHourOffset(hourOffset: number): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/setStreakHourOffset`, {
|
||||
payload: { hourOffset },
|
||||
});
|
||||
}
|
||||
|
||||
async revokeAllTokens(): Ape.EndpointResponse {
|
||||
async revokeAllTokens(): Ape.EndpointResponse<null> {
|
||||
return await this.httpClient.post(`${BASE_PATH}/revokeAllTokens`);
|
||||
}
|
||||
}
|
||||
|
|
3
frontend/src/ts/ape/types/ape.d.ts
vendored
3
frontend/src/ts/ape/types/ape.d.ts
vendored
|
@ -26,8 +26,7 @@ declare namespace Ape {
|
|||
data: TData | null;
|
||||
};
|
||||
|
||||
// todo: remove any after all ape endpoints are typed
|
||||
type EndpointResponse<TData = any> = Promise<HttpClientResponse<TData>>;
|
||||
type EndpointResponse<TData> = Promise<HttpClientResponse<TData>>;
|
||||
|
||||
type HttpClient = {
|
||||
get: HttpClientMethod;
|
||||
|
|
18
frontend/src/ts/ape/types/users.d.ts
vendored
Normal file
18
frontend/src/ts/ape/types/users.d.ts
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// for some reason when using the dot notaion, the types are not being recognized as used
|
||||
declare namespace Ape.Users {
|
||||
type GetUser = SharedTypes.User & {
|
||||
inboxUnreadSize: number;
|
||||
isPremium: boolean;
|
||||
};
|
||||
type GetOauthLink = {
|
||||
url: string;
|
||||
};
|
||||
type LinkDiscord = {
|
||||
discordId: string;
|
||||
discordAvatar: string;
|
||||
};
|
||||
type GetInbox = {
|
||||
inbox: SharedTypes.MonkeyMail[] | undefined;
|
||||
};
|
||||
}
|
|
@ -22,7 +22,7 @@ const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
|
|||
export async function withRetry<ResponseDataType>(
|
||||
fn: () => Ape.EndpointResponse<ResponseDataType>,
|
||||
opts?: RetryOptions<ResponseDataType>
|
||||
): Ape.EndpointResponse {
|
||||
): Ape.EndpointResponse<ResponseDataType> {
|
||||
const retry = async (
|
||||
previousData: Ape.HttpClientResponse<ResponseDataType>,
|
||||
completeOpts: Required<RetryOptions<ResponseDataType>>
|
||||
|
|
|
@ -805,7 +805,7 @@ $("footer").on("click", ".leftright .right .current-theme", (e) => {
|
|||
if (e.shiftKey) {
|
||||
if (!Config.customTheme) {
|
||||
if (isAuthenticated()) {
|
||||
if ((DB.getSnapshot()?.customThemes.length ?? 0) < 1) {
|
||||
if ((DB.getSnapshot()?.customThemes?.length ?? 0) < 1) {
|
||||
Notifications.add("No custom themes!", 0);
|
||||
UpdateConfig.setCustomTheme(false);
|
||||
// UpdateConfig.setCustomThemeId("");
|
||||
|
|
|
@ -33,7 +33,11 @@ export function update(): void {
|
|||
|
||||
if (!snapshot) return;
|
||||
|
||||
if (snapshot.customThemes.length === 0) {
|
||||
if (snapshot.customThemes === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (snapshot.customThemes?.length === 0) {
|
||||
return;
|
||||
}
|
||||
snapshot.customThemes.forEach((theme) => {
|
||||
|
|
|
@ -1440,7 +1440,7 @@ export function setRandomTheme(
|
|||
return false;
|
||||
}
|
||||
if (!DB.getSnapshot()) return true;
|
||||
if (DB.getSnapshot()?.customThemes.length === 0) {
|
||||
if (DB.getSnapshot()?.customThemes?.length === 0) {
|
||||
Notifications.add("You need to create a custom theme first", 0);
|
||||
config.randomTheme = "off";
|
||||
return false;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import defaultConfig from "./default-config";
|
||||
|
||||
export const defaultSnap: MonkeyTypes.Snapshot = {
|
||||
results: undefined,
|
||||
personalBests: {
|
||||
|
@ -8,13 +10,15 @@ export const defaultSnap: MonkeyTypes.Snapshot = {
|
|||
custom: {},
|
||||
},
|
||||
name: "",
|
||||
email: "",
|
||||
uid: "",
|
||||
isPremium: false,
|
||||
config: defaultConfig,
|
||||
customThemes: [],
|
||||
presets: [],
|
||||
tags: [],
|
||||
favouriteThemes: [],
|
||||
banned: undefined,
|
||||
verified: undefined,
|
||||
emailVerified: undefined,
|
||||
lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } },
|
||||
typingStats: {
|
||||
timeTyping: 0,
|
||||
|
|
|
@ -153,10 +153,7 @@ async function getDataAndInit(): Promise<boolean> {
|
|||
const areConfigsEqual =
|
||||
JSON.stringify(Config) === JSON.stringify(snapshot.config);
|
||||
|
||||
if (
|
||||
snapshot.config &&
|
||||
(UpdateConfig.localStorageConfig === undefined || !areConfigsEqual)
|
||||
) {
|
||||
if (UpdateConfig.localStorageConfig === undefined || !areConfigsEqual) {
|
||||
console.log(
|
||||
"no local config or local and db configs are different - applying db"
|
||||
);
|
||||
|
|
|
@ -164,6 +164,10 @@ class QuotesController {
|
|||
const quoteIds: string[] = [];
|
||||
const { favoriteQuotes } = snapshot;
|
||||
|
||||
if (favoriteQuotes === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object.keys(favoriteQuotes).forEach((language) => {
|
||||
if (removeLanguageSize(language) !== normalizedLanguage) {
|
||||
return;
|
||||
|
@ -190,6 +194,10 @@ class QuotesController {
|
|||
|
||||
const { favoriteQuotes } = snapshot;
|
||||
|
||||
if (favoriteQuotes === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedQuoteLanguage = removeLanguageSize(quoteLanguage);
|
||||
|
||||
const matchedLanguage = Object.keys(favoriteQuotes).find((language) => {
|
||||
|
|
|
@ -262,7 +262,7 @@ async function changeThemeList(): Promise<void> {
|
|||
return t.name;
|
||||
});
|
||||
} else if (Config.randomTheme === "custom" && DB.getSnapshot()) {
|
||||
themesList = DB.getSnapshot()?.customThemes.map((ct) => ct._id) ?? [];
|
||||
themesList = DB.getSnapshot()?.customThemes?.map((ct) => ct._id) ?? [];
|
||||
}
|
||||
Misc.shuffle(themesList);
|
||||
randomThemeIndex = 0;
|
||||
|
@ -284,7 +284,7 @@ export async function randomizeTheme(): Promise<void> {
|
|||
let colorsOverride: string[] | undefined;
|
||||
|
||||
if (Config.randomTheme === "custom") {
|
||||
const theme = DB.getSnapshot()?.customThemes.find(
|
||||
const theme = DB.getSnapshot()?.customThemes?.find(
|
||||
(ct) => ct._id === randomTheme
|
||||
);
|
||||
colorsOverride = theme?.colors;
|
||||
|
@ -297,7 +297,7 @@ export async function randomizeTheme(): Promise<void> {
|
|||
let name = randomTheme.replace(/_/g, " ");
|
||||
if (Config.randomTheme === "custom") {
|
||||
name = (
|
||||
DB.getSnapshot()?.customThemes.find((ct) => ct._id === randomTheme)
|
||||
DB.getSnapshot()?.customThemes?.find((ct) => ct._id === randomTheme)
|
||||
?.name ?? "custom"
|
||||
).replace(/_/g, " ");
|
||||
}
|
||||
|
|
|
@ -84,10 +84,17 @@ export async function initSnapshot(): Promise<
|
|||
};
|
||||
}
|
||||
|
||||
const userData = userResponse.data;
|
||||
const configData = configResponse.data;
|
||||
const presetsData = presetsResponse.data;
|
||||
|
||||
const [userData] = [userResponse].map((response) => response.data);
|
||||
if (userData === null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||
throw {
|
||||
message: "Request was successful but user data is null",
|
||||
responseCode: 200,
|
||||
};
|
||||
}
|
||||
|
||||
snap.name = userData.name;
|
||||
snap.personalBests = userData.personalBests;
|
||||
|
@ -110,15 +117,13 @@ export async function initSnapshot(): Promise<
|
|||
snap.discordAvatar = userData.discordAvatar;
|
||||
snap.needsToChangeName = userData.needsToChangeName;
|
||||
snap.typingStats = {
|
||||
timeTyping: userData.timeTyping,
|
||||
startedTests: userData.startedTests,
|
||||
completedTests: userData.completedTests,
|
||||
timeTyping: userData.timeTyping ?? 0,
|
||||
startedTests: userData.startedTests ?? 0,
|
||||
completedTests: userData.completedTests ?? 0,
|
||||
};
|
||||
snap.quoteMod = userData.quoteMod;
|
||||
snap.favoriteQuotes = userData.favoriteQuotes ?? {};
|
||||
snap.quoteRatings = userData.quoteRatings;
|
||||
snap.favouriteThemes =
|
||||
userData.favouriteThemes === undefined ? [] : userData.favouriteThemes;
|
||||
snap.details = userData.profileDetails;
|
||||
snap.addedAt = userData.addedAt;
|
||||
snap.inventory = userData.inventory;
|
||||
|
@ -133,13 +138,7 @@ export async function initSnapshot(): Promise<
|
|||
snap.streakHourOffset =
|
||||
hourOffset === undefined || hourOffset === null ? undefined : hourOffset;
|
||||
|
||||
if (
|
||||
userData.lbMemory?.time15 !== undefined ||
|
||||
userData.lbMemory?.time60 !== undefined
|
||||
) {
|
||||
//old memory format
|
||||
snap.lbMemory = {} as MonkeyTypes.LeaderboardMemory;
|
||||
} else if (userData.lbMemory !== undefined) {
|
||||
if (userData.lbMemory !== undefined) {
|
||||
snap.lbMemory = userData.lbMemory;
|
||||
}
|
||||
// if (ActivePage.get() === "loading") {
|
||||
|
@ -163,24 +162,43 @@ export async function initSnapshot(): Promise<
|
|||
// LoadingPage.updateText("Downloading tags...");
|
||||
snap.customThemes = userData.customThemes ?? [];
|
||||
|
||||
const userDataTags: MonkeyTypes.Tag[] = userData.tags ?? [];
|
||||
// const userDataTags: MonkeyTypes.UserTagWithDisplay[] = userData.tags ?? [];
|
||||
|
||||
userDataTags.forEach((tag) => {
|
||||
tag.display = tag.name.replaceAll("_", " ");
|
||||
tag.personalBests ??= {
|
||||
time: {},
|
||||
words: {},
|
||||
quote: {},
|
||||
zen: {},
|
||||
custom: {},
|
||||
};
|
||||
// userDataTags.forEach((tag) => {
|
||||
// tag.display = tag.name.replaceAll("_", " ");
|
||||
// tag.personalBests ??= {
|
||||
// time: {},
|
||||
// words: {},
|
||||
// quote: {},
|
||||
// zen: {},
|
||||
// custom: {},
|
||||
// };
|
||||
|
||||
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
|
||||
tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
|
||||
}
|
||||
});
|
||||
// for (const mode of ["time", "words", "quote", "zen", "custom"]) {
|
||||
// tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
|
||||
// }
|
||||
// });
|
||||
|
||||
snap.tags = userDataTags;
|
||||
// snap.tags = userDataTags;
|
||||
|
||||
snap.tags =
|
||||
userData.tags?.map((tag) => {
|
||||
const newTag = {
|
||||
...tag,
|
||||
display: tag.name.replaceAll("_", " "),
|
||||
personalBests: {
|
||||
time: {},
|
||||
words: {},
|
||||
quote: {},
|
||||
zen: {},
|
||||
custom: {},
|
||||
},
|
||||
};
|
||||
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
|
||||
newTag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
|
||||
}
|
||||
return newTag;
|
||||
}) ?? [];
|
||||
|
||||
snap.tags = snap.tags?.sort((a, b) => {
|
||||
if (a.name > b.name) {
|
||||
|
@ -304,7 +322,7 @@ export async function getUserResults(offset?: number): Promise<boolean> {
|
|||
function _getCustomThemeById(
|
||||
themeID: string
|
||||
): MonkeyTypes.CustomTheme | undefined {
|
||||
return dbSnapshot?.customThemes.find((t) => t._id === themeID);
|
||||
return dbSnapshot?.customThemes?.find((t) => t._id === themeID);
|
||||
}
|
||||
|
||||
export async function addCustomTheme(
|
||||
|
@ -312,6 +330,10 @@ export async function addCustomTheme(
|
|||
): Promise<boolean> {
|
||||
if (!dbSnapshot) return false;
|
||||
|
||||
if (dbSnapshot.customThemes === undefined) {
|
||||
dbSnapshot.customThemes = [];
|
||||
}
|
||||
|
||||
if (dbSnapshot.customThemes.length >= 10) {
|
||||
Notifications.add("Too many custom themes!", 0);
|
||||
return false;
|
||||
|
@ -323,9 +345,14 @@ export async function addCustomTheme(
|
|||
return false;
|
||||
}
|
||||
|
||||
if (response.data === null) {
|
||||
Notifications.add("Error adding custom theme: No data returned", -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCustomTheme: MonkeyTypes.CustomTheme = {
|
||||
...theme,
|
||||
_id: response.data.theme._id as string,
|
||||
_id: response.data._id as string,
|
||||
};
|
||||
|
||||
dbSnapshot.customThemes.push(newCustomTheme);
|
||||
|
@ -339,7 +366,11 @@ export async function editCustomTheme(
|
|||
if (!isAuthenticated()) return false;
|
||||
if (!dbSnapshot) return false;
|
||||
|
||||
const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId);
|
||||
if (dbSnapshot.customThemes === undefined) {
|
||||
dbSnapshot.customThemes = [];
|
||||
}
|
||||
|
||||
const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId);
|
||||
if (!customTheme) {
|
||||
Notifications.add(
|
||||
"Editing failed: Custom theme with id: " + themeId + " does not exist",
|
||||
|
@ -369,7 +400,7 @@ export async function deleteCustomTheme(themeId: string): Promise<boolean> {
|
|||
if (!isAuthenticated()) return false;
|
||||
if (!dbSnapshot) return false;
|
||||
|
||||
const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId);
|
||||
const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId);
|
||||
if (!customTheme) return false;
|
||||
|
||||
const response = await Ape.users.deleteCustomTheme(themeId);
|
||||
|
@ -378,7 +409,7 @@ export async function deleteCustomTheme(themeId: string): Promise<boolean> {
|
|||
return false;
|
||||
}
|
||||
|
||||
dbSnapshot.customThemes = dbSnapshot.customThemes.filter(
|
||||
dbSnapshot.customThemes = dbSnapshot.customThemes?.filter(
|
||||
(t) => t._id !== themeId
|
||||
);
|
||||
|
||||
|
@ -762,7 +793,7 @@ export async function saveLocalTagPB<M extends SharedTypes.Config.Mode>(
|
|||
function cont(): void {
|
||||
const filteredtag = dbSnapshot?.tags?.filter(
|
||||
(t) => t._id === tagId
|
||||
)[0] as MonkeyTypes.Tag;
|
||||
)[0] as MonkeyTypes.UserTag;
|
||||
|
||||
filteredtag.personalBests ??= {
|
||||
time: {},
|
||||
|
@ -879,8 +910,14 @@ export async function updateLbMemory<M extends SharedTypes.Config.Mode>(
|
|||
if (snapshot.lbMemory[timeMode][timeMode2] === undefined) {
|
||||
snapshot.lbMemory[timeMode][timeMode2] = {};
|
||||
}
|
||||
const current = snapshot.lbMemory[timeMode][timeMode2][language];
|
||||
snapshot.lbMemory[timeMode][timeMode2][language] = rank;
|
||||
const current = snapshot.lbMemory?.[timeMode]?.[timeMode2]?.[language];
|
||||
|
||||
//this is protected above so not sure why it would be undefined
|
||||
const mem = snapshot.lbMemory[timeMode][timeMode2] as Record<
|
||||
string,
|
||||
number
|
||||
>;
|
||||
mem[language] = rank;
|
||||
if (api && current !== rank) {
|
||||
await Ape.users.updateLeaderboardMemory(mode, mode2, language, rank);
|
||||
}
|
||||
|
@ -914,26 +951,17 @@ export function updateLocalStats(started: number, time: number): void {
|
|||
const snapshot = getSnapshot();
|
||||
if (!snapshot) return;
|
||||
if (snapshot.typingStats === undefined) {
|
||||
snapshot.typingStats = {} as MonkeyTypes.TypingStats;
|
||||
}
|
||||
if (snapshot?.typingStats !== undefined) {
|
||||
if (snapshot.typingStats.timeTyping === undefined) {
|
||||
snapshot.typingStats.timeTyping = time;
|
||||
} else {
|
||||
snapshot.typingStats.timeTyping += time;
|
||||
}
|
||||
if (snapshot.typingStats.startedTests === undefined) {
|
||||
snapshot.typingStats.startedTests = started;
|
||||
} else {
|
||||
snapshot.typingStats.startedTests += started;
|
||||
}
|
||||
if (snapshot.typingStats.completedTests === undefined) {
|
||||
snapshot.typingStats.completedTests = 1;
|
||||
} else {
|
||||
snapshot.typingStats.completedTests += 1;
|
||||
}
|
||||
snapshot.typingStats = {
|
||||
timeTyping: 0,
|
||||
startedTests: 0,
|
||||
completedTests: 0,
|
||||
};
|
||||
}
|
||||
|
||||
snapshot.typingStats.timeTyping += time;
|
||||
snapshot.typingStats.startedTests += started;
|
||||
snapshot.typingStats.completedTests += 1;
|
||||
|
||||
setSnapshot(snapshot);
|
||||
}
|
||||
|
||||
|
@ -948,7 +976,7 @@ export function addXp(xp: number): void {
|
|||
setSnapshot(snapshot);
|
||||
}
|
||||
|
||||
export function addBadge(badge: MonkeyTypes.Badge): void {
|
||||
export function addBadge(badge: SharedTypes.Badge): void {
|
||||
const snapshot = getSnapshot();
|
||||
if (!snapshot) return;
|
||||
|
||||
|
|
|
@ -251,7 +251,7 @@ function checkLbMemory(lb: LbKey): void {
|
|||
side = "right";
|
||||
}
|
||||
|
||||
const memory = DB.getSnapshot()?.lbMemory?.time?.[lb]?.["english"] ?? 0;
|
||||
const memory = DB.getSnapshot()?.lbMemory?.["time"]?.[lb]?.["english"] ?? 0;
|
||||
|
||||
const rank = currentRank[lb]?.rank;
|
||||
if (rank) {
|
||||
|
|
|
@ -9,15 +9,13 @@ import * as ActivePage from "../states/active-page";
|
|||
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
|
||||
|
||||
type ProfileViewPaths = "profile" | "account";
|
||||
type UserProfileOrSnapshot = SharedTypes.UserProfile | MonkeyTypes.Snapshot;
|
||||
|
||||
export type ProfileData = {
|
||||
allTimeLbs: MonkeyTypes.LeaderboardMemory;
|
||||
uid: string;
|
||||
} & MonkeyTypes.Snapshot;
|
||||
//this is probably the dirtiest code ive ever written
|
||||
|
||||
export async function update(
|
||||
where: ProfileViewPaths,
|
||||
profile: Partial<ProfileData>
|
||||
profile: UserProfileOrSnapshot
|
||||
): Promise<void> {
|
||||
const elementClass = where.charAt(0).toUpperCase() + where.slice(1);
|
||||
const profileElement = $(`.page${elementClass} .profile`);
|
||||
|
@ -140,10 +138,11 @@ export async function update(
|
|||
const results = DB.getSnapshot()?.results;
|
||||
const lastResult = results?.[0];
|
||||
|
||||
const streakOffset = (profile as MonkeyTypes.Snapshot).streakHourOffset;
|
||||
|
||||
const dayInMilis = 1000 * 60 * 60 * 24;
|
||||
|
||||
let target =
|
||||
Misc.getCurrentDayTimestamp(profile.streakHourOffset) + dayInMilis;
|
||||
let target = Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis;
|
||||
if (target < Date.now()) {
|
||||
target += dayInMilis;
|
||||
}
|
||||
|
@ -154,9 +153,7 @@ export async function update(
|
|||
console.debug("dayInMilis", dayInMilis);
|
||||
console.debug(
|
||||
"difTarget",
|
||||
new Date(
|
||||
Misc.getCurrentDayTimestamp(profile.streakHourOffset) + dayInMilis
|
||||
)
|
||||
new Date(Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis)
|
||||
);
|
||||
console.debug("timeDif", timeDif);
|
||||
console.debug(
|
||||
|
@ -164,18 +161,12 @@ export async function update(
|
|||
Misc.getCurrentDayTimestamp(),
|
||||
new Date(Misc.getCurrentDayTimestamp())
|
||||
);
|
||||
console.debug("profile.streakHourOffset", profile.streakHourOffset);
|
||||
console.debug("profile.streakHourOffset", streakOffset);
|
||||
|
||||
if (lastResult) {
|
||||
//check if the last result is from today
|
||||
const isToday = Misc.isToday(
|
||||
lastResult.timestamp,
|
||||
profile.streakHourOffset
|
||||
);
|
||||
const isYesterday = Misc.isYesterday(
|
||||
lastResult.timestamp,
|
||||
profile.streakHourOffset
|
||||
);
|
||||
const isToday = Misc.isToday(lastResult.timestamp, streakOffset);
|
||||
const isYesterday = Misc.isYesterday(lastResult.timestamp, streakOffset);
|
||||
|
||||
console.debug(
|
||||
"lastResult.timestamp",
|
||||
|
@ -185,10 +176,8 @@ export async function update(
|
|||
console.debug("isToday", isToday);
|
||||
console.debug("isYesterday", isYesterday);
|
||||
|
||||
const offsetString = profile.streakHourOffset
|
||||
? `(${profile.streakHourOffset > 0 ? "+" : ""}${
|
||||
profile.streakHourOffset
|
||||
} offset)`
|
||||
const offsetString = streakOffset
|
||||
? `(${streakOffset > 0 ? "+" : ""}${streakOffset} offset)`
|
||||
: "";
|
||||
|
||||
if (isToday) {
|
||||
|
@ -201,7 +190,7 @@ export async function update(
|
|||
|
||||
console.debug(hoverText);
|
||||
|
||||
if (profile.streakHourOffset === undefined) {
|
||||
if (streakOffset === undefined) {
|
||||
hoverText += `\n\nIf the streak reset time doesn't line up with your timezone, you can change it in Settings > Danger zone > Update streak hour offset.`;
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +311,10 @@ export async function update(
|
|||
} else {
|
||||
profileElement.find(".leaderboardsPositions").removeClass("hidden");
|
||||
|
||||
const lbPos = where === "profile" ? profile.allTimeLbs : profile.lbMemory;
|
||||
const lbPos =
|
||||
where === "profile"
|
||||
? (profile as SharedTypes.UserProfile).allTimeLbs
|
||||
: (profile as MonkeyTypes.Snapshot).lbMemory;
|
||||
|
||||
const t15 = lbPos?.time?.["15"]?.["english"];
|
||||
const t60 = lbPos?.time?.["60"]?.["english"];
|
||||
|
|
|
@ -151,7 +151,7 @@ function reset(): void {
|
|||
|
||||
type UpdateOptions = {
|
||||
uidOrName?: string;
|
||||
data?: undefined | Profile.ProfileData;
|
||||
data?: undefined | SharedTypes.UserProfile;
|
||||
};
|
||||
|
||||
async function update(options: UpdateOptions): Promise<void> {
|
||||
|
@ -159,14 +159,18 @@ async function update(options: UpdateOptions): Promise<void> {
|
|||
if (options.data) {
|
||||
$(".page.pageProfile .preloader").addClass("hidden");
|
||||
await Profile.update("profile", options.data);
|
||||
PbTables.update(options.data.personalBests, true);
|
||||
PbTables.update(
|
||||
// this cast is fine because pb tables can handle the partial data inside user profiles
|
||||
options.data.personalBests as unknown as SharedTypes.PersonalBests,
|
||||
true
|
||||
);
|
||||
} else if (options.uidOrName !== undefined && options.uidOrName !== "") {
|
||||
const response = getParamExists
|
||||
? await Ape.users.getProfileByUid(options.uidOrName)
|
||||
: await Ape.users.getProfileByName(options.uidOrName);
|
||||
$(".page.pageProfile .preloader").addClass("hidden");
|
||||
|
||||
if (response.status === 404) {
|
||||
if (response.status === 404 || response.data === null) {
|
||||
const message = getParamExists
|
||||
? "User not found"
|
||||
: `User ${options.uidOrName} not found`;
|
||||
|
@ -181,10 +185,13 @@ async function update(options: UpdateOptions): Promise<void> {
|
|||
);
|
||||
} else {
|
||||
window.history.replaceState(null, "", `/profile/${response.data.name}`);
|
||||
await Profile.update("profile", response.data);
|
||||
// this cast is fine because pb tables can handle the partial data inside user profiles
|
||||
PbTables.update(
|
||||
response.data.personalBests as unknown as SharedTypes.PersonalBests,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
await Profile.update("profile", response.data);
|
||||
PbTables.update(response.data.personalBests, true);
|
||||
} else {
|
||||
Notifications.add("Missing update parameter!", -1);
|
||||
}
|
||||
|
@ -199,7 +206,7 @@ $(".page.pageProfile").on("click", ".profile .userReportButton", () => {
|
|||
void UserReportPopup.show({ uid, name, lbOptOut });
|
||||
});
|
||||
|
||||
export const page = new Page<undefined | Profile.ProfileData>(
|
||||
export const page = new Page<undefined | SharedTypes.UserProfile>(
|
||||
"profile",
|
||||
$(".page.pageProfile"),
|
||||
"/profile",
|
||||
|
|
|
@ -1256,7 +1256,7 @@ $(".pageSettings .section.discordIntegration .getLinkAndGoToOauth").on(
|
|||
"click",
|
||||
() => {
|
||||
void Ape.users.getOauthLink().then((res) => {
|
||||
window.open(res.data.url, "_self");
|
||||
window.open(res.data?.url as string, "_self");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -98,8 +98,8 @@ async function apply(): Promise<void> {
|
|||
const tags = DB.getSnapshot()?.tags ?? [];
|
||||
|
||||
const activeTagIds: string[] = tags
|
||||
.filter((tag: MonkeyTypes.Tag) => tag.active)
|
||||
.map((tag: MonkeyTypes.Tag) => tag._id);
|
||||
.filter((tag: MonkeyTypes.UserTag) => tag.active)
|
||||
.map((tag: MonkeyTypes.UserTag) => tag._id);
|
||||
configChanges.tags = activeTagIds;
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ function hydrateInputs(): void {
|
|||
websiteInput.val(socialProfiles?.website ?? "");
|
||||
badgeIdsSelect.html("");
|
||||
|
||||
badges?.forEach((badge: MonkeyTypes.Badge) => {
|
||||
badges?.forEach((badge: SharedTypes.Badge) => {
|
||||
if (badge.selected) {
|
||||
currentSelectedBadgeId = badge.id;
|
||||
}
|
||||
|
@ -108,14 +108,14 @@ function hydrateInputs(): void {
|
|||
});
|
||||
}
|
||||
|
||||
function buildUpdatesFromInputs(): MonkeyTypes.UserDetails {
|
||||
function buildUpdatesFromInputs(): SharedTypes.UserProfileDetails {
|
||||
const bio = (bioInput.val() ?? "") as string;
|
||||
const keyboard = (keyboardInput.val() ?? "") as string;
|
||||
const twitter = (twitterInput.val() ?? "") as string;
|
||||
const github = (githubInput.val() ?? "") as string;
|
||||
const website = (websiteInput.val() ?? "") as string;
|
||||
|
||||
const profileUpdates: MonkeyTypes.UserDetails = {
|
||||
const profileUpdates: SharedTypes.UserProfileDetails = {
|
||||
bio,
|
||||
keyboard,
|
||||
socialProfiles: {
|
||||
|
|
|
@ -100,6 +100,12 @@ async function apply(): Promise<void> {
|
|||
-1
|
||||
);
|
||||
} else {
|
||||
if (response.data === null) {
|
||||
Notifications.add("Tag was added but data returned was null", -1);
|
||||
Loader.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
Notifications.add("Tag added", 1);
|
||||
DB.getSnapshot()?.tags?.push({
|
||||
display: propTagName,
|
||||
|
|
|
@ -230,7 +230,10 @@ export async function show(clearText = true): Promise<void> {
|
|||
$("#quoteSearchPopup #toggleShowFavorites").removeClass("hidden");
|
||||
}
|
||||
|
||||
if (DB.getSnapshot()?.quoteMod) {
|
||||
const isQuoteMod =
|
||||
DB.getSnapshot()?.quoteMod === true || DB.getSnapshot()?.quoteMod !== "";
|
||||
|
||||
if (isQuoteMod) {
|
||||
$("#quoteSearchPopup #goToApproveQuotes").removeClass("hidden");
|
||||
} else {
|
||||
$("#quoteSearchPopup #goToApproveQuotes").addClass("hidden");
|
||||
|
@ -431,10 +434,10 @@ $("#popups").on(
|
|||
|
||||
if (response.status === 200) {
|
||||
$button.removeClass("fas").addClass("far");
|
||||
const quoteIndex = dbSnapshot.favoriteQuotes[quoteLang]?.indexOf(
|
||||
const quoteIndex = dbSnapshot.favoriteQuotes?.[quoteLang]?.indexOf(
|
||||
quoteId
|
||||
) as number;
|
||||
dbSnapshot.favoriteQuotes[quoteLang]?.splice(quoteIndex, 1);
|
||||
dbSnapshot.favoriteQuotes?.[quoteLang]?.splice(quoteIndex, 1);
|
||||
}
|
||||
} else {
|
||||
// Add to favorites
|
||||
|
@ -446,6 +449,9 @@ $("#popups").on(
|
|||
|
||||
if (response.status === 200) {
|
||||
$button.removeClass("far").addClass("fas");
|
||||
if (dbSnapshot.favoriteQuotes === undefined) {
|
||||
dbSnapshot.favoriteQuotes = {};
|
||||
}
|
||||
if (!dbSnapshot.favoriteQuotes[quoteLang]) {
|
||||
dbSnapshot.favoriteQuotes[quoteLang] = [];
|
||||
}
|
||||
|
|
|
@ -1049,35 +1049,28 @@ list.clearTagPb = new SimplePopup(
|
|||
};
|
||||
}
|
||||
|
||||
if (response.data.resultCode === 1) {
|
||||
const tag = DB.getSnapshot()?.tags?.filter((t) => t._id === tagId)[0];
|
||||
const tag = DB.getSnapshot()?.tags?.filter((t) => t._id === tagId)[0];
|
||||
|
||||
if (tag === undefined) {
|
||||
return {
|
||||
status: -1,
|
||||
message: "Tag not found",
|
||||
};
|
||||
}
|
||||
tag.personalBests = {
|
||||
time: {},
|
||||
words: {},
|
||||
quote: {},
|
||||
zen: {},
|
||||
custom: {},
|
||||
};
|
||||
$(
|
||||
`.pageSettings .section.tags .tagsList .tag[id="${tagId}"] .clearPbButton`
|
||||
).attr("aria-label", "No PB found");
|
||||
return {
|
||||
status: 1,
|
||||
message: "Tag PB cleared",
|
||||
};
|
||||
} else {
|
||||
if (tag === undefined) {
|
||||
return {
|
||||
status: -1,
|
||||
message: "Failed to clear tag PB: " + response.data.message,
|
||||
message: "Tag not found",
|
||||
};
|
||||
}
|
||||
tag.personalBests = {
|
||||
time: {},
|
||||
words: {},
|
||||
quote: {},
|
||||
zen: {},
|
||||
custom: {},
|
||||
};
|
||||
$(
|
||||
`.pageSettings .section.tags .tagsList .tag[id="${tagId}"] .clearPbButton`
|
||||
).attr("aria-label", "No PB found");
|
||||
return {
|
||||
status: 1,
|
||||
message: "Tag PB cleared",
|
||||
};
|
||||
},
|
||||
(thisPopup) => {
|
||||
thisPopup.text = `Are you sure you want to clear PB for tag ${thisPopup.parameters[1]}?`;
|
||||
|
@ -1539,7 +1532,7 @@ list.updateCustomTheme = new SimplePopup(
|
|||
};
|
||||
}
|
||||
|
||||
const customTheme = snapshot.customThemes.find(
|
||||
const customTheme = snapshot.customThemes?.find(
|
||||
(t) => t._id === _thisPopup.parameters[0]
|
||||
);
|
||||
if (customTheme === undefined) {
|
||||
|
@ -1585,7 +1578,7 @@ list.updateCustomTheme = new SimplePopup(
|
|||
const snapshot = DB.getSnapshot();
|
||||
if (!snapshot) return;
|
||||
|
||||
const customTheme = snapshot.customThemes.find(
|
||||
const customTheme = snapshot.customThemes?.find(
|
||||
(t) => t._id === _thisPopup.parameters[0]
|
||||
);
|
||||
if (!customTheme) return;
|
||||
|
|
|
@ -341,7 +341,7 @@ $(".pageSettings").on("click", " .section.themes .customTheme.button", (e) => {
|
|||
if ($(e.target).hasClass("delButton")) return;
|
||||
if ($(e.target).hasClass("editButton")) return;
|
||||
const customThemeId = $(e.currentTarget).attr("customThemeId") ?? "";
|
||||
const theme = DB.getSnapshot()?.customThemes.find(
|
||||
const theme = DB.getSnapshot()?.customThemes?.find(
|
||||
(e) => e._id === customThemeId
|
||||
);
|
||||
|
||||
|
|
|
@ -417,7 +417,7 @@ export async function updateCrown(): Promise<void> {
|
|||
}
|
||||
|
||||
async function updateTags(dontSave: boolean): Promise<void> {
|
||||
const activeTags: MonkeyTypes.Tag[] = [];
|
||||
const activeTags: MonkeyTypes.UserTag[] = [];
|
||||
const userTagsCount = DB.getSnapshot()?.tags?.length ?? 0;
|
||||
try {
|
||||
DB.getSnapshot()?.tags?.forEach((tag) => {
|
||||
|
@ -885,10 +885,10 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => {
|
|||
|
||||
if (response.status === 200) {
|
||||
$button.removeClass("fas").addClass("far");
|
||||
const quoteIndex = dbSnapshot.favoriteQuotes[quoteLang]?.indexOf(
|
||||
const quoteIndex = dbSnapshot.favoriteQuotes?.[quoteLang]?.indexOf(
|
||||
quoteId
|
||||
) as number;
|
||||
dbSnapshot.favoriteQuotes[quoteLang]?.splice(quoteIndex, 1);
|
||||
dbSnapshot.favoriteQuotes?.[quoteLang]?.splice(quoteIndex, 1);
|
||||
}
|
||||
} else {
|
||||
// Add to favorites
|
||||
|
@ -900,6 +900,9 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => {
|
|||
|
||||
if (response.status === 200) {
|
||||
$button.removeClass("far").addClass("fas");
|
||||
if (dbSnapshot.favoriteQuotes === undefined) {
|
||||
dbSnapshot.favoriteQuotes = {};
|
||||
}
|
||||
if (!dbSnapshot.favoriteQuotes[quoteLang]) {
|
||||
dbSnapshot.favoriteQuotes[quoteLang] = [];
|
||||
}
|
||||
|
|
95
frontend/src/ts/types/types.d.ts
vendored
95
frontend/src/ts/types/types.d.ts
vendored
|
@ -170,14 +170,6 @@ declare namespace MonkeyTypes {
|
|||
display: string;
|
||||
};
|
||||
|
||||
type Tag = {
|
||||
_id: string;
|
||||
name: string;
|
||||
display: string;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
type RawCustomTheme = {
|
||||
name: string;
|
||||
colors: string[];
|
||||
|
@ -187,12 +179,6 @@ declare namespace MonkeyTypes {
|
|||
_id: string;
|
||||
} & RawCustomTheme;
|
||||
|
||||
type TypingStats = {
|
||||
timeTyping: number;
|
||||
startedTests: number;
|
||||
completedTests: number;
|
||||
};
|
||||
|
||||
type ConfigChanges = {
|
||||
tags?: string[];
|
||||
} & Partial<SharedTypes.Config>;
|
||||
|
@ -211,60 +197,41 @@ declare namespace MonkeyTypes {
|
|||
|
||||
type QuoteRatings = Record<string, Record<number, number>>;
|
||||
|
||||
type Snapshot = {
|
||||
banned?: boolean;
|
||||
emailVerified?: boolean;
|
||||
quoteRatings?: QuoteRatings;
|
||||
results?: SharedTypes.Result<SharedTypes.Config.Mode>[];
|
||||
verified?: boolean;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
name: string;
|
||||
customThemes: CustomTheme[];
|
||||
presets?: SnapshotPreset[];
|
||||
tags: Tag[];
|
||||
favouriteThemes?: string[];
|
||||
lbMemory?: LeaderboardMemory;
|
||||
typingStats?: TypingStats;
|
||||
quoteMod?: boolean;
|
||||
discordId?: string;
|
||||
config?: SharedTypes.Config;
|
||||
favoriteQuotes: FavoriteQuotes;
|
||||
needsToChangeName?: boolean;
|
||||
discordAvatar?: string;
|
||||
details?: UserDetails;
|
||||
inventory?: UserInventory;
|
||||
addedAt: number;
|
||||
filterPresets: SharedTypes.ResultFilters[];
|
||||
xp: number;
|
||||
type UserTag = SharedTypes.UserTag & {
|
||||
active?: boolean;
|
||||
display: string;
|
||||
};
|
||||
|
||||
type Snapshot = Omit<
|
||||
SharedTypes.User,
|
||||
| "timeTyping"
|
||||
| "startedTests"
|
||||
| "completedTests"
|
||||
| "profileDetails"
|
||||
| "streak"
|
||||
| "resultFilterPresets"
|
||||
| "tags"
|
||||
| "xp"
|
||||
> & {
|
||||
typingStats: {
|
||||
timeTyping: number;
|
||||
startedTests: number;
|
||||
completedTests: number;
|
||||
};
|
||||
details?: SharedTypes.UserProfileDetails;
|
||||
inboxUnreadSize: number;
|
||||
streak: number;
|
||||
maxStreak: number;
|
||||
filterPresets: SharedTypes.ResultFilters[];
|
||||
isPremium: boolean;
|
||||
streakHourOffset?: number;
|
||||
lbOptOut?: boolean;
|
||||
isPremium?: boolean;
|
||||
config: SharedTypes.Config;
|
||||
tags: UserTag[];
|
||||
presets: SnapshotPreset[];
|
||||
results?: SharedTypes.Result<SharedTypes.Config.Mode>[];
|
||||
xp: number;
|
||||
};
|
||||
|
||||
type UserDetails = {
|
||||
bio?: string;
|
||||
keyboard?: string;
|
||||
socialProfiles: {
|
||||
twitter?: string;
|
||||
github?: string;
|
||||
website?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type UserInventory = {
|
||||
badges: Badge[];
|
||||
};
|
||||
|
||||
type Badge = {
|
||||
id: number;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
type FavoriteQuotes = Record<string, string[]>;
|
||||
|
||||
type Group<
|
||||
G extends keyof SharedTypes.ResultFilters = keyof SharedTypes.ResultFilters
|
||||
> = G extends G ? SharedTypes.ResultFilters[G] : never;
|
||||
|
@ -466,8 +433,8 @@ declare namespace MonkeyTypes {
|
|||
|
||||
type BadgeReward = {
|
||||
type: "badge";
|
||||
item: Badge;
|
||||
} & Reward<Badge>;
|
||||
item: SharedTypes.Badge;
|
||||
} & Reward<SharedTypes.Badge>;
|
||||
|
||||
type AllRewards = XpReward | BadgeReward;
|
||||
|
||||
|
|
|
@ -32,6 +32,13 @@ export async function linkDiscord(hashOverride: string): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
if (response.data === null) {
|
||||
return Notifications.add(
|
||||
"Failed to link Discord: data returned was null",
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
||||
Notifications.add(response.message, 1);
|
||||
|
||||
const snapshot = DB.getSnapshot();
|
||||
|
|
|
@ -35,7 +35,13 @@ const BASE_CONFIG = {
|
|||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, loader: "ts-loader" },
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.s[ac]ss$/i,
|
||||
use: [
|
||||
|
|
138
shared-types/types.d.ts
vendored
138
shared-types/types.d.ts
vendored
|
@ -459,4 +459,142 @@ declare namespace SharedTypes {
|
|||
xpBreakdown: Record<string, number>;
|
||||
streak: number;
|
||||
};
|
||||
|
||||
type UserStreak = {
|
||||
lastResultTimestamp: number;
|
||||
length: number;
|
||||
maxLength: number;
|
||||
hourOffset?: number;
|
||||
};
|
||||
|
||||
type UserTag = {
|
||||
_id: string;
|
||||
name: string;
|
||||
personalBests: PersonalBests;
|
||||
};
|
||||
|
||||
type UserProfileDetails = {
|
||||
bio?: string;
|
||||
keyboard?: string;
|
||||
socialProfiles: {
|
||||
twitter?: string;
|
||||
github?: string;
|
||||
website?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CustomTheme = {
|
||||
_id: string;
|
||||
name: string;
|
||||
colors: string[];
|
||||
};
|
||||
|
||||
type PremiumInfo = {
|
||||
startTimestamp: number;
|
||||
expirationTimestamp: number;
|
||||
};
|
||||
|
||||
type UserQuoteRatings = Record<string, Record<string, number>>;
|
||||
|
||||
type UserLbMemory = Record<string, Record<string, Record<string, number>>>;
|
||||
|
||||
type UserInventory = {
|
||||
badges: Badge[];
|
||||
};
|
||||
|
||||
type Badge = {
|
||||
id: number;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
type User = {
|
||||
name: string;
|
||||
email: string;
|
||||
uid: string;
|
||||
addedAt: number;
|
||||
personalBests: PersonalBests;
|
||||
lastReultHashes?: string[]; //todo: fix typo (its in the db too)
|
||||
completedTests?: number;
|
||||
startedTests?: number;
|
||||
timeTyping?: number;
|
||||
streak?: UserStreak;
|
||||
xp?: number;
|
||||
discordId?: string;
|
||||
discordAvatar?: string;
|
||||
tags?: UserTag[];
|
||||
profileDetails?: UserProfileDetails;
|
||||
customThemes?: CustomTheme[];
|
||||
premium?: PremiumInfo;
|
||||
quoteRatings?: UserQuoteRatings;
|
||||
favoriteQuotes?: Record<string, string[]>;
|
||||
lbMemory?: UserLbMemory;
|
||||
inventory?: UserInventory;
|
||||
banned?: boolean;
|
||||
lbOptOut?: boolean;
|
||||
verified?: boolean;
|
||||
needsToChangeName?: boolean;
|
||||
quoteMod?: boolean | string;
|
||||
resultFilterPresets?: ResultFilters[];
|
||||
};
|
||||
|
||||
type Reward<T> = {
|
||||
type: string;
|
||||
item: T;
|
||||
};
|
||||
|
||||
type XpReward = {
|
||||
type: "xp";
|
||||
item: number;
|
||||
} & Reward<number>;
|
||||
|
||||
type BadgeReward = {
|
||||
type: "badge";
|
||||
item: SharedTypes.Badge;
|
||||
} & Reward<SharedTypes.Badge>;
|
||||
|
||||
type AllRewards = XpReward | BadgeReward;
|
||||
|
||||
type MonkeyMail = {
|
||||
id: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
timestamp: number;
|
||||
read: boolean;
|
||||
rewards: AllRewards[];
|
||||
};
|
||||
|
||||
type UserProfile = Pick<
|
||||
User,
|
||||
| "name"
|
||||
| "banned"
|
||||
| "addedAt"
|
||||
| "discordId"
|
||||
| "discordAvatar"
|
||||
| "xp"
|
||||
| "lbOptOut"
|
||||
| "inventory"
|
||||
| "uid"
|
||||
> & {
|
||||
typingStats: {
|
||||
completedTests: User["completedTests"];
|
||||
startedTests: User["startedTests"];
|
||||
timeTyping: User["timeTyping"];
|
||||
};
|
||||
streak: UserStreak["length"];
|
||||
maxStreak: UserStreak["maxLength"];
|
||||
details: UserProfileDetails;
|
||||
allTimeLbs: {
|
||||
time: Record<string, Record<string, number | null>>;
|
||||
};
|
||||
personalBests: {
|
||||
time: Pick<
|
||||
Record<`${number}`, SharedTypes.PersonalBest[]>,
|
||||
"15" | "30" | "60" | "120"
|
||||
>;
|
||||
words: Pick<
|
||||
Record<`${number}`, SharedTypes.PersonalBest[]>,
|
||||
"10" | "25" | "50" | "100"
|
||||
>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue