cubo widget tweaks

This commit is contained in:
Mononaut
2025-07-14 10:30:10 +00:00
parent 1878318faa
commit 3d5c7bf21f
4 changed files with 158 additions and 20 deletions

View File

@@ -334,6 +334,7 @@
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-simpleproof-cubo-widget [label]="widget.props.label" [key]="widget.props.key" [widget]="true"></app-simpleproof-cubo-widget>
<div style="margin-top: -0.6rem;"><a [routerLink]="['/sp/cubo' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View File

@@ -29,10 +29,22 @@
i18n-placeholder="simpleproof.search_placeholder"
>
</div>
<table class="table table-borderless" [class.table-fixed]="widget">
<table class="table table-borderless table-fixed">
<thead>
<th class="filename text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.student_name">Student Name</th>
<th class="id text-left" [ngClass]="{'widget': widget}" i18n="simpleproof.id_code">ID</th>
<th class="student text-left sortable" [ngClass]="{'widget': widget}" (click)="$event.stopPropagation(); sortBy('student_name')" style="cursor: pointer;">
<span i18n="simpleproof.student_name">Student Name</span>
<div class="sort-icons ml-1">
<fa-icon [icon]="['fas', 'caret-up']" [fixedWidth]="true" [class.active]="sortField === 'student_name' && sortDirection === 'asc'" (click)="$event.stopPropagation(); sortBy('student_name', 'asc')"></fa-icon>
<fa-icon [icon]="['fas', 'caret-down']" [fixedWidth]="true" [class.active]="sortField === 'student_name' && sortDirection === 'desc'" (click)="$event.stopPropagation(); sortBy('student_name', 'desc')"></fa-icon>
</div>
</th>
<th class="id text-left sortable" [ngClass]="{'widget': widget}" (click)="$event.stopPropagation(); sortBy('id_code')" style="cursor: pointer;">
<span i18n="simpleproof.id_code">ID</span>
<div class="sort-icons ml-1">
<fa-icon [icon]="['fas', 'caret-up']" [fixedWidth]="true" [class.active]="sortField === 'id_code' && sortDirection === 'asc'" (click)="$event.stopPropagation(); sortBy('id_code', 'asc')"></fa-icon>
<fa-icon [icon]="['fas', 'caret-down']" [fixedWidth]="true" [class.active]="sortField === 'id_code' && sortDirection === 'desc'" (click)="$event.stopPropagation(); sortBy('id_code', 'desc')"></fa-icon>
</div>
</th>
<th class="proof text-right" [ngClass]="{'widget': widget}" i18n="simpleproof.proof">Proof</th>
</thead>
<tbody *ngIf="verifiedPage; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
@@ -50,19 +62,34 @@
</td>
</tr>
<tr *ngFor="let item of verifiedPage">
<td class="filename text-left" [class]="widget ? 'widget' : ''">{{ item.student_name }}</td>
<td class="id text-left" [class]="widget ? 'widget' : ''">
@if (item.sanitized_download_url) {
<a [href]="item.sanitized_download_url" target="_blank">
<span>{{ item.id_code }}</span>
<span class="icon ml-2">
<fa-icon [icon]="['fas', 'file-pdf']" [fixedWidth]="true"></fa-icon>
</span>
</a>
} @else {
{{ item.id_code }}
}
</td>
@if (widget) {
<td class="student text-left" [class]="widget ? 'widget' : ''">
@if (item.sanitized_download_url) {
<a [href]="item.sanitized_download_url" target="_blank">
<span>{{ item.student_name }}</span>
<span class="icon ml-2">
<fa-icon [icon]="['fas', 'file-pdf']" [fixedWidth]="true"></fa-icon>
</span>
</a>
} @else {
{{ item.student_name }}
}
</td>
} @else {
<td class="student text-left" [class]="widget ? 'widget' : ''">{{ item.student_name }}</td>
<td class="id text-left" [class]="widget ? 'widget' : ''">
@if (item.sanitized_download_url) {
<a [href]="item.sanitized_download_url" target="_blank">
<span>{{ item.id_code }}</span>
<span class="icon ml-2">
<fa-icon [icon]="['fas', 'file-pdf']" [fixedWidth]="true"></fa-icon>
</span>
</a>
} @else {
{{ item.id_code }}
}
</td>
}
<td class="proof text-right" [class]="widget ? 'widget' : ''">
<a [href]="item.sanitized_simpleproof_url" target="_blank" class="badge badge-primary badge-verify" style="font-size: 1.035em;">
<span class="icon">

View File

@@ -13,6 +13,9 @@ export interface SimpleProofCubo {
parsed?: { type: string; year: number; studentNumber: number };
}
export type SortField = 'student_name' | 'id_code' | null;
export type SortDirection = 'asc' | 'desc';
@Component({
selector: 'app-simpleproof-cubo-widget',
templateUrl: './simpleproof-cubo-widget.component.html',
@@ -35,6 +38,8 @@ export class SimpleProofCuboWidgetComponent implements OnChanges {
lastPage = 1;
itemsPerPage = 15;
paginationMaxSize = window.innerWidth <= 767.98 ? 3 : 5;
sortField: SortField = null;
sortDirection: SortDirection = 'asc';
constructor(
private servicesApiService: ServicesApiServices,
@@ -122,6 +127,11 @@ export class SimpleProofCuboWidgetComponent implements OnChanges {
} else {
this.filteredVerified = this.verified;
}
if (this.sortField) {
this.applySort();
}
this.page = 1;
this.updatePage();
}
@@ -134,4 +144,56 @@ export class SimpleProofCuboWidgetComponent implements OnChanges {
this.page = page;
this.updatePage();
}
sortBy(field: SortField, direction?: SortDirection): boolean {
if (field && direction) {
this.sortField = field;
this.sortDirection = direction;
} else {
if (this.sortField === field) {
if (this.sortDirection === 'asc') {
this.sortDirection = 'desc';
} else {
this.sortField = null;
}
} else {
this.sortField = field;
this.sortDirection = 'asc';
}
}
this.applySort();
this.page = 1;
this.updatePage();
return false;
}
applySort(): void {
// default to ascending sort by id
const sortByField = this.sortField || 'id_code';
const sortDirection = this.sortField ? this.sortDirection : 'asc';
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
this.filteredVerified.sort((a, b) => {
let comparison = 0;
if (sortByField === 'student_name') {
comparison = collator.compare(a.student_name, b.student_name);
} else if (sortByField === 'id_code') {
// For ID sorting, try to use the parsed Cubo key logic first
if (a.parsed && b.parsed) {
if (a.parsed.year !== b.parsed.year) {
comparison = a.parsed.year - b.parsed.year;
} else if (a.parsed.type !== b.parsed.type) {
comparison = a.parsed.type.localeCompare(b.parsed.type);
} else {
comparison = a.parsed.studentNumber - b.parsed.studentNumber;
}
} else {
comparison = collator.compare(a.id_code, b.id_code);
}
}
return sortDirection === 'asc' ? comparison : -comparison;
});
}
}

View File

@@ -60,6 +60,14 @@ tr, td, th {
white-space: nowrap;
}
.student {
width: 50%;
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hash {
width: 20%;
max-width: 700px;
@@ -70,9 +78,7 @@ tr, td, th {
td.hash {
font-family: monospace;
}
.widget .hash {
display: none;
}
@media (max-width: 1200px) {
.hash {
display: none;
@@ -93,10 +99,52 @@ td.verified {
}
.proof {
width: 25%;
width: 120px;
padding-left: 0;
}
.widget {
&.id, &.hash {
display: none;
}
&.student {
width: 100%;
}
&.proof {
width: 120px;
}
}
.sortable {
user-select: none;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
.sort-icons {
display: inline-flex;
line-height: 1;
flex-direction: column;
width: 1em;
height: 100%;
align-items: center;
justify-content: center;
vertical-align: middle;
fa-icon {
transition: color 0.2s ease;
margin: -0.25em 0;
pointer-events: all;
color: var(--secondary);
&.active {
color: var(--info);
}
}
}
.badge-verify {
font-size: 1.05em;
font-weight: normal;