mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
fix(stats): fix inconsistent stat card styling and add median/session stats to completion race
This commit is contained in:
@@ -35,16 +35,26 @@
|
||||
<span class="stat-value">{{ totalBooks }}</span>
|
||||
<span class="stat-label">Books</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ totalSessions }}</span>
|
||||
<span class="stat-label">Sessions</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ avgDaysToFinish }}d</span>
|
||||
<span class="stat-label">Avg Days</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ medianDaysToFinish }}d</span>
|
||||
<span class="stat-label">Median Days</span>
|
||||
</div>
|
||||
<div class="stat-card fastest">
|
||||
<span class="stat-value-sm">{{ fastestCompletion }}</span>
|
||||
<span class="stat-value">{{ fastestDays }}</span>
|
||||
<span class="stat-subtitle">{{ fastestTitle }}</span>
|
||||
<span class="stat-label">Fastest</span>
|
||||
</div>
|
||||
<div class="stat-card slowest">
|
||||
<span class="stat-value-sm">{{ slowestCompletion }}</span>
|
||||
<span class="stat-value">{{ slowestDays }}</span>
|
||||
<span class="stat-subtitle">{{ slowestTitle }}</span>
|
||||
<span class="stat-label">Slowest</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,12 +97,15 @@
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.stat-value-sm {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #ffffff);
|
||||
.stat-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary-color);
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@@ -113,11 +116,11 @@
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
&.fastest .stat-value-sm {
|
||||
&.fastest .stat-value {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.slowest .stat-value-sm {
|
||||
&.slowest .stat-value {
|
||||
color: #ff9800;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,13 @@ export class CompletionRaceChartComponent implements OnInit, OnDestroy {
|
||||
public chartOptions: ChartConfiguration<'line'>['options'];
|
||||
|
||||
public totalBooks = 0;
|
||||
public fastestCompletion = '';
|
||||
public slowestCompletion = '';
|
||||
public avgDaysToFinish = 0;
|
||||
public medianDaysToFinish = 0;
|
||||
public totalSessions = 0;
|
||||
public fastestDays = '';
|
||||
public fastestTitle = '';
|
||||
public slowestDays = '';
|
||||
public slowestTitle = '';
|
||||
|
||||
private readonly userStatsService = inject(UserStatsService);
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
@@ -189,16 +193,26 @@ export class CompletionRaceChartComponent implements OnInit, OnDestroy {
|
||||
this.totalBooks = races.length;
|
||||
|
||||
if (races.length > 0) {
|
||||
const days = races.map(r => r.totalDays);
|
||||
const days = races.map(r => r.totalDays).sort((a, b) => a - b);
|
||||
const fastest = races.reduce((a, b) => a.totalDays <= b.totalDays ? a : b);
|
||||
const slowest = races.reduce((a, b) => a.totalDays >= b.totalDays ? a : b);
|
||||
this.fastestCompletion = `${fastest.bookTitle.substring(0, 25)}${fastest.bookTitle.length > 25 ? '...' : ''} (${fastest.totalDays}d)`;
|
||||
this.slowestCompletion = `${slowest.bookTitle.substring(0, 25)}${slowest.bookTitle.length > 25 ? '...' : ''} (${slowest.totalDays}d)`;
|
||||
this.avgDaysToFinish = Math.round(days.reduce((a, b) => a + b, 0) / days.length);
|
||||
this.medianDaysToFinish = days.length % 2 === 0
|
||||
? Math.round((days[days.length / 2 - 1] + days[days.length / 2]) / 2)
|
||||
: days[Math.floor(days.length / 2)];
|
||||
this.totalSessions = races.reduce((sum, r) => sum + r.sessions.length, 0);
|
||||
this.fastestDays = `${fastest.totalDays}d`;
|
||||
this.fastestTitle = fastest.bookTitle.length > 25 ? fastest.bookTitle.substring(0, 25) + '...' : fastest.bookTitle;
|
||||
this.slowestDays = `${slowest.totalDays}d`;
|
||||
this.slowestTitle = slowest.bookTitle.length > 25 ? slowest.bookTitle.substring(0, 25) + '...' : slowest.bookTitle;
|
||||
} else {
|
||||
this.fastestCompletion = '-';
|
||||
this.slowestCompletion = '-';
|
||||
this.avgDaysToFinish = 0;
|
||||
this.medianDaysToFinish = 0;
|
||||
this.totalSessions = 0;
|
||||
this.fastestDays = '-';
|
||||
this.fastestTitle = '';
|
||||
this.slowestDays = '-';
|
||||
this.slowestTitle = '';
|
||||
}
|
||||
|
||||
const datasets = races.map((race, i) => {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<span class="stat-label">Total Read</span>
|
||||
</div>
|
||||
<div class="stat-card type">
|
||||
<span class="stat-value-sm">{{ readerType }}</span>
|
||||
<span class="stat-value">{{ readerType }}</span>
|
||||
<span class="stat-label">Reader Type</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,12 +58,6 @@
|
||||
color: var(--text-color, #ffffff);
|
||||
}
|
||||
|
||||
.stat-value-sm {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #ffffff);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary-color);
|
||||
@@ -80,7 +74,7 @@
|
||||
color: #42a5f5;
|
||||
}
|
||||
|
||||
&.type .stat-value-sm {
|
||||
&.type .stat-value {
|
||||
color: #9c27b0;
|
||||
}
|
||||
}
|
||||
@@ -89,7 +83,8 @@
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
height: 350px;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
|
||||
@@ -25,11 +25,12 @@
|
||||
<span class="stat-label">Completion Rate</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value-sm">{{ medianDropout }}</span>
|
||||
<span class="stat-value">{{ medianDropout }}</span>
|
||||
<span class="stat-label">Median Dropout</span>
|
||||
</div>
|
||||
<div class="stat-card danger">
|
||||
<span class="stat-value-sm">{{ dangerZone }}</span>
|
||||
<span class="stat-value">{{ dangerZoneRange }}</span>
|
||||
<span class="stat-subtitle">{{ dangerZoneDrop }}</span>
|
||||
<span class="stat-label">Danger Zone</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,10 +58,9 @@
|
||||
color: #e91e63;
|
||||
}
|
||||
|
||||
.stat-value-sm {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #ffffff);
|
||||
.stat-subtitle {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -73,7 +72,11 @@
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
&.danger .stat-value-sm {
|
||||
&.danger .stat-value {
|
||||
color: #ff5722;
|
||||
}
|
||||
|
||||
&.danger .stat-subtitle {
|
||||
color: #ff5722;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ export class ReadingSurvivalChartComponent implements OnInit, OnDestroy {
|
||||
public totalStarted = 0;
|
||||
public completionRate = 0;
|
||||
public medianDropout = '';
|
||||
public dangerZone = '';
|
||||
public dangerZoneRange = '';
|
||||
public dangerZoneDrop = '';
|
||||
|
||||
public readonly chartOptions: ChartConfiguration<'line'>['options'] = {
|
||||
responsive: true,
|
||||
@@ -156,7 +157,8 @@ export class ReadingSurvivalChartComponent implements OnInit, OnDestroy {
|
||||
dangerIdx = i;
|
||||
}
|
||||
}
|
||||
this.dangerZone = `${THRESHOLDS[dangerIdx - 1]}-${THRESHOLDS[dangerIdx]}% (-${maxDrop.toFixed(0)}%)`;
|
||||
this.dangerZoneRange = `${THRESHOLDS[dangerIdx - 1]}-${THRESHOLDS[dangerIdx]}%`;
|
||||
this.dangerZoneDrop = `-${maxDrop.toFixed(0)}%`;
|
||||
|
||||
const labels = THRESHOLDS.map(t => `${t}%`);
|
||||
this.chartDataSubject.next({
|
||||
|
||||
Reference in New Issue
Block a user