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,136 @@
import { Component, inject, signal } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { MailService } from '../../services/mail.service';
import { Folder } from '../../models/mail.models';
@Component({
selector: 'app-layout',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="min-h-screen relative" [class.light-theme]="isLightTheme()">
<!-- Synthwave фон -->
<div class="synth-bg" aria-hidden="true">
<div class="synth-sun"></div>
<div class="synth-grid">
<div class="synth-grid-plane"></div>
</div>
</div>
<!-- Контент -->
<div class="relative z-10 flex flex-col min-h-screen">
<!-- Шапка -->
<header class="glass-panel rounded-none border-t-0 border-x-0 p-4 sticky top-0 z-40 backdrop-blur-xl">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-synth-pink to-synth-cyan flex items-center justify-center text-synth-dark font-black text-xl font-orbitron shadow-[0_0_15px_rgba(255,0,127,0.8)]">
M
</div>
<div>
<h1 class="text-2xl font-orbitron font-extrabold tracking-wider neon-text-primary">
MAIL<span class="text-synth-cyan font-normal">SERVER</span>
</h1>
<p class="text-xs text-synth-cyan/70 font-mono tracking-widest uppercase">Synthwave Edition</p>
</div>
</div>
<div class="flex items-center gap-4">
<!-- Переключатель темы -->
<button (click)="toggleTheme()" class="cyber-btn px-3 py-2 rounded-lg text-xs font-bold" [title]="isLightTheme() ? 'Переключить на тёмную тему' : 'Переключить на светлую тему'">
{{ isLightTheme() ? '🌙 Dark' : '☀️ Light' }}
</button>
<span class="text-sm font-mono text-slate-400">{{ email }}</span>
<button (click)="onLogout()" class="cyber-btn px-4 py-2 rounded-lg text-xs font-bold">
<i class="mr-1">⏻</i> Logout
</button>
</div>
</div>
</header>
<!-- Основная область -->
<div class="flex flex-1 max-w-7xl mx-auto w-full p-4 gap-4">
<!-- Боковая панель с папками -->
<aside class="w-64 flex-shrink-0">
<div class="glass-panel p-4">
<h2 class="text-sm font-orbitron font-bold text-synth-cyan mb-3 flex items-center gap-2">
<span class="text-synth-pink">📁</span> FOLDERS
</h2>
<nav class="space-y-1">
@for (folder of folders(); track folder.id) {
<a routerLink="/inbox" [queryParams]="{folderId: folder.id}"
routerLinkActive="bg-synth-pink/20 text-synth-pink border-synth-pink"
[queryParamsHandling]=" 'merge'"
class="block px-3 py-2 rounded-lg text-sm font-rajdhani text-slate-300 hover:bg-synth-cyan/10 hover:text-synth-cyan border border-transparent transition-all">
{{ folder.name }}
@if (folder.unreadCount > 0) {
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-synth-pink/30 text-synth-pink font-mono">
{{ folder.unreadCount }}
</span>
}
</a>
}
</nav>
</div>
<!-- Кнопка написать -->
<div class="glass-panel p-4 mt-4">
<a routerLink="/compose" class="cyber-btn w-full py-3 rounded-xl text-xs font-bold block text-center">
✉️ NEW MESSAGE
</a>
</div>
</aside>
<!-- Контент маршрутов -->
<main class="flex-1 min-w-0">
<router-outlet />
</main>
</div>
</div>
</div>
`,
styles: ``
})
export default class LayoutComponent {
authService = inject(AuthService);
mailService = inject(MailService);
email = this.authService.getEmail() || '';
folders = signal<Folder[]>([]);
isLightTheme = signal(false);
constructor() {
this.loadFolders();
// Загружаем сохранённую тему
const saved = localStorage.getItem('theme');
if (saved === 'light') {
this.isLightTheme.set(true);
document.documentElement.classList.add('light-theme');
}
}
toggleTheme(): void {
const isLight = !this.isLightTheme();
this.isLightTheme.set(isLight);
if (isLight) {
document.documentElement.classList.add('light-theme');
localStorage.setItem('theme', 'light');
} else {
document.documentElement.classList.remove('light-theme');
localStorage.setItem('theme', 'dark');
}
}
loadFolders(): void {
this.mailService.getFolders().subscribe({
next: (folders) => this.folders.set(folders),
error: () => console.error('Failed to load folders')
});
}
onLogout(): void {
this.authService.logout();
window.location.href = '/login';
}
}