aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikhil M Warrier <nikhilmw.dev@gmail.com>2026-03-13 08:52:29 +0530
committerNikhil M Warrier <nikhilmw.dev@gmail.com>2026-03-13 08:52:29 +0530
commit30a3651c17bfd8a771feaaaa4cb700788b757f80 (patch)
tree1a7560a6d9e0144c59a4792bcb629ea150564301
parent102a717ae001564349c2f0b2992459d6d5f91d08 (diff)
feat: Add forms
-rw-r--r--src/consts.ts2
-rw-r--r--src/content.config.ts38
-rw-r--r--src/content/forms/test.mdx26
-rw-r--r--src/lib/data-utils.ts5
-rw-r--r--src/pages/forms/[...id].astro68
5 files changed, 138 insertions, 1 deletions
diff --git a/src/consts.ts b/src/consts.ts
index f5e8992..93100c0 100644
--- a/src/consts.ts
+++ b/src/consts.ts
@@ -13,6 +13,8 @@ export const SITE: Site = {
eventsPerPage: 3,
}
+export const FORM_POST_URL = 'https://fosscet-forms.val.run/'
+
export const NAV_LINKS: SocialLink[] = [
{
href: '/blog',
diff --git a/src/content.config.ts b/src/content.config.ts
index 1bd3526..c9c4810 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -48,6 +48,42 @@ const members = defineCollection({
}),
})
+const forms = defineCollection({
+ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/forms' }),
+ schema: () =>
+ z.object({
+ title: z.string(),
+ description: z.string(),
+ date: z.coerce.date(),
+ // sqlite-safe
+ id: z.string().regex(/^[a-zA-Z0-9_]+$/, {
+ message:
+ "Form 'id' can only contain letters, numbers, and underscores.",
+ }),
+ submit_text: z.string().default('Submit'),
+ form: z.object({
+ submit_text: z.string().default('Submit'),
+ fields: z
+ .array(
+ z.object({
+ id: z.string().regex(/^[a-zA-Z0-9_]+$/, {
+ message:
+ "Field 'name' can only contain letters, numbers, and underscores.",
+ }),
+ label: z.string(),
+ type: z
+ .enum(['text', 'email', 'number', 'textarea', 'tel', 'url'])
+ .default('text'),
+ required: z.boolean().default(false),
+ }),
+ )
+ .min(1, {
+ message: 'A form must have at least one field defined.',
+ }),
+ }),
+ }),
+})
+
const projects = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/projects' }),
schema: ({ image }) =>
@@ -62,4 +98,4 @@ const projects = defineCollection({
}),
})
-export const collections = { events, blog, members, projects }
+export const collections = { events, blog, members, projects, forms }
diff --git a/src/content/forms/test.mdx b/src/content/forms/test.mdx
new file mode 100644
index 0000000..6e3d731
--- /dev/null
+++ b/src/content/forms/test.mdx
@@ -0,0 +1,26 @@
+---
+title: "Get in Touch"
+id: "main_contact_form"
+description: "Reach out to the FOSSCET team."
+date: 2026-03-13
+form:
+ submit_text: "Send Message"
+ fields:
+ - id: "name"
+ label: "Your Name"
+ type: "text"
+ required: true
+ - id: "email"
+ label: "Email Address"
+ type: "email"
+ required: true
+ - id: "message"
+ label: "What's on your mind?"
+ type: "textarea"
+ required: true
+---
+
+# Let's talk open source.
+
+This is a sample form.
+Everything below this text is generated automatically from the metadata.
diff --git a/src/lib/data-utils.ts b/src/lib/data-utils.ts
index 6e4439f..114fb97 100644
--- a/src/lib/data-utils.ts
+++ b/src/lib/data-utils.ts
@@ -26,6 +26,11 @@ export async function getAllEvents(): Promise<CollectionEntry<'events'>[]> {
return events.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
+export async function getAllForms(): Promise<CollectionEntry<'forms'>[]> {
+ const forms = await getCollection('forms')
+ return forms.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
+}
+
export function groupEventsByYear(
events: CollectionEntry<'events'>[],
): Record<string, CollectionEntry<'events'>[]> {
diff --git a/src/pages/forms/[...id].astro b/src/pages/forms/[...id].astro
new file mode 100644
index 0000000..713e599
--- /dev/null
+++ b/src/pages/forms/[...id].astro
@@ -0,0 +1,68 @@
+---
+import { FORM_POST_URL } from '@/consts'
+import Layout from '@/layouts/Layout.astro'
+import { getAllForms } from '@/lib/data-utils'
+
+export async function getStaticPaths() {
+ const forms = await getAllForms()
+ return forms.map((form) => ({
+ params: { id: form.id },
+ props: form,
+ }))
+}
+
+const form = Astro.props.data
+const fields = Astro.props.data.form.fields
+---
+
+<Layout>
+ <section
+ class="border-border mx-auto flex w-full max-w-3xl flex-col items-center gap-y-6 rounded-xl border p-8"
+ >
+ <form
+ action={`${FORM_POST_URL}${form.id}`}
+ method="POST"
+ class="flex w-full flex-col items-start gap-12"
+ >
+ <h1 class="w-full text-center text-3xl">
+ {form.title}
+ </h1>
+
+ {
+ fields.map((field) => (
+ <div class="flex w-full flex-col gap-2">
+ <label for={field.id} class="text-muted-foreground pl-1">
+ {field.label}
+ </label>
+
+ {field.type === 'textarea' ? (
+ <textarea
+ id={field.id}
+ name={field.id}
+ required={field.required}
+ rows="4"
+ class="border-border rounded-xl border px-4 py-2"
+ />
+ ) : (
+ <input
+ type={field.type || 'text'}
+ id={field.id}
+ name={field.id}
+ required={field.required}
+ class="border-border rounded-xl border px-4 py-2"
+ />
+ )}
+ </div>
+ ))
+ }
+
+ <div class="flex w-full justify-end">
+ <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
+ >
+ </div>
+ </form>
+ </section>
+</Layout>