- Replace 37 instances of 'any' type with proper TypeScript types - Fix trailing spaces in i18n.test.ts - Add proper interfaces for profanity analysis and matches - Extend Express Request interface with custom properties - Fix error handling in ProfanityFilterService for constraint violations - Update test mocks to satisfy TypeScript strict checking - All 147 tests now pass with 0 linting errors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
154 lines
No EOL
5.2 KiB
TypeScript
154 lines
No EOL
5.2 KiB
TypeScript
/**
|
|
* Shared header component for consistent headers across pages
|
|
*/
|
|
|
|
export interface ButtonConfig {
|
|
id?: string;
|
|
href?: string;
|
|
class?: string;
|
|
icon?: string;
|
|
text?: string;
|
|
i18nKey?: string;
|
|
attributes?: Record<string, string>;
|
|
}
|
|
|
|
export interface SharedHeaderConfig {
|
|
showLanguageSelector?: boolean;
|
|
showThemeToggle?: boolean;
|
|
additionalButtons?: ButtonConfig[];
|
|
titleKey?: string;
|
|
subtitleKey?: string | null;
|
|
containerId?: string;
|
|
}
|
|
|
|
export class SharedHeader {
|
|
private config: Required<SharedHeaderConfig>;
|
|
|
|
constructor(config: SharedHeaderConfig = {}) {
|
|
this.config = {
|
|
showLanguageSelector: config.showLanguageSelector !== false,
|
|
showThemeToggle: config.showThemeToggle !== false,
|
|
additionalButtons: config.additionalButtons || [],
|
|
titleKey: config.titleKey || 'common.appName',
|
|
subtitleKey: config.subtitleKey || null,
|
|
containerId: config.containerId || 'header-container'
|
|
};
|
|
}
|
|
|
|
public render(): void {
|
|
const container = document.getElementById(this.config.containerId);
|
|
if (!container) {
|
|
console.error(`Container with id "${this.config.containerId}" not found`);
|
|
return;
|
|
}
|
|
|
|
const header = document.createElement('header');
|
|
header.innerHTML = `
|
|
<div class="header-content">
|
|
<div class="header-text">
|
|
<h1><a href="/" style="text-decoration: none; color: inherit;">❄️</a> <span data-i18n="${this.config.titleKey}"></span></h1>
|
|
${this.config.subtitleKey ? `<p data-i18n="${this.config.subtitleKey}"></p>` : ''}
|
|
</div>
|
|
<div class="header-controls">
|
|
${this.config.showLanguageSelector ? '<div id="language-selector-container" class="language-selector-container"></div>' : ''}
|
|
${this.config.showThemeToggle ? this.renderThemeToggle() : ''}
|
|
${this.config.additionalButtons.map(btn => this.renderButton(btn)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(header);
|
|
|
|
// Initialize components after rendering
|
|
if (this.config.showThemeToggle) {
|
|
this.initializeThemeToggle();
|
|
}
|
|
|
|
// Update translations if i18n is available
|
|
if ((window as Window & typeof globalThis & { i18n?: { updatePageTranslations: () => void } }).i18n?.updatePageTranslations) {
|
|
(window as Window & typeof globalThis & { i18n?: { updatePageTranslations: () => void } }).i18n.updatePageTranslations();
|
|
}
|
|
}
|
|
|
|
private renderThemeToggle(): string {
|
|
return `
|
|
<button id="theme-toggle" class="theme-toggle js-only" title="Toggle dark mode" data-i18n-title="common.darkMode">
|
|
<span class="theme-icon">🌙</span>
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
private renderButton(btn: ButtonConfig): string {
|
|
const attrs = btn.attributes
|
|
? Object.entries(btn.attributes).map(([k, v]) => `${k}="${v}"`).join(' ')
|
|
: '';
|
|
|
|
const i18nAttr = btn.i18nKey ? `data-i18n="${btn.i18nKey}"` : '';
|
|
const text = btn.text || '';
|
|
const icon = btn.icon || '';
|
|
const idAttr = btn.id ? `id="${btn.id}"` : '';
|
|
|
|
if (btn.href) {
|
|
return `<a href="${btn.href}" class="${btn.class || 'header-btn'}" ${attrs} ${i18nAttr}>${icon}${icon && text ? ' ' : ''}${text}</a>`;
|
|
} else {
|
|
return `<button ${idAttr} class="${btn.class || 'header-btn'}" ${attrs} ${i18nAttr}>${icon}${icon && text ? ' ' : ''}${text}</button>`;
|
|
}
|
|
}
|
|
|
|
private initializeThemeToggle(): void {
|
|
const themeToggle = document.getElementById('theme-toggle') as HTMLButtonElement;
|
|
if (!themeToggle) return;
|
|
|
|
const updateThemeIcon = (): void => {
|
|
const theme = document.documentElement.getAttribute('data-theme');
|
|
const icon = themeToggle.querySelector('.theme-icon');
|
|
if (icon) {
|
|
icon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
|
}
|
|
};
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('theme', newTheme);
|
|
updateThemeIcon();
|
|
});
|
|
|
|
// Set initial icon
|
|
updateThemeIcon();
|
|
}
|
|
|
|
// Factory methods for common headers
|
|
public static createMainHeader(): SharedHeader {
|
|
return new SharedHeader({
|
|
titleKey: 'common.appName',
|
|
subtitleKey: 'meta.subtitle',
|
|
showLanguageSelector: true,
|
|
showThemeToggle: true
|
|
});
|
|
}
|
|
|
|
public static createAdminHeader(): SharedHeader {
|
|
return new SharedHeader({
|
|
titleKey: 'admin.adminPanel',
|
|
showLanguageSelector: true,
|
|
showThemeToggle: true,
|
|
additionalButtons: [
|
|
{ href: '/', class: 'header-btn btn-home', icon: '🏠', text: 'Homepage', i18nKey: 'common.homepage' },
|
|
{ id: 'refresh-btn', class: 'header-btn btn-refresh', icon: '🔄', text: 'Refresh Data', i18nKey: 'common.refresh' },
|
|
{ id: 'logout-btn', class: 'header-btn btn-logout', icon: '🚪', text: 'Logout', i18nKey: 'common.logout' }
|
|
]
|
|
});
|
|
}
|
|
|
|
public static createPrivacyHeader(): SharedHeader {
|
|
return new SharedHeader({
|
|
titleKey: 'privacy.title',
|
|
subtitleKey: 'common.appName',
|
|
showLanguageSelector: false,
|
|
showThemeToggle: true
|
|
});
|
|
}
|
|
} |