diff --git a/helpers/fetchProgress.ts b/helpers/fetchProgress.ts index 2dfcd34..f7d8caa 100644 --- a/helpers/fetchProgress.ts +++ b/helpers/fetchProgress.ts @@ -11,6 +11,7 @@ import { } from './parsers'; import { generateProfileHTML, generateImageHTML } from './profileBlock'; import { generateProgressTableHTML } from './progressBlock'; +import { generateTasksTableHTML } from './tasksBlock'; import { generateRatingTableHTML } from './ratingBlock'; import { generateLevelsTableHTML } from './levelsBlock'; import { generateAwardsTableHTML } from './awardsBlock'; @@ -145,9 +146,12 @@ function generateHTML(cache: CacheData, source: Source): HTMLElement { const imageElement = generateImageHTML(cache.accountData.account); container.appendChild(imageElement); - const progressElement = generateProgressTableHTML(cache, source); + const progressElement = generateProgressTableHTML(cache); container.appendChild(progressElement); + const tasksElement = generateTasksTableHTML(cache, source); + container.appendChild(tasksElement); + const locationRatingElement = generateRatingTableHTML(cache.locationUrl, cache.accountData.location, cache.progressData.solved, cache.locationRating); container.appendChild(locationRatingElement); diff --git a/helpers/progressBlock.ts b/helpers/progressBlock.ts index 7f09078..9662285 100644 --- a/helpers/progressBlock.ts +++ b/helpers/progressBlock.ts @@ -1,53 +1,6 @@ -import { AccountData, AwardData, AwardBlockData, CacheData, PersonalTask, ProgressData, RatingData, Source } from './types'; +import { AccountData, CacheData, ProgressData, RatingData } from './types'; import { createProgressBar, createSectionHeader } from './commonBlocks'; -interface Result { - message: string; - percentage: number; -} - -function determineTop(solved: number, rating: RatingData): Result { - const thresholds = [ - { limit: 100, top: rating.top100, title: 'Top 100' }, - { limit: 50, top: rating.top50, title: 'Top 50' }, - { limit: 25, top: rating.top25, title: 'Top 25' }, - { limit: 10, top: rating.top10, title: 'Top 10' }, - { limit: 5, top: rating.top5, title: 'Top 5' }, - { limit: 1, top: rating.top1, title: 'Top 1' }, - ]; - - for (let i = 0; i < thresholds.length; i++) { - const { limit, top, title } = thresholds[i]; - if (rating.place > limit) { - const prevTop = i > 0 ? thresholds[i - 1].top : 0; // Get the previous top - const rest = top - solved + 1; - const all = top - prevTop + 1; - - const percentage = all > 0 ? Math.round(((all - rest) / all) * 100) : 0; - const message = `${rest} problems away from ${title}`; - return { message, percentage }; - } - } - - return { message: 'You are in the Top 1', percentage: 100 }; -} - -/** Function to determine AwardData with the maximum number of members. - * - * @param awardBlocks - AwardBlockData array. - * @returns AwardData with the maximum number of members or null if there are no unfinished awards. - */ -function findAwardWithMaxMembers(awardBlocks: AwardBlockData[]): AwardData | null { - const allAwards = awardBlocks.flatMap(block => block.awards).filter(award => !award.isCompleted);; - - return allAwards.reduce((maxAward, currentAward) => { - const currentMembers = parseInt(currentAward.members.replace(/\D/g, '')) || 0; - const maxMembers = parseInt(maxAward.members.replace(/\D/g, '')) || 0; - - return currentMembers > maxMembers ? currentAward : maxAward; - }, allAwards[0] || null); -} - function createTableRow(label: string | HTMLElement, status: string, progressBar?: HTMLElement): HTMLElement { const row = document.createElement('tr'); const labelCell = document.createElement('td'); @@ -140,80 +93,13 @@ function createProgressTable( return table; } -function createPersonalTasksList(personalTasks: PersonalTask[]): HTMLElement { - const tasksList = document.createElement('ul'); - tasksList.className = 'tasks-list'; - - personalTasks.forEach(personalTask => { - const li = document.createElement('li'); - li.textContent = personalTask.task; - li.appendChild(createProgressBar(personalTask.percentage)); - tasksList.appendChild(li); - }); - - return tasksList; -} - -function createTasksList( - progressData: ProgressData, - accountData: AccountData, - locationRating: RatingData, - languageRating: RatingData, - awardsData: AwardBlockData[] -): HTMLElement { - const tasksList = document.createElement('ul'); - tasksList.className = 'tasks-list'; - - // Progress to the next level - const matchToTheNext = progressData.toTheNext.match(/(\d+)/); - const countToTheNext = matchToTheNext ? (25 - parseInt(matchToTheNext[1], 10)) * 4 : 0; - const toTheNextPercentage = createProgressBar(countToTheNext); - const levelTask = document.createElement('li'); - levelTask.textContent = `${progressData.level}: ${progressData.toTheNext}. `; - levelTask.appendChild(toTheNextPercentage); - tasksList.appendChild(levelTask); - - // Location progress - const locationResult = determineTop(progressData.solved, locationRating); - const locationLi = document.createElement('li'); - locationLi.textContent = `${accountData.location}: ${locationResult.message}. `; - locationLi.appendChild(createProgressBar(locationResult.percentage)); - tasksList.appendChild(locationLi); - - // Language progress - const languageResult = determineTop(progressData.solved, languageRating); - const languageLi = document.createElement('li'); - languageLi.textContent = `${accountData.language}: ${languageResult.message}. `; - languageLi.appendChild(createProgressBar(languageResult.percentage)); - tasksList.appendChild(languageLi); - - // Award progress - const award = findAwardWithMaxMembers(awardsData); - if (award) { - const awardLi = document.createElement('li'); - const awardLink = document.createElement('a'); - awardLink.href = award.link; - awardLink.textContent = award.award; - awardLi.textContent = `Most unresolved award: `; - awardLi.appendChild(awardLink); - awardLi.append(` (${award.description}) by ${award.members}. `); - awardLi.appendChild(createProgressBar(award.percentage)); - tasksList.appendChild(awardLi); - } - - return tasksList; -} /** * Generates HTML for the progress table. * @param cache - The cache data. - * @param source - The source data. * @returns The generated HTMLElement (progress table container). */ -export function generateProgressTableHTML( - cache: CacheData, - source: Source -): HTMLElement { +export function generateProgressTableHTML(cache: CacheData): HTMLElement { const progressContainer = document.createElement('div'); progressContainer.appendChild(createSectionHeader('Progress')); progressContainer.appendChild(createProgressBar(cache.progressData.percentage)); @@ -226,24 +112,5 @@ export function generateProgressTableHTML( cache.locationRating, cache.languageRating )); - - // Personal tasks section - const personalTasksHeader = document.createElement('h3'); - personalTasksHeader.textContent = 'Personal tasks'; - progressContainer.appendChild(personalTasksHeader); - progressContainer.appendChild(createPersonalTasksList(source.tasks)); - - // Tasks section - const tasksHeader = document.createElement('h3'); - tasksHeader.textContent = 'Tasks'; - progressContainer.appendChild(tasksHeader); - progressContainer.appendChild(createTasksList( - cache.progressData, - cache.accountData, - cache.locationRating, - cache.languageRating, - cache.awardsData - )); - return progressContainer; } diff --git a/helpers/tasksBlock.ts b/helpers/tasksBlock.ts new file mode 100644 index 0000000..226e8f9 --- /dev/null +++ b/helpers/tasksBlock.ts @@ -0,0 +1,174 @@ +import { AccountData, AwardData, AwardBlockData, CacheData, PersonalTask, ProgressData, RatingData, Source } from './types'; +import { createProgressBar, createSectionHeader } from './commonBlocks'; + +interface Result { + message: string; + percentage: number; + rest: number; +} + +/** + * Determines the user's position relative to rating thresholds + * @param solved - Number of solved problems + * @param rating - Rating data containing place and thresholds + * @returns Result object with message, percentage and remaining problems + */ +function determineTop(solved: number, rating: RatingData): Result { + const thresholds = [ + { limit: 100, top: rating.top100, title: 'Top 100' }, + { limit: 50, top: rating.top50, title: 'Top 50' }, + { limit: 25, top: rating.top25, title: 'Top 25' }, + { limit: 10, top: rating.top10, title: 'Top 10' }, + { limit: 5, top: rating.top5, title: 'Top 5' }, + { limit: 1, top: rating.top1, title: 'Top 1' }, + ]; + + for (let i = 0; i < thresholds.length; i++) { + const { limit, top, title } = thresholds[i]; + if (rating.place > limit) { + const prevTop = i > 0 ? thresholds[i - 1].top : 0; + const rest = top - solved + 1; + const range = top - prevTop + 1; + + const percentage = range > 0 ? Math.round(((range - rest) / range) * 100) : 0; + return { + message: `${rest} problems away from ${title}`, + percentage, + rest + }; + } + } + + return { + message: 'You are in the Top 1', + percentage: 100, + rest: 0 + }; +} + +/** + * Finds the award with the most members that isn't completed + * @param awardBlocks - Array of award blocks + * @returns AwardData with max members or null if none found + */ +function findAwardWithMaxMembers(awardBlocks: AwardBlockData[]): AwardData | null { + const allAwards = awardBlocks + .flatMap(block => block.awards) + .filter(award => !award.isCompleted); + + if (allAwards.length === 0) return null; + + return allAwards.reduce((maxAward, currentAward) => { + const currentMembers = parseInt(currentAward.members.replace(/\D/g, '')) || 0; + const maxMembers = parseInt(maxAward.members.replace(/\D/g, '')) || 0; + return currentMembers > maxMembers ? currentAward : maxAward; + }); +} + +/** + * Creates a prioritized list of tasks + * @param tasks - Configuration for task prioritization + * @returns HTMLElement containing the tasks list + */ +function createTasksList({ + personalTasks, + progressData, + accountData, + locationRating, + languageRating, + awardsData +}: { + personalTasks: PersonalTask[]; + progressData: ProgressData; + accountData: AccountData; + locationRating: RatingData; + languageRating: RatingData; + awardsData: AwardBlockData[]; +}): HTMLElement { + const tasksList = document.createElement('ul'); + tasksList.className = 'tasks-list'; + + // Add personal tasks + personalTasks.forEach(task => { + const li = document.createElement('li'); + li.textContent = task.task; + li.appendChild(createProgressBar(task.percentage)); + tasksList.appendChild(li); + }); + + // Calculate level progress + const levelRest = parseInt(progressData.toTheNext.match(/(\d+)/)?.[1] ?? '25', 10); + const levelPercentage = (25 - levelRest) * 4; + const levelTask = document.createElement('li'); + levelTask.textContent = `${progressData.level}: ${progressData.toTheNext}. `; + levelTask.appendChild(createProgressBar(levelPercentage)); + + // Calculate location and language progress + const locationResult = determineTop(progressData.solved, locationRating); + const languageResult = determineTop(progressData.solved, languageRating); + + const locationLi = document.createElement('li'); + locationLi.textContent = `${accountData.location}: ${locationResult.message}. `; + locationLi.appendChild(createProgressBar(locationResult.percentage)); + + const languageLi = document.createElement('li'); + languageLi.textContent = `${accountData.language}: ${languageResult.message}. `; + languageLi.appendChild(createProgressBar(languageResult.percentage)); + + // Prioritize tasks based on remaining problems + const tasksToSort = [ + { element: levelTask, rest: levelRest }, + { element: locationLi, rest: locationResult.rest }, + { element: languageLi, rest: languageResult.rest } + ]; + + tasksToSort + .sort((a, b) => a.rest - b.rest) + .forEach(({ element }) => tasksList.appendChild(element)); + + // Add award if exists + const award = findAwardWithMaxMembers(awardsData); + if (award) { + const awardLi = document.createElement('li'); + const awardLink = document.createElement('a'); + awardLink.href = award.link; + awardLink.textContent = award.award; + + awardLi.append( + 'Most unresolved award: ', + awardLink, + ` (${award.description}) by ${award.members}. ` + ); + awardLi.appendChild(createProgressBar(award.percentage)); + tasksList.appendChild(awardLi); + } + + return tasksList; +} + +/** + * Generates HTML for the tasks table + * @param cache - Cache data + * @param source - Source data + * @returns HTMLElement containing the tasks table + */ +export function generateTasksTableHTML( + cache: CacheData, + source: Source +): HTMLElement { + const tasksContainer = document.createElement('div'); + + tasksContainer.append( + createSectionHeader('Tasks'), + createTasksList({ + personalTasks: source.tasks, + progressData: cache.progressData, + accountData: cache.accountData, + locationRating: cache.locationRating, + languageRating: cache.languageRating, + awardsData: cache.awardsData + }) + ); + + return tasksContainer; +}