Add cinematic banner, real-site trailer, and first-visit intro
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m14s

This commit is contained in:
2026-10-17 19:54:23 +05:00
parent 128caff2a2
commit ad73132f15
19 changed files with 1578 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
"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>
);
}