- 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>
236 lines
10 KiB
TypeScript
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 © 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';
|
|
}
|
|
}
|