Initial commit: mail-server with WebFlux backend and Angular v22 Synthwave frontend

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-11-05 01:07:29 +01:00
co-authored by Junie
commit 0339413d6e
78 changed files with 6808 additions and 0 deletions
@@ -0,0 +1,145 @@
import { Component, inject, signal } from '@angular/core';
import { RouterLink, ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { MailService } from '../../services/mail.service';
import { Message, Folder } from '../../models/mail.models';
import { DatePipe, SlicePipe } from '@angular/common';
import { map, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-inbox',
standalone: true,
imports: [RouterLink, FormsModule, DatePipe, SlicePipe],
providers: [MailService],
template: `
<div class="space-y-4">
<!-- Поиск -->
<div class="glass-panel p-4">
<div class="flex gap-3">
<input [(ngModel)]="searchQuery" (keyup.enter)="loadMessages()"
class="flex-1 bg-synth-darker/80 border border-synth-cyan/40 rounded-xl px-4 py-2 text-sm text-slate-200 font-mono focus:outline-none focus:ring-2 focus:ring-synth-pink focus:border-synth-cyan transition-all"
placeholder="🔍 Search messages...">
<button (click)="loadMessages()" class="cyber-btn px-6 py-2 rounded-xl text-xs font-bold">
SEARCH
</button>
</div>
</div>
<!-- Список писем -->
<div class="glass-panel p-4">
<h2 class="text-lg font-orbitron font-bold text-synth-cyan mb-4 flex items-center gap-2">
<span class="text-synth-pink">📬</span> {{ folderName() }}
<span class="text-sm font-mono text-slate-400 ml-auto">{{ messages()?.length || 0 }} messages</span>
</h2>
@if (!messages() || messages().length === 0) {
<div class="text-center py-12 text-slate-400 font-mono">
<p class="text-4xl mb-4">📭</p>
<p>No messages yet</p>
</div>
} @else {
<div class="space-y-2">
@for (msg of messages() || []; track msg.id) {
<a routerLink="/message/{{ msg.id }}"
class="block p-4 rounded-xl border border-synth-cyan/10 hover:border-synth-cyan/40 hover:bg-synth-cyan/5 transition-all cursor-pointer group">
<div class="flex items-start gap-3">
<!-- Индикатор прочитанности -->
<div class="mt-1">
@if (!msg.read) {
<span class="w-3 h-3 rounded-full bg-synth-pink inline-block shadow-[0_0_8px_rgba(255,0,127,0.8)]"></span>
} @else {
<span class="w-3 h-3 rounded-full border border-slate-500 inline-block"></span>
}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="font-bold text-sm text-slate-200 font-rajdhani truncate">
{{ msg.senderName || msg.sender }}
</span>
@if (msg.spam) {
<span class="px-2 py-0.5 text-xs rounded-full bg-synth-yellow/20 text-synth-yellow font-mono">
SPAM
</span>
}
@if (msg.flagged) {
<span class="text-synth-yellow text-xs">★</span>
}
</div>
<div class="text-sm text-synth-cyan font-rajdhani truncate group-hover:text-synth-pink transition-colors">
{{ msg.subject || '(No subject)' }}
</div>
<div class="text-xs text-slate-400 font-mono truncate">
{{ msg.body || '' | slice:0:100 }}{{ (msg.body || '').length > 100 ? '...' : '' }}
</div>
</div>
<div class="text-right flex-shrink-0">
<div class="text-xs text-slate-400 font-mono">
{{ msg.sentAt | date:'MMM d, HH:mm' }}
</div>
@if (msg.hasAttachments) {
<span class="text-xs text-synth-purple font-mono">📎 {{ msg.attachmentCount }}</span>
}
</div>
</div>
</a>
}
</div>
}
</div>
</div>
`,
styles: ``
})
export default class InboxComponent {
private route = inject(ActivatedRoute);
private mailService = inject(MailService);
messages = signal<Message[]>([]);
folderName = signal<string>('INBOX');
searchQuery = '';
folderId = signal<number>(1);
constructor() {
// Подписываемся на изменения folderId в queryParams
this.route.queryParams.subscribe(params => {
const newFolderId = params['folderId'] ? Number(params['folderId']) : 1;
if (newFolderId !== this.folderId()) {
this.folderId.set(newFolderId);
this.loadFolderName(newFolderId);
this.loadMessages();
}
});
// Начальная загрузка
const initialFolderId = this.route.snapshot.queryParams['folderId'] ? Number(this.route.snapshot.queryParams['folderId']) : 1;
this.folderId.set(initialFolderId);
this.loadFolderName(initialFolderId);
this.loadMessages();
}
loadFolderName(folderId: number): void {
this.mailService.getFolders().subscribe({
next: (folders) => {
const folder = folders.find(f => f.id === folderId);
this.folderName.set(folder ? folder.name : 'INBOX');
},
error: () => this.folderName.set('INBOX')
});
}
loadMessages(): void {
const currentFolderId = this.folderId();
if (this.searchQuery.trim()) {
this.mailService.searchMessages(currentFolderId, this.searchQuery.trim()).subscribe({
next: (msgs) => this.messages.set(msgs),
error: () => console.error('Search failed')
});
} else {
this.mailService.getMessages(currentFolderId).subscribe({
next: (msgs) => this.messages.set(msgs),
error: () => console.error('Failed to load messages')
});
}
}
}