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
Empty file added docs/BACKEND-COHORT-SIGNUP.md
Empty file.
94 changes: 91 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -534,19 +534,107 @@
}

/* Contact Form Input Styles */
.contact-form-input {
.contact-form-input,
.contact-form-input-scroll-100 {
height: 40px;
border-radius: 0.375rem;
border-color: var(--moloch-800);
border: 1px solid var(--moloch-800);
background-color: var(--scroll-100);
color: var(--moloch-800);
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
}

.contact-form-input::placeholder {
.contact-form-input::placeholder,
.contact-form-input-scroll-100::placeholder {
color: rgb(from var(--moloch-800) r g b / 0.6);
}

.contact-form-label-scroll-100 {
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
display: block;
min-height: 24px;
color: var(--moloch-800);
}

/* Input on moloch-500 background */
.contact-form-input-moloch-500 {
height: 40px;
border-radius: 0.375rem;
border: 1px solid var(--moloch-800);
background-color: var(--moloch-500);
color: var(--scroll-100);
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
}

.contact-form-input-moloch-500::placeholder {
color: rgb(from var(--scroll-100) r g b / 0.6);
}

.contact-form-label-moloch-500 {
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
display: block;
min-height: 24px;
color: var(--scroll-100);
}

/* Input on moloch-800 background */
.contact-form-input-moloch-800 {
height: 40px;
border-radius: 0.375rem;
border: 1px solid var(--scroll-100);
background-color: var(--moloch-800);
color: var(--scroll-100);
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
}

.contact-form-input-moloch-800::placeholder {
color: rgb(from var(--scroll-100) r g b / 0.6);
}

.contact-form-label-moloch-800 {
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
display: block;
min-height: 24px;
color: var(--scroll-100);
}

/* Input on scroll-700 background */
.contact-form-input-scroll-700 {
height: 40px;
border-radius: 0.375rem;
border: 1px solid var(--scroll-100);
background-color: var(--scroll-700);
color: var(--scroll-100);
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
}

.contact-form-input-scroll-700::placeholder {
color: rgb(from var(--scroll-100) r g b / 0.6);
}

.contact-form-label-scroll-700 {
font-size: var(--font-size-body-base);
font-family: var(--font-body);
font-weight: 400;
display: block;
min-height: 24px;
color: var(--scroll-100);
}

.contact-form-textarea {
min-height: 80px;
border-radius: 0.375rem;
Expand Down
161 changes: 85 additions & 76 deletions src/components/CohortHero.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"use client";

import Image from "next/image";
import { Button } from "./ui/button";
import { useState, useEffect } from "react";
import { useState } from "react";
import { Input } from "./ui/input";
import { trackEvent } from "fathom-client";

const cohortImages = [
"/images/cohort-image-1-bw.webp",
Expand All @@ -11,69 +12,58 @@ const cohortImages = [
"/images/cohort-image-2-c.webp",
];

const DESKTOP_BREAKPOINT = "(min-width: 1024px)";
const DESKTOP_THIN_HEIGHT = 96;
const MOBILE_HEADER_HEIGHT = 72;

export default function CohortHero() {
// Deterministic image selection based on 10-minute intervals (no flash, no hydration mismatch)
const interval = Math.floor(Date.now() / (1000 * 60 * 10)); // 10 minutes
const imageSrc = cohortImages[interval % cohortImages.length];

const [isDesktop, setIsDesktop] = useState<boolean>(false);

useEffect(() => {
if (typeof window === "undefined") return;

const mediaQuery = window.matchMedia(DESKTOP_BREAKPOINT);
setIsDesktop(mediaQuery.matches);

const listener = (event: MediaQueryListEvent) =>
setIsDesktop(event.matches);
mediaQuery.addEventListener("change", listener);

return () => mediaQuery.removeEventListener("change", listener);
}, []);

const handleNavigate = (href: string) => {
if (typeof window === "undefined" || !href.startsWith("#")) return;

const id = href.slice(1);
const target = document.getElementById(id);
if (!target) return;

const targetTop = window.scrollY + target.getBoundingClientRect().top - 1;

let offset;
if (!isDesktop) {
// Mobile: simple offset (72 - 12 = 60px)
offset = MOBILE_HEADER_HEIGHT;
} else {
// Desktop: At scroll 0-20: offset = 96 + 100 = 196px, At scroll 21+: offset = 96px
const headerShrinkAdjustment = window.scrollY <= 20 ? 100 : 0;
offset = DESKTOP_THIN_HEIGHT + headerShrinkAdjustment;
}

const safeOffset = Math.max(offset - (isDesktop ? 16 : 12), 0);
const destination = Math.max(targetTop - safeOffset, 0);

window.scrollTo({
top: destination,
behavior: "smooth",
});

if (typeof window.history?.replaceState === "function") {
window.history.replaceState(null, "", href);
const [email, setEmail] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionStatus, setSubmissionStatus] = useState<
"idle" | "success" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState("");

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting || !email) return;

setIsSubmitting(true);
setSubmissionStatus("idle");
setErrorMessage("");

try {
const response = await fetch("/api/cohort-signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});

const result = await response.json();

if (response.ok) {
setSubmissionStatus("success");
trackEvent("cohort-hero-email-signup");
setEmail("");
} else {
setSubmissionStatus("error");
setErrorMessage(result.error || "Failed to submit. Please try again.");
}
Comment on lines +20 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Check result.success before treating the signup as successful.

The API contract returns { success: false, error } on failure; treating any 2xx as success can show a success state and fire analytics incorrectly.

🛠️ Suggested fix
-      if (response.ok) {
+      if (response.ok && result?.success) {
         setSubmissionStatus("success");
         trackEvent("cohort-hero-email-signup");
         setEmail("");
       } else {
         setSubmissionStatus("error");
-        setErrorMessage(result.error || "Failed to submit. Please try again.");
+        setErrorMessage(result?.error || "Failed to submit. Please try again.");
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [email, setEmail] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionStatus, setSubmissionStatus] = useState<
"idle" | "success" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting || !email) return;
setIsSubmitting(true);
setSubmissionStatus("idle");
setErrorMessage("");
try {
const response = await fetch("/api/cohort-signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const result = await response.json();
if (response.ok) {
setSubmissionStatus("success");
trackEvent("cohort-hero-email-signup");
setEmail("");
} else {
setSubmissionStatus("error");
setErrorMessage(result.error || "Failed to submit. Please try again.");
}
const [email, setEmail] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionStatus, setSubmissionStatus] = useState<
"idle" | "success" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting || !email) return;
setIsSubmitting(true);
setSubmissionStatus("idle");
setErrorMessage("");
try {
const response = await fetch("/api/cohort-signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
const result = await response.json();
if (response.ok && result?.success) {
setSubmissionStatus("success");
trackEvent("cohort-hero-email-signup");
setEmail("");
} else {
setSubmissionStatus("error");
setErrorMessage(result?.error || "Failed to submit. Please try again.");
}
🤖 Prompt for AI Agents
In `@src/components/CohortHero.tsx` around lines 20 - 51, The handler handleSubmit
currently treats any 2xx fetch as success; change it to inspect the parsed
response object (result) and require result.success === true before setting
submissionStatus("success"), calling trackEvent("cohort-hero-email-signup"), or
clearing email; otherwise set submissionStatus("error") and
setErrorMessage(result.error || "Failed to submit. Please try again."). Ensure
the JSON is parsed (const result = await response.json()) before the success
check and do not call trackEvent or clear the email on failure.

} catch {
setSubmissionStatus("error");
setErrorMessage("Network error. Please try again.");
} finally {
setIsSubmitting(false);
}
};

return (
<section id="cohort-hero" className="relative bg-moloch-800">
<div className="container-custom min-h-[795px]">
<div className="container-custom py-12 lg:py-24 lg:pt-36">
<div className="grid-custom gap-4">
<div className="col-span-4 md:col-span-8 lg:col-span-6 flex flex-col items-center gap-[60px] order-2 lg:order-1">
<div className="flex flex-col gap-10">
<h1 className="text-heading-lg text-scroll-100 text-center lg:pt-44 pt-12">
<h1 className="text-heading-lg text-scroll-100 text-center">
FORGE YOUR PATH.
<br />
EARN YOUR SEAT.
Expand All @@ -94,32 +84,51 @@ export default function CohortHero() {
width={300}
height={36}
/>
<div className="flex flex-col md:flex-row gap-4 w-full pb-12 lg:pb-0">
<Button
variant="primary"
className="w-full md:flex-1 cohort-btn-apply"
data-click="apply-cohort-hero"
onClick={(e) => {
e.preventDefault();
handleNavigate("#join-us");
}}
>
<span className="text-label">APPLY NOW</span>
</Button>
<Button
variant="secondary"
className="w-full md:flex-1 cohort-btn-learn"
data-click="learn-more-cohort-hero"
onClick={(e) => {
e.preventDefault();
handleNavigate("#cohort-process");
}}
>
<span className="text-label">LEARN MORE</span>
</Button>
<div className="flex flex-col gap-4 w-full">
<h2 className="text-heading-md text-scroll-100 text-center">
Pledge now, or venture forth for the full tale.
</h2>
{submissionStatus === "success" ? (
<p className="text-body-lg text-scroll-100 text-center">
Check your inbox for next steps.
</p>
) : (
<form
onSubmit={handleSubmit}
className="flex flex-col gap-4 w-full lg:w-4/5 mx-auto"
>
<div className="flex flex-col gap-2">
<label
htmlFor="hero-email"
className="contact-form-label-moloch-800"
>
Enter your email address
</label>
<Input
id="hero-email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="contact-form-input-moloch-800 w-full"
/>
</div>
{submissionStatus === "error" && (
<p className="text-body-md text-red-400">{errorMessage}</p>
)}
<button
type="submit"
disabled={isSubmitting}
className="contact-btn-active"
>
{isSubmitting ? "Submitting..." : "Begin My Quest"}
</button>
</form>
)}
</div>
</div>
<div className="col-span-4 md:col-span-8 lg:col-span-6 order-1 lg:order-2 lg:pt-32 pt-12">
<div className="col-span-4 md:col-span-8 lg:col-span-6 order-1 lg:order-2">
<Image
src={imageSrc}
alt="Cohort Hero"
Expand Down
20 changes: 2 additions & 18 deletions src/components/CohortValueSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,14 @@ function ValueCard({ item }: { item: ValueItem }) {
{item.title}
</h3>
</div>
<div className="bg-moloch-800 p-6 border-t-2 border-scroll-100 flex flex-col min-h-[280px]">
<div className="bg-moloch-800 p-6 border-t-2 border-scroll-100 flex flex-col min-h-[200px]">
<p className="text-body-lg text-scroll-100 mb-4 flex-grow">
{item.description}
</p>
<div className="border-l-4 border-scroll-100 pl-4">
<p className="text-body-lg text-scroll-150 italic mb-3">
<p className="text-body-lg text-scroll-150 italic">
&ldquo;{item.testimonial}&rdquo;
</p>
<p className="text-body-md text-scroll-100">
— {item.author}, {item.role}
</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -249,19 +246,6 @@ export default function CohortValueSection() {
deeply about the quality of your craft.
</p>
</div>

{/* Self-Assessment */}
<div>
<h3 className="text-heading-md text-scroll-100 mb-4">
Self-Assessment
</h3>
<p className="text-body-lg text-scroll-150">
Can you commit 10-20 hours per week to the campaign? Do you
have victories that showcase your skills? Are you ready to be
judged by your deeds? Do you thrive charting your own course
in async realms? If yes, you&apos;re ready to raid.
</p>
</div>
</div>
</div>
</div>
Expand Down
Loading