feat: implementar webapp app_compartir con visor universal, auth y purga 24h para guest
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" class="h-full bg-slate-950 text-slate-100">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "title" .}}Compartir — Pasa archivos y texto al instante{{end}}</title>
|
||||
<!-- Tailwind CSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#f0fdf4',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- Font Awesome Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #0f172a;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-full flex flex-col bg-slate-950 text-slate-100 font-sans selection:bg-brand-500 selection:text-black">
|
||||
<!-- Navbar -->
|
||||
<header class="border-b border-slate-800 bg-slate-900/80 backdrop-blur sticky top-0 z-50">
|
||||
<div class="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-2.5 font-bold text-lg tracking-tight hover:opacity-90 transition">
|
||||
<span class="w-9 h-9 rounded-xl bg-gradient-to-tr from-brand-600 to-emerald-400 flex items-center justify-center text-slate-950 font-black shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-solid fa-share-nodes text-base"></i>
|
||||
</span>
|
||||
<span class="text-white">app<span class="text-brand-500">_compartir</span></span>
|
||||
</a>
|
||||
|
||||
<!-- User status / Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
{{if .Session.IsGuest}}
|
||||
<div class="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs font-medium">
|
||||
<i class="fa-regular fa-clock"></i>
|
||||
<span>Modo Invitado (caduca en 24h)</span>
|
||||
</div>
|
||||
<a href="/login" class="px-3.5 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-semibold border border-slate-700 transition flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-arrow-right-to-bracket text-brand-500"></i>
|
||||
<span>Iniciar Sesión</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<div class="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-medium">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
<span>Almacenamiento Permanente</span>
|
||||
</div>
|
||||
<a href="/mis-archivos" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-medium border border-slate-700 transition flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-folder-open text-brand-500"></i>
|
||||
<span class="hidden md:inline">Mis Archivos</span>
|
||||
</a>
|
||||
<div class="flex items-center gap-2 pl-2 border-l border-slate-800">
|
||||
<span class="text-xs font-semibold text-slate-300 max-w-[120px] truncate">{{.Session.User.Name}}</span>
|
||||
<a href="/logout" title="Cerrar sesión" class="p-1.5 text-slate-400 hover:text-rose-400 transition">
|
||||
<i class="fa-solid fa-power-off text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 max-w-6xl w-full mx-auto px-4 py-8">
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-slate-900 py-6 text-center text-xs text-slate-500">
|
||||
<p>Ecosistema Telcotronics • Pasa archivos, texto y capturas sin usar WhatsApp</p>
|
||||
</footer>
|
||||
|
||||
{{block "scripts" .}}{{end}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Archivo Expirado — app_compartir{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-md mx-auto space-y-6 pt-12 text-center">
|
||||
<div class="w-16 h-16 mx-auto rounded-3xl bg-amber-500/10 border border-amber-500/20 text-amber-400 flex items-center justify-center text-2xl shadow-xl shadow-amber-500/10">
|
||||
<i class="fa-regular fa-clock"></i>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-2xl font-bold text-white">Este archivo ha caducado</h1>
|
||||
<p class="text-xs sm:text-sm text-slate-400 leading-relaxed">
|
||||
El archivo fue subido en <strong class="text-amber-300">Modo Invitado</strong> y de acuerdo a las políticas de privacidad y espacio de la plataforma, se eliminó automáticamente 24 horas después de su subida.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-slate-900 border border-slate-800 text-xs text-slate-400 text-left space-y-2">
|
||||
<p class="font-semibold text-slate-200"><i class="fa-solid fa-lightbulb text-amber-400 mr-1.5"></i> ¿Quieres guardar archivos para siempre?</p>
|
||||
<p>Inicia sesión con tu cuenta de WebControl o Google para que tus archivos se mantengan de forma permanente sin borrarse a las 24 horas.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-3 pt-2">
|
||||
<a href="/" class="px-5 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 text-slate-950 font-bold text-xs transition flex items-center gap-2 shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>Subir nuevo archivo</span>
|
||||
</a>
|
||||
<a href="/login" class="px-5 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold text-xs border border-slate-700 transition">
|
||||
<span>Iniciar Sesión</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,337 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Compartir — Inicio{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-3xl mx-auto space-y-8">
|
||||
<!-- Hero / Header -->
|
||||
<div class="text-center space-y-2">
|
||||
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight text-white">
|
||||
Pasa archivos y texto <span class="text-brand-500">al instante</span>
|
||||
</h1>
|
||||
<p class="text-slate-400 text-sm sm:text-base max-w-xl mx-auto">
|
||||
Olvídate de abrir WhatsApp para pasarte fotos, PDFs o notas entre tu PC y tu teléfono. Sube y escanea el QR o copia el enlace.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mode Selector Tabs -->
|
||||
<div class="flex items-center justify-center p-1 bg-slate-900 border border-slate-800 rounded-2xl max-w-md mx-auto">
|
||||
<button id="tabFilesBtn" onclick="switchTab('files')" class="flex-1 py-2 px-4 rounded-xl text-sm font-semibold transition flex items-center justify-center gap-2 bg-slate-800 text-white shadow">
|
||||
<i class="fa-solid fa-cloud-arrow-up text-brand-500"></i>
|
||||
<span>Archivos / Fotos</span>
|
||||
</button>
|
||||
<button id="tabTextBtn" onclick="switchTab('text')" class="flex-1 py-2 px-4 rounded-xl text-sm font-semibold transition flex items-center justify-center gap-2 text-slate-400 hover:text-white">
|
||||
<i class="fa-solid fa-code text-indigo-400"></i>
|
||||
<span>Texto / Código</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: File Upload -->
|
||||
<div id="tabFiles" class="space-y-4">
|
||||
<div id="dropZone" class="relative group border-2 border-dashed border-slate-700 hover:border-brand-500/80 bg-slate-900/50 hover:bg-slate-900/90 rounded-3xl p-8 sm:p-12 text-center transition-all duration-200 cursor-pointer">
|
||||
<input type="file" id="fileInput" class="hidden" multiple>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="w-16 h-16 mx-auto rounded-2xl bg-brand-500/10 border border-brand-500/20 text-brand-400 flex items-center justify-center text-2xl group-hover:scale-110 transition duration-200">
|
||||
<i class="fa-solid fa-arrow-up-from-bracket"></i>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<p class="text-base sm:text-lg font-semibold text-white">
|
||||
Arrastra tus archivos aquí o <span class="text-brand-400 underline decoration-brand-500/30">haz clic para explorar</span>
|
||||
</p>
|
||||
<p class="text-xs text-slate-400">
|
||||
Imágenes, videos, audios, PDFs, ZIPs o cualquier archivo. También puedes <kbd class="px-2 py-0.5 rounded bg-slate-800 border border-slate-700 text-slate-300 font-mono text-[11px]">Ctrl+V</kbd> para pegar capturas directamente.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-4 pt-2 text-xs text-slate-500">
|
||||
<span class="flex items-center gap-1.5"><i class="fa-solid fa-shield text-emerald-400"></i> Encriptación TLS</span>
|
||||
{{if .Session.IsGuest}}
|
||||
<span class="flex items-center gap-1.5"><i class="fa-regular fa-clock text-amber-400"></i> Borrado automático en 24h</span>
|
||||
{{else}}
|
||||
<span class="flex items-center gap-1.5"><i class="fa-solid fa-database text-emerald-400"></i> Guardado permanente</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uploading overlay / progress -->
|
||||
<div id="uploadProgress" class="hidden absolute inset-0 bg-slate-950/90 backdrop-blur-sm rounded-3xl flex flex-col items-center justify-center p-6 space-y-4">
|
||||
<div class="w-12 h-12 border-4 border-slate-700 border-t-brand-500 rounded-full animate-spin"></div>
|
||||
<div class="text-center space-y-1">
|
||||
<p id="progressText" class="text-sm font-semibold text-white">Subiendo archivo...</p>
|
||||
<div class="w-64 bg-slate-800 rounded-full h-2 overflow-hidden">
|
||||
<div id="progressBar" class="bg-brand-500 h-2 rounded-full w-0 transition-all duration-150"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Text / Code Share -->
|
||||
<div id="tabText" class="hidden space-y-4">
|
||||
<form id="textForm" onsubmit="handleShareText(event)" class="bg-slate-900 border border-slate-800 rounded-3xl p-6 space-y-4 shadow-xl">
|
||||
<div class="space-y-1">
|
||||
<label for="textTitle" class="block text-xs font-semibold text-slate-300">Título o descripción (opcional)</label>
|
||||
<input type="text" id="textTitle" placeholder="Ej: Notas de reunión, comando curl, contraseña temporal..." class="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-indigo-500 transition">
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<label for="textContent" class="block text-xs font-semibold text-slate-300">Contenido (Texto, Código, Enlaces, Markdown)</label>
|
||||
<textarea id="textContent" rows="8" required placeholder="Escribe o pega aquí tu texto o código para compartirlo..." class="w-full bg-slate-950 border border-slate-800 rounded-xl p-4 text-sm font-mono text-slate-100 placeholder-slate-500 focus:outline-none focus:border-indigo-500 transition custom-scrollbar"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<span class="text-xs text-slate-500">
|
||||
{{if .Session.IsGuest}}⏰ Caducará a las 24 horas{{else}}🔒 Se guardará de forma permanente{{end}}
|
||||
</span>
|
||||
<button type="submit" id="submitTextBtn" class="px-6 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-sm transition flex items-center gap-2 shadow-lg shadow-indigo-600/20">
|
||||
<i class="fa-solid fa-paper-plane"></i>
|
||||
<span>Compartir Texto</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Success Result Card (Hidden initially) -->
|
||||
<div id="resultCard" class="hidden bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-2xl animate-fade-in">
|
||||
<div class="flex items-center justify-between border-b border-slate-800 pb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center text-lg">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
<div>
|
||||
<h3 id="resultTitle" class="text-base font-bold text-white">¡Listo para compartir!</h3>
|
||||
<p id="resultSubtitle" class="text-xs text-slate-400">Escanea el QR o copia el enlace</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="resetUpload()" class="text-xs text-slate-400 hover:text-white px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 transition">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Subir otro
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-center">
|
||||
<!-- QR Code Section -->
|
||||
<div class="bg-slate-950 p-6 rounded-2xl border border-slate-800 flex flex-col items-center justify-center space-y-3">
|
||||
<div class="p-2 bg-white rounded-xl shadow-lg">
|
||||
<img id="qrImage" src="" alt="Código QR para escanear" class="w-44 h-44 object-contain">
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 text-center flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-mobile-screen text-brand-400"></i>
|
||||
<span>Apunta con la cámara de tu móvil para abrir</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Link & Actions Section -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-semibold text-slate-400">Enlace directo para compartir</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" id="shareUrlInput" readonly class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2.5 text-sm font-mono text-slate-200 select-all focus:outline-none">
|
||||
<button onclick="copyShareUrl()" id="copyBtn" class="px-4 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 text-slate-950 font-bold text-sm transition flex items-center gap-1.5 shrink-0 shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-regular fa-copy"></i>
|
||||
<span id="copyBtnText">Copiar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a id="openViewerBtn" href="#" target="_blank" class="flex-1 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-white font-semibold text-sm transition text-center flex items-center justify-center gap-2">
|
||||
<i class="fa-solid fa-eye text-brand-400"></i>
|
||||
<span>Abrir Visor</span>
|
||||
</a>
|
||||
<a id="directDownloadBtn" href="#" class="py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-slate-300 font-semibold text-sm transition flex items-center justify-center gap-2" title="Descarga directa">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Expiration notice -->
|
||||
<div id="retentionNotice" class="p-3 rounded-xl text-xs flex items-center gap-2">
|
||||
<!-- Dynamic notice text inserted via JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const uploadProgress = document.getElementById('uploadProgress');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressText = document.getElementById('progressText');
|
||||
const resultCard = document.getElementById('resultCard');
|
||||
|
||||
function switchTab(tab) {
|
||||
if (tab === 'files') {
|
||||
document.getElementById('tabFiles').classList.remove('hidden');
|
||||
document.getElementById('tabText').classList.add('hidden');
|
||||
document.getElementById('tabFilesBtn').className = "flex-1 py-2 px-4 rounded-xl text-sm font-semibold transition flex items-center justify-center gap-2 bg-slate-800 text-white shadow";
|
||||
document.getElementById('tabTextBtn').className = "flex-1 py-2 px-4 rounded-xl text-sm font-semibold transition flex items-center justify-center gap-2 text-slate-400 hover:text-white";
|
||||
} else {
|
||||
document.getElementById('tabFiles').classList.add('hidden');
|
||||
document.getElementById('tabText').classList.remove('hidden');
|
||||
document.getElementById('tabTextBtn').className = "flex-1 py-2 px-4 rounded-xl text-sm font-semibold transition flex items-center justify-center gap-2 bg-slate-800 text-white shadow";
|
||||
document.getElementById('tabFilesBtn').className = "flex-1 py-2 px-4 rounded-xl text-sm font-semibold transition flex items-center justify-center gap-2 text-slate-400 hover:text-white";
|
||||
}
|
||||
}
|
||||
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('border-brand-500', 'bg-slate-900/90');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('border-brand-500', 'bg-slate-900/90');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('border-brand-500', 'bg-slate-900/90');
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
uploadFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (fileInput.files.length > 0) {
|
||||
uploadFile(fileInput.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Ctrl+V paste anywhere on window
|
||||
window.addEventListener('paste', (e) => {
|
||||
// If active element is a textarea or text input, let default paste happen
|
||||
if (document.activeElement.tagName === 'TEXTAREA' || document.activeElement.tagName === 'INPUT') {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
for (let item of items) {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
uploadFile(file);
|
||||
break;
|
||||
}
|
||||
} else if (item.kind === 'string' && item.type === 'text/plain') {
|
||||
item.getAsString((text) => {
|
||||
switchTab('text');
|
||||
document.getElementById('textContent').value = text;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function uploadFile(file) {
|
||||
uploadProgress.classList.remove('hidden');
|
||||
progressBar.style.width = '0%';
|
||||
progressText.innerText = `Subiendo ${file.name}...`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/upload', true);
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
progressBar.style.width = percent + '%';
|
||||
progressText.innerText = `Subiendo ${file.name} (${percent}%)...`;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
uploadProgress.classList.add('hidden');
|
||||
if (xhr.status === 200) {
|
||||
const res = JSON.parse(xhr.responseText);
|
||||
showResult(res);
|
||||
} else {
|
||||
alert('Error al subir el archivo: ' + xhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
uploadProgress.classList.add('hidden');
|
||||
alert('Error de red al subir el archivo.');
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
async function handleShareText(e) {
|
||||
e.preventDefault();
|
||||
const title = document.getElementById('textTitle').value;
|
||||
const text = document.getElementById('textContent').value;
|
||||
const btn = document.getElementById('submitTextBtn');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fa-solid fa-circle-notch animate-spin"></i> Compartiendo...`;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/share-text', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, text })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (resp.ok) {
|
||||
showResult(data);
|
||||
} else {
|
||||
alert('Error al compartir texto: ' + (data.error || 'Desconocido'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error de red: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `<i class="fa-solid fa-paper-plane"></i> <span>Compartir Texto</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function showResult(data) {
|
||||
document.getElementById('tabFiles').classList.add('hidden');
|
||||
document.getElementById('tabText').classList.add('hidden');
|
||||
resultCard.classList.remove('hidden');
|
||||
|
||||
document.getElementById('resultTitle').innerText = data.title || 'Contenido compartido';
|
||||
document.getElementById('shareUrlInput').value = data.viewer_url;
|
||||
document.getElementById('qrImage').src = `/qr/${data.id}`;
|
||||
document.getElementById('openViewerBtn').href = data.viewer_url;
|
||||
document.getElementById('directDownloadBtn').href = data.download_url;
|
||||
|
||||
const notice = document.getElementById('retentionNotice');
|
||||
if (data.is_guest) {
|
||||
notice.className = "p-3 rounded-xl text-xs flex items-center gap-2 bg-amber-500/10 border border-amber-500/20 text-amber-300";
|
||||
notice.innerHTML = `<i class="fa-regular fa-clock text-amber-400"></i> <span><strong>Sesión Invitado:</strong> Este contenido se eliminará automáticamente en 24 horas (${data.expires_in || '24h'}).</span>`;
|
||||
} else {
|
||||
notice.className = "p-3 rounded-xl text-xs flex items-center gap-2 bg-emerald-500/10 border border-emerald-500/20 text-emerald-300";
|
||||
notice.innerHTML = `<i class="fa-solid fa-shield text-emerald-400"></i> <span><strong>Cuenta registrada:</strong> Almacenamiento permanente. Puedes gestionarlo desde Mis Archivos.</span>`;
|
||||
}
|
||||
|
||||
resultCard.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function resetUpload() {
|
||||
resultCard.classList.add('hidden');
|
||||
fileInput.value = '';
|
||||
document.getElementById('textContent').value = '';
|
||||
document.getElementById('textTitle').value = '';
|
||||
switchTab('files');
|
||||
}
|
||||
|
||||
function copyShareUrl() {
|
||||
const input = document.getElementById('shareUrlInput');
|
||||
input.select();
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
const btnText = document.getElementById('copyBtnText');
|
||||
btnText.innerText = '¡Copiado!';
|
||||
setTimeout(() => {
|
||||
btnText.innerText = 'Copiar';
|
||||
}, 2500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,81 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Iniciar Sesión — app_compartir{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-md mx-auto space-y-6 pt-4">
|
||||
<div class="text-center space-y-2">
|
||||
<div class="w-12 h-12 mx-auto rounded-2xl bg-gradient-to-tr from-brand-600 to-emerald-400 flex items-center justify-center text-slate-950 text-xl font-black shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-white">Acceso a app_compartir</h1>
|
||||
<p class="text-xs text-slate-400">
|
||||
Inicia sesión para que tus archivos se guarden de forma <strong class="text-emerald-400 font-semibold">permanente</strong> y no caduquen en 24h.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="p-3 rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span>{{.Error}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-2xl">
|
||||
<!-- Google OAuth Option -->
|
||||
{{if .HasGoogle}}
|
||||
<div>
|
||||
<a href="/auth/google" class="w-full py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700 text-white font-semibold text-sm transition flex items-center justify-center gap-3">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.06H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.94l2.85-2.22.81-.63z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84c.87-2.6 3.3-4.52 6.16-4.52z"/>
|
||||
</svg>
|
||||
<span>Continuar con Google</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="relative flex items-center justify-center">
|
||||
<div class="border-t border-slate-800 w-full"></div>
|
||||
<span class="bg-slate-900 px-3 text-[11px] text-slate-500 uppercase tracking-wider font-semibold">o con WebControl</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- WebControl Form -->
|
||||
<form action="/login/webcontrol" method="POST" class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<label for="email" class="block text-xs font-semibold text-slate-300">Correo electrónico / Usuario</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3.5 flex items-center text-slate-500 text-xs">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
</span>
|
||||
<input type="text" id="email" name="email" required placeholder="usuario@telcotronics.com" class="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-4 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-brand-500 transition">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<label for="password" class="block text-xs font-semibold text-slate-300">Contraseña</label>
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 pl-3.5 flex items-center text-slate-500 text-xs">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
</span>
|
||||
<input type="password" id="password" name="password" required placeholder="••••••••" class="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-4 py-2.5 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-brand-500 transition">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full py-2.5 px-4 rounded-xl bg-brand-600 hover:bg-brand-500 text-slate-950 font-bold text-sm transition flex items-center justify-center gap-2 shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-solid fa-building-shield"></i>
|
||||
<span>Ingresar con WebControl</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="border-t border-slate-800 pt-4 text-center">
|
||||
<a href="/" class="text-xs text-slate-400 hover:text-white transition flex items-center justify-center gap-1.5">
|
||||
<i class="fa-solid fa-arrow-left"></i>
|
||||
<span>Continuar como Invitado (sin registro)</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,109 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Mis Archivos — app_compartir{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-5xl mx-auto space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800 pb-5">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Mis Archivos Guardados</h1>
|
||||
<p class="text-xs text-slate-400">
|
||||
Archivos y notas asociados a tu cuenta. Se conservan de forma <span class="text-emerald-400 font-semibold">permanente</span> hasta que decidas eliminarlos.
|
||||
</p>
|
||||
</div>
|
||||
<a href="/" class="px-4 py-2 rounded-xl bg-brand-600 hover:bg-brand-500 text-slate-950 font-bold text-xs transition flex items-center gap-2 shadow-lg shadow-brand-500/20 shrink-0">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
<span>Subir nuevo archivo</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{if eq (len .Shares) 0}}
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-12 text-center space-y-4">
|
||||
<div class="w-16 h-16 mx-auto rounded-2xl bg-slate-800 text-slate-400 flex items-center justify-center text-2xl">
|
||||
<i class="fa-regular fa-folder-open"></i>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-semibold text-white">No tienes archivos guardados aún</h3>
|
||||
<p class="text-xs text-slate-400">Sube fotos, videos, documentos o notas para acceder a ellos en cualquier momento.</p>
|
||||
</div>
|
||||
<a href="/" class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-brand-400 text-xs font-semibold border border-slate-700 transition">
|
||||
<span>Comenzar a compartir</span>
|
||||
</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden shadow-xl">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-950/60 border-b border-slate-800 text-slate-400 uppercase font-semibold">
|
||||
<tr>
|
||||
<th class="py-3.5 px-4">Archivo / Título</th>
|
||||
<th class="py-3.5 px-4">Tipo</th>
|
||||
<th class="py-3.5 px-4">Tamaño</th>
|
||||
<th class="py-3.5 px-4">Visitas</th>
|
||||
<th class="py-3.5 px-4">Fecha</th>
|
||||
<th class="py-3.5 px-4 text-right">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/60 text-slate-200">
|
||||
{{range .Shares}}
|
||||
<tr class="hover:bg-slate-800/40 transition">
|
||||
<td class="py-3.5 px-4 font-medium max-w-xs truncate">
|
||||
<a href="/v/{{.ID}}" target="_blank" class="hover:text-brand-400 flex items-center gap-2 truncate" title="{{.Title}}">
|
||||
{{if eq .Type "text"}}
|
||||
<i class="fa-solid fa-code text-indigo-400 shrink-0"></i>
|
||||
{{else}}
|
||||
<i class="fa-solid fa-file text-slate-400 shrink-0"></i>
|
||||
{{end}}
|
||||
<span class="truncate">{{.Title}}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="py-3.5 px-4 text-slate-400">{{.MimeType}}</td>
|
||||
<td class="py-3.5 px-4 font-mono text-slate-300">{{.FormattedSize}}</td>
|
||||
<td class="py-3.5 px-4">{{.ViewsCount}}</td>
|
||||
<td class="py-3.5 px-4 text-slate-400">{{.CreatedAt.Format "02/01/2006 15:04"}}</td>
|
||||
<td class="py-3.5 px-4 text-right space-x-1">
|
||||
<a href="/v/{{.ID}}" target="_blank" class="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 transition" title="Abrir visor">
|
||||
<i class="fa-solid fa-eye text-xs"></i>
|
||||
</a>
|
||||
{{if ne .Type "text"}}
|
||||
<a href="/d/{{.ID}}" class="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 transition" title="Descargar">
|
||||
<i class="fa-solid fa-download text-xs"></i>
|
||||
</a>
|
||||
{{end}}
|
||||
<button onclick="deleteShare('{{.ID}}')" class="p-1.5 rounded-lg bg-slate-800 hover:bg-rose-900/40 text-slate-400 hover:text-rose-400 transition" title="Eliminar definitivamente">
|
||||
<i class="fa-solid fa-trash text-xs"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
async function deleteShare(id) {
|
||||
if (!confirm('¿Estás seguro de que deseas eliminar este archivo? Esta acción no se puede deshacer.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/shares/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const data = await resp.json();
|
||||
alert('Error al eliminar: ' + (data.error || 'Desconocido'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error de red al intentar eliminar el archivo.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,228 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}{{.Share.Title}} — Visor de app_compartir{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="max-w-4xl mx-auto space-y-6">
|
||||
<!-- Header Card -->
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-5 sm:p-6 shadow-xl flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="space-y-1.5 min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h1 class="text-xl sm:text-2xl font-bold text-white truncate max-w-lg" title="{{.Share.Title}}">
|
||||
{{.Share.Title}}
|
||||
</h1>
|
||||
{{if .Share.IsGuest}}
|
||||
<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-500/10 border border-amber-500/20 text-amber-400">
|
||||
<i class="fa-regular fa-clock mr-1"></i> Caduca en 24h
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-500/10 border border-emerald-500/20 text-emerald-400">
|
||||
<i class="fa-solid fa-shield mr-1"></i> Permanente
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-slate-400">
|
||||
<span><i class="fa-solid fa-user text-slate-500 mr-1"></i> {{.Share.UserDisplay}}</span>
|
||||
{{if ne .Share.Type "text"}}
|
||||
<span><i class="fa-solid fa-hard-drive text-slate-500 mr-1"></i> {{.Share.FormattedSize}}</span>
|
||||
{{end}}
|
||||
<span><i class="fa-solid fa-eye text-slate-500 mr-1"></i> {{.Share.ViewsCount}} visitas</span>
|
||||
<span><i class="fa-solid fa-calendar-day text-slate-500 mr-1"></i> {{.FormattedDate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
{{if ne .Share.Type "text"}}
|
||||
<a href="/d/{{.Share.ID}}" class="px-4 py-2 rounded-xl bg-brand-600 hover:bg-brand-500 text-slate-950 font-bold text-sm transition flex items-center gap-1.5 shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
<span>Descargar</span>
|
||||
</a>
|
||||
<a href="/raw/{{.Share.ID}}" target="_blank" class="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition" title="Ver archivo original / raw">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
</a>
|
||||
{{else}}
|
||||
<button onclick="copyTextContent()" id="copyTextBtn" class="px-4 py-2 rounded-xl bg-brand-600 hover:bg-brand-500 text-slate-950 font-bold text-sm transition flex items-center gap-1.5 shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-regular fa-copy"></i>
|
||||
<span id="copyTextBtnLabel">Copiar texto</span>
|
||||
</button>
|
||||
{{end}}
|
||||
|
||||
<button onclick="toggleQRModal()" class="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition" title="Ver código QR para móvil">
|
||||
<i class="fa-solid fa-qrcode"></i>
|
||||
</button>
|
||||
|
||||
<button onclick="copyCurrentURL()" class="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition" title="Copiar enlace">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Viewer Body -->
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-4 sm:p-6 shadow-2xl overflow-hidden">
|
||||
{{if .IsImage}}
|
||||
<!-- Image Viewer -->
|
||||
<div class="flex flex-col items-center justify-center space-y-3">
|
||||
<div class="relative group max-h-[80vh] flex items-center justify-center rounded-2xl overflow-hidden bg-slate-950/60 p-2 border border-slate-800/80">
|
||||
<img id="viewerImage" src="/raw/{{.Share.ID}}" alt="{{.Share.Title}}" class="max-h-[75vh] w-auto max-w-full object-contain rounded-xl shadow-lg transition-transform duration-200">
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="toggleImageZoom()" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-300 border border-slate-700 transition flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-magnifying-glass-plus text-brand-400"></i>
|
||||
<span id="zoomBtnLabel">Zoom 100%</span>
|
||||
</button>
|
||||
<a href="/raw/{{.Share.ID}}" target="_blank" class="px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-xs text-slate-300 border border-slate-700 transition flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-expand text-brand-400"></i>
|
||||
<span>Pantalla Completa</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else if .IsVideo}}
|
||||
<!-- Video Viewer -->
|
||||
<div class="rounded-2xl overflow-hidden bg-black border border-slate-800 flex items-center justify-center">
|
||||
<video controls autoplay playsinline class="w-full max-h-[75vh] rounded-2xl">
|
||||
<source src="/raw/{{.Share.ID}}" type="{{.Share.MimeType}}">
|
||||
Tu navegador no soporta la reproducción de video HTML5.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
{{else if .IsAudio}}
|
||||
<!-- Audio Viewer -->
|
||||
<div class="p-8 sm:p-12 flex flex-col items-center justify-center space-y-6 bg-slate-950/60 rounded-2xl border border-slate-800/80">
|
||||
<div class="w-20 h-20 rounded-3xl bg-brand-500/10 border border-brand-500/20 text-brand-400 flex items-center justify-center text-3xl shadow-xl shadow-brand-500/10 animate-pulse">
|
||||
<i class="fa-solid fa-music"></i>
|
||||
</div>
|
||||
<div class="text-center space-y-1">
|
||||
<h3 class="text-lg font-bold text-white">{{.Share.Title}}</h3>
|
||||
<p class="text-xs text-slate-400">{{.Share.FormattedSize}} • {{.Share.MimeType}}</p>
|
||||
</div>
|
||||
<div class="w-full max-w-md">
|
||||
<audio controls autoplay class="w-full">
|
||||
<source src="/raw/{{.Share.ID}}" type="{{.Share.MimeType}}">
|
||||
Tu navegador no soporta audio HTML5.
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else if .IsPDF}}
|
||||
<!-- PDF Viewer -->
|
||||
<div class="space-y-3">
|
||||
<div class="w-full h-[80vh] rounded-2xl overflow-hidden border border-slate-800 bg-slate-950">
|
||||
<iframe src="/raw/{{.Share.ID}}" class="w-full h-full border-none">
|
||||
Tu navegador no soporta iframe para PDFs. <a href="/raw/{{.Share.ID}}" class="text-brand-400 underline">Haz clic para descargarlo</a>.
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else if or (eq .Share.Type "text") .IsText}}
|
||||
<!-- Text / Code Viewer -->
|
||||
<div class="relative space-y-2">
|
||||
<div class="flex items-center justify-between text-xs text-slate-400 px-1">
|
||||
<span><i class="fa-solid fa-file-lines mr-1 text-slate-500"></i> Formato de texto</span>
|
||||
<span>{{len .Share.TextContent}} caracteres</span>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<pre id="rawTextContent" class="p-5 rounded-2xl bg-slate-950 border border-slate-800 font-mono text-xs sm:text-sm text-slate-100 overflow-x-auto whitespace-pre-wrap leading-relaxed custom-scrollbar selection:bg-brand-500 selection:text-black">{{.Share.TextContent}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<!-- Generic File Download Card -->
|
||||
<div class="p-10 sm:p-14 flex flex-col items-center justify-center space-y-6 text-center bg-slate-950/60 rounded-2xl border border-slate-800/80">
|
||||
<div class="w-20 h-20 rounded-3xl bg-slate-800 border border-slate-700 text-slate-300 flex items-center justify-center text-3xl shadow-xl">
|
||||
<i class="fa-solid fa-file-zipper"></i>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-lg font-bold text-white max-w-md break-all">{{.Share.Title}}</h3>
|
||||
<p class="text-xs text-slate-400">{{.Share.FormattedSize}} • {{.Share.MimeType}}</p>
|
||||
</div>
|
||||
<a href="/d/{{.Share.ID}}" class="px-6 py-3 rounded-xl bg-brand-600 hover:bg-brand-500 text-slate-950 font-bold text-sm transition flex items-center gap-2 shadow-lg shadow-brand-500/20">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
<span>Descargar archivo ahora</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Quick Share QR Drawer for Mobile -->
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-5 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3 text-center sm:text-left">
|
||||
<div class="w-10 h-10 rounded-xl bg-brand-500/10 border border-brand-500/20 text-brand-400 flex items-center justify-center text-lg shrink-0">
|
||||
<i class="fa-solid fa-mobile-screen"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-white">¿Quieres abrir esto en tu teléfono?</p>
|
||||
<p class="text-xs text-slate-400">Escanea el código QR sin enviar mensajes por WhatsApp</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="/qr/{{.Share.ID}}" alt="QR Code" class="w-16 h-16 rounded-lg bg-white p-1 shadow">
|
||||
<button onclick="toggleQRModal()" class="px-3.5 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-200 border border-slate-700 transition">
|
||||
Ver QR en grande
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Modal -->
|
||||
<div id="qrModal" class="hidden fixed inset-0 z-50 bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-3xl p-6 max-w-sm w-full space-y-4 text-center shadow-2xl animate-fade-in">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-bold text-white">Escanea con tu teléfono</h3>
|
||||
<button onclick="toggleQRModal()" class="text-slate-400 hover:text-white p-1"><i class="fa-solid fa-xmark"></i></button>
|
||||
</div>
|
||||
<div class="p-3 bg-white rounded-2xl shadow-inner inline-block">
|
||||
<img src="/qr/{{.Share.ID}}" alt="Código QR grande" class="w-56 h-56 object-contain">
|
||||
</div>
|
||||
<p class="text-xs text-slate-400">Apunta la cámara de tu móvil para abrir este visor directamente.</p>
|
||||
<button onclick="toggleQRModal()" class="w-full py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-sm font-semibold text-white transition">
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
let isZoomed = false;
|
||||
function toggleImageZoom() {
|
||||
const img = document.getElementById('viewerImage');
|
||||
const label = document.getElementById('zoomBtnLabel');
|
||||
if (!img) return;
|
||||
isZoomed = !isZoomed;
|
||||
if (isZoomed) {
|
||||
img.classList.remove('max-h-[75vh]');
|
||||
img.classList.add('max-h-none', 'scale-110');
|
||||
label.innerText = 'Restablecer';
|
||||
} else {
|
||||
img.classList.add('max-h-[75vh]');
|
||||
img.classList.remove('max-h-none', 'scale-110');
|
||||
label.innerText = 'Zoom 100%';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleQRModal() {
|
||||
const modal = document.getElementById('qrModal');
|
||||
modal.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function copyCurrentURL() {
|
||||
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||
alert('¡Enlace copiado al portapapeles!');
|
||||
});
|
||||
}
|
||||
|
||||
function copyTextContent() {
|
||||
const text = document.getElementById('rawTextContent').innerText;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const btn = document.getElementById('copyTextBtnLabel');
|
||||
btn.innerText = '¡Copiado!';
|
||||
setTimeout(() => {
|
||||
btn.innerText = 'Copiar texto';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user