mirror of
https://github.com/artemkorsakov/project-euler-stats.git
synced 2026-07-22 05:42:55 +00:00
Add progress bar
This commit is contained in:
parent
3ebeb576d5
commit
41061a1873
5 changed files with 130 additions and 50 deletions
|
|
@ -33,23 +33,35 @@ export function generateImageHTML(account: string): string {
|
||||||
return `<img src="${imageSrc}" alt="Profile ${account}" title="${account}">`;
|
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 = [
|
const thresholds = [
|
||||||
{ limit: 100, top: rating.top100, message: 'Top 100' },
|
{ limit: 100, top: rating.top100, title: 'Top 100' },
|
||||||
{ limit: 50, top: rating.top50, message: 'Top 50' },
|
{ limit: 50, top: rating.top50, title: 'Top 50' },
|
||||||
{ limit: 25, top: rating.top25, message: 'Top 25' },
|
{ limit: 25, top: rating.top25, title: 'Top 25' },
|
||||||
{ limit: 10, top: rating.top10, message: 'Top 10' },
|
{ limit: 10, top: rating.top10, title: 'Top 10' },
|
||||||
{ limit: 5, top: rating.top5, message: 'Top 5' },
|
{ limit: 5, top: rating.top5, title: 'Top 5' },
|
||||||
{ limit: 1, top: rating.top1, message: 'Top 1' },
|
{ 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) {
|
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);
|
}, 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.
|
* Generates HTML for the progress table.
|
||||||
* @param accountData - The account data.
|
* @param accountData - The account data.
|
||||||
|
|
@ -91,25 +114,44 @@ export function generateProgressTableHTML(
|
||||||
): string {
|
): string {
|
||||||
const locationPlace = locationRating.place > 100 ? 'You are not in the Top 100' : locationRating.place;
|
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 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 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 `
|
return `
|
||||||
<h2>Progress</h2>
|
<h2>Progress</h2>
|
||||||
|
${progressPercentage}
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><th>Competition</th><th>Status</th></tr>
|
<tr><th>Competition</th><th>Status</th><th>Progress bar</th></tr>
|
||||||
<tr><td>Progress</td><td>${progressData.progress}</td></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></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></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></tr>
|
<tr><td>Place in <a href="${languageUrl}">${accountData.language}</a></td><td>${languagePlace}</td><td>${languagePercentage}</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h3>Tasks</h3>
|
<h3>Tasks</h3>
|
||||||
<ul class="tasks-list">
|
<ul class="tasks-list">
|
||||||
<li class="next-goal">${progressData.level}: ${progressData.toTheNext}</li>
|
<li>${progressData.level}: ${progressData.toTheNext}. ${toTheNextPercentage}</li>
|
||||||
<li>${accountData.location}: ${determineTop(progressData.solved, locationRating)}</li>
|
${locationLiElement}
|
||||||
<li>${accountData.language}: ${determineTop(progressData.solved, languageRating)}</li>
|
${languageLiElement}
|
||||||
${awardPlace}
|
${awardPlace}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
|
|
@ -200,16 +242,19 @@ export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string {
|
||||||
const length = awards.length;
|
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) {
|
if (isCompleted) {
|
||||||
acc.completed++;
|
acc.completed++;
|
||||||
} else {
|
} else {
|
||||||
|
const progressBar = createProgressBar(percentage);
|
||||||
|
|
||||||
acc.rows.push(`
|
acc.rows.push(`
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="${link}">${award}</a></td>
|
<td><a href="${link}">${award}</a></td>
|
||||||
<td>${description}</td>
|
<td>${description}</td>
|
||||||
<td>${progress}</td>
|
|
||||||
<td>${members}</td>
|
<td>${members}</td>
|
||||||
|
<td>${progress}</td>
|
||||||
|
<td>${progressBar}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +267,7 @@ export function generateAwardsTableHTML(awardsData: AwardBlockData[]): string {
|
||||||
<h4>Uncompleted awards: ${length - completed}</h4>
|
<h4>Uncompleted awards: ${length - completed}</h4>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<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('')}
|
${rows.join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@ export function extractSolvedCount(text: string): number {
|
||||||
return match && match[1] ? parseInt(match[1], 10) : 0;
|
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.
|
* Converts a string to a number by removing non-numeric characters.
|
||||||
* @param input - The input string.
|
* @param input - The input string.
|
||||||
|
|
@ -52,9 +59,10 @@ export function parseProgressData(html: string): ProgressData {
|
||||||
const level = doc.querySelector('h3#level_text')?.textContent || '';
|
const level = doc.querySelector('h3#level_text')?.textContent || '';
|
||||||
const progress = doc.querySelector('div#progress_page > h3')?.textContent || '';
|
const progress = doc.querySelector('div#progress_page > h3')?.textContent || '';
|
||||||
const solved = extractSolvedCount(progress);
|
const solved = extractSolvedCount(progress);
|
||||||
|
const percentage = extractPercentage(progress);
|
||||||
const toTheNext = doc.querySelector('#progress_page > .progress_bar_with_threshold > span')?.textContent || '';
|
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[] {
|
export function parseAwardsData(myAwardsHtml: string, awardsHtml: string): AwardBlockData[] {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const myAwardsDoc = parser.parseFromString(myAwardsHtml, 'text/html');
|
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 description = tooltipTextParts[0].replace(award, '').replace('Completed', '').trim();
|
||||||
const isCompleted = tooltipText.endsWith('Completed');
|
const isCompleted = tooltipText.endsWith('Completed');
|
||||||
const progress = tooltipTextParts[1]?.trim() || '';
|
const progress = tooltipTextParts[1]?.trim() || '';
|
||||||
|
const percentage = calculatePercentage(progress);
|
||||||
|
|
||||||
const membersSource = membersTextArray.find(memberText => memberText.startsWith(award)) ?? '\n0 members';
|
const membersSource = membersTextArray.find(memberText => memberText.startsWith(award)) ?? '\n0 members';
|
||||||
const members = membersSource.split('\n').pop() || '0 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);
|
return new AwardBlockData(name, awards);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export class ProgressData {
|
||||||
constructor(
|
constructor(
|
||||||
public level: string,
|
public level: string,
|
||||||
public solved: number,
|
public solved: number,
|
||||||
|
public percentage: number,
|
||||||
public progress: string,
|
public progress: string,
|
||||||
public toTheNext: string
|
public toTheNext: string
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -78,6 +79,7 @@ export class AwardData {
|
||||||
* @param description - A description of the award.
|
* @param description - A description of the award.
|
||||||
* @param isCompleted - A boolean indicating whether the award has been completed.
|
* @param isCompleted - A boolean indicating whether the award has been completed.
|
||||||
* @param progress - A string representing the progress associated with the award.
|
* @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.
|
* @param members - A string representing the members associated with this award.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -86,6 +88,7 @@ export class AwardData {
|
||||||
public description: string,
|
public description: string,
|
||||||
public isCompleted: boolean,
|
public isCompleted: boolean,
|
||||||
public progress: string,
|
public progress: string,
|
||||||
|
public percentage: number,
|
||||||
public members: string
|
public members: string
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
main.ts
14
main.ts
|
|
@ -16,20 +16,6 @@ export default class ProjectEulerStatsPlugin extends Plugin {
|
||||||
|
|
||||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
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 session = this.settings.session_id;
|
||||||
const keep_alive = this.settings.keep_alive;
|
const keep_alive = this.settings.keep_alive;
|
||||||
const cookies = 'PHPSESSID=' + session + '; keep_alive=' + keep_alive;
|
const cookies = 'PHPSESSID=' + session + '; keep_alive=' + keep_alive;
|
||||||
|
|
|
||||||
45
styles.css
45
styles.css
|
|
@ -120,24 +120,12 @@ h5 {
|
||||||
|
|
||||||
/* Медиа-запросы для адаптивности */
|
/* Медиа-запросы для адаптивности */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.progress-bar-container {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tasks-list {
|
.tasks-list {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.award-row {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.progress-bar-height {
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-table td {
|
.profile-table td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
@ -147,3 +135,36 @@ h5 {
|
||||||
#your-account td {
|
#your-account td {
|
||||||
background-color: #90ee90; /* Светло-зеленый цвет */
|
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; /* Текст не переносится */
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue