diff --git a/data/static/fonts/Cadman_Bold.woff2 b/data/static/fonts/Cadman_Bold.woff2 new file mode 100644 index 0000000..6795a60 Binary files /dev/null and b/data/static/fonts/Cadman_Bold.woff2 differ diff --git a/data/static/fonts/Cadman_BoldItalic.woff2 b/data/static/fonts/Cadman_BoldItalic.woff2 new file mode 100644 index 0000000..fd58c33 Binary files /dev/null and b/data/static/fonts/Cadman_BoldItalic.woff2 differ diff --git a/data/static/fonts/Cadman_Italic.woff2 b/data/static/fonts/Cadman_Italic.woff2 new file mode 100644 index 0000000..28fae73 Binary files /dev/null and b/data/static/fonts/Cadman_Italic.woff2 differ diff --git a/data/static/fonts/Cadman_Roman.woff2 b/data/static/fonts/Cadman_Roman.woff2 new file mode 100644 index 0000000..6eda943 Binary files /dev/null and b/data/static/fonts/Cadman_Roman.woff2 differ diff --git a/data/static/fonts/ChicagoFLF.woff2 b/data/static/fonts/ChicagoFLF.woff2 new file mode 100644 index 0000000..13a7fcf Binary files /dev/null and b/data/static/fonts/ChicagoFLF.woff2 differ diff --git a/data/static/js/babycode-editor.js b/data/static/js/babycode-editor.js new file mode 100644 index 0000000..5247e83 --- /dev/null +++ b/data/static/js/babycode-editor.js @@ -0,0 +1,159 @@ +{ + let ta = document.getElementById("babycode-content"); + + ta.addEventListener("keydown", (e) => { + if(e.key === "Enter" && e.ctrlKey) { + // console.log(e.target.form) + e.target.form?.submit(); + } + }) + + const inThread = () => { + const scheme = window.location.pathname.split("/"); + return scheme[1] === "threads" && scheme[2] !== "create"; + } + + ta.addEventListener("input", () => { + if (!inThread()) return; + + localStorage.setItem(window.location.pathname, ta.value); + }) + + document.addEventListener("DOMContentLoaded", () => { + if (!inThread()) return; + const prevContent = localStorage.getItem(window.location.pathname); + if (!prevContent) return; + ta.value = prevContent; + }) + + const buttonBold = document.getElementById("post-editor-bold"); + const buttonItalics = document.getElementById("post-editor-italics"); + const buttonStrike = document.getElementById("post-editor-strike"); + const buttonUrl = document.getElementById("post-editor-url"); + const buttonCode = document.getElementById("post-editor-code"); + const buttonImg = document.getElementById("post-editor-img"); + const buttonOl = document.getElementById("post-editor-ol"); + const buttonUl = document.getElementById("post-editor-ul"); + + function insertTag(tagStart, newline = false, prefill = "") { + const hasAttr = tagStart[tagStart.length - 1] === "="; + let tagEnd = tagStart; + let tagInsertStart = `[${tagStart}]${newline ? "\n" : ""}`; + if (hasAttr) { + tagEnd = tagEnd.slice(0, -1); + } + const tagInsertEnd = `${newline ? "\n" : ""}[/${tagEnd}]`; + const hasSelection = ta.selectionStart !== ta.selectionEnd; + const text = ta.value; + if (hasSelection) { + const realStart = Math.min(ta.selectionStart, ta.selectionEnd); + const realEnd = Math.max(ta.selectionStart, ta.selectionEnd); + const selectionLength = realEnd - realStart; + + const strStart = text.slice(0, realStart); + const strEnd = text.substring(realEnd); + const frag = `${tagInsertStart}${text.slice(realStart, realEnd)}${tagInsertEnd}`; + const reconst = `${strStart}${frag}${strEnd}`; + ta.value = reconst; + if (!hasAttr){ + ta.setSelectionRange(realStart + tagInsertStart.length, realStart + tagInsertStart.length + selectionLength); + } else { + ta.setSelectionRange(realStart + tagInsertEnd.length - 1, realStart + tagInsertEnd.length - 1); // cursor on attr + } + ta.focus() + } else { + if (hasAttr) { + tagInsertStart += prefill; + } + const cursor = ta.selectionStart; + const strStart = text.slice(0, cursor); + const strEnd = text.substr(cursor); + + let newCursor = strStart.length + tagInsertStart.length; + if (hasAttr) { + newCursor = cursor + tagInsertStart.length - prefill.length - 1; + } + const reconst = `${strStart}${tagInsertStart}${tagInsertEnd}${strEnd}`; + ta.value = reconst; + ta.setSelectionRange(newCursor, newCursor); + ta.focus() + } + } + + buttonBold.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("b") + }) + buttonItalics.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("i") + }) + buttonStrike.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("s") + }) + buttonUrl.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("url=", false, "link label"); + }) + buttonCode.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("code", true) + }) + buttonImg.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("img=", false, "alt text"); + }) + buttonOl.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("ol", true); + }) + buttonUl.addEventListener("click", (e) => { + e.preventDefault(); + insertTag("ul", true); + }) + + const previewEndpoint = "/api/babycode-preview"; + let previousMarkup = ""; + const previewTab = document.getElementById("tab-preview"); + previewTab.addEventListener("tab-activated", async () => { + const previewContainer = document.getElementById("babycode-preview-container"); + const previewErrorsContainer = document.getElementById("babycode-preview-errors-container"); + // previewErrorsContainer.textContent = ""; + const markup = ta.value.trim(); + if (markup === "" || markup === previousMarkup) { + return; + } + previousMarkup = markup; + const req = await fetch(previewEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({markup: markup}) + }) + if (!req.ok) { + switch (req.status) { + case 429: + previewErrorsContainer.textContent = "(Old preview, try again in a few seconds.)" + previousMarkup = ""; + break; + case 400: + previewErrorsContainer.textContent = "(Request got malformed.)" + break; + case 401: + previewErrorsContainer.textContent = "(You are not logged in.)" + break; + default: + previewErrorsContainer.textContent = "(Error. Check console.)" + console.error(req.error); + break; + } + return; + } + + const json_resp = await req.json(); + previewContainer.innerHTML = json_resp.html; + previewErrorsContainer.textContent = ""; + }); +} diff --git a/data/static/js/copy-code.js b/data/static/js/copy-code.js new file mode 100644 index 0000000..ac9c7c7 --- /dev/null +++ b/data/static/js/copy-code.js @@ -0,0 +1,7 @@ +for (let button of document.querySelectorAll(".copy-code")) { + button.addEventListener("click", async () => { + await navigator.clipboard.writeText(button.value) + button.textContent = "Copied!" + setTimeout(() => {button.textContent = "Copy"}, 1000.0) + }) +} diff --git a/data/static/js/date-fmt.js b/data/static/js/date-fmt.js new file mode 100644 index 0000000..521f206 --- /dev/null +++ b/data/static/js/date-fmt.js @@ -0,0 +1,10 @@ +document.addEventListener("DOMContentLoaded", () => { + const timestampSpans = document.getElementsByClassName("timestamp"); + for (let timestampSpan of timestampSpans) { + const timestamp = parseInt(timestampSpan.dataset.utc); + if (!isNaN(timestamp)) { + const date = new Date(timestamp * 1000); + timestampSpan.textContent = date.toLocaleString(); + } + } +}) diff --git a/data/static/js/sort-topics.js b/data/static/js/sort-topics.js new file mode 100644 index 0000000..e288266 --- /dev/null +++ b/data/static/js/sort-topics.js @@ -0,0 +1,45 @@ +// https://codepen.io/crouchingtigerhiddenadam/pen/qKXgap +let selected = null; +let container = document.getElementById("topics-container") + +function isBefore(el1, el2) { + let cur + if (el2.parentNode === el1.parentNode) { + for (cur = el1.previousSibling; cur; cur = cur.previousSibling) { + if (cur === el2) return true + } + } + return false; +} + +function dragOver(e) { + let target = e.target.closest(".draggable-topic") + + if (!target || target === selected) { + return; + } + + if (isBefore(selected, target)) { + container.insertBefore(selected, target) + } else { + container.insertBefore(selected, target.nextSibling) + } +} + +function dragEnd() { + if (!selected) return; + + selected.classList.remove("dragged") + selected = null; + for (let i = 0; i < container.childElementCount - 1; i++) { + let input = container.children[i].querySelector(".topic-input"); + input.value = i + 1; + } +} + +function dragStart(e) { + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData('text/plain', null) + selected = e.target + selected.classList.add("dragged") +} diff --git a/data/static/js/thread.js b/data/static/js/thread.js new file mode 100644 index 0000000..0679b74 --- /dev/null +++ b/data/static/js/thread.js @@ -0,0 +1,80 @@ +{ + const ta = document.getElementById("babycode-content"); + + for (let button of document.querySelectorAll(".reply-button")) { + button.addEventListener("click", (e) => { + ta.value += button.value; + ta.scrollIntoView() + ta.focus(); + }) + } + + const deleteDialog = document.getElementById("delete-dialog"); + const deleteDialogCloseButton = document.getElementById("post-delete-dialog-close"); + let deletionTargetPostContainer; + + function closeDeleteDialog() { + deletionTargetPostContainer.style.removeProperty("background-color"); + deleteDialog.close(); + } + + deleteDialogCloseButton.addEventListener("click", (e) => { + closeDeleteDialog(); + }) + deleteDialog.addEventListener("click", (e) => { + if (e.target === deleteDialog) { + closeDeleteDialog(); + } + }) + for (let button of document.querySelectorAll(".post-delete-button")) { + button.addEventListener("click", (e) => { + deleteDialog.showModal(); + const postId = button.value; + deletionTargetPostContainer = document.getElementById("post-" + postId).querySelector(".post-content-container"); + deletionTargetPostContainer.style.setProperty("background-color", "#fff"); + const form = document.getElementById("post-delete-form"); + form.action = `/post/${postId}/delete` + }) + } + + const threadEndpoint = document.getElementById("thread-subscribe-endpoint").value; + let now = Math.floor(new Date() / 1000); + function hideNotification() { + const notification = document.getElementById('new-post-notification'); + notification.classList.add('hidden'); + } + + function showNewPostNotification(url) { + const notification = document.getElementById("new-post-notification"); + + notification.classList.remove("hidden"); + + document.getElementById("dismiss-new-post-button").onclick = () => { + now = Math.floor(new Date() / 1000); + hideNotification(); + tryFetchUpdate(); + } + + document.getElementById("go-to-new-post-button").href = url; + + document.getElementById("unsub-new-post-button").onclick = () => { + hideNotification(); + } + } + + function tryFetchUpdate() { + if (!threadEndpoint) return; + const body = JSON.stringify({since: now}); + fetch(threadEndpoint, {method: "POST", headers: {"Content-Type": "application/json"}, body: body}) + .then(res => res.json()) + .then(json => { + if (json.status === "none") { + setTimeout(tryFetchUpdate, 5000); + } else if (json.status === "new_post") { + showNewPostNotification(json.url); + } + }) + .catch(error => console.log(error)) + } + tryFetchUpdate(); +} diff --git a/data/static/js/topic.js b/data/static/js/topic.js new file mode 100644 index 0000000..6489843 --- /dev/null +++ b/data/static/js/topic.js @@ -0,0 +1,16 @@ +{ + const deleteDialog = document.getElementById("delete-dialog"); + const deleteDialogOpenButton = document.getElementById("topic-delete-dialog-open"); + deleteDialogOpenButton.addEventListener("click", (e) => { + deleteDialog.showModal(); + }); + const deleteDialogCloseButton = document.getElementById("topic-delete-dialog-close"); + deleteDialogCloseButton.addEventListener("click", (e) => { + deleteDialog.close(); + }) + deleteDialog.addEventListener("click", (e) => { + if (e.target === deleteDialog) { + deleteDialog.close(); + } + }) +} diff --git a/data/static/js/ui.js b/data/static/js/ui.js new file mode 100644 index 0000000..9c640bc --- /dev/null +++ b/data/static/js/ui.js @@ -0,0 +1,147 @@ +function activateSelfDeactivateSibs(button) { + if (button.classList.contains("active")) return; + + Array.from(button.parentNode.children).forEach(s => { + if (s === button){ + button.classList.add('active'); + } else { + s.classList.remove('active'); + } + const targetId = s.dataset.targetId; + const target = document.getElementById(targetId); + + if (!target) return; + + if (s.classList.contains('active')) { + target.classList.add('active'); + target.dispatchEvent(new CustomEvent("tab-activated", {bubbles: false})) + } else { + target.classList.remove('active'); + } + }); +} + +function openLightbox(post, idx) { + lightboxCurrentPost = post; + lightboxCurrentIdx = idx; + lightboxObj.img.src = lightboxImages.get(post)[idx].src; + lightboxObj.openOriginalAnchor.href = lightboxImages.get(post)[idx].src + lightboxObj.prevButton.disabled = lightboxImages.get(post).length === 1 + lightboxObj.nextButton.disabled = lightboxImages.get(post).length === 1 + lightboxObj.imageCount.textContent = `Image ${idx + 1} of ${lightboxImages.get(post).length}` + + if (!lightboxObj.dialog.open) { + lightboxObj.dialog.showModal(); + } +} + +const modulo = (n, m) => ((n % m) + m) % m + +function lightboxNext() { + const l = lightboxImages.get(lightboxCurrentPost).length; + const target = modulo(lightboxCurrentIdx + 1, l); + openLightbox(lightboxCurrentPost, target); +} + +function lightboxPrev() { + const l = lightboxImages.get(lightboxCurrentPost).length; + const target = modulo(lightboxCurrentIdx - 1, l); + openLightbox(lightboxCurrentPost, target); +} + +function constructLightbox() { + const dialog = document.createElement("dialog"); + dialog.classList.add("lightbox-dialog"); + dialog.addEventListener("click", (e) => { + if (e.target === dialog) { + dialog.close(); + } + }) + const dialogInner = document.createElement("div"); + dialogInner.classList.add("lightbox-inner"); + dialog.appendChild(dialogInner); + const img = document.createElement("img"); + img.classList.add("lightbox-image") + dialogInner.appendChild(img); + const openOriginalAnchor = document.createElement("a") + openOriginalAnchor.text = "Open original in new window" + openOriginalAnchor.target = "_blank" + openOriginalAnchor.rel = "noopener noreferrer nofollow" + dialogInner.appendChild(openOriginalAnchor); + + const navSpan = document.createElement("span"); + navSpan.classList.add("lightbox-nav"); + const prevButton = document.createElement("button"); + prevButton.type = "button"; + prevButton.textContent = "Previous"; + prevButton.addEventListener("click", lightboxPrev); + const nextButton = document.createElement("button"); + nextButton.type = "button"; + nextButton.textContent = "Next"; + nextButton.addEventListener("click", lightboxNext); + const imageCount = document.createElement("span"); + imageCount.textContent = "Image of "; + navSpan.appendChild(prevButton); + navSpan.appendChild(imageCount); + navSpan.appendChild(nextButton); + + dialogInner.appendChild(navSpan); + return { + img: img, + dialog: dialog, + openOriginalAnchor: openOriginalAnchor, + prevButton: prevButton, + nextButton: nextButton, + imageCount: imageCount, + } +} + +let lightboxImages = new Map(); //.post-inner : Array +let lightboxObj = null; +let lightboxCurrentPost = null; +let lightboxCurrentIdx = -1; + +document.addEventListener("DOMContentLoaded", () => { + // tabs + document.querySelectorAll(".tab-button").forEach(button => { + button.addEventListener("click", () => { + activateSelfDeactivateSibs(button); + }); + }); + + // accordions + const accordions = document.querySelectorAll(".accordion"); + accordions.forEach(accordion => { + const header = accordion.querySelector(".accordion-header"); + const toggleButton = header.querySelector(".accordion-toggle"); + const content = accordion.querySelector(".accordion-content"); + + const toggle = (e) => { + e.stopPropagation(); + accordion.classList.toggle("hidden"); + content.classList.toggle("hidden"); + toggleButton.textContent = content.classList.contains("hidden") ? "►" : "▼" + } + + toggleButton.addEventListener("click", toggle); + }); + + //lightboxes + lightboxObj = constructLightbox(); + document.body.appendChild(lightboxObj.dialog); + const postImages = document.querySelectorAll(".post-inner img.block-img"); + postImages.forEach(postImage => { + const belongingTo = postImage.closest(".post-inner"); + const images = lightboxImages.get(belongingTo) ?? []; + images.push({ + src: postImage.src, + alt: postImage.alt, + }); + const idx = images.length - 1; + lightboxImages.set(belongingTo, images); + postImage.style.cursor = "pointer"; + postImage.addEventListener("click", () => { + openLightbox(belongingTo, idx); + }); + }); +}); diff --git a/data/static/style.css b/data/static/style.css new file mode 100644 index 0000000..3889747 --- /dev/null +++ b/data/static/style.css @@ -0,0 +1,735 @@ +@font-face { + font-family: "site-title"; + src: url("/static/fonts/ChicagoFLF.woff2"); +} +@font-face { + font-family: "Cadman"; + src: url("/static/fonts/Cadman_Roman.woff2"); + font-weight: normal; + font-style: normal; +} +@font-face { + font-family: "Cadman"; + src: url("/static/fonts/Cadman_Bold.woff2"); + font-weight: bold; + font-style: normal; +} +@font-face { + font-family: "Cadman"; + src: url("/static/fonts/Cadman_Italic.woff2"); + font-weight: normal; + font-style: italic; +} +@font-face { + font-family: "Cadman"; + src: url("/static/fonts/Cadman_BoldItalic.woff2"); + font-weight: bold; + font-style: italic; +} +.tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton { + cursor: default; + color: black; + font-size: 0.9em; + font-family: "Cadman"; + text-decoration: none; + border: 1px solid black; + border-radius: 3px; + padding: 5px 20px; + margin: 10px 0; +} + +body { + font-family: "Cadman"; + margin: 20px 100px; + background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126); +} + +.big { + font-size: 1.8rem; +} + +#topnav { + padding: 10px; + display: flex; + justify-content: end; + background-color: #c1ceb1; + justify-content: space-between; + align-items: baseline; +} + +#bottomnav { + padding: 10px; + display: flex; + justify-content: end; + background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016); +} + +.darkbg { + padding-bottom: 10px; + padding-left: 10px; + padding-right: 10px; + background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016); +} + +.user-actions { + display: flex; + column-gap: 15px; +} + +.site-title { + font-family: "site-title"; + font-size: 3rem; + margin: 0 20px; + text-decoration: none; + color: black; +} + +.thread-title { + margin: 0; + font-size: 1.5rem; + font-weight: bold; +} + +.post { + display: grid; + grid-template-columns: 200px 1fr; + grid-template-rows: 1fr; + gap: 0; + grid-auto-flow: row; + grid-template-areas: "usercard post-content-container"; + border: 2px outset rgb(135.1928346457, 145.0974015748, 123.0025984252); +} + +.usercard { + grid-area: usercard; + padding: 20px 10px; + border: 4px outset rgb(217.26, 220.38, 213.42); + background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016); + border-right: solid 2px; +} + +.usercard-inner { + display: flex; + flex-direction: column; + align-items: center; + top: 10px; + position: sticky; +} + +.post-content-container { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 70px 2.5fr; + gap: 0px 0px; + grid-auto-flow: row; + grid-template-areas: "post-info" "post-content"; + grid-area: post-content-container; +} + +.post-info { + grid-area: post-info; + display: flex; + justify-content: space-between; + padding: 5px 20px; + align-items: center; + border-top: 1px solid black; + border-bottom: 1px solid black; +} + +.post-content { + grid-area: post-content; + padding: 20px; + margin-right: 25%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.post-content.wider { + margin-right: 12.5%; +} + +.post-inner { + height: 100%; +} + +pre code { + display: block; + background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126); + font-size: 1rem; + color: white; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + border-left: 10px solid rgb(229.84, 231.92, 227.28); + padding: 20px; + overflow: scroll; + tab-size: 4; +} + +.inline-code { + background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126); + color: white; + padding: 5px 10px; + display: inline-block; + margin: 4px; + border-radius: 4px; + font-size: 1rem; +} + +#delete-dialog, .lightbox-dialog { + padding: 0; + border-radius: 4px; + border: 2px solid black; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.25); +} + +.delete-dialog-inner { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +.lightbox-inner { + display: flex; + flex-direction: column; + padding: 20px; + min-width: 400px; + background-color: #c1ceb1; + gap: 10px; +} + +.lightbox-image { + max-width: 70vw; + max-height: 70vh; + object-fit: scale-down; +} + +.lightbox-nav { + display: flex; + justify-content: space-between; + align-items: center; +} + +.copy-code-container { + position: sticky; + width: calc(100% - 4px); + display: flex; + justify-content: space-between; + align-items: last baseline; + font-family: "Cadman"; + border-top-right-radius: 8px; + border-top-left-radius: 8px; + background-color: #c1ceb1; + border-left: 2px solid black; + border-right: 2px solid black; + border-top: 2px solid black; +} +.copy-code-container::before { + content: "code block"; + font-style: italic; + margin-left: 10px; +} + +.copy-code { + margin-right: 10px; +} + +blockquote { + padding: 10px 20px; + margin: 10px; + border-radius: 4px; + border-left: 10px solid rgb(229.84, 231.92, 227.28); + background-color: rgb(135.1928346457, 145.0974015748, 123.0025984252); +} + +.user-info { + display: grid; + grid-template-columns: 300px 1fr; + grid-template-rows: 1fr; + gap: 0; + grid-template-areas: "user-page-usercard user-page-stats"; +} + +.user-page-usercard { + grid-area: user-page-usercard; + padding: 20px 10px; + border: 4px outset rgb(217.26, 220.38, 213.42); + background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016); + border-right: solid 2px; +} + +.user-page-stats { + grid-area: user-page-stats; + padding: 20px 30px; + border: 1px solid black; +} + +.user-stats-list { + list-style: none; + margin: 0 0 10px 0; +} + +.user-page-posts { + border-left: solid 1px black; + border-right: solid 1px black; + border-bottom: solid 1px black; + background-color: #c1ceb1; +} + +.user-page-post-preview { + max-height: 200px; + mask-image: linear-gradient(180deg, #000 60%, transparent); +} + +.avatar { + width: 90%; + height: 90%; + object-fit: contain; + margin-bottom: 10px; +} + +.username-link { + overflow-wrap: anywhere; +} + +.user-status { + text-align: center; +} + +button, input[type=submit], .linkbutton { + display: inline-block; + background-color: rgb(177, 206, 204.5); +} +button:hover, input[type=submit]:hover, .linkbutton:hover { + background-color: rgb(192.6, 215.8, 214.6); +} +button:active, input[type=submit]:active, .linkbutton:active { + background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323); +} +button:disabled, input[type=submit]:disabled, .linkbutton:disabled { + background-color: rgb(209.535, 211.565, 211.46); +} +button.critical, input[type=submit].critical, .linkbutton.critical { + color: white; + background-color: red; +} +button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover { + background-color: #ff3333; +} +button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active { + background-color: rgb(149.175, 80.325, 80.325); +} +button.critical:disabled, input[type=submit].critical:disabled, .linkbutton.critical:disabled { + background-color: rgb(174.675, 156.825, 156.825); +} +button.warn, input[type=submit].warn, .linkbutton.warn { + background-color: #fbfb8d; +} +button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover { + background-color: rgb(251.8, 251.8, 163.8); +} +button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active { + background-color: rgb(198.3813559322, 198.3813559322, 154.4186440678); +} +button.warn:disabled, input[type=submit].warn:disabled, .linkbutton.warn:disabled { + background-color: rgb(217.55, 217.55, 209.85); +} + +input[type=file]::file-selector-button { + background-color: rgb(177, 206, 204.5); + margin: 10px 10px; +} +input[type=file]::file-selector-button:hover { + background-color: rgb(192.6, 215.8, 214.6); +} +input[type=file]::file-selector-button:active { + background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323); +} +input[type=file]::file-selector-button:disabled { + background-color: rgb(209.535, 211.565, 211.46); +} + +p { + margin: 15px 0; +} + +.pagebutton { + background-color: rgb(177, 206, 204.5); + padding: 5px 5px; + margin: 0; + display: inline-block; + min-width: 20px; + text-align: center; +} +.pagebutton:hover { + background-color: rgb(192.6, 215.8, 214.6); +} +.pagebutton:active { + background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323); +} +.pagebutton:disabled { + background-color: rgb(209.535, 211.565, 211.46); +} + +.currentpage { + border: none; + padding: 5px 5px; + margin: 0; + display: inline-block; + min-width: 20px; + text-align: center; +} + +.modform { + display: inline; +} + +.login-container > * { + width: 25%; + margin: auto; +} + +.settings-container > * { + width: 40%; + margin: auto; +} + +.avatar-form { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px 0; +} + +input[type=text], input[type=password], textarea, select { + border: 1px solid black; + border-radius: 3px; + padding: 7px 10px; + width: 100%; + box-sizing: border-box; + resize: vertical; + background-color: rgb(217.8, 225.6, 208.2); +} +input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus { + background-color: rgb(230.2, 235.4, 223.8); +} + +.infobox { + border: 2px solid black; + background-color: #81a3e6; + padding: 20px 15px; +} +.infobox.critical { + background-color: rgb(237, 129, 129); +} +.infobox.warn { + background-color: #fbfb8d; +} + +.infobox > span { + display: flex; + align-items: center; +} + +.infobox-icon-container { + min-width: 60px; + padding-right: 15px; +} + +.thread { + display: grid; + grid-template-columns: 96px 1.6fr 96px; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-auto-flow: row; + min-height: 96px; + grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container"; +} + +.thread-sticky-container { + grid-area: thread-sticky-container; + border: 2px outset rgb(217.26, 220.38, 213.42); +} + +.thread-locked-container { + grid-area: thread-locked-container; + border: 2px outset rgb(217.26, 220.38, 213.42); +} + +.contain-svg { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} +.contain-svg:not(.full) > svg { + height: 50%; + width: 50%; +} + +.block-img { + object-fit: contain; + max-width: 400px; + max-height: 400px; +} + +.thread-info-container { + grid-area: thread-info-container; + background-color: #c1ceb1; + padding: 5px 20px; + border-top: 1px solid black; + border-bottom: 1px solid black; + display: flex; + flex-direction: column; + overflow: hidden; + max-height: 110px; + mask-image: linear-gradient(180deg, #000 60%, transparent); +} + +.thread-info-post-preview { + overflow: hidden; + text-overflow: ellipsis; + display: inline; + margin-right: 25%; +} + +.babycode-guide-section { + background-color: #c1ceb1; + padding: 5px 20px; + border: 1px solid black; + padding-right: 25%; +} + +.babycode-guide-container { + display: grid; + grid-template-columns: 1.5fr 300px; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-auto-flow: row; + grid-template-areas: "guide-topics guide-toc"; +} + +.guide-topics { + grid-area: guide-topics; + overflow: hidden; +} + +.guide-toc { + grid-area: guide-toc; + position: sticky; + top: 100px; + align-self: start; + padding: 10px; + border-bottom-right-radius: 8px; + background-color: rgb(177, 206, 204.5); + border-right: 1px solid black; + border-top: 1px solid black; + border-bottom: 1px solid black; +} + +.emoji-table tr td { + text-align: center; +} + +.emoji-table tr th { + padding-left: 50px; + padding-right: 50px; +} + +.emoji-table { + margin: auto; +} + +.emoji-table, th, td { + border: 1px solid black; + border-collapse: collapse; +} + +.topic { + display: grid; + grid-template-columns: 1.5fr 64px; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-auto-flow: row; + grid-template-areas: "topic-info-container topic-locked-container"; +} + +.topic-info-container { + grid-area: topic-info-container; + background-color: #c1ceb1; + padding: 5px 20px; + border: 1px solid black; + display: flex; + flex-direction: column; +} + +.topic-locked-container { + grid-area: topic-locked-container; + border: 2px outset rgb(217.26, 220.38, 213.42); +} + +.draggable-topic { + cursor: pointer; + user-select: none; + background-color: #c1ceb1; + padding: 20px; + margin: 12px 0; + border-top: 6px outset rgb(217.26, 220.38, 213.42); + border-bottom: 6px outset rgb(135.1928346457, 145.0974015748, 123.0025984252); +} +.draggable-topic.dragged { + background-color: rgb(177, 206, 204.5); +} + +.editing { + background-color: rgb(217.26, 220.38, 213.42); +} + +.context-explain { + margin: 20px 0; + display: flex; + justify-content: space-evenly; +} + +.post-edit-form { + display: flex; + flex-direction: column; + align-items: baseline; + height: 100%; +} + +.babycode-editor { + height: 150px; + font-size: 1rem; +} + +.babycode-editor-container { + width: 100%; +} + +.babycode-preview-errors-container { + font-size: 0.8rem; +} + +.tab-button { + background-color: rgb(177, 206, 204.5); + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + margin-bottom: 0; +} +.tab-button:hover { + background-color: rgb(192.6, 215.8, 214.6); +} +.tab-button:active { + background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323); +} +.tab-button:disabled { + background-color: rgb(209.535, 211.565, 211.46); +} +.tab-button.active { + background-color: #beb1ce; + padding-top: 8px; +} + +.tab-content { + display: none; +} +.tab-content.active { + min-height: 250px; + display: block; + background-color: rgb(191.3137931034, 189.7, 193.3); + border: 1px solid black; + padding: 10px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +ul, ol { + margin: 10px 0 10px 30px; + padding: 0; +} + +.new-concept-notification.hidden { + display: none; +} + +.new-concept-notification { + position: fixed; + bottom: 80px; + right: 80px; + border: 2px solid black; + background-color: #81a3e6; + padding: 20px 15px; + border-radius: 4px; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.25); +} + +.emoji { + max-width: 15px; + max-height: 15px; +} + +.accordion { + border-top-right-radius: 3px; + border-top-left-radius: 3px; + box-sizing: border-box; + border: 1px solid black; + margin: 10px 5px; + overflow: hidden; +} + +.accordion.hidden { + border-bottom: none; +} + +.accordion-header { + display: flex; + align-items: center; + background-color: rgb(159.0271653543, 162.0727712915, 172.9728346457); + padding: 0 10px; + gap: 10px; + border-bottom: 1px solid black; +} + +.accordion-toggle { + padding: 0; + width: 36px; + height: 36px; + min-width: 36px; + min-height: 36px; +} + +.accordion-title { + margin-right: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accordion-content { + padding: 0 15px; +} + +.accordion-content.hidden { + display: none; +} + +.inbox-container { + padding: 10px; +} + +.babycode-button-container { + display: flex; + gap: 10px; +} + +.babycode-button { + padding: 5px 10px; + min-width: 36px; +} +.babycode-button > * { + font-size: 1rem; +} diff --git a/sass/style.scss b/sass/style.scss new file mode 100644 index 0000000..7793da6 --- /dev/null +++ b/sass/style.scss @@ -0,0 +1,744 @@ +@use "sass:color"; + +@font-face { + font-family: "site-title"; + src: url("/static/fonts/ChicagoFLF.woff2"); +} + +@mixin cadman($var) { + font-family: "Cadman"; + src: url("/static/fonts/Cadman_#{$var}.woff2"); +} + +@font-face { + @include cadman("Roman"); + font-weight: normal; + font-style: normal; +} + +@font-face { + @include cadman("Bold"); + font-weight: bold; + font-style: normal; +} + +@font-face { + @include cadman("Italic"); + font-weight: normal; + font-style: italic; +} + +@font-face { + @include cadman("BoldItalic"); + font-weight: bold; + font-style: italic; +} + +$accent_color: #c1ceb1; + +$dark_bg: color.scale($accent_color, $lightness: -25%, $saturation: -97%); +$dark2: color.scale($accent_color, $lightness: -30%, $saturation: -60%); +$verydark: color.scale($accent_color, $lightness: -80%, $saturation: -70%); + +$light: color.scale($accent_color, $lightness: 40%, $saturation: -60%); +$lighter: color.scale($accent_color, $lightness: 60%, $saturation: -60%); + +$main_bg: color.scale($accent_color, $lightness: -10%, $saturation: -40%); +$button_color: color.adjust($accent_color, $hue: 90); +$button_color2: color.adjust($accent_color, $hue: 180); +$accordion_color: color.adjust($accent_color, $hue: 140, $lightness: -10%, $saturation: -15%); + +%button-base { + cursor: default; + color: black; + font-size: 0.9em; + font-family: "Cadman"; + text-decoration: none; + border: 1px solid black; + border-radius: 3px; + padding: 5px 20px; + margin: 10px 0; +} + +@mixin button($color) { + @extend %button-base; + background-color: $color; + + &:hover { + background-color: color.scale($color, $lightness: 20%); + } + + &:active { + background-color: color.scale($color, $lightness: -10%, $saturation: -70%); + } + + &:disabled { + background-color: color.scale($color, $lightness: 30%, $saturation: -90%); + } +} + +@mixin navbar($color) { + padding: 10px; + display: flex; + justify-content: end; + background-color: $color; +} + +body { + font-family: "Cadman"; + // font-size: 18px; + margin: 20px 100px; + background-color: $main_bg; +} + +.big { + font-size: 1.8rem; +} + +#topnav { + @include navbar($accent_color); + justify-content: space-between; + align-items: baseline; +} + +#bottomnav { + @include navbar($dark_bg); +} + +.darkbg { + padding-bottom: 10px; + padding-left: 10px; + padding-right: 10px; + background-color: $dark_bg; +} + +.user-actions { + display: flex; + column-gap: 15px; +} + +.site-title { + font-family: "site-title"; + font-size: 3rem; + margin: 0 20px; + text-decoration: none; + color: black; +} + +.thread-title { + margin: 0; + font-size: 1.5rem; + font-weight: bold; +} + +.post { + display: grid; + grid-template-columns: 200px 1fr; + grid-template-rows: 1fr; + gap: 0; + grid-auto-flow: row; + grid-template-areas: + "usercard post-content-container"; + border: 2px outset $dark2; +} + +.usercard { + grid-area: usercard; + padding: 20px 10px; + border: 4px outset $light; + background-color: $dark_bg; + border-right: solid 2px; +} + +.usercard-inner { + display: flex; + flex-direction: column; + align-items: center; + top: 10px; + position: sticky; +} + +.post-content-container { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 70px 2.5fr; + gap: 0px 0px; + grid-auto-flow: row; + grid-template-areas: + "post-info" + "post-content"; + grid-area: post-content-container; +} + +.post-info { + grid-area: post-info; + display: flex; + justify-content: space-between; + padding: 5px 20px; + align-items: center; + border-top: 1px solid black; + border-bottom: 1px solid black; +} + +.post-content { + grid-area: post-content; + padding: 20px; + margin-right: 25%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.post-content.wider { + margin-right: 12.5%; +} + +.post-inner { + height: 100%; +} + +pre code { + display: block; + background-color: $verydark; + font-size: 1rem; + color: white; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; + border-left: 10px solid $lighter; + padding: 20px; + overflow: scroll; + tab-size: 4; +} + +.inline-code { + background-color: $verydark; + color: white; + padding: 5px 10px; + display: inline-block; + margin: 4px; + border-radius: 4px; + font-size: 1rem; +} + +#delete-dialog, .lightbox-dialog { + padding: 0; + border-radius: 4px; + border: 2px solid black; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.25); +} + +.delete-dialog-inner { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +.lightbox-inner { + display: flex; + flex-direction: column; + padding: 20px; + min-width: 400px; + background-color: $accent_color; + gap: 10px; +} + +.lightbox-image { + max-width: 70vw; + max-height: 70vh; + object-fit: scale-down; +} + +.lightbox-nav { + display: flex; + justify-content: space-between; + align-items: center; +} + +.copy-code-container { + position: sticky; + // width: 100%; + width: calc(100% - 4px); + display: flex; + justify-content: space-between; + align-items: last baseline; + font-family: "Cadman"; + border-top-right-radius: 8px; + border-top-left-radius: 8px; + background-color: $accent_color; + border-left: 2px solid black; + border-right: 2px solid black; + border-top: 2px solid black; + + &::before { + content: "code block"; + font-style: italic; + margin-left: 10px; + } +} + +.copy-code { + margin-right: 10px; +} + +blockquote { + padding: 10px 20px; + margin: 10px; + border-radius: 4px; + border-left: 10px solid $lighter; + background-color: $dark2; +} + +.user-info { + display: grid; + grid-template-columns: 300px 1fr; + grid-template-rows: 1fr; + gap: 0; + grid-template-areas: + "user-page-usercard user-page-stats"; +} + +.user-page-usercard { + grid-area: user-page-usercard; + padding: 20px 10px; + border: 4px outset $light; + background-color: $dark_bg; + border-right: solid 2px; +} + +.user-page-stats { + grid-area: user-page-stats; + padding: 20px 30px; + border: 1px solid black; +} + +.user-stats-list { + list-style: none; + margin: 0 0 10px 0; +} + +.user-page-posts { + border-left: solid 1px black; + border-right: solid 1px black; + border-bottom: solid 1px black; + background-color: $accent_color; +} + +.user-page-post-preview { + max-height: 200px; + mask-image: linear-gradient(180deg,#000 60%,transparent); +} + +.avatar { + width: 90%; + height: 90%; + object-fit: contain; + margin-bottom: 10px; +} + +.username-link { + overflow-wrap: anywhere; +} + +.user-status { + text-align: center; +} + +button, input[type="submit"], .linkbutton { + display: inline-block; + @include button($button_color); + + &.critical { + color: white; + @include button(red); + } + + &.warn { + @include button(#fbfb8d); + } +} + +// not sure why this one has to be separate, but if it's included in the rule above everything breaks +input[type="file"]::file-selector-button { + @include button($button_color); + margin: 10px 10px; +} + +p { + margin: 15px 0; +} + +.pagebutton { + @include button($button_color); + padding: 5px 5px; + margin: 0; + display: inline-block; + min-width: 20px; + text-align: center; +} + +.currentpage { + @extend %button-base; + border: none; + padding: 5px 5px; + margin: 0; + display: inline-block; + min-width: 20px; + text-align: center; +} + +.modform { + display: inline; +} + +.login-container > * { + width: 25%; + margin: auto; +} + +.settings-container > * { + width: 40%; + margin: auto; +} + +.avatar-form { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px 0; +} + +input[type="text"], input[type="password"], textarea, select { + border: 1px solid black; + border-radius: 3px; + padding: 7px 10px; + width: 100%; + box-sizing: border-box; + resize: vertical; + background-color: color.scale($accent_color, $lightness: 40%); + + &:focus { + background-color: color.scale($accent_color, $lightness: 60%); + } +} + +.infobox { + border: 2px solid black; + background-color: #81a3e6; + padding: 20px 15px; + + &.critical { + background-color: rgb(237, 129, 129); + } + + &.warn { + background-color: #fbfb8d; + } +} + +.infobox > span { + display: flex; + align-items: center; +} + +.infobox-icon-container { + min-width: 60px; + padding-right: 15px; +} + +.thread { + display: grid; + grid-template-columns: 96px 1.6fr 96px; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-auto-flow: row; + min-height: 96px; + grid-template-areas: + "thread-sticky-container thread-info-container thread-locked-container"; +} + +.thread-sticky-container { + grid-area: thread-sticky-container; + border: 2px outset $light; +} + +.thread-locked-container { + grid-area: thread-locked-container; + border: 2px outset $light; +} + +.contain-svg { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + &:not(.full) > svg { + height: 50%; + width: 50%; + } +} + +.block-img { + object-fit: contain; + max-width: 400px; + max-height: 400px; +} + +.thread-info-container { + grid-area: thread-info-container; + background-color: $accent_color; + padding: 5px 20px; + border-top: 1px solid black; + border-bottom: 1px solid black; + display: flex; + flex-direction: column; + overflow: hidden; + max-height: 110px; + mask-image: linear-gradient(180deg,#000 60%,transparent); +} + +.thread-info-post-preview { + overflow: hidden; + text-overflow: ellipsis; + display: inline; + margin-right: 25%; +} + +.babycode-guide-section { + background-color: $accent_color; + padding: 5px 20px; + border: 1px solid black; + padding-right: 25%; +} + +.babycode-guide-container { + display: grid; + grid-template-columns: 1.5fr 300px; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-auto-flow: row; + grid-template-areas: + "guide-topics guide-toc"; +} + +.guide-topics { + grid-area: guide-topics; + overflow: hidden; +} + +.guide-toc { + grid-area: guide-toc; + position: sticky; + top: 100px; + align-self: start; + padding: 10px; + // border-top-right-radius: 16px; + border-bottom-right-radius: 8px; + background-color: $button_color; + border-right: 1px solid black; + border-top: 1px solid black; + border-bottom: 1px solid black; +} + +.emoji-table tr td { + text-align: center; +} + +.emoji-table tr th { + padding-left: 50px; + padding-right: 50px; +} + +.emoji-table { + margin: auto; +} + +.emoji-table, th, td { + border: 1px solid black; + border-collapse: collapse; +} + +.topic { + display: grid; + grid-template-columns: 1.5fr 64px; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-auto-flow: row; + grid-template-areas: + "topic-info-container topic-locked-container"; +} + +.topic-info-container { + grid-area: topic-info-container; + background-color: $accent_color; + padding: 5px 20px; + border: 1px solid black; + display: flex; + flex-direction: column; +} + +.topic-locked-container { + grid-area: topic-locked-container; + border: 2px outset $light; +} + + +.draggable-topic { + cursor: pointer; + user-select: none; + background-color: $accent_color; + padding: 20px; + margin: 12px 0; + border-top: 6px outset $light; + border-bottom: 6px outset $dark2; + + &.dragged { + background-color: $button_color; + } +} + +.editing { + background-color: $light; +} + +.context-explain { + margin: 20px 0; + display: flex; + justify-content: space-evenly; +} + +.post-edit-form { + display: flex; + flex-direction: column; + align-items: baseline; + height: 100%; +} + +.babycode-editor { + height: 150px; + font-size: 1rem; +} + +.babycode-editor-container { + width: 100%; +} + +.babycode-preview-errors-container { + font-size: 0.8rem; +} + +.tab-button { + @include button($button_color); + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + margin-bottom: 0; + + &.active { + background-color: $button_color2; + padding-top: 8px; + } +} + +.tab-content { + display: none; + + &.active { + min-height: 250px; + display: block; + background-color: color.adjust($button_color2, $saturation: -20%); + border: 1px solid black; + padding: 10px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + } +} + +ul, ol { + margin: 10px 0 10px 30px; + padding: 0; +} + +.new-concept-notification.hidden { + display: none; +} + +.new-concept-notification { + position: fixed; + bottom: 80px; + right: 80px; + border: 2px solid black; + background-color: #81a3e6; + padding: 20px 15px; + border-radius: 4px; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.25); +} + +.emoji { + max-width: 15px; + max-height: 15px; +} + +.accordion { + border-top-right-radius: 3px; + border-top-left-radius: 3px; + box-sizing: border-box; + border: 1px solid black; + margin: 10px 5px; + overflow: hidden; +} + +.accordion.hidden { + border-bottom: none; +} + +.accordion-header { + display: flex; + align-items: center; + background-color: $accordion_color; + padding: 0 10px; + gap: 10px; + border-bottom: 1px solid black; +} + +.accordion-toggle { + padding: 0; + width: 36px; + height: 36px; + min-width: 36px; + min-height: 36px; +} + +.accordion-title { + margin-right: auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accordion-content { + padding: 0 15px; +} + +.accordion-content.hidden { + display: none; +} + +.inbox-container { + padding: 10px; +} + +.babycode-button-container { + display: flex; + gap: 10px; +} + +.babycode-button { + padding: 5px 10px; + min-width: 36px; + + &> * { + font-size: 1rem; + } +}