Compare commits
4 Commits
c36ae9d91b
...
d164a58ba0
Author | SHA1 | Date |
---|---|---|
Ben Visness | d164a58ba0 | |
mark.dev | a295d0ed52 | |
Ben Visness | 1806da0389 | |
ilidemi | 25e13df04d |
|
@ -0,0 +1,246 @@
|
|||
// Requires base64.js
|
||||
|
||||
/**
|
||||
* Sets up file / image uploading for Markdown content.
|
||||
*
|
||||
* @param eSubmits A list of elements of buttons to submit/save the page you're on. They
|
||||
* will be disabled and tell users files are uploading while uploading is
|
||||
* happening.
|
||||
* @param eFileInput The `<input type="file">`
|
||||
* @param eUploadBar Usually looks like
|
||||
* ```
|
||||
* <div class="upload_bar flex-grow-1">
|
||||
* <div class="instructions">
|
||||
* Upload files by dragging & dropping, pasting, or <label class="pointer link" for="file_input">selecting</label> them.
|
||||
* </div>
|
||||
* <div class="progress flex">
|
||||
* <div class="progress_text mr3"></div>
|
||||
* <div class="progress_bar flex-grow-1 flex-shrink-1 pa1"><div class=""></div></div>
|
||||
* </div>
|
||||
* </div>
|
||||
* ```
|
||||
* @param eText The text field that can be dropped into and is editing the markdown.
|
||||
* @param doMarkdown The function returned by `initLiveMarkdown`.
|
||||
* @param maxFileSize The max allowed file size in bytes.
|
||||
* @param uploadUrl The URL to POST assets to (unique per project to avoid CORS issues).
|
||||
*/
|
||||
function setupMarkdownUpload(eSubmits, eFileInput, eUploadBar, eText, doMarkdown, maxFileSize, uploadUrl) {
|
||||
const submitTexts = Array.from(eSubmits).map(e => e.value);
|
||||
const uploadProgress = eUploadBar.querySelector('.progress');
|
||||
const uploadProgressText = eUploadBar.querySelector('.progress_text');
|
||||
const uploadProgressBar = eUploadBar.querySelector('.progress_bar');
|
||||
const uploadProgressBarFill = eUploadBar.querySelector('.progress_bar > div');
|
||||
let fileCounter = 0;
|
||||
let enterCounter = 0;
|
||||
let uploadQueue = [];
|
||||
let currentUpload = null;
|
||||
let currentXhr = null;
|
||||
let currentBatchSize = 0;
|
||||
let currentBatchDone = 0;
|
||||
|
||||
eFileInput.addEventListener("change", function(ev) {
|
||||
if (eFileInput.files.length > 0) {
|
||||
importUserFiles(eFileInput.files);
|
||||
}
|
||||
});
|
||||
|
||||
eText.addEventListener("dragover", function(ev) {
|
||||
let effect = "none";
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
||||
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
||||
effect = "copy";
|
||||
break;
|
||||
}
|
||||
}
|
||||
ev.dataTransfer.dropEffect = effect;
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
eText.addEventListener("dragenter", function(ev) {
|
||||
enterCounter++;
|
||||
let droppable = false;
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
||||
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
||||
droppable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (droppable) {
|
||||
eText.classList.add("drop");
|
||||
}
|
||||
});
|
||||
|
||||
eText.addEventListener("dragleave", function(ev) {
|
||||
enterCounter--;
|
||||
if (enterCounter == 0) {
|
||||
eText.classList.remove("drop");
|
||||
}
|
||||
});
|
||||
|
||||
function makeUploadString(uploadNumber, filename) {
|
||||
return `Uploading file #${uploadNumber}: \`${filename}\`...`;
|
||||
}
|
||||
|
||||
eText.addEventListener("drop", function(ev) {
|
||||
enterCounter = 0;
|
||||
eText.classList.remove("drop");
|
||||
|
||||
if (ev.dataTransfer && ev.dataTransfer.files) {
|
||||
importUserFiles(ev.dataTransfer.files)
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
eText.addEventListener("paste", function(ev) {
|
||||
const files = ev.clipboardData?.files ?? [];
|
||||
if (files.length > 0) {
|
||||
importUserFiles(files)
|
||||
}
|
||||
});
|
||||
|
||||
function importUserFiles(files) {
|
||||
let items = [];
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
let f = files[i];
|
||||
if (f.size < maxFileSize) {
|
||||
items.push({ file: f, error: null });
|
||||
} else {
|
||||
items.push({ file: null, error: `\`${f.name}\` is too big! Max size is ${maxFileSize} but the file is ${f.size}.` });
|
||||
}
|
||||
}
|
||||
|
||||
let cursorStart = eText.selectionStart;
|
||||
let cursorEnd = eText.selectionEnd;
|
||||
|
||||
let toInsert = "";
|
||||
let linesToCursor = eText.value.substr(0, cursorStart).split("\n");
|
||||
let cursorLine = linesToCursor[linesToCursor.length-1].trim();
|
||||
if (cursorLine.length > 0) {
|
||||
toInsert = "\n\n";
|
||||
}
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
if (items[i].file) {
|
||||
fileCounter++;
|
||||
toInsert += makeUploadString(fileCounter, items[i].file.name) + "\n\n";
|
||||
queueUpload(fileCounter, items[i].file);
|
||||
} else {
|
||||
toInsert += `${items[i].error}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
eText.value = eText.value.substring(0, cursorStart) + toInsert + eText.value.substring(cursorEnd, eText.value.length);
|
||||
doMarkdown();
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
function replaceUploadString(upload, newString) {
|
||||
let cursorStart = eText.selectionStart;
|
||||
let cursorEnd = eText.selectionEnd;
|
||||
let uploadString = makeUploadString(upload.uploadNumber, upload.file.name);
|
||||
let insertIndex = eText.value.indexOf(uploadString)
|
||||
eText.value = eText.value.replace(uploadString, newString);
|
||||
if (cursorStart <= insertIndex + uploadString.length) {
|
||||
eText.selectionStart = cursorStart;
|
||||
} else {
|
||||
eText.selectionStart = cursorStart - uploadString.length + newString.length;
|
||||
}
|
||||
if (cursorEnd <= insertIndex + uploadString.length) {
|
||||
eText.selectionEnd = cursorEnd;
|
||||
} else {
|
||||
eText.selectionEnd = cursorEnd - uploadString.length + newString.length;
|
||||
}
|
||||
doMarkdown();
|
||||
}
|
||||
|
||||
function replaceUploadStringError(upload) {
|
||||
replaceUploadString(upload, `There was a problem uploading your file \`${upload.file.name}\`.`);
|
||||
}
|
||||
|
||||
function queueUpload(uploadNumber, file) {
|
||||
uploadQueue.push({
|
||||
uploadNumber: uploadNumber,
|
||||
file: file
|
||||
});
|
||||
|
||||
currentBatchSize++;
|
||||
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
|
||||
}
|
||||
|
||||
function uploadDone(ev) {
|
||||
try {
|
||||
if (currentXhr.status == 200 && currentXhr.response) {
|
||||
if (currentXhr.response.url) {
|
||||
let url = currentXhr.response.url;
|
||||
let newString = `[${currentUpload.file.name}](${url})`;
|
||||
if (currentXhr.response.mime.startsWith("image")) {
|
||||
newString = "!" + newString;
|
||||
}
|
||||
|
||||
replaceUploadString(currentUpload, newString);
|
||||
} else if (currentXhr.response.error) {
|
||||
replaceUploadString(currentUpload, `Upload failed for \`${currentUpload.file.name}\`: ${currentXhr.response.error}.`);
|
||||
} else {
|
||||
replaceUploadStringError(currentUpload);
|
||||
}
|
||||
} else {
|
||||
replaceUploadStringError(currentUpload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
replaceUploadStringError(currentUpload);
|
||||
}
|
||||
currentUpload = null;
|
||||
currentXhr = null;
|
||||
currentBatchDone++;
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
function updateUploadProgress(ev) {
|
||||
if (ev.lengthComputable) {
|
||||
let progress = ev.loaded / ev.total;
|
||||
uploadProgressBarFill.style.width = Math.floor(progress * 100) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
function uploadNext() {
|
||||
if (currentUpload == null) {
|
||||
next = uploadQueue.shift();
|
||||
if (next) {
|
||||
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
|
||||
eUploadBar.classList.add("uploading");
|
||||
uploadProgressBarFill.style.width = "0%";
|
||||
for (const e of eSubmits) {
|
||||
e.disabled = true;
|
||||
e.value = "Uploading files...";
|
||||
}
|
||||
|
||||
try {
|
||||
let utf8Filename = strToUTF8Arr(next.file.name);
|
||||
let base64Filename = base64EncArr(utf8Filename);
|
||||
// NOTE(asaf): We use XHR because fetch can't do upload progress reports. Womp womp. https://youtu.be/Pubd-spHN-0?t=2
|
||||
currentXhr = new XMLHttpRequest();
|
||||
currentXhr.upload.addEventListener("progress", updateUploadProgress);
|
||||
currentXhr.open("POST", uploadUrl, true);
|
||||
currentXhr.setRequestHeader("Hmn-Upload-Filename", base64Filename);
|
||||
currentXhr.responseType = "json";
|
||||
currentXhr.addEventListener("loadend", uploadDone);
|
||||
currentXhr.send(next.file);
|
||||
currentUpload = next;
|
||||
} catch (err) {
|
||||
replaceUploadStringError(next);
|
||||
console.error(err);
|
||||
uploadNext();
|
||||
}
|
||||
} else {
|
||||
for (const [i, e] of eSubmits.entries()) {
|
||||
e.disabled = false;
|
||||
e.value = submitTexts[i];
|
||||
}
|
||||
eUploadBar.classList.remove("uploading");
|
||||
currentBatchSize = 0;
|
||||
currentBatchDone = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -163,16 +163,8 @@ func SampleSeed() {
|
|||
}
|
||||
}
|
||||
|
||||
// admin := CreateAdminUser("admin", "12345678")
|
||||
// user := CreateUser("regular_user", "12345678")
|
||||
// hmnProject := CreateProject("hmn", "Handmade Network")
|
||||
// Create category
|
||||
// Create thread
|
||||
// Create accepted user project
|
||||
// Create pending user project
|
||||
// Create showcase items
|
||||
// Create codelanguages
|
||||
// Create library and library resources
|
||||
// Finally, set sequence numbers to things that won't conflict
|
||||
utils.Must1(tx.Exec(ctx, "SELECT setval('project_id_seq', 100, true);"))
|
||||
|
||||
utils.Must0(tx.Commit(ctx))
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
{{ template "markdown_previews.html" . }}
|
||||
|
||||
<script src="{{ static "js/base64.js" }}"></script>
|
||||
<script src="{{ static "js/markdown_upload.js" }}"></script>
|
||||
|
||||
<style>
|
||||
#editor {
|
||||
|
@ -121,222 +122,14 @@
|
|||
/*
|
||||
/ Asset upload
|
||||
*/
|
||||
const submitButton = document.querySelector("#form input[type=submit]");
|
||||
const submitText = submitButton.value;
|
||||
const fileInput = document.querySelector('#file_input');
|
||||
const uploadBar = document.querySelector('.upload_bar');
|
||||
const uploadProgress = document.querySelector('.upload_bar .progress');
|
||||
const uploadProgressText = document.querySelector('.upload_bar .progress_text');
|
||||
const uploadProgressBar = document.querySelector('.upload_bar .progress_bar');
|
||||
const uploadProgressBarFill = document.querySelector('.upload_bar .progress_bar > div');
|
||||
let fileCounter = 0;
|
||||
let enterCounter = 0;
|
||||
let uploadQueue = [];
|
||||
let currentUpload = null;
|
||||
let currentXhr = null;
|
||||
let currentBatchSize = 0;
|
||||
let currentBatchDone = 0;
|
||||
|
||||
fileInput.addEventListener("change", function(ev) {
|
||||
if (fileInput.files.length > 0) {
|
||||
importUserFiles(fileInput.files);
|
||||
}
|
||||
});
|
||||
|
||||
textField.addEventListener("dragover", function(ev) {
|
||||
let effect = "none";
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
||||
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
||||
effect = "copy";
|
||||
break;
|
||||
}
|
||||
}
|
||||
ev.dataTransfer.dropEffect = effect;
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
textField.addEventListener("dragenter", function(ev) {
|
||||
enterCounter++;
|
||||
let droppable = false;
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
||||
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
||||
droppable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (droppable) {
|
||||
textField.classList.add("drop");
|
||||
}
|
||||
});
|
||||
|
||||
textField.addEventListener("dragleave", function(ev) {
|
||||
enterCounter--;
|
||||
if (enterCounter == 0) {
|
||||
textField.classList.remove("drop");
|
||||
}
|
||||
});
|
||||
|
||||
function makeUploadString(uploadNumber, filename) {
|
||||
return `Uploading file #${uploadNumber}: \`${filename}\`...`;
|
||||
}
|
||||
|
||||
textField.addEventListener("drop", function(ev) {
|
||||
enterCounter = 0;
|
||||
textField.classList.remove("drop");
|
||||
|
||||
if (ev.dataTransfer && ev.dataTransfer.files) {
|
||||
importUserFiles(ev.dataTransfer.files)
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
textField.addEventListener("paste", function(ev) {
|
||||
const files = ev.clipboardData?.files ?? [];
|
||||
if (files.length > 0) {
|
||||
importUserFiles(files)
|
||||
}
|
||||
});
|
||||
|
||||
function importUserFiles(files) {
|
||||
let items = [];
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
let f = files[i];
|
||||
if (f.size < maxFileSize) {
|
||||
items.push({ file: f, error: null });
|
||||
} else {
|
||||
items.push({ file: null, error: `\`${f.name}\` is too big! Max size is ${maxFileSize} but the file is ${f.size}.` });
|
||||
}
|
||||
}
|
||||
|
||||
let cursorStart = textField.selectionStart;
|
||||
let cursorEnd = textField.selectionEnd;
|
||||
|
||||
let toInsert = "";
|
||||
let linesToCursor = textField.value.substr(0, cursorStart).split("\n");
|
||||
let cursorLine = linesToCursor[linesToCursor.length-1].trim();
|
||||
if (cursorLine.length > 0) {
|
||||
toInsert = "\n\n";
|
||||
}
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
if (items[i].file) {
|
||||
fileCounter++;
|
||||
toInsert += makeUploadString(fileCounter, items[i].file.name) + "\n\n";
|
||||
queueUpload(fileCounter, items[i].file);
|
||||
} else {
|
||||
toInsert += `${items[i].error}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
textField.value = textField.value.substring(0, cursorStart) + toInsert + textField.value.substring(cursorEnd, textField.value.length);
|
||||
doMarkdown();
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
function replaceUploadString(upload, newString) {
|
||||
let cursorStart = textField.selectionStart;
|
||||
let cursorEnd = textField.selectionEnd;
|
||||
let uploadString = makeUploadString(upload.uploadNumber, upload.file.name);
|
||||
let insertIndex = textField.value.indexOf(uploadString)
|
||||
textField.value = textField.value.replace(uploadString, newString);
|
||||
if (cursorStart <= insertIndex + uploadString.length) {
|
||||
textField.selectionStart = cursorStart;
|
||||
} else {
|
||||
textField.selectionStart = cursorStart - uploadString.length + newString.length;
|
||||
}
|
||||
if (cursorEnd <= insertIndex + uploadString.length) {
|
||||
textField.selectionEnd = cursorEnd;
|
||||
} else {
|
||||
textField.selectionEnd = cursorEnd - uploadString.length + newString.length;
|
||||
}
|
||||
doMarkdown();
|
||||
}
|
||||
|
||||
function replaceUploadStringError(upload) {
|
||||
replaceUploadString(upload, `There was a problem uploading your file \`${upload.file.name}\`.`);
|
||||
}
|
||||
|
||||
function queueUpload(uploadNumber, file) {
|
||||
uploadQueue.push({
|
||||
uploadNumber: uploadNumber,
|
||||
file: file
|
||||
});
|
||||
|
||||
currentBatchSize++;
|
||||
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
|
||||
}
|
||||
|
||||
function uploadDone(ev) {
|
||||
try {
|
||||
if (currentXhr.status == 200 && currentXhr.response) {
|
||||
if (currentXhr.response.url) {
|
||||
let url = currentXhr.response.url;
|
||||
let newString = `[${currentUpload.file.name}](${url})`;
|
||||
if (currentXhr.response.mime.startsWith("image")) {
|
||||
newString = "!" + newString;
|
||||
}
|
||||
|
||||
replaceUploadString(currentUpload, newString);
|
||||
} else if (currentXhr.response.error) {
|
||||
replaceUploadString(currentUpload, `Upload failed for \`${currentUpload.file.name}\`: ${currentXhr.response.error}.`);
|
||||
} else {
|
||||
replaceUploadStringError(currentUpload);
|
||||
}
|
||||
} else {
|
||||
replaceUploadStringError(currentUpload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
replaceUploadStringError(currentUpload);
|
||||
}
|
||||
currentUpload = null;
|
||||
currentXhr = null;
|
||||
currentBatchDone++;
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
function updateUploadProgress(ev) {
|
||||
if (ev.lengthComputable) {
|
||||
let progress = ev.loaded / ev.total;
|
||||
uploadProgressBarFill.style.width = Math.floor(progress * 100) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
function uploadNext() {
|
||||
if (currentUpload == null) {
|
||||
next = uploadQueue.shift();
|
||||
if (next) {
|
||||
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
|
||||
uploadBar.classList.add("uploading");
|
||||
uploadProgressBarFill.style.width = "0%";
|
||||
submitButton.disabled = true;
|
||||
submitButton.value = "Uploading files...";
|
||||
|
||||
try {
|
||||
let utf8Filename = strToUTF8Arr(next.file.name);
|
||||
let base64Filename = base64EncArr(utf8Filename);
|
||||
// NOTE(asaf): We use XHR because fetch can't do upload progress reports. Womp womp. https://youtu.be/Pubd-spHN-0?t=2
|
||||
currentXhr = new XMLHttpRequest();
|
||||
currentXhr.upload.addEventListener("progress", updateUploadProgress);
|
||||
currentXhr.open("POST", uploadUrl, true);
|
||||
currentXhr.setRequestHeader("Hmn-Upload-Filename", base64Filename);
|
||||
currentXhr.responseType = "json";
|
||||
currentXhr.addEventListener("loadend", uploadDone);
|
||||
currentXhr.send(next.file);
|
||||
currentUpload = next;
|
||||
} catch (err) {
|
||||
replaceUploadStringError(next);
|
||||
console.error(err);
|
||||
uploadNext();
|
||||
}
|
||||
} else {
|
||||
submitButton.disabled = false;
|
||||
submitButton.value = submitText;
|
||||
uploadBar.classList.remove("uploading");
|
||||
currentBatchSize = 0;
|
||||
currentBatchDone = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
setupMarkdownUpload(
|
||||
document.querySelectorAll("#form input[type=submit]"),
|
||||
document.querySelector('#file_input'),
|
||||
document.querySelector('.upload_bar'),
|
||||
textField,
|
||||
doMarkdown,
|
||||
maxFileSize,
|
||||
uploadUrl
|
||||
);
|
||||
</script>
|
||||
{{ end }}
|
||||
|
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 89 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFDB5E" d="M34.956 17.916c0-.503-.12-.975-.321-1.404-1.341-4.326-7.619-4.01-16.549-4.221-1.493-.035-.639-1.798-.115-5.668.341-2.517-1.282-6.382-4.01-6.382-4.498 0-.171 3.548-4.148 12.322-2.125 4.688-6.875 2.062-6.875 6.771v10.719c0 1.833.18 3.595 2.758 3.885C8.195 34.219 7.633 36 11.238 36h18.044c1.838 0 3.333-1.496 3.333-3.334 0-.762-.267-1.456-.698-2.018 1.02-.571 1.72-1.649 1.72-2.899 0-.76-.266-1.454-.696-2.015 1.023-.57 1.725-1.649 1.725-2.901 0-.909-.368-1.733-.961-2.336.757-.611 1.251-1.535 1.251-2.581z"/><path fill="#EE9547" d="M23.02 21.249h8.604c1.17 0 2.268-.626 2.866-1.633.246-.415.109-.952-.307-1.199-.415-.247-.952-.108-1.199.307-.283.479-.806.775-1.361.775h-8.81c-.873 0-1.583-.71-1.583-1.583s.71-1.583 1.583-1.583H28.7c.483 0 .875-.392.875-.875s-.392-.875-.875-.875h-5.888c-1.838 0-3.333 1.495-3.333 3.333 0 1.025.475 1.932 1.205 2.544-.615.605-.998 1.445-.998 2.373 0 1.028.478 1.938 1.212 2.549-.611.604-.99 1.441-.99 2.367 0 1.12.559 2.108 1.409 2.713-.524.589-.852 1.356-.852 2.204 0 1.838 1.495 3.333 3.333 3.333h5.484c1.17 0 2.269-.625 2.867-1.632.247-.415.11-.952-.305-1.199-.416-.245-.953-.11-1.199.305-.285.479-.808.776-1.363.776h-5.484c-.873 0-1.583-.71-1.583-1.583s.71-1.583 1.583-1.583h6.506c1.17 0 2.27-.626 2.867-1.633.247-.416.11-.953-.305-1.199-.419-.251-.954-.11-1.199.305-.289.487-.799.777-1.363.777h-7.063c-.873 0-1.583-.711-1.583-1.584s.71-1.583 1.583-1.583h8.091c1.17 0 2.269-.625 2.867-1.632.247-.415.11-.952-.305-1.199-.417-.246-.953-.11-1.199.305-.289.486-.799.776-1.363.776H23.02c-.873 0-1.583-.71-1.583-1.583s.709-1.584 1.583-1.584z"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#662113" d="M34.539 33.488s1.18.215 1.18 1.256c0 1.043-1.042 1.256-2.084 1.256h-17.72C13.831 36 10 32 8 32l-.592 4H1v-2.936c0-2.084-.881-2.285-.881-3.211 0-2.084.672-4.395.672-4.395L17 33l17.539.488z"/><path fill="#C1694F" d="M25.449 27.111C23.146 26.054 19.194 21.618 19 21.4 14.149 13.139 19 1 19 1s-1.916-1-7-1c-.674 0-1.343.018-2 .049L9 1 8 .182C4.616.47 2 1 2 1s1 15 1 17-.723 3.401-.723 3.401S.84 23.829.583 24.959c-.287 1.26.22 2.113 1.084 2.322C4.704 28.013 8 29 15.637 32.66c0 0 2 1.34 4.363 1.34h14.539s1.18-.257 1.18-2.424c0-1.084-7.907-3.381-10.27-4.465z"/><path fill="#D99E82" d="M10 20V.049c-.687.032-1.356.078-2 .133V20h2z"/></svg>
|
After Width: | Height: | Size: 718 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#BB1A34" d="M1.728 21c-.617 0-.953-.256-1.127-.471-.171-.211-.348-.585-.225-1.165L3.104 6.658l-1.714.097h-.013c-.517 0-.892-.168-1.127-.459-.22-.272-.299-.621-.221-.98.15-.702.883-1.286 1.667-1.329l4.008-.227c.078-.005.15-.008.217-.008.147 0 .536 0 .783.306.252.312.167.709.139.839L3.719 19.454c-.187.884-.919 1.489-1.866 1.542L1.728 21zm10.743-2c-1.439 0-2.635-.539-3.459-1.559-1.163-1.439-1.467-3.651-.878-6.397 1.032-4.812 4.208-8.186 7.902-8.395 1.59-.089 2.906.452 3.793 1.549 1.163 1.439 1.467 3.651.878 6.397-1.032 4.81-4.208 8.184-7.904 8.394-.112.008-.223.011-.332.011zm3.414-13.746l-.137.004c-1.94.111-3.555 2.304-4.32 5.866-.478 2.228-.381 3.899.272 4.707.297.368.717.555 1.249.555l.14-.004c1.94-.109 3.554-2.301 4.318-5.864.478-2.228.382-3.9-.27-4.708-.296-.369-.718-.556-1.252-.556zm11.591 12.107c-1.439 0-2.637-.539-3.462-1.56-1.163-1.439-1.467-3.651-.878-6.397 1.033-4.813 4.209-8.186 7.903-8.394 1.603-.09 2.903.453 3.79 1.549 1.163 1.439 1.467 3.651.878 6.396-1.031 4.809-4.206 8.183-7.902 8.396-.112.008-.221.01-.329.01zm3.411-13.747l-.136.004c-1.941.111-3.556 2.304-4.32 5.865-.478 2.229-.381 3.901.272 4.708.297.368.719.555 1.251.555l.14-.004c1.939-.109 3.554-2.302 4.318-5.864.479-2.227.383-3.899-.27-4.707-.298-.37-.72-.557-1.255-.557zM11 35.001c-.81 0-1.572-.496-1.873-1.299-.388-1.034.136-2.187 1.17-2.575.337-.126 8.399-3.108 20.536-4.12 1.101-.096 2.067.727 2.159 1.827.092 1.101-.727 2.067-1.827 2.159-11.59.966-19.386 3.851-19.464 3.88-.23.086-.468.128-.701.128zM2.001 29c-.804 0-1.563-.488-1.868-1.283-.396-1.031.118-2.188 1.149-2.583.542-.209 13.516-5.126 32.612-6.131 1.113-.069 2.045.789 2.103 1.892.059 1.103-.789 2.045-1.892 2.103-18.423.97-31.261 5.821-31.389 5.87-.235.089-.477.132-.715.132z"/></svg>
|
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 386 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#EF9645" d="M26.992 19.016c-.255-.255-.799-.611-1.44-.962l-1.911-2-2.113 2h-.58l-2.509-3.634c-1.379.01-2.497 1.136-2.487 2.515l-3.556-2.112c-.817.364-1.389 1.18-1.389 2.133v.96l-4 4.168.016 2.185 9.984 10.729S27.525 19.71 27.55 19.74c-.129-.223-.513-.702-.558-.724z"/><g fill="#FFDC5D"><path d="M25.552 5.81c0-1.107-.906-2.013-2.013-2.013-1.107 0-2.013.906-2.013 2.013v12.245h4.025V5.81zm-4.605 12.244V16.01c-.008-1.103-.909-1.991-2.012-1.983-1.103.008-1.991.909-1.983 2.012l.012 2.016h3.983zM8.916 16h.168c1.059 0 1.916.858 1.916 1.917v4.166C11 23.142 10.143 24 9.084 24h-.168C7.857 24 7 23.142 7 22.083v-4.166C7 16.858 7.857 16 8.916 16zm6.918 2.96l-.056.062C15.304 19.551 15 20.233 15 21c0 .063.013.123.018.185.044.678.308 1.292.728 1.774-.071.129-.163.243-.259.353-.366.417-.89.688-1.487.688-1.104 0-2-.896-2-2v-6c0-.441.147-.845.389-1.176.364-.497.947-.824 1.611-.824 1.104 0 2 .896 2 2v2.778c-.061.055-.109.123-.166.182z"/><path d="M9.062 25c1.024 0 1.925-.526 2.45-1.322.123.183.271.346.431.497 1.185 1.115 3.034 1.044 4.167-.086.152-.152.303-.305.419-.488l-.003-.003C16.727 23.713 17 24 18 24h2.537c-.37.279-.708.623-1.024 1-1.228 1.467-2.013 3.606-2.013 6 0 .276.224.5.5.5s.5-.224.5-.5c0-2.548.956-4.775 2.377-6 .732-.631 1.584-1 2.498-1 .713.079.847-1 .125-1H18c-1.104 0-2-.896-2-2s.896-2 2-2h8c.858 0 1.66.596 1.913 1.415L29 24c.103.335.479 1.871.411 2.191C29.411 31 24.715 36 19 36c-6.537 0-11.844-5.231-11.986-11.734l.014.01c.515.445 1.176.724 1.91.724h.124z"/></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DD2E44" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/></svg>
|
After Width: | Height: | Size: 368 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 357 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 168 KiB |
After Width: | Height: | Size: 148 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFCC4D" d="M36 18c0 9.941-8.059 18-18 18-9.94 0-18-8.059-18-18C0 8.06 8.06 0 18 0c9.941 0 18 8.06 18 18"/><path fill="#664500" d="M28.457 17.797c-.06-.135-1.499-3.297-4.457-3.297-2.957 0-4.397 3.162-4.457 3.297-.092.207-.032.449.145.591.175.142.426.147.61.014.012-.009 1.262-.902 3.702-.902 2.426 0 3.674.881 3.702.901.088.066.194.099.298.099.11 0 .221-.037.312-.109.177-.142.238-.386.145-.594zm-12 0c-.06-.135-1.499-3.297-4.457-3.297-2.957 0-4.397 3.162-4.457 3.297-.092.207-.032.449.144.591.176.142.427.147.61.014.013-.009 1.262-.902 3.703-.902 2.426 0 3.674.881 3.702.901.088.066.194.099.298.099.11 0 .221-.037.312-.109.178-.142.237-.386.145-.594zM18 22c-3.623 0-6.027-.422-9-1-.679-.131-2 0-2 2 0 4 4.595 9 11 9 6.404 0 11-5 11-9 0-2-1.321-2.132-2-2-2.973.578-5.377 1-9 1z"/><path fill="#FFF" d="M9 23s3 1 9 1 9-1 9-1-2 4-9 4-9-4-9-4z"/></svg>
|
After Width: | Height: | Size: 920 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFCC4D" d="M36 18c0 9.941-8.059 18-18 18-9.94 0-18-8.059-18-18C0 8.06 8.06 0 18 0c9.941 0 18 8.06 18 18"/><ellipse fill="#66471B" cx="11.5" cy="14.5" rx="2.5" ry="3.5"/><ellipse fill="#66471B" cx="24.5" cy="14.5" rx="2.5" ry="3.5"/><path fill="#66471B" d="M7 21.262c0 3.964 4.596 9 11 9s11-5 11-9c0 0-10.333 2.756-22 0z"/><path fill="#E8596E" d="M18.545 23.604l-1.091-.005c-3.216-.074-5.454-.596-5.454-.596v6.961c0 3 2 6 6 6s6-3 6-6v-6.92c-1.922.394-3.787.542-5.455.56z"/><path fill="#DD2F45" d="M18 31.843c.301 0 .545-.244.545-.545v-7.694l-1.091-.005v7.699c.001.301.245.545.546.545z"/></svg>
|
After Width: | Height: | Size: 665 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><circle fill="#FFCC4D" cx="18" cy="18" r="18"/><path fill="#664500" d="M10.515 23.621C10.56 23.8 11.683 28 18 28c6.318 0 7.44-4.2 7.485-4.379.055-.217-.043-.442-.237-.554-.195-.111-.439-.078-.6.077C24.629 23.163 22.694 25 18 25s-6.63-1.837-6.648-1.855C11.256 23.05 11.128 23 11 23c-.084 0-.169.021-.246.064-.196.112-.294.339-.239.557z"/><ellipse fill="#664500" cx="12" cy="13.5" rx="2.5" ry="3.5"/><ellipse fill="#664500" cx="24" cy="13.5" rx="2.5" ry="3.5"/></svg>
|
After Width: | Height: | Size: 525 B |
|
@ -5,6 +5,8 @@
|
|||
<script src="{{ static "js/tabs.js" }}"></script>
|
||||
<script src="{{ static "js/image_selector.js" }}"></script>
|
||||
<script src="{{ static "js/templates.js" }}"></script>
|
||||
<script src="{{ static "js/base64.js" }}"></script>
|
||||
<script src="{{ static "js/markdown_upload.js" }}"></script>
|
||||
|
||||
<style>
|
||||
#desc-preview:empty::after {
|
||||
|
@ -12,6 +14,10 @@
|
|||
color: var(--dimmer-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#description.drop {
|
||||
box-shadow: inset 0px 0px 5px yellow;
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
|
||||
|
@ -156,6 +162,17 @@
|
|||
<textarea id="description" class="w-100 h5 minh-5 mono lh-copy" name="description">
|
||||
{{- .ProjectSettings.Description -}}
|
||||
</textarea>
|
||||
<div class="flex justify-end items-center mt2">
|
||||
<div class="upload_bar flex-grow-1">
|
||||
<div class="instructions">
|
||||
Upload files by dragging & dropping, pasting, or <label class="pointer link" for="file_input">selecting</label> them.
|
||||
</div>
|
||||
<div class="progress flex">
|
||||
<div class="progress_text mr3"></div>
|
||||
<div class="progress_bar flex-grow-1 flex-shrink-1 pa1"><div class=""></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="b mt3 mb2">Preview:</div>
|
||||
<div id="desc-preview" class="w-100"></div>
|
||||
</div>
|
||||
|
@ -196,6 +213,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<input type="file" multiple name="file_input" id="file_input" class="dn" />{{/* NOTE(mark): copied NOTE(asaf): Placing this outside the form to avoid submitting it to the server by accident */}}
|
||||
</div>
|
||||
<script>
|
||||
let csrf = JSON.parse({{ csrftokenjs .Session }});
|
||||
|
@ -366,6 +384,19 @@
|
|||
logoMaxFileSize,
|
||||
document.querySelector(".dark_logo")
|
||||
);
|
||||
|
||||
|
||||
//////////////////
|
||||
// Asset upload //
|
||||
//////////////////
|
||||
setupMarkdownUpload(
|
||||
document.querySelectorAll("#project_form input[type=submit]"),
|
||||
document.querySelector('#file_input'),
|
||||
document.querySelector('.upload_bar'),
|
||||
description,
|
||||
doMarkdown,
|
||||
{{ .MaxFileSize }},
|
||||
{{ .UploadUrl }}
|
||||
);
|
||||
|
||||
</script>
|
||||
{{ end }}
|
||||
|
|
|
@ -105,6 +105,13 @@ var fishbowls = [...]fishbowlInfo{
|
|||
Month: time.May, Year: 2022,
|
||||
ContentsPath: "oop/OOP.html",
|
||||
},
|
||||
{
|
||||
Slug: "libraries",
|
||||
Title: "When do libraries go sour?",
|
||||
Description: "The Handmade community is often opposed to using libraries. But let's get more specific about why that can be, and whether that's reasonable. What do we look for in a library? When do libraries go sour? How do we evaluate libraries before using them? How can the libraries we make avoid these problems?",
|
||||
Month: time.July, Year: 2022,
|
||||
ContentsPath: "code-reuse/code-reuse.html",
|
||||
},
|
||||
}
|
||||
|
||||
func FishbowlIndex(c *RequestContext) ResponseData {
|
||||
|
|
|
@ -402,6 +402,9 @@ type ProjectEditData struct {
|
|||
|
||||
APICheckUsernameUrl string
|
||||
LogoMaxFileSize int
|
||||
|
||||
MaxFileSize int
|
||||
UploadUrl string
|
||||
}
|
||||
|
||||
func ProjectNew(c *RequestContext) ResponseData {
|
||||
|
@ -428,6 +431,9 @@ func ProjectNew(c *RequestContext) ResponseData {
|
|||
|
||||
APICheckUsernameUrl: hmnurl.BuildAPICheckUsername(),
|
||||
LogoMaxFileSize: ProjectLogoMaxFileSize,
|
||||
|
||||
MaxFileSize: AssetMaxSize(c.CurrentUser),
|
||||
UploadUrl: c.UrlContext.BuildAssetUpload(),
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
@ -552,6 +558,9 @@ func ProjectEdit(c *RequestContext) ResponseData {
|
|||
|
||||
APICheckUsernameUrl: hmnurl.BuildAPICheckUsername(),
|
||||
LogoMaxFileSize: ProjectLogoMaxFileSize,
|
||||
|
||||
MaxFileSize: AssetMaxSize(c.CurrentUser),
|
||||
UploadUrl: c.UrlContext.BuildAssetUpload(),
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
|
|
@ -186,14 +186,15 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
fmt.Sprintf("blog%s", c.PathParams["remainder"]), nil,
|
||||
), http.StatusMovedPermanently)
|
||||
})
|
||||
|
||||
rb.POST(hmnurl.RegexAssetUpload, AssetUpload)
|
||||
}
|
||||
officialProjectRoutes := anyProject.WithMiddleware(officialProjectMiddleware)
|
||||
personalProjectRoutes := hmnOnly.Group(hmnurl.RegexPersonalProject, personalProjectMiddleware)
|
||||
attachProjectRoutes(&officialProjectRoutes)
|
||||
attachProjectRoutes(&personalProjectRoutes)
|
||||
|
||||
anyProject.POST(hmnurl.RegexAssetUpload, AssetUpload)
|
||||
|
||||
// TODO(ben): Uh, should these all be pulled into the project route group above...?
|
||||
anyProject.GET(hmnurl.RegexEpisodeList, EpisodeList)
|
||||
anyProject.GET(hmnurl.RegexEpisode, Episode)
|
||||
anyProject.GET(hmnurl.RegexCineraIndex, CineraIndex)
|
||||
|
|