diff --git a/app/(app)/speakers/_client.tsx b/app/(app)/speakers/_client.tsx new file mode 100644 index 00000000..e8ed9cd0 --- /dev/null +++ b/app/(app)/speakers/_client.tsx @@ -0,0 +1,31 @@ +import { SpeakerForm } from "@/components/Speaker/SpeakerForm"; + +export function SpeakersClient() { + return ( +
+
+
+

+ Speak at Codú +

+

+ Pitch a talk at a Codú meetup +

+

+ Codú runs regular meetups across Ireland and we're always + looking for speakers. Whether it's your first talk or your + fiftieth, we'd love to hear your pitch. Propose a talk (or up + to three) and we'll be in touch. +

+
+ + + +

+ Takes about 3 minutes. First-time speakers welcome — we'll help + you prep. +

+
+
+ ); +} diff --git a/app/(app)/speakers/page.tsx b/app/(app)/speakers/page.tsx new file mode 100644 index 00000000..9e060433 --- /dev/null +++ b/app/(app)/speakers/page.tsx @@ -0,0 +1,37 @@ +import type { Metadata } from "next"; +import { SpeakersClient } from "./_client"; + +const PAGE_URL = "https://www.codu.co/speakers"; +const PAGE_TITLE = "Speak at Codú — Pitch a Talk for Our Meetups"; +const PAGE_DESCRIPTION = + "Pitch a talk at a Codú meetup. First-time speakers welcome. We run regular developer meetups across Ireland and are always looking for people to share what they've built, learned, or broken."; + +export const metadata: Metadata = { + title: PAGE_TITLE, + description: PAGE_DESCRIPTION, + keywords: [ + "Codú speaker", + "tech meetup speaker Ireland", + "developer meetup Dublin", + "first time speaker", + "web development talk", + "speak at meetup Ireland", + ], + alternates: { canonical: PAGE_URL }, + robots: { index: true, follow: true }, + openGraph: { + title: "Speak at Codú", + description: PAGE_DESCRIPTION, + url: PAGE_URL, + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Speak at Codú", + description: PAGE_DESCRIPTION, + }, +}; + +export default function SpeakersPage() { + return ; +} diff --git a/app/(app)/volunteer/_client.tsx b/app/(app)/volunteer/_client.tsx new file mode 100644 index 00000000..a8118e3f --- /dev/null +++ b/app/(app)/volunteer/_client.tsx @@ -0,0 +1,31 @@ +import { VolunteerForm } from "@/components/Volunteer/VolunteerForm"; + +export function VolunteerClient() { + return ( +
+
+
+

+ Volunteer with Codú +

+

+ Help us build Ireland's largest web dev community +

+

+ Codú is Ireland's largest web dev community — thousands of + developers, regular meetups, and a newsletter across the Irish tech + ecosystem. We're opening volunteer spots for people interested + in marketing and events. +

+
+ + + +

+ Takes about 3 minutes. We read every application and reply within 2 + weeks. +

+
+
+ ); +} diff --git a/app/(app)/volunteer/page.tsx b/app/(app)/volunteer/page.tsx new file mode 100644 index 00000000..cf9536db --- /dev/null +++ b/app/(app)/volunteer/page.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from "next"; +import { VolunteerClient } from "./_client"; + +const PAGE_URL = "https://www.codu.co/volunteer"; +const PAGE_TITLE = + "Volunteer with Codú — Help Build Ireland's Largest Dev Community"; +const PAGE_DESCRIPTION = + "Join the team behind Codú. We're recruiting volunteer marketers and event organisers to help run meetups, newsletters, partnerships, and socials across the Irish tech ecosystem."; + +export const metadata: Metadata = { + title: PAGE_TITLE, + description: PAGE_DESCRIPTION, + keywords: [ + "Codú volunteer", + "volunteer developer community", + "Ireland tech community", + "web developer volunteer", + "tech meetup organiser Ireland", + "marketing volunteer", + "events volunteer", + ], + alternates: { canonical: PAGE_URL }, + robots: { index: true, follow: true }, + openGraph: { + title: "Volunteer with Codú", + description: PAGE_DESCRIPTION, + url: PAGE_URL, + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Volunteer with Codú", + description: PAGE_DESCRIPTION, + }, +}; + +export default function VolunteerPage() { + return ; +} diff --git a/app/sitemap.ts b/app/sitemap.ts index 27abc0b1..76cbc1cb 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -14,6 +14,8 @@ const ROUTES_TO_INDEX = [ "/feed", "/advertise", "/code-of-conduct", + "/volunteer", + "/speakers", ]; export default async function sitemap(): Promise { diff --git a/components/Speaker/SpeakerForm.tsx b/components/Speaker/SpeakerForm.tsx new file mode 100644 index 00000000..6412ad79 --- /dev/null +++ b/components/Speaker/SpeakerForm.tsx @@ -0,0 +1,379 @@ +"use client"; + +import { useForm, useFieldArray, type FieldPath } from "react-hook-form"; +import { + SpeakerApplicationSchema, + type SpeakerApplicationInput, + speakerFormats, + speakerFormatLabels, + speakerExperiences, + speakerExperienceLabels, + talkLengths, + talkLengthLabels, +} from "@/schema/speaker"; +import { Input } from "@/components/ui-components/input"; +import { Textarea } from "@/components/ui-components/textarea"; +import { + Field, + Label, + ErrorMessage, +} from "@/components/ui-components/fieldset"; +import { api } from "@/server/trpc/react"; +import clsx from "clsx"; +import { CheckIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; + +const MAX_TALKS = 3; + +function SuccessState() { + return ( +
+
+ +
+

Thanks for pitching!

+

+ We read every submission and reply within 2 weeks. In the meantime, say + hi in our{" "} + + Discord + + . +

+
+ ); +} + +export function SpeakerForm() { + const { + register, + control, + handleSubmit, + setError, + formState: { errors, isSubmitting, isSubmitSuccessful }, + } = useForm({ + defaultValues: { + name: "", + email: "", + link: "", + location: "", + bio: "", + talks: [{ title: "", length: "STANDARD_20", abstract: "" }], + other: "", + website: "", + }, + }); + + const { fields, append, remove } = useFieldArray({ + control, + name: "talks", + }); + + const submitMutation = api.speaker.submit.useMutation(); + + const onSubmit = async (data: SpeakerApplicationInput) => { + const result = SpeakerApplicationSchema.safeParse(data); + if (!result.success) { + result.error.issues.forEach((issue) => { + setError(issue.path.join(".") as FieldPath, { + type: "manual", + message: issue.message, + }); + }); + return; + } + await submitMutation.mutateAsync(result.data); + }; + + if (isSubmitSuccessful && submitMutation.isSuccess) { + return ; + } + + return ( +
+ + +
+ + + + {errors.name && {errors.name.message}} + + + + + + {errors.email && {errors.email.message}} + +
+ + + + + {errors.link && {errors.link.message}} + + + + + + {errors.location && ( + {errors.location.message} + )} + + + + +