summaryrefslogtreecommitdiff
path: root/views.ts
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-12-28 16:13:33 -0500
committergrothedev <grothedev@gmail.com>2025-12-28 16:13:33 -0500
commitf978ad7db04ced4cbcf04a82bf6f0cc3f4ce66a3 (patch)
treece97a8ffe34bd5907a0efdd4c453b64f10e6f568 /views.ts
parent119dd4129b91d8a3437a4f4e099b74991ee0044d (diff)
use template files
Diffstat (limited to 'views.ts')
-rw-r--r--views.ts705
1 files changed, 108 insertions, 597 deletions
diff --git a/views.ts b/views.ts
index bfd089c..4db06ce 100644
--- a/views.ts
+++ b/views.ts
@@ -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()">&times;</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);
}