add: better layout

This commit is contained in:
trisua 2025-07-25 15:12:15 -04:00
parent 2cc9ed7445
commit dbd70d9592
19 changed files with 451 additions and 87 deletions

9
Cargo.lock generated
View file

@ -2551,9 +2551,9 @@ dependencies = [
[[package]]
name = "tetratto-core"
version = "12.0.1"
version = "12.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1b01499f7471ee6f05299c06ebb18760440452714c6f9a6c0c9e0cf9a663bd"
checksum = "a367ac3ced8ff302080e1b4a82a67acd24fa606245c4381a6f77dbaaf6ef4b58"
dependencies = [
"async-recursion",
"base16ct",
@ -2570,6 +2570,7 @@ dependencies = [
"serde_json",
"tetratto-l10n",
"tetratto-shared",
"tokio",
"toml 0.9.2",
"totp-rs",
]
@ -2587,9 +2588,9 @@ dependencies = [
[[package]]
name = "tetratto-shared"
version = "12.0.5"
version = "12.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11c2ba2be9c92a4ac566b9c3b615d7311b3d0e98b175ad84e81b44644b34dd8f"
checksum = "286290ad09be3c507f9a47d38e92b024e6fcde34dbb515113f5bdb6b926cbee3"
dependencies = [
"ammonia",
"chrono",

View file

@ -8,8 +8,8 @@ license = "AGPL-3.0-or-later"
homepage = "https://tetratto.com"
[dependencies]
tetratto-core = "12.0.1"
tetratto-shared = "12.0.5"
tetratto-core = "12.0.2"
tetratto-shared = "12.0.6"
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
pathbufd = "0.1.4"
serde = { version = "1.0.219", features = ["derive"] }

3
app/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
public/docs/**/*
!public/docs/metadata.md
icons/

11
app/docs/metadata.md Normal file
View file

@ -0,0 +1,11 @@
# What is metadata?
Each entry also includes a separate metadata content. Metadata is used to further customize the look and display of your entries to a finer degree than you can through just Markdown.
Metadata includes options to customize how your entries look when shared to other platforms, as well as to customize how your entries render when viewed.
All option names can either be in all lowercase, or all uppercase. While option values for `String` type metadata options _should_ be encased in double quotes, that will be done for you if you leave them out. This is included just for compatibility.
Metadata options go in the "Metadata" tab in the entry editor page. Each option should be on a new line, and should be formatted as `NAME = value`. If you're familiar with TOML, you should be comfortable with metadata formatting.
You can view a list of all options and what they do [here](http://localhost:9119/public/reference/attobin/model/struct.EntryMetadata.html#fields).

View file

@ -224,3 +224,109 @@ globalThis.check_exists_input = (e) => {
e.target.reportValidity();
}, 1000);
};
// components
function close_dropdowns() {
for (const dropdown of Array.from(
document.querySelectorAll(".inner.open"),
)) {
dropdown.classList.remove("open");
}
}
globalThis.open_dropdown = (event) => {
event.stopImmediatePropagation();
let target = event.target;
while (!target.matches(".dropdown")) {
target = target.parentElement;
}
// close all others
close_dropdowns();
// open
setTimeout(() => {
for (const dropdown of Array.from(target.querySelectorAll(".inner"))) {
// check y
const box = target.getBoundingClientRect();
let parent = dropdown.parentElement;
while (!parent.matches("html, .window")) {
parent = parent.parentElement;
}
let parent_height = parent.getBoundingClientRect().y;
if (parent.nodeName === "HTML") {
parent_height = window.screen.height;
}
const scroll = window.scrollY;
const height = parent_height;
const y = box.y + scroll;
if (y > height - scroll - 375) {
dropdown.classList.add("top");
} else {
dropdown.classList.remove("top");
}
// open
dropdown.classList.add("open");
if (dropdown.classList.contains("open")) {
dropdown.removeAttribute("aria-hidden");
} else {
dropdown.setAttribute("aria-hidden", "true");
}
}
}, 5);
};
globalThis.init_dropdowns = (bind_to) => {
for (const dropdown of Array.from(document.querySelectorAll(".inner"))) {
dropdown.setAttribute("aria-hidden", "true");
}
bind_to.addEventListener("click", (event) => {
if (
event.target.matches(".dropdown") ||
event.target.matches("[exclude=dropdown]")
) {
return;
}
for (const dropdown of Array.from(
document.querySelectorAll(".inner.open"),
)) {
dropdown.classList.remove("open");
}
});
};
globalThis.METADATA_CSS_ENABLED = true;
globalThis.toggle_metadata_css = (e) => {
e.target.classList.add("yellow");
METADATA_CSS_ENABLED = !METADATA_CSS_ENABLED;
if (!METADATA_CSS_ENABLED) {
media_theme_pref(); // user user theme
document.getElementById("metadata_css").remove(); // remove css
// reset colored text
for (const element of Array.from(
document.querySelectorAll(".color_block"),
)) {
element.removeAttribute("style");
element.classList.remove("color_block");
}
// strikethrough auto theme since it's disabled
document.getElementById("auto_theme").style.textDecoration =
"line-through";
} else {
window.location.reload();
}
};

1
app/public/reference Symbolic link
View file

@ -0,0 +1 @@
../../target/doc

View file

@ -12,7 +12,7 @@
--color-shadow: rgba(0, 0, 0, 0.08);
--color-red: hsl(0, 84%, 40%);
--color-green: hsl(100, 84%, 20%);
--color-yellow: hsl(41, 63%, 75%);
--color-yellow: oklch(47% 0.157 37.304);
--color-purple: hsl(284, 84%, 20%);
--color-primary: oklch(67.3% 0.182 276.935);
@ -24,6 +24,9 @@
--pad-2: 0.35rem;
--pad-3: 0.5rem;
--pad-4: 1rem;
--radius: 0.2rem;
--nav-height: 36px;
}
* {
@ -45,7 +48,7 @@
--color-link: #93c5fd;
--color-red: hsl(0, 94%, 82%);
--color-green: hsl(100, 94%, 82%);
--color-yellow: hsl(41, 63%, 65%);
--color-yellow: oklch(90.1% 0.076 70.697);
--color-purple: hsl(284, 94%, 82%);
}
@ -73,7 +76,7 @@ main {
article {
margin: var(--pad-2) 0;
height: calc(100dvh - var(--pad-4) * 2);
height: calc(100dvh - var(--pad-4) - var(--nav-height) * 2);
}
.tab {
@ -89,6 +92,14 @@ article {
animation: fadein ease-in-out 1 0.5s forwards running;
}
nav {
/* background: var(--color-raised); */
height: var(--nav-height);
/* position: sticky;
z-index: 2;
top: 2; */
}
@media screen and (max-width: 900px) {
main,
article,
@ -117,7 +128,7 @@ article {
}
.content_container {
margin: var(--pad-2) auto;
margin: 0 auto var(--pad-2);
width: 100%;
}
@ -167,7 +178,7 @@ video {
display: flex;
justify-content: center;
align-items: center;
gap: var(--gap-2);
gap: var(--pad-2);
padding: var(--pad-2) calc(var(--pad-3) * 1.5);
cursor: pointer;
background: var(--color-raised);
@ -187,7 +198,7 @@ video {
--h: 28px;
}
.button:hover {
.button:not(:has(.button:hover)):not(.camo):hover {
background: var(--color-super-raised);
}
@ -196,10 +207,70 @@ video {
color: inherit;
}
.bar .button.camo:hover {
.bar .button:not(.simple).camo:hover {
color: var(--color-link);
}
.button.simple {
--size: 18px;
font-weight: 600;
padding: var(--pad-2) !important;
border-radius: var(--radius);
width: var(--size);
height: var(--size);
aspect-ratio: 1 / 1;
font-size: 12px;
}
.button.surface {
background: var(--color-surface);
}
.button.surface.simple:is(.camo *) {
background: var(--color-super-raised);
}
/* dropdown */
.dropdown {
position: relative;
}
.dropdown .inner {
display: none;
flex-direction: column;
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
var(--color-shadow);
background: var(--color-raised);
color: inherit;
position: absolute;
z-index: 2;
top: 100%;
}
.dropdown .inner.open {
display: flex;
}
.dropdown .inner .button {
padding: var(--pad-3) var(--pad-4);
justify-content: flex-start;
width: 100%;
}
.dropdown:has(.inner.open) .button:nth-child(1):not(.inner *) {
background: var(--color-raised);
}
.dropdown .inner.top {
top: unset;
bottom: 100%;
}
.dropdown .inner.left {
left: 0;
right: unset;
}
/* input */
input {
--h: 36px;
@ -276,6 +347,7 @@ pre {
padding: var(--pad-2) var(--pad-4);
border-left: solid 5px var(--color-primary);
background: var(--color-surface);
border-radius: var(--radius);
}
code {
@ -297,8 +369,22 @@ code * {
font-size: 0.8rem !important;
}
code:not(pre *) {
padding: var(--pad-1) var(--pad-2);
background: oklch(98% 0.016 73.684 / 25%);
color: oklch(90.1% 0.076 70.697);
border-radius: var(--radius);
white-space: break-spaces;
}
code:not(pre *):not(.dark *) {
background: oklch(83.7% 0.128 66.29 / 25%);
color: oklch(47% 0.157 37.304);
}
svg.icon {
stroke: currentColor;
fill: currentColor;
width: 18px;
height: 1em;
}
@ -307,6 +393,10 @@ svg.icon.filled {
fill: currentColor;
}
.no_fill svg.icon {
fill: transparent;
}
button svg {
pointer-events: none;
}
@ -384,6 +474,10 @@ a {
color: var(--color-link);
}
.color_block a {
color: inherit;
}
a.flush {
color: inherit;
}
@ -477,12 +571,12 @@ blockquote {
.cm-comment,
.hljs-keyword {
color: rgb(153 27 27) !important;
color: oklch(47% 0.157 37.304) !important;
}
.cm-comment:is(.dark *),
.hljs-keyword:is(.dark *) {
color: rgb(254, 202, 202) !important;
color: oklch(90.1% 0.076 70.697) !important;
}
.cm-link {

View file

@ -1,40 +0,0 @@
(text "{% macro footer() -%}")
(footer
("class" "flex flex-col items-center gap-2")
(hr ("style" "min-width: 20rem; margin-top: calc(var(--pad-4) * 4)"))
(div
("class" "w-full flex justify-between")
(div ("style" "width: 50px"))
(div
("class" "flex flex-col gap-2 items-center")
(div
("class" "flex gap-2 flex-wrap")
(a
("href" "/")
(text "new"))
(a
("href" "/{{ what_page_slug }}")
(text "what"))
(a
("href" "https://trisua.com/t/attobin")
(text "source")))
(span ("style" "font-size: 14px") ("class" "fade") (text "{{ name }}")))
; theme switches
(button
("class" "button camo fade")
("id" "switch_light")
("title" "Switch theme")
("onclick" "set_theme('Dark')")
(text "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-sun-icon lucide-sun icon\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2\"/><path d=\"M12 20v2\"/><path d=\"m4.93 4.93 1.41 1.41\"/><path d=\"m17.66 17.66 1.41 1.41\"/><path d=\"M2 12h2\"/><path d=\"M20 12h2\"/><path d=\"m6.34 17.66-1.41 1.41\"/><path d=\"m19.07 4.93-1.41 1.41\"/></svg>"))
(button
("class" "button camo fade hidden")
("id" "switch_dark")
("title" "Switch theme")
("onclick" "set_theme('Light')")
(text "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-moon-icon lucide-moon icon\"><path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"/></svg>"))))
(text "{%- endmacro %}")

View file

@ -0,0 +1,9 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "{{ file_name }} - {{ name }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "card container")
(p (text "{{ text|markdown|safe }}")))
(text "{% endblock %}")

View file

@ -19,7 +19,13 @@
("class" "button camo tab_button")
("id" "metadata_tab_button")
("onclick" "tab_metadata()")
(text "Metadata")))
(text "Metadata")
(a
("class" "button simple surface")
("href" "/docs/metadata")
("target" "_blank")
("title" "Info")
(text "i"))))
(div
("class" "card tab tabs container")
("id" "tabs_group")
@ -74,7 +80,6 @@
("class" "button red")
("ui_ident" "delete")
(text "Delete"))))
(text "{{ components::footer() }}")
; editor
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js"))

View file

@ -6,5 +6,4 @@
(div
("class" "card")
(p (text "{{ error }}")))
(text "{{ components::footer() }}")
(text "{% endblock %}")

View file

@ -1,6 +1,9 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "{{ name }}"))
(meta ("property" "og:title") ("content" "{{ name }}"))
(meta ("property" "twitter:title") ("content" "{{ name }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
@ -19,7 +22,13 @@
("class" "button camo tab_button")
("id" "metadata_tab_button")
("onclick" "tab_metadata()")
(text "Metadata")))
(text "Metadata")
(a
("class" "button simple surface")
("href" "/docs/metadata")
("target" "_blank")
("title" "Info")
(text "i"))))
(div
("class" "card tab tabs container")
("id" "tabs_group")
@ -55,7 +64,6 @@
("name" "slug")
("oninput" "check_exists_input(event)")
("placeholder" "Custom url"))))
(text "{{ components::footer() }}")
; editor
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js"))

View file

@ -1,4 +1,3 @@
(text "{%- import \"components.lisp\" as components -%}")
(text "<!doctype html>")
(html
("lang" "en")
@ -19,8 +18,64 @@
(text "{% block head %}{% endblock %}"))
(body
; nav
(nav
("class" "flex w-full justify-between gap-2")
(div
("class" "flex side")
(div
("class" "dropdown")
(button
("onclick" "open_dropdown(event)")
("exclude" "dropdown")
("class" "button camo fade")
(text "{{ icon \"menu\" }}"))
(div
("class" "inner")
(a
("class" "button")
("href" "/")
(text "new"))
(a
("class" "button")
("href" "/{{ what_page_slug }}")
(text "what"))
(a
("class" "button")
("href" "https://trisua.com/t/attobin")
(text "source"))))
(a
("class" "button camo fade")
("href" "/")
("title" "new")
(text "{{ icon \"plus\" }}")))
(div
("class" "side flex")
(text "{% block nav_extras %}{% endblock %}")
; theme switches
(button
("class" "button camo fade")
("id" "switch_light")
("title" "Switch theme")
("onclick" "set_theme('Dark')")
(text "{{ icon \"sun\" }}"))
(button
("class" "button camo fade hidden")
("id" "switch_dark")
("title" "Switch theme")
("onclick" "set_theme('Light')")
(text "{{ icon \"moon\" }}"))))
; page
(article
("class" "content_container flex flex-col")
("id" "page")
(ul ("id" "messages"))
(text "{% block body %}{% endblock %}"))))
(text "{% block body %}{% endblock %}")
(div ("style" "min-height: 32px")))
(script (text "setTimeout(() => init_dropdowns(document.body), 150);"))))

View file

@ -4,6 +4,11 @@
(text "{{ entry.slug }}"))
(text "{%- endif %} {{ metadata_head|safe }}")
(text "{% if not metadata.share_title -%}")
(meta ("property" "og:title") ("content" "{{ entry.slug }}"))
(meta ("property" "twitter:title") ("content" "{{ entry.slug }}"))
(text "{%- endif %}")
(text "{% if metadata.page_icon|length == 0 -%}")
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{%- endif %}")
@ -30,7 +35,7 @@
; auto theme
(text "{% if metadata.access_recommended_theme != 'None' -%}")
(span (text "Auto theme: {{ metadata.access_recommended_theme }}"))
(span ("id" "auto_theme") (text "Auto theme: {{ metadata.access_recommended_theme }}"))
(script ("defer" "true") (text "setTimeout(() => { temporary_set_theme('{{ metadata.access_recommended_theme }}') }, 150);"))
(text "{%- endif %}")
@ -44,11 +49,17 @@
(a ("class" "button small") ("href" "/{{ metadata.access_easy_read }}") (b (text "E2R")))
(text "{%- endif %}"))))
(text "{{ metadata_css|safe }}")
(div ("style" "display: none") ("id" "metadata_css") (text "{{ metadata_css|safe }}"))
(link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"))
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))
(script (text "hljs.highlightAll();"))
(text "{{ components::footer() }}")
(text "{% endblock %}")
(text "{% block nav_extras %}")
(button
("class" "button camo fade no_fill")
("title" "Toggle high-contrast")
("onclick" "toggle_metadata_css(event)")
(text "{{ icon \"contrast\" }}"))
(text "{% endblock %}")

6
justfile Normal file
View file

@ -0,0 +1,6 @@
doc:
cargo doc --no-deps --document-private-items
build:
just doc
cargo build -r

View file

@ -7,7 +7,7 @@ use axum::{Extension, Router};
use nanoneo::core::element::Render;
use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
use tera::{Tera, Value};
use tetratto_core::sdk::DataClient;
use tetratto_core::{html, sdk::DataClient};
use tetratto_shared::hash::salt;
use tokio::sync::RwLock;
use tower_http::{
@ -64,20 +64,29 @@ async fn main() {
// build lisp
create_dir_if_not_exists!("./templates_build");
create_dir_if_not_exists!("./icons");
for x in glob::glob("./templates_src/**/*").expect("failed to read pattern") {
match x {
Ok(x) => std::fs::write(
x.to_str()
.unwrap()
.replace("templates_src/", "templates_build/"),
html::pull_icons(
nanoneo::parse(&std::fs::read_to_string(x).expect("failed to read template"))
.render(&mut HashMap::new()),
"./icons",
)
.await,
)
.expect("failed to write template"),
Err(e) => panic!("{e}"),
}
}
// create docs dir
create_dir_if_not_exists!("./docs");
// ...
let mut tera = match Tera::new(&format!("./templates_build/**/*")) {
Ok(t) => t,

View file

@ -76,7 +76,7 @@ fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
// by this point, we have: !
// %color_buffer%main_buffer%%
output.push_str(&format!(
"<span style=\"color: {color_buffer}\">{buffer}</span>"
"<span style=\"color: {color_buffer}\" class=\"color_block\">{buffer}</span>"
));
color_buffer.clear();
@ -468,7 +468,14 @@ fn parse_image_line(output: &mut String, buffer: &mut String, line: &str) {
if in_image {
// end
output.push_str(&format!(
"<img loading=\"lazy\" alt=\"{alt}\" src=\"{buffer}\" />"
"<img loading=\"lazy\" alt=\"{alt}\" src=\"{buffer}\" style=\"float: {}\" />",
if buffer.ends_with("#left") {
"left"
} else if buffer.ends_with("#right") {
"right"
} else {
"unset"
}
));
alt = String::new();

View file

@ -357,6 +357,17 @@ macro_rules! metadata_css {
}
};
($selector:literal, $property:literal !important, $self:ident.$field:ident->$output:ident) => {
if !$self.$field.is_empty() {
$output.push_str(&format!(
"{} {{ {}: {} !important; }}\n",
$selector,
$property,
EntryMetadata::css_escape(&$self.$field)
));
}
};
($selector:literal, $property:literal, $format:literal, $self:ident.$field:ident->$output:ident) => {
if !$self.$field.is_empty() {
$output.push_str(&format!(
@ -462,7 +473,7 @@ impl EntryMetadata {
metadata_css!(".container", "border-radius", self.container_border_radius->output);
metadata_css!(".container", "box-shadow", self.container_shadow->output);
metadata_css!(".container", "text-shadow", self.content_text_shadow->output);
metadata_css!(".container a", "color", self.content_text_link_color->output);
metadata_css!("*, html *", "--color-link" !important, self.content_text_link_color->output);
if self.content_text_align != TextAlignment::Left {
output.push_str(&format!(

View file

@ -8,6 +8,8 @@ use axum::{
response::{Html, IntoResponse},
routing::{get, get_service, post},
};
use axum_extra::extract::CookieJar;
use pathbufd::PathBufD;
use serde::Deserialize;
use serde_valid::Validate;
use tera::Context;
@ -32,6 +34,7 @@ pub fn routes() -> Router {
get_service(tower_http::services::ServeDir::new("./public")),
)
.fallback(not_found_request)
.route("/docs/{name}", get(view_doc_request))
// pages
.route("/", get(index_request))
.route("/{slug}", get(view_request))
@ -78,6 +81,39 @@ async fn index_request(Extension(data): Extension<State>) -> impl IntoResponse {
)
}
async fn view_doc_request(
Extension(data): Extension<State>,
Path(name): Path<String>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let path = PathBufD::current().extend(&["docs", &format!("{name}.md")]);
if !std::fs::exists(&path).unwrap_or(false) {
let mut ctx = default_context(&data, &build_code);
ctx.insert(
"error",
&Error::GeneralNotFound("entry".to_string()).to_string(),
);
return Html(tera.render("error.lisp", &ctx).unwrap());
}
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) => {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &Error::MiscError(e.to_string()).to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
};
let mut ctx = default_context(&data, &build_code);
ctx.insert("text", &text);
ctx.insert("file_name", &name);
return Html(tera.render("doc.lisp", &ctx).unwrap());
}
async fn view_request(
Extension(data): Extension<State>,
Path(slug): Path<String>,
@ -258,27 +294,46 @@ fn default_random() -> String {
salt()
}
/// The time that must be waited between each entry creation.
const CREATE_WAIT_TIME: usize = 5000;
async fn create_request(
jar: CookieJar,
Extension(data): Extension<State>,
Json(req): Json<CreateEntry>,
) -> impl IntoResponse {
) -> std::result::Result<impl IntoResponse, Json<ApiReturn<()>>> {
let (ref data, _, _) = *data.read().await;
// check wait time
if let Some(cookie) = jar.get("__Secure-Claim-Next") {
if unix_epoch_timestamp()
!= cookie
.to_string()
.replace("__Secure-Claim-Next=", "")
.parse::<usize>()
.unwrap_or(0)
{
return Err(Json(
Error::MiscError("You must wait a bit to create another entry".to_string()).into(),
));
}
}
// check lengths
if req.slug.len() < 2 {
return Json(Error::DataTooShort("slug".to_string()).into());
return Err(Json(Error::DataTooShort("slug".to_string()).into()));
}
if req.slug.len() > 32 {
return Json(Error::DataTooLong("slug".to_string()).into());
return Err(Json(Error::DataTooLong("slug".to_string()).into()));
}
if req.content.len() < 2 {
return Json(Error::DataTooShort("content".to_string()).into());
return Err(Json(Error::DataTooShort("content".to_string()).into()));
}
if req.content.len() > 150_000 {
return Json(Error::DataTooLong("content".to_string()).into());
return Err(Json(Error::DataTooLong("content".to_string()).into()));
}
// check slug
@ -288,17 +343,19 @@ async fn create_request(
.unwrap();
if regex.captures(&req.slug).is_some() {
return Json(Error::MiscError("This slug contains invalid characters".to_string()).into());
return Err(Json(
Error::MiscError("This slug contains invalid characters".to_string()).into(),
));
}
// check metadata
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
Err(e) => return Err(Json(Error::MiscError(e.to_string()).into())),
};
if let Err(e) = metadata.validate() {
return Json(Error::MiscError(e.to_string()).into());
return Err(Json(Error::MiscError(e.to_string()).into()));
}
// check for existing
@ -310,7 +367,9 @@ async fn create_request(
.await
.is_ok()
{
return Json(Error::MiscError("Slug already in use".to_string()).into());
return Err(Json(
Error::MiscError("Slug already in use".to_string()).into(),
));
}
// create
@ -333,22 +392,31 @@ async fn create_request(
)
.await
{
return Json(e.into());
return Err(Json(e.into()));
}
if let Err(e) = data
.insert(format!("entries.views('{}')", req.slug), 0.to_string())
.await
{
return Json(e.into());
return Err(Json(e.into()));
}
// return
Ok((
[(
"Set-Cookie",
format!(
"__Secure-Claim-Next={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=5",
unix_epoch_timestamp() + CREATE_WAIT_TIME
),
)],
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some((req.slug, req.edit_code)),
})
}),
))
}
#[derive(Deserialize)]