|
| 1 | +import { i18n } from '../i18n.js' |
| 2 | + |
| 3 | +const STORAGE_KEY = 'selected-contracts' |
| 4 | + |
| 5 | +function esc(str) { |
| 6 | + const d = document.createElement('div') |
| 7 | + d.textContent = str |
| 8 | + return d.innerHTML |
| 9 | +} |
| 10 | + |
| 11 | +function getSelectedContracts() { |
| 12 | + try { |
| 13 | + const val = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') |
| 14 | + return Array.isArray(val) ? val : [] |
| 15 | + } catch { |
| 16 | + return [] |
| 17 | + } |
| 18 | +} |
| 19 | + |
| 20 | +function setSelectedContracts(ids) { |
| 21 | + try { |
| 22 | + localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)) |
| 23 | + } catch { |
| 24 | + // localStorage may be blocked |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +function getLocalizedField(contract, field) { |
| 29 | + const lang = i18n.currentLanguage || 'en' |
| 30 | + if (lang === 'de' && contract[field + 'De']) { |
| 31 | + return contract[field + 'De'] |
| 32 | + } |
| 33 | + return contract[field] |
| 34 | +} |
| 35 | + |
| 36 | +export function renderContractsPage() { |
| 37 | + const selected = getSelectedContracts() |
| 38 | + const selectedCount = selected.length |
| 39 | + |
| 40 | + return ` |
| 41 | + <div class="mx-auto max-w-5xl px-4 py-8 sm:px-6 lg:px-8"> |
| 42 | + <div class="mb-8"> |
| 43 | + <h1 class="text-3xl font-bold text-[var(--color-text)] mb-2" data-i18n="contracts.title"> |
| 44 | + ${i18n.t('contracts.title')} |
| 45 | + </h1> |
| 46 | + <p class="text-[var(--color-text-secondary)] mb-6" data-i18n="contracts.subtitle"> |
| 47 | + ${i18n.t('contracts.subtitle')} |
| 48 | + </p> |
| 49 | +
|
| 50 | + <div class="flex items-center gap-4 mb-6"> |
| 51 | + <button |
| 52 | + id="contracts-download" |
| 53 | + class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" |
| 54 | + > |
| 55 | + <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 56 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/> |
| 57 | + </svg> |
| 58 | + <span data-i18n="contracts.download">${i18n.t('contracts.download')}</span> |
| 59 | + <span id="contracts-count" class="rounded-full bg-blue-500 px-2 py-0.5 text-xs">${selectedCount}</span> |
| 60 | + </button> |
| 61 | + <button |
| 62 | + id="contracts-select-all" |
| 63 | + class="text-sm text-blue-600 dark:text-blue-400 hover:underline" |
| 64 | + data-i18n="contracts.selectAll" |
| 65 | + >${i18n.t('contracts.selectAll')}</button> |
| 66 | + <button |
| 67 | + id="contracts-deselect-all" |
| 68 | + class="text-sm text-[var(--color-text-secondary)] hover:underline" |
| 69 | + data-i18n="contracts.deselectAll" |
| 70 | + >${i18n.t('contracts.deselectAll')}</button> |
| 71 | + </div> |
| 72 | + </div> |
| 73 | +
|
| 74 | + <div id="contracts-grid" class="grid gap-4"> |
| 75 | + </div> |
| 76 | + </div> |
| 77 | + ` |
| 78 | +} |
| 79 | + |
| 80 | +function renderContractCard(contract, isSelected) { |
| 81 | + const title = getLocalizedField(contract, 'title') |
| 82 | + const description = getLocalizedField(contract, 'description') |
| 83 | + const template = getLocalizedField(contract, 'template') |
| 84 | + |
| 85 | + const anchorLinks = contract.anchors |
| 86 | + .map( |
| 87 | + (id) => |
| 88 | + `<a href="#/anchor/${esc(id)}" class="inline-block rounded-full bg-blue-100 dark:bg-blue-900/30 px-2 py-0.5 text-xs text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-800/40 transition-colors">${esc(id)}</a>` |
| 89 | + ) |
| 90 | + .join(' ') |
| 91 | + |
| 92 | + const templateHtml = template |
| 93 | + .split('\n') |
| 94 | + .map((line) => { |
| 95 | + if (line.startsWith('- ')) { |
| 96 | + return `<span class="text-[var(--color-text-secondary)]">• ${esc(line.slice(2))}</span>` |
| 97 | + } |
| 98 | + return `<span class="font-medium">${esc(line)}</span>` |
| 99 | + }) |
| 100 | + .join('<br>') |
| 101 | + |
| 102 | + return ` |
| 103 | + <div class="contract-card rounded-lg border transition-all cursor-pointer |
| 104 | + ${isSelected ? 'border-blue-500 bg-blue-50/50 dark:bg-blue-900/10 shadow-sm' : 'border-[var(--color-border)] bg-[var(--color-bg)] hover:border-blue-300 dark:hover:border-blue-700'}" |
| 105 | + data-contract-id="${esc(contract.id)}" |
| 106 | + > |
| 107 | + <div class="p-5"> |
| 108 | + <div class="flex items-start gap-3"> |
| 109 | + <input |
| 110 | + type="checkbox" |
| 111 | + class="contract-checkbox mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 112 | + data-contract-id="${esc(contract.id)}" |
| 113 | + ${isSelected ? 'checked' : ''} |
| 114 | + /> |
| 115 | + <div class="flex-1 min-w-0"> |
| 116 | + <h3 class="text-lg font-semibold text-[var(--color-text)] mb-1">${esc(title)}</h3> |
| 117 | + <p class="text-sm text-[var(--color-text-secondary)] mb-3">${esc(description)}</p> |
| 118 | + <div class="rounded-md bg-[var(--color-bg-secondary)] p-3 mb-3 text-sm leading-relaxed"> |
| 119 | + ${templateHtml} |
| 120 | + </div> |
| 121 | + <div class="flex flex-wrap gap-1.5"> |
| 122 | + ${anchorLinks} |
| 123 | + </div> |
| 124 | + </div> |
| 125 | + </div> |
| 126 | + </div> |
| 127 | + </div> |
| 128 | + ` |
| 129 | +} |
| 130 | + |
| 131 | +export function initContractsPage(contracts) { |
| 132 | + const oldGrid = document.getElementById('contracts-grid') |
| 133 | + if (!oldGrid || !contracts) return |
| 134 | + |
| 135 | + // Replace grid to remove stale event listeners from previous init |
| 136 | + const grid = oldGrid.cloneNode(false) |
| 137 | + oldGrid.replaceWith(grid) |
| 138 | + |
| 139 | + const selected = getSelectedContracts() |
| 140 | + |
| 141 | + grid.innerHTML = contracts.map((c) => renderContractCard(c, selected.includes(c.id))).join('') |
| 142 | + |
| 143 | + // Checkbox toggle |
| 144 | + grid.addEventListener('change', (e) => { |
| 145 | + if (e.target.classList.contains('contract-checkbox')) { |
| 146 | + const id = e.target.dataset.contractId |
| 147 | + const current = getSelectedContracts() |
| 148 | + if (e.target.checked) { |
| 149 | + if (!current.includes(id)) current.push(id) |
| 150 | + } else { |
| 151 | + const idx = current.indexOf(id) |
| 152 | + if (idx >= 0) current.splice(idx, 1) |
| 153 | + } |
| 154 | + setSelectedContracts(current) |
| 155 | + updateUI() |
| 156 | + } |
| 157 | + }) |
| 158 | + |
| 159 | + // Card click toggles checkbox |
| 160 | + grid.addEventListener('click', (e) => { |
| 161 | + const card = e.target.closest('.contract-card') |
| 162 | + if (!card || e.target.tagName === 'A' || e.target.tagName === 'INPUT') return |
| 163 | + const checkbox = card.querySelector('.contract-checkbox') |
| 164 | + if (checkbox) { |
| 165 | + checkbox.checked = !checkbox.checked |
| 166 | + checkbox.dispatchEvent(new Event('change', { bubbles: true })) |
| 167 | + } |
| 168 | + }) |
| 169 | + |
| 170 | + // Download button |
| 171 | + const downloadBtn = document.getElementById('contracts-download') |
| 172 | + if (downloadBtn) { |
| 173 | + downloadBtn.addEventListener('click', () => downloadContracts(contracts)) |
| 174 | + } |
| 175 | + |
| 176 | + // Select all |
| 177 | + const selectAllBtn = document.getElementById('contracts-select-all') |
| 178 | + if (selectAllBtn) { |
| 179 | + selectAllBtn.addEventListener('click', () => { |
| 180 | + setSelectedContracts(contracts.map((c) => c.id)) |
| 181 | + initContractsPage(contracts) |
| 182 | + }) |
| 183 | + } |
| 184 | + |
| 185 | + // Deselect all |
| 186 | + const deselectAllBtn = document.getElementById('contracts-deselect-all') |
| 187 | + if (deselectAllBtn) { |
| 188 | + deselectAllBtn.addEventListener('click', () => { |
| 189 | + setSelectedContracts([]) |
| 190 | + initContractsPage(contracts) |
| 191 | + }) |
| 192 | + } |
| 193 | + |
| 194 | + updateUI(contracts) |
| 195 | +} |
| 196 | + |
| 197 | +function updateUI() { |
| 198 | + const selected = getSelectedContracts() |
| 199 | + const countEl = document.getElementById('contracts-count') |
| 200 | + if (countEl) countEl.textContent = selected.length |
| 201 | + |
| 202 | + const downloadBtn = document.getElementById('contracts-download') |
| 203 | + if (downloadBtn) downloadBtn.disabled = selected.length === 0 |
| 204 | + |
| 205 | + // Update card styles |
| 206 | + document.querySelectorAll('.contract-card').forEach((card) => { |
| 207 | + const id = card.dataset.contractId |
| 208 | + const isSelected = selected.includes(id) |
| 209 | + card.classList.toggle('border-blue-500', isSelected) |
| 210 | + card.classList.toggle('bg-blue-50/50', isSelected) |
| 211 | + card.classList.toggle('dark:bg-blue-900/10', isSelected) |
| 212 | + card.classList.toggle('shadow-sm', isSelected) |
| 213 | + card.classList.toggle('border-[var(--color-border)]', !isSelected) |
| 214 | + }) |
| 215 | +} |
| 216 | + |
| 217 | +function downloadContracts(contracts) { |
| 218 | + const selected = getSelectedContracts() |
| 219 | + const lang = i18n.currentLanguage || 'en' |
| 220 | + const filtered = contracts.filter((c) => selected.includes(c.id)) |
| 221 | + |
| 222 | + if (filtered.length === 0) return |
| 223 | + |
| 224 | + let md = '# Semantic Contracts\n\n' |
| 225 | + md += |
| 226 | + lang === 'de' |
| 227 | + ? 'Füge dies zu deiner AGENTS.md oder CLAUDE.md hinzu.\n\n' |
| 228 | + : 'Add this to your AGENTS.md or CLAUDE.md.\n\n' |
| 229 | + |
| 230 | + for (const c of filtered) { |
| 231 | + const title = getLocalizedField(c, 'title') |
| 232 | + const template = getLocalizedField(c, 'template') |
| 233 | + md += `## ${title}\n\n${template}\n\n` |
| 234 | + } |
| 235 | + |
| 236 | + md += '---\n' |
| 237 | + md += |
| 238 | + lang === 'de' |
| 239 | + ? 'Generiert von https://llm-coding.github.io/Semantic-Anchors/#/contracts\n' |
| 240 | + : 'Generated from https://llm-coding.github.io/Semantic-Anchors/#/contracts\n' |
| 241 | + |
| 242 | + const blob = new Blob([md], { type: 'text/markdown' }) |
| 243 | + const url = URL.createObjectURL(blob) |
| 244 | + const a = document.createElement('a') |
| 245 | + a.href = url |
| 246 | + a.download = 'semantic-contracts.md' |
| 247 | + a.click() |
| 248 | + URL.revokeObjectURL(url) |
| 249 | +} |
0 commit comments