feat: consistent styling

This commit is contained in:
Per Stark
2024-12-28 23:54:58 +01:00
parent bdf7a12e9c
commit 0110ffa5bf
18 changed files with 256 additions and 209 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
assets/icon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -1007,6 +1007,16 @@ html {
}
@media (hover: hover) {
.btm-nav > *.disabled:hover,
.btm-nav > *[disabled]:hover {
pointer-events: none;
--tw-border-opacity: 0;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-bg-opacity: 0.1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
--tw-text-opacity: 0.2;
}
.btn:hover {
--tw-border-opacity: 1;
border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));
@@ -1278,6 +1288,16 @@ html {
align-items: center;
}
.btm-nav > *.disabled,
.btm-nav > *[disabled] {
pointer-events: none;
--tw-border-opacity: 0;
background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));
--tw-bg-opacity: 0.1;
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
--tw-text-opacity: 0.2;
}
.btm-nav > * .label {
font-size: 1rem;
line-height: 1.5rem;
@@ -1304,6 +1324,14 @@ html {
.btn-primary {
--btn-color: var(--fallback-p);
}
.btn-secondary {
--btn-color: var(--fallback-s);
}
.btn-error {
--btn-color: var(--fallback-er);
}
}
@supports (color: color-mix(in oklab, black, black)) {
@@ -1359,6 +1387,26 @@ html {
.btn-primary {
--btn-color: var(--p);
}
.btn-secondary {
--btn-color: var(--s);
}
.btn-error {
--btn-color: var(--er);
}
}
.btn-secondary {
--tw-text-opacity: 1;
color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)));
outline-color: var(--fallback-s,oklch(var(--s)/1));
}
.btn-error {
--tw-text-opacity: 1;
color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
outline-color: var(--fallback-er,oklch(var(--er)/1));
}
.btn.glass {
@@ -1683,27 +1731,6 @@ html {
outline-offset: 2px;
}
.loading {
pointer-events: none;
display: inline-block;
aspect-ratio: 1 / 1;
width: 1.5rem;
background-color: currentColor;
-webkit-mask-size: 100%;
mask-size: 100%;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
}
.loading-spinner {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
}
:where(.menu li:empty) {
--tw-bg-opacity: 1;
background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));
@@ -1897,11 +1924,6 @@ html {
}
}
[type="checkbox"].checkbox-sm {
height: 1.25rem;
width: 1.25rem;
}
.menu-horizontal {
display: inline-flex;
flex-direction: row;
@@ -1950,14 +1972,6 @@ html {
margin-bottom: 2rem;
}
.mr-2 {
margin-right: 0.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-4 {
margin-top: 1rem;
}
@@ -1986,10 +2000,22 @@ html {
min-height: 100vh;
}
.min-h-full {
min-height: 100%;
}
.w-full {
width: 100%;
}
.min-w-20 {
min-width: 5rem;
}
.min-w-40 {
min-width: 10rem;
}
.max-w-2xl {
max-width: 42rem;
}
@@ -2010,6 +2036,10 @@ html {
flex: none;
}
.flex-grow {
flex-grow: 1;
}
.cursor-pointer {
cursor: pointer;
}
@@ -2030,12 +2060,20 @@ html {
align-items: center;
}
.justify-start {
justify-content: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
@@ -2044,32 +2082,16 @@ html {
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
.space-y-8 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(2rem * var(--tw-space-y-reverse));
}
.rounded-t-none {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.border-transparent {
border-color: transparent;
}
.bg-base-100 {
--tw-bg-opacity: 1;
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity, 1)));
}
.bg-base-200 {
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity, 1)));
@@ -2099,25 +2121,11 @@ html {
background-clip: text;
}
.p-2 {
padding: 0.5rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.text-center {
text-align: center;
}
@@ -2156,11 +2164,6 @@ html {
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
}
.text-primary {
--tw-text-opacity: 1;
color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity, 1)));
}
.text-transparent {
color: transparent;
}

6
flake.lock generated
View File

@@ -511,11 +511,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1728492678,
"narHash": "sha256-9UTxR8eukdg+XZeHgxW5hQA9fIKHsKCdOIUycTryeVw=",
"lastModified": 1735291276,
"narHash": "sha256-NYVcA06+blsLG6wpAbSPTCyLvxD/92Hy4vlY9WxFI1M=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5633bcff0c6162b9e4b5f1264264611e950c8ec7",
"rev": "634fd46801442d760e09493a794c4f15db2d0cbb",
"type": "github"
},
"original": {

View File

@@ -1,7 +1,7 @@
use axum::{
extract::State,
http::{StatusCode, Uri},
response::{IntoResponse, Redirect},
response::{Html, IntoResponse, Redirect},
Form,
};
use axum_htmx::{HxBoosted, HxRedirect};
@@ -52,10 +52,18 @@ pub async fn authenticate_user(
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<SignupParams>,
) -> Result<impl IntoResponse, ApiError> {
let user = User::authenticate(form.email, form.password, &state.surreal_db_client).await?;
let user = match User::authenticate(form.email, form.password, &state.surreal_db_client).await {
Ok(user) => user,
Err(_) => {
return Ok(Html("<p>Invalid email or password.</p>").into_response());
}
};
auth.login_user(user.id);
if form.remember_me.is_some_and(|string| string == *"on") {
auth.remember_user(true);
}
Ok((HxRedirect::from(Uri::from_static("/")), StatusCode::OK).into_response())
}

View File

@@ -1,9 +1,10 @@
use axum::{
extract::State,
response::{IntoResponse, Redirect},
http::{StatusCode, Uri},
response::{Html, IntoResponse, Redirect},
Form,
};
use axum_htmx::HxBoosted;
use axum_htmx::{HxBoosted, HxRedirect};
use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use serde::{Deserialize, Serialize};
@@ -50,7 +51,14 @@ pub async fn process_signup_and_show_verification(
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<SignupParams>,
) -> Result<impl IntoResponse, ApiError> {
let user = User::create_new(form.email, form.password, &state.surreal_db_client).await?;
let user = match User::create_new(form.email, form.password, &state.surreal_db_client).await {
Ok(user) => user,
Err(_) => {
return Ok(Html("<p>User already exists</p>").into_response());
}
};
auth.login_user(user.id);
Ok(())
Ok((HxRedirect::from(Uri::from_static("/")), StatusCode::OK).into_response())
}

View File

@@ -1,49 +1,47 @@
{% extends "body_base.html" %}
{% block content %}
{% block main %}
<style>
form.htmx-request {
opacity: 0.5;
}
</style>
<div class="min-h-screen grid place-items-center place-content-center">
<div class="max-w-lg mx-auto">
<h2 class="text-2xl font-bold text-center mb-8">Account Settings</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" name="email" value="{{ user.email }}" class="input input-bordered w-full" disabled />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">API key</span>
</label>
{% block api_key_section %}
{% if user.api_key %}
<input type="text" name="api-key" value="{{ user.api_key }}" class="input input-bordered w-full" disabled />
{% else %}
<button hx-post="/set-api-key" class="btn btn-secondary w-full" hx-swap="outerHTML">
Create API-Key
</button>
{% endif %}
{% endblock %}
</div>
<div class="form-control mt-4">
<button hx-post="/verify-email" class="btn btn-secondary w-full">
Verify Email
</button>
</div>
<div class="form-control mt-4">
<button hx-get="/change-password" class="btn btn-primary w-full">
Change Password
</button>
</div>
<div class="form-control mt-4">
<button hx-delete="/delete-account" class="btn btn-error w-full">
Delete Account
</button>
</div>
<div id="account-result" class="mt-4"></div>
<div class="max-w-lg w-full">
<h2 class="text-2xl font-bold text-center mb-8">Account Settings</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" name="email" value="{{ user.email }}" class="input input-bordered w-full" disabled />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">API key</span>
</label>
{% block api_key_section %}
{% if user.api_key %}
<input type="text" name="api-key" value="{{ user.api_key }}" class="input input-bordered w-full" disabled />
{% else %}
<button hx-post="/set-api-key" class="btn btn-secondary w-full" hx-swap="outerHTML">
Create API-Key
</button>
{% endif %}
{% endblock %}
</div>
<div class="form-control mt-4">
<button hx-post="/verify-email" class="btn btn-secondary w-full">
Verify Email
</button>
</div>
<div class="form-control mt-4">
<button hx-get="/change-password" class="btn btn-primary w-full">
Change Password
</button>
</div>
<div class="form-control mt-4">
<button hx-delete="/delete-account" class="btn btn-error w-full">
Delete Account
</button>
</div>
<div id="account-result" class="mt-4"></div>
</div>
{% endblock %}

View File

@@ -5,41 +5,41 @@
opacity: 0.5;
}
</style>
<div class="min-h-screen grid place-items-center place-content-center">
<div class="max-w-lg mx-auto">
<h2 class="text-2xl font-bold text-center mb-8">Login to your account</h2>
<form hx-post="/signin">
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" name="email" placeholder="Enter your email" class="input input-bordered w-full" required />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Password</span>
</label>
<input type="password" name="password" placeholder="Enter your password" class="input input-bordered w-full"
required minlength="8" />
</div>
<div class="form-control mt-4">
<label class="label cursor-pointer justify-start gap-2">
<input type="checkbox" name="remember_me" class="checkbox checkbox-primary" />
<span class="label-text">Remember me</span>
</label>
</div>
<div class="form-control mt-6">
<button id="submit-btn" class="btn btn-primary w-full">
Login
</button>
</div>
<div id="login-result"></div>
</form>
<div class="divider">OR</div>
<div class="text-center text-sm">
Don't have an account?
<a href="/signup" class="link link-primary">Sign up</a>
<div class="min-h-screen container mx-auto max-w-md flex justify-center flex-col">
<h2 class="text-2xl font-bold text-center mb-8">Login to your account</h2>
<form hx-post="/signin" hx-target="#login-result">
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" name="email" placeholder="Enter your email" class="input input-bordered w-full" required />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Password</span>
</label>
<input type="password" name="password" placeholder="Enter your password" class="input input-bordered w-full"
required minlength="8" />
</div>
<div class="form-control mt-4">
<label class="label cursor-pointer justify-start gap-4">
<input type="checkbox" name="remember_me" class="checkbox checkbox-primary" />
<span class="label-text">Remember me</span>
</label>
</div>
<div class="mt-4" id="login-result"></div>
<div class="form-control mt-6">
<button id="submit-btn" class="btn btn-primary w-full">
Login
</button>
</div>
</form>
<div class="divider">OR</div>
<div class="text-center text-sm">
Don't have an account?
<a href="/signup" hx-boost="true" class="link link-primary">Sign up</a>
</div>
</div>
{% endblock %}

View File

@@ -6,41 +6,39 @@
opacity: 0.5;
}
</style>
<div class="min-h-screen grid place-items-center place-content-center">
<div class="max-w-lg mx-auto">
<h2 class="text-2xl font-bold text-center mb-8">Create your account</h2>
<form hx-post="/signup" hx-target="#signup-result" class="">
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" name="email" placeholder="Enter your email" class="input input-bordered w-full" required />
</div>
<div class="min-h-screen container mx-auto max-w-md flex justify-center flex-col">
<h2 class="text-2xl font-bold text-center mb-8">Create your account</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Password</span>
</label>
<input type="password" name="password" placeholder="Create a password" class="input input-bordered w-full"
required minlength="8" />
</div>
<div class="form-control mt-6">
<button id="submit-btn" class="btn btn-primary w-full">
Create Account
</button>
</div>
<div id="signup-result"></div>
</form>
<div class="divider">OR</div>
<div class="text-center text-sm">
Already have an account?
<a href="/signin" class="link link-primary">Sign in</a>
<form hx-post="/signup" hx-target="#signup-result" class="">
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input type="email" name="email" placeholder="Enter your email" class="input input-bordered w-full" required />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Password</span>
</label>
<input type="password" name="password" placeholder="Create a password" class="input input-bordered w-full"
required minlength="8" />
</div>
<div class="mt-4" id="signup-result"></div>
<div class="form-control mt-6">
<button id="submit-btn" class="btn btn-primary w-full">
Create Account
</button>
</div>
</form>
<div class="divider">OR</div>
<div class="text-center text-sm">
Already have an account?
<a href="/signin" hx-boost="true" class="link link-primary">Sign in</a>
</div>
</div>
{% endblock %}

View File

@@ -1,27 +1,30 @@
{% extends "head_base.html" %}
{% block body %}
<body class="min-h-screen">
<nav class="navbar bg-base-200">
<div class="flex-1">
<a class="btn text-xl border-transparent btn-outline btn-primary" href="/">Minne</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1">
{% if user %}
<li><a hx-boost="true" href="/account">Account</a></li>
<li><a hx-boost="true" href="/signout">Sign out</a></li>
{% else %}
<li><a hx-boost="true" href="/signin">Login</a></li>
<li><a hx-boost="true" href="/signup">Sign up</a></li>
{% endif %}
</ul>
</div>
</nav>
<body>
<div class="min-h-screen flex flex-col">
<!-- Navbar -->
<nav class="navbar bg-base-200">
<div class="flex-1">
<a class="btn text-xl border-transparent btn-outline btn-primary" href="/">Minne</a>
</div>
<div>
<ul class="menu menu-horizontal px-1">
{% if user %}
<li><a hx-boost="true" href="/account">Account</a></li>
<li><a hx-boost="true" href="/signout">Sign out</a></li>
{% else %}
<li><a hx-boost="true" href="/signin">Login</a></li>
<li><a hx-boost="true" href="/signup">Sign up</a></li>
{% endif %}
</ul>
</div>
</nav>
<main class="container mx-auto ">
{% block content %}{% endblock %}
</main>
<!-- Main Content -->
<main class="flex-grow flex items-center justify-center">
{% block main %}{% endblock %}
</main>
</div>
</body>
{% endblock %}

27
templates/body_base.jinja Normal file
View File

@@ -0,0 +1,27 @@
{% extends "head_base.html" %}
{% block body %}
<body class="min-h-screen">
<nav class="navbar bg-base-200">
<div class="flex-1">
<a class="btn text-xl border-transparent btn-outline btn-primary" href="/">Minne</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1">
{% if user %}
<li><a hx-boost="true" href="/account">Account</a></li>
<li><a hx-boost="true" href="/signout">Sign out</a></li>
{% else %}
<li><a hx-boost="true" href="/signin">Login</a></li>
<li><a hx-boost="true" href="/signup">Sign up</a></li>
{% endif %}
</ul>
</div>
</nav>
<main class="container mx-auto ">
{% block content %}{% endblock %}
</main>
</body>
{% endblock %}

View File

@@ -11,7 +11,8 @@
<!-- Optional but recommended for iOS support -->
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="/assets/icons/icon-192x192.png">
<link rel="icon" href="/assets/icon/favicon.ico">
<link rel="apple-touch-icon" href="/assets/icon/apple-touch-icon.png">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
{% block head %}{% endblock %}
</head>

View File

@@ -1,5 +1,5 @@
{% extends "body_base.html" %}
{% block content %}
{% block main %}
<div class="flex flex-col items-center justify-center min-h-[80vh] space-y-8">
<!-- Hero Section -->
<div class="text-center space-y-4 mb-8">