From 4045f1baf5b2ec0d38e07af01e3e5aff7652c4f4 Mon Sep 17 00:00:00 2001 From: Artem Korsakov Date: Thu, 13 Mar 2025 09:26:43 +0300 Subject: [PATCH] Finish progress table --- helpers/fetchProgress.ts | 23 ++++++---- helpers/htmlGenerators.ts | 92 +++++++++++++++++++++++++++++++++++---- helpers/parsers.ts | 19 +++++++- helpers/types.ts | 10 +++++ main.ts | 2 + 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/helpers/fetchProgress.ts b/helpers/fetchProgress.ts index e80b30c..d321849 100644 --- a/helpers/fetchProgress.ts +++ b/helpers/fetchProgress.ts @@ -5,7 +5,8 @@ import { parseProgressData, parseRatingData, parseEuleriansData, - parseLevelData + parseLevelData, + parseFriends } from './parsers'; import { generateProfileHTML, @@ -13,7 +14,8 @@ import { generateProgressTableHTML, generateRatingTableHTML, generateLevelsTableHTML, - generateAwardsTableHTML + generateAwardsTableHTML, + generateFriendsHTML } from './htmlGenerators'; import { AccountData, @@ -34,13 +36,14 @@ export async function fetchProgress(cookies: string): Promise { const progressData = await fetchData('https://projecteuler.net/progress', parseProgressData, cookies); const euleriansPlace = await fetchData('https://projecteuler.net/eulerians', parseEuleriansData, cookies); const levelDataArray = await fetchData('https://projecteuler.net/levels', parseLevelData, cookies); + const friends = await fetchData('https://projecteuler.net/friends', parseFriends, cookies); const locationUrl = `https://projecteuler.net/location=${accountData.location}`; const languageUrl = `https://projecteuler.net/language=${accountData.language}`; const { locationRating, languageRating } = await fetchRatings(locationUrl, languageUrl, cookies); const awardsData = await fetchAwardsData(cookies); - return generateHTML(accountData, progressData, euleriansPlace, locationUrl, languageUrl, locationRating, languageRating, levelDataArray, awardsData); + return generateHTML(accountData, progressData, euleriansPlace, locationUrl, languageUrl, locationRating, languageRating, levelDataArray, awardsData, friends); } catch (error) { return `
Error fetching progress: ${error.message}.
`; } @@ -73,21 +76,25 @@ 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[]): string { +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); + const progressHTML = generateProgressTableHTML(accountData, progressData, locationUrl, languageUrl, euleriansPlace, locationRating, languageRating, awardsData); console.log("Successfully generated progress HTML."); - const locationRatingHTML = generateRatingTableHTML(`Progress in the ${accountData.location}'s rating`, progressData.solved, locationRating); + const locationTitle = `Progress in the ${accountData.location}'s rating` + const locationRatingHTML = generateRatingTableHTML(locationTitle, progressData.solved, locationRating); console.log("Successfully generated location rating HTML."); - const languageRatingHTML = generateRatingTableHTML(`Progress in the ${accountData.language}'s rating`, progressData.solved, languageRating); + 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."); - return profileHTML + imageHTML + progressHTML + locationRatingHTML + languageRatingHTML + levelsHTML + awardsHTML; + return profileHTML + imageHTML + progressHTML + locationRatingHTML + languageRatingHTML + levelsHTML + awardsHTML + friendsHTML; } diff --git a/helpers/htmlGenerators.ts b/helpers/htmlGenerators.ts index 9669a52..6247303 100644 --- a/helpers/htmlGenerators.ts +++ b/helpers/htmlGenerators.ts @@ -1,5 +1,5 @@ import { stringToNumber } from './parsers'; -import { AccountData, AwardData, AwardBlockData, ProgressData, RatingData, LevelData } from './types'; +import { AccountData, AwardData, AwardBlockData, FriendData, ProgressData, RatingData, LevelData } from './types'; /** * Generates HTML for the profile section. @@ -33,6 +33,41 @@ export function generateImageHTML(account: string): string { return `Profile ${account}`; } +export function determineTop(solved: number, rating: RatingData): string { + const thresholds = [ + { limit: 100, top: rating.top100, message: 'Top 100' }, + { limit: 50, top: rating.top50, message: 'Top 50' }, + { limit: 25, top: rating.top25, message: 'Top 25' }, + { limit: 10, top: rating.top10, message: 'Top 10' }, + { limit: 5, top: rating.top5, message: 'Top 5' }, + { limit: 1, top: rating.top1, message: 'Top 1' }, + ]; + + for (const { limit, top, message } of thresholds) { + if (rating.place > limit) { + return `${top - solved + 1} problems away from ${message}`; + } + } + + return 'You are in the Top 1'; +} + +/** + * Функция для нахождения 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); +} + /** * Generates HTML for the progress table. * @param accountData - The account data. @@ -51,10 +86,13 @@ export function generateProgressTableHTML( languageUrl: string, euleriansPlace: string, locationRating: RatingData, - languageRating: 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 award = findAwardWithMaxMembers(awardsData); + const awardPlace = award ? `
  • Most unresolved award: ${award.award} (${award.description}) by ${award.members}
  • ` : ''; return `

    Progress

    @@ -62,12 +100,18 @@ export function generateProgressTableHTML( CompetitionStatus Progress${progressData.progress} - To the next level${progressData.toTheNext} Place in Eulerians${euleriansPlace} Place in ${accountData.location}${locationPlace} Place in ${accountData.language}${languagePlace} +

    Tasks

    +
      +
    • ${progressData.level}: ${progressData.toTheNext}
    • +
    • ${accountData.location}: ${determineTop(progressData.solved, locationRating)}
    • +
    • ${accountData.language}: ${determineTop(progressData.solved, languageRating)}
    • + ${awardPlace} +
    `; } @@ -81,11 +125,11 @@ export function generateProgressTableHTML( 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` : ''; + 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}

    @@ -191,3 +235,35 @@ export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string { `; } +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 rank = friend.username === accountData.alias ? `${friend.rank}` : friend.rank; + const link = `${friend.username}`; + const username = friend.username === accountData.alias ? `${link}` : link; + 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 ` + + ${rank} + ${username} + ${solved} + ${level} + ${awards} + + `; + }); + + return ` +

    Friends

    + + + + ${rows.join('')} + +
    RankUsernameSolvedLevelAwards
    + `; +} diff --git a/helpers/parsers.ts b/helpers/parsers.ts index c0b49ad..bc20b8a 100644 --- a/helpers/parsers.ts +++ b/helpers/parsers.ts @@ -1,4 +1,4 @@ -import { AccountData, AwardData, AwardBlockData, ProgressData, RatingData, LevelData } from './types'; +import { AccountData, AwardData, AwardBlockData, FriendData, ProgressData, RatingData, LevelData } from './types'; /** * Extracts the number of solved problems from a progress string. @@ -143,3 +143,20 @@ export function parseAwardsData(myAwardsHtml: string, awardsHtml: string): Award return awardBlocks; } + +export function parseFriends(html: string): FriendData[] { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + const rows = doc.querySelectorAll('#friends_table > tbody > tr'); + + return Array.from(rows).slice(1).map(row => { + const rank = row.querySelector('td.rank_column')?.textContent || ''; + const username = row.querySelector('td.username_column a')?.textContent || ''; + const solved = stringToNumber(row.querySelector('td.solved_column')?.textContent || '0'); + const level = stringToNumber(row.querySelector('td.level_column')?.textContent || '0'); + const awards = stringToNumber(row.querySelector('td.awards_column')?.textContent || '0'); + + return new FriendData(rank, username, solved, level, awards); + }); +} diff --git a/helpers/types.ts b/helpers/types.ts index 480b8b4..62ed4e0 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -89,3 +89,13 @@ export class AwardData { public members: string ) {} } + +export class FriendData { + constructor( + public rank: string, + public username: string, + public solved: number, + public level: number, + public awards: number + ) {} +} diff --git a/main.ts b/main.ts index 9b86f09..f5093c7 100644 --- a/main.ts +++ b/main.ts @@ -37,6 +37,8 @@ export default class ProjectEulerStatsPlugin extends Plugin { this.registerMarkdownCodeBlockProcessor('euler-stats', async (source, el, ctx) => { const stats = await fetchProgress(cookies); + console.log(stats) + const container = el.createEl('div'); container.innerHTML = stats; });