dom.ts RawHistoryFolder
1import type { History, Shortcut } from "../types/generated.js";
2
3export class DOMRenderer {
4 private historyListElement: HTMLElement | null = null;
5 private shortcutsElement: HTMLElement | null = null;
6
7 constructor() {
8 this.historyListElement = document.getElementById("historyList");
9 this.shortcutsElement = document.getElementById("shortcuts");
10 }
11
12 displayHistory(historyEntries: History[]): void {
13 if (!this.historyListElement) return;
14
15 if (!historyEntries || historyEntries.length === 0) {
16 this.historyListElement.innerHTML = `
17 <div class="empty-state">
18 <h3>No history yet</h3>
19 <p>Use CLI commands to browse websites</p>
20 </div>
21 `;
22 return;
23 }
24
25 this.historyListElement.innerHTML = "";
26 historyEntries.forEach((item) => {
27 const historyItem = this.createHistoryItem(item);
28 this.historyListElement!.appendChild(historyItem);
29 });
30 }
31
32 displayShortcuts(shortcutsData: Record<string, Shortcut>): void {
33 if (!this.shortcutsElement) return;
34
35 if (!shortcutsData || Object.keys(shortcutsData).length === 0) {
36 this.shortcutsElement.innerHTML = `
37 <div class="empty-state">
38 <h3>No shortcuts configured</h3>
39 <p>Add shortcuts to your config file</p>
40 </div>
41 `;
42 return;
43 }
44
45 this.shortcutsElement.innerHTML = "";
46 Object.entries(shortcutsData).forEach(([key, shortcut]) => {
47 const shortcutEl = this.createShortcutElement(key, shortcut);
48 this.shortcutsElement!.appendChild(shortcutEl);
49 });
50 }
51
52 showLoading(container: "history" | "shortcuts"): void {
53 const element =
54 container === "history" ? this.historyListElement : this.shortcutsElement;
55 if (element) {
56 element.innerHTML = '<div class="loading">Loading...</div>';
57 }
58 }
59
60 showError(container: "history" | "shortcuts", message: string): void {
61 const element =
62 container === "history" ? this.historyListElement : this.shortcutsElement;
63 if (element) {
64 element.innerHTML = `
65 <div class="empty-state">
66 <h3>Error</h3>
67 <p>${message}</p>
68 </div>
69 `;
70 }
71 }
72
73 private createHistoryItem(item: History): HTMLElement {
74 const historyItem = document.createElement("div");
75 historyItem.className = "history-item";
76 const parsed = this.safeParseURL(item.url);
77 const title =
78 item.title && item.title.trim() !== ""
79 ? item.title
80 : parsed.host || "Untitled";
81
82 const line = document.createElement("div");
83 line.className = "history-line";
84
85 // Favicon chip (wrapper ensures perfect circle sizing)
86 const iconWrap = document.createElement("div");
87 iconWrap.className = "history-favicon-chip";
88 const icon = document.createElement("img");
89 icon.className = "history-favicon-img";
90 icon.width = 16;
91 icon.height = 16;
92 icon.loading = "lazy";
93 icon.referrerPolicy = "no-referrer";
94 icon.src = this.buildFaviconURL(item.url);
95 icon.addEventListener("error", () => {
96 iconWrap.style.display = "none";
97 });
98 iconWrap.appendChild(icon);
99
100 const titleEl = document.createElement("span");
101 titleEl.className = "history-title";
102 titleEl.textContent = title;
103
104 const sep1 = document.createElement("span");
105 sep1.className = "history-sep";
106 sep1.textContent = " - ";
107
108 const domainEl = document.createElement("span");
109 domainEl.className = "history-domain";
110 domainEl.textContent = parsed.host || "";
111
112 const sep2 = document.createElement("span");
113 sep2.className = "history-sep";
114 sep2.textContent = " - ";
115
116 const urlEl = document.createElement("span");
117 urlEl.className = "history-url";
118 urlEl.textContent = item.url;
119
120 line.appendChild(iconWrap);
121 line.appendChild(titleEl);
122 line.appendChild(sep1);
123 line.appendChild(domainEl);
124 line.appendChild(sep2);
125 line.appendChild(urlEl);
126
127 historyItem.appendChild(line);
128
129 historyItem.addEventListener("click", () => {
130 this.navigateToUrl(item.url);
131 });
132
133 return historyItem;
134 }
135
136 private createShortcutElement(key: string, shortcut: Shortcut): HTMLElement {
137 const shortcutEl = document.createElement("div");
138 shortcutEl.className = "shortcut";
139 shortcutEl.innerHTML = `
140 <div class="shortcut-key">${this.escapeHtml(key)}:</div>
141 <div class="shortcut-desc">${this.escapeHtml(shortcut.description || "No description")}</div>
142 `;
143
144 shortcutEl.addEventListener("click", () => {
145 // Navigate to the base URL of the shortcut (remove %s template parameter)
146 const baseUrl = this.extractBaseUrl(shortcut.url_template);
147 this.navigateToUrl(baseUrl);
148 });
149
150 return shortcutEl;
151 }
152
153 private async navigateToUrl(url: string): Promise<void> {
154 console.log("Navigating to:", url);
155 window.location.href = url;
156 }
157
158 private extractBaseUrl(templateUrl: string): string {
159 try {
160 // Support both %s and {query} placeholders
161 const candidate = templateUrl.replace(/%s|\{query\}/g, "");
162 const url = new URL(candidate);
163 // Remove empty query params left after placeholder removal
164 const params = new URLSearchParams(url.search);
165 for (const key of Array.from(params.keys())) {
166 const v = params.get(key);
167 if (v === null || v === "") {
168 params.delete(key);
169 }
170 }
171 url.search = params.toString();
172 return url.toString();
173 } catch (error) {
174 console.error("Failed to parse shortcut URL:", templateUrl, error);
175 throw error;
176 }
177 }
178
179 private escapeHtml(text: string): string {
180 const div = document.createElement("div");
181 div.textContent = text;
182 return div.innerHTML;
183 }
184
185 private safeParseURL(raw: string): { host: string } {
186 try {
187 const u = new URL(raw);
188 return { host: u.host };
189 } catch {
190 return { host: "" };
191 }
192 }
193
194 private buildFaviconURL(raw: string): string {
195 try {
196 const u = new URL(raw);
197 const scheme = u.protocol && u.protocol !== ":" ? u.protocol : "https:";
198 return `${scheme}//${u.host}/favicon.ico`;
199 } catch {
200 return "";
201 }
202 }
203}