Fix four bugs and add logo/branding polish

- docker-compose: add register-user service (profiles: [tools]) with YAML anchor to avoid env duplication
- src/index.ts: show localhost instead of 0.0.0.0 in startup message
- file-view: render <img> inline for image/* MIME types
- file-list: add Copy link button per row (requires baseUrl param)
- layout: add hideLogo option; file view page hides the logo
- style.css: remove uppercase from .logo (Nanodrop not NANODROP), add button.copy-link styles, add .file-view img styles, widen last td

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 16:18:34 -08:00
parent b5ea21d44c
commit 5a47ae938e
8 changed files with 108 additions and 25 deletions

View File

@@ -12,7 +12,8 @@ const app = createServer({ config, db });
try {
await app.listen({ port: config.port, host: config.host });
console.log(`Nanodrop running on http://${config.host}:${config.port}`);
const displayHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
console.log(`Nanodrop running on http://${displayHost}:${config.port}`);
} catch (err) {
console.error(err);
process.exit(1);

View File

@@ -100,7 +100,7 @@ export const pageRoutes: FastifyPluginAsync<{ deps: Deps }> = async (app, { deps
app.get('/files', { preHandler: requireAuth }, async (request, reply) => {
const { sub: userId } = request.user as JwtPayload;
const files = getFilesByUserId(db, userId);
reply.type('text/html').send(fileListPage(files));
reply.type('text/html').send(fileListPage(files, config.baseUrl));
});
// POST /files/:id/delete

View File

@@ -1,21 +1,25 @@
import { layout, escHtml } from './layout.ts';
import type { FileRow } from '../db/files.ts';
export function fileListPage(files: FileRow[]): string {
export function fileListPage(files: FileRow[], baseUrl: string): string {
const rows = files.length === 0
? '<tr><td colspan="5">No files yet. <a href="/upload">Upload one.</a></td></tr>'
: files.map((f) => `
: files.map((f) => {
const shareUrl = `${baseUrl}/f/${escHtml(f.id)}`;
return `
<tr>
<td><a href="/f/${escHtml(f.id)}">${escHtml(f.original_name)}</a></td>
<td>${escHtml(f.mime_type)}</td>
<td>${formatBytes(f.size)}</td>
<td>${escHtml(f.created_at)}</td>
<td>
<button class="copy-link" onclick="navigator.clipboard.writeText('${shareUrl}')">Copy link</button>
<form method="POST" action="/files/${escHtml(f.id)}/delete" style="display:inline">
<button type="submit" class="danger">Delete</button>
</form>
</td>
</tr>`).join('');
</tr>`;
}).join('');
return layout('My files', `
<h1>My files</h1>

View File

@@ -11,7 +11,9 @@ export function fileViewPage(file: FileRow): string {
</div>`;
let media = '';
if (file.mime_type.startsWith('video/')) {
if (file.mime_type.startsWith('image/')) {
media = `<img src="${rawUrl}" alt="${safeName}">`;
} else if (file.mime_type.startsWith('video/')) {
media = `<video controls src="${rawUrl}" preload="metadata"></video>`;
} else if (file.mime_type.startsWith('audio/')) {
media = `<audio controls src="${rawUrl}" preload="metadata"></audio>`;
@@ -23,5 +25,5 @@ export function fileViewPage(file: FileRow): string {
${media}
${actions}
</div>
`);
`, { hideLogo: true });
}

View File

@@ -1,5 +1,5 @@
export function layout(title: string, body: string, opts: { authed?: boolean } = {}): string {
const { authed = false } = opts;
export function layout(title: string, body: string, opts: { authed?: boolean; hideLogo?: boolean } = {}): string {
const { authed = false, hideLogo = false } = opts;
const nav = authed
? `<nav>
<a href="/upload">Upload</a>
@@ -20,7 +20,7 @@ export function layout(title: string, body: string, opts: { authed?: boolean } =
</head>
<body>
<header>
<a href="/" class="logo">Nanodrop</a>
${hideLogo ? '' : '<a href="/" class="logo">Nanodrop</a>'}
${nav}
</header>
<main>