Initial commit: dating app web frontend (Next.js)
Phone+OTP auth, staged onboarding, admin-approval-gated home screen, and the full discovery/match/chat UI (swipe deck with drag gestures, match modal, profile detail, filters, messages/chat) wired to the real backend, styled after a Figma dating-app UI kit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion } from "framer-motion";
|
||||
import { ShieldCheck, Sparkles, Heart } from "lucide-react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Slide {
|
||||
icon: React.ReactNode;
|
||||
gradient: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const SLIDES: Slide[] = [
|
||||
{
|
||||
icon: <ShieldCheck size={64} className="text-white" strokeWidth={1.75} />,
|
||||
gradient: "linear-gradient(160deg, #ff8ca3 0%, #ff4d6d 55%, #d81b47 100%)",
|
||||
title: "پروفایلهای تایید شده",
|
||||
description:
|
||||
"قبل از اینکه کسی وارد بخش دوستیابی بشه، تیم دلدل پروفایلش رو بررسی میکنه؛ یعنی خبری از ربات و پروفایلهای تقلبی نیست.",
|
||||
},
|
||||
{
|
||||
icon: <Sparkles size={64} className="text-white" strokeWidth={1.75} />,
|
||||
gradient: "linear-gradient(160deg, #ffc93c 0%, #ff8ca3 55%, #ff4d6d 100%)",
|
||||
title: "مچهای واقعی",
|
||||
description:
|
||||
"بر اساس علاقهمندیها، سبک زندگی و چیزهایی که برات مهمه، با آدمهای مناسبتر آشنا میشی.",
|
||||
},
|
||||
{
|
||||
icon: <Heart size={64} className="text-white" fill="white" strokeWidth={0} />,
|
||||
gradient: "linear-gradient(160deg, #8b5cf6 0%, #ff4d6d 55%, #ff8ca3 100%)",
|
||||
title: "دوستیابی، ساده و امن",
|
||||
description:
|
||||
"یکییکی پروفایلها رو ببین، اگه خوشت اومد لایک کن، نه که نه؛ همهچیز دست خودته.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function WelcomePage() {
|
||||
const router = useRouter();
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const [active, setActive] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollerRef.current;
|
||||
if (!el) return;
|
||||
function onScroll() {
|
||||
if (!el) return;
|
||||
const index = Math.round(el.scrollLeft / el.clientWidth);
|
||||
setActive((prev) => (prev === index ? prev : index));
|
||||
}
|
||||
el.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => el.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
function goToSlide(index: number) {
|
||||
const el = scrollerRef.current;
|
||||
if (!el) return;
|
||||
el.scrollTo({ left: index * el.clientWidth, behavior: "smooth" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-dvh bg-(--color-bg-primary)">
|
||||
<div
|
||||
ref={scrollerRef}
|
||||
dir="ltr"
|
||||
className="flex-1 flex overflow-x-auto snap-x snap-mandatory scroll-smooth [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{SLIDES.map((slide, i) => (
|
||||
<div
|
||||
key={i}
|
||||
dir="rtl"
|
||||
className="w-full shrink-0 snap-center flex flex-col items-center px-8 pt-[calc(env(safe-area-inset-top)+32px)]"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.85, y: 12 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="w-full aspect-square max-w-[280px] rounded-(--radius-xl) flex items-center justify-center shadow-(--shadow-cute-lg) mt-6"
|
||||
style={{ background: slide.gradient }}
|
||||
>
|
||||
{slide.icon}
|
||||
</motion.div>
|
||||
|
||||
<div className="flex flex-col items-center text-center gap-3 mt-10 max-w-xs">
|
||||
<h1 className="font-[var(--font-display)] font-extrabold text-[26px] text-(--color-content-primary)">
|
||||
{slide.title}
|
||||
</h1>
|
||||
<p className="text-[14px] text-(--color-content-secondary) leading-7">
|
||||
{slide.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 px-8 pb-[calc(env(safe-area-inset-bottom)+28px)] pt-4">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{SLIDES.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
aria-label={`اسلاید ${i + 1}`}
|
||||
onClick={() => goToSlide(i)}
|
||||
className={cn(
|
||||
"h-2 rounded-(--radius-full) transition-all duration-300",
|
||||
i === active
|
||||
? "w-6 bg-(--color-brand-500)"
|
||||
: "w-2 bg-(--color-bg-tertiary)"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button size="lg" isBlock onClick={() => router.push("/login")}>
|
||||
بساز حساب کاربری
|
||||
</Button>
|
||||
<button
|
||||
onClick={() => router.push("/login")}
|
||||
className="text-[13px] text-center text-(--color-content-secondary)"
|
||||
>
|
||||
قبلاً حساب داری؟ <span className="font-bold text-(--color-brand-600)">ورود</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user