diff --git a/frontend/src/ts/constants/default-snapshot.ts b/frontend/src/ts/constants/default-snapshot.ts index 96b54209f405..0b634ab4f464 100644 --- a/frontend/src/ts/constants/default-snapshot.ts +++ b/frontend/src/ts/constants/default-snapshot.ts @@ -1,5 +1,6 @@ import { ResultFilters, + TestActivity, User, UserProfileDetails, UserTag, @@ -8,10 +9,6 @@ import { getDefaultConfig } from "./default-config"; import { Mode } from "@monkeytype/schemas/shared"; import { Result } from "@monkeytype/schemas/results"; import { Config, Difficulty, FunboxName } from "@monkeytype/schemas/configs"; -import { - ModifiableTestActivityCalendar, - TestActivityCalendar, -} from "../elements/test-activity-calendar"; import { Preset } from "@monkeytype/schemas/presets"; import { Language } from "@monkeytype/schemas/languages"; import { ConnectionStatus } from "@monkeytype/schemas/connections"; @@ -83,8 +80,7 @@ export type Snapshot = Omit< presets: SnapshotPreset[]; results?: SnapshotResult[]; xp: number; - testActivity?: ModifiableTestActivityCalendar; - testActivityByYear?: { [key: string]: TestActivityCalendar }; + testActivityData?: TestActivity; connections: Record; }; diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index f71418abdb90..a83e29b8ed74 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -4,11 +4,7 @@ import { isAuthenticated, getAuthenticatedUser } from "./firebase"; import * as ConnectionState from "./states/connection"; import { lastElementFromArray } from "./utils/arrays"; import { migrateConfig } from "./utils/config"; -import * as Dates from "date-fns"; -import { - TestActivityCalendar, - ModifiableTestActivityCalendar, -} from "./elements/test-activity-calendar"; +import { ModifiableTestActivityCalendar } from "./elements/test-activity-calendar"; import * as Loader from "./elements/loader"; import { Badge, CustomTheme } from "@monkeytype/schemas/users"; @@ -36,8 +32,15 @@ import { get as getServerConfiguration, } from "./ape/server-configuration"; import { Connection } from "@monkeytype/schemas/connections"; +import { createStore, unwrap } from "solid-js/store"; +import { GetTestActivityResponse } from "@monkeytype/contracts/users"; + +const [snapshot, setSnapshotStore] = createStore< + // oxlint-disable-next-line typescript/no-unnecessary-type-arguments + Snapshot | Record +>({}); -let dbSnapshot: Snapshot | undefined; +// let dbSnapshot: Snapshot | undefined; const firstDayOfTheWeek = getFirstDayOfTheWeek(); export class SnapshotInitError extends Error { @@ -54,16 +57,32 @@ export class SnapshotInitError extends Error { } export function getSnapshot(): Snapshot | undefined { - return dbSnapshot; + if (Object.keys(snapshot).length === 0) { + return undefined; + } + return unwrap(snapshot) as Snapshot; } export function setSnapshot( newSnapshot: Snapshot | undefined, options?: { dispatchEvent?: boolean }, ): void { - const originalBanned = dbSnapshot?.banned; - const originalVerified = dbSnapshot?.verified; - const lbOptOut = dbSnapshot?.lbOptOut; + if (newSnapshot === undefined) { + setSnapshotStore({}); + + if (options?.dispatchEvent !== false) { + AuthEvent.dispatch({ + type: "snapshotUpdated", + data: { isInitial: false }, + }); + } + return; + } + + const currentSnapshot = getSnapshot(); + const originalBanned = currentSnapshot?.banned; + const originalVerified = currentSnapshot?.verified; + const lbOptOut = currentSnapshot?.lbOptOut; //not allowing user to override these values i guess? try { @@ -75,12 +94,11 @@ export function setSnapshot( try { delete newSnapshot?.lbOptOut; } catch {} - dbSnapshot = newSnapshot; - if (dbSnapshot) { - dbSnapshot.banned = originalBanned; - dbSnapshot.verified = originalVerified; - dbSnapshot.lbOptOut = lbOptOut; - } + newSnapshot.banned = originalBanned; + newSnapshot.verified = originalVerified; + newSnapshot.lbOptOut = lbOptOut; + + setSnapshotStore(newSnapshot); if (options?.dispatchEvent !== false) { AuthEvent.dispatch({ type: "snapshotUpdated", data: { isInitial: false } }); @@ -190,11 +208,7 @@ export async function initSnapshot(): Promise { snap.allTimeLbs = userData.allTimeLbs; if (userData.testActivity !== undefined) { - snap.testActivity = new ModifiableTestActivityCalendar( - userData.testActivity.testsByDays, - new Date(userData.testActivity.lastDay), - firstDayOfTheWeek, - ); + snap.testActivityData = userData.testActivity; } const hourOffset = userData?.streak?.hourOffset; @@ -273,10 +287,10 @@ export async function initSnapshot(): Promise { snap.connections = convertConnections(connectionsData); - dbSnapshot = snap; - return dbSnapshot; + setSnapshot(snap, { dispatchEvent: false }); + return snap; } catch (e) { - dbSnapshot = getDefaultSnapshot(); + setSnapshot(getDefaultSnapshot(), { dispatchEvent: false }); throw e; } } @@ -284,6 +298,8 @@ export async function initSnapshot(): Promise { export async function getUserResults(offset?: number): Promise { if (!isAuthenticated()) return false; + const dbSnapshot = getSnapshot(); + if (!dbSnapshot) return false; if ( dbSnapshot.results !== undefined && @@ -336,16 +352,14 @@ export async function getUserResults(offset?: number): Promise { } else { dbSnapshot.results = results; } + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } -function _getCustomThemeById(themeID: string): CustomTheme | undefined { - return dbSnapshot?.customThemes?.find((t) => t._id === themeID); -} - export async function addCustomTheme( theme: Omit, ): Promise { + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return false; dbSnapshot.customThemes ??= []; @@ -372,6 +386,8 @@ export async function addCustomTheme( }; dbSnapshot.customThemes.push(newCustomTheme); + + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } @@ -380,6 +396,7 @@ export async function editCustomTheme( newTheme: Omit, ): Promise { if (!isAuthenticated()) return false; + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return false; dbSnapshot.customThemes ??= []; @@ -408,12 +425,13 @@ export async function editCustomTheme( dbSnapshot.customThemes[dbSnapshot.customThemes.indexOf(customTheme)] = newCustomTheme; - + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } export async function deleteCustomTheme(themeId: string): Promise { if (!isAuthenticated()) return false; + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return false; const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId); @@ -428,7 +446,7 @@ export async function deleteCustomTheme(themeId: string): Promise { dbSnapshot.customThemes = dbSnapshot.customThemes?.filter( (t) => t._id !== themeId, ); - + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } @@ -512,7 +530,7 @@ export async function getUserAverage10( } const retval: [number, number] = - snapshot === null || (await getUserResults()) === null ? [0, 0] : cont(); + (await getUserResults()) === null ? [0, 0] : cont(); return retval; } @@ -623,6 +641,8 @@ export async function getLocalPB( lazyMode: boolean, funboxes: FunboxMetadata[], ): Promise { + const dbSnapshot = getSnapshot(); + if (!funboxes.every((f) => f.canGetPb)) { return undefined; } @@ -655,29 +675,27 @@ function saveLocalPB( consistency: number, ): void { if (mode === "quote") return; + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return; - function cont(): void { - if (!dbSnapshot) return; - let found = false; + let found = false; - dbSnapshot.personalBests ??= { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; + dbSnapshot.personalBests ??= { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; - dbSnapshot.personalBests[mode] ??= { - [mode2]: [], - }; + dbSnapshot.personalBests[mode] ??= { + [mode2]: [], + }; - dbSnapshot.personalBests[mode][mode2] ??= - [] as unknown as PersonalBests[M][Mode2]; + dbSnapshot.personalBests[mode][mode2] ??= + [] as unknown as PersonalBests[M][Mode2]; - ( - dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[] - ).forEach((pb) => { + (dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[]).forEach( + (pb) => { if ( (pb.punctuation ?? false) === punctuation && (pb.numbers ?? false) === numbers && @@ -693,29 +711,25 @@ function saveLocalPB( pb.consistency = consistency; pb.lazyMode = lazyMode; } + }, + ); + if (!found) { + //nothing found + (dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[]).push({ + language, + difficulty, + lazyMode, + punctuation, + numbers, + wpm, + acc, + raw, + timestamp: Date.now(), + consistency, }); - if (!found) { - //nothing found - (dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[]).push( - { - language, - difficulty, - lazyMode, - punctuation, - numbers, - wpm, - acc, - raw, - timestamp: Date.now(), - consistency, - }, - ); - } } - if (dbSnapshot !== null) { - cont(); - } + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export async function getLocalTagPB( @@ -728,11 +742,12 @@ export async function getLocalTagPB( difficulty: Difficulty, lazyMode: boolean, ): Promise { - if (dbSnapshot === null) return 0; + const dbSnapshot = getSnapshot(); + if (dbSnapshot === undefined) return 0; let ret = 0; - const filteredtag = (getSnapshot()?.tags ?? []).find((t) => t._id === tagId); + const filteredtag = (dbSnapshot.tags ?? []).find((t) => t._id === tagId); if (filteredtag === undefined) return ret; @@ -780,108 +795,108 @@ export async function saveLocalTagPB( acc: number, raw: number, consistency: number, -): Promise { +): Promise { + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return; if (mode === "quote") return; - function cont(): void { - const filteredtag = dbSnapshot?.tags?.find( - (t) => t._id === tagId, - ) as SnapshotUserTag; + const filteredtag = dbSnapshot?.tags?.find( + (t) => t._id === tagId, + ) as SnapshotUserTag; - filteredtag.personalBests ??= { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; + filteredtag.personalBests ??= { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; - filteredtag.personalBests[mode] ??= { - [mode2]: [], - }; + filteredtag.personalBests[mode] ??= { + [mode2]: [], + }; - filteredtag.personalBests[mode][mode2] ??= - [] as unknown as PersonalBests[M][Mode2]; + filteredtag.personalBests[mode][mode2] ??= + [] as unknown as PersonalBests[M][Mode2]; - try { - let found = false; + try { + let found = false; + ( + filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] + ).forEach((pb) => { + if ( + (pb.punctuation ?? false) === punctuation && + (pb.numbers ?? false) === numbers && + pb.difficulty === difficulty && + pb.language === language && + (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)) + ) { + found = true; + pb.wpm = wpm; + pb.acc = acc; + pb.raw = raw; + pb.timestamp = Date.now(); + pb.consistency = consistency; + pb.lazyMode = lazyMode; + } + }); + if (!found) { + //nothing found ( filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] - ).forEach((pb) => { - if ( - (pb.punctuation ?? false) === punctuation && - (pb.numbers ?? false) === numbers && - pb.difficulty === difficulty && - pb.language === language && - (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)) - ) { - found = true; - pb.wpm = wpm; - pb.acc = acc; - pb.raw = raw; - pb.timestamp = Date.now(); - pb.consistency = consistency; - pb.lazyMode = lazyMode; - } + ).push({ + language, + difficulty, + lazyMode, + punctuation, + numbers, + wpm, + acc, + raw, + timestamp: Date.now(), + consistency, }); - if (!found) { - //nothing found - ( - filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] - ).push({ - language, - difficulty, - lazyMode, - punctuation, - numbers, - wpm, - acc, - raw, - timestamp: Date.now(), - consistency, - }); - } - } catch (e) { - //that mode or mode2 is not found - filteredtag.personalBests = { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; - filteredtag.personalBests[mode][mode2] = [ - { - language: language, - difficulty: difficulty, - lazyMode: lazyMode, - punctuation: punctuation, - numbers: numbers, - wpm: wpm, - acc: acc, - raw: raw, - timestamp: Date.now(), - consistency: consistency, - }, - ] as unknown as PersonalBests[M][Mode2]; } + } catch (e) { + //that mode or mode2 is not found + filteredtag.personalBests = { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; + filteredtag.personalBests[mode][mode2] = [ + { + language: language, + difficulty: difficulty, + lazyMode: lazyMode, + punctuation: punctuation, + numbers: numbers, + wpm: wpm, + acc: acc, + raw: raw, + timestamp: Date.now(), + consistency: consistency, + }, + ] as unknown as PersonalBests[M][Mode2]; } - if (dbSnapshot !== null) { - cont(); - } - - return; + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export function deleteLocalTag(tagId: string): void { - getSnapshot()?.results?.forEach((result) => { + const dbSnapshot = getSnapshot(); + if (dbSnapshot === undefined) return; + + for (const result of dbSnapshot.results ?? []) { const tagIndex = result.tags.indexOf(tagId); if (tagIndex > -1) { result.tags.splice(tagIndex, 1); } - }); + } + + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export async function updateLocalTagPB( @@ -894,9 +909,10 @@ export async function updateLocalTagPB( difficulty: Difficulty, lazyMode: boolean, ): Promise { - if (dbSnapshot === null) return; + const dbSnapshot = getSnapshot(); + if (dbSnapshot === undefined) return; - const filteredtag = (getSnapshot()?.tags ?? []).find((t) => t._id === tagId); + const filteredtag = (dbSnapshot?.tags ?? []).find((t) => t._id === tagId); if (filteredtag === undefined) return; @@ -907,7 +923,7 @@ export async function updateLocalTagPB( consistency: 0, }; - getSnapshot()?.results?.forEach((result) => { + dbSnapshot.results?.forEach((result) => { if (result.tags.includes(tagId) && result.wpm > pb.wpm) { if ( result.mode === mode && @@ -940,6 +956,8 @@ export async function updateLocalTagPB( pb.rawWpm, pb.consistency, ); + + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export async function updateLbMemory( @@ -973,7 +991,7 @@ export async function updateLbMemory( body: { mode, mode2, language, rank }, }); } - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } } @@ -1010,8 +1028,14 @@ export function saveLocalResult(data: SaveLocalResultData): void { if (snapshot?.results !== undefined) { snapshot.results.unshift(data.result); } - if (snapshot.testActivity !== undefined) { - snapshot.testActivity.increment(new Date(data.result.timestamp)); + if (snapshot.testActivityData !== undefined) { + const calendar = new ModifiableTestActivityCalendar( + snapshot.testActivityData.testsByDays, + new Date(snapshot.testActivityData.lastDay), + firstDayOfTheWeek, + ); + calendar.increment(new Date(data.result.timestamp)); + snapshot.testActivityData = calendar.getRawData(); } snapshot.typingStats ??= { timeTyping: 0, @@ -1079,7 +1103,7 @@ export function updateInboxUnreadSize(newSize: number): void { if (!snapshot) return; snapshot.inboxUnreadSize = newSize; - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } export function addBadge(badge: Badge): void { @@ -1090,54 +1114,22 @@ export function addBadge(badge: Badge): void { badges: [], }; snapshot.inventory.badges.push(badge); - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } -export async function getTestActivityCalendar( - yearString: string, -): Promise { - if (!isAuthenticated() || dbSnapshot === undefined) return undefined; - - if (yearString === "current") return dbSnapshot.testActivity; - - const currentYear = new Date().getFullYear().toString(); - if (yearString === currentYear) { - return dbSnapshot.testActivity?.getFullYearCalendar(); - } - - if (dbSnapshot.testActivityByYear === undefined) { - if (!ConnectionState.get()) { - return undefined; - } - - Loader.show(); - const response = await Ape.users.getTestActivity(); - if (response.status !== 200) { - Notifications.add("Error getting test activities", -1, { response }); - Loader.hide(); - return undefined; - } - - dbSnapshot.testActivityByYear = {}; - for (const year in response.body.data) { - if (year === currentYear) continue; - const testsByDays = response.body.data[year] ?? []; - const lastDay = Dates.addDays( - new Date(parseInt(year), 0, 1), - testsByDays.length, - ); - - dbSnapshot.testActivityByYear[year] = new TestActivityCalendar( - testsByDays, - lastDay, - firstDayOfTheWeek, - true, - ); - } +export async function getTestActivity(): Promise< + GetTestActivityResponse["data"] | undefined +> { + Loader.show(); + const response = await Ape.users.getTestActivity(); + if (response.status !== 200) { + Notifications.add("Error getting test activity", -1, { response }); Loader.hide(); + return undefined; } + Loader.hide(); - return dbSnapshot.testActivityByYear[yearString]; + return response.body.data ?? undefined; } export function mergeConnections(connections: Connection[]): void { @@ -1150,7 +1142,7 @@ export function mergeConnections(connections: Connection[]): void { snapshot.connections[key] = value; } - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } function convertConnections( @@ -1181,36 +1173,3 @@ export function isFriend(uid: string | undefined): boolean { ([receiverUid, status]) => receiverUid === uid && status === "accepted", ); } - -// export async function DB.getLocalTagPB(tagId) { -// function cont() { -// let ret = 0; -// try { -// ret = dbSnapshot.tags.filter((t) => t.id === tagId)[0].pb; -// if (ret === undefined) { -// ret = 0; -// } -// return ret; -// } catch (e) { -// return ret; -// } -// } - -// const retval = dbSnapshot !== null ? cont() : undefined; - -// return retval; -// } - -// export async functio(tagId, wpm) { -// function cont() { -// dbSnapshot.tags.forEach((tag) => { -// if (tag._id === tagId) { -// tag.pb = wpm; -// } -// }); -// } - -// if (dbSnapshot !== null) { -// cont(); -// } -// } diff --git a/frontend/src/ts/elements/test-activity-calendar.ts b/frontend/src/ts/elements/test-activity-calendar.ts index 01b72b158c92..a75a52083589 100644 --- a/frontend/src/ts/elements/test-activity-calendar.ts +++ b/frontend/src/ts/elements/test-activity-calendar.ts @@ -1,4 +1,5 @@ import { UTCDateMini } from "@date-fns/utc/date/mini"; +import { TestActivity } from "@monkeytype/schemas/users"; import { safeNumber } from "@monkeytype/util/numbers"; import { format, @@ -36,6 +37,7 @@ export class TestActivityCalendar implements TestActivityCalendar { protected endDay: Date; protected isFullYear: boolean; public firstDayOfWeek: Day; + protected lastDay: Date; constructor( data: (number | null | undefined)[], @@ -44,6 +46,7 @@ export class TestActivityCalendar implements TestActivityCalendar { fullYear = false, ) { this.firstDayOfWeek = firstDayOfWeek; + this.lastDay = new UTCDateMini(lastDay); const local = new UTCDateMini(lastDay); const interval = this.getInterval(local, fullYear); @@ -203,14 +206,33 @@ export class TestActivityCalendar implements TestActivityCalendar { private nextLastDayOfWeek(date: Date): Date { return nextDay(date, ((this.firstDayOfWeek + 6) % 7) as Day); } + + getRawData(): TestActivity { + return { + testsByDays: this.data.map((v) => (v === undefined ? null : v)), + lastDay: this.lastDay.getTime(), + }; + } + + getFullYearCalendar(): TestActivityCalendar { + const today = new Date(); + if (this.lastDay.getFullYear() !== new UTCDateMini(today).getFullYear()) { + return new TestActivityCalendar([], today, this.firstDayOfWeek, true); + } else { + return new TestActivityCalendar( + this.data, + this.lastDay, + this.firstDayOfWeek, + true, + ); + } + } } export class ModifiableTestActivityCalendar extends TestActivityCalendar implements ModifiableTestActivityCalendar { - private lastDay: Date; - constructor(data: (number | null)[], lastDay: Date, firstDayOfWeek: Day) { super(data, lastDay, firstDayOfWeek); this.lastDay = new UTCDateMini(lastDay); @@ -239,18 +261,4 @@ export class ModifiableTestActivityCalendar this.data = this.buildData(this.data, this.lastDay); } - - getFullYearCalendar(): TestActivityCalendar { - const today = new Date(); - if (this.lastDay.getFullYear() !== new UTCDateMini(today).getFullYear()) { - return new TestActivityCalendar([], today, this.firstDayOfWeek, true); - } else { - return new TestActivityCalendar( - this.data, - this.lastDay, - this.firstDayOfWeek, - true, - ); - } - } } diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 1793bf67fafb..ee3deb1f3f7d 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -1,6 +1,5 @@ import SlimSelect from "slim-select"; import { DataObjectPartial } from "slim-select/store"; -import { getTestActivityCalendar } from "../db"; import * as ServerConfiguration from "../ape/server-configuration"; import * as DB from "../db"; import { @@ -8,18 +7,48 @@ import { TestActivityMonth, } from "./test-activity-calendar"; import { safeNumber } from "@monkeytype/util/numbers"; +import { TestActivity } from "@monkeytype/schemas/users"; +import { getFirstDayOfTheWeek } from "../utils/date-and-time"; +import { addDays } from "date-fns/addDays"; +import * as AuthEvent from "../observables/auth-event"; let yearSelector: SlimSelect | undefined = undefined; +let calendar: TestActivityCalendar | undefined = undefined; +let activityByYear: Map | undefined = undefined; + +async function initActivityByYear(): Promise { + if (activityByYear !== undefined) return; + activityByYear = new Map(); + + const data = await DB.getTestActivity(); + if (data === undefined) return; + + for (const year in data) { + const testsByDays = data[year] as (number | null)[]; + const lastDay = addDays(new Date(parseInt(year), 0, 1), testsByDays.length); + activityByYear.set( + year, + new TestActivityCalendar(testsByDays, lastDay, getFirstDayOfTheWeek()), + ); + } +} export function init( element: HTMLElement, - calendar?: TestActivityCalendar, + testActivityData?: TestActivity, userSignUpDate?: Date, ): void { - if (calendar === undefined) { + if (testActivityData === undefined) { clear(element); return; } + + calendar = new TestActivityCalendar( + testActivityData.testsByDays, + new Date(testActivityData.lastDay), + getFirstDayOfTheWeek(), + ); + element.classList.remove("hidden"); if (element.querySelector(".yearSelect") !== null) { @@ -39,7 +68,11 @@ export function clear(element?: HTMLElement): void { element?.querySelector(".activity")?.replaceChildren(); } -function update(element: HTMLElement, calendar?: TestActivityCalendar): void { +function update( + element: HTMLElement, + calendar?: TestActivityCalendar, + fullYear = false, +): void { const container = element.querySelector(".activity"); if (container === null) { @@ -55,7 +88,14 @@ function update(element: HTMLElement, calendar?: TestActivityCalendar): void { return; } - updateMonths(calendar.getMonths()); + let calendarToShow: TestActivityCalendar; + if (fullYear) { + calendarToShow = calendar.getFullYearCalendar(); + } else { + calendarToShow = calendar; + } + + updateMonths(calendarToShow.getMonths()); element.querySelector(".nodata")?.classList.add("hidden"); const title = element.querySelector(".title"); @@ -65,7 +105,7 @@ function update(element: HTMLElement, calendar?: TestActivityCalendar): void { } } - for (const day of calendar.getDays()) { + for (const day of calendarToShow.getDays()) { const elem = document.createElement("div"); elem.setAttribute("data-level", day.level); if (day.label !== undefined) { @@ -137,8 +177,20 @@ function getYearSelector(element: HTMLElement): SlimSelect { // oxlint-disable-next-line no-unsafe-call yearSelector?.disable(); const selected = newVal[0]?.value as string; - const activity = await getTestActivityCalendar(selected); - update(element, activity); + const currentYear = new Date().getFullYear().toString(); + + if (selected === "current") { + update(element, calendar, false); + } else if (selected === currentYear) { + update(element, calendar, true); + } else { + if (activityByYear === undefined) { + await initActivityByYear(); + } + const activity = activityByYear?.get(selected); + update(element, activity, true); + } + // oxlint-disable-next-line no-unsafe-call if ((yearSelector?.getData() ?? []).length > 1) { // oxlint-disable-next-line no-unsafe-call @@ -186,3 +238,9 @@ function updateLabels(element: HTMLElement, firstDayOfWeek: number): void { (element.querySelector(".daysFull") as HTMLElement).innerHTML = buildHtml(); (element.querySelector(".days") as HTMLElement).innerHTML = buildHtml(3); } + +AuthEvent.subscribe((data) => { + if (data.type === "snapshotUpdated" && DB.getSnapshot() === undefined) { + activityByYear?.clear(); + } +}); diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 9e6c9d9d580a..d970ebb684ed 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -71,6 +71,8 @@ function loadMoreLines(lineIndex?: number): void { } function buildResultRow(result: SnapshotResult): HTMLTableRowElement { + const snapshot = DB.getSnapshot(); + let diff = result.difficulty ?? "normal"; let icons = ` { - DB.getSnapshot()?.tags?.forEach((snaptag) => { + snapshot?.tags?.forEach((snaptag) => { if (tag === snaptag._id) { tagNames += snaptag.display + ", "; } @@ -223,7 +225,7 @@ async function fillContent(): Promise { TestActivity.init( testActivityEl as HTMLElement, - snapshot.testActivity, + snapshot.testActivityData, new Date(snapshot.addedAt), ); void ResultBatches.update(); @@ -284,7 +286,7 @@ async function fillContent(): Promise { filteredResults = []; qs(".pageAccount .history table tbody")?.empty(); - DB.getSnapshot()?.results?.forEach((result) => { + snapshot.results?.forEach((result) => { // totalSeconds += tt; //apply filters @@ -439,14 +441,14 @@ async function fillContent(): Promise { let tagHide = true; if (result.tags === undefined || result.tags.length === 0) { //no tags, show when no tag is enabled - if ((DB.getSnapshot()?.tags?.length ?? 0) > 0) { + if ((snapshot.tags?.length ?? 0) > 0) { if (ResultFilters.getFilter("tags", "none")) tagHide = false; } else { tagHide = false; } } else { //tags exist - const validTags = DB.getSnapshot()?.tags?.map((t) => t._id); + const validTags = snapshot.tags?.map((t) => t._id); if (validTags === undefined) return; @@ -1005,10 +1007,11 @@ async function update(): Promise { export function updateTagsForResult(resultId: string, tagIds: string[]): void { const tagNames: string[] = []; + const snapshot = DB.getSnapshot(); if (tagIds.length > 0) { for (const tag of tagIds) { - DB.getSnapshot()?.tags?.forEach((snaptag) => { + snapshot?.tags?.forEach((snaptag) => { if (tag === snaptag._id) { tagNames.push(snaptag.display); } @@ -1124,9 +1127,8 @@ qs(".pageAccount")?.onChild( //update local cache result.chartData = chartData; - const dbResult = DB.getSnapshot()?.results?.find( - (it) => it._id === result._id, - ); + const snapshot = DB.getSnapshot(); + const dbResult = snapshot?.results?.find((it) => it._id === result._id); if (dbResult !== undefined) { dbResult["chartData"] = result.chartData; } diff --git a/frontend/src/ts/pages/profile.ts b/frontend/src/ts/pages/profile.ts index abe923649a42..49e596f6b1b6 100644 --- a/frontend/src/ts/pages/profile.ts +++ b/frontend/src/ts/pages/profile.ts @@ -9,13 +9,9 @@ import * as Skeleton from "../utils/skeleton"; import { UserProfile } from "@monkeytype/schemas/users"; import { PersonalBests } from "@monkeytype/schemas/shared"; import * as TestActivity from "../elements/test-activity"; -import { TestActivityCalendar } from "../elements/test-activity-calendar"; -import { getFirstDayOfTheWeek } from "../utils/date-and-time"; import { addFriend } from "./friends"; import { onDOMReady, qs, qsr } from "../utils/dom"; -const firstDayOfTheWeek = getFirstDayOfTheWeek(); - function reset(): void { qs(".page.pageProfile .error")?.hide(); qs(".page.pageProfile .preloader")?.show(); @@ -216,12 +212,11 @@ async function update(options: UpdateOptions): Promise { ) as HTMLElement; if (profile.testActivity !== undefined) { - const calendar = new TestActivityCalendar( - profile.testActivity.testsByDays, + TestActivity.init( + testActivity, + profile.testActivity, new Date(profile.testActivity.lastDay), - firstDayOfTheWeek, ); - TestActivity.init(testActivity, calendar); const title = testActivity.querySelector(".top .title") as HTMLElement; title.innerHTML = title?.innerHTML + " in last 12 months"; } else {