Rework design & implement link shorten page
This commit is contained in:
parent
46a645973d
commit
94f645b9a5
11 changed files with 839 additions and 458 deletions
621
Cargo.lock
generated
621
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
@ -5,8 +5,9 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
dotenv = "0.15.0"
|
||||
serde = { version = "1.0.133", features = ["derive"] }
|
||||
serde_json = "1.0.82"
|
||||
sqlx = { version = "0.5.10", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
warp = "0.3.2"
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
serde_json = "1.0.99"
|
||||
sqlx = { version = "0.6.3", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
warp = "0.3.5"
|
||||
|
|
|
@ -42,6 +42,11 @@ struct AddRedirectForm {
|
|||
status_code: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct DeleteRedirectForm {
|
||||
path: String,
|
||||
}
|
||||
|
||||
fn auth_valid(authorization: &str) -> bool {
|
||||
if let Some(token) = authorization.strip_prefix("Bearer ") {
|
||||
if std::env::var("REDIRECTS_AUTH_KEY").ok().as_deref() == Some(token) {
|
||||
|
@ -64,12 +69,6 @@ async fn add_redirect(
|
|||
add_redirect_form: AddRedirectForm,
|
||||
authorization: String,
|
||||
) -> Result<Response, Rejection> {
|
||||
if !auth_valid(&authorization) {
|
||||
return Ok(
|
||||
json_error_response("Invalid authorization", StatusCode::UNAUTHORIZED).into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn create_redirect(db: SqlitePool, redir: Redirect) -> sqlx::Result<()> {
|
||||
let mut conn = db.acquire().await?;
|
||||
|
||||
|
@ -84,9 +83,29 @@ async fn add_redirect(
|
|||
|
||||
Ok(())
|
||||
}
|
||||
let path = add_redirect_form
|
||||
.path
|
||||
.unwrap_or_else(|| todo!("generate random string"));
|
||||
|
||||
if !auth_valid(&authorization) {
|
||||
return Ok(
|
||||
json_error_response("Invalid authorization", StatusCode::UNAUTHORIZED).into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
let path = add_redirect_form.path.unwrap_or_else(|| {
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
const LEN: usize = 16;
|
||||
const CHARSET: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
let mut rng = thread_rng();
|
||||
let mut random_string = String::with_capacity(LEN);
|
||||
for _ in 0..LEN {
|
||||
let idx = rng.gen_range(0..CHARSET.len());
|
||||
let c: &str = &CHARSET[idx..=idx + 1]; // we know the charset is ASCII so this is okay
|
||||
random_string.push_str(c);
|
||||
}
|
||||
|
||||
random_string
|
||||
});
|
||||
|
||||
let redir = Redirect {
|
||||
path,
|
||||
|
@ -94,12 +113,42 @@ async fn add_redirect(
|
|||
redirect_code: add_redirect_form.status_code.map(i64::from).unwrap_or(302),
|
||||
};
|
||||
|
||||
Ok(match create_redirect(db, redir).await {
|
||||
Ok(_) => warp::reply::json(&serde_json::json!({"success": true})).into_response(),
|
||||
Err(_) => {
|
||||
json_error_response("Internal error", StatusCode::INTERNAL_SERVER_ERROR).into_response()
|
||||
}
|
||||
})
|
||||
if let Err(_e) = create_redirect(db, redir).await {
|
||||
return Ok(
|
||||
json_error_response("Internal error", StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(warp::reply::json(&serde_json::json!({"success": true})).into_response())
|
||||
}
|
||||
|
||||
async fn delete_redirect(
|
||||
db: SqlitePool,
|
||||
form: DeleteRedirectForm,
|
||||
authorization: String,
|
||||
) -> Result<Response, Rejection> {
|
||||
async fn delete_redirect(db: SqlitePool, redir_path: &str) -> sqlx::Result<()> {
|
||||
let mut conn = db.acquire().await?;
|
||||
let query = sqlx::query!("DELETE FROM redirects WHERE path = ?", redir_path);
|
||||
let _ = conn.execute(query).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if !auth_valid(&authorization) {
|
||||
return Ok(
|
||||
json_error_response("Invalid authorization", StatusCode::UNAUTHORIZED).into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(_e) = delete_redirect(db, &form.path).await {
|
||||
return Ok(
|
||||
json_error_response("Internal error", StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(warp::reply::json(&serde_json::json!({"success": true})).into_response())
|
||||
}
|
||||
|
||||
async fn list_redirects(db: SqlitePool, authorization: String) -> Result<impl Reply, Rejection> {
|
||||
|
@ -167,6 +216,14 @@ pub async fn routes(static_dir: &str) -> BoxedFilter<(Response,)> {
|
|||
.and(warp::body::form())
|
||||
.and(warp::header::<String>("authorization"))
|
||||
.and_then(add_redirect)
|
||||
})
|
||||
.or({
|
||||
let db = db.clone();
|
||||
warp::delete()
|
||||
.map(move || db.clone())
|
||||
.and(warp::body::form())
|
||||
.and(warp::header::<String>("authorization"))
|
||||
.and_then(delete_redirect)
|
||||
});
|
||||
|
||||
let lookup_route = warp::path::param()
|
||||
|
|
140
static/css/aesthetic.css
Normal file
140
static/css/aesthetic.css
Normal file
|
@ -0,0 +1,140 @@
|
|||
:root {
|
||||
--bg: #121212;
|
||||
--fg: white;
|
||||
--accent: rgb(255, 167, 248);
|
||||
--font: "Inter", sans-serif;
|
||||
--font-mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
font-size: 1.125em;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul li,
|
||||
ol li {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.inline-list {
|
||||
list-style: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inline-list li {
|
||||
display: block;
|
||||
margin: 0;
|
||||
|
||||
padding-inline-end: 0.5ch;
|
||||
}
|
||||
|
||||
.inline-list li + li {
|
||||
border-inline-start: 2px solid white;
|
||||
padding-inline-start: 0.5ch;
|
||||
}
|
||||
|
||||
abbr {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.lavender-webring-container {
|
||||
font-family: "Inter", sans-serif !important;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: start;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
|
||||
font-family: var(--font);
|
||||
font-size: 1em;
|
||||
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
padding: 0.5rem 1.5rem;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
--accent: rgb(255, 111, 111);
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="url"] {
|
||||
font-family: var(--font);
|
||||
|
||||
margin: 0.25em 0;
|
||||
margin-bottom: 0.35em;
|
||||
padding: 0.5em 0.5em;
|
||||
background-color: #100f11;
|
||||
border: 1px solid #e7ebf080;
|
||||
border-radius: 6px;
|
||||
color: var(--fg);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="url"]:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
label,
|
||||
th,
|
||||
button {
|
||||
text-transform: lowercase;
|
||||
}
|
|
@ -16,30 +16,33 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: 60ch;
|
||||
width: 60ch;
|
||||
max-width: 80ch;
|
||||
width: 80ch;
|
||||
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.about-me {
|
||||
#about-me {
|
||||
padding: 1em;
|
||||
border: 2px solid white;
|
||||
border-radius: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding-bottom: 0.5em;
|
||||
border-bottom: 1px dashed #444;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
header aside {
|
||||
header > * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
main {
|
||||
|
@ -52,25 +55,29 @@ footer {
|
|||
}
|
||||
|
||||
.hero-image {
|
||||
height: 10rem;
|
||||
border-radius: 6px;
|
||||
height: 8rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
section + section {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
h1 {
|
||||
margin: 0;
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.webring-container {
|
||||
p + p {
|
||||
margin-top: 1em;
|
||||
}
|
28
static/css/shortlinks-layout.css
Normal file
28
static/css/shortlinks-layout.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
table td.clipped {
|
||||
overflow-x: clip;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 40ch;
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
html {
|
||||
background-color: #121212;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(255, 167, 248);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 1.125em;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
.inline-list {
|
||||
list-style: none;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.inline-list li {
|
||||
display: block;
|
||||
|
||||
padding-inline-end: 0.5ch;
|
||||
}
|
||||
|
||||
.inline-list li + li {
|
||||
border-inline-start: 2px solid white;
|
||||
padding-inline-start: 0.5ch;
|
||||
}
|
||||
|
||||
abbr {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.lavender-webring-container {
|
||||
font-family: "Inter", sans-serif !important;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: start;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
table td.clipped {
|
||||
overflow-x: clip;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 30ch;
|
||||
}
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
<title>charlotte ✨</title>
|
||||
|
||||
<link rel="stylesheet" href="/css/layout.css" />
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
<link rel="stylesheet" href="/css/main-layout.css" />
|
||||
<link rel="stylesheet" href="/css/aesthetic.css" />
|
||||
|
||||
<link rel="stylesheet" href="/inter/inter.css" />
|
||||
|
||||
|
@ -39,7 +39,7 @@
|
|||
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<article class="about-me">
|
||||
<article id="about-me">
|
||||
<header>
|
||||
<aside>
|
||||
<h1><abbr title="charlotte athena som">charlotte ✨</abbr></h1>
|
||||
|
@ -47,8 +47,8 @@
|
|||
<ul id="traits" class="inline-list">
|
||||
<li id="age">22</li>
|
||||
<li>trans</li>
|
||||
<li>catgirl bunnygirl enby</li>
|
||||
<li>lesbian</li>
|
||||
<li>bunnygirl enby</li>
|
||||
<li>anarchist</li>
|
||||
<li>communist</li>
|
||||
</ul>
|
||||
|
@ -63,19 +63,23 @@
|
|||
|
||||
<main>
|
||||
<p>
|
||||
welcome to <code>char.lt</code>: the domain, is, like, my name!!
|
||||
hey, welcome to <code>char.lt</code>: the domain, is, like, my
|
||||
name!!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
hey, i'm charlotte! i'm transfem and non-binary and in my early 20s
|
||||
:)
|
||||
i'm charlotte! i'm transfem and non-binary and in my early 20s :)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
i do multimedia with computers, which means i mostly do electronic
|
||||
music, software, procedural animations and graphics, and other silly
|
||||
art projects!
|
||||
</p>
|
||||
<p>i do multimedia projects with computers! that means:</p>
|
||||
|
||||
<ul id="activities">
|
||||
<li>electronic music</li>
|
||||
<li>software</li>
|
||||
<li>procedural animations</li>
|
||||
<li>visual effects</li>
|
||||
<li>other silly art projects!</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
i am the founder + director of a fledgling digital studio,
|
||||
|
@ -83,21 +87,18 @@
|
|||
</p>
|
||||
|
||||
<section>
|
||||
my pronouns are:
|
||||
<h2>my pronouns are:</h2>
|
||||
|
||||
<ul id="pronouns" class="inline-list">
|
||||
<li>
|
||||
<a href="https://pronoun.is/bun/bun/buns/buns/bunself">
|
||||
bun/buns
|
||||
</a>
|
||||
<a href="https://pronouns.within.lgbt/bun">bun/buns</a>
|
||||
</li>
|
||||
<li><a href="https://pronoun.is/they">they/them</a></li>
|
||||
<li><a href="https://pronoun.is/she">she/her</a></li>
|
||||
<li><a href="https://pronouns.within.lgbt/she">she/her</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
talk to me in:
|
||||
<h2>talk to me in:</h2>
|
||||
|
||||
<ul id="languages" class="inline-list">
|
||||
<li>english</li>
|
||||
|
@ -109,23 +110,21 @@
|
|||
</section>
|
||||
|
||||
<section>
|
||||
<ul id="links" class="inline-list">
|
||||
<h2>find me around the web:</h2>
|
||||
|
||||
<ul id="socials" class="inline-list">
|
||||
<li>
|
||||
<a rel="me" href="https://som.codes">som.codes</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://hackery.site">hackery.site</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://trans.enby.town/bun">
|
||||
<a rel="me" href="https://trans.enby.town/charlotte">
|
||||
trans.enby.town
|
||||
</a>
|
||||
(fediverse)
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://github.com/char">github</a>
|
||||
<a rel="me" href="https://pet.bun.how/"> pet.bun.how </a>
|
||||
(bluesky)
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://twitter.com/bhop_art"> twitter </a>
|
||||
<a rel="me" href="https://twitter.com/char_bun">twitter</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://last.fm/user/half-kh-hacker">
|
||||
|
@ -137,6 +136,15 @@
|
|||
soundcloud
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>find my software:</h2>
|
||||
|
||||
<ul id="forges" class="inline-list">
|
||||
<li>
|
||||
<a rel="me" href="https://github.com/char">github</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="me" href="https://git.lavender.software/charlotte">
|
||||
git.lavender.software
|
||||
|
@ -144,12 +152,20 @@
|
|||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>blogs:</h2>
|
||||
|
||||
<ul id="websites" class="inline-list">
|
||||
<li><a href="https://som.codes/">som.codes</a></li>
|
||||
<li><a href="https://hackery.site">hackery.site</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>a site by <a href="https://som.codes">charlotte som</a></footer>
|
||||
</article>
|
||||
|
||||
<article class="webring-container">
|
||||
<article id="webring-container">
|
||||
<script
|
||||
src="https://lavender.software/webring/webring-0.2.0.js"
|
||||
data-site-id="charlotte"
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
export const q = (selector) => document.querySelector(selector);
|
||||
export const t = (text) => document.createTextNode(text);
|
||||
export const h = (tag, attributes, children) => {
|
||||
if (!Array.isArray(children)) {
|
||||
children = [children];
|
||||
}
|
||||
|
||||
children = children.map((x) => {
|
||||
if (x instanceof HTMLElement) return x;
|
||||
return t(x);
|
||||
});
|
||||
|
||||
const elem = document.createElement(tag);
|
||||
Object.assign(elem, attributes);
|
||||
elem.append(...children);
|
||||
return elem;
|
||||
};
|
||||
export const r = (f) => {
|
||||
if (document.readyState === "complete") {
|
||||
f();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", f);
|
||||
}
|
||||
};
|
|
@ -1,65 +1,70 @@
|
|||
import { r, q, h } from "./brief-html.js";
|
||||
|
||||
const populateRedirects = async (redirKey) => {
|
||||
const body = q("main table tbody");
|
||||
body.innerHTML = "Loading...";
|
||||
|
||||
const redirects = await fetch("/redirects.json", {
|
||||
headers: { authorization: `Bearer ${redirKey}` },
|
||||
}).then((r) => r.json());
|
||||
|
||||
body.innerHTML = "";
|
||||
|
||||
for (const redirect of redirects) {
|
||||
body.appendChild(
|
||||
h("tr", {}, [
|
||||
h(
|
||||
"td",
|
||||
{},
|
||||
h("a", { href: "/" + redirect.path }, [h("pre", {}, [redirect.path])])
|
||||
),
|
||||
h(
|
||||
"td",
|
||||
{ className: "clipped" },
|
||||
h("a", { href: redirect.target_location }, [redirect.target_location])
|
||||
),
|
||||
h("td", {}, [redirect.redirect_code]),
|
||||
])
|
||||
);
|
||||
const q = (selector, elem = document) => elem.querySelector(selector);
|
||||
const t = (templateSelector, element) =>
|
||||
q(templateSelector).content.cloneNode(true).querySelector(element);
|
||||
const onReady = (f) => {
|
||||
if (document.readyState === "complete") {
|
||||
f();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", f);
|
||||
}
|
||||
};
|
||||
|
||||
const setupForm = (redirKey) => {
|
||||
const form = h("form", {}, [
|
||||
h("h2", {}, "create or replace"),
|
||||
const populateRedirects = async (redirKey) => {
|
||||
const status = q("main #status-message");
|
||||
|
||||
h("label", { for: "path" }, ["local path (random if empty)"]),
|
||||
h(
|
||||
"input",
|
||||
{
|
||||
type: "text",
|
||||
name: "path",
|
||||
id: "path",
|
||||
placeholder: "my-cool-link",
|
||||
},
|
||||
[]
|
||||
),
|
||||
let redirects;
|
||||
try {
|
||||
redirects = await fetch("/redirects.json", {
|
||||
headers: { authorization: `Bearer ${redirKey}` },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.catch();
|
||||
} catch (e) {
|
||||
status.textContent =
|
||||
"You are not authorized to fetch the list of redirects.";
|
||||
return;
|
||||
}
|
||||
|
||||
h("label", { for: "url" }, ["target url"]),
|
||||
h(
|
||||
"input",
|
||||
{
|
||||
type: "url",
|
||||
name: "url",
|
||||
id: "url",
|
||||
placeholder: "https://…",
|
||||
required: true,
|
||||
},
|
||||
[]
|
||||
),
|
||||
status.style.display = "none";
|
||||
|
||||
h("button", {}, ["Create"]),
|
||||
]);
|
||||
const table = t("template#redirect-table", "table");
|
||||
const body = q("tbody", table);
|
||||
q("main").appendChild(table);
|
||||
|
||||
for (const redirect of redirects) {
|
||||
const deleteBody = new URLSearchParams();
|
||||
deleteBody.append("path", redirect.path);
|
||||
|
||||
const row = t("template#redirect-row", "tr");
|
||||
const rowEntries = row.querySelectorAll("td");
|
||||
|
||||
rowEntries[0].querySelector("a").href = `/${redirect.path}`;
|
||||
rowEntries[0].querySelector("pre").textContent = redirect.path;
|
||||
|
||||
rowEntries[1].querySelector("a").href = redirect.target_location;
|
||||
rowEntries[1].querySelector("a").textContent = redirect.target_location;
|
||||
|
||||
rowEntries[2].textContent = redirect.redirect_code;
|
||||
rowEntries[3]
|
||||
.querySelector("button")
|
||||
.addEventListener("click", async (e) => {
|
||||
await fetch("/shorten", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
authorization: `Bearer ${redirKey}`,
|
||||
},
|
||||
body: deleteBody,
|
||||
});
|
||||
row.remove();
|
||||
});
|
||||
|
||||
body.appendChild(row);
|
||||
}
|
||||
};
|
||||
|
||||
const setupRedirectForm = (redirKey) => {
|
||||
const form = t("template#redirect-form", "form");
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -86,9 +91,13 @@ const setupForm = (redirKey) => {
|
|||
q("main").appendChild(form);
|
||||
};
|
||||
|
||||
r(async () => {
|
||||
const redirKey = window.localStorage.getItem("redirect-key");
|
||||
if (redirKey == null) return;
|
||||
onReady(async () => {
|
||||
const redirKey = window.localStorage.getItem("char.lt/redirect-key");
|
||||
if (redirKey == null) {
|
||||
// TODO: Show a form entry for setting the redirect key
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([populateRedirects(redirKey), setupForm(redirKey)]);
|
||||
await populateRedirects(redirKey);
|
||||
setupRedirectForm(redirKey);
|
||||
});
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
<title>shorten - char.lt</title>
|
||||
|
||||
<link rel="stylesheet" href="/css/layout.css" />
|
||||
<link rel="stylesheet" href="/css/styles.css" />
|
||||
<link rel="stylesheet" href="/css/shortlinks-layout.css" />
|
||||
<link rel="stylesheet" href="/css/aesthetic.css" />
|
||||
|
||||
<link rel="stylesheet" href="/inter/inter.css" />
|
||||
</head>
|
||||
|
@ -14,29 +14,60 @@
|
|||
<body>
|
||||
<div class="page-wrapper">
|
||||
<main>
|
||||
<h1>redirects</h1>
|
||||
<h1>Redirects</h1>
|
||||
|
||||
<p>this is where you can manage the redirects for the site.</p>
|
||||
<noscript>if you have javascript turned on.</noscript>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>path</th>
|
||||
<th>url</th>
|
||||
<th>code</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
You are not authorised to fetch the list of redirects.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<section id="status-message">Loading...</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<template id="redirect-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>URL</th>
|
||||
<th>Code</th>
|
||||
<th>Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template id="redirect-form">
|
||||
<form>
|
||||
<h2>Create or Replace</h2>
|
||||
|
||||
<label for="path">Local Path (random if empty)</label>
|
||||
<input type="text" name="path" id="path" placeholder="my-cool-link" />
|
||||
|
||||
<label for="url">Target URL</label>
|
||||
<input
|
||||
type="url"
|
||||
name="url"
|
||||
id="url"
|
||||
placeholder="https://…"
|
||||
required
|
||||
/>
|
||||
|
||||
<button>Create</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template id="redirect-row">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{path}"><pre>{path}</pre></a>
|
||||
</td>
|
||||
<td class="clipped"><a href="{location}">{location}</a></td>
|
||||
<td>{code}</td>
|
||||
<td><button class="danger">Delete</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script type="module" src="/js/shorten.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue