Initial Paprika Next.js implementation

This commit is contained in:
2026-07-06 12:12:36 -04:00
commit 7d585484a6
38 changed files with 14824 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import type { LegacyPageKey } from "@/lib/legacy-pages";
import { legacyPages } from "@/lib/legacy-pages";
export function LegacyPage({ pageKey }: { pageKey: LegacyPageKey }) {
const page = legacyPages[pageKey];
return <div dangerouslySetInnerHTML={{ __html: page.html }} />;
}

56
components/SiteHeader.tsx Normal file
View File

@@ -0,0 +1,56 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navItems, resourceItems } from "@/lib/navigation";
export function SiteHeader() {
const pathname = usePathname();
const current = pathname === "/" ? "home" : pathname.split("/")[1];
const resourcesActive = current === "faq" || current === "resources";
return (
<header className="site-header">
<Link className="brand" href="/">
<div className="brand-name">Paprika</div>
<div className="brand-tag">Attorney Curated Evidence Building for O-1 Talent</div>
</Link>
<nav className="nav" id="nav">
{navItems.map((item) => (
<Link
key={item.key}
href={item.href}
data-page={item.key}
className={current === item.key ? "active" : undefined}
>
{item.label}
</Link>
))}
<div className="nav-dropdown">
<Link
href="/resources"
className={`nav-trigger${resourcesActive ? " active" : ""}`}
data-page="resources"
>
Resources <span className="nav-chevron"></span>
</Link>
<div className="resources-menu" aria-label="Resources menu">
{resourceItems.map((item) => (
<Link key={item.key} href={item.href} data-page={item.key}>
<strong>{item.label}</strong>
<span>{item.description}</span>
</Link>
))}
</div>
</div>
<Link
href="/start"
className={`start${current === "start" ? " active" : ""}`}
data-page="start"
>
Start Here <span className="button-arrow"></span>
</Link>
</nav>
</header>
);
}

View File

@@ -0,0 +1,150 @@
"use client";
import { useRouter } from "next/navigation";
import { useState, type FormEvent } from "react";
type FormType = "start" | "ask";
type FieldErrors = Record<string, string[] | undefined>;
export function SubmissionForm({ type }: { type: FormType }) {
const router = useRouter();
const [errors, setErrors] = useState<FieldErrors>({});
const [status, setStatus] = useState<"idle" | "submitting" | "error">("idle");
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setErrors({});
setStatus("submitting");
const form = new FormData(event.currentTarget);
const payload = Object.fromEntries(form.entries());
const response = await fetch("/api/submissions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...payload,
type,
consentAccepted: form.get("consentAccepted") === "on",
sourcePath: type === "start" ? "/start" : "/ask"
})
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
setErrors(result.issues || { form: [result.error || "Submission failed"] });
setStatus("error");
return;
}
router.push(`/thanks?submission_id=${encodeURIComponent(result.submission_id)}`);
}
return (
<form className="form-wrap" onSubmit={handleSubmit}>
<label className="field">
Name <span className="req">*</span>
<input className="field-control" name="name" autoComplete="name" required />
<FieldError errors={errors} name="name" />
</label>
<label className="field">
Email <span className="req">*</span>
<input className="field-control" name="email" type="email" autoComplete="email" required />
<FieldError errors={errors} name="email" />
</label>
<label className="field">
LinkedIn {type === "ask" ? <span>(optional)</span> : null}
<input className="field-control" name="linkedin" type="url" placeholder="https://linkedin.com/in/..." />
<FieldError errors={errors} name="linkedin" />
</label>
{type === "start" ? <StartFields errors={errors} /> : <AskFields errors={errors} />}
<label className="field large checkbox-field">
<input name="consentAccepted" type="checkbox" required />
<span>
I understand that submitting this form does not create an attorney-client relationship or
constitute legal advice. Paprika is not a law firm and does not provide legal services.
</span>
<FieldError errors={errors} name="consentAccepted" />
</label>
{errors.form?.length ? <div className="form-error">{errors.form[0]}</div> : null}
<button className="button primary" type="submit" disabled={status === "submitting"}>
{status === "submitting" ? "Submitting..." : type === "start" ? "Submit Profile" : "Send Question"}
</button>
<div className="form-note">
File upload is intentionally disabled until storage, size limits, access control, and privacy
handling are approved.
</div>
</form>
);
}
function StartFields({ errors }: { errors: FieldErrors }) {
return (
<>
<label className="field">
Company Website
<input className="field-control" name="companyWebsite" type="url" placeholder="https://..." />
<FieldError errors={errors} name="companyWebsite" />
</label>
<label className="field">
Current Role / Company
<input className="field-control" name="currentRoleCompany" />
</label>
<label className="field">
Field or Industry
<input className="field-control" name="fieldOrIndustry" />
</label>
<label className="field large start-question-field">
<strong>
What brings you to Paprika? <span className="req">*</span>
</strong>
<span>
Tell us where you are in your O-1 journey. Whether you have a specific goal, are building
evidence early, or are just exploring, a quick summary is enough.
</span>
<textarea className="field-control" name="reason" required />
<FieldError errors={errors} name="reason" />
</label>
<label className="field large start-question-field">
<strong>What recognition have you already built, if any?</strong>
<span>
Press, interviews, judging roles, awards, speaking, scholarly articles, patents, funding,
major projects, or other recognition.
</span>
<textarea className="field-control" name="existingRecognition" />
</label>
<label className="field large start-question-field">
<strong>Is there a timeline on your mind?</strong>
<span>
Whether it is urgent, months away, or just a future possibility, an approximate timeline
helps us recommend the right pace.
</span>
<textarea className="field-control" name="timeline" />
</label>
</>
);
}
function AskFields({ errors }: { errors: FieldErrors }) {
return (
<label className="field large question-field">
<strong>
Your question <span className="req">*</span>
</strong>
<span>Ask us anything about our services, your situation, or where to start.</span>
<textarea className="field-control" name="question" required />
<FieldError errors={errors} name="question" />
</label>
);
}
function FieldError({ errors, name }: { errors: FieldErrors; name: string }) {
const message = errors[name]?.[0];
return message ? <span className="form-error">{message}</span> : null;
}