feat: implementar webapp app_compartir con visor universal, auth y purga 24h para guest

This commit is contained in:
2026-10-13 22:46:02 -05:00
parent 6ea00e4ec5
commit 8519590cc7
24 changed files with 2970 additions and 71 deletions
+96
View File
@@ -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>
+32
View File
@@ -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}}
+337
View File
@@ -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}}
+81
View File
@@ -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}}
+109
View File
@@ -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}}
+228
View File
@@ -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}}