Skip to content

Commit 3049fb3

Browse files
committed
feat(seo): Add JSON-LD schemas to homepage, pricing, and affiliates pages
- Homepage: WebSite, Organization, SoftwareApplication schemas with SearchAction - Pricing: Product schema with multiple offers (Free, Pay-as-you-go, Team, Enterprise) + BreadcrumbList - Affiliates: WebPage + Service schema for referral program + BreadcrumbList - All pages: Canonical URLs, metadata exports, OpenGraph/Twitter cards
1 parent 7e9a156 commit 3049fb3

File tree

6 files changed

+1237
-819
lines changed

6 files changed

+1237
-819
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
'use client'
2+
3+
import { env } from '@codebuff/common/env'
4+
import {
5+
CREDITS_REFERRAL_BONUS,
6+
AFFILIATE_USER_REFFERAL_LIMIT,
7+
} from '@codebuff/common/old-constants'
8+
import Link from 'next/link'
9+
import { useSession } from 'next-auth/react'
10+
import React, { useEffect, useState, useCallback } from 'react'
11+
import { useFormState, useFormStatus } from 'react-dom'
12+
13+
import { setAffiliateHandleAction } from './actions'
14+
15+
import type { SetHandleFormState } from './actions'
16+
17+
import CardWithBeams from '@/components/card-with-beams'
18+
import { SignInCardFooter } from '@/components/sign-in/sign-in-card-footer'
19+
import { Button } from '@/components/ui/button'
20+
import {
21+
Card,
22+
CardContent,
23+
CardDescription,
24+
CardHeader,
25+
CardTitle,
26+
} from '@/components/ui/card'
27+
import { Input } from '@/components/ui/input'
28+
import { Label } from '@/components/ui/label'
29+
import { Skeleton } from '@/components/ui/skeleton'
30+
import { useToast } from '@/components/ui/use-toast'
31+
32+
function SubmitButton() {
33+
const { pending } = useFormStatus()
34+
return (
35+
<Button type="submit" disabled={pending} aria-disabled={pending}>
36+
{pending ? 'Setting Handle...' : 'Set Handle'}
37+
</Button>
38+
)
39+
}
40+
41+
function SetHandleForm({
42+
onHandleSetSuccess,
43+
}: {
44+
onHandleSetSuccess: () => void
45+
}) {
46+
const { toast } = useToast()
47+
const initialState: SetHandleFormState = {
48+
message: '',
49+
success: false,
50+
fieldErrors: {},
51+
}
52+
const [state, formAction] = useFormState(
53+
setAffiliateHandleAction,
54+
initialState,
55+
)
56+
57+
useEffect(() => {
58+
if (state.message) {
59+
toast({
60+
title: state.success ? 'Success!' : 'Error',
61+
description: state.message,
62+
variant: state.success ? 'default' : 'destructive',
63+
})
64+
if (state.success) {
65+
onHandleSetSuccess()
66+
}
67+
}
68+
}, [state, toast, onHandleSetSuccess])
69+
70+
return (
71+
<form action={formAction} className="space-y-4">
72+
<div>
73+
<Label htmlFor="handle">Set Your Affiliate Handle</Label>
74+
<p className="text-sm text-muted-foreground mt-1">
75+
This will be part of your referral link (e.g.,
76+
codebuff.com/your_unique_handle).
77+
</p>
78+
<p className="text-sm text-muted-foreground mt-1">
79+
3-20 chars. letters, numbers, underscores only.
80+
</p>
81+
<Input
82+
id="handle"
83+
name="handle"
84+
type="text"
85+
required
86+
minLength={3}
87+
maxLength={20}
88+
pattern="^[a-zA-Z0-9_]+$"
89+
placeholder="your_unique_handle"
90+
aria-describedby="handle-error"
91+
className="mt-1"
92+
/>
93+
94+
{state.fieldErrors?.handle && (
95+
<p id="handle-error" className="text-sm text-red-600 mt-1">
96+
{state.fieldErrors.handle.join(', ')}
97+
</p>
98+
)}
99+
{!state.success && state.message && !state.fieldErrors?.handle && (
100+
<p className="text-sm text-red-600 mt-1">{state.message}</p>
101+
)}
102+
</div>
103+
<SubmitButton />
104+
</form>
105+
)
106+
}
107+
108+
export default function AffiliatesClient() {
109+
const { status: sessionStatus } = useSession()
110+
const [
111+
userProfile,
112+
setUserProfile,
113+
] = useState<{ handle: string | null; referralCode: string | null } | undefined>(
114+
undefined,
115+
)
116+
const [fetchError, setFetchError] = useState<string | null>(null)
117+
118+
const fetchUserProfile = useCallback(() => {
119+
setFetchError(null)
120+
fetch('/api/user/profile')
121+
.then(async (res) => {
122+
if (!res.ok) {
123+
const errorData = await res.json().catch(() => ({}))
124+
throw new Error(
125+
errorData.error || `HTTP error! status: ${res.status}`,
126+
)
127+
}
128+
return res.json()
129+
})
130+
.then((data) => {
131+
setUserProfile({
132+
handle: data.handle ?? null,
133+
referralCode: data.referral_code ?? null,
134+
})
135+
})
136+
.catch((error) => {
137+
console.error('Failed to fetch user profile:', error)
138+
setFetchError(error.message || 'Failed to load profile data.')
139+
setUserProfile({ handle: null, referralCode: null })
140+
})
141+
}, [])
142+
143+
useEffect(() => {
144+
if (sessionStatus === 'authenticated') {
145+
fetchUserProfile()
146+
} else if (sessionStatus === 'unauthenticated') {
147+
setUserProfile({ handle: null, referralCode: null })
148+
}
149+
}, [sessionStatus, fetchUserProfile])
150+
151+
if (sessionStatus === 'loading' || userProfile === undefined) {
152+
return (
153+
<div className="container mx-auto px-4 py-8">
154+
<div className="max-w-4xl mx-auto">
155+
<Card>
156+
<CardHeader>
157+
<Skeleton className="h-8 w-1/2 mb-2" />
158+
<Skeleton className="h-4 w-3/4" />
159+
</CardHeader>
160+
<CardContent className="space-y-4">
161+
<Skeleton className="h-4 w-full" />
162+
<Skeleton className="h-4 w-full" />
163+
<Skeleton className="h-20 w-full" />
164+
</CardContent>
165+
</Card>
166+
</div>
167+
</div>
168+
)
169+
}
170+
171+
if (sessionStatus === 'unauthenticated') {
172+
return (
173+
<CardWithBeams
174+
title="Join Our Affiliate Program"
175+
description="Log in to access the affiliate sign-up form."
176+
content={
177+
<>
178+
<p className="text-center mb-4">
179+
Want to partner with Codebuff and earn rewards? Log in first!
180+
</p>
181+
<SignInCardFooter />
182+
</>
183+
}
184+
/>
185+
)
186+
}
187+
188+
if (fetchError) {
189+
return (
190+
<div className="container mx-auto px-4 py-8">
191+
<div className="max-w-4xl mx-auto text-center text-red-600">
192+
<p>Error loading affiliate information: {fetchError}</p>
193+
<p>Please try refreshing the page or contact support.</p>
194+
</div>
195+
</div>
196+
)
197+
}
198+
199+
const userHandle = userProfile?.handle
200+
const referralCode = userProfile?.referralCode
201+
202+
return (
203+
<div className="container mx-auto px-4 py-8">
204+
<div className="max-w-4xl mx-auto">
205+
<Card>
206+
<CardHeader>
207+
<CardTitle className="text-3xl font-bold">
208+
Codebuff Affiliate Program
209+
</CardTitle>
210+
<CardDescription className="text-lg text-muted-foreground">
211+
Share Codebuff and earn credits!
212+
</CardDescription>
213+
</CardHeader>
214+
<CardContent className="space-y-6">
215+
{userHandle === null && (
216+
<div>
217+
<h2 className="text-xl font-semibold mb-2">
218+
Become an Affiliate
219+
</h2>
220+
<p className="pb-8">
221+
Generate your unique referral link, that grants you{' '}
222+
{AFFILIATE_USER_REFFERAL_LIMIT.toLocaleString()} referrals for
223+
your friends, colleagues, and followers. When they sign up
224+
using your link, you'll both earn an extra{' '}
225+
{CREDITS_REFERRAL_BONUS} credits!
226+
</p>
227+
228+
<SetHandleForm onHandleSetSuccess={fetchUserProfile} />
229+
</div>
230+
)}
231+
232+
{userHandle && (
233+
<div>
234+
<h2 className="text-xl font-semibold mb-2">
235+
Your Affiliate Handle
236+
</h2>
237+
<p>
238+
Your affiliate handle is set to:{' '}
239+
<code className="font-mono bg-muted px-1 py-0.5 rounded">
240+
{userHandle}
241+
</code>
242+
. You can now refer up to{' '}
243+
{AFFILIATE_USER_REFFERAL_LIMIT.toLocaleString()} new users!
244+
</p>
245+
<p className="text-sm text-muted-foreground mt-1">
246+
Your referral link is:{' '}
247+
<Link
248+
href={`/${userHandle}`}
249+
className="underline"
250+
>{`${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/${userHandle}`}</Link>
251+
</p>
252+
</div>
253+
)}
254+
255+
<p className="text-sm text-muted-foreground border-t pt-4 mt-6">
256+
Questions? Contact us at{' '}
257+
<Link
258+
href={`mailto:${env.NEXT_PUBLIC_SUPPORT_EMAIL}`}
259+
className="underline"
260+
>
261+
{env.NEXT_PUBLIC_SUPPORT_EMAIL}
262+
</Link>
263+
.
264+
</p>
265+
</CardContent>
266+
</Card>
267+
</div>
268+
</div>
269+
)
270+
}

0 commit comments

Comments
 (0)