Add progress bar

This commit is contained in:
Artem Korsakov 2025-03-14 09:37:04 +03:00
parent 3ebeb576d5
commit 41061a1873
5 changed files with 130 additions and 50 deletions

View file

@ -33,23 +33,35 @@ export function generateImageHTML(account: string): string {
return `<img src="${imageSrc}" alt="Profile ${account}" title="${account}">`;
}
export function determineTop(solved: number, rating: RatingData): string {
interface Result {
message: string;
percentage: number;
}
export function determineTop(solved: number, rating: RatingData): Result {
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' },
{ 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 (const { limit, top, message } of thresholds) {
for (let i = 0; i < thresholds.length; i++) {
const { limit, top, title } = thresholds[i];
if (rating.place > limit) {
return `${top - solved + 1} problems away from ${message}`;
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 'You are in the Top 1';
return { message: 'You are in the Top 1', percentage: 100 };
}
/**
@ -68,6 +80,17 @@ export function findAwardWithMaxMembers(awardBlocks: AwardBlockData[]): AwardDat
}, 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.
@ -91,25 +114,44 @@ export function generateProgressTableHTML(
): 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}</li>` : '';
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></tr>
<tr><td>Progress</td><td>${progressData.progress}</td></tr>
<tr><td>Place in <a href="https://projecteuler.net/eulerians">Eulerians</a></td><td>${euleriansPlace}</td></tr>
<tr><td>Place in <a href="${locationUrl}">${accountData.location}</a></td><td>${locationPlace}</td></tr>
<tr><td>Place in <a href="${languageUrl}">${accountData.language}</a></td><td>${languagePlace}</td></tr>
<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 class="next-goal">${progressData.level}: ${progressData.toTheNext}</li>
<li>${accountData.location}: ${determineTop(progressData.solved, locationRating)}</li>
<li>${accountData.language}: ${determineTop(progressData.solved, languageRating)}</li>
<li>${progressData.level}: ${progressData.toTheNext}. ${toTheNextPercentage}</li>
${locationLiElement}
${languageLiElement}
${awardPlace}
</ul>
`;
@ -200,16 +242,19 @@ export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string {
const length = awards.length;
// Подсчитываем завершенные и создаем строки таблицы за один проход
const { completed, rows } = awards.reduce((acc, { award, link, description, isCompleted, progress, members }) => {
const { completed, rows } = awards.reduce((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>${progress}</td>
<td>${members}</td>
<td>${progress}</td>
<td>${progressBar}</td>
</tr>
`);
}
@ -222,7 +267,7 @@ export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string {
<h4>Uncompleted awards: ${length - completed}</h4>
<table>
<tbody>
<tr><th>Award</th><th>Description</th><th>Progress</th><th>Members</th></tr>
<tr><th>Award</th><th>Description</th><th>Members</th><th>Progress</th><th>Progress bar</th></tr>
${rows.join('')}
</tbody>
</table>

View file

@ -12,6 +12,13 @@ export function extractSolvedCount(text: string): number {
return match && match[1] ? parseInt(match[1], 10) : 0;
}
function extractPercentage(input: string): number {
const regex = /(\d+(\.\d+)?)%/;
const match = input.match(regex);
return match ? parseFloat(match[1]) : 0;
}
/**
* Converts a string to a number by removing non-numeric characters.
* @param input - The input string.
@ -52,9 +59,10 @@ export function parseProgressData(html: string): ProgressData {
const level = doc.querySelector('h3#level_text')?.textContent || '';
const progress = doc.querySelector('div#progress_page > h3')?.textContent || '';
const solved = extractSolvedCount(progress);
const percentage = extractPercentage(progress);
const toTheNext = doc.querySelector('#progress_page > .progress_bar_with_threshold > span')?.textContent || '';
return new ProgressData(level, solved, progress, toTheNext);
return new ProgressData(level, solved, percentage, progress, toTheNext);
}
/**
@ -108,6 +116,22 @@ export function parseLevelData(html: string): LevelData[] {
});
}
function calculatePercentage(input: string): number {
const regex = /(\d+)\s*\/\s*(\d+)/;
const match = input.match(regex);
if (match) {
const solved = parseInt(match[1], 10);
const total = parseInt(match[2], 10);
if (total > 0) {
return ((solved / total) * 100).toFixed(2);
}
}
return 0;
}
export function parseAwardsData(myAwardsHtml: string, awardsHtml: string): AwardBlockData[] {
const parser = new DOMParser();
const myAwardsDoc = parser.parseFromString(myAwardsHtml, 'text/html');
@ -131,11 +155,12 @@ export function parseAwardsData(myAwardsHtml: string, awardsHtml: string): Award
const description = tooltipTextParts[0].replace(award, '').replace('Completed', '').trim();
const isCompleted = tooltipText.endsWith('Completed');
const progress = tooltipTextParts[1]?.trim() || '';
const percentage = calculatePercentage(progress);
const membersSource = membersTextArray.find(memberText => memberText.startsWith(award)) ?? '\n0 members';
const members = membersSource.split('\n').pop() || '0 members';
return new AwardData(award, link, description, isCompleted, progress, members);
return new AwardData(award, link, description, isCompleted, progress, percentage, members);
});
return new AwardBlockData(name, awards);

View file

@ -17,6 +17,7 @@ export class ProgressData {
constructor(
public level: string,
public solved: number,
public percentage: number,
public progress: string,
public toTheNext: string
) {}
@ -78,6 +79,7 @@ export class AwardData {
* @param description - A description of the award.
* @param isCompleted - A boolean indicating whether the award has been completed.
* @param progress - A string representing the progress associated with the award.
* @param percentage - A number representing the percentage of completion.
* @param members - A string representing the members associated with this award.
*/
constructor(
@ -86,6 +88,7 @@ export class AwardData {
public description: string,
public isCompleted: boolean,
public progress: string,
public percentage: number,
public members: string
) {}
}

14
main.ts
View file

@ -16,20 +16,6 @@ export default class ProjectEulerStatsPlugin extends Plugin {
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
this.registerMarkdownCodeBlockProcessor('euler-stats-profile', (source, el, ctx) => {
const matchingLine = source.split('\n').find((line) => line.startsWith("account="));
if (matchingLine) {
const account = matchingLine.split("=")[1].trim();
const imgElement = el.createEl('img');
imgElement.src = 'https://projecteuler.net/profile/' + account + '.png';
imgElement.alt = 'Profile ' + account;
} else {
const divElement = el.createEl('div');
divElement.textContent = 'The "account=" parameter is not set or is set incorrectly!';
}
});
const session = this.settings.session_id;
const keep_alive = this.settings.keep_alive;
const cookies = 'PHPSESSID=' + session + '; keep_alive=' + keep_alive;

View file

@ -120,24 +120,12 @@ h5 {
/* Медиа-запросы для адаптивности */
@media (max-width: 768px) {
.progress-bar-container {
margin: 10px 0;
}
.tasks-list {
padding: 10px;
}
.award-row {
padding: 10px;
}
}
@media (max-width: 480px) {
.progress-bar-height {
height: 15px;
}
.profile-table td {
padding: 8px;
font-size: 14px;
@ -147,3 +135,36 @@ h5 {
#your-account td {
background-color: #90ee90; /* Светло-зеленый цвет */
}
/* Контейнер для progress bar */
.progress-container {
width: 100%; /* Занимает всю ширину ячейки таблицы */
background-color: #e0e0e0; /* Фон контейнера */
border-radius: 4px; /* Закругленные углы */
overflow: hidden; /* Скрываем содержимое, выходящее за пределы */
height: 12px; /* Компактная высота */
position: relative; /* Для позиционирования текста */
}
/* Сам progress bar */
.progress-bar {
position: absolute; /* Позиционируем текст поверх progress bar */
top: 0%; /* Выравниваем по вертикали */
left: 0%; /* Выравниваем по горизонтали */
height: 100%; /* Занимает всю высоту контейнера */
background-color: #1abc9c; /* Цвет прогресса */
border-radius: 4px; /* Закругленные углы */
padding: 0; /* Отсутствие отступов */
}
/* Текст с процентом */
.progress-text {
position: absolute; /* Позиционируем текст поверх progress bar */
top: 50%; /* Выравниваем по вертикали */
left: 50%; /* Выравниваем по горизонтали */
transform: translate(-50%, -50%); /* Центрируем текст */
color: #2c3e50; /* Цвет текста */
font-size: 10px; /* Компактный размер шрифта */
font-weight: bold; /* Жирный шрифт */
white-space: nowrap; /* Текст не переносится */
}