Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules/
.next/
.env
2026_hackathon/
tsconfig.tsbuildinfo
69 changes: 63 additions & 6 deletions app/api/contact/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,57 @@
import { Resend } from "resend";
import { NextResponse } from "next/server";

const resend = new Resend(process.env.RESEND_API_KEY);
const recipientEmail = process.env.CONTACT_FORM_RECIPIENT ?? "info@pushquantum.tech";

type ContactRequestBody = {
name?: string;
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
university?: string;
customUniversity?: string;
inquiry?: string;
message?: string;
};

function escapeHtml(value: string) {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

function normalizeContactRequest(body: ContactRequestBody) {
const firstName = body.firstName?.trim() ?? "";
const lastName = body.lastName?.trim() ?? "";
const name = body.name?.trim() || [firstName, lastName].filter(Boolean).join(" ");
const email = body.email?.trim() ?? "";
const inquiry = body.inquiry?.trim() || body.message?.trim() || "";
const phone = body.phone?.trim() ?? "";
const university =
body.university === "Other"
? body.customUniversity?.trim() ?? ""
: body.university?.trim() ?? "";

return { name, email, inquiry, phone, university };
}

export async function POST(request: Request) {
try {
const { name, email, inquiry } = await request.json();
const apiKey = process.env.RESEND_API_KEY;

if (!apiKey) {
return NextResponse.json(
{ error: "Missing RESEND_API_KEY environment variable" },
{ status: 500 }
);
}

const body = (await request.json()) as ContactRequestBody;
const { name, email, inquiry, phone, university } = normalizeContactRequest(body);

// Validate required fields
if (!name || !email || !inquiry) {
Expand All @@ -25,24 +71,35 @@ export async function POST(request: Request) {
);
}

const resend = new Resend(apiKey);
const safeName = escapeHtml(name);
const safeEmail = escapeHtml(email);
const safePhone = escapeHtml(phone);
const safeUniversity = escapeHtml(university);
const safeInquiry = escapeHtml(inquiry).replace(/\n/g, "<br>");

// Send email using Resend
const { data, error } = await resend.emails.send({
from: "PushQuantum Contact Form <onboarding@resend.dev>",
to: ["info@pushquantum.tech"],
to: [recipientEmail],
replyTo: email,
subject: `New Contact Form Submission from ${name}`,
html: `
<h2>New Contact Form Submission</h2>
<p><strong>Name:</strong> ${name}</p>
<p><strong>Email:</strong> ${email}</p>
<p><strong>Name:</strong> ${safeName}</p>
<p><strong>Email:</strong> ${safeEmail}</p>
${phone ? `<p><strong>Phone:</strong> ${safePhone}</p>` : ""}
${university ? `<p><strong>Institution:</strong> ${safeUniversity}</p>` : ""}
<p><strong>Inquiry:</strong></p>
<p>${inquiry.replace(/\n/g, "<br>")}</p>
<p>${safeInquiry}</p>
`,
text: `
New Contact Form Submission

Name: ${name}
Email: ${email}
${phone ? `Phone: ${phone}` : ""}
${university ? `Institution: ${university}` : ""}

Inquiry:
${inquiry}
Expand Down
18 changes: 18 additions & 0 deletions app/hackathon/challenge-partners/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from "next";
import { HackathonChallengeHero } from "@/components/hackathon/hackathon-challenge-hero";
import { HackathonChallengePartners } from "@/components/hackathon/hackathon-challenge-partners";

export const metadata: Metadata = {
title: "Challenge Partners - PushQuantum Hackathon 2026",
description:
"Meet the industry and research challenge partners bringing real-world quantum problems to the PushQuantum Hackathon, November 20-22, 2026.",
};

export default function ChallengePartnersPage() {
return (
<>
<HackathonChallengeHero />
<HackathonChallengePartners />
</>
);
}
30 changes: 30 additions & 0 deletions app/hackathon/conference/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { ConferenceHero } from "@/components/conference/conference-hero";
import { EventAgenda } from "@/components/events/event-agenda";
import { ConferenceTalks } from "@/components/conference/conference-talks";
import { ConferenceScienceSlam } from "@/components/conference/conference-science-slam";
import { hackathon2026 } from "@/lib/hackathon-2026-data";

export const metadata: Metadata = {
title: "Conference - PushQuantum Hackathon 2026",
description:
"Frontiers in Quantum Science and Technology — talks, itinerary and Science Slam details for the PushQuantum Conference on November 20, 2026.",
};

// Reuse the Nov 20 agenda already defined for the combined event,
// scoped down to just the Conference day.
const conferenceOnlyEvent = {
...hackathon2026,
agenda: hackathon2026.agenda ? [hackathon2026.agenda[0]] : [],
};

export default function ConferencePage() {
return (
<>
<ConferenceHero />
<EventAgenda event={conferenceOnlyEvent} />
<ConferenceTalks />
<ConferenceScienceSlam />
</>
);
}
32 changes: 32 additions & 0 deletions app/hackathon/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Metadata } from "next";
import { EventHero } from "@/components/events/event-hero";
import { HackathonTracks } from "@/components/hackathon/hackathon-tracks";
import { EventKeyFacts } from "@/components/events/event-key-facts";
import { EventAgenda } from "@/components/events/event-agenda";
import { HackathonPartners } from "@/components/hackathon/hackathon-partners";
import { EventFaq } from "@/components/events/event-faq";
import { HackathonContact } from "@/components/hackathon/hackathon-contact";
import { hackathon2026 } from "@/lib/hackathon-2026-data";

export const metadata: Metadata = {
title: "Conference + Hackathon 2026 - PushQuantum",
description: hackathon2026.shortDescription,
};

// Only the hero shows the sign-up button; the other sections
// get a variant without signUpUrl so they don't render their own.
const eventWithoutSignUp = { ...hackathon2026, signUpUrl: undefined };

export default function HackathonPage() {
return (
<>
<EventHero event={hackathon2026} ctaVariant="wide-centered" />
<HackathonTracks />
<EventKeyFacts event={eventWithoutSignUp} />
<EventAgenda event={hackathon2026} />
<HackathonPartners event={hackathon2026} />
<EventFaq event={eventWithoutSignUp} allowMultipleOpen />
<HackathonContact event={eventWithoutSignUp} />
</>
);
}
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={`font-sans antialiased`}>
<body className={`font-sans antialiased`} suppressHydrationWarning>
<Header />
{children}
<Analytics />
Expand Down
18 changes: 18 additions & 0 deletions app/partners/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Partners - PushQuantum",
description: "PushQuantum partners page.",
};

export default function PartnersPage() {
return (
<main className="bg-white px-6 pt-32 pb-16">
<div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center text-center">
<h1 className="text-3xl font-bold text-pq-dark-purple sm:text-4xl">
Under construction <span className="text-2xl sm:text-3xl">🚧</span>
</h1>
</div>
</main>
);
}
6 changes: 3 additions & 3 deletions components/about/events-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { ArrowRight } from "lucide-react";

const events = [
{
date: { month: "OCT", day: "31" },
title: "PushQuantum Hackathon",
date: { month: "NOV", day: "20" },
title: "Conference + Hackathon 2026",
description: "A 3-day challenge of full immersion in the world of quantum technologies",
href: "#",
href: "/hackathon",
},
{
date: { month: "NOV", day: "02" },
Expand Down
37 changes: 37 additions & 0 deletions components/conference/conference-hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Link from "next/link";
import { ArrowLeft } from "lucide-react";

export function ConferenceHero() {
return (
<section className="relative min-h-[320px] overflow-hidden pt-24">
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: "url('/images/events/placeholder-quantum-1.png')" }}
/>
<div className="absolute inset-0 bg-pq-dark-purple/80" />

<div className="relative z-10 mx-auto max-w-4xl px-6 py-16">
<Link
href="/hackathon"
className="inline-flex items-center gap-1.5 text-sm text-gray-300 hover:text-white transition-colors mb-4"
>
<ArrowLeft className="size-4" />
Back to Conference + Hackathon 2026
</Link>

<p className="text-sm font-semibold uppercase tracking-widest text-sky-400 mb-2">
Conference · Friday, November 20
</p>
<h1 className="text-3xl font-bold text-white sm:text-4xl lg:text-5xl mb-4">
Frontiers in Quantum Science and Technology
</h1>
<p className="text-gray-300 text-base lg:text-lg leading-relaxed max-w-2xl">
The Frontiers in Quantum Science and Technology conference unites researchers and industry leaders to discuss the latest breakthroughs and emerging trends in quantum computing, sensing, communication, and simulation. It aims to bridge fundamental research with real-world applications, driving innovation at the forefront of quantum technology.
<br></br>
<br></br>
Whether you're a student mapping out your next steps in quantum tech or a researcher looking to connect with leading companies, FQST is where the community comes together.
</p>
</div>
</section>
);
}
39 changes: 39 additions & 0 deletions components/conference/conference-science-slam.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { scienceSlam } from "@/lib/conference-2026-data";

export function ConferenceScienceSlam() {
return (
<section className="py-16 px-6 bg-[#f9fafb]">
<div className="mx-auto max-w-3xl">
<h2 className="text-2xl font-bold text-pq-dark-purple sm:text-3xl mb-8">
Science Slam
</h2>

<div className="flex flex-col gap-6">
<p className="text-gray-600 leading-relaxed">{scienceSlam.intro}</p>
<p className="text-gray-600 leading-relaxed">{scienceSlam.format}</p>

<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-2">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<h3 className="font-bold text-pq-dark-purple text-sm uppercase tracking-wide">
Application window
</h3>
<p className="text-gray-600 mt-1">
{scienceSlam.applicationWindow}
</p>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<h3 className="font-bold text-pq-dark-purple text-sm uppercase tracking-wide">
Live slot
</h3>
<p className="text-gray-600 mt-1">{scienceSlam.slot}</p>
</div>
</div>

<p className="text-gray-600 leading-relaxed">
{scienceSlam.applicationNote}
</p>
</div>
</div>
</section>
);
}
48 changes: 48 additions & 0 deletions components/conference/conference-talks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { conferenceTalks } from "@/lib/conference-2026-data";

export function ConferenceTalks() {
return (
<section className="py-16 px-6 bg-white">
<div className="mx-auto max-w-3xl">
<h2 className="text-2xl font-bold text-pq-dark-purple sm:text-3xl mb-2">
Talks & abstracts
</h2>
<p className="text-gray-600 mb-8">
Abstracts below are placeholders and will be updated as speakers
confirm their talks.
</p>

<Accordion type="single" collapsible className="w-full">
{conferenceTalks.map((talk, idx) => (
<AccordionItem key={idx} value={`talk-${idx}`}>
<AccordionTrigger className="text-left text-pq-dark-purple font-medium">
<div className="flex flex-col items-start gap-1">
<span className="text-xs font-semibold uppercase tracking-widest text-pq-bright-pink">
{talk.time} · {talk.kind}
</span>
<span>
{talk.title}
<span className="text-gray-500 font-normal">
{" "}
— {talk.speaker}
{talk.affiliation ? `, ${talk.affiliation}` : ""}
</span>
</span>
</div>
</AccordionTrigger>
<AccordionContent className="text-gray-600">
{talk.abstract}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</section>
);
}
Loading
Loading