aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikhil M Warrier <nikhilmw.dev@gmail.com>2026-03-15 18:57:46 +0530
committerNikhil M Warrier <nikhilmw.dev@gmail.com>2026-03-15 18:57:46 +0530
commit1394400b767c086ab0d71e9b2f95785b8cfe1b69 (patch)
tree80b1d953adc3ffb618b585dfc0b4d4ef4cb62f5d
parent17dcdb8076fd470880662be9550c9699b8614e24 (diff)
Add autosave and clear button to forms.
-rw-r--r--src/content/forms/execom.mdx60
-rw-r--r--src/pages/forms/[...id].astro81
-rw-r--r--src/styles/global.css4
3 files changed, 82 insertions, 63 deletions
diff --git a/src/content/forms/execom.mdx b/src/content/forms/execom.mdx
deleted file mode 100644
index 43dc72a..0000000
--- a/src/content/forms/execom.mdx
+++ /dev/null
@@ -1,60 +0,0 @@
----
-title: "Interim Execom Recruitment"
-id: "interim_execom_recruit_form"
-description: "Recruitment form for FOSS-CET's interim executive committee"
-date: 2026-03-14
-form:
- submit_text: "Submit"
- fields:
- - id: "name"
- label: "Your Name"
- type: "text"
- required: true
- - id: "email"
- label: "Email Address"
- type: "email"
- required: true
- - id: "year"
- label: "Year"
- type: "select"
- options:
- - '1'
- - '2'
- - '3'
- - '4'
- required: true
- - id: "role"
- label: "Which role/s do you wish to apply for?"
- type: "checkbox"
- options:
- - 'Project Lead'
- - 'Event Lead'
- - 'Marketing & Design Lead'
- - 'Media Lead'
- - 'Women in FOSS Lead'
- - 'Administrative Committee (2nd, 3rd Years only)'
- required: true
- - id: "experience"
- label: "Relevant Experience, if any:"
- type: "textarea"
- required: false
- - id: "contributions"
- label: "If recruited to your role, how do you plan to take the club forward?"
- type: "textarea"
- required: true
- - id: "vision"
- label: "What is your vision for FOSS-CET?"
- type: "textarea"
- required: true
- - id: "help"
- label: "How would being a part of our club help with your goals/passions?"
- type: "textarea"
- required: true
- - id: "link"
- label: "Upload a google drive link to your previous works, if any: (for marketing and design applications)"
- type: "textarea"
- required: false
-
----
-
-# Let's talk open source. \ No newline at end of file
diff --git a/src/pages/forms/[...id].astro b/src/pages/forms/[...id].astro
index d26c51d..cadf14a 100644
--- a/src/pages/forms/[...id].astro
+++ b/src/pages/forms/[...id].astro
@@ -4,6 +4,7 @@ import { FORM_POST_URL } from '@/consts'
import Layout from '@/layouts/Layout.astro'
import { getAllForms } from '@/lib/data-utils'
import { formatDate } from '@/lib/utils'
+import { Icon } from 'astro-icon/components'
import { render } from 'astro:content'
export async function getStaticPaths() {
@@ -29,7 +30,7 @@ const fields = Astro.props.data.form.fields
{form.title}
</h1>
<div
- class="text-muted-foreground mb-4 mb-6 flex w-fit py-2 text-xs sm:px-2 sm:py-0 sm:text-sm"
+ class="text-muted-foreground mb-6 flex w-fit py-2 text-xs sm:px-2 sm:py-0 sm:text-sm"
>
<span>{formatDate(form.date)}</span>
</div>
@@ -40,18 +41,92 @@ const fields = Astro.props.data.form.fields
<form
action={`${FORM_POST_URL}${form.id}`}
+ data-form-id={form.id}
method="POST"
class="flex w-full flex-col items-start gap-12"
>
{fields.map((field) => <FormField field={field} />)}
+ <div class="flex w-full items-center justify-end gap-4">
+ <span
+ id="save-indicator"
+ class="text-muted-foreground ext-sm flex items-center justify-center gap-2 opacity-0 transition-opacity duration-300"
+ >
+ <Icon name="lucide:check" class="text-primary" />
+ Draft autosaved
+ </span>
- <div class="flex w-full justify-end">
+ <!-- Click again to confirm clear. -->
+ <button
+ type="button"
+ onclick="if(this.textContent==='Confirm?') { this.form.reset(); this.textContent='Clear'; this.classList.replace('bg-destructive', 'bg-muted'); } else { this.textContent='Confirm?'; this.classList.replace('bg-muted', 'bg-destructive'); this.classList.add('text-destructive-foreground'); }"
+ onblur="this.textContent='Clear'; this.classList.replace('bg-destructive', 'bg-muted'); this.classList.remove('text-destructive-foreground');"
+ class="bg-muted text-foreground rounded-lg px-4 py-2 font-sans text-lg font-bold transition-all duration-200"
+ >
+ Clear
+ </button>
<button
type="submit"
class="bg-primary text-primary-foreground rounded-lg px-4 py-2 font-sans text-lg font-bold"
- >{form.submit_text || 'Submit'}</button
>
+ {form.submit_text || 'Submit'}
+ </button>
</div>
</form>
</section>
+
+ <script>
+ const setupAutosave = () => {
+ const form = document.querySelector<HTMLFormElement>('form[data-form-id]')
+ const indicator = document.getElementById('save-indicator')
+ if (!form) return
+
+ const storageKey = `autosave-form-${form.dataset.formId}`
+ let saveTimeout: ReturnType<typeof setTimeout>
+
+ // load saved data on mount
+ const savedData = JSON.parse(localStorage.getItem(storageKey) || '{}')
+
+ Object.entries(savedData).forEach(([name, value]) => {
+ const inputs = form.querySelectorAll(`[name="${name}"]`)
+ inputs.forEach((input: any) => {
+ if (input.type === 'checkbox' || input.type === 'radio') {
+ input.checked = Array.isArray(value)
+ ? value.includes(input.value)
+ : input.value === value
+ } else {
+ input.value = value
+ }
+ })
+ })
+
+ // save data to localStorage on any input change
+ form.addEventListener('input', () => {
+ const formData = new FormData(form)
+ const data: Record<string, any> = {}
+
+ for (const [key, value] of formData.entries()) {
+ if (data[key]) {
+ data[key] = [].concat(data[key], value as any)
+ } else {
+ data[key] = value
+ }
+ }
+
+ localStorage.setItem(storageKey, JSON.stringify(data))
+
+ // show the "Draft saved" indicator
+ if (indicator) {
+ indicator.classList.remove('opacity-0')
+ }
+ })
+
+ // clear localStorage on submit
+ form.addEventListener('submit', () => {
+ localStorage.removeItem(storageKey)
+ })
+ }
+
+ document.addEventListener('DOMContentLoaded', setupAutosave)
+ document.addEventListener('astro:page-load', setupAutosave)
+ </script>
</Layout>
diff --git a/src/styles/global.css b/src/styles/global.css
index 2d4de9f..b80b9ed 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -103,4 +103,8 @@
::file-selector-button {
@apply border-border outline-ring/50 tracking-tight;
}
+
+ button {
+ @apply cursor-pointer;
+ }
}