v3: reworked posting — three panels, drafts, duplicate detection, upload progress
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
Posting a dump was a single form where the important choices were the easiest to miss. It is now three panels: link or file, why & where, playlists. Composition: - No more URL/File toggle. An empty panel offers both ways in at once and the kind follows what you actually did; a file dropped anywhere in the modal is accepted, not just on the zone. - Categories and visibility get their own panel instead of a disclosure that read as optional, and the primary button stays "Next" until they've been seen. Visibility carries a real label now. - The draft (link, title, why, categories, visibility) is mirrored to localStorage on every change and restored on reopen, so Escape or a stray backdrop click costs nothing. Only an attached file can't be restored, so that is the one case that asks before closing. - URL dumps can carry a poster-supplied title instead of being stuck with whatever the page scraped, editable right under the preview. - Multipart uploads go through XHR so there is a real progress bar and a percentage on the button, rather than 50 MB of silence. Duplicates: - New dumps.url_canonical column (+ index, backfilled by 0013) holding a lossy key that ignores scheme, www., trailing slashes, tracking parameters and YouTube share shapes. GET /api/dumps/by-url reads it, and the create form warns "already dumped by X" while it fetches the preview. Never blocking. Fixes: - /api/preview now reports whether the page was actually reached: a failed fetch still yields a hostname-only stub, so a dead link and a page without metadata used to render identically. - The Web Share Target never worked. The manifest posts to "/", but the index redirect dropped the query string, so every Android share landed on the feed with nothing pre-filled. - File dumps no longer take the extension into their title. - The link field no longer autofocuses on touch, where it raised a keyboard over the modal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiAPtJZeCYYk8rKehtLUQU
This commit is contained in:
251
src/App.css
251
src/App.css
@@ -438,6 +438,222 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ── Create-dump modal ── */
|
||||
|
||||
/* A file dropped anywhere in the modal is accepted, so the whole body lights up. */
|
||||
.dump-create--drag {
|
||||
outline: 2px dashed var(--color-accent);
|
||||
outline-offset: 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dump-create-draft,
|
||||
.dump-create-confirm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.9rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.dump-create-draft {
|
||||
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dump-create-draft-discard {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
color: var(--color-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dump-create-confirm {
|
||||
background: var(--color-danger-bg);
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.dump-create-confirm p {
|
||||
margin: 0;
|
||||
flex: 1 1 14rem;
|
||||
}
|
||||
|
||||
.dump-create-confirm-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Not reached, or reached with nothing worth showing. */
|
||||
.preview-note {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* ── "Already dumped" hint ── */
|
||||
.dump-duplicate {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 9px;
|
||||
background: color-mix(in srgb, var(--color-accent) 9%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
|
||||
}
|
||||
|
||||
.dump-duplicate-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1.3;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dump-duplicate-body {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.dump-duplicate-lead {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dump-duplicate-link {
|
||||
font-size: 0.85rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dump-duplicate-more,
|
||||
.dump-duplicate-hint {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* ── Step indicator ── */
|
||||
.dump-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
margin: 0 0 1.1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dump-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Connector between steps — drawn on the gap, not on the labels. */
|
||||
.dump-step + .dump-step::before {
|
||||
content: "";
|
||||
width: 1.1rem;
|
||||
height: 1px;
|
||||
background: var(--color-border-subtle);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dump-step-marker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.35rem;
|
||||
height: 1.35rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 999px;
|
||||
border: 1.5px solid var(--color-border-subtle);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dump-step-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dump-step--current {
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dump-step--current .dump-step-marker {
|
||||
background: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-on-accent);
|
||||
}
|
||||
|
||||
.dump-step--done {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dump-step--done .dump-step-marker {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Only the current step keeps its label once the modal gets narrow. */
|
||||
@media (max-width: 30rem) {
|
||||
.dump-step:not(.dump-step--current) .dump-step-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Panels ── */
|
||||
.dump-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem; /* matches .form, so panels and plain forms space identically */
|
||||
animation: dump-panel-in 0.18s ease-out;
|
||||
}
|
||||
|
||||
.dump-panel--back {
|
||||
animation-name: dump-panel-in-back;
|
||||
}
|
||||
|
||||
@keyframes dump-panel-in {
|
||||
from { opacity: 0; transform: translateX(12px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
@keyframes dump-panel-in-back {
|
||||
from { opacity: 0; transform: translateX(-12px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dump-panel {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* One line naming the thing the second panel's choices apply to. */
|
||||
.dump-panel-recap {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Mode toggle — segmented control ── */
|
||||
.visibility-toggle {
|
||||
display: flex;
|
||||
@@ -592,6 +808,41 @@
|
||||
background: var(--color-border-subtle);
|
||||
}
|
||||
|
||||
/* ── Upload progress (second panel, while the file is being sent) ── */
|
||||
.dump-upload-progress {
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-border-subtle);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dump-upload-progress-fill {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--color-accent);
|
||||
transition: width 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Length unknown, or the body is sent and the server is still thinking. */
|
||||
.dump-upload-progress--indeterminate .dump-upload-progress-fill {
|
||||
width: 40%;
|
||||
animation: dump-upload-slide 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes dump-upload-slide {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(250%); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dump-upload-progress--indeterminate .dump-upload-progress-fill {
|
||||
width: 100%;
|
||||
animation: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Local file / URL preview (DumpCreate) ── */
|
||||
.local-preview-image {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user