add: finish ui rewrite

This commit is contained in:
trisua 2025-06-01 12:25:33 -04:00
parent e9846016e6
commit 5dec98d698
119 changed files with 8776 additions and 9350 deletions

View file

@ -1,9 +0,0 @@
{% extends "root.html" %} {% block body %}
<main class="flex flex-col gap-2" style="max-width: 25rem">
<h2 class="w-full text-center">{% block title %}{% endblock %}</h2>
<div class="card w-full flex flex-col gap-4 justify-center align-center">
{% block content %}{% endblock %}
</div>
{% block footer %}{% endblock %}
</main>
{% endblock %}

View file

@ -0,0 +1,16 @@
(text "{% extends \"root.html\" %} {% block body %}")
(main
("class" "flex flex-col gap-2")
("style" "max-width: 25rem")
(h2
("class" "w-full text-center")
; block for title
(text "{% block title %}{% endblock %}"))
(div
("class" "card w-full flex flex-col gap-4 justify-center align-center")
; block for actual page content
(text "{% block content %}{% endblock %}"))
; small footer block (for switching context)
(text "{% block footer %}{% endblock %}"))
(text "{% endblock %}")

View file

@ -1,75 +0,0 @@
{% extends "auth/base.html" %} {% block head %}
<title>Connection</title>
{% endblock %} {% block title %}Connection{% endblock %} {% block content %}
<div class="w-full flex-col gap-2" id="status"><b>Working...</b></div>
{% if connection_type == "Spotify" and user and user.connections.Spotify and
config.connections.spotify_client_id %}
<script>
setTimeout(async () => {
const code = new URLSearchParams(window.location.search).get("code");
const client_id = "{{ config.connections.spotify_client_id }}";
const verifier = "{{ user.connections.Spotify[0].data.verifier }}";
if (!code) {
alert("Connection failed (did not get code from Spotify)");
return;
}
const [token, refresh_token, expires_in] = await trigger(
"spotify::get_token",
[client_id, verifier, code],
);
const profile = await trigger("spotify::profile", [token]);
const { message } = await trigger("connections::push_con_data", [
"Spotify",
{
token,
refresh_token,
expires_in: expires_in.toString(),
name: profile.display_name,
url: profile.external_urls.spotify,
},
]);
document.getElementById("status").innerHTML =
`<b>${message}.</b> You can now close this tab.`;
setTimeout(() => {
window.location.href = "/settings#/connections";
}, 500);
}, 150);
</script>
{% elif connection_type == "LastFm" and user and user.connections.LastFm and
config.connections.last_fm_key %}
<script>
setTimeout(async () => {
const token = new URLSearchParams(window.location.search).get("token");
const api_key = "{{ config.connections.last_fm_key }}";
if (!token) {
alert("Connection failed (did not get token from Last.fm)");
return;
}
const res = await trigger("last_fm::get_session", [token]);
const { message } = await trigger("connections::push_con_data", [
"LastFm",
{
session_token: res.session.key,
name: res.session.name,
url: `https://last.fm/user/${res.session.name}`,
},
]);
document.getElementById("status").innerHTML =
`<b>${message}.</b> You can now close this tab.`;
setTimeout(() => {
window.location.href = "/settings#/connections";
}, 500);
}, 1000);
</script>
{%- endif %} {% endblock %}

View file

@ -0,0 +1,80 @@
(text "{% extends \"auth/base.html\" %} {% block head %}")
(title
(text "Connection"))
(text "{% endblock %} {% block title %}Connection{% endblock %} {% block content %}")
(div
("class" "w-full flex-col gap-2")
("id" "status")
; display loading text because we have to wait for the data to update on remote
(b
(text "Working...")))
(text "{% if connection_type == \"Spotify\" and user and user.connections.Spotify and config.connections.spotify_client_id %}")
(script
(text "setTimeout(async () => {
const code = new URLSearchParams(window.location.search).get(\"code\");
const client_id = \"{{ config.connections.spotify_client_id }}\";
const verifier = \"{{ user.connections.Spotify[0].data.verifier }}\";
if (!code) {
alert(\"Connection failed (did not get code from Spotify)\");
return;
}
const [token, refresh_token, expires_in] = await trigger(
\"spotify::get_token\",
[client_id, verifier, code],
);
const profile = await trigger(\"spotify::profile\", [token]);
const { message } = await trigger(\"connections::push_con_data\", [
\"Spotify\",
{
token,
refresh_token,
expires_in: expires_in.toString(),
name: profile.display_name,
url: profile.external_urls.spotify,
},
]);
document.getElementById(\"status\").innerHTML =
`<b>${message}.</b> You can now close this tab.`;
setTimeout(() => {
window.location.href = \"/settings#/connections\";
}, 500);
}, 150);"))
(text "{% elif connection_type == \"LastFm\" and user and user.connections.LastFm and config.connections.last_fm_key %}")
(script
(text "setTimeout(async () => {
const token = new URLSearchParams(window.location.search).get(\"token\");
const api_key = \"{{ config.connections.last_fm_key }}\";
if (!token) {
alert(\"Connection failed (did not get token from Last.fm)\");
return;
}
const res = await trigger(\"last_fm::get_session\", [token]);
const { message } = await trigger(\"connections::push_con_data\", [
\"LastFm\",
{
session_token: res.session.key,
name: res.session.name,
url: `https://last.fm/user/${res.session.name}`,
},
]);
document.getElementById(\"status\").innerHTML =
`<b>${message}.</b> You can now close this tab.`;
setTimeout(() => {
window.location.href = \"/settings#/connections\";
}, 500);
}, 1000);"))
(text "{%- endif %} {% endblock %}")

View file

@ -1,103 +0,0 @@
{% extends "auth/base.html" %} {% block head %}
<title>Login</title>
{% endblock %} {% block title %}Login{% endblock %} {% block content %}
<form class="w-full flex flex-col gap-4" onsubmit="login(event)">
<div id="flow_1" style="display: contents">
<div class="flex flex-col gap-1">
<label for="username"><b>Username</b></label>
<input
type="text"
placeholder="username"
required
name="username"
id="username"
/>
</div>
<div class="flex flex-col gap-1">
<label for="username"><b>Password</b></label>
<input
type="password"
placeholder="password"
required
name="password"
id="password"
/>
</div>
</div>
<div id="flow_2" style="display: none">
<div class="flex flex-col gap-1">
<label for="totp"><b>TOTP code</b></label>
<input type="text" placeholder="totp code" name="totp" id="totp" />
</div>
</div>
<button>Submit</button>
</form>
<script>
let flow_page = 1;
function next_page() {
document.getElementById(`flow_${flow_page}`).style.display = "none";
flow_page += 1;
document.getElementById(`flow_${flow_page}`).style.display = "contents";
}
async function login(e) {
e.preventDefault();
if (flow_page === 1) {
// check if we need TOTP
const res = await (
await fetch(
`/api/v1/auth/user/${e.target.username.value}/totp/check`,
)
).json();
trigger("atto::toast", [res.ok ? "success" : "error", res.message]);
if (res.ok && res.payload) {
// user exists AND totp is required
return next_page();
}
}
fetch("/api/v1/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: e.target.username.value,
password: e.target.password.value,
totp: e.target.totp.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
// update tokens
const new_tokens = ns("me").LOGIN_ACCOUNT_TOKENS;
new_tokens[e.target.username.value] = res.message;
trigger("me::set_login_account_tokens", [new_tokens]);
// redirect
setTimeout(() => {
window.location.href = "/";
}, 150);
}
});
}
</script>
{% endblock %} {% block footer %}
<span class="small w-full text-center"
>Or, <a href="/auth/register">register</a></span
>
{% endblock %}

View file

@ -0,0 +1,121 @@
(text "{% extends \"auth/base.html\" %} {% block head %}")
(title
(text "Login"))
(text "{% endblock %} {% block title %}Login{% endblock %} {% block content %}")
(form
("class" "w-full flex flex-col gap-4")
("onsubmit" "login(event)")
(div
("id" "flow_1")
("style" "display: contents")
(div
("class" "flex flex-col gap-1")
(label
("for" "username")
(b
(text "Username")))
(input
("type" "text")
("placeholder" "username")
("required" "")
("name" "username")
("id" "username")))
(div
("class" "flex flex-col gap-1")
(label
("for" "username")
(b
(text "Password")))
(input
("type" "password")
("placeholder" "password")
("required" "")
("name" "password")
("id" "password"))))
(div
("id" "flow_2")
("style" "display: none")
(div
("class" "flex flex-col gap-1")
(label
("for" "totp")
(b
(text "TOTP code")))
(input
("type" "text")
("placeholder" "totp code")
("name" "totp")
("id" "totp"))))
(button
(text "Submit")))
(script
(text "let flow_page = 1;
function next_page() {
document.getElementById(`flow_${flow_page}`).style.display = \"none\";
flow_page += 1;
document.getElementById(`flow_${flow_page}`).style.display = \"contents\";
}
async function login(e) {
e.preventDefault();
if (flow_page === 1) {
// check if we need TOTP
const res = await (
await fetch(
`/api/v1/auth/user/${e.target.username.value}/totp/check`,
)
).json();
trigger(\"atto::toast\", [res.ok ? \"success\" : \"error\", res.message]);
if (res.ok && res.payload) {
// user exists AND totp is required
return next_page();
}
}
fetch(\"/api/v1/auth/login\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
username: e.target.username.value,
password: e.target.password.value,
totp: e.target.totp.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
// update tokens
const new_tokens = ns(\"me\").LOGIN_ACCOUNT_TOKENS;
new_tokens[e.target.username.value] = res.message;
trigger(\"me::set_login_account_tokens\", [new_tokens]);
// redirect
setTimeout(() => {
window.location.href = \"/\";
}, 150);
}
});
}"))
(text "{% endblock %} {% block footer %}")
(span
("class" "small w-full text-center")
(text "Or, ")
(a
("href" "/auth/register")
(text "register")))
(text "{% endblock %}")

View file

@ -1,120 +0,0 @@
{% extends "auth/base.html" %} {% block head %}
<title>Register</title>
{% endblock %} {% block title %}Register{% endblock %} {% block content %}
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
defer
></script>
<form class="w-full flex flex-col gap-4" onsubmit="register(event)">
<div class="flex flex-col gap-1">
<label for="username"><b>Username</b></label>
<input
type="text"
placeholder="username"
required
name="username"
id="username"
/>
</div>
<div class="flex flex-col gap-1">
<label for="username"><b>Password</b></label>
<input
type="password"
placeholder="password"
required
name="password"
id="password"
/>
</div>
<hr />
<div class="card-nest w-full">
<div class="card small flex items-center gap-2">
{{ icon "scroll-text" }}
<b>Policies</b>
</div>
<div class="card secondary flex flex-col gap-2">
<span>By continuing, you agree to the following policies:</span>
<ul>
<li>
<a href="{{ config.policies.terms_of_service }}"
>Terms of service</a
>
</li>
<li>
<a href="{{ config.policies.privacy }}">Privacy policy</a>
</li>
</ul>
<div class="flex gap-2">
<input
type="checkbox"
name="policy_consent"
id="policy_consent"
class="w-content"
required
/>
<label for="policy_consent">I agree</label>
</div>
</div>
</div>
<div
class="cf-turnstile"
data-sitekey="{{ config.turnstile.site_key }}"
></div>
<hr />
<button>Submit</button>
</form>
<script>
async function register(e) {
e.preventDefault();
await trigger("atto::debounce", ["users::create"]);
fetch("/api/v1/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: e.target.username.value,
password: e.target.password.value,
policy_consent: e.target.policy_consent.checked,
captcha_response: e.target.querySelector(
"[name=cf-turnstile-response]",
).value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
// update tokens
const new_tokens = ns("me").LOGIN_ACCOUNT_TOKENS;
new_tokens[e.target.username.value] = res.message;
trigger("me::set_login_account_tokens", [new_tokens]);
// redirect
setTimeout(() => {
window.location.href = "/";
}, 150);
}
});
}
</script>
{% endblock %} {% block footer %}
<span class="small w-full text-center"
>Or, <a href="/auth/login">login</a></span
>
{% endblock %}

View file

@ -0,0 +1,123 @@
(text "{% extends \"auth/base.html\" %} {% block head %}")
(title
(text "Register"))
(text "{% endblock %} {% block title %}Register{% endblock %} {% block content %}")
(script
("src" "https://challenges.cloudflare.com/turnstile/v0/api.js")
("defer" ""))
(form
("class" "w-full flex flex-col gap-4")
("onsubmit" "register(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "username")
(b
(text "Username")))
(input
("type" "text")
("placeholder" "username")
("required" "")
("name" "username")
("id" "username")))
(div
("class" "flex flex-col gap-1")
(label
("for" "username")
(b
(text "Password")))
(input
("type" "password")
("placeholder" "password")
("required" "")
("name" "password")
("id" "password")))
(hr)
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"scroll-text\" }}")
(b
(text "Policies")))
(div
("class" "card secondary flex flex-col gap-2")
(span
(text "By continuing, you agree to the following policies:"))
(ul
(li
(a
("href" "{{ config.policies.terms_of_service }}")
(text "Terms of service")))
(li
(a
("href" "{{ config.policies.privacy }}")
(text "Privacy policy"))))
(div
("class" "flex gap-2")
(input
("type" "checkbox")
("name" "policy_consent")
("id" "policy_consent")
("class" "w-content")
("required" ""))
(label
("for" "policy_consent")
(text "I agree")))))
(div
("class" "cf-turnstile")
("data-sitekey" "{{ config.turnstile.site_key }}"))
(hr)
(button
(text "Submit")))
(script
(text "async function register(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"users::create\"]);
fetch(\"/api/v1/auth/register\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
username: e.target.username.value,
password: e.target.password.value,
policy_consent: e.target.policy_consent.checked,
captcha_response: e.target.querySelector(
\"[name=cf-turnstile-response]\",
).value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
// update tokens
const new_tokens = ns(\"me\").LOGIN_ACCOUNT_TOKENS;
new_tokens[e.target.username.value] = res.message;
trigger(\"me::set_login_account_tokens\", [new_tokens]);
// redirect
setTimeout(() => {
window.location.href = \"/\";
}, 150);
}
});
}"))
(text "{% endblock %} {% block footer %}")
(span
("class" "small w-full text-center")
(text "Or, ")
(a
("href" "/auth/login")
(text "login")))
(text "{% endblock %}")