book.html: a real Choose file button, and drop the paste box
"Drop the manuscript here or click to choose a file" is a drop zone pretending to be a button - it reads as an instruction rather than a control, and on a laptop nobody drags a file from anywhere. There is now an actual button. The zone still accepts a drop, because that costs nothing once the button exists. The paste box is gone. It was there for a manuscript that would not upload cleanly, but a book is 100,000 words and nobody pastes that; keeping it meant offering a route that only works for the case it was not for. Word and PDF cover it, and a PDF that turns out to be a scan is refused with the reason.
This commit is contained in:
parent
d5e6df23d2
commit
d1c9320966
1 changed files with 30 additions and 47 deletions
77
book.html
77
book.html
|
|
@ -20,8 +20,11 @@
|
||||||
.state.reading{color:var(--muted)}
|
.state.reading{color:var(--muted)}
|
||||||
.state.failed{color:#e0685f;border-color:#e0685f}
|
.state.failed{color:#e0685f;border-color:#e0685f}
|
||||||
.why{color:#e0685f;font-size:13px;margin-top:4px}
|
.why{color:#e0685f;font-size:13px;margin-top:4px}
|
||||||
.drop.on{border-color:var(--accent)}
|
|
||||||
.tiny{font-size:12px;color:var(--muted)}
|
.tiny{font-size:12px;color:var(--muted)}
|
||||||
|
.pick{display:flex;gap:14px;align-items:center;flex-wrap:wrap;
|
||||||
|
border:1px dashed var(--line);border-radius:12px;padding:18px}
|
||||||
|
.pick.on{border-color:var(--accent);border-style:solid}
|
||||||
|
.pick .or{color:var(--muted);font-size:13px}
|
||||||
.writers{display:flex;flex-wrap:wrap;gap:10px;margin-top:8px}
|
.writers{display:flex;flex-wrap:wrap;gap:10px;margin-top:8px}
|
||||||
.wr{display:flex;gap:8px;align-items:center;border:1px solid var(--line);
|
.wr{display:flex;gap:8px;align-items:center;border:1px solid var(--line);
|
||||||
border-radius:10px;padding:9px 12px;cursor:pointer}
|
border-radius:10px;padding:9px 12px;cursor:pointer}
|
||||||
|
|
@ -44,20 +47,13 @@
|
||||||
|
|
||||||
<h2 class="sec">1 · The manuscript</h2>
|
<h2 class="sec">1 · The manuscript</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Upload a file</label>
|
<label>The book</label>
|
||||||
<span class="hint">A <strong>.docx</strong>, a text <strong>.pdf</strong>, or a <strong>.txt</strong>, up to 6MB. A scanned PDF has no text in it and will be refused — upload the original document instead.</span>
|
<span class="hint">A Word <strong>.docx</strong> or a <strong>.pdf</strong>, up to 6MB. A scanned PDF has no text in it and will be refused — upload the original document instead.</span>
|
||||||
<div class="drop" id="drop">
|
<div class="pick" id="pick">
|
||||||
<p><strong>Drop the manuscript here</strong></p>
|
<button type="button" class="btn primary" id="choose">Choose a file…</button>
|
||||||
<p class="tiny">or click to choose a file</p>
|
<span class="or" id="pickmsg">or drag the manuscript onto this panel</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="file" accept=".docx,.pdf,.txt,.md,.text" style="display:none">
|
<input type="file" id="file" accept=".docx,.pdf,.txt,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document" style="display:none">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label for="paste">Or paste the text</label>
|
|
||||||
<span class="hint">For anything that will not upload cleanly — Google Docs, Scrivener, a PDF that is really a scan.</span>
|
|
||||||
<textarea id="paste" rows="5" placeholder="Paste the whole manuscript here."></textarea>
|
|
||||||
<div style="margin-top:8px"><button type="button" class="btn" id="usepaste">Use this text</button></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="books" id="books"><p class="empty">No manuscripts yet.</p></div>
|
<div class="books" id="books"><p class="empty">No manuscripts yet.</p></div>
|
||||||
|
|
@ -70,13 +66,13 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="guidance">Your guidance</label>
|
<label for="guidance">Your guidance</label>
|
||||||
<span class="hint">You know this book better than any of them will. Say what the trailer has to carry, which scenes matter, which it must not give away, who the audience is, and the tone. This outranks the house defaults.</span>
|
<span class="hint">You know this book better than any of them will. Say what the trailer has to carry, which scenes matter, which it must not give away, who the audience is, and the tone. This outranks the house defaults.</span>
|
||||||
<textarea id="guidance" rows="7" placeholder="The bell is the spine of the story — it has to be in the first ten seconds and it has to be explained, not teased. Do not resolve the 1971 frame; the book leaves it open and so should the trailer. The dolphin never speaks aloud."></textarea>
|
<textarea id="guidance" rows="7" placeholder="What the trailer has to carry, what it must not give away, and the tone you want."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="sec" style="margin-top:34px">3 · Who writes it</h2>
|
<h2 class="sec" style="margin-top:34px">3 · Who writes it</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Writers</label>
|
<label>Writers</label>
|
||||||
<span class="hint">Each one reads the whole book and writes its own trailer, with its own cast. Tick at least two if you want them to critique each other. The <em>lead</em> is the one that will write the revision after the reviews come back.</span>
|
<span class="hint">Each one reads the whole book and writes its own trailer, with its own cast. Tick at least two if you want them to critique each other. The <em>lead</em> is the one that will write the revision after the reviews come back — click a ticked name to make it the lead.</span>
|
||||||
<div class="writers" id="writers"></div>
|
<div class="writers" id="writers"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -110,7 +106,7 @@ function readFileB64(file) {
|
||||||
if (file.size > MAX_DOC_BYTES) {
|
if (file.size > MAX_DOC_BYTES) {
|
||||||
reject(new Error("That file is " + (file.size / 1048576).toFixed(1)
|
reject(new Error("That file is " + (file.size / 1048576).toFixed(1)
|
||||||
+ "MB and the limit is 6MB. Upload the .docx rather than an "
|
+ "MB and the limit is 6MB. Upload the .docx rather than an "
|
||||||
+ "illustrated PDF, or paste the text in instead."));
|
+ "illustrated PDF."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
|
|
@ -197,16 +193,20 @@ function readFileB64(file) {
|
||||||
|
|
||||||
// ---------------------------------------------------------------- upload
|
// ---------------------------------------------------------------- upload
|
||||||
|
|
||||||
var drop = document.getElementById("drop");
|
var pick = document.getElementById("pick");
|
||||||
|
var pickMsg = document.getElementById("pickmsg");
|
||||||
var fileInput = document.getElementById("file");
|
var fileInput = document.getElementById("file");
|
||||||
drop.addEventListener("click", function () { fileInput.click(); });
|
|
||||||
|
document.getElementById("choose").addEventListener("click", function () {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
["dragenter", "dragover"].forEach(function (k) {
|
["dragenter", "dragover"].forEach(function (k) {
|
||||||
drop.addEventListener(k, function (e) { e.preventDefault(); drop.classList.add("on"); });
|
pick.addEventListener(k, function (e) { e.preventDefault(); pick.classList.add("on"); });
|
||||||
});
|
});
|
||||||
["dragleave", "drop"].forEach(function (k) {
|
["dragleave", "drop"].forEach(function (k) {
|
||||||
drop.addEventListener(k, function (e) { e.preventDefault(); drop.classList.remove("on"); });
|
pick.addEventListener(k, function (e) { e.preventDefault(); pick.classList.remove("on"); });
|
||||||
});
|
});
|
||||||
drop.addEventListener("drop", function (e) {
|
pick.addEventListener("drop", function (e) {
|
||||||
var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||||||
if (f) send(f);
|
if (f) send(f);
|
||||||
});
|
});
|
||||||
|
|
@ -217,40 +217,23 @@ function readFileB64(file) {
|
||||||
|
|
||||||
async function send(file) {
|
async function send(file) {
|
||||||
hideMsg("msg");
|
hideMsg("msg");
|
||||||
drop.innerHTML = '<p><strong>Reading ' + esc(file.name) + '…</strong></p>';
|
pickMsg.textContent = "reading " + file.name + "…";
|
||||||
var doc;
|
var doc;
|
||||||
try { doc = await readFileB64(file); }
|
try { doc = await readFileB64(file); }
|
||||||
catch (err) { resetDrop(); showMsg("msg", err.message); return; }
|
catch (err) { resetPick(); showMsg("msg", err.message); return; }
|
||||||
drop.innerHTML = '<p><strong>Uploading ' + esc(file.name) + '…</strong></p>';
|
pickMsg.textContent = "uploading " + file.name + " ("
|
||||||
|
+ (doc.bytes / 1048576).toFixed(1) + "MB)…";
|
||||||
var r = await api("upload_book", { filename: doc.name, data_b64: doc.b64 });
|
var r = await api("upload_book", { filename: doc.name, data_b64: doc.b64 });
|
||||||
resetDrop();
|
resetPick();
|
||||||
if (!r.ok) { showMsg("msg", r.error || "That upload did not go through."); return; }
|
if (!r.ok) { showMsg("msg", r.error || "That upload did not go through."); return; }
|
||||||
PICKED = null;
|
PICKED = null;
|
||||||
loadBooks();
|
loadBooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetDrop() {
|
function resetPick() {
|
||||||
drop.innerHTML = '<p><strong>Drop the manuscript here</strong></p>'
|
pickMsg.textContent = "or drag the manuscript onto this panel";
|
||||||
+ '<p class="tiny">or click to choose a file</p>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("usepaste").addEventListener("click", async function () {
|
|
||||||
var btn = this;
|
|
||||||
var text = document.getElementById("paste").value;
|
|
||||||
if (text.trim().length < 2000) {
|
|
||||||
showMsg("msg", "That is " + text.trim().length + " characters. Paste the manuscript, not a summary.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hideMsg("msg");
|
|
||||||
btn.disabled = true; btn.textContent = "Saving…";
|
|
||||||
var r = await api("paste_book", { text: text, filename: "pasted.txt" });
|
|
||||||
btn.disabled = false; btn.textContent = "Use this text";
|
|
||||||
if (!r.ok) { showMsg("msg", r.error || "That did not save."); return; }
|
|
||||||
document.getElementById("paste").value = "";
|
|
||||||
PICKED = null;
|
|
||||||
loadBooks();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- writers
|
// ---------------------------------------------------------------- writers
|
||||||
|
|
||||||
function paintWriters() {
|
function paintWriters() {
|
||||||
|
|
@ -292,7 +275,7 @@ function readFileB64(file) {
|
||||||
// Writing is the cheap half. Drawing the storyboards is priced separately,
|
// Writing is the cheap half. Drawing the storyboards is priced separately,
|
||||||
// per writer, once you have read the scripts.
|
// per writer, once you have read the scripts.
|
||||||
el.innerHTML = n + " writer" + (n > 1 ? "s" : "")
|
el.innerHTML = n + " writer" + (n > 1 ? "s" : "")
|
||||||
+ " · about $" + (n * 0.5).toFixed(2) + " to write"
|
+ " · about $" + (n * 0.6).toFixed(2) + " to write"
|
||||||
+ " · storyboards are priced separately once you have read them";
|
+ " · storyboards are priced separately once you have read them";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue