Add cinematic banner, real-site trailer, and first-visit intro
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m14s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m14s
This commit is contained in:
96
components/FirstVisitIntro.tsx
Normal file
96
components/FirstVisitIntro.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
components/SiteTrailer.tsx
Normal file
55
components/SiteTrailer.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Download, Film } from "lucide-react";
|
||||
|
||||
export async function SiteTrailer() {
|
||||
const { t } = await getI18n();
|
||||
return (
|
||||
<section
|
||||
className="section site-trailer"
|
||||
aria-labelledby="site-trailer-title"
|
||||
>
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<span className="eyebrow">
|
||||
<Film size={14} aria-hidden="true" /> {t("THE EXPEDITION AWAITS")}
|
||||
</span>
|
||||
<h2 id="site-trailer-title">{t("A world worth getting lost in.")}</h2>
|
||||
</div>
|
||||
<span className="trailer-duration">
|
||||
{t("30-second cinematic teaser")}
|
||||
</span>
|
||||
</div>
|
||||
<video
|
||||
className="site-trailer-player"
|
||||
controls
|
||||
preload="none"
|
||||
playsInline
|
||||
poster="/media/minegeo-trailer-poster.jpg"
|
||||
aria-label={t("MineGeo cinematic teaser")}
|
||||
aria-describedby="site-trailer-description"
|
||||
>
|
||||
<source src="/media/minegeo-trailer.mp4" type="video/mp4" />
|
||||
<track
|
||||
kind="captions"
|
||||
src="/media/minegeo-trailer.vtt"
|
||||
srcLang="en"
|
||||
label={t("English")}
|
||||
/>
|
||||
<p>
|
||||
{t("Your browser cannot play this video.")}{" "}
|
||||
<a href="/media/minegeo-trailer.mp4">{t("Download the trailer")}</a>
|
||||
</p>
|
||||
</video>
|
||||
<div className="trailer-caption">
|
||||
<p id="site-trailer-description">
|
||||
{t(
|
||||
"A tour of the real MineGeo homepage, community maps, map setup, and in-game panorama, blended with animated voxel landscapes. Ambient music; no spoken dialogue.",
|
||||
)}
|
||||
</p>
|
||||
<a className="text-link" href="/media/minegeo-trailer.mp4" download>
|
||||
<Download size={15} aria-hidden="true" /> {t("Download the trailer")}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user