feat(coming-soon): hard-lock the live curtain; closable only on local/dev hosts
The curtain was sessionStorage-dismissible everywhere. NODE_ENV can't tell the live deploy from the local Docker site (both are prod builds), so gate on the hostname instead: localhost + private LAN ranges (incl. 172.28.x) keep the "view experimental (local)" close button; any public domain is hard-locked to just the countdown. Starts the curtain up by default so the live site never flashes a page before it mounts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,25 @@ const faDigits = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
|
|||||||
const fa = (n: number | string) =>
|
const fa = (n: number | string) =>
|
||||||
String(n).replace(/[0-9]/g, (d) => faDigits[Number(d)]);
|
String(n).replace(/[0-9]/g, (d) => faDigits[Number(d)]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local/dev hosts may close the curtain; the live (public) site is hard-locked.
|
||||||
|
* NODE_ENV can't distinguish them — both local Docker and the deploy are prod
|
||||||
|
* builds — so we key off the hostname: localhost + private LAN ranges = local.
|
||||||
|
*/
|
||||||
|
function isLocalHost(): boolean {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
const h = window.location.hostname;
|
||||||
|
return (
|
||||||
|
h === "localhost" ||
|
||||||
|
h === "127.0.0.1" ||
|
||||||
|
h === "0.0.0.0" ||
|
||||||
|
h.endsWith(".local") ||
|
||||||
|
/^10\./.test(h) ||
|
||||||
|
/^192\.168\./.test(h) ||
|
||||||
|
/^172\.(1[6-9]|2\d|3[01])\./.test(h) // 172.16–31.x (incl. the 172.28.x dev LAN)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const FEATURES = [
|
const FEATURES = [
|
||||||
{ icon: "⚡", title: "رندر فوقسریع ویدیو", desc: "ویدیوهایتان را در چند دقیقه و در فضای ابری رندر کنید" },
|
{ icon: "⚡", title: "رندر فوقسریع ویدیو", desc: "ویدیوهایتان را در چند دقیقه و در فضای ابری رندر کنید" },
|
||||||
{ icon: "💰", title: "هزینهٔ بسیار پایین", desc: "پرداخت بر اساس ثانیه؛ مقرونبهصرفهتر از همیشه" },
|
{ icon: "💰", title: "هزینهٔ بسیار پایین", desc: "پرداخت بر اساس ثانیه؛ مقرونبهصرفهتر از همیشه" },
|
||||||
@@ -49,14 +68,22 @@ function useCountdown(target: Date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ComingSoonOverlay() {
|
export function ComingSoonOverlay() {
|
||||||
const [open, setOpen] = useState(false);
|
// Start UP so the live site never flashes the page before the curtain mounts.
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
// Only local/dev hosts get a way out; the live site stays locked.
|
||||||
|
const [canDismiss, setCanDismiss] = useState(false);
|
||||||
const cd = useCountdown(LAUNCH);
|
const cd = useCountdown(LAUNCH);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Show unless dismissed earlier this session.
|
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
const local = isLocalHost();
|
||||||
|
setCanDismiss(local);
|
||||||
|
if (local) {
|
||||||
|
// Local: respect a prior dismissal so dev navigation isn't blocked.
|
||||||
const dismissed = sessionStorage.getItem(STORAGE_KEY);
|
const dismissed = sessionStorage.getItem(STORAGE_KEY);
|
||||||
setOpen(dismissed !== "1");
|
setOpen(dismissed !== "1");
|
||||||
|
}
|
||||||
|
// Live: no dismissal path — the curtain stays up, only the countdown shows.
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Lock background scroll while the curtain is up.
|
// Lock background scroll while the curtain is up.
|
||||||
@@ -198,20 +225,22 @@ export function ComingSoonOverlay() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA — local/dev only; the live site has no way past the curtain. */}
|
||||||
|
{canDismiss && (
|
||||||
<div className="mt-9">
|
<div className="mt-9">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={dismiss}
|
onClick={dismiss}
|
||||||
className="group inline-flex items-center gap-2 rounded-full bg-gradient-to-l from-[#3ba7ff] to-[#a855f7] px-8 py-3.5 text-base font-bold text-white shadow-lg shadow-indigo-500/30 transition hover:scale-[1.03] hover:shadow-indigo-500/50"
|
className="group inline-flex items-center gap-2 rounded-full bg-gradient-to-l from-[#3ba7ff] to-[#a855f7] px-8 py-3.5 text-base font-bold text-white shadow-lg shadow-indigo-500/30 transition hover:scale-[1.03] hover:shadow-indigo-500/50"
|
||||||
>
|
>
|
||||||
مشاهدهٔ نسخهٔ آزمایشی
|
مشاهدهٔ نسخهٔ آزمایشی (محلی)
|
||||||
<span className="transition group-hover:-translate-x-1">←</span>
|
<span className="transition group-hover:-translate-x-1">←</span>
|
||||||
</button>
|
</button>
|
||||||
<p className="mt-3 text-xs text-white/40">
|
<p className="mt-3 text-xs text-white/40">
|
||||||
میتوانید همین حالا نسخهٔ آزمایشی را ببینید
|
این دکمه فقط در نسخهٔ محلی نمایش داده میشود
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user