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
4 changes: 3 additions & 1 deletion env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
HASURA_GRAPHQL_ADMIN_SECRET=
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_API_URL=
EMAIL_REFERRALS_API_URL=
FORM_INGEST_API_KEY=
91 changes: 91 additions & 0 deletions src/app/api/email-referrals/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

const emailReferralSchema = z.object({
email: z.email("Valid email is required"),
referral: z.string().optional(),
});

export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validationResult = emailReferralSchema.safeParse(body);

if (!validationResult.success) {
const errors = validationResult.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
}));

return NextResponse.json(
{
success: false,
error: "Validation failed",
details: errors,
},
{ status: 400 }
);
}

const apiUrl = process.env.EMAIL_REFERRALS_API_URL;
const apiKey = process.env.FORM_INGEST_API_KEY;

if (!apiUrl || !apiKey) {
return NextResponse.json(
{
success: false,
error: "Email referral API is not configured",
},
{ status: 500 }
);
}

const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-form-api-key": apiKey,
},
body: JSON.stringify(validationResult.data),
});

const contentType = response.headers.get("content-type") || "";
const responseBody = contentType.includes("application/json")
? await response.json()
: await response.text();

if (!response.ok) {
return NextResponse.json(
{
success: false,
error:
(typeof responseBody === "object" &&
responseBody &&
"error" in responseBody &&
responseBody.error) ||
responseBody ||
"Failed to submit email referral",
},
{ status: response.status }
);
}

return NextResponse.json(
{
success: true,
data: responseBody || { ok: true },
},
{ status: 200 }
);
} catch (error) {
console.error("Error submitting email referral:", error);

return NextResponse.json(
{
success: false,
error: "Failed to submit email referral",
},
{ status: 500 }
);
}
}
13 changes: 11 additions & 2 deletions src/app/join/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ import CohortValueSection from "@/components/CohortValueSection";
import CohortJoinBanner from "@/components/CohortJoinBanner";
import JoinUsSection from "@/components/JoinUsSection";

export default function Home() {
type JoinPageProps = {
searchParams?: {
ref?: string;
referral?: string;
};
};

export default function Home({ searchParams }: JoinPageProps) {
const referral = searchParams?.referral ?? searchParams?.ref;

return (
<div className="min-h-screen bg-background">
<HeaderJoin />
Expand All @@ -26,7 +35,7 @@ export default function Home() {

<CohortJoinBanner />

<JoinUsSection />
<JoinUsSection referral={referral} />

<Footer />
</div>
Expand Down
151 changes: 25 additions & 126 deletions src/components/JoinUs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Form,
FormControl,
Expand All @@ -14,11 +13,7 @@ import {
FormLabel,
RequiredFieldIndicator,
} from "@/components/ui/form";
import {
joinUsFormSchema,
type JoinUsFormData,
transformApplicationDataToApiFormat,
} from "@/lib/validation";
import { joinUsFormSchema, type JoinUsFormData } from "@/lib/validation";
import Image from "next/image";
import { trackEvent } from "fathom-client";

Expand All @@ -29,7 +24,11 @@ const joinUsImages = [
"/images/join-image-2-c.webp",
];

export default function JoinUs() {
type JoinUsProps = {
referral?: string;
};

export default function JoinUs({ referral }: JoinUsProps) {
// Deterministic image selection based on 8-minute intervals (no flash, no hydration mismatch)
const interval = Math.floor(Date.now() / (1000 * 60 * 8)); // 8 minutes
const imageSrc = joinUsImages[interval % joinUsImages.length];
Expand All @@ -39,55 +38,50 @@ export default function JoinUs() {
"idle" | "success" | "error"
>("idle");
const [errorMessage, setErrorMessage] = useState<string>("");

const form = useForm<JoinUsFormData>({
resolver: zodResolver(joinUsFormSchema),
defaultValues: {
name: "",
email: "",
discordHandle: "",
showcaseComments: "",
showcaseUrl: "",
introduction: "",
},
});

const onSubmit = async (data: JoinUsFormData) => {
if (isSubmitting) return; // Prevent multiple submissions

console.log("Join Us form submitted:", data);
console.log("Join Us email submitted:", data);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging emails/results to the console (PII exposure).
Client logs can be captured by monitoring tools or shared devices; redact or remove.

🧹 Proposed fix (remove/redact PII from logs)
-    console.log("Join Us email submitted:", data);
+    console.log("Join Us email submitted");

...
-        console.log("Email referral submitted successfully:", result);
+        console.log("Email referral submitted successfully");

...
-        console.error("Failed to submit email referral:", result);
+        console.error("Failed to submit email referral");

Also applies to: 73-85

🤖 Prompt for AI Agents
In `@src/components/JoinUs.tsx` at line 51, Remove or redact PII from console logs
in the JoinUs component: eliminate the console.log("Join Us email submitted:",
data) in the submit handler (and any other console.log calls in the same
component around lines 73-85) so emails/submit results are not written to client
logs; if lightweight debugging is required, log only non-PII (e.g.,
success/failure boolean or anonymized/id-only info) using the same handler
functions or submitEmail function names to locate the places to change.


// Reset states
setIsSubmitting(true);
setSubmissionStatus("idle");
setErrorMessage("");

try {
const applicationData = transformApplicationDataToApiFormat(data);

const response = await fetch("/api/applications", {
const response = await fetch("/api/email-referrals", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ applicationData }),
body: JSON.stringify({
email: data.email,
...(referral ? { referral } : {}),
}),
});

const result = await response.json();

if (response.ok) {
console.log("Application submitted successfully:", result);
console.log("Email referral submitted successfully:", result);
setSubmissionStatus("success");

//tracking
trackEvent("join-us-submission");
// Reset form after successful submission
form.reset();
} else {
console.error("Failed to submit application:", result);
console.error("Failed to submit email referral:", result);
setSubmissionStatus("error");
setErrorMessage(
result.error || "Failed to submit application. Please try again.",
result.error || "Failed to submit. Please try again.",
);
}
} catch (error) {
Expand All @@ -105,7 +99,7 @@ export default function JoinUs() {
const SuccessState = () => (
<div className="text-center space-y-4 p-8">
<h3 className="font-body text-3xl font-bold text-moloch-500">
Your Words Have Been Passed On.
You&apos;re On The List.
</h3>
<div className="flex items-center justify-center">
<Image
Expand All @@ -116,15 +110,9 @@ export default function JoinUs() {
className="flex-shrink-0"
/>
</div>
<div className="pt-12 w-full">
<a
href="https://discord.gg/raidguild"
target="_blank"
className="text-body-lg text-moloch-500 hover:text-moloch-800"
>
Look for the Tavern Keeper in Discord
</a>
</div>
<p className="text-body-lg text-moloch-500">
Watch your inbox for next steps.
</p>
</div>
);

Expand Down Expand Up @@ -184,13 +172,11 @@ export default function JoinUs() {
</h3>
{submissionStatus === "success" ? (
<p className="text-body-md">
Thank you for your interest in joining RaidGuild!
Thanks for joining the cohort updates.
</p>
) : (
<p className="text-body-lg font-body">
Ready to embark on your journey and join the ranks? Share
your tale with us—what epic skills await the Guild&apos;s
discovery?
Drop your email to start the onboarding journey.
</p>
)}
</div>
Expand All @@ -211,103 +197,16 @@ export default function JoinUs() {
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
Name <RequiredFieldIndicator />
</FormLabel>
<FormControl>
<Input
placeholder="Enter your full name"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
Email Address <RequiredFieldIndicator />
</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Enter your email"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="discordHandle"
render={({ field }) => (
<FormItem>
<FormLabel>Discord Username</FormLabel>
<FormControl>
<Input placeholder="username#1234" {...field} />
</FormControl>
</FormItem>
)}
/>
</div>

<FormField
control={form.control}
name="introduction"
render={({ field }) => (
<FormItem>
<FormLabel>
Introduce Yourself <RequiredFieldIndicator />
</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about yourself, your skills, and why you want to join Raid Guild..."
{...field}
/>
</FormControl>
</FormItem>
)}
/>

<FormField
control={form.control}
name="showcaseComments"
render={({ field }) => (
<FormItem>
<FormLabel>
Work You&apos;re Proud Of{" "}
<RequiredFieldIndicator />
</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us about a project, portfolio, or piece of work you're particularly proud of."
{...field}
/>
</FormControl>
</FormItem>
)}
/>

<FormField
control={form.control}
name="showcaseUrl"
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
Link to Your Work <RequiredFieldIndicator />
Email Address <RequiredFieldIndicator />
</FormLabel>
<FormControl>
<Input
type="url"
placeholder="https://github.com/username, https://portfolio.com, https://linkedin.com/in/username, etc."
type="email"
placeholder="you@domain.com"
{...field}
/>
</FormControl>
Expand Down
8 changes: 6 additions & 2 deletions src/components/JoinUsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import JoinUs from "./JoinUs";

export default function JoinUsSection() {
return <JoinUs />;
type JoinUsSectionProps = {
referral?: string;
};

export default function JoinUsSection({ referral }: JoinUsSectionProps) {
return <JoinUs referral={referral} />;
}
Loading