Почтовый кабинет: регистрация, папки, черновики, вложения, грид @jambotron/aria-grid, интерфейс по synthwave-шаблону
Бэкенд: регистрация со входом сразу и /api/config, профиль и смена пароля, свои папки, PATCH письма (прочитано, флажок, перенос; Спам ставит отметку), корзина и очистка, черновики, несколько адресатов и копия, вложения черновика уходят с письмом, вложения и заголовки To/Cc входящих сохраняются, письма заблокированных — в Спам. Исправлено: загрузка файла (415), колонка content_id (500 на любых вложениях), SPA-маршрут с точкой. Фронтенд переписан: все таблицы — грид, оформление — шаблон synthwave-design-showcase.html. α Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import {
|
||||
JtGridBulkEvent, JtGridBulkService, JtGridCellDirective, JtGridColumn, JtGridComponent, JtGridRowEvent, RowAction
|
||||
} from '@jambotron/aria-grid';
|
||||
import { ApiService } from '../../core/api.service';
|
||||
import { BlacklistEntry, errorText } from '../../core/models';
|
||||
import { ToastService } from '../../ui/toast.service';
|
||||
|
||||
/** Blocked senders: their mail goes straight to Spam. */
|
||||
@Component({
|
||||
selector: 'app-blocked',
|
||||
imports: [DatePipe, FormsModule, MatIconModule, JtGridComponent, JtGridCellDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<section class="glass-panel static p-4 md:p-6" aria-labelledby="blocked-title">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<mat-icon class="text-pink-400">block</mat-icon>
|
||||
<div>
|
||||
<h1 id="blocked-title" class="text-xl font-orbitron font-bold neon-text-secondary">BLOCKED SENDERS</h1>
|
||||
<p class="text-xs text-slate-400 font-mono">Messages from these addresses go straight to Spam</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="flex flex-wrap items-end gap-3 mb-5 p-4 rounded-xl bg-slate-950/50 border border-pink-500/20" (ngSubmit)="add()">
|
||||
<div class="flex-grow min-w-[220px]">
|
||||
<label class="synth-label" for="b-email">Sender address</label>
|
||||
<input id="b-email" name="email" type="email" class="synth-input" [(ngModel)]="email" placeholder="spammer@example.com">
|
||||
</div>
|
||||
<div class="flex-grow min-w-[180px]">
|
||||
<label class="synth-label" for="b-reason">Reason (optional)</label>
|
||||
<input id="b-reason" name="reason" class="synth-input" [(ngModel)]="reason" maxlength="255">
|
||||
</div>
|
||||
<button type="submit" class="cyber-btn px-4 py-2.5 rounded-lg text-xs font-bold"><mat-icon>block</mat-icon> BLOCK</button>
|
||||
</form>
|
||||
|
||||
<jt-grid [rows]="entries()" [columns]="columns" [trackBy]="bySender" label="Blocked senders"
|
||||
[emptyMessage]="loading() ? 'Loading…' : 'Nobody is blocked'" [bulkActions]="bulkActions" (bulkAction)="onBulk($event)"
|
||||
[rowActions]="rowActions" (rowAction)="onRow($event)" [rowSubject]="senderOf" [initialSort]="{ key: 'date', direction: 'desc' }">
|
||||
<ng-template jtGridCell="sender" let-row><span class="font-mono text-sm text-cyan-300">{{ row.senderEmail }}</span></ng-template>
|
||||
<ng-template jtGridCell="reason" let-row><span class="text-slate-300">{{ row.reason || '' }}</span></ng-template>
|
||||
<ng-template jtGridCell="date" let-row><span class="font-mono text-xs text-slate-400">{{ row.createdAt | date: 'd MMM y, HH:mm' }}</span></ng-template>
|
||||
</jt-grid>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
export class BlockedPage {
|
||||
private api = inject(ApiService);
|
||||
private bulk = inject(JtGridBulkService);
|
||||
private toasts = inject(ToastService);
|
||||
|
||||
readonly entries = signal<BlacklistEntry[]>([]);
|
||||
readonly loading = signal(true);
|
||||
email = '';
|
||||
reason = '';
|
||||
|
||||
readonly columns: JtGridColumn<BlacklistEntry>[] = [
|
||||
{ key: 'sender', label: 'Sender', sort: e => e.senderEmail },
|
||||
{ key: 'reason', label: 'Reason', sort: e => e.reason || '' },
|
||||
{ key: 'date', label: 'Blocked', sort: e => e.createdAt || '', align: 'end' }
|
||||
];
|
||||
readonly bulkActions: RowAction[] = [{ value: 'unblock', label: 'Unblock', icon: 'lock_open' }];
|
||||
readonly rowActions = (): RowAction[] => [{ value: 'unblock', label: 'Unblock', icon: 'lock_open', inline: true }];
|
||||
readonly bySender = (e: BlacklistEntry) => e.senderEmail;
|
||||
readonly senderOf = (e: BlacklistEntry) => e.senderEmail;
|
||||
|
||||
constructor() { this.load(); }
|
||||
|
||||
load(): void {
|
||||
this.api.blacklist().subscribe({
|
||||
next: list => { this.entries.set(list); this.loading.set(false); },
|
||||
error: e => { this.loading.set(false); this.toasts.error(errorText(e)); }
|
||||
});
|
||||
}
|
||||
|
||||
add(): void {
|
||||
const email = this.email.trim().toLowerCase();
|
||||
if (!/^[^@\s]+@[^@\s]+$/.test(email)) {
|
||||
this.toasts.error('Enter a valid sender address');
|
||||
return;
|
||||
}
|
||||
this.api.block(email, this.reason.trim()).subscribe({
|
||||
next: () => { this.toasts.ok(`${email} blocked`); this.email = ''; this.reason = ''; this.load(); },
|
||||
error: e => this.toasts.error(errorText(e))
|
||||
});
|
||||
}
|
||||
|
||||
onRow(e: JtGridRowEvent<BlacklistEntry>): void {
|
||||
this.api.unblock(e.row.senderEmail).subscribe({
|
||||
next: () => { this.toasts.ok(`${e.row.senderEmail} unblocked`); this.load(); },
|
||||
error: err => this.toasts.error(errorText(err))
|
||||
});
|
||||
}
|
||||
|
||||
onBulk(e: JtGridBulkEvent<BlacklistEntry>): void {
|
||||
this.bulk.run({ rows: e.rows, label: 'Unblock', run: x => this.api.unblock(x.senderEmail) }).subscribe(() => this.load());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user