import type { History, Shortcut } from '../types/generated.js';
export class DOMRenderer {
private historyListElement: HTMLElement | null = null;
private shortcutsElement: HTMLElement | null = null;
constructor() {
this.historyListElement = document.getElementById('historyList');
this.shortcutsElement = document.getElementById('shortcuts');
}
displayHistory(historyEntries: History[]): void {
if (!this.historyListElement) return;
if (!historyEntries || historyEntries.length === 0) {
this.historyListElement.innerHTML = `
No history yet
Use CLI commands to browse websites
`;
return;
}
this.historyListElement.innerHTML = '';
historyEntries.forEach(item => {
const historyItem = this.createHistoryItem(item);
this.historyListElement!.appendChild(historyItem);
});
}
displayShortcuts(shortcutsData: Record): void {
if (!this.shortcutsElement) return;
if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
this.shortcutsElement.innerHTML = `
No shortcuts configured
Add shortcuts to your config file
`;
return;
}
this.shortcutsElement.innerHTML = '';
Object.entries(shortcutsData).forEach(([key, shortcut]) => {
const shortcutEl = this.createShortcutElement(key, shortcut);
this.shortcutsElement!.appendChild(shortcutEl);
});
}
showLoading(container: 'history' | 'shortcuts'): void {
const element = container === 'history' ? this.historyListElement : this.shortcutsElement;
if (element) {
element.innerHTML = 'Loading...
';
}
}
showError(container: 'history' | 'shortcuts', message: string): void {
const element = container === 'history' ? this.historyListElement : this.shortcutsElement;
if (element) {
element.innerHTML = `
`;
}
}
private createHistoryItem(item: History): HTMLElement {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
const parsed = this.safeParseURL(item.url);
const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled');
const line = document.createElement('div');
line.className = 'history-line';
// Favicon chip (wrapper ensures perfect circle sizing)
const iconWrap = document.createElement('div');
iconWrap.className = 'history-favicon-chip';
const icon = document.createElement('img');
icon.className = 'history-favicon-img';
icon.width = 16; icon.height = 16;
icon.loading = 'lazy';
icon.referrerPolicy = 'no-referrer';
icon.src = this.buildFaviconURL(item.url);
icon.addEventListener('error', () => { iconWrap.style.display = 'none'; });
iconWrap.appendChild(icon);
const titleEl = document.createElement('span');
titleEl.className = 'history-title';
titleEl.textContent = title;
const sep1 = document.createElement('span');
sep1.className = 'history-sep';
sep1.textContent = ' - ';
const domainEl = document.createElement('span');
domainEl.className = 'history-domain';
domainEl.textContent = parsed.host || '';
const sep2 = document.createElement('span');
sep2.className = 'history-sep';
sep2.textContent = ' - ';
const urlEl = document.createElement('span');
urlEl.className = 'history-url';
urlEl.textContent = item.url;
line.appendChild(iconWrap);
line.appendChild(titleEl);
line.appendChild(sep1);
line.appendChild(domainEl);
line.appendChild(sep2);
line.appendChild(urlEl);
historyItem.appendChild(line);
historyItem.addEventListener('click', () => {
this.navigateToUrl(item.url);
});
return historyItem;
}
private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement {
const shortcutEl = document.createElement('div');
shortcutEl.className = 'shortcut';
shortcutEl.innerHTML = `
${this.escapeHtml(key)}:
${this.escapeHtml(shortcut.description || 'No description')}
`;
shortcutEl.addEventListener('click', () => {
// Navigate to the base URL of the shortcut (remove %s template parameter)
const baseUrl = this.extractBaseUrl(shortcut.url_template);
this.navigateToUrl(baseUrl);
});
return shortcutEl;
}
private async navigateToUrl(url: string): Promise {
console.log('Navigating to:', url);
window.location.href = url;
}
private extractBaseUrl(templateUrl: string): string {
try {
// Support both %s and {query} placeholders
const candidate = templateUrl.replace(/%s|\{query\}/g, "");
const url = new URL(candidate);
// Remove empty query params left after placeholder removal
const params = new URLSearchParams(url.search);
for (const key of Array.from(params.keys())) {
const v = params.get(key);
if (v === null || v === "") {
params.delete(key);
}
}
url.search = params.toString();
return url.toString();
} catch (error) {
console.error('Failed to parse shortcut URL:', templateUrl, error);
throw error;
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
private safeParseURL(raw: string): { host: string } {
try {
const u = new URL(raw);
return { host: u.host };
} catch {
return { host: '' };
}
}
private buildFaviconURL(raw: string): string {
try {
const u = new URL(raw);
const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:';
return `${scheme}//${u.host}/favicon.ico`;
} catch {
return '';
}
}
}