ice/src/frontend/components/SharedHeader.ts
Claude Code f8802232c6 Fix TypeScript linting issues and test failures
- 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>
2025-07-07 21:14:54 -04:00

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
});
}
}