diff options
| author | grothedev <grothedev@gmail.com> | 2025-12-28 16:13:33 -0500 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2025-12-28 16:13:33 -0500 |
| commit | f978ad7db04ced4cbcf04a82bf6f0cc3f4ce66a3 (patch) | |
| tree | ce97a8ffe34bd5907a0efdd4c453b64f10e6f568 /views.ts | |
| parent | 119dd4129b91d8a3437a4f4e099b74991ee0044d (diff) | |
use template files
Diffstat (limited to 'views.ts')
| -rw-r--r-- | views.ts | 705 |
1 files changed, 108 insertions, 597 deletions
@@ -1,282 +1,73 @@ import { config } from "./config.ts"; import { formatFileSize, getMimeType, isViewableInBrowser } from "./fileUtils.ts"; -function renderTemplate(title: string, content: string, scripts = "") { - return `<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>${title}</title> - <link rel="stylesheet" href="/css/skeleton.css"> - <link rel="stylesheet" href="/css/home.css"> -</head> -<body> - <header> - <h2>File Upload</h2> - <section class="status"> - <p> </p> - </section> - </header> - <main> - <center>- - -</center> - ${content} - <center>- - -</center> - </main> - ${scripts} -</body> -</html>`; -} - -export function renderUploadPage(chunked = false) { - const maxSizeMB = Math.floor(config.maxFileSize / (1024 * 1024)); - const expirationDays = Math.floor(config.fileExpiration / (24 * 60 * 60 * 1000)); - - const content = ` -<section> - <h5>Shared Files</h5> - <p>Welcome to the file sharing service</p> - <center> - <a href="/browse">Browse All Files</a> | - <a href="/gallery">View Gallery</a> - </center> -</section> - -<section id="fileupload${chunked ? '-js' : '-nojs'}"> - <div class="widget"> - <h4>File Upload ${chunked ? '(Chunked)' : ''}</h4> - <h6> - ${chunked ? '<a href="/">Simple Upload</a>' : '<a href="/chunked">Chunked Upload</a>'} - | <a href="/browse">Browse Files</a> - | <a href="/gallery">Gallery</a> - </h6> - <form id="uploadForm" ${chunked ? '' : 'action="/upload" method="POST" enctype="multipart/form-data"'}> - <input type="file" id="fileInput" name="files" multiple ${chunked ? '' : 'required'}> - <div class="file-info" id="fileInfo" style="margin: 10px 0; color: #666;"> - Max size: ${maxSizeMB}MB per file - </div> - <button type="submit" id="uploadBtn">Upload</button> - </form> - - <div class="progress" id="progress" style="display: none; margin-top: 15px;"> - <div style="width: 100%; height: 30px; background: #f0f0f0; border-radius: 4px; overflow: hidden;"> - <div id="progressFill" style="height: 100%; background: #33C3F0; width: 0%; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: 600; transition: width 0.3s;">0%</div> - </div> - </div> - - <div class="result" id="result" style="display: none; margin-top: 15px; padding: 15px; border-radius: 4px;"></div> - - <div style="background: #e7f3ff; padding: 15px; border-radius: 4px; margin-top: 15px; font-size: 14px; color: #004085;"> - <strong>Info:</strong> Files expire after ${expirationDays} days. - Rate limit: ${config.rateLimit.maxUploads} uploads per ${config.rateLimit.windowMs / 60000} minutes. - </div> - </div> -</section> - -<footer> - <p>Last updated December 28th, 2025</p> -</footer>`; +const templateCache = new Map<string, string>(); - const scripts = chunked ? getChunkedUploadScript() : getSimpleUploadScript(); +async function loadTemplate(path: string): Promise<string> { + if (templateCache.has(path)) { + return templateCache.get(path)!; + } - return renderTemplate("File Upload", content, scripts); + const content = await Deno.readTextFile(path); + templateCache.set(path, content); + return content; } -function getSimpleUploadScript() { - return `<script> - const fileInput = document.getElementById('fileInput'); - const fileInfo = document.getElementById('fileInfo'); - const form = document.getElementById('uploadForm'); - const result = document.getElementById('result'); - const uploadBtn = document.getElementById('uploadBtn'); - - fileInput.addEventListener('change', (e) => { - const files = e.target.files; - if (files.length > 0) { - const names = Array.from(files).map(f => f.name).join(', '); - fileInfo.textContent = \`Selected: \${files.length} file(s) - \${names}\`; - } - }); - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - const files = fileInput.files; - - if (files.length === 0) { - showResult('Please select at least one file', 'error'); - return; - } - - uploadBtn.disabled = true; - uploadBtn.textContent = 'Uploading...'; - - const formData = new FormData(); - for (let i = 0; i < files.length; i++) { - formData.append('files', files[i]); - } - - try { - const response = await fetch('/upload', { - method: 'POST', - body: formData - }); - - const data = await response.json(); - - if (response.ok) { - const links = data.files.map(f => - \`<a href="/download/\${f}" target="_blank" style="display: block; margin: 5px 0;">\${window.location.origin}/download/\${f}</a>\` - ).join(''); - showResult('Upload successful!<br>' + links, 'success'); - form.reset(); - fileInfo.textContent = 'Max size: ${Math.floor(config.maxFileSize / (1024 * 1024))}MB per file'; - } else { - showResult('Error: ' + data.error, 'error'); - } - } catch (error) { - showResult('Upload failed: ' + error.message, 'error'); - } finally { - uploadBtn.disabled = false; - uploadBtn.textContent = 'Upload'; - } - }); - - function showResult(message, type) { - result.innerHTML = message; - result.style.display = 'block'; - if (type === 'success') { - result.style.background = '#d4edda'; - result.style.color = '#155724'; - result.style.border = '1px solid #c3e6cb'; - } else { - result.style.background = '#f8d7da'; - result.style.color = '#721c24'; - result.style.border = '1px solid #f5c6cb'; - } - } - </script>`; +function replaceVariables(template: string, vars: Record<string, string>): string { + let result = template; + for (const [key, value] of Object.entries(vars)) { + result = result.replaceAll(`{{${key}}}`, value); + } + return result; } -function getChunkedUploadScript() { - return `<script> - const fileInput = document.getElementById('fileInput'); - const fileInfo = document.getElementById('fileInfo'); - const form = document.getElementById('uploadForm'); - const result = document.getElementById('result'); - const progress = document.getElementById('progress'); - const progressFill = document.getElementById('progressFill'); - const uploadBtn = document.getElementById('uploadBtn'); - const CHUNK_SIZE = ${config.chunkSize}; - - fileInput.addEventListener('change', (e) => { - const files = e.target.files; - if (files.length > 0) { - const names = Array.from(files).map(f => f.name).join(', '); - const totalSize = Array.from(files).reduce((sum, f) => sum + f.size, 0); - fileInfo.textContent = \`Selected: \${files.length} file(s) - \${formatBytes(totalSize)}\`; - } - }); - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - const files = fileInput.files; - - if (files.length === 0) { - showResult('Please select at least one file', 'error'); - return; - } - - uploadBtn.disabled = true; - uploadBtn.textContent = 'Uploading...'; - progress.style.display = 'block'; - result.style.display = 'none'; - - try { - const uploadedFiles = []; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - const filename = await uploadFileChunked(file, i, files.length); - uploadedFiles.push(filename); - } - - const links = uploadedFiles.map(f => - \`<a href="/download/\${f}" target="_blank" style="display: block; margin: 5px 0;">\${window.location.origin}/download/\${f}</a>\` - ).join(''); - showResult('Upload successful!<br>' + links, 'success'); - form.reset(); - fileInfo.textContent = 'Max size: ${Math.floor(config.maxFileSize / (1024 * 1024))}MB per file'; - } catch (error) { - showResult('Upload failed: ' + error.message, 'error'); - } finally { - uploadBtn.disabled = false; - uploadBtn.textContent = 'Upload'; - progress.style.display = 'none'; - } - }); - - async function uploadFileChunked(file, fileIndex, totalFiles) { - const totalChunks = Math.ceil(file.size / CHUNK_SIZE); - - for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { - const start = chunkIndex * CHUNK_SIZE; - const end = Math.min(start + CHUNK_SIZE, file.size); - const chunk = file.slice(start, end); - - const formData = new FormData(); - formData.append('chunk', chunk); - formData.append('filename', file.name); - formData.append('chunkIndex', chunkIndex); - formData.append('totalChunks', totalChunks); +async function renderLayout(title: string, content: string, scripts = "", styles = ""): Promise<string> { + const layout = await loadTemplate("./templates/layout.html"); + return replaceVariables(layout, { + title, + content, + scripts, + styles, + }); +} - const response = await fetch('/upload-chunk', { - method: 'POST', - body: formData - }); +export async function renderUploadPage(chunked = false): Promise<string> { + const maxSizeMB = Math.floor(config.maxFileSize / (1024 * 1024)); + const expirationDays = Math.floor(config.fileExpiration / (24 * 60 * 60 * 1000)); - if (!response.ok) { - const data = await response.json(); - throw new Error(data.error || 'Upload failed'); - } + const uploadTemplate = await loadTemplate("./templates/upload.html"); - const overallProgress = ((fileIndex * totalChunks + chunkIndex + 1) / (totalFiles * totalChunks)) * 100; - updateProgress(overallProgress); - } + const scriptTemplate = chunked + ? await loadTemplate("./templates/scripts/chunked-upload.js") + : await loadTemplate("./templates/scripts/simple-upload.js"); - return file.name; - } + const scriptContent = replaceVariables(scriptTemplate, { + "max-size": String(maxSizeMB), + "chunk-size": String(config.chunkSize), + }); - function updateProgress(percent) { - const rounded = Math.round(percent); - progressFill.style.width = rounded + '%'; - progressFill.textContent = rounded + '%'; - } + const content = replaceVariables(uploadTemplate, { + "section-suffix": chunked ? "-js" : "-nojs", + "upload-type": chunked ? "(Chunked)" : "", + "nav-links": chunked + ? '<a href="/">Simple Upload</a>' + : '<a href="/chunked">Chunked Upload</a>', + "form-attrs": chunked ? "" : 'action="/upload" method="POST" enctype="multipart/form-data"', + "input-attrs": chunked ? "" : "required", + "max-size": String(maxSizeMB), + "expiration-days": String(expirationDays), + "rate-limit-max": String(config.rateLimit.maxUploads), + "rate-limit-minutes": String(config.rateLimit.windowMs / 60000), + }); - function showResult(message, type) { - result.innerHTML = message; - result.style.display = 'block'; - if (type === 'success') { - result.style.background = '#d4edda'; - result.style.color = '#155724'; - result.style.border = '1px solid #c3e6cb'; - } else { - result.style.background = '#f8d7da'; - result.style.color = '#721c24'; - result.style.border = '1px solid #f5c6cb'; - } - } + const scripts = `<script>\n${scriptContent}\n</script>`; - function formatBytes(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; - } - </script>`; + return renderLayout("File Upload", content, scripts); } -export function renderBrowsePage(files: Array<{ name: string; size: number; uploaded: Date }>) { +export async function renderBrowsePage(files: Array<{ name: string; size: number; uploaded: Date }>): Promise<string> { + const browseTemplate = await loadTemplate("./templates/browse.html"); + const fileRows = files.map(file => { const mimeType = getMimeType(file.name); const viewable = isViewableInBrowser(mimeType); @@ -294,51 +85,39 @@ export function renderBrowsePage(files: Array<{ name: string; size: number; uplo `; }).join(''); - const content = ` -<section> - <h5>Browse Files</h5> - <h6> - <a href="/">Simple Upload</a> | - <a href="/chunked">Chunked Upload</a> | - <a href="/browse">Browse Files</a> | - <a href="/gallery">Gallery</a> - </h6> -</section> - -<section> - <div class="widget"> - ${files.length > 0 ? ` - <table style="width: 100%; border-collapse: collapse;"> - <thead> - <tr> - <th>Filename</th> - <th>Size</th> - <th>Uploaded</th> - <th>Actions</th> - </tr> - </thead> - <tbody> - ${fileRows} - </tbody> - </table> - ` : ` - <div style="text-align: center; padding: 60px 20px; color: #666;"> - <div style="font-size: 64px; margin-bottom: 20px;">📭</div> - <h3>No files uploaded yet</h3> - <p>Upload some files to see them here!</p> - </div> - `} - </div> -</section> + const filesContent = files.length > 0 ? ` + <table style="width: 100%; border-collapse: collapse;"> + <thead> + <tr> + <th>Filename</th> + <th>Size</th> + <th>Uploaded</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + ${fileRows} + </tbody> + </table> + ` : ` + <div style="text-align: center; padding: 60px 20px; color: #666;"> + <div style="font-size: 64px; margin-bottom: 20px;">📭</div> + <h3>No files uploaded yet</h3> + <p>Upload some files to see them here!</p> + </div> + `; -<footer> - <p>Last updated December 28th, 2025</p> -</footer>`; + const content = replaceVariables(browseTemplate, { + "files-content": filesContent, + }); - return renderTemplate("Browse Files", content); + return renderLayout("Browse Files", content); } -export function renderGalleryPage(files: Array<{ name: string; size: number; uploaded: Date }>) { +export async function renderGalleryPage(files: Array<{ name: string; size: number; uploaded: Date }>): Promise<string> { + const galleryTemplate = await loadTemplate("./templates/gallery.html"); + const galleryCss = await loadTemplate("./templates/styles/gallery.css"); + // Filter only images and videos const mediaFiles = files.filter(file => { const mimeType = getMimeType(file.name); @@ -367,50 +146,21 @@ export function renderGalleryPage(files: Array<{ name: string; size: number; upl `; }).join(''); - const content = ` -<section> - <h5>Gallery</h5> - <h6> - <a href="/">Simple Upload</a> | - <a href="/chunked">Chunked Upload</a> | - <a href="/browse">Browse Files</a> | - <a href="/gallery">Gallery</a> - </h6> -</section> - -<section> - <div class="widget"> - ${mediaFiles.length > 0 ? ` - <div class="gallery-grid"> - ${galleryItems} - </div> - ` : ` - <div style="text-align: center; padding: 60px 20px; color: #666;"> - <div style="font-size: 64px; margin-bottom: 20px;">🖼️</div> - <h3>No images or videos yet</h3> - <p>Upload some media files to see them here!</p> - </div> - `} - </div> -</section> - -<!-- Slideshow Modal --> -<div id="slideshow-modal" class="slideshow-modal"> - <span class="slideshow-close" onclick="closeSlideshow()">×</span> - <button class="slideshow-nav slideshow-prev" onclick="changeSlide(-1)">❮</button> - <div class="slideshow-content"> - <div id="slideshow-media-container"></div> - <div class="slideshow-info"> - <div id="slideshow-filename"></div> - <div id="slideshow-counter"></div> + const galleryContent = mediaFiles.length > 0 ? ` + <div class="gallery-grid"> + ${galleryItems} </div> - </div> - <button class="slideshow-nav slideshow-next" onclick="changeSlide(1)">❯</button> -</div> + ` : ` + <div style="text-align: center; padding: 60px 20px; color: #666;"> + <div style="font-size: 64px; margin-bottom: 20px;">🖼️</div> + <h3>No images or videos yet</h3> + <p>Upload some media files to see them here!</p> + </div> + `; -<footer> - <p>Last updated December 28th, 2025</p> -</footer>`; + const content = replaceVariables(galleryTemplate, { + "gallery-content": galleryContent, + }); const mediaFilesJson = JSON.stringify(mediaFiles.map(file => ({ name: file.name, @@ -419,263 +169,24 @@ export function renderGalleryPage(files: Array<{ name: string; size: number; upl uploaded: file.uploaded.toLocaleDateString() }))); - const styles = ` - <style> - .gallery-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 20px; - padding: 10px 0; - } - .gallery-item { - position: relative; - background: #f5f5f5; - border-radius: 4px; - overflow: hidden; - transition: transform 0.2s, box-shadow 0.2s; - cursor: pointer; - } - .gallery-item:hover { - transform: translateY(-4px); - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - } - .gallery-item a { - display: block; - position: relative; - text-decoration: none; - } - .video-overlay { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 48px; - color: white; - text-shadow: 0 2px 4px rgba(0,0,0,0.5); - pointer-events: none; - } - .gallery-info { - padding: 10px; - background: white; - } - .gallery-filename { - font-size: 14px; - font-weight: 600; - color: #333; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-bottom: 4px; - } - .gallery-meta { - font-size: 12px; - color: #666; - } - - /* Slideshow Styles */ - .slideshow-modal { - display: none; - position: fixed; - z-index: 9999; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.95); - } - .slideshow-modal.active { - display: flex; - align-items: center; - justify-content: center; - } - .slideshow-close { - position: absolute; - top: 20px; - right: 40px; - color: #fff; - font-size: 50px; - font-weight: bold; - cursor: pointer; - z-index: 10001; - transition: color 0.3s; - } - .slideshow-close:hover { - color: #bbb; - } - .slideshow-content { - max-width: 90%; - max-height: 90%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - } - #slideshow-media-container { - display: flex; - align-items: center; - justify-content: center; - max-width: 100%; - max-height: 80vh; - } - #slideshow-media-container img, - #slideshow-media-container video { - max-width: 100%; - max-height: 80vh; - object-fit: contain; - border-radius: 4px; - } - .slideshow-info { - color: white; - text-align: center; - margin-top: 20px; - font-size: 16px; - } - #slideshow-filename { - font-weight: 600; - margin-bottom: 8px; - } - #slideshow-counter { - font-size: 14px; - color: #ccc; - } - .slideshow-nav { - position: absolute; - top: 50%; - transform: translateY(-50%); - background-color: rgba(255, 255, 255, 0.2); - color: white; - border: none; - font-size: 40px; - padding: 20px; - cursor: pointer; - transition: background-color 0.3s; - z-index: 10001; - } - .slideshow-nav:hover { - background-color: rgba(255, 255, 255, 0.4); - } - .slideshow-prev { - left: 20px; - } - .slideshow-next { - right: 20px; - } - - @media (max-width: 550px) { - .gallery-grid { - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: 10px; - } - .gallery-item a img, - .gallery-item a video { - height: 150px !important; - } - .slideshow-close { - top: 10px; - right: 20px; - font-size: 40px; - } - .slideshow-nav { - font-size: 30px; - padding: 15px; - } - .slideshow-prev { - left: 10px; - } - .slideshow-next { - right: 10px; - } - } - </style> - - <script> - const mediaFiles = ${mediaFilesJson}; - let currentSlide = 0; - - function openSlideshow(index) { - currentSlide = index; - showSlide(currentSlide); - document.getElementById('slideshow-modal').classList.add('active'); - document.body.style.overflow = 'hidden'; - } - - function closeSlideshow() { - document.getElementById('slideshow-modal').classList.remove('active'); - document.body.style.overflow = 'auto'; - - // Pause any playing videos - const videos = document.querySelectorAll('#slideshow-media-container video'); - videos.forEach(v => v.pause()); - } - - function changeSlide(direction) { - currentSlide += direction; - - if (currentSlide >= mediaFiles.length) { - currentSlide = 0; - } - if (currentSlide < 0) { - currentSlide = mediaFiles.length - 1; - } - - showSlide(currentSlide); - } - - function showSlide(index) { - const file = mediaFiles[index]; - const container = document.getElementById('slideshow-media-container'); - - if (file.isVideo) { - container.innerHTML = \`<video src="/download/\${file.name}" controls autoplay style="max-width: 100%; max-height: 80vh;"></video>\`; - } else { - container.innerHTML = \`<img src="/download/\${file.name}" alt="\${file.name}" style="max-width: 100%; max-height: 80vh;">\`; - } - - document.getElementById('slideshow-filename').textContent = file.name; - document.getElementById('slideshow-counter').textContent = \`\${index + 1} / \${mediaFiles.length}\`; - } - - // Keyboard navigation - document.addEventListener('keydown', (e) => { - const modal = document.getElementById('slideshow-modal'); - if (!modal.classList.contains('active')) return; - - if (e.key === 'ArrowLeft') { - changeSlide(-1); - } else if (e.key === 'ArrowRight') { - changeSlide(1); - } else if (e.key === 'Escape') { - closeSlideshow(); - } - }); + const slideshowScript = await loadTemplate("./templates/scripts/slideshow.js"); + const scriptContent = replaceVariables(slideshowScript, { + "media-files-json": mediaFilesJson, + }); - // Close on background click - document.getElementById('slideshow-modal').addEventListener('click', (e) => { - if (e.target.id === 'slideshow-modal') { - closeSlideshow(); - } - }); - </script>`; + const styles = `<style>\n${galleryCss}\n</style>`; + const scripts = `<script>\n${scriptContent}\n</script>`; - return renderTemplate("Gallery", content, styles); + return renderLayout("Gallery", content, scripts, styles); } -export function renderError(error: string, status = 400) { - const content = ` -<section> - <div class="widget" style="text-align: center;"> - <div style="font-size: 64px; margin-bottom: 20px;">⚠️</div> - <h3>Error ${status}</h3> - <p>${error}</p> - <p style="margin-top: 30px;"> - <a href="/">← Back to Home</a> - </p> - </div> -</section> +export async function renderError(error: string, status = 400): Promise<string> { + const errorTemplate = await loadTemplate("./templates/error.html"); -<footer> - <p>Last updated December 28th, 2025</p> -</footer>`; + const content = replaceVariables(errorTemplate, { + status: String(status), + "error-message": error, + }); - return renderTemplate("Error", content); + return renderLayout("Error", content); } |
