added form browser persistence

This commit is contained in:
2026-02-19 17:24:51 +01:00
parent 429a5cc051
commit 2599e491ca
7 changed files with 615 additions and 158 deletions
Generated
+431 -134
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -17,3 +17,5 @@ toml = "0.9.8"
chrono = { version = "0.4.42", features = ["serde"] } chrono = { version = "0.4.42", features = ["serde"] }
askama = "0.14.0" askama = "0.14.0"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
uuid = { version = "1.21.0", features = ["v4"] }
axum-extra = { version = "0.12.5", features = ["cookie"] }
+6
View File
@@ -7,6 +7,12 @@ form_title = "User Onboarding Survey"
# Text displayed on the primary action button # Text displayed on the primary action button
submit_button = "Complete Submission" submit_button = "Complete Submission"
# Text displayed in the success popup shown after submitting
success_message = "Success sending submission"
# Text displayed in a popup shown after submitting, in case of an error
error_message = "Error while sending submission"
# Standard single-line input # Standard single-line input
[[fields]] [[fields]]
name = "username" name = "username"
+17 -11
View File
@@ -49,6 +49,8 @@ pub struct AppConfig {
pub json_output: Option<String>, pub json_output: Option<String>,
pub form_title: String, pub form_title: String,
pub submit_button: String, pub submit_button: String,
pub success_message: String,
pub error_message: String,
pub fields: Vec<FieldDef>, pub fields: Vec<FieldDef>,
} }
@@ -65,6 +67,8 @@ pub async fn render_form(State(state): State<AppState>) -> impl IntoResponse {
struct FormTemplate<'a> { struct FormTemplate<'a> {
form_title: &'a str, form_title: &'a str,
submit_button: &'a str, submit_button: &'a str,
success_message: &'a str,
error_message: &'a str,
fields: &'a [FieldDef], fields: &'a [FieldDef],
lang: &'a str, lang: &'a str,
} }
@@ -72,6 +76,8 @@ pub async fn render_form(State(state): State<AppState>) -> impl IntoResponse {
let tmpl = FormTemplate { let tmpl = FormTemplate {
form_title: &state.cfg.form_title, form_title: &state.cfg.form_title,
submit_button: &state.cfg.submit_button, submit_button: &state.cfg.submit_button,
success_message: &state.cfg.success_message,
error_message: &state.cfg.error_message,
fields: &state.cfg.fields, fields: &state.cfg.fields,
lang: "en", lang: "en",
}; };
@@ -133,16 +139,16 @@ pub async fn submit(
.into_response(); .into_response();
} }
// tracing::info!("Entry submitted: {:?}", entry.answers); tracing::debug!("Entry submitted: {:?}", entry.answers);
Html( // Html(
r#" // r#"
<html> // <html>
<body> // <body>
<p>Saved. <a href="/">Back</a></p> // <p>Saved. <a href="/">Back</a></p>
</body> // </body>
</html> // </html>
"#, // "#,
) // )
.into_response() StatusCode::OK.into_response()
} }
+64
View File
@@ -53,6 +53,10 @@ h1 {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
h1 i, h2 i, h3 i {
margin-right: 0.5rem;
}
.form-container { .form-container {
background-color: var(--bg-card); background-color: var(--bg-card);
width: 100%; width: 100%;
@@ -215,6 +219,66 @@ button:hover {
transform: translateY(-1px); transform: translateY(-1px);
} }
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: var(--bg-card);
padding: 1.5rem;
border-radius: var(--radius-lg);
border: 1px solid var(--border-base);
max-width: 400px;
width: 90%;
text-align: center;
box-shadow: var(--shadow);
transform: translateY(20px);
transition: transform 0.3s ease;
}
.modal-overlay.active {
display: flex;
}
.modal-overlay.active .modal-content {
transform: translateY(0);
}
.modal-content h2 {
margin-top: 0;
color: var(--text-main);
}
.modal-content p {
color: var(--text-muted);
margin-bottom: 2rem;
}
.modal-close-btn {
background-color: var(--primary);
color: var(--text-inv);
border: none;
padding: 0.8rem 2rem;
border-radius: var(--radius-md);
font-weight: 700;
cursor: pointer;
width: auto;
}
.modal-content h2.error-title {
color: #ef4444;
}
@media (max-width: 640px) { @media (max-width: 640px) {
body { body {
padding: 1rem; padding: 1rem;
+4
View File
@@ -3,6 +3,10 @@
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
/>
<title>{% block title %}Form{% endblock %}</title> <title>{% block title %}Form{% endblock %}</title>
<style> <style>
/*<![CDATA[*/ /*<![CDATA[*/
+91 -13
View File
@@ -6,16 +6,11 @@
<div class="form-container"> <div class="form-container">
<h1>{{ form_title }}</h1> <h1>{{ form_title }}</h1>
<form action="submit" method="POST" autocomplete="off"> <form id="form-gen" action="submit" method="POST" autocomplete="off">
{% for f in fields %} {% for f in fields %}
{% if let Some(html) = f.html_before %}{{ html | safe }}{% endif %}
{% if let Some(html) = f.html_before %}
{{ html | safe }}
{% endif %}
<div class="field"> <div class="field">
{% match f.widget() %} {% match f.widget() %}
{% when FieldWidget::Checkbox %} {% when FieldWidget::Checkbox %}
<label for="{{ f.name }}" class="checkbox-label"> <label for="{{ f.name }}" class="checkbox-label">
<input id="{{ f.name }}" name="{{ f.name }}" type="checkbox" value="true"> <input id="{{ f.name }}" name="{{ f.name }}" type="checkbox" value="true">
@@ -50,14 +45,9 @@
<span class="desc">{{ f.description }}</span> <span class="desc">{{ f.description }}</span>
</label> </label>
<input id="{{ f.name }}" name="{{ f.name }}" type="{{ input_type }}" placeholder="{{ f.title|lower }}"> <input id="{{ f.name }}" name="{{ f.name }}" type="{{ input_type }}" placeholder="{{ f.title|lower }}">
{% endmatch %} {% endmatch %}
</div> </div>
{% if let Some(html) = f.html_after %}{{ html | safe }}{% endif %}
{% if let Some(html) = f.html_after %}
{{ html | safe }}
{% endif %}
{% endfor %} {% endfor %}
<div class="submit-container"> <div class="submit-container">
@@ -65,4 +55,92 @@
</div> </div>
</form> </form>
</div> </div>
<div id="status-modal" class="modal-overlay">
<div class="modal-content">
<h2 id="modal-title">Status</h2>
<p id="modal-text"></p>
<button class="modal-close-btn" onclick="closeModal()">OK</button>
</div>
</div>
<script>
const form = document.getElementById('form-gen');
const modal = document.getElementById('status-modal');
const modalTitle = document.getElementById('modal-title');
const modalText = document.getElementById('modal-text');
const STORAGE_KEY = 'form_generator_draft';
// Load Draft
function loadDraft() {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
try {
const data = JSON.parse(raw);
Object.keys(data).forEach(key => {
const input = form.elements[key];
if (!input) return;
if (input.type === 'checkbox') input.checked = data[key] === "true";
else input.value = data[key];
});
} catch (e) {}
}
// Save Draft
function saveDraft() {
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => data[key] = value);
form.querySelectorAll('input[type="checkbox"]').forEach(cb => {
data[cb.name] = cb.checked ? "true" : "false";
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function showModal(title, message, isError = false) {
modalTitle.innerText = title;
modalText.innerText = message;
modalTitle.className = isError ? 'error-title' : 'success-title';
modal.classList.add('active');
}
function closeModal() {
modal.classList.remove('active');
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const submitBtn = form.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerText;
submitBtn.disabled = true;
submitBtn.innerText = "Submitting...";
try {
const response = await fetch(form.action, {
method: 'POST',
body: new URLSearchParams(new FormData(form)),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
if (response.ok) {
localStorage.removeItem(STORAGE_KEY);
form.reset();
showModal("{{ success_message }}", "", false);
} else {
showModal("{{ error_message }}", "", true);
}
} catch (err) {
showModal("{{ error_message }}", err, true);
} finally {
submitBtn.disabled = false;
submitBtn.innerText = originalBtnText;
}
});
form.addEventListener('input', saveDraft);
window.addEventListener('DOMContentLoaded', loadDraft);
</script>
{% endblock %} {% endblock %}