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