Files
MineGeo/components/FirstVisitIntro.tsx
pavel444-byte ad73132f15
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m14s
Add cinematic banner, real-site trailer, and first-visit intro
2026-10-17 19:54:23 +05:00

97 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useId, useRef } from "react";
import { Compass, MapPin } from "lucide-react";
import { useI18n } from "./I18nProvider";
const storageKey = "minegeo:intro:v1";
let seenThisSession = false;
/** Home-only, decorative welcome. The underlying page always renders normally. */
export function FirstVisitIntro() {
const { t } = useI18n();
const titleId = useId();
const dialog = useRef<HTMLDialogElement>(null);
useEffect(() => {
const element = dialog.current;
if (!element) return;
const motion = window.matchMedia("(prefers-reduced-motion: reduce)");
let end: ReturnType<typeof setTimeout> | undefined;
const finish = () => {
clearTimeout(end);
if (element.open) element.close();
};
// Defer the browser-only decision; Strict Mode can clean up its first setup.
const start = setTimeout(() => {
if (seenThisSession) return;
try {
if (localStorage.getItem(storageKey)) return;
localStorage.setItem(storageKey, "seen");
} catch {
try {
if (sessionStorage.getItem(storageKey)) return;
sessionStorage.setItem(storageKey, "seen");
} catch {
// Both stores denied: keep the fallback for this page session in memory.
}
}
seenThisSession = true;
if (motion.matches || !element.isConnected) return;
// A welcome must never interrupt another modal, such as account notices.
if (document.querySelector("dialog[open]")) return;
element.showModal();
end = setTimeout(finish, 4750);
}, 0);
const motionChanged = () => {
if (motion.matches) finish();
};
motion.addEventListener("change", motionChanged);
element.addEventListener("close", finish);
return () => {
clearTimeout(start);
finish();
motion.removeEventListener("change", motionChanged);
element.removeEventListener("close", finish);
};
}, []);
return (
<dialog
ref={dialog}
className="first-visit-intro"
aria-labelledby={titleId}
>
<div className="intro-landscape" aria-hidden="true">
<div className="intro-horizon" />
<div className="intro-terrain">
{Array.from({ length: 15 }, (_, index) => (
<i key={index} />
))}
</div>
<div className="intro-compass">
<span />
<span />
<Compass strokeWidth={1} />
<MapPin className="intro-pin" strokeWidth={1.4} />
</div>
<span className="intro-coordinate intro-coordinate-left">X: −128</span>
<span className="intro-coordinate intro-coordinate-right">Z: +256</span>
</div>
<div className="intro-title">
<p className="eyebrow">{t("YOUR NEXT EXPEDITION")}</p>
<h2 id={titleId}>{t("MineGeo")}</h2>
<p>{t("Find your place in a blocky world.")}</p>
</div>
<button
type="button"
className="intro-skip button"
autoFocus
onClick={() => dialog.current?.close()}
>
{t("Skip intro")} <span aria-hidden="true">↵</span>
</button>
</dialog>
);
}