Rework
parent
46a645973d
commit
a36e360e49
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()
|
||||
|
|
|
@ -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,30 @@ 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;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
header aside {
|
||||
header > * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: 50%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
main {
|
||||
|
@ -52,25 +52,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;
|
||||
}
|
|
@ -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,7 +87,7 @@
|
|||
</p>
|
||||
|
||||
<section>
|
||||
my pronouns are:
|
||||
<h2>my pronouns are:</h2>
|
||||
|
||||
<ul id="pronouns" class="inline-list">
|
||||
<li>
|
||||
|
@ -91,13 +95,12 @@
|
|||
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>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
talk to me in:
|
||||
<h2>talk to me in:</h2>
|
||||
|
||||
<ul id="languages" class="inline-list">
|
||||
<li>english</li>
|
||||
|
@ -109,23 +112,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 +138,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 +154,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;
|
||||
}
|
||||
|
||||
status.style.display = "none";
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
h("label", { for: "url" }, ["target url"]),
|
||||
h(
|
||||
"input",
|
||||
{
|
||||
type: "url",
|
||||
name: "url",
|
||||
id: "url",
|
||||
placeholder: "https://…",
|
||||
required: true,
|
||||
},
|
||||
[]
|
||||
),
|
||||
body.appendChild(row);
|
||||
}
|
||||
};
|
||||
|
||||
h("button", {}, ["Create"]),
|
||||
]);
|
||||
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,28 +14,59 @@
|
|||
<body>
|
||||
<div class="page-wrapper">
|
||||
<main>
|
||||
<h1>redirects</h1>
|
||||
<p>this is where you can manage the redirects for the site.</p>
|
||||
<h1>Redirects</h1>
|
||||
|
||||
<p>this is where you can manage the redirects for the site.</p>
|
||||
<noscript>if you have javascript turned on.</noscript>
|
||||
|
||||
<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>Path</th>
|
||||
<th>URL</th>
|
||||
<th>Code</th>
|
||||
<th>Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
You are not authorised to fetch the list of redirects.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</main>
|
||||
</div>
|
||||
</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>
|
||||
|
|
Loading…
Reference in New Issue