Files
MineGeo/components/FirstVisitIntro.tsx
pavel444-byte a7085e4e80
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
Improve MineGeo UI and application flows
2026-10-18 11:44:04 +05:00

112 lines
3.8 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 { 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;
// This is decorative delight, so reduced motion goes straight to the site.
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, 4000);
}, 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-cartography" aria-hidden="true" />
<div className="intro-content">
<div className="intro-map-stage" aria-hidden="true">
<span className="intro-coordinate intro-coordinate-left">
X: −128
</span>
<span className="intro-coordinate intro-coordinate-right">
Z: +256
</span>
<div className="intro-map-enter">
<div className="intro-map-surface">
<span className="intro-map-water" />
<span className="intro-map-path" />
<i className="intro-map-block intro-map-block-one" />
<i className="intro-map-block intro-map-block-two" />
<i className="intro-map-block intro-map-block-three" />
<span className="intro-map-target" />
</div>
</div>
<div className="intro-pin-anchor">
<MapPin className="intro-pin" strokeWidth={1.5} />
</div>
<span className="intro-bearing">N</span>
</div>
<div className="intro-title">
<p className="eyebrow">{t("YOUR NEXT EXPEDITION")}</p>
<h2 id={titleId} aria-label={t("MineGeo")}>
<span aria-hidden="true">MINE</span>
<span aria-hidden="true">GEO</span>
</h2>
<p className="intro-subtitle">
{t("Find your place in a blocky world.")}
</p>
</div>
</div>
<button
type="button"
className="intro-skip button"
autoFocus
onClick={() => dialog.current?.close()}
>
{t("Skip intro")} <span aria-hidden="true">Esc</span>
</button>
</dialog>
);
}