Rework design & implement link shorten page
parent
46a645973d
commit
94f645b9a5
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
|
@ -5,8 +5,9 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
serde = { version = "1.0.133", features = ["derive"] }
|
rand = "0.8.5"
|
||||||
serde_json = "1.0.82"
|
serde = { version = "1.0.164", features = ["derive"] }
|
||||||
sqlx = { version = "0.5.10", features = ["sqlite", "runtime-tokio-rustls"] }
|
serde_json = "1.0.99"
|
||||||
tokio = { version = "1.15.0", features = ["full"] }
|
sqlx = { version = "0.6.3", features = ["sqlite", "runtime-tokio-rustls"] }
|
||||||
warp = "0.3.2"
|
tokio = { version = "1.29.1", features = ["full"] }
|
||||||
|
warp = "0.3.5"
|
||||||
|
|
|
@ -42,6 +42,11 @@ struct AddRedirectForm {
|
||||||
status_code: Option<u16>,
|
status_code: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct DeleteRedirectForm {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn auth_valid(authorization: &str) -> bool {
|
fn auth_valid(authorization: &str) -> bool {
|
||||||
if let Some(token) = authorization.strip_prefix("Bearer ") {
|
if let Some(token) = authorization.strip_prefix("Bearer ") {
|
||||||
if std::env::var("REDIRECTS_AUTH_KEY").ok().as_deref() == Some(token) {
|
if std::env::var("REDIRECTS_AUTH_KEY").ok().as_deref() == Some(token) {
|
||||||
|
@ -64,12 +69,6 @@ async fn add_redirect(
|
||||||
add_redirect_form: AddRedirectForm,
|
add_redirect_form: AddRedirectForm,
|
||||||
authorization: String,
|
authorization: String,
|
||||||
) -> Result<Response, Rejection> {
|
) -> 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<()> {
|
async fn create_redirect(db: SqlitePool, redir: Redirect) -> sqlx::Result<()> {
|
||||||
let mut conn = db.acquire().await?;
|
let mut conn = db.acquire().await?;
|
||||||
|
|
||||||
|
@ -84,9 +83,29 @@ async fn add_redirect(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
let path = add_redirect_form
|
|
||||||
.path
|
if !auth_valid(&authorization) {
|
||||||
.unwrap_or_else(|| todo!("generate random string"));
|
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 {
|
let redir = Redirect {
|
||||||
path,
|
path,
|
||||||
|
@ -94,12 +113,42 @@ async fn add_redirect(
|
||||||
redirect_code: add_redirect_form.status_code.map(i64::from).unwrap_or(302),
|
redirect_code: add_redirect_form.status_code.map(i64::from).unwrap_or(302),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(match create_redirect(db, redir).await {
|
if let Err(_e) = create_redirect(db, redir).await {
|
||||||
Ok(_) => warp::reply::json(&serde_json::json!({"success": true})).into_response(),
|
return Ok(
|
||||||
Err(_) => {
|
json_error_response("Internal error", StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
json_error_response("Internal error", StatusCode::INTERNAL_SERVER_ERROR).into_response()
|
.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> {
|
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::body::form())
|
||||||
.and(warp::header::<String>("authorization"))
|
.and(warp::header::<String>("authorization"))
|
||||||
.and_then(add_redirect)
|
.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()
|
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,33 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
max-width: 60ch;
|
max-width: 80ch;
|
||||||
width: 60ch;
|
width: 80ch;
|
||||||
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-me {
|
#about-me {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
@ -52,25 +55,29 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-image {
|
.hero-image {
|
||||||
height: 10rem;
|
height: 8rem;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
section + section {
|
section + section {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1 {
|
||||||
h2,
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 0.5em;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.webring-container {
|
p + p {
|
||||||
margin-top: 1em;
|
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>
|
<title>charlotte ✨</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/layout.css" />
|
<link rel="stylesheet" href="/css/main-layout.css" />
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/aesthetic.css" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/inter/inter.css" />
|
<link rel="stylesheet" href="/inter/inter.css" />
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<article class="about-me">
|
<article id="about-me">
|
||||||
<header>
|
<header>
|
||||||
<aside>
|
<aside>
|
||||||
<h1><abbr title="charlotte athena som">charlotte ✨</abbr></h1>
|
<h1><abbr title="charlotte athena som">charlotte ✨</abbr></h1>
|
||||||
|
@ -47,8 +47,8 @@
|
||||||
<ul id="traits" class="inline-list">
|
<ul id="traits" class="inline-list">
|
||||||
<li id="age">22</li>
|
<li id="age">22</li>
|
||||||
<li>trans</li>
|
<li>trans</li>
|
||||||
<li>catgirl bunnygirl enby</li>
|
|
||||||
<li>lesbian</li>
|
<li>lesbian</li>
|
||||||
|
<li>bunnygirl enby</li>
|
||||||
<li>anarchist</li>
|
<li>anarchist</li>
|
||||||
<li>communist</li>
|
<li>communist</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -63,19 +63,23 @@
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<p>
|
<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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<p>
|
<p>i do multimedia projects with computers! that means:</p>
|
||||||
i do multimedia with computers, which means i mostly do electronic
|
|
||||||
music, software, procedural animations and graphics, and other silly
|
<ul id="activities">
|
||||||
art projects!
|
<li>electronic music</li>
|
||||||
</p>
|
<li>software</li>
|
||||||
|
<li>procedural animations</li>
|
||||||
|
<li>visual effects</li>
|
||||||
|
<li>other silly art projects!</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
i am the founder + director of a fledgling digital studio,
|
i am the founder + director of a fledgling digital studio,
|
||||||
|
@ -83,21 +87,18 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
my pronouns are:
|
<h2>my pronouns are:</h2>
|
||||||
|
|
||||||
<ul id="pronouns" class="inline-list">
|
<ul id="pronouns" class="inline-list">
|
||||||
<li>
|
<li>
|
||||||
<a href="https://pronoun.is/bun/bun/buns/buns/bunself">
|
<a href="https://pronouns.within.lgbt/bun">bun/buns</a>
|
||||||
bun/buns
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li><a href="https://pronoun.is/they">they/them</a></li>
|
<li><a href="https://pronouns.within.lgbt/she">she/her</a></li>
|
||||||
<li><a href="https://pronoun.is/she">she/her</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
talk to me in:
|
<h2>talk to me in:</h2>
|
||||||
|
|
||||||
<ul id="languages" class="inline-list">
|
<ul id="languages" class="inline-list">
|
||||||
<li>english</li>
|
<li>english</li>
|
||||||
|
@ -109,23 +110,21 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<ul id="links" class="inline-list">
|
<h2>find me around the web:</h2>
|
||||||
|
|
||||||
|
<ul id="socials" class="inline-list">
|
||||||
<li>
|
<li>
|
||||||
<a rel="me" href="https://som.codes">som.codes</a>
|
<a rel="me" href="https://trans.enby.town/charlotte">
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a rel="me" href="https://hackery.site">hackery.site</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a rel="me" href="https://trans.enby.town/bun">
|
|
||||||
trans.enby.town
|
trans.enby.town
|
||||||
</a>
|
</a>
|
||||||
|
(fediverse)
|
||||||
</li>
|
</li>
|
||||||
<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>
|
||||||
<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>
|
||||||
<li>
|
<li>
|
||||||
<a rel="me" href="https://last.fm/user/half-kh-hacker">
|
<a rel="me" href="https://last.fm/user/half-kh-hacker">
|
||||||
|
@ -137,6 +136,15 @@
|
||||||
soundcloud
|
soundcloud
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<li>
|
||||||
<a rel="me" href="https://git.lavender.software/charlotte">
|
<a rel="me" href="https://git.lavender.software/charlotte">
|
||||||
git.lavender.software
|
git.lavender.software
|
||||||
|
@ -144,12 +152,20 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
|
|
||||||
<footer>a site by <a href="https://som.codes">charlotte som</a></footer>
|
<footer>a site by <a href="https://som.codes">charlotte som</a></footer>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="webring-container">
|
<article id="webring-container">
|
||||||
<script
|
<script
|
||||||
src="https://lavender.software/webring/webring-0.2.0.js"
|
src="https://lavender.software/webring/webring-0.2.0.js"
|
||||||
data-site-id="charlotte"
|
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 q = (selector, elem = document) => elem.querySelector(selector);
|
||||||
|
const t = (templateSelector, element) =>
|
||||||
const populateRedirects = async (redirKey) => {
|
q(templateSelector).content.cloneNode(true).querySelector(element);
|
||||||
const body = q("main table tbody");
|
const onReady = (f) => {
|
||||||
body.innerHTML = "Loading...";
|
if (document.readyState === "complete") {
|
||||||
|
f();
|
||||||
const redirects = await fetch("/redirects.json", {
|
} else {
|
||||||
headers: { authorization: `Bearer ${redirKey}` },
|
document.addEventListener("DOMContentLoaded", f);
|
||||||
}).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 setupForm = (redirKey) => {
|
const populateRedirects = async (redirKey) => {
|
||||||
const form = h("form", {}, [
|
const status = q("main #status-message");
|
||||||
h("h2", {}, "create or replace"),
|
|
||||||
|
|
||||||
h("label", { for: "path" }, ["local path (random if empty)"]),
|
let redirects;
|
||||||
h(
|
try {
|
||||||
"input",
|
redirects = await fetch("/redirects.json", {
|
||||||
{
|
headers: { authorization: `Bearer ${redirKey}` },
|
||||||
type: "text",
|
})
|
||||||
name: "path",
|
.then((r) => r.json())
|
||||||
id: "path",
|
.catch();
|
||||||
placeholder: "my-cool-link",
|
} catch (e) {
|
||||||
},
|
status.textContent =
|
||||||
[]
|
"You are not authorized to fetch the list of redirects.";
|
||||||
),
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
h("label", { for: "url" }, ["target url"]),
|
status.style.display = "none";
|
||||||
h(
|
|
||||||
"input",
|
|
||||||
{
|
|
||||||
type: "url",
|
|
||||||
name: "url",
|
|
||||||
id: "url",
|
|
||||||
placeholder: "https://…",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
),
|
|
||||||
|
|
||||||
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) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -86,9 +91,13 @@ const setupForm = (redirKey) => {
|
||||||
q("main").appendChild(form);
|
q("main").appendChild(form);
|
||||||
};
|
};
|
||||||
|
|
||||||
r(async () => {
|
onReady(async () => {
|
||||||
const redirKey = window.localStorage.getItem("redirect-key");
|
const redirKey = window.localStorage.getItem("char.lt/redirect-key");
|
||||||
if (redirKey == null) return;
|
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>
|
<title>shorten - char.lt</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/layout.css" />
|
<link rel="stylesheet" href="/css/shortlinks-layout.css" />
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/aesthetic.css" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="/inter/inter.css" />
|
<link rel="stylesheet" href="/inter/inter.css" />
|
||||||
</head>
|
</head>
|
||||||
|
@ -14,29 +14,60 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<main>
|
<main>
|
||||||
<h1>redirects</h1>
|
<h1>Redirects</h1>
|
||||||
|
|
||||||
<p>this is where you can manage the redirects for the site.</p>
|
<p>this is where you can manage the redirects for the site.</p>
|
||||||
|
<noscript>if you have javascript turned on.</noscript>
|
||||||
|
|
||||||
<table>
|
<section id="status-message">Loading...</section>
|
||||||
<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>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
<script type="module" src="/js/shorten.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue