diff --git a/docs/images/progress.png b/docs/images/progress.png index 61ad3c0..35f274d 100644 Binary files a/docs/images/progress.png and b/docs/images/progress.png differ diff --git a/helpers/awardsBlock.ts b/helpers/awardsBlock.ts new file mode 100644 index 0000000..df2cee7 --- /dev/null +++ b/helpers/awardsBlock.ts @@ -0,0 +1,95 @@ +import { AwardData, AwardBlockData } from './types'; +import { createProgressBar, createSectionHeader } from './commonBlocks'; + +function createAwardTableRow(award: AwardData): HTMLElement { + const row = document.createElement('tr'); + + const awardCell = document.createElement('td'); + const awardLink = document.createElement('a'); + awardLink.href = award.link; + awardLink.textContent = award.award; + awardCell.appendChild(awardLink); + row.appendChild(awardCell); + + const descriptionCell = document.createElement('td'); + descriptionCell.textContent = award.description; + row.appendChild(descriptionCell); + + const membersCell = document.createElement('td'); + membersCell.textContent = award.members; + row.appendChild(membersCell); + + const progressCell = document.createElement('td'); + progressCell.textContent = award.progress; + row.appendChild(progressCell); + + const progressBarCell = document.createElement('td'); + progressBarCell.appendChild(createProgressBar(award.percentage)); + row.appendChild(progressBarCell); + + return row; +} + +function createAwardTable(awardData: AwardBlockData): HTMLElement { + const table = document.createElement('table'); + const tableBody = document.createElement('tbody'); + + const headerRow = document.createElement('tr'); + ['Award', 'Description', 'Members', 'Progress', 'Progress bar'].forEach(headerText => { + const headerCell = document.createElement('th'); + headerCell.textContent = headerText; + headerRow.appendChild(headerCell); + }); + tableBody.appendChild(headerRow); + + awardData.awards.forEach(award => { + if (!award.isCompleted) { + tableBody.appendChild(createAwardTableRow(award)); + } + }); + + table.appendChild(tableBody); + return table; +} + +function createAwardBlock(awardData: AwardBlockData): HTMLElement { + const awardBlock = document.createElement('div'); + + const awardName = document.createElement('h3'); + awardName.textContent = awardData.name; + awardBlock.appendChild(awardName); + + const completed = awardData.getCompletedCount(); + const total = awardData.awards.length; + + const completedStatus = document.createElement('h4'); + completedStatus.textContent = `Status: Won ${completed} out of ${total}`; + awardBlock.appendChild(completedStatus); + + const uncompletedStatus = document.createElement('h4'); + uncompletedStatus.textContent = `Uncompleted awards: ${total - completed}`; + awardBlock.appendChild(uncompletedStatus); + + awardBlock.appendChild(createAwardTable(awardData)); + + return awardBlock; +} + +/** + * Generates HTML for the awards table. + * @param awardsData - The awards data. + * @returns The generated HTMLElement (awards table container). + */ +export function generateAwardsTableHTML(awardsData: AwardBlockData[]): HTMLElement { + const awardsContainer = document.createElement('div'); + + const awardsHeader = document.createElement('h2'); + awardsHeader.textContent = 'Awards'; + awardsContainer.appendChild(awardsHeader); + + awardsData.forEach(awardData => { + awardsContainer.appendChild(createAwardBlock(awardData)); + }); + + return awardsContainer; +} diff --git a/helpers/commonBlocks.ts b/helpers/commonBlocks.ts new file mode 100644 index 0000000..3bdb386 --- /dev/null +++ b/helpers/commonBlocks.ts @@ -0,0 +1,30 @@ +/** + * Creates a progress bar element. + * @param percentage - The progress percentage (0 to 100). + * @returns The generated HTMLElement (progress bar container). + */ +export function createProgressBar(percentage: number): HTMLElement { + const clampedPercentage = Math.min(100, Math.max(0, percentage)); + + const progressContainer = document.createElement('div'); + progressContainer.className = 'progress-container'; + + const progressBar = document.createElement('span'); + progressBar.className = 'progress-bar'; + progressBar.style.width = `${clampedPercentage}%`; + + const progressText = document.createElement('span'); + progressText.className = 'progress-text'; + progressText.textContent = `${clampedPercentage}%`; + + progressContainer.appendChild(progressBar); + progressContainer.appendChild(progressText); + + return progressContainer; +} + +export function createSectionHeader(title: string, tagName: 'h2' | 'h4' | 'h5' = 'h2'): HTMLElement { + const header = document.createElement(tagName); + header.textContent = title; + return header; +} diff --git a/helpers/fetchProgress.ts b/helpers/fetchProgress.ts index 3108c84..9ceb943 100644 --- a/helpers/fetchProgress.ts +++ b/helpers/fetchProgress.ts @@ -8,15 +8,12 @@ import { parseLevelData, parseFriends } from './parsers'; -import { - generateProfileHTML, - generateImageHTML, - generateProgressTableHTML, - generateRatingTableHTML, - generateLevelsTableHTML, - generateAwardsTableHTML, - generateFriendsHTML -} from './htmlGenerators'; +import { generateProfileHTML, generateImageHTML } from './profileBlock'; +import { generateProgressTableHTML } from './progressBlock'; +import { generateRatingTableHTML } from './ratingBlock'; +import { generateLevelsTableHTML } from './levelsBlock'; +import { generateAwardsTableHTML } from './awardsBlock'; +import { generateFriendsHTML } from './friendsBlock'; import { AccountData, AwardBlockData, @@ -30,20 +27,28 @@ const mainUrl = 'https://projecteuler.net/'; const sessionIdName = 'PHPSESSID'; const keepAliveName = 'keep_alive'; -export async function fetchProgress(session: string, keep_alive: string, retries: number = 1): Promise { - const cookies = `${sessionIdName}=${session}; ${keepAliveName}=${keep_alive}`; - console.log(`Fetching progress with cookies: ${cookies}`); +/** + * Fetches progress data and returns it as an HTMLElement. + * @param session - The session cookie value. + * @param keep_alive - The keep-alive cookie value. + * @param retries - The number of retries in case of failure. + * @returns A Promise that resolves to an HTMLElement. + */ +export async function fetchProgress(session: string, keep_alive: string): Promise { + const cookies = `${sessionIdName}=${session}; ${keepAliveName}=${keep_alive}`; try { return await tryToFetchProgress(cookies); } catch (error) { console.error('Error fetching progress:', error); - return `
Error fetching progress. Please update your cookies in the settings and try again.
`; + const errorContainer = document.createElement('div'); + errorContainer.textContent = 'Error fetching progress. Please update your cookies in the settings and try again.'; + return errorContainer; } } -async function tryToFetchProgress(cookies: string): Promise { +async function tryToFetchProgress(cookies: string): Promise { const [ accountData, progressData, @@ -104,25 +109,43 @@ async function fetchAwardsData(cookies: string): Promise { return parseAwardsData(myAwardsHtml, awardsHtml); } -function generateHTML(accountData: AccountData, progressData: ProgressData, euleriansPlace: string, locationUrl: string, languageUrl: string, locationRating: RatingData, languageRating: RatingData, levelDataArray: LevelData[], awardsData: AwardBlockData[], friends: FriendData[]): string { - const profileHTML = generateProfileHTML(accountData, progressData); - console.log("Successfully generated profile HTML."); - const imageHTML = generateImageHTML(accountData.account); - console.log("Successfully generated image HTML."); - const progressHTML = generateProgressTableHTML(accountData, progressData, locationUrl, languageUrl, euleriansPlace, locationRating, languageRating, awardsData); - console.log("Successfully generated progress HTML."); - const locationTitle = `Progress in the ${accountData.location}'s rating` - const locationRatingHTML = generateRatingTableHTML(locationTitle, progressData.solved, locationRating); - console.log("Successfully generated location rating HTML."); - const languageTitle = `Progress in the ${accountData.language}'s rating` - const languageRatingHTML = generateRatingTableHTML(languageTitle, progressData.solved, languageRating); - console.log("Successfully generated language rating HTML."); - const levelsHTML = generateLevelsTableHTML(progressData, levelDataArray); - console.log("Successfully generated levels HTML."); - const awardsHTML = generateAwardsTableHTML(awardsData); - console.log("Successfully generated awards HTML."); - const friendsHTML = generateFriendsHTML(friends, accountData); - console.log("Successfully generated friends HTML."); +function generateHTML( + accountData: AccountData, + progressData: ProgressData, + euleriansPlace: string, + locationUrl: string, + languageUrl: string, + locationRating: RatingData, + languageRating: RatingData, + levelDataArray: LevelData[], + awardsData: AwardBlockData[], + friends: FriendData[] +): HTMLElement { + const container = document.createElement('div'); - return profileHTML + imageHTML + progressHTML + locationRatingHTML + languageRatingHTML + levelsHTML + awardsHTML + friendsHTML; + const profileElement = generateProfileHTML(accountData, progressData); + container.appendChild(profileElement); + + const imageElement = generateImageHTML(accountData.account); + container.appendChild(imageElement); + + const progressElement = generateProgressTableHTML(accountData, progressData, locationUrl, languageUrl, euleriansPlace, locationRating, languageRating, awardsData); + container.appendChild(progressElement); + + const locationRatingElement = generateRatingTableHTML(locationUrl, accountData.location, progressData.solved, locationRating); + container.appendChild(locationRatingElement); + + const languageRatingElement = generateRatingTableHTML(languageUrl, accountData.language, progressData.solved, languageRating); + container.appendChild(languageRatingElement); + + const levelsElement = generateLevelsTableHTML(progressData, levelDataArray); + container.appendChild(levelsElement); + + const awardsElement = generateAwardsTableHTML(awardsData); + container.appendChild(awardsElement); + + const friendsElement = generateFriendsHTML(friends, accountData); + container.appendChild(friendsElement); + + return container; } diff --git a/helpers/friendsBlock.ts b/helpers/friendsBlock.ts new file mode 100644 index 0000000..dd02be4 --- /dev/null +++ b/helpers/friendsBlock.ts @@ -0,0 +1,89 @@ +import { AccountData, FriendData } from './types'; +import { createSectionHeader } from './commonBlocks'; + +function createFriendTableRow(friend: FriendData, myAccount: FriendData, accountData: AccountData): HTMLElement { + const row = document.createElement('tr'); + + // Get the current account + if (friend.username === accountData.alias) { + row.id = 'your-account'; + } + + // Rank + const rankCell = document.createElement('td'); + rankCell.textContent = friend.rank; + row.appendChild(rankCell); + + // Username + const usernameCell = document.createElement('td'); + const usernameLink = document.createElement('a'); + usernameLink.href = `https://projecteuler.net/progress=${friend.username}`; + usernameLink.textContent = friend.username; + usernameCell.appendChild(usernameLink); + row.appendChild(usernameCell); + + // Solved tasks + const solvedCell = document.createElement('td'); + solvedCell.textContent = friend.username === accountData.alias + ? friend.solved.toString() + : friend.solved > myAccount.solved + ? `${friend.solved} (+${friend.solved - myAccount.solved})` + : friend.solved.toString(); + row.appendChild(solvedCell); + + // Level + const levelCell = document.createElement('td'); + levelCell.textContent = friend.username === accountData.alias + ? friend.level.toString() + : friend.level > myAccount.level + ? `${friend.level} (+${friend.level - myAccount.level})` + : friend.level.toString(); + row.appendChild(levelCell); + + // Awards + const awardsCell = document.createElement('td'); + awardsCell.textContent = friend.username === accountData.alias + ? friend.awards.toString() + : friend.awards > myAccount.awards + ? `${friend.awards} (+${friend.awards - myAccount.awards})` + : friend.awards.toString(); + row.appendChild(awardsCell); + + return row; +} + +function createFriendsTable(friends: FriendData[], myAccount: FriendData, accountData: AccountData): HTMLElement { + const table = document.createElement('table'); + const tableBody = document.createElement('tbody'); + + const headerRow = document.createElement('tr'); + ['Rank', 'Username', 'Solved', 'Level', 'Awards'].forEach(headerText => { + const headerCell = document.createElement('th'); + headerCell.textContent = headerText; + headerRow.appendChild(headerCell); + }); + tableBody.appendChild(headerRow); + + friends.forEach(friend => { + tableBody.appendChild(createFriendTableRow(friend, myAccount, accountData)); + }); + + table.appendChild(tableBody); + return table; +} + +/** + * Generates HTML for the friends table. + * @param friends - The friends data. + * @param accountData - The account data. + * @returns The generated HTMLElement (friends table container). + */ +export function generateFriendsHTML(friends: FriendData[], accountData: AccountData): HTMLElement { + const friendsContainer = document.createElement('div'); + friendsContainer.appendChild(createSectionHeader('Friends')); + const myAccount = friends.find(friend => friend.username === accountData.alias) ?? new FriendData('-', accountData.alias, 0, 0, 0); + friendsContainer.appendChild(createFriendsTable(friends, myAccount, accountData)); + + return friendsContainer; +} + diff --git a/helpers/htmlGenerators.ts b/helpers/htmlGenerators.ts deleted file mode 100644 index df928b4..0000000 --- a/helpers/htmlGenerators.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { stringToNumber } from './parsers'; -import { AccountData, AwardData, AwardBlockData, FriendData, ProgressData, RatingData, LevelData } from './types'; - -/** - * Generates HTML for the profile section. - * @param accountData - The account data. - * @param progressData - The progress data. - * @returns The generated HTML string. - */ -export function generateProfileHTML(accountData: AccountData, progressData: ProgressData): string { - return ` -

Profile

- - - - - - - - - -
Account${accountData.account}
Alias${accountData.alias}
Location${accountData.location}
Language${accountData.language}
Level${progressData.level}
Solved${progressData.solved}
- `; -} - -/** - * Generates HTML for the profile image. - * @param account - The account name. - * @returns The generated HTML string. - */ -export function generateImageHTML(account: string): string { - const imageSrc = `https://projecteuler.net/profile/${account}.png`; - return `Profile ${account}`; -} - -interface Result { - message: string; - percentage: number; -} - -export 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; // Получаем предыдущее значение 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 }; -} - -/** - * Функция для нахождения AwardData с максимальным значением members. - * @param awardBlocks - Массив AwardBlockData. - * @returns AwardData с максимальным значением members или null, если не найдено. - */ -export 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 createProgressBar(percentage: number): string { - const clampedPercentage = Math.min(100, Math.max(0, percentage)); - - return ` -
- - ${clampedPercentage}% -
- `; -} - -/** - * Generates HTML for the progress table. - * @param accountData - The account data. - * @param progressData - The progress data. - * @param locationUrl - The URL for the location rating. - * @param languageUrl - The URL for the language rating. - * @param euleriansPlace - The user's place in Eulerians' rating. - * @param locationRating - The location rating data. - * @param languageRating - The language rating data. - * @returns The generated HTML string. - */ -export function generateProgressTableHTML( - accountData: AccountData, - progressData: ProgressData, - locationUrl: string, - languageUrl: string, - euleriansPlace: string, - locationRating: RatingData, - languageRating: RatingData, - awardsData: AwardBlockData[] -): string { - const locationPlace = locationRating.place > 100 ? 'You are not in the Top 100' : locationRating.place; - const languagePlace = languageRating.place > 100 ? 'You are not in the Top 100' : languageRating.place; - - const progressPercentage = createProgressBar(progressData.percentage); - - const locationPlaceToTop = locationRating.place > 100 ? 0 : 100 - locationRating.place; - const locationPercentage = createProgressBar(locationPlaceToTop); - const languagePlaceToTop = languageRating.place > 100 ? 0 : 100 - languageRating.place; - const languagePercentage = createProgressBar(languagePlaceToTop); - - const matchToTheNext = progressData.toTheNext.match(/(\d+)/); - const countToTheNext = matchToTheNext ? (25 - parseInt(matchToTheNext[1], 10)) * 4 : 0; - const toTheNextPercentage = createProgressBar(countToTheNext); - - const locationResult = determineTop(progressData.solved, locationRating) - const locationLiElement = `
  • ${accountData.location}: ${locationResult.message}. ${createProgressBar(locationResult.percentage)}
  • `; - - const languageResult = determineTop(progressData.solved, languageRating) - const languageLiElement = `
  • ${accountData.language}: ${languageResult.message}. ${createProgressBar(languageResult.percentage)}
  • `; - - const award = findAwardWithMaxMembers(awardsData); - const awardPlace = award ? `
  • Most unresolved award: ${award.award} (${award.description}) by ${award.members}. ${createProgressBar(award.percentage)}
  • ` : ''; - - return ` -

    Progress

    - ${progressPercentage} - - - - - - - - -
    CompetitionStatusProgress bar
    Progress${progressData.progress}${progressPercentage}
    Place in Eulerians${euleriansPlace}
    Place in ${accountData.location}${locationPlace}${locationPercentage}
    Place in ${accountData.language}${languagePlace}${languagePercentage}
    -

    Tasks

    -
      -
    • ${progressData.level}: ${progressData.toTheNext}. ${toTheNextPercentage}
    • - ${locationLiElement} - ${languageLiElement} - ${awardPlace} -
    - `; -} - -/** - * Generates HTML for the rating table. - * @param title - The title of the rating table. - * @param solved - The number of solved problems. - * @param rating - The rating data. - * @returns The generated HTML string. - */ -export function generateRatingTableHTML(title: string, solved: number, rating: RatingData): string { - const place = rating.place > 100 ? 'You are not in the Top 100' : rating.place; - const top1Row = rating.place >= 1 ? `Top 1${rating.top1}${rating.top1 - solved + 1} problems away from Top 1` : ''; - const top5Row = rating.place > 5 ? `Top 5${rating.top5}${rating.top5 - solved + 1} problems away from Top 5` : ''; - const top10Row = rating.place > 10 ? `Top 10${rating.top10}${rating.top10 - solved + 1} problems away from Top 10` : ''; - const top25Row = rating.place > 25 ? `Top 25${rating.top25}${rating.top25 - solved + 1} problems away from Top 25` : ''; - const top50Row = rating.place > 50 ? `Top 50${rating.top50}${rating.top50 - solved + 1} problems away from Top 50` : ''; - const top100Row = rating.place > 100 ? `Top 100${rating.top100}${rating.top100 - solved + 1} problems away from Top 100` : ''; - - return ` -

    ${title}

    -

    Current place: ${place}

    -
    Solved problems: ${solved}
    - - - - ${top100Row} - ${top50Row} - ${top25Row} - ${top10Row} - ${top5Row} - ${top1Row} - -
    CompetitionSolvedRemaining
    - `; -} - -export function generateLevelsTableHTML(progressData: ProgressData, levelDataArray: LevelData[]): string { - const level = stringToNumber(progressData.level); - - // Проверяем, что levelDataArray не пустой и уровень не выходит за пределы массива - if (!levelDataArray.length || level >= levelDataArray.length) { - return `

    No levels available

    `; - } - - // Используем slice для получения подмассива, начиная с текущего уровня - const slicedArray = levelDataArray.slice(level - 1); - - // Преобразуем каждый элемент в строку - const rows = slicedArray.map(levelData => { - const rowLevel = stringToNumber(levelData.level); - const needForLevel = rowLevel * 25; - const remaining = Math.max(0, needForLevel - progressData.solved); - - return ` - - ${levelData.level} - ${needForLevel} - ${remaining} - ${levelData.members} - - `; - }).join(''); - - const allMembers = slicedArray.reduce((total, levelData) => { - const membersCount = stringToNumber(levelData.members); - return total + (isNaN(membersCount) ? 0 : membersCount); - }, 0); - - return ` -

    Level progress

    -

    Current level: ${progressData.level}

    -
    Solved problems: ${progressData.solved}
    -
    Status: ${allMembers} members have made it this far.
    - - - - ${rows} - -
    LevelSolveRemainingMembers
    - `; -} -export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string { - const tables = awardsData.map(awardData => { - const awards = awardData.awards; - const length = awards.length; - - // Подсчитываем завершенные и создаем строки таблицы за один проход - const { completed, rows } = awards.reduce<{ completed: number; rows: string[] }>( - (acc, { award, link, description, isCompleted, progress, percentage, members }) => { - if (isCompleted) { - acc.completed++; - } else { - const progressBar = createProgressBar(percentage); - - acc.rows.push(` - - ${award} - ${description} - ${members} - ${progress} - ${progressBar} - - `); - } - return acc; - }, - { completed: 0, rows: [] } // Начальное значение аккумулятора - ); - - return ` -

    ${awardData.name}

    -

    Status: Won ${completed} out of ${length}

    -

    Uncompleted awards: ${length - completed}

    - - - - ${rows.join('')} - -
    AwardDescriptionMembersProgressProgress bar
    - `; - }).join(''); - - return ` -

    Awards

    - ${tables} - `; -} - -export function generateFriendsHTML(friends: FriendData[], accountData: AccountData): string { - const myAccount = friends.find(friend => friend.username === accountData.alias) ?? new FriendData( `-`, accountData.alias, 0, 0, 0); - - const rows = friends.map(friend => { - const idCss = friend.username === accountData.alias ? 'id="your-account"' : ''; - const username = `${friend.username}`; - const solved = friend.username === accountData.alias ? friend.solved : friend.solved > myAccount.solved ? `${friend.solved} (+${friend.solved - myAccount.solved})` : friend.solved; - const level = friend.username === accountData.alias ? friend.level : friend.level > myAccount.level ? `${friend.level} (+${friend.level - myAccount.level})` : friend.level; - const awards = friend.username === accountData.alias ? friend.awards : friend.awards > myAccount.awards ? `${friend.awards} (+${friend.awards - myAccount.awards})` : friend.awards; - - return ` - - ${friend.rank} - ${username} - ${solved} - ${level} - ${awards} - - `; - }); - - return ` -

    Friends

    - - - - ${rows.join('')} - -
    RankUsernameSolvedLevelAwards
    - `; -} diff --git a/helpers/levelsBlock.ts b/helpers/levelsBlock.ts new file mode 100644 index 0000000..9081150 --- /dev/null +++ b/helpers/levelsBlock.ts @@ -0,0 +1,86 @@ +import { stringToNumber } from './parsers'; +import { ProgressData, LevelData } from './types'; +import { createSectionHeader } from './commonBlocks'; + +function createLevelTableRow(levelData: LevelData, progressData: ProgressData): HTMLElement { + const row = document.createElement('tr'); + const cell1 = document.createElement('td'); + const cell2 = document.createElement('td'); + const cell3 = document.createElement('td'); + const cell4 = document.createElement('td'); + + const rowLevel = stringToNumber(levelData.level); + const needForLevel = rowLevel * 25; + const remaining = Math.max(0, needForLevel - progressData.solved); + + const levelLink = document.createElement('a'); + levelLink.href = `https://projecteuler.net/level=${rowLevel}`; + levelLink.textContent = levelData.level; + + cell1.appendChild(levelLink); + cell2.textContent = needForLevel.toString(); + cell3.textContent = remaining.toString(); + cell4.textContent = levelData.members; + + row.appendChild(cell1); + row.appendChild(cell2); + row.appendChild(cell3); + row.appendChild(cell4); + return row; +} + +function createLevelsTable(progressData: ProgressData, levelDataArray: LevelData[]): HTMLElement { + const table = document.createElement('table'); + const tableBody = document.createElement('tbody'); + + const headerRow = document.createElement('tr'); + ['Level', 'Solve', 'Remaining', 'Members'].forEach(headerText => { + const headerCell = document.createElement('th'); + headerCell.textContent = headerText; + headerRow.appendChild(headerCell); + }); + tableBody.appendChild(headerRow); + + levelDataArray.forEach(levelData => { + tableBody.appendChild(createLevelTableRow(levelData, progressData)); + }); + + table.appendChild(tableBody); + return table; +} + +function calculateTotalMembers(levelDataArray: LevelData[]): number { + return levelDataArray.reduce((total, levelData) => { + const membersCount = stringToNumber(levelData.members); + return total + (isNaN(membersCount) ? 0 : membersCount); + }, 0); +} + +/** + * Generates HTML for the levels table. + * @param progressData - The progress data. + * @param levelDataArray - The array of level data. + * @returns The generated HTMLElement (levels table container). + */ +export function generateLevelsTableHTML(progressData: ProgressData, levelDataArray: LevelData[]): HTMLElement { + const level = stringToNumber(progressData.level); + + const levelsContainer = document.createElement('div'); + + // If there are no levels or the current level is higher than the last available level, display a message + if (!levelDataArray.length || level >= levelDataArray.length) { + levelsContainer.appendChild(createSectionHeader('No levels available')); + return levelsContainer; + } + + const slicedArray = levelDataArray.slice(level - 1); + levelsContainer.appendChild(createSectionHeader('Level progress')); + levelsContainer.appendChild(createSectionHeader(`Current level: ${progressData.level}`, 'h4')); + levelsContainer.appendChild(createSectionHeader(`Solved problems: ${progressData.solved}`, 'h5')); + const allMembers = calculateTotalMembers(slicedArray); + levelsContainer.appendChild(createSectionHeader(`Status: ${allMembers} members have made it this far.`, 'h5')); + + levelsContainer.appendChild(createLevelsTable(progressData, slicedArray)); + + return levelsContainer; +} diff --git a/helpers/parsers.ts b/helpers/parsers.ts index 3b08aad..5d87955 100644 --- a/helpers/parsers.ts +++ b/helpers/parsers.ts @@ -115,10 +115,8 @@ export function parseLevelData(html: string): LevelData[] { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); - // Находим все элементы, соответствующие селектору const tileBoxes = doc.querySelectorAll('#tile_grid > .tile_box'); - // Используем map для создания массива LevelData return Array.from(tileBoxes).map(tileBox => { const levelElement = tileBox.querySelector('a'); const membersElement = tileBox.querySelector('.small_notice'); diff --git a/helpers/profileBlock.ts b/helpers/profileBlock.ts new file mode 100644 index 0000000..eb8b210 --- /dev/null +++ b/helpers/profileBlock.ts @@ -0,0 +1,60 @@ +import { AccountData, ProgressData } from './types'; + +/** + * Generates HTML for the profile section. + * @param accountData - The account data. + * @param progressData - The progress data. + * @returns The generated HTMLElement. + */ +export function generateProfileHTML(accountData: AccountData, progressData: ProgressData): HTMLElement { + const profileContainer = document.createElement('div'); + + const profileHeader = document.createElement('h2'); + profileHeader.textContent = 'Profile'; + profileContainer.appendChild(profileHeader); + + const profileTable = document.createElement('table'); + profileTable.className = 'profile-table'; + + const tableBody = document.createElement('tbody'); + + const addRow = (label: string, value: string | number) => { + const row = document.createElement('tr'); + const labelCell = document.createElement('td'); + const valueCell = document.createElement('td'); + + labelCell.textContent = label; + valueCell.textContent = String(value); + + row.appendChild(labelCell); + row.appendChild(valueCell); + tableBody.appendChild(row); + }; + + addRow('Account', accountData.account); + addRow('Alias', accountData.alias); + addRow('Location', accountData.location); + addRow('Language', accountData.language); + addRow('Level', progressData.level); + addRow('Solved', progressData.solved); + + profileTable.appendChild(tableBody); + profileContainer.appendChild(profileTable); + + return profileContainer; +} + +/** + * Generates HTML for the profile image. + * @param account - The account name. + * @returns The generated HTMLElement (img element). + */ +export function generateImageHTML(account: string): HTMLElement { + const imageElement = document.createElement('img'); + + imageElement.src = `https://projecteuler.net/profile/${account}.png`; + imageElement.alt = `Profile ${account}`; + imageElement.title = account; + + return imageElement; +} diff --git a/helpers/progressBlock.ts b/helpers/progressBlock.ts new file mode 100644 index 0000000..ed00e55 --- /dev/null +++ b/helpers/progressBlock.ts @@ -0,0 +1,241 @@ +import { AccountData, AwardData, AwardBlockData, 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'); + const statusCell = document.createElement('td'); + const progressCell = document.createElement('td'); + + // If label is a string, set the text content directly + if (typeof label === 'string') { + labelCell.textContent = label; + } else { // If label is an HTMLElement, append it to the cell + labelCell.appendChild(label); + } + + statusCell.textContent = status; + + if (progressBar) { + progressCell.appendChild(progressBar); + } + + row.appendChild(labelCell); + row.appendChild(statusCell); + row.appendChild(progressCell); + return row; +} + +function createProgressTable( + progressData: ProgressData, + euleriansPlace: string, + locationUrl: string, + languageUrl: string, + accountData: AccountData, + locationRating: RatingData, + languageRating: RatingData +): HTMLElement { + const table = document.createElement('table'); + const tableBody = document.createElement('tbody'); + + const headerRow = document.createElement('tr'); + ['Competition', 'Status', 'Progress bar'].forEach(headerText => { + const headerCell = document.createElement('th'); + headerCell.textContent = headerText; + headerRow.appendChild(headerCell); + }); + tableBody.appendChild(headerRow); + + // Solved out of total + const progressPercentage = createProgressBar(progressData.percentage); + tableBody.appendChild(createTableRow( + `Progress`, + progressData.progress, + progressPercentage + )); + + // Eulerians' place + const euleriansLink = document.createElement('a'); + euleriansLink.href = 'https://projecteuler.net/eulerians'; + euleriansLink.textContent = 'Place in Eulerians'; + tableBody.appendChild(createTableRow( + euleriansLink, + euleriansPlace + )); + + // Location rating + const locationPlace = locationRating.place > 100 ? 'You are not in the Top 100' : locationRating.place.toString(); + const locationPlaceToTop = locationRating.place > 100 ? 0 : 100 - locationRating.place; + const locationPercentage = createProgressBar(locationPlaceToTop); + const locationLink = document.createElement('a'); + locationLink.href = locationUrl; + locationLink.textContent = `Place in ${accountData.location}`; + tableBody.appendChild(createTableRow( + locationLink, + locationPlace, + locationPercentage + )); + + // Language rating + const languagePlace = languageRating.place > 100 ? 'You are not in the Top 100' : languageRating.place.toString(); + const languagePlaceToTop = languageRating.place > 100 ? 0 : 100 - languageRating.place; + const languagePercentage = createProgressBar(languagePlaceToTop); + const languageLink = document.createElement('a'); + languageLink.href = languageUrl; + languageLink.textContent = `Place in ${accountData.language}`; + tableBody.appendChild(createTableRow( + languageLink, + languagePlace, + languagePercentage + )); + + table.appendChild(tableBody); + return table; +} + +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 accountData - The account data. + * @param progressData - The progress data. + * @param locationUrl - The URL for the location rating. + * @param languageUrl - The URL for the language rating. + * @param euleriansPlace - The user's place in Eulerians' rating. + * @param locationRating - The location rating data. + * @param languageRating - The language rating data. + * @param awardsData - The awards data. + * @returns The generated HTMLElement (progress table container). + */ +export function generateProgressTableHTML( + accountData: AccountData, + progressData: ProgressData, + locationUrl: string, + languageUrl: string, + euleriansPlace: string, + locationRating: RatingData, + languageRating: RatingData, + awardsData: AwardBlockData[] +): HTMLElement { + const progressContainer = document.createElement('div'); + progressContainer.appendChild(createSectionHeader('Progress')); + progressContainer.appendChild(createProgressBar(progressData.percentage)); + progressContainer.appendChild(createProgressTable( + progressData, + euleriansPlace, + locationUrl, + languageUrl, + accountData, + locationRating, + languageRating + )); + + // Tasks section + const tasksHeader = document.createElement('h3'); + tasksHeader.textContent = 'Tasks'; + progressContainer.appendChild(tasksHeader); + progressContainer.appendChild(createTasksList( + progressData, + accountData, + locationRating, + languageRating, + awardsData + )); + + return progressContainer; +} diff --git a/helpers/ratingBlock.ts b/helpers/ratingBlock.ts new file mode 100644 index 0000000..adebb11 --- /dev/null +++ b/helpers/ratingBlock.ts @@ -0,0 +1,82 @@ +import { RatingData } from './types'; +import { createSectionHeader } from './commonBlocks'; + +function createTableRow(label: string, solvedCount: number, remaining: number): HTMLElement { + const row = document.createElement('tr'); + const cell1 = document.createElement('td'); + const cell2 = document.createElement('td'); + const cell3 = document.createElement('td'); + + cell1.textContent = label; + cell2.textContent = solvedCount.toString(); + cell3.textContent = `${remaining} problems away from ${label}`; + + row.appendChild(cell1); + row.appendChild(cell2); + row.appendChild(cell3); + return row; +} + +function createRatingTable(solved: number, rating: RatingData): HTMLElement { + const table = document.createElement('table'); + const tableBody = document.createElement('tbody'); + + const headerRow = document.createElement('tr'); + ['Competition', 'Solved', 'Remaining'].forEach(headerText => { + const headerCell = document.createElement('th'); + headerCell.textContent = headerText; + headerRow.appendChild(headerCell); + }); + tableBody.appendChild(headerRow); + + if (rating.place > 100) { + tableBody.appendChild(createTableRow('Top 100', rating.top100, rating.top100 - solved + 1)); + } + if (rating.place > 50) { + tableBody.appendChild(createTableRow('Top 50', rating.top50, rating.top50 - solved + 1)); + } + if (rating.place > 25) { + tableBody.appendChild(createTableRow('Top 25', rating.top25, rating.top25 - solved + 1)); + } + if (rating.place > 10) { + tableBody.appendChild(createTableRow('Top 10', rating.top10, rating.top10 - solved + 1)); + } + if (rating.place > 5) { + tableBody.appendChild(createTableRow('Top 5', rating.top5, rating.top5 - solved + 1)); + } + if (rating.place >= 1) { + tableBody.appendChild(createTableRow('Top 1', rating.top1, rating.top1 - solved + 1)); + } + + table.appendChild(tableBody); + return table; +} + +/** + * Generates HTML for the rating table. + * @param title - The title of the rating table. + * @param solved - The number of solved problems. + * @param rating - The rating data. + * @returns The generated HTMLElement (rating table container). + */ +export function generateRatingTableHTML(url: string, title: string, solved: number, rating: RatingData): HTMLElement { + const ratingContainer = document.createElement('div'); + + // Header with link + const header = document.createElement('h2'); + const textNode = document.createTextNode(`Progress in `); + header.appendChild(textNode); + const link = document.createElement('a'); + link.href = url; + link.textContent = `the ${title}'s rating`; + header.appendChild(link); + ratingContainer.appendChild(header); + + const place = rating.place > 100 ? 'You are not in the Top 100' : rating.place.toString(); + ratingContainer.appendChild(createSectionHeader(`Current place: ${place}`, 'h4')); + ratingContainer.appendChild(createSectionHeader(`Solved problems: ${solved}`, 'h5')); + ratingContainer.appendChild(createRatingTable(solved, rating)); + + return ratingContainer; +} + diff --git a/helpers/types.ts b/helpers/types.ts index 606e849..3bcd13a 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -66,6 +66,14 @@ export class AwardBlockData { public name: string, public awards: AwardData[] ) {} + + /** + * Calculates the number of completed awards in this block. + * @returns The number of completed awards. + */ + getCompletedCount(): number { + return this.awards.filter(award => award.isCompleted).length; + } } /** diff --git a/main.ts b/main.ts index 3e82096..4858748 100644 --- a/main.ts +++ b/main.ts @@ -9,20 +9,13 @@ export default class ProjectEulerStatsPlugin extends Plugin { this.addSettingTab(new ProjectEulerStatsSettingTab(this.app, this)); - this.registerDomEvent(document, 'click', (evt: MouseEvent) => { - console.log('click', evt); - }); - - this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); - const session = this.settings.session_id; const keep_alive = this.settings.keep_alive; - this.registerMarkdownCodeBlockProcessor('euler-stats', async (source, el, ctx) => { + this.registerMarkdownCodeBlockProcessor('euler-stats', async (source, el, ctx) => { const stats = await fetchProgress(session, keep_alive); - const container = el.createEl('div'); - container.innerHTML = stats; + container.appendChild(stats); }); } diff --git a/manifest.json b/manifest.json index fdcd5d4..2eb4e84 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "project-euler-stats", "name": "Project Euler Stats", - "version": "1.0.0", + "version": "1.0.1", "minAppVersion": "0.15.0", "description": "Plugin for getting statistics from ProjectEuler.", "author": "Artem Korsakov",