AI Implementation feature(863): Challenges Page and Challenge Solve Modal (#45)

This commit was merged in pull request #45.
This commit is contained in:
2026-07-23 00:13:59 +00:00
parent dcda3dfd4d
commit 0bd1d9472a
40 changed files with 4710 additions and 130 deletions
@@ -0,0 +1,58 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BoardCard } from './challenges.pure';
@Component({
selector: 'app-challenge-card',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
styles: [`
:host { display: block; }
.card {
display: flex; flex-direction: column; gap: 6px;
padding: 10px 12px; border: 1px solid var(--color-border, #2a2f3a);
border-radius: var(--radius-sm, 4px); background: var(--color-surface, #1c1f26);
cursor: pointer; transition: transform 0.1s ease;
min-height: 96px;
}
.card:hover { transform: translateY(-1px); border-color: var(--color-primary, #3b82f6); }
.card.solved { border-color: var(--color-success, #22c55e); }
.card-head { display: flex; align-items: center; gap: 8px; }
.icon { width: 28px; height: 28px; object-fit: contain; }
.name { font-weight: 600; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.check { color: var(--color-success, #22c55e); font-size: 18px; }
.meta { display: flex; gap: 8px; font-size: 12px; opacity: 0.85; align-items: center; }
.points { font-weight: 600; }
.diff {
display: inline-block; padding: 1px 6px; border-radius: 999px; font-size: 10px; letter-spacing: 0.04em;
}
.diff-LOW { background: rgba(34,197,94,0.18); color: #16a34a; }
.diff-MEDIUM { background: rgba(234,179,8,0.18); color: #ca8a04; }
.diff-HIGH { background: rgba(239,68,68,0.18); color: #dc2626; }
`],
template: `
<div
class="card"
[class.solved]="card.solvedByMe"
[attr.data-testid]="'challenge-card-' + card.id"
(click)="open.emit(card.id)"
>
<div class="card-head">
<img *ngIf="card.categoryIconPath" class="icon" [src]="card.categoryIconPath" [alt]="card.categoryAbbreviation" />
<span class="name">{{ card.name }}</span>
<span *ngIf="card.solvedByMe" class="check" aria-label="Solved">✓</span>
</div>
<div class="meta">
<span class="diff" [class]="'diff-' + card.difficulty">{{ card.difficulty }}</span>
<span class="points" [attr.data-testid]="'points-' + card.id">{{ card.livePoints }} pts</span>
<span>·</span>
<span>{{ card.solveCount }} solve<span *ngIf="card.solveCount !== 1">s</span></span>
</div>
</div>
`,
})
export class ChallengeCardComponent {
@Input({ required: true }) card!: BoardCard;
@Output() open = new EventEmitter<string>();
}