added form browser persistence
This commit is contained in:
Generated
+431
-134
File diff suppressed because it is too large
Load Diff
@@ -17,3 +17,5 @@ toml = "0.9.8"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
askama = "0.14.0"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
uuid = { version = "1.21.0", features = ["v4"] }
|
||||
axum-extra = { version = "0.12.5", features = ["cookie"] }
|
||||
|
||||
@@ -7,6 +7,12 @@ form_title = "User Onboarding Survey"
|
||||
# Text displayed on the primary action button
|
||||
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
|
||||
[[fields]]
|
||||
name = "username"
|
||||
|
||||
+17
-11
@@ -49,6 +49,8 @@ pub struct AppConfig {
|
||||
pub json_output: Option<String>,
|
||||
pub form_title: String,
|
||||
pub submit_button: String,
|
||||
pub success_message: String,
|
||||
pub error_message: String,
|
||||
pub fields: Vec<FieldDef>,
|
||||
}
|
||||
|
||||
@@ -65,6 +67,8 @@ pub async fn render_form(State(state): State<AppState>) -> impl IntoResponse {
|
||||
struct FormTemplate<'a> {
|
||||
form_title: &'a str,
|
||||
submit_button: &'a str,
|
||||
success_message: &'a str,
|
||||
error_message: &'a str,
|
||||
fields: &'a [FieldDef],
|
||||
lang: &'a str,
|
||||
}
|
||||
@@ -72,6 +76,8 @@ pub async fn render_form(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let tmpl = FormTemplate {
|
||||
form_title: &state.cfg.form_title,
|
||||
submit_button: &state.cfg.submit_button,
|
||||
success_message: &state.cfg.success_message,
|
||||
error_message: &state.cfg.error_message,
|
||||
fields: &state.cfg.fields,
|
||||
lang: "en",
|
||||
};
|
||||
@@ -133,16 +139,16 @@ pub async fn submit(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// tracing::info!("Entry submitted: {:?}", entry.answers);
|
||||
tracing::debug!("Entry submitted: {:?}", entry.answers);
|
||||
|
||||
Html(
|
||||
r#"
|
||||
<html>
|
||||
<body>
|
||||
<p>Saved. <a href="/">Back</a></p>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
)
|
||||
.into_response()
|
||||
// Html(
|
||||
// r#"
|
||||
// <html>
|
||||
// <body>
|
||||
// <p>Saved. <a href="/">Back</a></p>
|
||||
// </body>
|
||||
// </html>
|
||||
// "#,
|
||||
// )
|
||||
StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ h1 {
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
h1 i, h2 i, h3 i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: var(--bg-card);
|
||||
width: 100%;
|
||||
@@ -215,6 +219,66 @@ button:hover {
|
||||
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) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<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>
|
||||
<style>
|
||||
/*<![CDATA[*/
|
||||
|
||||
+91
-13
@@ -6,16 +6,11 @@
|
||||
<div class="form-container">
|
||||
<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 %}
|
||||
|
||||
{% if let Some(html) = f.html_before %}
|
||||
{{ html | safe }}
|
||||
{% endif %}
|
||||
|
||||
{% if let Some(html) = f.html_before %}{{ html | safe }}{% endif %}
|
||||
<div class="field">
|
||||
{% match f.widget() %}
|
||||
|
||||
{% when FieldWidget::Checkbox %}
|
||||
<label for="{{ f.name }}" class="checkbox-label">
|
||||
<input id="{{ f.name }}" name="{{ f.name }}" type="checkbox" value="true">
|
||||
@@ -50,14 +45,9 @@
|
||||
<span class="desc">{{ f.description }}</span>
|
||||
</label>
|
||||
<input id="{{ f.name }}" name="{{ f.name }}" type="{{ input_type }}" placeholder="{{ f.title|lower }}">
|
||||
|
||||
{% endmatch %}
|
||||
</div>
|
||||
|
||||
{% if let Some(html) = f.html_after %}
|
||||
{{ html | safe }}
|
||||
{% endif %}
|
||||
|
||||
{% if let Some(html) = f.html_after %}{{ html | safe }}{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="submit-container">
|
||||
@@ -65,4 +55,92 @@
|
||||
</div>
|
||||
</form>
|
||||
</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 %}
|
||||
|
||||
Reference in New Issue
Block a user