Add the submission pipeline and the review loop
Steps 3 and 4 of the build order. A member can now upload a song, watch it convert, publish it, and review what everyone else has published. Pipeline: - ffprobe reads tags synchronously at submit so prefill never races typing; ffmpeg converts to Opus in the background, two at a time - ffmpeg succeeding is the validation — no container sniffing - publish moves the file inside the transaction, so a song row and its .ogg appear together or neither does - five submissions per rolling 24h, failures excluded Reviews and the reveal rule: - the queue is unreviewed songs only, oldest first, never your own - other people's reviews and the average are withheld in the query, not the template — a hidden average is never sent - 30 minutes to edit or delete your own review, enforced in the WHERE clause - deleting the last review unlocks the song for its submitter again The waiting page has one button: the metadata form autosaves after a pause in typing, and Julkaise submits it and publishes in the same request, so nothing is lost without JS. Genres store an English code and render a Finnish label.
This commit is contained in:
@@ -145,3 +145,86 @@ tr.banned { opacity: 0.55; }
|
||||
code { background: var(--surface-2); padding: 0.1rem 0.35rem; border-radius: var(--radius); }
|
||||
|
||||
.invite { word-break: break-all; }
|
||||
|
||||
/* Drop target that is the file input — the native control is hidden behind it, so keyboard
|
||||
focus, validation and submission keep working. */
|
||||
.dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 2.2rem 1rem;
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.dropzone:hover { border-color: var(--accent); }
|
||||
.dropzone.over { border-color: var(--accent); background: var(--surface-2); }
|
||||
.dropzone.has-file { border-style: solid; border-color: var(--accent); }
|
||||
|
||||
/* Visually hidden, still focusable and still the thing that gets submitted. */
|
||||
.dropzone input[type="file"] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dropzone:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
.dz-title { font-family: var(--font-head); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.filename { color: var(--accent); word-break: break-all; }
|
||||
.small { font-size: 0.85rem; }
|
||||
|
||||
select, textarea {
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.5rem 0.6rem;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
textarea { resize: vertical; }
|
||||
select:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
|
||||
.player { width: 100%; margin: 0.6rem 0 1rem; }
|
||||
.byline { color: var(--muted); margin-top: -0.3rem; }
|
||||
.intro { white-space: pre-wrap; background: var(--surface); padding: 0.8rem 1rem;
|
||||
border-left: 3px solid var(--border); border-radius: var(--radius); }
|
||||
.empty { font-family: var(--font-head); text-transform: uppercase; color: var(--muted);
|
||||
padding: 2rem 0; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
.tag.done { background: var(--surface-2); color: var(--muted); }
|
||||
|
||||
.average { font-size: 1.1rem; }
|
||||
.score { display: inline-block; font-family: var(--font-head); font-size: 1.1rem;
|
||||
color: var(--accent); }
|
||||
|
||||
.review { background: var(--surface); border-radius: var(--radius); padding: 0.8rem 1rem;
|
||||
margin-bottom: 0.8rem; }
|
||||
.review header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.4rem; }
|
||||
.review p { white-space: pre-wrap; margin: 0; }
|
||||
|
||||
.scorerow { display: flex; align-items: center; gap: 0.8rem; }
|
||||
.scorerow input[type="range"] { flex: 1; accent-color: var(--accent); }
|
||||
.scorerow output { font-family: var(--font-head); font-size: 1.3rem; color: var(--accent);
|
||||
min-width: 2.5ch; text-align: right; }
|
||||
|
||||
.editbox { background: var(--surface); border-radius: var(--radius); padding: 0.6rem 1rem;
|
||||
margin-bottom: 1.5rem; }
|
||||
.editbox summary { cursor: pointer; font-family: var(--font-head); text-transform: uppercase; }
|
||||
.editbox form { margin: 0.8rem 0; }
|
||||
|
||||
button.danger { background: var(--accent-2); color: var(--text); }
|
||||
button.danger:hover { background: #e53935; }
|
||||
|
||||
.saved { color: var(--muted); font-size: 0.85rem; min-height: 1.2em; }
|
||||
.status { background: var(--surface); border-left: 3px solid var(--accent); border-radius: var(--radius);
|
||||
padding: 0.8rem 1rem; margin-bottom: 1.2rem; }
|
||||
.status p { margin: 0 0 0.5rem; }
|
||||
button:disabled { background: var(--surface-2); color: var(--muted); cursor: not-allowed; }
|
||||
|
||||
Reference in New Issue
Block a user