docsys/templates/document.html

523 lines
28 KiB
HTML

{{template "base" .}}
{{define "content"}}
<div class="space-y-6">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div class="flex items-start space-x-4">
<a href="/browse/{{.Document.Category}}" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors mt-1">
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</a>
<div>
<div class="flex items-center space-x-3 mb-1">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-brand-100 dark:bg-brand-900/30 text-brand-700 dark:text-brand-300">
{{categoryIcon .Document.Category}} {{title .Document.Category}}
</span>
{{if .Document.Type}}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
{{title .Document.Type}}
</span>
{{end}}
</div>
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white">{{.Document.Title}}</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">ID: {{.Document.ID}}</p>
</div>
</div>
<div class="flex gap-2">
<button onclick="toggleEdit()" class="inline-flex items-center px-3 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-all">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
Edit
</button>
<button onclick="openShareModal()" class="inline-flex items-center px-3 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 text-sm font-medium rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-all">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
</svg>
Share
</button>
{{if .Document.PDFPath}}
<a href="/pdf/{{.Document.ID}}" target="_blank" class="inline-flex items-center px-3 py-2 bg-brand-600 hover:bg-brand-700 text-white text-sm font-medium rounded-lg transition-all">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"></path>
</svg>
Download
</a>
{{end}}
</div>
</div>
<!-- Row 1: Details (left) | Summary + Notes (right) — equal height -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden flex flex-col">
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
<h2 class="font-semibold text-gray-900 dark:text-white">Details</h2>
</div>
<div class="p-6 flex-1">
<div class="grid grid-cols-2 gap-x-8 gap-y-4">
<dl class="space-y-4">
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">Category</dt>
<dd class="mt-1">
<select onchange="inlineUpdateCategory(this.value)"
class="font-medium text-gray-900 dark:text-white bg-gray-100 dark:bg-gray-700 rounded-lg px-2 py-1 border-0 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 text-sm">
{{range .Categories}}
<option value="{{.}}" {{if eq . $.Document.Category}}selected{{end}}>{{categoryIcon .}} {{title .}}</option>
{{end}}
</select>
</dd>
</div>
{{if .Document.Vendor}}
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">Vendor</dt>
<dd class="mt-1 font-medium text-gray-900 dark:text-white">{{.Document.Vendor}}</dd>
</div>
{{end}}
{{if .Document.Amount}}
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">Amount</dt>
<dd class="mt-1 font-medium text-gray-900 dark:text-white">{{.Document.Amount}}</dd>
</div>
{{end}}
</dl>
<dl class="space-y-4">
{{if .Document.Date}}
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">Date</dt>
<dd class="mt-1 font-medium text-gray-900 dark:text-white">{{formatDate .Document.Date}}</dd>
</div>
{{end}}
{{if .Document.ProcessedAt}}
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">Processed</dt>
<dd class="mt-1 font-medium text-gray-900 dark:text-white">{{formatDateTime .Document.ProcessedAt}}</dd>
</div>
{{end}}
{{if .Document.OriginalFile}}
<div>
<dt class="text-sm text-gray-500 dark:text-gray-400">Original File</dt>
<dd class="mt-1 font-medium text-gray-900 dark:text-white truncate">{{.Document.OriginalFile}}</dd>
</div>
{{end}}
</dl>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden flex flex-col">
{{if .Document.Summary}}
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
<h2 class="font-semibold text-gray-900 dark:text-white">Summary</h2>
</div>
<div class="p-6 border-b border-gray-100 dark:border-gray-700">
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{.Document.Summary}}</p>
</div>
{{end}}
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
<h2 class="font-semibold text-gray-900 dark:text-white">Notes</h2>
</div>
<div class="p-6 flex-1">
{{if .Document.Notes}}
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{.Document.Notes}}</p>
{{else}}
<p class="text-gray-400 dark:text-gray-500 italic">No notes yet. Click Edit to add notes.</p>
{{end}}
</div>
</div>
</div>
<!-- Row 2: OCR Text (left) | PDF Viewer (right) -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{{if .Document.FullText}}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700 flex items-center justify-between">
<h2 class="font-semibold text-gray-900 dark:text-white">OCR Text</h2>
<button onclick="copyText()" class="text-sm text-brand-600 dark:text-brand-400 hover:underline">Copy</button>
</div>
<div class="p-6 max-h-[700px] overflow-auto">
<div id="ocr-text" class="text-sm text-gray-700 dark:text-gray-300 prose dark:prose-invert max-w-none">{{.Document.FullText | safe}}</div>
<script>
(function() {
const el = document.getElementById('ocr-text');
let md = el.textContent;
md = md.replace(/^### (.+)$/gm, '<h4 class="font-semibold mt-4 mb-2">$1</h4>');
md = md.replace(/^## (.+)$/gm, '<h3 class="font-semibold text-lg mt-4 mb-2">$1</h3>');
md = md.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
md = md.replace(/\|(.+)\|/g, function(match) {
const cells = match.split('|').filter(c => c.trim());
return '<tr>' + cells.map(c => '<td class="border px-2 py-1">' + c.trim() + '</td>').join('') + '</tr>';
});
md = md.replace(/(<tr>.*<\/tr>\n?)+/g, '<table class="border-collapse border my-2">$&</table>');
md = md.replace(/^- (.+)$/gm, '<li>$1</li>');
md = md.replace(/(<li>.*<\/li>\n?)+/g, '<ul class="list-disc ml-4 my-2">$&</ul>');
md = md.replace(/\n/g, '<br>');
el.innerHTML = md;
})();
</script>
</div>
</div>
{{else}}
<div></div>
{{end}}
<div>
{{if .Document.PDFPath}}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700 flex items-center justify-between">
<h2 class="font-semibold text-gray-900 dark:text-white">Document Preview</h2>
<div class="flex items-center space-x-2">
<button onclick="zoomOut()" class="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4"></path>
</svg>
</button>
<span id="zoom-level" class="text-sm text-gray-500">100%</span>
<button onclick="zoomIn()" class="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
</div>
<div class="pdf-container p-4">
<div id="pdf-viewer" class="flex flex-col items-center space-y-4 min-h-[600px]">
<div class="flex items-center justify-center h-full text-gray-400">
<svg class="animate-spin w-8 h-8" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</div>
</div>
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
<div class="flex items-center justify-between">
<button onclick="prevPage()" class="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors">← Previous</button>
<span id="page-info" class="text-sm text-gray-500 dark:text-gray-400">Page 1 of 1</span>
<button onclick="nextPage()" class="px-3 py-1.5 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors">Next →</button>
</div>
</div>
</div>
{{else}}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-100 dark:border-gray-700 p-12 text-center">
<svg class="w-16 h-16 mx-auto text-gray-300 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-1">No PDF Available</h3>
<p class="text-gray-500 dark:text-gray-400">This document doesn't have an associated PDF file</p>
</div>
{{end}}
</div>
</div>
</div>
<!-- Share Modal -->
<div id="share-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div class="fixed inset-0 bg-gray-500/75 dark:bg-gray-900/80" onclick="closeShareModal()"></div>
<div class="relative bg-white dark:bg-gray-800 rounded-2xl shadow-xl max-w-lg w-full p-6 animate-fade-in">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Share Document</h3>
<button onclick="closeShareModal()" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Create new share -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Expiry</label>
<div class="flex gap-2">
<select id="share-days" class="flex-1 px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 border-0 focus:ring-2 focus:ring-brand-500 text-sm">
<option value="1">1 day</option>
<option value="7" selected>7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
<option value="0">Permanent</option>
</select>
<button onclick="createShare()" class="px-4 py-2 bg-brand-600 hover:bg-brand-700 text-white text-sm font-medium rounded-lg transition-colors">
Create Link
</button>
</div>
</div>
<!-- New share result -->
<div id="share-result" class="hidden mb-6 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<p class="text-sm text-green-700 dark:text-green-300 mb-2">Share link created!</p>
<div class="flex gap-2">
<input id="share-url" type="text" readonly class="flex-1 px-3 py-2 text-sm bg-white dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600">
<button onclick="copyShareUrl(this)" class="px-3 py-2 bg-gray-100 dark:bg-gray-600 hover:bg-gray-200 dark:hover:bg-gray-500 text-sm rounded-lg transition-colors">Copy</button>
</div>
</div>
<!-- Existing shares -->
<div>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Active Shares</h4>
<div id="shares-list" class="space-y-2">
<p class="text-sm text-gray-400 italic">Loading...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div id="edit-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div class="fixed inset-0 bg-gray-500/75 dark:bg-gray-900/80" onclick="toggleEdit()"></div>
<div class="relative bg-white dark:bg-gray-800 rounded-2xl shadow-xl max-w-lg w-full p-6 animate-fade-in">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Edit Document</h3>
<button onclick="toggleEdit()" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<form id="edit-form" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
<input type="text" name="title" value="{{.Document.Title}}"
class="w-full px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 border-0 focus:ring-2 focus:ring-brand-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Category</label>
<select name="category" class="w-full px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 border-0 focus:ring-2 focus:ring-brand-500">
{{range $.Categories}}
<option value="{{.}}" {{if eq . $.Document.Category}}selected{{end}}>{{title .}}</option>
{{end}}
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Notes</label>
<textarea name="notes" rows="4"
class="w-full px-4 py-2 rounded-lg bg-gray-100 dark:bg-gray-700 border-0 focus:ring-2 focus:ring-brand-500 resize-none">{{.Document.Notes}}</textarea>
</div>
<div class="flex justify-between pt-4">
<button type="button" onclick="deleteDocument()" class="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors">
Delete Document
</button>
<div class="flex gap-3">
<button type="button" onclick="toggleEdit()" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
Cancel
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-brand-600 hover:bg-brand-700 rounded-lg transition-colors">
Save Changes
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script type="module">
// PDF.js
const pdfjsLib = window['pdfjs-dist/build/pdf'] || await import('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.min.mjs');
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.0.379/pdf.worker.min.mjs';
let pdfDoc = null;
let currentPage = 1;
let scale = 1.0;
const docId = "{{.Document.ID}}";
{{if .Document.PDFPath}}
// Load PDF
async function loadPDF() {
try {
pdfDoc = await pdfjsLib.getDocument('/pdf/' + docId).promise;
document.getElementById('page-info').textContent = `Page 1 of ${pdfDoc.numPages}`;
renderPage(1);
} catch (err) {
document.getElementById('pdf-viewer').innerHTML = `
<div class="flex flex-col items-center justify-center h-64 text-gray-400">
<svg class="w-12 h-12 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<p>Could not load PDF</p>
<p class="text-sm">${err.message}</p>
</div>
`;
}
}
async function renderPage(num) {
const page = await pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: scale });
const canvas = document.createElement('canvas');
canvas.className = 'shadow-lg rounded';
canvas.height = viewport.height;
canvas.width = viewport.width;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
const viewer = document.getElementById('pdf-viewer');
viewer.innerHTML = '';
viewer.appendChild(canvas);
document.getElementById('page-info').textContent = `Page ${num} of ${pdfDoc.numPages}`;
currentPage = num;
}
window.prevPage = () => { if (currentPage > 1) renderPage(currentPage - 1); };
window.nextPage = () => { if (pdfDoc && currentPage < pdfDoc.numPages) renderPage(currentPage + 1); };
window.zoomIn = () => { scale = Math.min(scale + 0.25, 3); document.getElementById('zoom-level').textContent = Math.round(scale * 100) + '%'; if (pdfDoc) renderPage(currentPage); };
window.zoomOut = () => { scale = Math.max(scale - 0.25, 0.5); document.getElementById('zoom-level').textContent = Math.round(scale * 100) + '%'; if (pdfDoc) renderPage(currentPage); };
loadPDF();
{{end}}
</script>
<script>
function toggleEdit() {
document.getElementById('edit-modal').classList.toggle('hidden');
}
const _docMeta = {
id: "{{.Document.ID}}",
title: "{{.Document.Title}}",
notes: "{{.Document.Notes}}"
};
async function inlineUpdateCategory(newCategory) {
const res = await fetch('/api/document/' + _docMeta.id, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: _docMeta.title,
category: newCategory,
notes: _docMeta.notes
})
});
if (res.ok) {
location.reload();
}
}
function copyText() {
const text = document.getElementById('ocr-text').textContent;
navigator.clipboard.writeText(text);
}
document.getElementById('edit-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const data = {
title: form.title.value,
category: form.category.value,
notes: form.notes.value
};
const res = await fetch('/api/document/{{.Document.ID}}', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (res.ok) {
location.reload();
} else {
alert('Failed to save changes');
}
});
// Share functions
function openShareModal() {
document.getElementById('share-modal').classList.remove('hidden');
document.getElementById('share-result').classList.add('hidden');
loadShares();
}
function closeShareModal() {
document.getElementById('share-modal').classList.add('hidden');
}
const EXTERNAL_BASE = 'https://docs.jongsma.me';
function copyToClipboard(text, btn) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => showCopied(btn));
} else {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showCopied(btn);
}
}
function showCopied(btn) {
if (!btn) return;
const orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = orig, 1500);
}
async function createShare() {
const days = parseInt(document.getElementById('share-days').value);
const res = await fetch('/api/share/{{.Document.ID}}', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({days: days})
});
if (res.ok) {
const data = await res.json();
const fullUrl = EXTERNAL_BASE + data.url;
document.getElementById('share-url').value = fullUrl;
document.getElementById('share-result').classList.remove('hidden');
loadShares();
}
}
function copyShareUrl(btn) {
const input = document.getElementById('share-url');
copyToClipboard(input.value, btn);
}
async function loadShares() {
const res = await fetch('/api/shares/{{.Document.ID}}');
const shares = await res.json();
const list = document.getElementById('shares-list');
if (!shares || shares.length === 0) {
list.innerHTML = '<p class="text-sm text-gray-400 italic">No active shares</p>';
return;
}
list.innerHTML = shares.map(s => {
const url = EXTERNAL_BASE + '/s/' + s.Token;
return `
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="text-sm truncate mr-2">
<span class="font-mono text-gray-600 dark:text-gray-300">${url}</span>
<span class="text-gray-400 ml-2">${s.ExpiresAt ? 'expires ' + s.ExpiresAt : 'permanent'}</span>
</div>
<div class="flex gap-2 shrink-0">
<button onclick="copyToClipboard('${url}', this)" class="text-brand-600 dark:text-brand-400 hover:text-brand-700 text-sm">Copy</button>
<button onclick="revokeShare('${s.Token}')" class="text-red-500 hover:text-red-700 text-sm">Revoke</button>
</div>
</div>`;
}).join('');
}
async function revokeShare(token) {
await fetch('/api/share/' + token, {method: 'DELETE'});
loadShares();
}
async function deleteDocument() {
if (!confirm('Are you sure you want to delete this document?')) return;
const res = await fetch('/api/document/{{.Document.ID}}', { method: 'DELETE' });
if (res.ok) {
location.href = '/browse/{{.Document.Category}}';
} else {
alert('Failed to delete document');
}
}
</script>
{{end}}