import {
ChangeDetectionStrategy,
Component,
EventEmitter,
HostListener,
Input,
OnChanges,
Output,
SimpleChanges,
computed,
inject,
signal,
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MarkdownService } from '../../core/services/markdown.service';
import { ChallengesStore } from './challenges.store';
import {
BoardCard,
SolveEventLivePayload,
SolverRow,
formatUtcToLocal,
messageForSolveError,
parseSolveError,
} from './challenges.pure';
import { ApiErrorEnvelope } from './challenges.service';
@Component({
selector: 'app-challenge-modal',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, FormsModule],
styles: [`
.backdrop {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
display: flex; align-items: center; justify-content: center; z-index: 1000;
}
.modal {
width: min(720px, 92vw); max-height: 90vh; overflow: auto;
background: var(--color-surface, #1c1f26); color: var(--color-text, #f4f4f5);
border-radius: var(--radius-md, 8px);
padding: 16px; display: flex; flex-direction: column; gap: 12px;
}
.header { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.header h3 { margin: 0; flex: 1; }
.pill { padding: 2px 8px; border-radius: 999px; font-size: 12px; background: rgba(255,255,255,0.06); }
.pill.diff-LOW { background: rgba(34,197,94,0.18); color: #16a34a; }
.pill.diff-MEDIUM { background: rgba(234,179,8,0.18); color: #ca8a04; }
.pill.diff-HIGH { background: rgba(239,68,68,0.18); color: #dc2626; }
.body { white-space: normal; line-height: 1.45; }
.form { display: flex; gap: 8px; align-items: center; }
.form input { flex: 1; padding: 8px 10px; border-radius: var(--radius-sm, 4px); border: 1px solid var(--color-border, #2a2f3a); background: var(--color-input, rgba(255,255,255,0.04)); color: inherit; }
.actions { display: flex; gap: 8px; justify-content: flex-end; }
.error { color: var(--color-danger, #ef4444); font-size: 14px; }
.notice { color: var(--color-warning, #eab308); font-size: 14px; }
.solvers { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
.solvers li { display: grid; grid-template-columns: 28px 1fr auto auto; gap: 8px; align-items: center; padding: 4px 8px; border-radius: var(--radius-sm, 4px); background: rgba(255,255,255,0.03); }
.rank-1 { color: #fbbf24; font-weight: 700; }
.rank-2 { color: #d4d4d8; font-weight: 700; }
.rank-3 { color: #b45309; font-weight: 700; }
.rank-other { color: var(--color-success, #22c55e); }
.points { font-variant-numeric: tabular-nums; }
.close { background: transparent; border: 1px solid var(--color-border, #2a2f3a); color: inherit; padding: 4px 10px; border-radius: var(--radius-sm, 4px); cursor: pointer; }
.solved-banner { padding: 8px 10px; border-radius: var(--radius-sm, 4px); background: rgba(34,197,94,0.12); color: #16a34a; }
.awarded-banner { padding: 8px 10px; border-radius: var(--radius-sm, 4px); background: rgba(59,130,246,0.12); color: #60a5fa; }
`],
template: `
You solved this challenge.
Awarded {{ a.awardedPoints }} pts
0">(base {{ a.basePoints }} + rank bonus {{ a.rankBonus }})
{{ n }}
{{ e }}
Solvers ({{ solvers().length }})
-
#{{ s.position }}
{{ s.playerName || ('Player ' + s.playerId.slice(0, 6)) }}
{{ formatLocal(s.solvedAtUtc) }}
{{ s.awardedPoints }} pts
- No solvers yet.
`,
})
export class ChallengeModalComponent implements OnChanges {
private readonly markdown = inject(MarkdownService);
private readonly store = inject(ChallengesStore);
@Input({ required: true }) card: BoardCard | null = null;
@Input() initialSolvers: SolverRow[] = [];
@Input() canSubmit: boolean = false;
@Input() noticeOverride: string | null = null;
@Output() close = new EventEmitter();
@Output() submitted = new EventEmitter<{ flag: string }>();
private _flagValue = signal('');
get flagValue(): string {
return this._flagValue();
}
set flagValue(v: string) {
this._flagValue.set(v ?? '');
}
readonly submitting = signal(false);
readonly errorMessage = signal(null);
readonly awarded = signal<{ basePoints: number; rankBonus: number; awardedPoints: number; awardedAtUtc: string } | null>(null);
readonly notice = signal(null);
private _solvers = signal([]);
readonly solvers = this._solvers.asReadonly();
readonly renderedDescription = computed(() => {
return this.markdown.render(this.card?.descriptionMd ?? '');
});
ngOnChanges(changes: SimpleChanges): void {
if (changes['card']) {
this._flagValue.set('');
this.submitting.set(false);
this.errorMessage.set(null);
this.awarded.set(null);
this.notice.set(this.noticeOverride ?? null);
this._solvers.set([...(this.initialSolvers ?? [])]);
}
if (changes['noticeOverride']) {
this.notice.set(this.noticeOverride ?? null);
}
}
onBackdrop(ev: MouseEvent): void {
if (ev.target === ev.currentTarget) this.close.emit();
}
@HostListener('document:keydown.escape')
onEscape(): void {
if (!this.submitting()) this.close.emit();
}
async onSubmit(): Promise {
if (!this.card || this.submitting() || !this.canSubmit) return;
const flag = this._flagValue().trim();
if (!flag) return;
this.submitting.set(true);
this.errorMessage.set(null);
try {
const resp = await this.store.submit(this.card.id, flag);
this._flagValue.set('');
this.awarded.set(resp.awarded);
this._solvers.set(this.store.buildSolversFromResponse(resp.solvers));
this.submitted.emit({ flag });
if (resp.status === 'already_solved') {
this.errorMessage.set(messageForSolveError('already_solved'));
}
} catch (err) {
const envelope = err as ApiErrorEnvelope;
const code = parseSolveError(envelope.status, envelope);
this.errorMessage.set(messageForSolveError(code));
} finally {
this.submitting.set(false);
}
}
applyLiveSolve(payload: SolveEventLivePayload): void {
this._solvers.set(this.store.mergeLiveSolver(this._solvers(), payload));
}
formatLocal(utc: string): string {
return formatUtcToLocal(utc);
}
rankClass(position: number): string {
if (position === 1) return 'rank-1';
if (position === 2) return 'rank-2';
if (position === 3) return 'rank-3';
return 'rank-other';
}
trackBySolver(_idx: number, s: SolverRow): string {
return `${s.playerId}-${s.solvedAtUtc}`;
}
}