From d80368e6c2e40bad951179490c66520650cc3e8b Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 20 Jul 2025 20:43:01 -0400 Subject: [PATCH] add: taken slug check --- app/public/app.js | 31 +++++++++++++++++++++++++++++ app/public/style.css | 21 +++++++++++++------- app/templates_src/edit.lisp | 1 + app/templates_src/index.lisp | 1 + src/routes.rs | 38 ++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/app/public/app.js b/app/public/app.js index b05da7f..6724b3f 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -137,3 +137,34 @@ globalThis.tab_preview = async () => { document.getElementById("editor_tab_button").classList.add("camo"); document.getElementById("preview_tab_button").classList.remove("camo"); }; + +let exists_timeout = null; +globalThis.check_exists_input = (e) => { + if (exists_timeout) { + clearTimeout(exists_timeout); + } + + exists_timeout = setTimeout(async () => { + if (e.target.value.length < 2 || e.target.value.length > 32) { + e.target.setCustomValidity(""); + e.target.removeAttribute("data-invalid"); + e.target.reportValidity(); + return; + } + + const exists = ( + await (await fetch(`/api/v1/entries/${e.target.value}`)).json() + ).payload; + + console.log(exists); + if (exists) { + e.target.setCustomValidity("Slug is already in use"); + e.target.setAttribute("data-invalid", "true"); + } else { + e.target.setCustomValidity(""); + e.target.removeAttribute("data-invalid"); + } + + e.target.reportValidity(); + }, 1000); +}; diff --git a/app/public/style.css b/app/public/style.css index 4941128..ffe2463 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -158,7 +158,7 @@ video { /* button */ .button { - --h: 35.2px; + --h: 36px; display: flex; justify-content: center; align-items: center; @@ -171,11 +171,11 @@ video { border: none; width: max-content; height: var(--h); - min-height: var(--h); - max-height: var(--h); + line-height: var(--h); transition: background 0.15s; text-decoration: none !important; user-select: none; + appearance: none; } .button:hover { @@ -193,17 +193,19 @@ video { /* input */ input { - --h: 35.2px; + --h: 36px; padding: var(--pad-2) var(--pad-4); background: var(--color-raised); color: var(--color-text); outline: none; border: none; width: max-content; - transition: background 0.15s; + transition: + background 0.15s, + border 0.15s; height: var(--h); - min-height: var(--h); - max-height: var(--h); + line-height: var(--h); + border-left: solid 0px transparent; } input:focus { @@ -212,6 +214,11 @@ input:focus { background: var(--color-super-raised); } +input:user-invalid, +input[data-invalid] { + border-left: inset 5px var(--color-red); +} + /* typo */ p { margin-bottom: var(--pad-4); diff --git a/app/templates_src/edit.lisp b/app/templates_src/edit.lisp index c0548d6..3b28730 100644 --- a/app/templates_src/edit.lisp +++ b/app/templates_src/edit.lisp @@ -46,6 +46,7 @@ ("type" "text") ("minlength" "2") ("name" "new_slug") + ("oninput" "check_exists_input(event)") ("placeholder" "New url"))) (div ("class" "w-full flex justify-between gap-2") diff --git a/app/templates_src/index.lisp b/app/templates_src/index.lisp index e9b110d..3256d50 100644 --- a/app/templates_src/index.lisp +++ b/app/templates_src/index.lisp @@ -43,6 +43,7 @@ ("minlength" "2") ("maxlength" "32") ("name" "slug") + ("oninput" "check_exists_input(event)") ("placeholder" "Custom url")))) (text "{{ components::footer() }}") diff --git a/src/routes.rs b/src/routes.rs index 81b2d0c..9b001de 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -34,6 +34,7 @@ pub fn routes() -> Router { .route("/api/v1/render", post(render_request)) .route("/api/v1/entries", post(create_request)) .route("/api/v1/entries/{slug}", post(edit_request)) + .route("/api/v1/entries/{slug}", get(exists_request)) } fn default_context(data: &DataClient, build_code: &str) -> Context { @@ -178,6 +179,25 @@ async fn render_request(Json(req): Json) -> impl IntoResponse { crate::markdown::render_markdown(&req.content) } +async fn exists_request( + Extension(data): Extension, + Path(slug): Path, +) -> impl IntoResponse { + let (ref data, _, _) = *data.read().await; + + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: data + .query(&SimplifiedQuery { + query: AppDataSelectQuery::KeyIs(format!("entries('{}')", slug)), + mode: AppDataSelectMode::One(0), + }) + .await + .is_ok(), + }) +} + #[derive(Deserialize)] struct CreateEntry { content: String, @@ -206,6 +226,14 @@ async fn create_request( return Json(Error::DataTooLong("slug".to_string()).into()); } + if req.content.len() < 2 { + return Json(Error::DataTooShort("content".to_string()).into()); + } + + if req.content.len() > 150_000 { + return Json(Error::DataTooLong("content".to_string()).into()); + } + // check for existing if data .query(&SimplifiedQuery { @@ -274,6 +302,16 @@ async fn edit_request( ) -> impl IntoResponse { let (ref data, _, _) = *data.read().await; + // check content length + if req.content.len() < 2 { + return Json(Error::DataTooShort("content".to_string()).into()); + } + + if req.content.len() > 150_000 { + return Json(Error::DataTooLong("content".to_string()).into()); + } + + // ... let (id, mut entry) = match data .query(&SimplifiedQuery { query: AppDataSelectQuery::KeyIs(format!("entries('{}')", slug)),