Files
reactive-mail-server/frontend/src/app/components/layout/layout.component.ts
T
Liosha84andJunie b6f6d41813 Синхронизация моделей, Docker настройки и исправление Angular шаблонов
- V1__init.sql: синхронизированы таблицы с Java моделями (users, folders, messages, contacts)
- docker-compose.yml: обновлён контекст сборки, переменные R2DBC/Flyway, порты (5433, 3026, 1443)
- backend/Dockerfile: обновлены пути COPY для нового контекста сборки
- settings.gradle: удалён frontend проект из Gradle
- contacts.component.html: исправлен ternary оператор с HTML на Angular @if/@else
- Фронтенд компоненты: обновления compose, inbox, layout, login, message-detail, register
- index.html, styles.css: обновления стилей и мета-данных

Co-authored-by: Junie <junie@jetbrains.com>
2026-11-05 03:08:36 +01:00

236 lines
10 KiB
TypeScript

import { Component, inject, signal, effect } 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';
const THEMES = [
{ value: 'theme-cyber-magenta', label: '💖 Cyber Magenta' },
{ value: 'theme-neon-cyan', label: '🩵 Neon Cyan' },
{ value: 'theme-sunset-orange', label: '🧡 Sunset Orange' },
{ value: 'theme-matrix-green', label: '💚 Matrix Green' },
];
@Component({
selector: 'app-layout',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div #root class="min-h-screen relative theme-cyber-magenta" [class.light-theme]="isLightTheme()" [class.high-contrast]="isHighContrast()">
<!-- Synthwave анимированный фон -->
<div class="synth-stage" aria-hidden="true">
<div class="synth-sun"></div>
<div class="synth-mountains"></div>
<div class="grid-container">
<div class="grid-plane"></div>
</div>
</div>
<!-- Контент -->
<div class="relative z-10 flex flex-col min-h-screen">
<!-- Шапка -->
<header class="p-4 lg:p-6 border-b border-cyan-500/20 glass-panel rounded-none border-t-0 border-x-0 sticky top-0 z-40 backdrop-blur-xl">
<div class="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
<!-- Логотип -->
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-pink-500 to-cyan-400 flex items-center justify-center text-slate-950 font-black text-xl shadow-[0_0_15px_rgba(255,0,127,0.8)] font-orbitron">
M
</div>
<div>
<h1 class="text-2xl font-extrabold font-orbitron tracking-wider neon-text-primary">
MAIL<span class="text-cyan-400 font-normal">SERVER</span>
</h1>
<p class="text-xs text-cyan-300/70 font-mono tracking-widest uppercase">Synthwave Edition</p>
</div>
</div>
<!-- Статистика -->
<div class="flex items-center space-x-4 bg-slate-950/60 px-4 py-2 rounded-full border border-pink-500/30 text-xs font-mono">
<div class="flex items-center space-x-2">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-ping"></span>
<span class="text-slate-300">SMTP: <span class="text-emerald-400 font-bold">ONLINE</span></span>
</div>
<span class="text-slate-600">|</span>
<div class="text-slate-300">User: <span class="text-cyan-400 font-bold">{{ email }}</span></div>
<span class="text-slate-600">|</span>
<div class="text-slate-300">Folders: <span class="text-pink-400 font-bold">{{ folders().length }}</span></div>
</div>
<!-- Контроли -->
<div class="flex items-center space-x-3">
<!-- Выбор цветовой темы -->
<div class="relative">
<select #themeSelect (change)="onThemeChange(themeSelect.value)"
class="bg-slate-900/90 text-cyan-300 border border-cyan-500/40 rounded-lg px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-pink-500 cursor-pointer">
@for (theme of themes; track theme.value) {
<option [value]="theme.value" [selected]="currentTheme() === theme.value">
{{ theme.label }}
</option>
}
</select>
</div>
<!-- High Contrast -->
<button (click)="toggleHighContrast()"
[attr.aria-pressed]="isHighContrast()"
title="Toggle High Contrast Mode"
class="px-3 py-1.5 rounded-lg border border-cyan-500/40 bg-slate-900/80 text-xs font-mono hover:bg-cyan-500/20 focus:outline-none focus:ring-2 focus:ring-pink-500 transition-all flex items-center gap-1.5">
<i class="fa-solid fa-circle-half-stroke text-yellow-400"></i>
<span>Contrast</span>
</button>
<!-- Light/Dark -->
<button (click)="toggleTheme()" class="cyber-btn px-3 py-1.5 rounded-lg text-xs font-bold">
{{ isLightTheme() ? '🌙 Dark' : '☀️ Light' }}
</button>
<!-- Logout -->
<button (click)="onLogout()" class="cyber-btn px-4 py-1.5 rounded-lg text-xs font-bold">
<i class="fa-solid fa-power-off 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-cyan-300 mb-3 flex items-center gap-2">
<i class="fa-solid fa-folder-tree text-pink-500"></i> FOLDERS
</h2>
<nav class="space-y-1">
@for (folder of folders(); track folder.id) {
<a routerLink="/inbox" [queryParams]="{folderId: folder.id}"
routerLinkActive="bg-pink-500/20 text-pink-400 border-pink-500"
[queryParamsHandling]="'merge'"
class="block px-3 py-2 rounded-lg text-sm font-rajdhani text-slate-300 hover:bg-cyan-500/10 hover:text-cyan-300 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-pink-500/30 text-pink-400 font-mono">
{{ folder.unreadCount }}
</span>
}
</a>
}
</nav>
</div>
<!-- Контакты -->
<div class="glass-panel p-4 mt-4">
<a routerLink="/contacts"
routerLinkActive="bg-cyan-500/20 text-cyan-300 border-cyan-500"
class="block px-3 py-2 rounded-lg text-sm font-rajdhani text-slate-300 hover:bg-cyan-500/10 hover:text-cyan-300 border border-transparent transition-all">
<i class="fa-solid fa-address-book mr-2"></i> Contacts
</a>
</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">
<i class="fa-solid fa-pen-to-square mr-2"></i> NEW MESSAGE
</a>
</div>
</aside>
<!-- Контент маршрутов -->
<main class="flex-1 min-w-0">
<router-outlet />
</main>
</div>
<!-- Футер -->
<footer class="p-4 border-t border-cyan-500/20 glass-panel rounded-none border-b-0 border-x-0 mt-6 text-center text-xs font-mono text-slate-400">
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-2">
<div>MAILSERVER SYNTHWAVE &copy; 2026</div>
<div class="flex items-center space-x-4">
<span class="text-cyan-400"><i class="fa-solid fa-universal-access mr-1"></i> A11Y READY</span>
<span class="text-pink-400"><i class="fa-solid fa-bolt mr-1"></i> HIGH PERF CSS3D</span>
</div>
</div>
</footer>
</div>
</div>
`,
styles: ``
})
export default class LayoutComponent {
authService = inject(AuthService);
mailService = inject(MailService);
email = this.authService.getEmail() || '';
folders = signal<Folder[]>([]);
isLightTheme = signal(false);
isHighContrast = signal(false);
currentTheme = signal('theme-cyber-magenta');
themes = THEMES;
constructor() {
this.loadFolders();
// Загружаем сохранённые настройки
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
this.isLightTheme.set(true);
}
const savedPalette = localStorage.getItem('palette');
if (savedPalette) {
this.currentTheme.set(savedPalette);
}
const savedContrast = localStorage.getItem('highContrast');
if (savedContrast === 'true') {
this.isHighContrast.set(true);
}
// Применяем тему при изменении
effect(() => {
const root = document.documentElement;
// Удаляем все theme классы
THEMES.forEach(t => root.classList.remove(t.value));
root.classList.add(this.currentTheme());
});
}
onThemeChange(theme: string): void {
this.currentTheme.set(theme);
localStorage.setItem('palette', 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');
}
}
toggleHighContrast(): void {
const hc = !this.isHighContrast();
this.isHighContrast.set(hc);
if (hc) {
document.documentElement.classList.add('high-contrast');
localStorage.setItem('highContrast', 'true');
} else {
document.documentElement.classList.remove('high-contrast');
localStorage.setItem('highContrast', 'false');
}
}
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';
}
}