Fixed the comments

This commit is contained in:
Artem Korsakov 2025-03-16 11:02:32 +03:00
parent d8ea37bfb0
commit f720b25e47
14 changed files with 751 additions and 362 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 89 KiB

95
helpers/awardsBlock.ts Normal file
View file

@ -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;
}

30
helpers/commonBlocks.ts Normal file
View file

@ -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;
}

View file

@ -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<string> {
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<HTMLElement> {
const cookies = `${sessionIdName}=${session}; ${keepAliveName}=${keep_alive}`;
try {
return await tryToFetchProgress(cookies);
} catch (error) {
console.error('Error fetching progress:', error);
return `<div>Error fetching progress. Please update your cookies in the settings and try again.</div>`;
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<string> {
async function tryToFetchProgress(cookies: string): Promise<HTMLElement> {
const [
accountData,
progressData,
@ -104,25 +109,43 @@ async function fetchAwardsData(cookies: string): Promise<AwardBlockData[]> {
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 <a href="${locationUrl}">the ${accountData.location}'s rating</a>`
const locationRatingHTML = generateRatingTableHTML(locationTitle, progressData.solved, locationRating);
console.log("Successfully generated location rating HTML.");
const languageTitle = `Progress in <a href="${languageUrl}">the ${accountData.language}'s rating</a>`
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;
}

89
helpers/friendsBlock.ts Normal file
View file

@ -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;
}

View file

@ -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 `
<h2>Profile</h2>
<table class="profile-table">
<tbody>
<tr><td>Account</td><td>${accountData.account}</td></tr>
<tr><td>Alias</td><td>${accountData.alias}</td></tr>
<tr><td>Location</td><td>${accountData.location}</td></tr>
<tr><td>Language</td><td>${accountData.language}</td></tr>
<tr><td>Level</td><td>${progressData.level}</td></tr>
<tr><td>Solved</td><td>${progressData.solved}</td></tr>
</tbody>
</table>
`;
}
/**
* 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 `<img src="${imageSrc}" alt="Profile ${account}" title="${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 `
<div class="progress-container">
<span class="progress-bar" style="width: ${clampedPercentage}%;"></span>
<span class="progress-text">${clampedPercentage}%</span>
</div>
`;
}
/**
* 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 = `<li>${accountData.location}: ${locationResult.message}. ${createProgressBar(locationResult.percentage)}</li>`;
const languageResult = determineTop(progressData.solved, languageRating)
const languageLiElement = `<li>${accountData.language}: ${languageResult.message}. ${createProgressBar(languageResult.percentage)}</li>`;
const award = findAwardWithMaxMembers(awardsData);
const awardPlace = award ? `<li>Most unresolved award: <a href="${award.link}">${award.award}</a> (${award.description}) by ${award.members}. ${createProgressBar(award.percentage)}</li>` : '';
return `
<h2>Progress</h2>
${progressPercentage}
<table>
<tbody>
<tr><th>Competition</th><th>Status</th><th>Progress bar</th></tr>
<tr><td>Progress</td><td>${progressData.progress}</td><td>${progressPercentage}</td></tr>
<tr><td>Place in <a href="https://projecteuler.net/eulerians">Eulerians</a></td><td>${euleriansPlace}</td><td></td></tr>
<tr><td>Place in <a href="${locationUrl}">${accountData.location}</a></td><td>${locationPlace}</td><td>${locationPercentage}</td></tr>
<tr><td>Place in <a href="${languageUrl}">${accountData.language}</a></td><td>${languagePlace}</td><td>${languagePercentage}</td></tr>
</tbody>
</table>
<h3>Tasks</h3>
<ul class="tasks-list">
<li>${progressData.level}: ${progressData.toTheNext}. ${toTheNextPercentage}</li>
${locationLiElement}
${languageLiElement}
${awardPlace}
</ul>
`;
}
/**
* 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 ? `<tr><td>Top 1</td><td>${rating.top1}</td><td>${rating.top1 - solved + 1} problems away from Top 1</td></tr>` : '';
const top5Row = rating.place > 5 ? `<tr><td>Top 5</td><td>${rating.top5}</td><td>${rating.top5 - solved + 1} problems away from Top 5</td></tr>` : '';
const top10Row = rating.place > 10 ? `<tr><td>Top 10</td><td>${rating.top10}</td><td>${rating.top10 - solved + 1} problems away from Top 10</td></tr>` : '';
const top25Row = rating.place > 25 ? `<tr><td>Top 25</td><td>${rating.top25}</td><td>${rating.top25 - solved + 1} problems away from Top 25</td></tr>` : '';
const top50Row = rating.place > 50 ? `<tr><td>Top 50</td><td>${rating.top50}</td><td>${rating.top50 - solved + 1} problems away from Top 50</td></tr>` : '';
const top100Row = rating.place > 100 ? `<tr><td>Top 100</td><td>${rating.top100}</td><td>${rating.top100 - solved + 1} problems away from Top 100</td></tr>` : '';
return `
<h2>${title}</h2>
<h4>Current place: ${place}</h4>
<h5>Solved problems: ${solved}</h5>
<table>
<tbody>
<tr><th>Competition</th><th>Solved</th><th>Remaining</th></tr>
${top100Row}
${top50Row}
${top25Row}
${top10Row}
${top5Row}
${top1Row}
</tbody>
</table>
`;
}
export function generateLevelsTableHTML(progressData: ProgressData, levelDataArray: LevelData[]): string {
const level = stringToNumber(progressData.level);
// Проверяем, что levelDataArray не пустой и уровень не выходит за пределы массива
if (!levelDataArray.length || level >= levelDataArray.length) {
return `<h2>No levels available</h2>`;
}
// Используем slice для получения подмассива, начиная с текущего уровня
const slicedArray = levelDataArray.slice(level - 1);
// Преобразуем каждый элемент в строку <tr>
const rows = slicedArray.map(levelData => {
const rowLevel = stringToNumber(levelData.level);
const needForLevel = rowLevel * 25;
const remaining = Math.max(0, needForLevel - progressData.solved);
return `
<tr">
<td><a href="https://projecteuler.net/level=${rowLevel}">${levelData.level}</a></td>
<td>${needForLevel}</td>
<td>${remaining}</td>
<td>${levelData.members}</td>
</tr>
`;
}).join('');
const allMembers = slicedArray.reduce((total, levelData) => {
const membersCount = stringToNumber(levelData.members);
return total + (isNaN(membersCount) ? 0 : membersCount);
}, 0);
return `
<h2>Level progress</h2>
<h4>Current level: ${progressData.level}</h4>
<h5>Solved problems: ${progressData.solved}</h5>
<h5>Status: ${allMembers} members have made it this far.</h5>
<table>
<tbody>
<tr><th>Level</th><th>Solve</th><th>Remaining</th><th>Members</th></tr>
${rows}
</tbody>
</table>
`;
}
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(`
<tr>
<td><a href="${link}">${award}</a></td>
<td>${description}</td>
<td>${members}</td>
<td>${progress}</td>
<td>${progressBar}</td>
</tr>
`);
}
return acc;
},
{ completed: 0, rows: [] } // Начальное значение аккумулятора
);
return `
<h3>${awardData.name}</h3>
<h4>Status: Won ${completed} out of ${length}</h4>
<h4>Uncompleted awards: ${length - completed}</h4>
<table>
<tbody>
<tr><th>Award</th><th>Description</th><th>Members</th><th>Progress</th><th>Progress bar</th></tr>
${rows.join('')}
</tbody>
</table>
`;
}).join('');
return `
<h2>Awards</h2>
${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 = `<a href="https://projecteuler.net/progress=${friend.username}">${friend.username}</a>`;
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 `
<tr ${idCss}>
<td>${friend.rank}</td>
<td>${username}</td>
<td>${solved}</td>
<td>${level}</td>
<td>${awards}</td>
</tr>
`;
});
return `
<h2>Friends</h2>
<table>
<tbody>
<tr><th>Rank</th><th>Username</th><th>Solved</th><th>Level</th><th>Awards</th></tr>
${rows.join('')}
</tbody>
</table>
`;
}

86
helpers/levelsBlock.ts Normal file
View file

@ -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;
}

View file

@ -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');

60
helpers/profileBlock.ts Normal file
View file

@ -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;
}

241
helpers/progressBlock.ts Normal file
View file

@ -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;
}

82
helpers/ratingBlock.ts Normal file
View file

@ -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;
}

View file

@ -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;
}
}
/**

11
main.ts
View file

@ -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);
});
}

View file

@ -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",