Add homepage subscription popup with promo video and phone signup.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
25cab1104a
commit
b3df0d3eb4
Binary file not shown.
@@ -9,6 +9,7 @@ import FeaturesBar from "@/components/home/FeaturesBar";
|
||||
import BlackCollection from "@/components/home/BlackCollection";
|
||||
import AboutSection from "@/components/home/AboutSection";
|
||||
import SiteFooter from "@/components/home/SiteFooter";
|
||||
import SubscribeModal from "@/components/home/SubscribeModal";
|
||||
import { mapHomeSlots } from "@/lib/map-home-slots";
|
||||
import {
|
||||
fetchPublicWebsiteSlots,
|
||||
@@ -50,6 +51,7 @@ export default async function HomePage() {
|
||||
|
||||
return (
|
||||
<main className="home-page min-h-dvh bg-white text-ink">
|
||||
<SubscribeModal />
|
||||
<div className="relative">
|
||||
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
|
||||
<HeroSlider slides={home.heroSlides} />
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useCallback, useEffect, useId, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { API_URL } from "@/lib/public-api";
|
||||
|
||||
const HIDDEN_KEY = "mana-subscribe-hidden";
|
||||
const LEGACY_DONE_KEY = "mana-subscribe-done";
|
||||
const PROMO_VIDEO = "/videos/promo.mp4";
|
||||
|
||||
function isHiddenForever() {
|
||||
try {
|
||||
return (
|
||||
window.localStorage.getItem(HIDDEN_KEY) === "1" ||
|
||||
window.localStorage.getItem(LEGACY_DONE_KEY) === "1"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function rememberHidden() {
|
||||
try {
|
||||
window.localStorage.setItem(HIDDEN_KEY, "1");
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
const PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹";
|
||||
const ARABIC_DIGITS = "٠١٢٣٤٥٦٧٨٩";
|
||||
|
||||
function normalizeCell(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/[۰-۹]/g, (digit) => String(PERSIAN_DIGITS.indexOf(digit)))
|
||||
.replace(/[٠-٩]/g, (digit) => String(ARABIC_DIGITS.indexOf(digit)))
|
||||
.replace(/[^\d]/g, "");
|
||||
}
|
||||
|
||||
function apiErrorMessage(body: unknown) {
|
||||
if (body && typeof body === "object" && "message" in body) {
|
||||
const message = (body as { message: unknown }).message;
|
||||
if (Array.isArray(message) && message.length) return String(message[0]);
|
||||
if (typeof message === "string" && message.trim()) return message;
|
||||
}
|
||||
return "ثبت شماره ناموفق بود. دوباره تلاش کنید.";
|
||||
}
|
||||
|
||||
export default function SubscribeModal() {
|
||||
const titleId = useId();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [phone, setPhone] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
if (isHiddenForever()) return;
|
||||
const timer = window.setTimeout(() => setOpen(true), 600);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const decline = useCallback(() => {
|
||||
rememberHidden();
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const previous = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") dismiss();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.body.style.overflow = previous;
|
||||
window.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open, dismiss]);
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const cellNumber = normalizeCell(phone);
|
||||
if (!/^09\d{9}$/.test(cellNumber)) {
|
||||
setError("شماره موبایل را با ۰۹ و ۱۱ رقم وارد کنید.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/public/subscribe`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ cellNumber }),
|
||||
});
|
||||
const body: unknown = await res.json().catch(() => null);
|
||||
if (!res.ok) {
|
||||
setError(apiErrorMessage(body));
|
||||
return;
|
||||
}
|
||||
rememberHidden();
|
||||
setDone(true);
|
||||
} catch {
|
||||
setError("ارتباط با سرور برقرار نشد. دوباره تلاش کنید.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted || !open) return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[220] flex items-center justify-center p-4 sm:p-6"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 bg-ink/45 backdrop-blur-sm"
|
||||
aria-label="بستن"
|
||||
onClick={dismiss}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex max-h-[min(94dvh,920px)] w-full max-w-[1080px] flex-col overflow-hidden rounded-[5px] bg-white shadow-[0_24px_80px_rgba(0,0,0,0.28)] md:h-[min(92dvh,880px)] md:w-auto md:max-w-[min(96vw,1080px)] md:flex-row">
|
||||
<div className="relative aspect-[9/16] max-h-[42dvh] w-full shrink-0 bg-ink md:aspect-[9/16] md:h-full md:max-h-none md:w-auto">
|
||||
<video
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={PROMO_VIDEO}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative flex min-h-0 w-full flex-col justify-center overflow-y-auto px-6 py-8 text-right sm:px-10 sm:py-10 md:w-[420px] md:shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="بستن"
|
||||
onClick={dismiss}
|
||||
className="absolute top-4 left-4 flex size-10 items-center justify-center rounded-full transition-opacity hover:opacity-60"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden>
|
||||
<path
|
||||
d="M6 6l12 12M18 6L6 18"
|
||||
stroke="#0a0a0a"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{done ? (
|
||||
<div>
|
||||
<p
|
||||
dir="ltr"
|
||||
className="font-montserrat text-[11px] font-medium uppercase tracking-[0.22em] text-muted"
|
||||
>
|
||||
You are on the list
|
||||
</p>
|
||||
<h2
|
||||
id={titleId}
|
||||
className="font-doran mt-3 text-[clamp(1.6rem,2.4vw,2.15rem)] font-medium leading-tight text-ink"
|
||||
>
|
||||
شماره شما ثبت شد
|
||||
</h2>
|
||||
<p className="mt-4 text-sm font-light leading-8 text-ink/75">
|
||||
از این پس خبر کالکشنهای تازه و فروش ویژه مانا را برای همین
|
||||
شماره میفرستیم.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="mt-8 h-[52px] w-full rounded-[5px] bg-ink text-base font-light text-white transition-opacity hover:opacity-80"
|
||||
>
|
||||
بستن
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<p
|
||||
dir="ltr"
|
||||
className="font-montserrat text-[11px] font-medium uppercase tracking-[0.22em] text-muted"
|
||||
>
|
||||
Join Mana
|
||||
</p>
|
||||
<h2
|
||||
id={titleId}
|
||||
className="font-doran mt-3 text-[clamp(1.6rem,2.4vw,2.15rem)] font-medium leading-tight text-ink"
|
||||
>
|
||||
به جمع مانا بپیوندید
|
||||
</h2>
|
||||
<p className="mt-4 text-sm font-light leading-8 text-ink/75">
|
||||
شماره موبایلتان را ثبت کنید تا از کالکشنهای تازه، فروش ویژه و
|
||||
خبرهای مانا باخبر شوید.
|
||||
</p>
|
||||
|
||||
<label className="mt-8 block space-y-2">
|
||||
<span className="text-xs text-muted">شماره موبایل</span>
|
||||
<input
|
||||
name="cellNumber"
|
||||
type="tel"
|
||||
inputMode="tel"
|
||||
autoComplete="tel"
|
||||
dir="ltr"
|
||||
required
|
||||
value={phone}
|
||||
onChange={(event) => {
|
||||
setPhone(event.target.value);
|
||||
if (error) setError(null);
|
||||
}}
|
||||
placeholder="09xxxxxxxxx"
|
||||
className="h-12 w-full rounded-[5px] border border-line bg-white px-3 text-left text-sm text-ink outline-none placeholder:text-[#818181] focus:border-ink"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{error ? (
|
||||
<p className="mt-3 text-sm text-ink" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="mt-5 h-[52px] w-full rounded-[5px] bg-ink text-base font-light text-white transition-opacity hover:opacity-80 disabled:opacity-50"
|
||||
>
|
||||
{saving ? "در حال ثبت..." : "ثبت شماره"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={decline}
|
||||
className="mt-3 w-full py-1.5 text-center text-[13px] font-light text-muted transition-colors hover:text-ink"
|
||||
>
|
||||
مایل نیستم، دیگر این پیام را نشان نده
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user