feat: modal component, edit functionality

This commit is contained in:
Per Stark committed 2025-02-11 13:46:17 +01:00
1 parent aa8574f4ce
commit f92e97e541
9 files changed
+171 -10

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
/*! tailwindcss v4.0.0-beta.9 | MIT License | https://tailwindcss.com */
@layer theme{:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-serif:ui-serif,Georgia,Cambria,"Times New Roman",Times,serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(.971 .013 17.38);--color-red-100:oklch(.936 .032 17.717);--color-red-200:oklch(.885 .062 18.334);--color-red-300:oklch(.808 .114 19.571);--color-red-400:oklch(.704 .191 22.216);--color-red-500:oklch(.637 .237 25.331);--color-red-600:oklch(.577 .245 27.325);--color-red-700:oklch(.505 .213 27.518);--color-red-800:oklch(.444 .177 26.899);--color-red-900:oklch(.396 .141 25.723);--color-red-950:oklch(.258 .092 26.042);--color-orange-50:oklch(.98 .016 73.684);--color-orange-100:oklch(.954 .038 75.164);--color-orange-200:oklch(.901 .076 70.697);--color-orange-300:oklch(.837 .128 66.29);--color-orange-400:oklch(.75 .183 55.934);--color-orange-500:oklch(.705 .213 47.604);--color-orange-600:oklch(.646 .222 41.116);--color-orange-700:oklch(.553 .195 38.402);--color-orange-800:oklch(.47 .157 37.304);--color-orange-900:oklch(.408 .123 38.172);--color-orange-950:oklch(.266 .079 36.259);--color-amber-50:oklch(.987 .022 95.277);--color-amber-100:oklch(.962 .059 95.617);--color-amber-200:oklch(.924 .12 95.746);--color-amber-300:oklch(.879 .169 91.605);--color-amber-400:oklch(.828 .189 84.429);--color-amber-500:oklch(.769 .188 70.08);--color-amber-600:oklch(.666 .179 58.318);--color-amber-700:oklch(.555 .163 48.998);--color-amber-800:oklch(.473 .137 46.201);--color-amber-900:oklch(.414 .112 45.904);--color-amber-950:oklch(.279 .077 45.635);--color-yellow-50:oklch(.987 .026 102.212);--color-yellow-100:oklch(.973 .071 103.193);--color-yellow-200:oklch(.945 .129 101.54);--color-yellow-300:oklch(.905 .182 98.111);--color-yellow-400:oklch(.852 .199 91.936);--color-yellow-500:oklch(.795 .184 86.047);--color-yellow-600:oklch(.681 .162 75.834);--color-yellow-700:oklch(.554 .135 66.442);--color-yellow-800:oklch(.476 .114 61.907);--color-yellow-900:oklch(.421 .095 57.708);--color-yellow-950:oklch(.286 .066 53.813);--color-lime-50:oklch(.986 .031 120.757);--color-lime-100:oklch(.967 .067 122.328);--color-lime-200:oklch(.938 .127 124.321);--color-lime-300:oklch(.897 .196 126.665);--color-lime-400:oklch(.841 .238 128.85);--color-lime-500:oklch(.768 .233 130.85);--color-lime-600:oklch(.648 .2 131.684);--color-lime-700:oklch(.532 .157 131.589);--color-lime-800:oklch(.453 .124 130.933);--color-lime-900:oklch(.405 .101 131.063);--color-lime-950:oklch(.274 .072 132.109);--color-green-50:oklch(.982 .018 155.826);--color-green-100:oklch(.962 .044 156.743);--color-green-200:oklch(.925 .084 155.995);--color-green-300:oklch(.871 .15 154.449);--color-green-400:oklch(.792 .209 151.711);--color-green-500:oklch(.723 .219 149.579);--color-green-600:oklch(.627 .194 149.214);--color-green-700:oklch(.527 .154 150.069);--color-green-800:oklch(.448 .119 151.328);--color-green-900:oklch(.393 .095 152.535);--color-green-950:oklch(.266 .065 152.934);--color-emerald-50:oklch(.979 .021 166.113);--color-emerald-100:oklch(.95 .052 163.051);--color-emerald-200:oklch(.905 .093 164.15);--color-emerald-300:oklch(.845 .143 164.978);--color-emerald-400:oklch(.765 .177 163.223);--color-emerald-500:oklch(.696 .17 162.48);--color-emerald-600:oklch(.596 .145 163.225);--color-emerald-700:oklch(.508 .118 165.612);--color-emerald-800:oklch(.432 .095 166.913);--color-emerald-900:oklch(.378 .077 168.94);--color-emerald-950:oklch(.262 .051 172.552);--color-teal-50:oklch(.984 .014 180.72);--color-teal-100:oklch(.953 .051 180.801);--color-teal-200:oklch(.91 .096 180.426);--color-teal-300:oklch(.855 .138 181.071);--color-teal-400:oklch(.777 .152 181.912);--color-teal-500:oklch(.704 .14 182.503);--color-teal-600:oklch(.6 .118 184.704);--color-teal-700:oklch(.511 .096 186.391);--color-teal-800:oklch(.437 .078 188.216);--color-teal-900:oklch(.386 .063 188.416);--color-teal-950:oklch(.277 .046 192.524);--color-cyan-50:oklch(.984 .019 200.873);--color-cyan-100:oklch(.956 .045 203.388);--color-cyan-200:oklch(.917 .08 205.041);--color-cyan-300:oklch(.865 .127 207.078);--color-cyan-400:oklch(.789 .154 211.53);--color-cyan-500:oklch(.715 .143 215.221);--color-cyan-600:oklch(.609 .126 221.723);--color-cyan-700:oklch(.52 .105 223.128);--color-cyan-800:oklch(.45 .085 224.283);--color-cyan-900:oklch(.398 .07 227.392);--color-cyan-950:oklch(.302 .056 229.695);--color-sky-50:oklch(.977 .013 236.62);--color-sky-100:oklch(.951 .026 236.824);--color-sky-200:oklch(.901 .058 230.902);--color-sky-300:oklch(.828 .111 230.318);--color-sky-400:oklch(.746 .16 232.661);--color-sky-500:oklch(.685 .169 237.323);--color-sky-600:oklch(.588 .158 241.966);--color-sky-700:oklch(.5 .134 242.749);--color-sky-800:oklch(.443 .11 240.79);--color-sky-900:oklch(.391 .09 240.876);--color-sky-950:oklch(.293 .066 243.157);--color-blue-50:oklch(.97 .014 254.604);--color-blue-100:oklch(.932 .032 255Line truncated
@layer theme{:root{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-serif:ui-serif,Georgia,Cambria,"Times New Roman",Times,serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(.971 .013 17.38);--color-red-100:oklch(.936 .032 17.717);--color-red-200:oklch(.885 .062 18.334);--color-red-300:oklch(.808 .114 19.571);--color-red-400:oklch(.704 .191 22.216);--color-red-500:oklch(.637 .237 25.331);--color-red-600:oklch(.577 .245 27.325);--color-red-700:oklch(.505 .213 27.518);--color-red-800:oklch(.444 .177 26.899);--color-red-900:oklch(.396 .141 25.723);--color-red-950:oklch(.258 .092 26.042);--color-orange-50:oklch(.98 .016 73.684);--color-orange-100:oklch(.954 .038 75.164);--color-orange-200:oklch(.901 .076 70.697);--color-orange-300:oklch(.837 .128 66.29);--color-orange-400:oklch(.75 .183 55.934);--color-orange-500:oklch(.705 .213 47.604);--color-orange-600:oklch(.646 .222 41.116);--color-orange-700:oklch(.553 .195 38.402);--color-orange-800:oklch(.47 .157 37.304);--color-orange-900:oklch(.408 .123 38.172);--color-orange-950:oklch(.266 .079 36.259);--color-amber-50:oklch(.987 .022 95.277);--color-amber-100:oklch(.962 .059 95.617);--color-amber-200:oklch(.924 .12 95.746);--color-amber-300:oklch(.879 .169 91.605);--color-amber-400:oklch(.828 .189 84.429);--color-amber-500:oklch(.769 .188 70.08);--color-amber-600:oklch(.666 .179 58.318);--color-amber-700:oklch(.555 .163 48.998);--color-amber-800:oklch(.473 .137 46.201);--color-amber-900:oklch(.414 .112 45.904);--color-amber-950:oklch(.279 .077 45.635);--color-yellow-50:oklch(.987 .026 102.212);--color-yellow-100:oklch(.973 .071 103.193);--color-yellow-200:oklch(.945 .129 101.54);--color-yellow-300:oklch(.905 .182 98.111);--color-yellow-400:oklch(.852 .199 91.936);--color-yellow-500:oklch(.795 .184 86.047);--color-yellow-600:oklch(.681 .162 75.834);--color-yellow-700:oklch(.554 .135 66.442);--color-yellow-800:oklch(.476 .114 61.907);--color-yellow-900:oklch(.421 .095 57.708);--color-yellow-950:oklch(.286 .066 53.813);--color-lime-50:oklch(.986 .031 120.757);--color-lime-100:oklch(.967 .067 122.328);--color-lime-200:oklch(.938 .127 124.321);--color-lime-300:oklch(.897 .196 126.665);--color-lime-400:oklch(.841 .238 128.85);--color-lime-500:oklch(.768 .233 130.85);--color-lime-600:oklch(.648 .2 131.684);--color-lime-700:oklch(.532 .157 131.589);--color-lime-800:oklch(.453 .124 130.933);--color-lime-900:oklch(.405 .101 131.063);--color-lime-950:oklch(.274 .072 132.109);--color-green-50:oklch(.982 .018 155.826);--color-green-100:oklch(.962 .044 156.743);--color-green-200:oklch(.925 .084 155.995);--color-green-300:oklch(.871 .15 154.449);--color-green-400:oklch(.792 .209 151.711);--color-green-500:oklch(.723 .219 149.579);--color-green-600:oklch(.627 .194 149.214);--color-green-700:oklch(.527 .154 150.069);--color-green-800:oklch(.448 .119 151.328);--color-green-900:oklch(.393 .095 152.535);--color-green-950:oklch(.266 .065 152.934);--color-emerald-50:oklch(.979 .021 166.113);--color-emerald-100:oklch(.95 .052 163.051);--color-emerald-200:oklch(.905 .093 164.15);--color-emerald-300:oklch(.845 .143 164.978);--color-emerald-400:oklch(.765 .177 163.223);--color-emerald-500:oklch(.696 .17 162.48);--color-emerald-600:oklch(.596 .145 163.225);--color-emerald-700:oklch(.508 .118 165.612);--color-emerald-800:oklch(.432 .095 166.913);--color-emerald-900:oklch(.378 .077 168.94);--color-emerald-950:oklch(.262 .051 172.552);--color-teal-50:oklch(.984 .014 180.72);--color-teal-100:oklch(.953 .051 180.801);--color-teal-200:oklch(.91 .096 180.426);--color-teal-300:oklch(.855 .138 181.071);--color-teal-400:oklch(.777 .152 181.912);--color-teal-500:oklch(.704 .14 182.503);--color-teal-600:oklch(.6 .118 184.704);--color-teal-700:oklch(.511 .096 186.391);--color-teal-800:oklch(.437 .078 188.216);--color-teal-900:oklch(.386 .063 188.416);--color-teal-950:oklch(.277 .046 192.524);--color-cyan-50:oklch(.984 .019 200.873);--color-cyan-100:oklch(.956 .045 203.388);--color-cyan-200:oklch(.917 .08 205.041);--color-cyan-300:oklch(.865 .127 207.078);--color-cyan-400:oklch(.789 .154 211.53);--color-cyan-500:oklch(.715 .143 215.221);--color-cyan-600:oklch(.609 .126 221.723);--color-cyan-700:oklch(.52 .105 223.128);--color-cyan-800:oklch(.45 .085 224.283);--color-cyan-900:oklch(.398 .07 227.392);--color-cyan-950:oklch(.302 .056 229.695);--color-sky-50:oklch(.977 .013 236.62);--color-sky-100:oklch(.951 .026 236.824);--color-sky-200:oklch(.901 .058 230.902);--color-sky-300:oklch(.828 .111 230.318);--color-sky-400:oklch(.746 .16 232.661);--color-sky-500:oklch(.685 .169 237.323);--color-sky-600:oklch(.588 .158 241.966);--color-sky-700:oklch(.5 .134 242.749);--color-sky-800:oklch(.443 .11 240.79);--color-sky-900:oklch(.391 .09 240.876);--color-sky-950:oklch(.293 .066 243.157);--color-blue-50:oklch(.97 .014 254.604);--color-blue-100:oklch(.932 .032 255Line truncated
+10 -1
View File
@@ -36,7 +36,10 @@ use zettle_db::{
gdpr::{accept_gdpr, deny_gdpr},
index::{delete_job, delete_text_content, index_handler},
ingress_form::{hide_ingress_form, process_ingress_form, show_ingress_form},
knowledge::show_knowledge_page,
knowledge::{
delete_knowledge_entity, patch_knowledge_entity,
show_edit_knowledge_entity_form, show_knowledge_page,
},
search_result::search_result_handler,
signin::{authenticate_user, show_signin_form},
signout::sign_out_user,
@@ -174,6 +177,12 @@ fn html_routes(
.route("/text-content/:id", delete(delete_text_content))
.route("/jobs/:job_id", delete(delete_job))
.route("/knowledge", get(show_knowledge_page))
.route(
"/knowledge-entity/:id",
get(show_edit_knowledge_entity_form)
.delete(delete_knowledge_entity)
.patch(patch_knowledge_entity),
)
.route("/account", get(show_account_page))
.route("/admin", get(show_admin_panel))
.route("/toggle-registrations", patch(toggle_registration_status))
+85
View File
@@ -1,6 +1,7 @@
use axum::{
extract::{Path, State},
response::{IntoResponse, Redirect},
Form,
};
use axum_session::Session;
use axum_session_auth::AuthSession;
@@ -148,3 +149,87 @@ pub async fn show_knowledge_page(
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct EntityData {
entity: KnowledgeEntity,
user: User,
}
pub async fn show_edit_knowledge_entity_form(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
// SORT OUT ERROR HANDLING AND VALIDATE INPUT
let entity = get_item(&state.surreal_db_client, &id)
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
let output = render_template(
"knowledge/edit_knowledge_entity_modal.html",
EntityData {
entity: entity.unwrap(),
user,
},
state.templates,
)?;
Ok(output.into_response())
}
#[derive(Serialize)]
pub struct EntityListData {
entities: Vec<KnowledgeEntity>,
user: User,
}
#[derive(Debug, Deserialize)]
pub struct PatchKnowledgeEntityParams {
name: String,
description: String,
}
pub async fn patch_knowledge_entity(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<PatchKnowledgeEntityParams>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
info!("{:#?}", form);
let entities = User::get_knowledge_entities(&user.id, &state.surreal_db_client)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
"knowledge/entity_list.html",
EntityListData { entities, user },
state.templates,
)?;
Ok(output.into_response())
}
pub async fn delete_knowledge_entity(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
Ok("Thanks".into_response())
}
+3 -3
View File
@@ -5,19 +5,19 @@
<div class="stats stats-vertical lg:stats-horizontal shadow">
<div class="stat">
<div class="stat-title">Page loads</div>
<div class="stat-title font-bold">Page loads</div>
<div class="stat-value text-secondary">{{analytics.page_loads}}</div>
<div class="stat-desc">Amount of page loads</div>
</div>
<div class="stat">
<div class="stat-title">Unique visitors</div>
<div class="stat-title font-bold">Unique visitors</div>
<div class="stat-value text-primary">{{analytics.visitors}}</div>
<div class="stat-desc">Amount of unique visitors</div>
</div>
<div class="stat">
<div class="stat-title">Users</div>
<div class="stat-title font-bold">Users</div>
<div class="stat-value text-accent">{{users}}</div>
<div class="stat-desc">Amount of registered users</div>
</div>
+2
View File
@@ -8,6 +8,8 @@
<!-- Main Content -->
{% block main %}{% endblock %}
<div id="modal"></div>
</div>
</body>
{% endblock %}
+2 -2
View File
@@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}Minne{% endblock %}</title>
<!-- <meta http-equiv="refresh" content="4"> -->
<!-- <meta http-equiv=" refresh" content="4"> -->
<!-- Preload critical assets -->
<link rel="preload" href="/assets/htmx.min.js" as="script">
@@ -0,0 +1,31 @@
{% extends "modal_base.html" %}
{% block form_attributes %}
hx-patch="/knowledge-entity/{{entity.id}}"
hx-target="#entity-list"
hx-swap="outerHTML"
{% endblock %}
{% block modal_content %}
<h3 class="text-lg font-bold mb-4">Edit Entity</h3>
<div class="form-control">
<label class="label">
<span class="label-text">Entity Name</span>
</label>
<input type="text" name="name" value="{{ entity.name }}" class="input input-bordered">
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Description</span>
</label>
<textarea name="description" class="textarea textarea-bordered h-32">{{ entity.description }}</textarea>
</div>
{% endblock %}
{% block primary_actions %}
<button type="submit" class="btn btn-primary">
Save Changes
</button>
{% endblock %}
+4 -3
View File
@@ -1,4 +1,4 @@
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-4" id="entity_list">
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-4" id="entity-list">
{% for entity in entities %}
<div class="card min-w-72 bg-base-100 shadow">
<div class="card-body">
@@ -8,10 +8,11 @@
<div class="flex justify-between items-center">
<p>{{entity.updated_at | datetimeformat(format="short", tz=user.timezeone)}}</p>
<div>
<button hx-patch="/knowledge-entity/{{entity.id}}" class="btn btn-square btn-ghost btn-sm">
<button hx-get="/knowledge-entity/{{entity.id}}" hx-target="#modal" hx-swap="innerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/edit_icon.html" %}
</button>
<button hx-delete="/knowledge-entity/{{entity.id}}" hx-target="#entity_list" hx-swap="outerHTML"
<button hx-delete="/knowledge-entity/{{entity.id}}" hx-target="#entity-list" hx-swap="outerHTML"
class="btn btn-square btn-ghost btn-sm">
{% include "icons/delete_icon.html" %}
</button>
+33
View File
@@ -0,0 +1,33 @@
<dialog id="body_modal" class="modal">
<div class="modal-box">
<form id="modal_form" {% block form_attributes %}{% endblock %}>
{% block modal_content %}
<!-- Form fields go here in child templates -->
{% endblock %}
<div class="modal-action">
<!-- Close button (always visible) -->
<button type="button" class="btn" onclick="document.getElementById('body_modal').close()">
Close
</button>
<!-- Primary actions block -->
{% block primary_actions %}
<!-- Submit/Save buttons go here in child templates -->
{% endblock %}
</div>
</form>
</div>
<script>
// Auto-open modal when injected
document.getElementById('body_modal').showModal();
// Close modal on successful form submission
document.getElementById('modal_form').addEventListener('htmx:afterRequest', (evt) => {
if (evt.detail.successful) {
document.getElementById('body_modal').close();
}
});
</script>
</dialog>