feat: bold Swiss/constructivist design overhaul Typography: - Inter for body text, Oswald for headings (bold condensed) - ALL CAPS labels, large type hierarchy, tight letter-spacing Colors: - Black (#1a1a1a) background on UI elements, white text - Red (#c62828) accent for hover states, underlines, CTA buttons - Light gray (#f5f5f5) page background for contrast Layout: - Black nav bar with red bottom border, red hover states - Red accent underline blocks (3px x 3rem) on page headings - No border-radius anywhere — sharp rectangular constructivist forms - Full-width 6-column gallery grid with 2px gaps All pages redesigned: home, browse, upload, login, image detail, and image viewer overlay. 💘 Generated with Crush Assisted-by: Crush:deepseek-reasoner
@@ -3,6 +3,9 @@ <html lang="en">
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="preconnect" href="https://fonts.googleapis.com" /> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Oswald:wght@500;600;700&display=swap" rel="stylesheet" /> <title>weimar</title> %sveltekit.head% </head>
@@ -1,5 +1,5 @@
<script lang="ts"> - import { thumbnailUrl, imageDownloadUrl, getImage, type Image, type TagDetail } from '$lib/api'; + import { imageDownloadUrl, getImage, type Image, type TagDetail } from '$lib/api'; let { images,@@ -18,7 +18,6 @@ let tagDetails = $state<TagDetail[]>([]);
let idleTimer: ReturnType<typeof setTimeout> | undefined = $state(); let current = $derived(images[currentIndex]); - let totalPillCount = $derived(tagDetails.reduce((sum, t) => sum + t.usage_count, 0)); async function loadTagDetails() { try {@@ -89,7 +88,6 @@ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } - // Load initial tag details. $effect(() => { current.id; loadTagDetails();@@ -100,17 +98,14 @@ <svelte:window onkeydown={handleKeydown} on:mousemove={resetIdleTimer} />
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions --> <div class="viewer-overlay" onclick={handleOverlayClick} role="dialog" aria-label="Image viewer" tabindex="-1"> - <!-- Close button --> - <button class="viewer-close" onclick={onclose} aria-label="Close">×</button> + <button class="viewer-close" onclick={onclose} aria-label="Close">CLOSE</button> - <!-- Loading indicator --> {#if loading} <div class="viewer-loader"> <div class="spinner"></div> </div> {/if} - <!-- Previous button --> {#if currentIndex > 0} <button class="viewer-nav viewer-prev"@@ -120,7 +115,6 @@ class:controls-hidden={!showControls}
>‹</button> {/if} - <!-- Next button --> {#if currentIndex < images.length - 1} <button class="viewer-nav viewer-next"@@ -130,7 +124,6 @@ class:controls-hidden={!showControls}
>›</button> {/if} - <!-- Media --> <div class="viewer-content" class:loading> {#if current.mime_type.startsWith('video/')} <!-- svelte-ignore a11y_media_has_caption -->@@ -153,25 +146,25 @@ />
{/if} </div> - <!-- Bottom info bar --> <div class="viewer-info" class:controls-hidden={!showControls}> <div class="viewer-info-left"> - <span class="viewer-filename">{current.filename}</span> - <span class="viewer-meta"> + <div class="viewer-filename">{current.filename}</div> + <div class="viewer-meta"> {current.width}×{current.height} - · {formatSize(current.file_size)} - · {current.mime_type} - </span> + <span class="meta-dot">·</span> + {formatSize(current.file_size)} + <span class="meta-dot">·</span> + {current.mime_type} + </div> </div> <div class="viewer-info-right"> <a href={imageDownloadUrl(current.id)} download={current.filename} class="viewer-download"> - Download + DOWNLOAD </a> <span class="viewer-counter">{currentIndex + 1} / {images.length}</span> </div> </div> - <!-- Tags bar --> {#if tagDetails.length > 0} <div class="viewer-tags" class:controls-hidden={!showControls}> {#each tagDetails as t (t.tag)}@@ -186,6 +179,8 @@ </button>
{/each} </div> {/if} + + <div class="viewer-accent" class:controls-hidden={!showControls}></div> </div> <style>@@ -193,7 +188,7 @@ .viewer-overlay {
position: fixed; inset: 0; z-index: 1000; - background: rgba(0, 0, 0, 0.92); + background: #0d0d0d; display: flex; align-items: center; justify-content: center;@@ -202,25 +197,23 @@ }
.viewer-close { position: absolute; - top: 1rem; - right: 1rem; + top: 0.75rem; + right: 0.75rem; z-index: 10; - background: rgba(0, 0, 0, 0.5); - border: none; + background: none; + border: 2px solid rgba(255,255,255,0.3); color: #fff; - font-size: 2rem; - width: 2.5rem; - height: 2.5rem; - border-radius: 50%; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.8rem; + letter-spacing: 0.12em; + padding: 0.4rem 1rem; cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - line-height: 1; - transition: opacity 0.3s; + transition: background 0.15s, border-color 0.15s; } .viewer-close:hover { - background: rgba(255, 255, 255, 0.2); + background: #c62828; + border-color: #c62828; } .viewer-loader {@@ -234,8 +227,8 @@ }
.spinner { width: 40px; height: 40px; - border: 3px solid rgba(255, 255, 255, 0.2); - border-top-color: #fff; + border: 3px solid rgba(255, 255, 255, 0.15); + border-top-color: #c62828; border-radius: 50%; animation: spin 0.7s linear infinite; }@@ -248,28 +241,27 @@ position: absolute;
top: 50%; transform: translateY(-50%); z-index: 10; - background: rgba(0, 0, 0, 0.4); + background: rgba(0, 0, 0, 0.5); border: none; color: #fff; - font-size: 3rem; - width: 3.5rem; - height: 5rem; + font-size: 2.5rem; + width: 3rem; + height: 4rem; cursor: pointer; display: flex; align-items: center; justify-content: center; - border-radius: 6px; - transition: opacity 0.3s; + transition: background 0.15s, opacity 0.3s; line-height: 1; } .viewer-nav:hover { - background: rgba(255, 255, 255, 0.15); + background: #c62828; } .viewer-prev { - left: 1rem; + left: 0; } .viewer-next { - right: 1rem; + right: 0; } .viewer-content {@@ -283,7 +275,6 @@ .viewer-media {
max-width: 90vw; max-height: 70vh; object-fit: contain; - border-radius: 4px; transition: opacity 0.3s; } .viewer-content.loading .viewer-media {@@ -295,13 +286,14 @@ position: absolute;
bottom: 0; left: 0; right: 0; - background: linear-gradient(transparent, rgba(0, 0, 0, 0.85)); + background: linear-gradient(transparent, rgba(13, 13, 13, 0.95)); padding: 2.5rem 1.5rem 1rem; display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; transition: opacity 0.3s; + z-index: 3; } .viewer-info-left { display: flex;@@ -311,15 +303,21 @@ min-width: 0;
} .viewer-filename { color: #fff; + font-family: 'Inter', sans-serif; font-weight: 600; - font-size: 1rem; + font-size: 0.95rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .viewer-meta { - color: rgba(255, 255, 255, 0.6); - font-size: 0.8rem; + color: rgba(255, 255, 255, 0.5); + font-size: 0.78rem; + font-weight: 500; + } + .meta-dot { + margin: 0 0.35rem; + color: rgba(255, 255, 255, 0.25); } .viewer-info-right { display: flex;@@ -329,30 +327,44 @@ flex-shrink: 0;
} .viewer-download { display: inline-block; - padding: 0.5rem 1rem; - background: #1a1a2e; - color: #eee; + padding: 0.45rem 1rem; + background: #c62828; + color: #fff; text-decoration: none; - border-radius: 6px; - font-size: 0.85rem; + font-family: 'Oswald', sans-serif; font-weight: 500; + font-size: 0.8rem; + letter-spacing: 0.1em; transition: background 0.15s; } .viewer-download:hover { - background: #16213e; + background: #b71c1c; + text-decoration: none; } .viewer-counter { - color: rgba(255, 255, 255, 0.5); - font-size: 0.85rem; + color: rgba(255, 255, 255, 0.35); + font-family: 'Oswald', sans-serif; + font-size: 0.8rem; + letter-spacing: 0.1em; white-space: nowrap; } + .viewer-accent { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: #c62828; + z-index: 4; + transition: opacity 0.3s; + } + .controls-hidden { opacity: 0; pointer-events: none; } - /* Tags bar */ .viewer-tags { position: absolute; top: 4.5rem;@@ -370,32 +382,36 @@ .tag-pill {
display: inline-flex; align-items: center; gap: 0.4rem; - background: rgba(255, 255, 255, 0.12); - border: 1px solid rgba(255, 255, 255, 0.25); - border-radius: 24px; - padding: 0.35rem 0.75rem 0.35rem 0.9rem; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 0; + padding: 0.35rem 0.7rem 0.35rem 0.9rem; font: inherit; - font-size: 0.9rem; + font-family: 'Inter', sans-serif; + font-size: 0.85rem; + font-weight: 500; color: #fff; white-space: nowrap; - backdrop-filter: blur(4px); cursor: pointer; - transition: background 0.15s; + transition: background 0.15s, border-color 0.15s; } .tag-pill:hover { - background: rgba(255, 255, 255, 0.22); + background: #c62828; + border-color: #c62828; } .tag-name { - font-weight: 500; + font-weight: 600; } .tag-count { background: rgba(255, 255, 255, 0.15); - border-radius: 12px; - padding: 0.1rem 0.45rem; - font-size: 0.78rem; + padding: 0.08rem 0.4rem; + font-size: 0.72rem; font-weight: 600; - color: rgba(255, 255, 255, 0.8); - min-width: 1.3rem; + color: rgba(255, 255, 255, 0.7); + min-width: 1.2rem; text-align: center; + } + .tag-pill:hover .tag-count { + background: rgba(0, 0, 0, 0.25); } </style>
@@ -24,59 +24,158 @@ }
</script> <nav> - <a href="/">weimar</a> + <div class="nav-brand"> + <a href="/" class="nav-logo">WEIMAR</a> + <span class="nav-divider"></span> + </div> <div class="nav-links"> - <a href="/browse">Browse</a> - <a href="/upload">Upload</a> + <a href="/browse">BROWSE</a> + <a href="/upload">UPLOAD</a> {#if !checking} {#if loggedIn} - <button class="link logout" onclick={handleLogout}>Logout</button> + <button class="link logout" onclick={handleLogout}>LOGOUT</button> {:else} - <a href="/login">Login</a> + <a href="/login">LOGIN</a> {/if} {/if} </div> </nav> -<main> +<div class="main"> {@render children()} -</main> +</div> <style> + :global(*) { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + :global(body) { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: #f5f5f5; + color: #1a1a1a; + -webkit-font-smoothing: antialiased; + } + + :global(h1) { + font-family: 'Oswald', sans-serif; + font-weight: 700; + font-size: 2.5rem; + text-transform: uppercase; + letter-spacing: 0.03em; + line-height: 1.1; + color: #1a1a1a; + } + + :global(h2) { + font-family: 'Oswald', sans-serif; + font-weight: 600; + font-size: 1.5rem; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + :global(a) { + color: #c62828; + text-decoration: none; + font-weight: 600; + } + + :global(a:hover) { + text-decoration: underline; + } + nav { display: flex; align-items: center; justify-content: space-between; - padding: 0.75rem 1.5rem; - background: #1a1a2e; - color: #eee; + padding: 0 1.5rem; + height: 3.5rem; + background: #1a1a1a; + border-bottom: 3px solid #c62828; + } + + .nav-brand { + display: flex; + align-items: center; + gap: 1rem; + } + + .nav-logo { + font-family: 'Oswald', sans-serif; + font-weight: 700; + font-size: 1.4rem; + letter-spacing: 0.15em; + color: #fff; + text-decoration: none; } - nav a { - color: #eee; + .nav-logo:hover { text-decoration: none; + color: #ef5350; } - nav a:hover { - text-decoration: underline; + + .nav-divider { + display: block; + width: 1px; + height: 1.6rem; + background: rgba(255,255,255,0.2); } + .nav-links { display: flex; - gap: 1rem; align-items: center; + gap: 0; + height: 100%; } + + .nav-links a, .logout { + display: flex; + align-items: center; + height: 100%; + padding: 0 1.25rem; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.85rem; + letter-spacing: 0.12em; + color: rgba(255,255,255,0.8); + text-decoration: none; + text-transform: uppercase; + transition: background 0.15s, color 0.15s; + position: relative; + } + + .nav-links a:hover, .logout:hover { + background: #c62828; + color: #fff; + text-decoration: none; + } + + .nav-links a::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 0; + background: #c62828; + transition: height 0.15s; + } + + .nav-links a:hover::after { + height: 3px; + } + .logout { background: none; border: none; - color: #eee; cursor: pointer; - font-size: inherit; - padding: 0; - text-decoration: none; + font-size: 0.85rem; } - .logout:hover { - text-decoration: underline; - } - main { - max-width: 1200px; + + .main { + max-width: 1400px; margin: 0 auto; padding: 1.5rem; }
@@ -14,32 +14,86 @@ }
}); </script> -<h1>weimar</h1> -<p>{status}</p> - -<div class="actions"> - <a href="/browse">Browse images</a> - <a href="/upload">Upload image</a> +<div class="hero"> + <h1>WEIMAR</h1> + <div class="hero-line"></div> + <p class="hero-sub">Media Repository</p> + <p class="hero-status">{status}</p> + <div class="hero-actions"> + <a href="/browse" class="btn">BROWSE</a> + <a href="/upload" class="btn btn-outline">UPLOAD</a> + </div> </div> <style> - h1 { - font-size: 3rem; - margin-bottom: 0.5rem; + .hero { + padding: 4rem 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; } - .actions { - margin-top: 2rem; + .hero h1 { + font-family: 'Oswald', sans-serif; + font-weight: 700; + font-size: 5rem; + letter-spacing: 0.08em; + color: #1a1a1a; + line-height: 1; + margin: 0; + } + .hero-line { + height: 4px; + width: 5rem; + background: #c62828; + margin: 0.25rem 0 0.5rem; + } + .hero-sub { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 1.1rem; + letter-spacing: 0.15em; + color: #888; + text-transform: uppercase; + } + .hero-status { + font-family: 'Inter', sans-serif; + font-size: 0.9rem; + color: #666; + margin-top: 0.25rem; + } + + .hero-actions { display: flex; - gap: 1rem; + gap: 0.75rem; + margin-top: 1.5rem; } - .actions a { - padding: 0.5rem 1.5rem; - background: #1a1a2e; - color: #eee; + .btn { + display: inline-flex; + align-items: center; + padding: 0.65rem 1.5rem; + background: #1a1a1a; + color: #fff; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.9rem; + letter-spacing: 0.1em; text-decoration: none; - border-radius: 6px; + text-transform: uppercase; + transition: background 0.15s; + } + .btn:hover { + background: #c62828; + text-decoration: none; + } + .btn-outline { + background: none; + color: #1a1a1a; + border: 2px solid #1a1a1a; } - .actions a:hover { - background: #16213e; + .btn-outline:hover { + background: #1a1a1a; + color: #fff; + border-color: #1a1a1a; } </style>
@@ -52,8 +52,12 @@ viewerIndex = index;
} </script> - <div class="header"> - <h1>Browse</h1> +<div class="header"> + <div class="header-top"> + <h1>Browse</h1> + <span class="header-count">{total} {total === 1 ? 'image' : 'images'}</span> + </div> + <div class="header-line"></div> <div class="controls"> <input type="text"@@ -61,23 +65,26 @@ placeholder="Filter by tag..."
bind:value={filterTag} onkeydown={(e) => { if (e.key === 'Enter') applyFilter(); }} /> - <button onclick={applyFilter}>Filter</button> + <button class="btn" onclick={applyFilter}>FILTER</button> {#if filterTag} - <button class="clear" onclick={() => { filterTag = ''; applyFilter(); }}>Clear</button> + <button class="btn btn-clear" onclick={() => { filterTag = ''; applyFilter(); }}>CLEAR</button> {/if} </div> </div> {#if loading} - <p class="empty-state">Loading...</p> + <p class="state-msg">Loading...</p> {:else if error} - <p class="empty-state error">{error}</p> + <p class="state-msg error">{error}</p> {:else if images.length === 0} - {#if filterTag} - <p class="empty-state">No results found for tag “{filterTag}”.</p> - {:else} - <p class="empty-state">No images yet. <a href="/upload">Upload one!</a></p> - {/if} + <div class="empty"> + {#if filterTag} + <p>No results for tag <strong>“{filterTag}”</strong>.</p> + {:else} + <p>The archive is empty.</p> + <a href="/upload" class="btn">UPLOAD MEDIA</a> + {/if} + </div> {:else} <div class="grid"> {#each images as img, i (img.id)}@@ -97,9 +104,9 @@ {/each}
</div> <div class="pagination"> - <button onclick={prevPage} disabled={page <= 1}>Previous</button> - <span>Page {page} of {Math.ceil(total / perPage)} ({total} total)</span> - <button onclick={nextPage} disabled={page * perPage >= total}>Next</button> + <button class="btn" onclick={prevPage} disabled={page <= 1}>PREVIOUS</button> + <span class="page-info">PAGE {page} OF {Math.ceil(total / perPage)}</span> + <button class="btn" onclick={nextPage} disabled={page * perPage >= total}>NEXT</button> </div> {/if}@@ -120,31 +127,78 @@
.header { padding: 1.5rem 1.5rem 0.75rem; } - .header h1 { - margin: 0 0 0.5rem; + .header-top { + display: flex; + align-items: baseline; + gap: 1rem; + } + .header-top h1 { + margin: 0; + } + .header-count { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.9rem; + letter-spacing: 0.1em; + color: #888; + text-transform: uppercase; + } + .header-line { + height: 3px; + background: #c62828; + width: 3rem; + margin: 0.5rem 0 1rem; } .controls { display: flex; gap: 0.5rem; } .controls input { - padding: 0.5rem; - border: 1px solid #ccc; - border-radius: 4px; + padding: 0.5rem 0.75rem; + border: 2px solid #1a1a1a; + border-radius: 0; + font-family: 'Inter', sans-serif; + font-size: 0.85rem; + font-weight: 500; flex: 1; - max-width: 300px; + max-width: 280px; + background: #fff; + } + .controls input:focus { + outline: none; + border-color: #c62828; } - .controls button { - padding: 0.5rem 1rem; - background: #1a1a2e; - color: #eee; + + .btn { + display: inline-flex; + align-items: center; + padding: 0.5rem 1.25rem; + background: #1a1a1a; + color: #fff; border: none; - border-radius: 4px; + border-radius: 0; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.85rem; + letter-spacing: 0.1em; + text-transform: uppercase; cursor: pointer; + text-decoration: none; + transition: background 0.15s; } - .controls button.clear { + .btn:hover { + background: #c62828; + } + .btn:disabled { + opacity: 0.3; + cursor: default; + } + .btn-clear { background: #666; } + .btn-clear:hover { + background: #c62828; + } .grid { display: grid;@@ -177,15 +231,16 @@ color: inherit;
cursor: pointer; text-align: left; overflow: hidden; + outline: none; } .card:focus-visible { - outline: 2px solid #1a1a2e; - outline-offset: -2px; + outline: 3px solid #c62828; + outline-offset: -3px; } .card-image { position: relative; overflow: hidden; - background: #222; + background: #1a1a1a; aspect-ratio: 16 / 10; } .card-image img {@@ -193,10 +248,11 @@ display: block;
width: 100%; height: 100%; object-fit: cover; - transition: transform 200ms ease; + transition: transform 300ms ease, filter 300ms ease; } .card:hover .card-image img { - transform: scale(1.05); + transform: scale(1.08); + filter: grayscale(0.5) contrast(1.1); } .placeholder { display: flex;@@ -204,8 +260,9 @@ align-items: center;
justify-content: center; width: 100%; height: 100%; - color: #666; - font-size: 0.85rem; + color: #555; + font-size: 0.8rem; + font-weight: 600; } .card-overlay {@@ -213,7 +270,7 @@ position: absolute;
bottom: 0; left: 0; right: 0; - background: linear-gradient(transparent, rgba(0, 0, 0, 0.8)); + background: linear-gradient(transparent 30%, #1a1a1a 95%); padding: 2rem 0.6rem 0.5rem; opacity: 0; transition: opacity 200ms ease;@@ -224,12 +281,12 @@ opacity: 1;
} .card-title { color: #fff; - font-size: 0.8rem; + font-family: 'Inter', sans-serif; + font-size: 0.78rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); } .pagination {@@ -237,25 +294,34 @@ display: flex;
align-items: center; justify-content: center; gap: 1rem; - padding: 2rem 1.5rem; + padding: 2rem 1.5rem 3rem; } - .pagination button { - padding: 0.5rem 1rem; - background: #1a1a2e; - color: #eee; - border: none; - border-radius: 4px; - cursor: pointer; - } - .pagination button:disabled { - opacity: 0.4; - cursor: default; + .page-info { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.85rem; + letter-spacing: 0.12em; + color: #888; } - .empty-state { - padding: 2rem 1.5rem; + .state-msg { + padding: 3rem 1.5rem; + font-size: 1.1rem; + color: #888; } .error { - color: #c0392b; + color: #c62828; + } + + .empty { + padding: 3rem 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + .empty p { + font-size: 1.1rem; + color: #888; } </style>
@@ -31,154 +31,219 @@ }
</script> {#if loading} - <p>Loading...</p> + <p class="state-msg">Loading...</p> {:else if error} - <p class="error">{error}</p> + <p class="state-msg error">{error}</p> {:else if img} - <h1>{img.filename}</h1> + <div class="page-head"> + <h1>{img.filename}</h1> + <div class="head-line"></div> + </div> - <div class="detail"> + <div class="layout"> <div class="preview"> {#if img.thumbnail_path} - <a href={imageDownloadUrl(img.id)} download={img.filename}> - <img src={thumbnailUrl(img.id)} alt={img.filename} /> - </a> + <img src={imageDownloadUrl(img.id)} alt={img.filename} /> {:else} - <div class="placeholder">No thumbnail</div> + <div class="no-thumb">NO THUMBNAIL</div> {/if} </div> <div class="meta"> - <dl> - <dt>Dimensions</dt> - <dd>{img.width} × {img.height}</dd> - <dt>File size</dt> - <dd>{formatSize(img.file_size)}</dd> - <dt>Type</dt> - <dd>{img.mime_type}</dd> - <dt>Tags</dt> - <dd> + <div class="meta-block"> + <span class="meta-label">DIMENSIONS</span> + <span class="meta-value">{img.width} × {img.height}</span> + </div> + <div class="meta-block"> + <span class="meta-label">FILE SIZE</span> + <span class="meta-value">{formatSize(img.file_size)}</span> + </div> + <div class="meta-block"> + <span class="meta-label">TYPE</span> + <span class="meta-value">{img.mime_type}</span> + </div> + <div class="meta-block"> + <span class="meta-label">UPLOADED</span> + <span class="meta-value">{formatDate(img.created_at)}</span> + </div> + <div class="meta-block tags-block"> + <span class="meta-label">TAGS</span> + <div class="tag-pills"> {#if img.tags_detail.length > 0} - <div class="tag-pills"> - {#each img.tags_detail as t (t.tag)} - <a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}"> - <span class="tag-name">{t.tag}</span> - <span class="tag-count">{t.usage_count}</span> - </a> - {/each} - </div> + {#each img.tags_detail as t (t.tag)} + <a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}"> + <span class="tag-name">{t.tag}</span> + <span class="tag-count">{t.usage_count}</span> + </a> + {/each} {:else} - <span class="dim">none</span> + <span class="none">none</span> {/if} - </dd> - <dt>Uploaded</dt> - <dd>{formatDate(img.created_at)}</dd> - </dl> + </div> + </div> - <a href={imageDownloadUrl(img.id)} class="download" download={img.filename}> - Download original + <a href={imageDownloadUrl(img.id)} class="btn" download={img.filename}> + DOWNLOAD ORIGINAL </a> </div> </div> - <p class="back"><a href="/browse">← Back to browse</a></p> + <p class="back"><a href="/browse">← BACK TO BROWSE</a></p> {/if} <style> - .detail { + .state-msg { + padding: 2rem 0; + font-size: 1.1rem; + color: #888; + } + .error { + color: #c62828; + } + + .page-head { + margin-bottom: 1.5rem; + } + .page-head h1 { + word-break: break-all; + } + .head-line { + height: 3px; + background: #c62828; + width: 3rem; + margin-top: 0.4rem; + } + + .layout { display: flex; gap: 2rem; - margin-top: 1rem; flex-wrap: wrap; } - .preview img { - max-width: 100%; - max-height: 60vh; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.15); - } - .placeholder { - width: 300px; - height: 200px; + .preview { + flex: 1; + min-width: 300px; + background: #1a1a1a; display: flex; align-items: center; justify-content: center; - background: #f0f0f0; - border-radius: 8px; - color: #999; } - .meta { - min-width: 220px; + .preview img { + display: block; + width: 100%; + height: auto; + max-height: 70vh; + object-fit: contain; } - dl { - margin: 0; + .no-thumb { + padding: 4rem 2rem; + color: #555; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.9rem; + letter-spacing: 0.12em; } - dt { - font-weight: 600; - margin-top: 0.5rem; - font-size: 0.85rem; - color: #555; + + .meta { + width: 260px; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 1rem; } - dd { - margin: 0; - font-size: 0.95rem; + .meta-block { + display: flex; + flex-direction: column; + gap: 0.15rem; } - .dim { + .meta-label { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.7rem; + letter-spacing: 0.15em; color: #999; } - .download { - display: inline-block; - margin-top: 1.5rem; - padding: 0.5rem 1.5rem; - background: #1a1a2e; - color: #eee; - text-decoration: none; - border-radius: 6px; + .meta-value { + font-family: 'Inter', sans-serif; + font-size: 0.9rem; + font-weight: 600; + color: #1a1a1a; } - .download:hover { - background: #16213e; + + .tags-block { + flex: 1; } - .back { - margin-top: 2rem; - } - .tag-pills { display: flex; flex-wrap: wrap; - gap: 0.4rem; - margin-top: 0.25rem; + gap: 0.35rem; } .tag-pill { display: inline-flex; align-items: center; - gap: 0.35rem; - background: #1a1a2e; - border-radius: 20px; - padding: 0.3rem 0.65rem 0.3rem 0.8rem; - font-size: 0.85rem; + gap: 0.3rem; + background: #1a1a1a; + padding: 0.25rem 0.5rem 0.25rem 0.65rem; + font-family: 'Inter', sans-serif; + font-size: 0.78rem; + font-weight: 500; color: #eee; text-decoration: none; - white-space: nowrap; transition: background 0.15s; } .tag-pill:hover { - background: #16213e; + background: #c62828; + text-decoration: none; } .tag-name { - font-weight: 500; + font-weight: 600; } .tag-count { - background: rgba(255, 255, 255, 0.15); - border-radius: 10px; - padding: 0.06rem 0.4rem; - font-size: 0.72rem; + background: rgba(255, 255, 255, 0.12); + padding: 0.05rem 0.35rem; + font-size: 0.68rem; font-weight: 600; - color: rgba(255, 255, 255, 0.75); - min-width: 1.2rem; + color: rgba(255, 255, 255, 0.65); + min-width: 1.1rem; text-align: center; } + .tag-pill:hover .tag-count { + background: rgba(0, 0, 0, 0.2); + } + .none { + color: #bbb; + font-size: 0.85rem; + } - .error { - color: #c0392b; + .btn { + display: inline-flex; + align-items: center; + align-self: flex-start; + padding: 0.55rem 1.25rem; + background: #c62828; + color: #fff; + border: none; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.8rem; + letter-spacing: 0.1em; + text-decoration: none; + text-transform: uppercase; + cursor: pointer; + transition: background 0.15s; + margin-top: 0.5rem; + } + .btn:hover { + background: #b71c1c; + text-decoration: none; + } + + .back { + margin-top: 2rem; + } + .back a { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.85rem; + letter-spacing: 0.1em; } </style>
@@ -25,76 +25,127 @@ }
} </script> -<h1>{isRegister ? 'Register' : 'Login'}</h1> +<div class="page-head"> + <h1>{isRegister ? 'REGISTER' : 'LOGIN'}</h1> + <div class="head-line"></div> +</div> <form onsubmit={handleSubmit}> {#if error} <p class="error">{error}</p> {/if} <label> - Username + <span class="label-text">USERNAME</span> <input type="text" bind:value={username} required minlength={3} disabled={loading} /> </label> <label> - Password + <span class="label-text">PASSWORD</span> <input type="password" bind:value={password} required minlength={8} disabled={loading} /> </label> - <button type="submit" disabled={loading}> - {isRegister ? 'Register' : 'Login'} + <button type="submit" class="btn" disabled={loading}> + {loading ? 'PLEASE WAIT...' : isRegister ? 'REGISTER' : 'LOGIN'} </button> </form> <p class="toggle"> <button class="link" onclick={() => { isRegister = !isRegister; error = ''; }}> - {isRegister ? 'Already have an account? Log in' : 'No account? Register'} + {isRegister ? 'ALREADY HAVE AN ACCOUNT? LOG IN' : 'NO ACCOUNT? REGISTER'} </button> </p> <style> - h1 { - margin-bottom: 1rem; + .page-head { + margin-bottom: 1.5rem; + } + .page-head h1 { + margin: 0; } + .head-line { + height: 3px; + width: 3rem; + background: #c62828; + margin-top: 0.4rem; + } + form { display: flex; flex-direction: column; - gap: 0.75rem; - max-width: 320px; + gap: 1rem; + max-width: 340px; } label { display: flex; flex-direction: column; gap: 0.25rem; } + .label-text { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.75rem; + letter-spacing: 0.15em; + color: #888; + } input { - padding: 0.5rem; - border: 1px solid #ccc; - border-radius: 4px; + padding: 0.6rem 0.75rem; + border: 2px solid #1a1a1a; + border-radius: 0; + font-family: 'Inter', sans-serif; + font-size: 0.9rem; + font-weight: 500; + background: #fff; + } + input:focus { + outline: none; + border-color: #c62828; } - button[type="submit"] { - padding: 0.5rem; - background: #1a1a2e; - color: #eee; + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.6rem 1.5rem; + background: #1a1a1a; + color: #fff; border: none; - border-radius: 4px; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.85rem; + letter-spacing: 0.1em; cursor: pointer; + text-transform: uppercase; + transition: background 0.15s; + align-self: flex-start; } - button[type="submit"]:disabled { - opacity: 0.6; + .btn:hover { + background: #c62828; + } + .btn:disabled { + opacity: 0.4; + cursor: default; } + .error { - color: #c0392b; - font-size: 0.9rem; + color: #c62828; + font-family: 'Inter', sans-serif; + font-size: 0.85rem; + font-weight: 600; } + .toggle { margin-top: 1rem; } - button.link { + .link { background: none; border: none; - color: #1a1a2e; - text-decoration: underline; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.8rem; + letter-spacing: 0.08em; + color: #c62828; cursor: pointer; padding: 0; - font-size: inherit; + } + .link:hover { + text-decoration: underline; } </style>
@@ -53,26 +53,34 @@ }
} </script> -<h1>Upload</h1> +<div class="page-head"> + <h1>UPLOAD</h1> + <div class="head-line"></div> +</div> <form onsubmit={handleSubmit}> {#if error} <p class="error">{error}</p> {/if} {#if success} - <p class="success">{success} <a href="/browse">Browse images</a></p> + <p class="success">{success} <a href="/browse">BROWSE</a></p> {/if} <label> - Image / Video - <input type="file" accept="image/*,video/mp4,video/webm" required - onchange={(e) => { file = (e.target as HTMLInputElement).files?.[0] ?? null; }} - disabled={loading} - /> + <span class="label-text">FILE</span> + <div class="file-wrap"> + <input type="file" id="file-input" accept="image/*,video/mp4,video/webm" required + onchange={(e) => { file = (e.target as HTMLInputElement).files?.[0] ?? null; }} + disabled={loading} + /> + <label for="file-input" class="file-label"> + {file ? file.name : 'SELECT MEDIA'} + </label> + </div> </label> <label> - Tags (comma-separated) + <span class="label-text">TAGS (COMMA-SEPARATED)</span> <div class="tag-wrapper"> <input type="text" placeholder="funny, meme, cat" bind:value={tags} oninput={onTagInput}@@ -89,28 +97,82 @@ {/if}
</div> </label> - <button type="submit" disabled={loading || !file}> - {loading ? 'Uploading...' : 'Upload'} + <button type="submit" class="btn" disabled={loading || !file}> + {loading ? 'UPLOADING...' : 'UPLOAD'} </button> </form> <style> + .page-head { + margin-bottom: 1.5rem; + } + .page-head h1 { + margin: 0; + } + .head-line { + height: 3px; + width: 3rem; + background: #c62828; + margin-top: 0.4rem; + } + form { display: flex; flex-direction: column; gap: 1rem; max-width: 480px; } - label { + + .label-text { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.75rem; + letter-spacing: 0.15em; + color: #888; + } + + .file-wrap { + position: relative; + } + #file-input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; + } + .file-label { display: flex; - flex-direction: column; - gap: 0.25rem; + align-items: center; + padding: 0.6rem 0.75rem; + border: 2px solid #1a1a1a; + font-family: 'Inter', sans-serif; + font-size: 0.85rem; + font-weight: 500; + color: #888; + background: #fff; + cursor: pointer; + transition: border-color 0.15s; + } + .file-label:hover { + border-color: #c62828; + } + + input[type="text"] { + padding: 0.6rem 0.75rem; + border: 2px solid #1a1a1a; + border-radius: 0; + font-family: 'Inter', sans-serif; + font-size: 0.9rem; + font-weight: 500; + background: #fff; + width: 100%; } - input[type="file"], input[type="text"] { - padding: 0.5rem; - border: 1px solid #ccc; - border-radius: 4px; + input[type="text"]:focus { + outline: none; + border-color: #c62828; } + .tag-wrapper { position: relative; }@@ -120,7 +182,7 @@ top: 100%;
left: 0; right: 0; background: #fff; - border: 1px solid #ccc; + border: 2px solid #1a1a1a; border-top: none; list-style: none; margin: 0;@@ -130,29 +192,58 @@ max-height: 150px;
overflow-y: auto; } .suggestions li { - padding: 0.4rem 0.5rem; + padding: 0.45rem 0.75rem; cursor: pointer; - font-size: 0.9rem; + font-family: 'Inter', sans-serif; + font-size: 0.85rem; + font-weight: 500; } .suggestions li:hover { background: #f0f0f0; } - button[type="submit"] { - padding: 0.5rem 1.5rem; - background: #1a1a2e; - color: #eee; + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.6rem 1.5rem; + background: #1a1a1a; + color: #fff; border: none; - border-radius: 4px; + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.85rem; + letter-spacing: 0.1em; cursor: pointer; + text-transform: uppercase; + transition: background 0.15s; align-self: flex-start; } - button[type="submit"]:disabled { - opacity: 0.6; + .btn:hover { + background: #c62828; + } + .btn:disabled { + opacity: 0.4; + cursor: default; } + .error { - color: #c0392b; + color: #c62828; + font-family: 'Inter', sans-serif; + font-size: 0.85rem; + font-weight: 600; } .success { - color: #27ae60; + color: #2e7d32; + font-family: 'Inter', sans-serif; + font-size: 0.85rem; + font-weight: 600; + } + .success a { + font-family: 'Oswald', sans-serif; + font-weight: 500; + font-size: 0.8rem; + letter-spacing: 0.1em; + color: #c62828; } </style>