216 lines
7.8 KiB
TypeScript
216 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Logo } from "@/components/Logo";
|
|
import { US_STATES, setUserState, requiresIdVerification } from "@/lib/id-verification";
|
|
import { AudiencePicker } from "@/components/AudiencePicker";
|
|
import { saveAudienceView, formatSiteTitle, type AudienceViewMode } from "@/lib/audience";
|
|
|
|
interface AgeGateProps {
|
|
onVerified: () => void;
|
|
}
|
|
|
|
export function AgeGate({ onVerified }: AgeGateProps) {
|
|
const [month, setMonth] = useState("");
|
|
const [day, setDay] = useState("");
|
|
const [year, setYear] = useState("");
|
|
const [state, setState] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [agreed, setAgreed] = useState(false);
|
|
const [audienceView, setAudienceView] = useState<AudienceViewMode>("gay-men");
|
|
|
|
const handleVerify = () => {
|
|
const m = parseInt(month, 10);
|
|
const d = parseInt(day, 10);
|
|
const y = parseInt(year, 10);
|
|
|
|
if (!m || !d || !y) {
|
|
setError("Please enter your full date of birth.");
|
|
return;
|
|
}
|
|
|
|
if (!state) {
|
|
setError("Please select your state — required for content access rules.");
|
|
return;
|
|
}
|
|
|
|
const dob = new Date(y, m - 1, d);
|
|
const today = new Date();
|
|
let age = today.getFullYear() - dob.getFullYear();
|
|
const monthDiff = today.getMonth() - dob.getMonth();
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
|
|
age--;
|
|
}
|
|
|
|
if (age < 18) {
|
|
setError("You must be 18 or older to access this site.");
|
|
return;
|
|
}
|
|
|
|
if (!agreed) {
|
|
setError("You must confirm you are 18+ and agree to the disclaimer.");
|
|
return;
|
|
}
|
|
|
|
saveAudienceView(audienceView);
|
|
localStorage.setItem("eg_age_verified", "true");
|
|
localStorage.setItem("eg_age_verified_at", new Date().toISOString());
|
|
setUserState(state);
|
|
|
|
if (requiresIdVerification(state)) {
|
|
localStorage.setItem("eg_id_verification_required", "true");
|
|
} else {
|
|
localStorage.removeItem("eg_id_verification_required");
|
|
}
|
|
|
|
onVerified();
|
|
};
|
|
|
|
const idRequired = requiresIdVerification(state);
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/95 p-4 overflow-y-auto">
|
|
<div className="w-full max-w-md rounded-2xl border border-horny-pink/30 bg-horny-surface p-6 sm:p-8 shadow-2xl shadow-horny-pink/20 my-auto">
|
|
<div className="mb-6 text-center">
|
|
<div className="mx-auto mb-4 flex justify-center">
|
|
<Logo size="lg" showText={false} />
|
|
</div>
|
|
<h1 className="text-2xl font-bold gradient-text">
|
|
{formatSiteTitle(audienceView)}
|
|
</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Adults only. Explicit content ahead.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label className="mb-2 block">Date of Birth</Label>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<select
|
|
value={month}
|
|
onChange={(e) => setMonth(e.target.value)}
|
|
className="h-11 rounded-md border border-input bg-background px-2 text-sm touch-manipulation"
|
|
>
|
|
<option value="">Month</option>
|
|
{Array.from({ length: 12 }, (_, i) => (
|
|
<option key={i + 1} value={i + 1}>
|
|
{new Date(2000, i).toLocaleString("en", { month: "long" })}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
value={day}
|
|
onChange={(e) => setDay(e.target.value)}
|
|
className="h-11 rounded-md border border-input bg-background px-2 text-sm touch-manipulation"
|
|
>
|
|
<option value="">Day</option>
|
|
{Array.from({ length: 31 }, (_, i) => (
|
|
<option key={i + 1} value={i + 1}>
|
|
{i + 1}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
value={year}
|
|
onChange={(e) => setYear(e.target.value)}
|
|
className="h-11 rounded-md border border-input bg-background px-2 text-sm touch-manipulation"
|
|
>
|
|
<option value="">Year</option>
|
|
{Array.from({ length: 80 }, (_, i) => {
|
|
const y = new Date().getFullYear() - 18 - i;
|
|
return (
|
|
<option key={y} value={y}>
|
|
{y}
|
|
</option>
|
|
);
|
|
})}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="mb-2 block">Your State</Label>
|
|
<select
|
|
value={state}
|
|
onChange={(e) => setState(e.target.value)}
|
|
className="h-11 w-full rounded-md border border-input bg-background px-3 text-sm touch-manipulation"
|
|
>
|
|
<option value="">Select state...</option>
|
|
{US_STATES.map((s) => (
|
|
<option key={s.code} value={s.code}>
|
|
{s.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{idRequired && (
|
|
<p className="text-xs text-amber-400 mt-1.5">
|
|
{US_STATES.find((s) => s.code === state)?.name} requires ID verification
|
|
for nude photos — not required to browse as a guest.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="mb-2 block">Who do you want to see?</Label>
|
|
<AudiencePicker
|
|
value={audienceView}
|
|
onChange={setAudienceView}
|
|
compact
|
|
/>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-primary/25 bg-primary/10 p-3">
|
|
<p className="text-xs text-foreground/90 leading-relaxed">
|
|
<strong className="text-pulse-pink-light">No account needed.</strong> Verify your age
|
|
with date of birth only — browse the map, spots, and profiles free. Explicit
|
|
photos and chat require a free signup. Government ID is only needed for nude
|
|
content in certain states.
|
|
</p>
|
|
</div>
|
|
|
|
<label className="flex items-start gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={agreed}
|
|
onChange={(e) => setAgreed(e.target.checked)}
|
|
className="mt-1 h-5 w-5 rounded border-input accent-horny-pink touch-manipulation"
|
|
/>
|
|
<span className="text-xs text-muted-foreground leading-relaxed">
|
|
I confirm I am 18 years of age or older. I understand this site contains
|
|
explicit adult content including nudity and sexual material. I agree to the{" "}
|
|
<a href="/terms" className="text-horny-pink underline">
|
|
Terms of Service
|
|
</a>{" "}
|
|
and{" "}
|
|
<a href="/privacy" className="text-horny-pink underline">
|
|
Privacy Policy
|
|
</a>
|
|
. Pursuant to applicable state law, I acknowledge government ID verification
|
|
may be required to view nude content in certain states.
|
|
</span>
|
|
</label>
|
|
|
|
{error && (
|
|
<p className="text-sm text-destructive text-center">{error}</p>
|
|
)}
|
|
|
|
<Button
|
|
variant="horny"
|
|
className="w-full h-12 text-base touch-manipulation"
|
|
size="lg"
|
|
onClick={handleVerify}
|
|
>
|
|
Confirm I am 18+
|
|
</Button>
|
|
|
|
<p className="text-center text-xs text-muted-foreground">
|
|
100% free forever. No subscriptions. No credit card required.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |