feat: add landing-style start screen with four start modes

Phase 3 of turning the game into the company landing page.

- Replace the Iran-map briefing with a brand start screen over the
  orbiting 3D factory: logo, tagline and four ways in.
- Real world: safety notice, then GPS; if location is denied or
  missing, a friendly dialog offers offline play instead.
- Offline: start at Park Mellat beside the family car with the route
  to the factory, where the first purchase mission is.
- About Us: start in the factory yard. Products: start at the machine
  samples with the showroom open.
- Contact, address with map links, FAQ, careers, gallery and social
  links open in a sheet (bottom sheet on phones) from company config.
- Restart replays the chosen mode. Layout scrolls on short screens and
  respects safe areas.
- Add tests for the start screen, map links and theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 08:39:22 +03:30
co-authored by Claude Opus 5.5
parent 611960511e
commit d9ae8a45da
28 changed files with 838 additions and 261 deletions
@@ -0,0 +1,74 @@
import { useState } from 'react';
import { GameNode } from '@/components/shared/GameNode.jsx';
import { COMPANY } from '@/config/company.js';
import { ASSETS } from '@/config/assets.js';
import { strings } from '@/i18n/fa.js';
import { StartIcon } from './StartIcon.jsx';
import { StartOptions } from './StartOptions.jsx';
import { InfoSheet } from './InfoSheet.jsx';
// Company information links under the start buttons, with their icons.
const SECTIONS = [
['about', 'building'],
['contact', 'phone'],
['address', 'map'],
['faq', 'help'],
['careers', 'briefcase'],
['gallery', 'image'],
['social', 'share'],
];
/** @typedef {{nodes: object, actions: object}} StartScreenProps */
/**
* Landing-page style start screen over the slowly orbiting 3D factory: brand, the four ways
* into the game, and the company information a website would normally show.
* @param {StartScreenProps} props - Interface snapshots and actions.
* @returns {import('react').ReactElement} Start screen.
*/
export function StartScreen({ nodes, actions }) {
const [section, setSection] = useState(null);
const text = strings.landing;
return (
<GameNode as="section" id="landing" aria-labelledby="landing-title" model={nodes.landing}>
<div className="landing-panel">
<header className="flex items-center gap-3">
<img src={ASSETS.brand.logo} alt={text.logoAlt} className="h-12 w-12 rounded-xl" />
<div>
<p className="text-lg font-bold">{COMPANY.shortName}</p>
<p className="text-sm text-primary">{COMPANY.tagline}</p>
</div>
</header>
<div className="my-6 md:my-10">
<h1 id="landing-title" className="text-2xl font-black leading-snug md:text-4xl">
{text.heading}
</h1>
<p className="mt-3 max-w-md opacity-80">{text.intro}</p>
</div>
<StartOptions actions={actions} />
<nav aria-label={text.sectionsLabel} className="mt-6 flex flex-wrap gap-2">
{SECTIONS.map(([key, icon]) => (
<button key={key} type="button" className="start-chip" onClick={() => setSection(key)}>
<StartIcon name={icon} className="h-4 w-4" />
{key === 'about' ? text.history : text.sections[key]}
</button>
))}
<GameNode
as="button"
id="install"
type="button"
className="start-chip"
hidden
model={nodes.install}
onAction={() => actions.emit('install')}
>
<StartIcon name="download" className="h-4 w-4" />
{text.install}
</GameNode>
</nav>
<p className="mt-6 hidden text-xs opacity-60 md:block">{text.keys}</p>
</div>
<InfoSheet section={section} onClose={() => setSection(null)} />
</GameNode>
);
}