Skip to content

Commit 6935912

Browse files
committed
add sponsorship page
1 parent 9f92f92 commit 6935912

5 files changed

Lines changed: 147 additions & 37 deletions

File tree

app/(main)/(sponsor)/sponsors/page/[num]/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { BecomeSponsorPopup } from "@/components/become-sponsor-popup";
2+
import { Button } from "@/components/ui/button";
3+
import Link from "next/link";
14
import MoreContent from "@/components/more-content";
25
import type { DocCountResult } from "@/sanity/types";
36
import { sanityFetch } from "@/sanity/lib/live";
@@ -28,13 +31,21 @@ export default async function Page({ params }: { params: Params }) {
2831

2932
return (
3033
<div className="container px-5 mx-auto mb-32">
34+
<div className="flex flex-col items-center my-8">
35+
<h2 className="text-2xl font-bold">Want to see your name here?</h2>
36+
<p className="text-lg text-muted-foreground">Become a sponsor and support our content.</p>
37+
<Button asChild className="mt-4">
38+
<Link href="/sponsorships">Become a Sponsor</Link>
39+
</Button>
40+
</div>
3141
<MoreContent type="sponsor" limit={limit} offset={offset} showHeader />
3242
<PaginateList
3343
base="sponsors"
3444
num={Number(num)}
3545
limit={LIMIT}
3646
count={count}
3747
/>
48+
<BecomeSponsorPopup />
3849
</div>
3950
);
4051
}

app/(main)/(top-level-pages)/sponsorships/page.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
'use client';
22

33
import { z } from "zod";
4+
import { toast } from "sonner";
5+
import { useRouter } from "next/navigation";
6+
import { useState } from "react";
47
import { useForm } from "react-hook-form";
58
import { zodResolver } from "@hookform/resolvers/zod";
69
import { Button } from "@/components/ui/button";
@@ -81,6 +84,8 @@ const formSchema = z.object({
8184
});
8285

8386
export default function SponsorshipsPage() {
87+
const [isSuccess, setIsSuccess] = useState(false);
88+
const router = useRouter();
8489
const form = useForm<z.infer<typeof formSchema>>({
8590
resolver: zodResolver(formSchema),
8691
defaultValues: {
@@ -97,36 +102,49 @@ export default function SponsorshipsPage() {
97102
async function onSubmit(values: z.infer<typeof formSchema>) {
98103
// Honeypot check
99104
if (values.honeypot) {
100-
console.log("Honeypot field filled out, ignoring submission.");
105+
toast.error("Spam detected!");
101106
return;
102107
}
103-
// TODO: Add Cloudflare Turnstile verification here
104108

105109
const response = await fetch("/api/sponsorship", {
106110
method: "POST",
107111
headers: {
108112
"Content-Type": "application/json",
109113
},
110-
// We only send the values that the API route expects, excluding the turnstile response
111-
// which is verified on the server from the body.
112-
// body: JSON.stringify({
113-
// ...values,
114-
// // The turnstile token is automatically included in the form data
115-
// // and will be sent to the server.
116-
// }),
117114
body: JSON.stringify(values),
118115
});
119116

117+
const result = await response.json();
118+
120119
if (response.ok) {
120+
toast.success("Sponsorship request submitted successfully!");
121121
form.reset();
122-
// TODO: Show a success message to the user
123-
console.log("Sponsorship request submitted successfully!");
122+
setIsSuccess(true);
123+
setTimeout(() => {
124+
router.push("/sponsors/page/1");
125+
}, 3000);
124126
} else {
125-
// TODO: Show an error message to the user
126-
console.error("Failed to submit sponsorship request.");
127+
toast.error(result.message, {
128+
description: result.details ? JSON.stringify(result.details) : "",
129+
});
130+
if (result.message === "Invalid CAPTCHA") {
131+
window.location.reload();
132+
}
127133
}
128134
}
129135

136+
if (isSuccess) {
137+
return (
138+
<div className="container px-5 mx-auto">
139+
<div className="w-full flex flex-col gap-4 md:gap-8 my-8 md:my-12 items-center">
140+
<h1 className="text-3xl font-bold">Thank you for your submission!</h1>
141+
<p>We will get back to you shortly.</p>
142+
<p>Redirecting you to our sponsors page...</p>
143+
</div>
144+
</div>
145+
);
146+
}
147+
130148
return (
131149
<div className="container px-5 mx-auto">
132150
<div className="w-full flex flex-col gap-4 md:gap-8 my-8 md:my-12">

app/api/sponsorship/route.ts

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11

22
import { NextResponse } from "next/server";
33
import { z } from "zod";
4-
import { client } from "@/sanity/lib/client";
4+
import { apiVersion, dataset, projectId } from "@/sanity/lib/api";
5+
import { createClient } from "next-sanity";
6+
7+
const sanityWriteClient = createClient({
8+
projectId,
9+
dataset,
10+
apiVersion,
11+
token: process.env.SANITY_API_WRITE_TOKEN,
12+
perspective: "published",
13+
useCdn: false,
14+
});
515

616
const formSchema = z.object({
717
fullName: z.string(),
@@ -10,6 +20,7 @@ const formSchema = z.object({
1020
sponsorshipTier: z.array(z.string()),
1121
message: z.string().optional(),
1222
honeypot: z.string().optional(),
23+
"cf-turnstile-response": z.string(),
1324
});
1425

1526
export async function POST(request: Request) {
@@ -23,34 +34,36 @@ export async function POST(request: Request) {
2334
sponsorshipTier,
2435
message,
2536
honeypot,
37+
"cf-turnstile-response": turnstileToken,
2638
} = formSchema.parse(body);
2739

2840
// Honeypot check
2941
if (honeypot) {
3042
return NextResponse.json({ message: "Spam detected" }, { status: 400 });
3143
}
3244

33-
// TODO: Verify Cloudflare Turnstile token
34-
// const turnstileToken = request.headers.get("X-Turnstile-Token");
35-
// const ip = request.headers.get("CF-Connecting-IP");
36-
// const turnstileResponse = await fetch(
37-
// "https://challenges.cloudflare.com/turnstile/v0/siteverify",
38-
// {
39-
// method: "POST",
40-
// headers: {
41-
// "Content-Type": "application/json",
42-
// },
43-
// body: JSON.stringify({
44-
// secret: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY,
45-
// response: turnstileToken,
46-
// remoteip: ip,
47-
// }),
48-
// }
49-
// );
50-
// const turnstileData = await turnstileResponse.json();
51-
// if (!turnstileData.success) {
52-
// return NextResponse.json({ message: "Spam detected" }, { status: 400 });
53-
// }
45+
const ip = request.headers.get("CF-Connecting-IP");
46+
const turnstileResponse = await fetch(
47+
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
48+
{
49+
method: "POST",
50+
headers: {
51+
"Content-Type": "application/json",
52+
},
53+
body: JSON.stringify({
54+
secret: process.env.CLOUDFLARE_TURNSTILE_SECRET_KEY,
55+
response: turnstileToken,
56+
remoteip: ip,
57+
}),
58+
}
59+
);
60+
const turnstileData = await turnstileResponse.json();
61+
if (!turnstileData.success) {
62+
return NextResponse.json(
63+
{ message: "Invalid CAPTCHA", details: turnstileData["error-codes"] },
64+
{ status: 400 }
65+
);
66+
}
5467

5568
const sponsorshipRequest = {
5669
_type: "sponsorshipRequest",
@@ -61,7 +74,14 @@ export async function POST(request: Request) {
6174
message,
6275
};
6376

64-
await client.create(sponsorshipRequest);
77+
try {
78+
await sanityWriteClient.create(sponsorshipRequest);
79+
} catch (error) {
80+
return NextResponse.json(
81+
{ message: "Failed to save sponsorship request", details: error },
82+
{ status: 500 }
83+
);
84+
}
6585

6686
return NextResponse.json({ message: "Sponsorship request submitted successfully" });
6787
} catch (error) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
'use client';
3+
4+
import { useEffect, useState } from 'react';
5+
import { Button } from '@/components/ui/button';
6+
import {
7+
AlertDialog,
8+
AlertDialogAction,
9+
AlertDialogCancel,
10+
AlertDialogContent,
11+
AlertDialogDescription,
12+
AlertDialogFooter,
13+
AlertDialogHeader,
14+
AlertDialogTitle,
15+
} from '@/components/ui/alert-dialog';
16+
import Link from 'next/link';
17+
18+
export function BecomeSponsorPopup() {
19+
const [isOpen, setIsOpen] = useState(false);
20+
21+
useEffect(() => {
22+
const timer = setTimeout(() => {
23+
setIsOpen(true);
24+
}, 5000); // 5 seconds
25+
26+
return () => clearTimeout(timer);
27+
}, []);
28+
29+
return (
30+
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
31+
<AlertDialogContent>
32+
<AlertDialogHeader>
33+
<AlertDialogTitle>Become a Sponsor!</AlertDialogTitle>
34+
<AlertDialogDescription>
35+
Enjoying the content? Help us keep it going by becoming a sponsor.
36+
You'll get your brand in front of a large audience of developers.
37+
</AlertDialogDescription>
38+
</AlertDialogHeader>
39+
<AlertDialogFooter>
40+
<AlertDialogCancel>Maybe later</AlertDialogCancel>
41+
<AlertDialogAction asChild>
42+
<Link href="/sponsorships">Learn More</Link>
43+
</AlertDialogAction>
44+
</AlertDialogFooter>
45+
</AlertDialogContent>
46+
</AlertDialog>
47+
);
48+
}

components/cloudflare-turnstile.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,23 @@
33

44
import { Turnstile } from '@marsidev/react-turnstile';
55

6-
export function CloudflareTurnstileWidget({ value, ...props }: { value?: string, [key: string]: any }) {
6+
export function CloudflareTurnstileWidget({
7+
value,
8+
onChange,
9+
...props
10+
}: {
11+
value?: string;
12+
onChange?: (token: string) => void;
13+
[key: string]: any;
14+
}) {
715
return (
816
<Turnstile
917
siteKey={process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY!}
18+
onSuccess={(token) => {
19+
if (onChange) {
20+
onChange(token);
21+
}
22+
}}
1023
{...props}
1124
/>
1225
);

0 commit comments

Comments
 (0)