From 76e203c573196b9b55dc1702a195c04f80f9d8d4 Mon Sep 17 00:00:00 2001 From: Sayan De Date: Tue, 1 Sep 2026 04:39:58 +0530 Subject: [PATCH 1/2] feat: add careers route with role pages and application intake Three open roles live as data in lib/careers/roles.ts rather than the database: a job post is content that ships with a deploy and wants review in a pull request, and there are three of them. The database side is only the inbound applications, which is the part that actually grows. /careers lists the roles; /careers/[slug] renders the full JD and the application form (name, email, message to the recruiter, an explicit permission checkbox). Role pages are prerendered and carry JobPosting JSON-LD so they can surface in Google's job results. The zod schema in lib/careers/application.ts is shared by the form and the route, so the browser and the server cannot disagree about what a valid application looks like. The route is still the authority and re-validates everything. job_applications has no users reference: an applicant is almost never a signed-in user, and nothing here should cascade from an account deletion. role_title is a snapshot of the title as advertised, so a later retitle cannot rewrite what someone believes they applied for. The route dedupes per email per role over 24 hours and reports success on a repeat rather than an error, since the applicant already told us. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Naki3DJALSYSKgZLkEjuYU --- app/(company)/careers/[slug]/page.tsx | 138 +++ app/(company)/careers/page.tsx | 75 ++ app/api/careers/route.ts | 89 ++ components/footer.tsx | 5 + drizzle/0005_cold_red_shift.sql | 13 + drizzle/meta/0005_snapshot.json | 1368 +++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + features/careers/application-form.tsx | 228 +++++ lib/careers/application.ts | 25 + lib/careers/roles.ts | 166 +++ lib/db/schema.ts | 29 + lib/seo/metadata.ts | 9 +- lib/seo/routes.ts | 6 + 13 files changed, 2157 insertions(+), 1 deletion(-) create mode 100644 app/(company)/careers/[slug]/page.tsx create mode 100644 app/(company)/careers/page.tsx create mode 100644 app/api/careers/route.ts create mode 100644 drizzle/0005_cold_red_shift.sql create mode 100644 drizzle/meta/0005_snapshot.json create mode 100644 features/careers/application-form.tsx create mode 100644 lib/careers/application.ts create mode 100644 lib/careers/roles.ts diff --git a/app/(company)/careers/[slug]/page.tsx b/app/(company)/careers/[slug]/page.tsx new file mode 100644 index 0000000..647770c --- /dev/null +++ b/app/(company)/careers/[slug]/page.tsx @@ -0,0 +1,138 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; + +import { siteConfig } from "@/site"; +import { ArrowLeft, Globe, MapPin } from "lucide-react"; + +import { JsonLd } from "@/components/json-ld"; + +import { type Role, getRole, roles } from "@/lib/careers/roles"; +import { buildMetadata } from "@/lib/seo/metadata"; + +import { ApplicationForm } from "@/features/careers/application-form"; + +export function generateStaticParams() { + return roles.map((role) => ({ slug: role.slug })); +} + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const role = getRole(slug); + if (!role) return {}; + + return buildMetadata({ + path: `/careers/${slug}`, + title: role.title, + description: role.tagline, + }); +} + +/** schema.org/JobPosting, so the role can surface in Google's job results. */ +function jobPostingJsonLd(role: Role) { + return { + "@context": "https://schema.org", + "@type": "JobPosting", + title: role.title, + description: [...role.about, ...role.responsibilities].join(" "), + employmentType: "FULL_TIME", + hiringOrganization: { + "@type": "Organization", + name: siteConfig.name, + sameAs: siteConfig.url, + }, + jobLocationType: "TELECOMMUTE", + applicantLocationRequirements: { "@type": "Country", name: "Worldwide" }, + directApply: true, + url: `${siteConfig.url}/careers/${role.slug}`, + }; +} + +/** A titled list block. Every JD section on the page is one of these. */ +function ListSection({ title, items }: { title: string; items: string[] }) { + return ( +
+

{title}

+
    + {items.map((item) => ( +
  • + {item} +
  • + ))} +
+
+ ); +} + +export default async function RolePage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const role = getRole(slug); + if (!role) notFound(); + + return ( +
+ + + + + All open roles + + +

+ {role.title} +

+ +
+ + + {role.location} + + + + {role.type} + +
+ +
    + {role.stack.map((item) => ( +
  • + {item} +
  • + ))} +
+ +
+

About the role

+ {role.about.map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+ + + + + + +
+

Apply

+

+ No resume upload and no application portal. Write us something real. A short, specific + message about what you have built beats three pages of adjectives every time. +

+ +
+ +
+
+
+ ); +} diff --git a/app/(company)/careers/page.tsx b/app/(company)/careers/page.tsx new file mode 100644 index 0000000..3d0177a --- /dev/null +++ b/app/(company)/careers/page.tsx @@ -0,0 +1,75 @@ +import Link from "next/link"; + +import { ArrowRight, Globe, MapPin } from "lucide-react"; + +import { BreadcrumbJsonLd } from "@/components/json-ld"; + +import { roles } from "@/lib/careers/roles"; +import { buildMetadata } from "@/lib/seo/metadata"; + +export const metadata = buildMetadata({ + path: "/careers", + title: "Careers", + description: + "Open roles at Interview Resources. A small, remote, deliberately unglamorous team building evidence-graded interview research. Engineering and go-to-market.", +}); + +export default function CareersPage() { + return ( +
+ + +

Careers

+

+ We build interview research that shows its sources. Every question in a report is graded by + how well it is grounded, and the parts we cannot back up say so. That standard is easy to + write down and hard to keep, which is most of what the work here is. +

+

+ The team is small and remote. You will own whole surfaces rather than tickets, you will talk + to customers, and there is nobody between you and the thing you ship. +

+ +

Open roles

+ +
    + {roles.map((role) => ( +
  • + +
    +

    {role.title}

    + +
    +

    + {role.tagline} +

    +
    + + + {role.location} + + + + {role.type} + +
    + +
  • + ))} +
+ +
+

Nothing here fits?

+

+ Apply to the role closest to what you do and say so in your message. We would rather read + a good application to the wrong opening than miss you entirely. +

+
+
+ ); +} diff --git a/app/api/careers/route.ts b/app/api/careers/route.ts new file mode 100644 index 0000000..88a2e9e --- /dev/null +++ b/app/api/careers/route.ts @@ -0,0 +1,89 @@ +import { and, eq, gt } from "drizzle-orm"; + +import { jobApplicationSchema } from "@/lib/careers/application"; +import { getRole } from "@/lib/careers/roles"; +import { db } from "@/lib/db/index"; +import { jobApplications } from "@/lib/db/schema"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** + * How long one email address has to wait before applying to the same role + * again. Public, unauthenticated, and writing to the database, so it needs + * some floor — but the floor is per role, because applying to two different + * openings in one sitting is a reasonable thing to do. + */ +const REAPPLY_WINDOW_MS = 24 * 60 * 60 * 1000; + +/** Receives a careers application and files it. Deliberately unauthenticated. */ +export async function POST(request: Request) { + let payload: unknown; + try { + payload = await request.json(); + } catch { + return Response.json({ error: "Send JSON." }, { status: 400 }); + } + + const parsed = jobApplicationSchema.safeParse(payload); + if (!parsed.success) { + // Field-keyed so the form can put each message next to its input. + const fieldErrors: Record = {}; + for (const issue of parsed.error.issues) { + const field = String(issue.path[0] ?? "form"); + fieldErrors[field] ??= issue.message; + } + return Response.json( + { error: "Please fix the highlighted fields.", fieldErrors }, + { status: 400 } + ); + } + + const { roleSlug, name, email, message, consent } = parsed.data; + + // The schema already rejected unknown slugs; this is what gives us the title + // to snapshot alongside the row. + const role = getRole(roleSlug); + if (!role) { + return Response.json({ error: "That role is not open." }, { status: 400 }); + } + + const normalisedEmail = email.trim().toLowerCase(); + + try { + const [recent] = await db + .select({ id: jobApplications.id }) + .from(jobApplications) + .where( + and( + eq(jobApplications.email, normalisedEmail), + eq(jobApplications.roleSlug, roleSlug), + gt(jobApplications.createdAt, new Date(Date.now() - REAPPLY_WINDOW_MS)) + ) + ) + .limit(1); + + // Not an error as far as the applicant is concerned: they already told us, + // and a second copy of the same message helps nobody. Report success. + if (recent) { + return Response.json({ ok: true, duplicate: true }); + } + + await db.insert(jobApplications).values({ + roleSlug, + roleTitle: role.title, + name: name.trim(), + email: normalisedEmail, + message: message.trim(), + consent, + }); + } catch (error) { + console.error("careers: could not file application", error); + return Response.json( + { error: "We could not file that. Please try again in a moment." }, + { status: 500 } + ); + } + + return Response.json({ ok: true }); +} diff --git a/components/footer.tsx b/components/footer.tsx index fcb1180..8e36449 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -71,6 +71,11 @@ export function Footer() { Blog +
  • + + Careers + +
  • Contact diff --git a/drizzle/0005_cold_red_shift.sql b/drizzle/0005_cold_red_shift.sql new file mode 100644 index 0000000..1e61771 --- /dev/null +++ b/drizzle/0005_cold_red_shift.sql @@ -0,0 +1,13 @@ +CREATE TABLE "job_applications" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "role_slug" text NOT NULL, + "role_title" text NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "message" text NOT NULL, + "consent" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX "job_applications_role_slug_created_at_idx" ON "job_applications" USING btree ("role_slug","created_at");--> statement-breakpoint +CREATE INDEX "job_applications_email_idx" ON "job_applications" USING btree ("email"); \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..24c927f --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,1368 @@ +{ + "id": "4954a8e2-d9c0-4e9d-a6ee-0001c973fc72", + "prevId": "abcc4980-3f62-4e44-962d-6dfff11033c8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credits_ledger": { + "name": "credits_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "delta": { + "name": "delta", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_ref": { + "name": "payment_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "research_id": { + "name": "research_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credits_ledger_user_id_idx": { + "name": "credits_ledger_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credits_ledger_user_id_users_id_fk": { + "name": "credits_ledger_user_id_users_id_fk", + "tableFrom": "credits_ledger", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credits_ledger_research_id_researches_id_fk": { + "name": "credits_ledger_research_id_researches_id_fk", + "tableFrom": "credits_ledger", + "tableTo": "researches", + "columnsFrom": [ + "research_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credits_ledger_payment_ref_unique": { + "name": "credits_ledger_payment_ref_unique", + "nullsNotDistinct": false, + "columns": [ + "payment_ref" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_applications": { + "name": "job_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role_slug": { + "name": "role_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_title": { + "name": "role_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consent": { + "name": "consent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_applications_role_slug_created_at_idx": { + "name": "job_applications_role_slug_created_at_idx", + "columns": [ + { + "expression": "role_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_applications_email_idx": { + "name": "job_applications_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_disputes": { + "name": "payment_disputes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "payment_id": { + "name": "payment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_dispute_id": { + "name": "provider_dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_fee_micros": { + "name": "estimated_fee_micros", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "economics_version": { + "name": "economics_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "payment_disputes_payment_id_payments_id_fk": { + "name": "payment_disputes_payment_id_payments_id_fk", + "tableFrom": "payment_disputes", + "tableTo": "payments", + "columnsFrom": [ + "payment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "payment_disputes_provider_dispute_id_unique": { + "name": "payment_disputes_provider_dispute_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_dispute_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_refunds": { + "name": "payment_refunds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "payment_id": { + "name": "payment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_refund_id": { + "name": "provider_refund_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credits_reversed": { + "name": "credits_reversed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "estimated_fee_micros": { + "name": "estimated_fee_micros", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "economics_version": { + "name": "economics_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "payment_refunds_payment_id_payments_id_fk": { + "name": "payment_refunds_payment_id_payments_id_fk", + "tableFrom": "payment_refunds", + "tableTo": "payments", + "columnsFrom": [ + "payment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "payment_refunds_provider_refund_id_unique": { + "name": "payment_refunds_provider_refund_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_refund_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payments": { + "name": "payments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dodo'" + }, + "provider_payment_id": { + "name": "provider_payment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pack": { + "name": "pack", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_price_usd_minor": { + "name": "catalog_price_usd_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credits_granted": { + "name": "credits_granted", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "estimated_dodo_fee_micros": { + "name": "estimated_dodo_fee_micros", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "economics_version": { + "name": "economics_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credits_reversed": { + "name": "credits_reversed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "refunded_amount_minor": { + "name": "refunded_amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'succeeded'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payments_user_id_idx": { + "name": "payments_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payments_user_id_users_id_fk": { + "name": "payments_user_id_users_id_fk", + "tableFrom": "payments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "payments_provider_payment_id_unique": { + "name": "payments_provider_payment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.product_events": { + "name": "product_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "product_events_name_created_at_idx": { + "name": "product_events_name_created_at_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "product_events_user_id_idx": { + "name": "product_events_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "product_events_user_id_users_id_fk": { + "name": "product_events_user_id_users_id_fk", + "tableFrom": "product_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.question_feedback": { + "name": "question_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question_idx": { + "name": "question_idx", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "question_feedback_report_question_idx": { + "name": "question_feedback_report_question_idx", + "columns": [ + { + "expression": "report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "question_idx", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "question_feedback_report_id_reports_id_fk": { + "name": "question_feedback_report_id_reports_id_fk", + "tableFrom": "question_feedback", + "tableTo": "reports", + "columnsFrom": [ + "report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "research_id": { + "name": "research_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "json_payload": { + "name": "json_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_slug": { + "name": "published_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reports_research_id_researches_id_fk": { + "name": "reports_research_id_researches_id_fk", + "tableFrom": "reports", + "tableTo": "researches", + "columnsFrom": [ + "research_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reports_share_token_unique": { + "name": "reports_share_token_unique", + "nullsNotDistinct": false, + "columns": [ + "share_token" + ] + }, + "reports_published_slug_unique": { + "name": "reports_published_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "published_slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.research_cache": { + "name": "research_cache", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.researches": { + "name": "researches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_domain": { + "name": "company_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interviewers": { + "name": "interviewers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "interview_type": { + "name": "interview_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_context": { + "name": "role_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cost_micros_llm": { + "name": "cost_micros_llm", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_micros_search": { + "name": "cost_micros_search", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credits_charged": { + "name": "credits_charged", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "researches_one_running_per_user_idx": { + "name": "researches_one_running_per_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"researches\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "researches_user_id_users_id_fk": { + "name": "researches_user_id_users_id_fk", + "tableFrom": "researches", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referred_by": { + "name": "referred_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "marketing_email_opt_in": { + "name": "marketing_email_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_referred_by_users_id_fk": { + "name": "users_referred_by_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "referred_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_referral_code_unique": { + "name": "users_referral_code_unique", + "nullsNotDistinct": false, + "columns": [ + "referral_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 71a7985..220e568 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1785007937919, "tag": "0004_omniscient_spencer_smythe", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1788217241526, + "tag": "0005_cold_red_shift", + "breakpoints": true } ] } \ No newline at end of file diff --git a/features/careers/application-form.tsx b/features/careers/application-form.tsx new file mode 100644 index 0000000..1580c62 --- /dev/null +++ b/features/careers/application-form.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { useState } from "react"; + +import Link from "next/link"; + +import { ArrowLeft, CheckCircle2, Loader2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; + +import { jobApplicationSchema } from "@/lib/careers/application"; +import { cn } from "@/lib/utils"; + +interface ApplicationFormProps { + roleSlug: string; + roleTitle: string; +} + +type FieldErrors = Partial>; + +/** One label + control + error, so the three fields cannot drift apart. */ +function Field({ + id, + label, + hint, + error, + children, +}: { + id: string; + label: string; + hint?: string; + error?: string; + children: React.ReactNode; +}) { + return ( +
    + + {children} + {error ? ( +

    + {error} +

    + ) : hint ? ( +

    {hint}

    + ) : null} +
    + ); +} + +export function ApplicationForm({ roleSlug, roleTitle }: ApplicationFormProps) { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [message, setMessage] = useState(""); + const [consent, setConsent] = useState(false); + const [errors, setErrors] = useState({}); + const [isSending, setIsSending] = useState(false); + const [isSent, setIsSent] = useState(false); + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (isSending) return; + + // Validated with the same schema the route uses, so the browser and the + // server can never disagree about what a valid application looks like. + const parsed = jobApplicationSchema.safeParse({ roleSlug, name, email, message, consent }); + if (!parsed.success) { + const next: FieldErrors = {}; + for (const issue of parsed.error.issues) { + const field = String(issue.path[0] ?? "form") as keyof FieldErrors; + next[field] ??= issue.message; + } + setErrors(next); + return; + } + + setErrors({}); + setIsSending(true); + try { + const res = await fetch("/api/careers", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(parsed.data), + }); + const body = (await res.json().catch(() => ({}))) as { + error?: string; + fieldErrors?: FieldErrors; + }; + + if (!res.ok) { + setErrors({ ...body.fieldErrors, form: body.error ?? "Something went wrong." }); + return; + } + setIsSent(true); + } catch { + setErrors({ form: "We could not reach the server. Please try again." }); + } finally { + setIsSending(false); + } + } + + if (isSent) { + return ( +
    + +

    + Applied. Your application is sent. +

    +

    + Thanks for applying to {roleTitle}. We read every application ourselves and we will get + back to you until then, use our application and tell us what you would change about it. + That is the most useful thing you can put in front of us. +

    +
    + + +
    +
    + ); + } + + return ( +
    + + setName(event.target.value)} + aria-invalid={Boolean(errors.name)} + aria-describedby={errors.name ? "name-error" : undefined} + className="h-10" + /> + + + + setEmail(event.target.value)} + aria-invalid={Boolean(errors.email)} + aria-describedby={errors.email ? "email-error" : undefined} + className="h-10" + /> + + + +