diff --git a/docker-compose.yml b/docker-compose.yml index b97c29e..c576f9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,33 @@ +x-env: &env + PORT: "${PORT:-3000}" + HOST: "${HOST:-0.0.0.0}" + JWT_SECRET: "${JWT_SECRET}" + JWT_EXPIRY: "${JWT_EXPIRY:-7d}" + DB_PATH: "${DB_PATH:-./data/nanodrop.db}" + UPLOAD_DIR: "${UPLOAD_DIR:-./data/uploads}" + LOG_FILE: "${LOG_FILE:-./data/nanodrop.log}" + MAX_FILE_SIZE: "${MAX_FILE_SIZE:-104857600}" + BASE_URL: "${BASE_URL:-http://localhost:3000}" + COOKIE_SECURE: "${COOKIE_SECURE:-false}" + TRUST_PROXY: "${TRUST_PROXY:-false}" + services: nanodrop: build: . ports: - "${PORT:-3000}:3000" - environment: - PORT: "${PORT:-3000}" - HOST: "${HOST:-0.0.0.0}" - JWT_SECRET: "${JWT_SECRET}" - JWT_EXPIRY: "${JWT_EXPIRY:-7d}" - DB_PATH: "${DB_PATH:-./data/nanodrop.db}" - UPLOAD_DIR: "${UPLOAD_DIR:-./data/uploads}" - LOG_FILE: "${LOG_FILE:-./data/nanodrop.log}" - MAX_FILE_SIZE: "${MAX_FILE_SIZE:-104857600}" - BASE_URL: "${BASE_URL:-http://localhost:3000}" - COOKIE_SECURE: "${COOKIE_SECURE:-false}" - TRUST_PROXY: "${TRUST_PROXY:-false}" + environment: { <<: *env } volumes: - nanodrop-data:/app/data restart: unless-stopped + register-user: + build: . + profiles: [tools] + entrypoint: ["node", "--import", "tsx", "src/cli/register-user.ts"] + environment: { <<: *env } + volumes: + - nanodrop-data:/app/data + volumes: nanodrop-data: diff --git a/public/style.css b/public/style.css index 136a5f5..5ced5f4 100644 --- a/public/style.css +++ b/public/style.css @@ -57,8 +57,7 @@ header { .logo { font-size: 13px; font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; + letter-spacing: 0.02em; text-decoration: none; color: var(--black); } @@ -349,8 +348,26 @@ td a { } td:last-child { - width: 80px; + width: 160px; text-align: right; + white-space: nowrap; +} + +/* ── Copy Link Button ───────────────────────────────────── */ +button.copy-link { + display: inline-block; + padding: 0.3rem 0.6rem; + font-family: var(--font); + font-size: 11px; + letter-spacing: 0.04em; + background: var(--white); + color: var(--black); + border: var(--border); + cursor: pointer; + margin-right: 0.4rem; +} +button.copy-link:hover { + background: var(--gray-100); } /* ── File View ──────────────────────────────────────────── */ @@ -376,6 +393,7 @@ td:last-child { margin-bottom: 0; } +.file-view img, .file-view video, .file-view audio { width: 100%; diff --git a/src/index.ts b/src/index.ts index f53b2b2..2a572eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); diff --git a/src/routes/pages.ts b/src/routes/pages.ts index b31df77..c14f363 100644 --- a/src/routes/pages.ts +++ b/src/routes/pages.ts @@ -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 diff --git a/src/views/file-list.ts b/src/views/file-list.ts index e5d39ae..0e6f982 100644 --- a/src/views/file-list.ts +++ b/src/views/file-list.ts @@ -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 ? 'No files yet. Upload one.' - : files.map((f) => ` + : files.map((f) => { + const shareUrl = `${baseUrl}/f/${escHtml(f.id)}`; + return ` ${escHtml(f.original_name)} ${escHtml(f.mime_type)} ${formatBytes(f.size)} ${escHtml(f.created_at)} +
- `).join(''); + `; + }).join(''); return layout('My files', `

My files

diff --git a/src/views/file-view.ts b/src/views/file-view.ts index 994da40..4ff28ae 100644 --- a/src/views/file-view.ts +++ b/src/views/file-view.ts @@ -11,7 +11,9 @@ export function fileViewPage(file: FileRow): string { `; let media = ''; - if (file.mime_type.startsWith('video/')) { + if (file.mime_type.startsWith('image/')) { + media = `${safeName}`; + } else if (file.mime_type.startsWith('video/')) { media = ``; } else if (file.mime_type.startsWith('audio/')) { media = ``; @@ -23,5 +25,5 @@ export function fileViewPage(file: FileRow): string { ${media} ${actions} - `); + `, { hideLogo: true }); } diff --git a/src/views/layout.ts b/src/views/layout.ts index ca37110..c4312ba 100644 --- a/src/views/layout.ts +++ b/src/views/layout.ts @@ -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 ? `