Landscape: whole-app landscape-first + Home 2-column landscape layout
- Move orientation lock + RotatePrompt to app root → whole app is landscape- first now (UNO-style), not just the game. Generalized rotate copy. - Home: portrait unchanged; in landscape it becomes a 2-column app layout (col A = branding + play actions, col B = tiles + footer) that fits the short height with no scroll (landscape: Tailwind variants, overflow-hidden). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ import { ResumeGameBar } from "@/components/online/ResumeGameBar";
|
|||||||
import { CelebrationOverlay } from "@/components/online/CelebrationOverlay";
|
import { CelebrationOverlay } from "@/components/online/CelebrationOverlay";
|
||||||
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
||||||
import { PublicProfileModal } from "@/components/online/PublicProfileModal";
|
import { PublicProfileModal } from "@/components/online/PublicProfileModal";
|
||||||
|
import { RotatePrompt } from "@/components/online/RotatePrompt";
|
||||||
import { CapacitorBack } from "@/components/CapacitorBack";
|
import { CapacitorBack } from "@/components/CapacitorBack";
|
||||||
import { useSessionStore } from "@/lib/session-store";
|
import { useSessionStore } from "@/lib/session-store";
|
||||||
import { useGameStore } from "@/lib/game-store";
|
import { useGameStore } from "@/lib/game-store";
|
||||||
@@ -191,6 +192,14 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
}, [init]);
|
}, [init]);
|
||||||
|
|
||||||
|
// Landscape-first app (UNO-style): best-effort lock the whole app to landscape
|
||||||
|
// on Android / installed PWA. iOS & desktop reject it harmlessly; the
|
||||||
|
// RotatePrompt covers the portrait case there.
|
||||||
|
useEffect(() => {
|
||||||
|
const o = (screen as unknown as { orientation?: { lock?: (m: string) => Promise<void> } }).orientation;
|
||||||
|
o?.lock?.("landscape").catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// reducedMotion="user" makes every Framer Motion animation honor the OS
|
// reducedMotion="user" makes every Framer Motion animation honor the OS
|
||||||
// "reduce motion" accessibility setting (coin rain, confetti, count-ups…).
|
// "reduce motion" accessibility setting (coin rain, confetti, count-ups…).
|
||||||
@@ -202,6 +211,7 @@ export default function Page() {
|
|||||||
<ResumeGameBar />
|
<ResumeGameBar />
|
||||||
<CelebrationOverlay />
|
<CelebrationOverlay />
|
||||||
<PublicProfileModal />
|
<PublicProfileModal />
|
||||||
|
<RotatePrompt />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<CapacitorBack />
|
<CapacitorBack />
|
||||||
{loading && null}
|
{loading && null}
|
||||||
|
|||||||
@@ -73,18 +73,24 @@ export function HomeScreen() {
|
|||||||
const Chevron = locale === "fa" ? ChevronLeft : ChevronRight;
|
const Chevron = locale === "fa" ? ChevronLeft : ChevronRight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="persian-pattern relative h-dvh w-full overflow-y-auto overscroll-contain safe-top safe-x safe-bottom">
|
<main className="persian-pattern relative h-dvh w-full overflow-y-auto landscape:overflow-hidden overscroll-contain safe-top safe-x safe-bottom">
|
||||||
<FloatingSuits />
|
<FloatingSuits />
|
||||||
<div className="relative z-10 mx-auto w-full max-w-md p-4 sm:p-6 flex flex-col min-h-dvh">
|
<div className="relative z-10 mx-auto w-full max-w-md landscape:max-w-5xl p-4 sm:p-6 flex flex-col min-h-dvh landscape:min-h-0 landscape:h-dvh">
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* content: single column (portrait) → two columns (landscape) */}
|
||||||
|
<div className="flex-1 flex flex-col min-h-0 landscape:flex-row landscape:items-stretch landscape:justify-center landscape:gap-6">
|
||||||
|
|
||||||
|
{/* ===== column A: branding + play actions ===== */}
|
||||||
|
<div className="flex flex-col landscape:flex-1 landscape:max-w-md landscape:justify-center landscape:min-h-0">
|
||||||
|
|
||||||
{/* logo */}
|
{/* logo */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 16 }}
|
initial={{ opacity: 0, y: 16 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="flex flex-col items-center mt-4 mb-5 sm:mt-6 sm:mb-7"
|
className="flex flex-col items-center mt-4 mb-5 sm:mt-6 sm:mb-7 landscape:mt-0 landscape:mb-3"
|
||||||
>
|
>
|
||||||
{/* logo + title on one row (no overflow); subtitle beneath the title */}
|
{/* logo + title on one row (no overflow); subtitle beneath the title */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -159,19 +165,24 @@ export function HomeScreen() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* ===== end column A ===== */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ===== column B: tiles + footer ===== */}
|
||||||
|
<div className="flex flex-col landscape:flex-1 landscape:max-w-sm landscape:justify-center landscape:min-h-0">
|
||||||
|
|
||||||
{/* tiles */}
|
{/* tiles */}
|
||||||
<div className="grid grid-cols-4 gap-2.5 mt-3 sm:mt-4">
|
<div className="grid grid-cols-4 gap-2.5 mt-3 sm:mt-4 landscape:mt-0">
|
||||||
<Tile icon={<User className="size-5" />} label={t("menu.profile")} tint="teal" onClick={() => nav("profile")} />
|
<Tile icon={<User className="size-5" />} label={t("menu.profile")} tint="teal" onClick={() => nav("profile")} />
|
||||||
<Tile icon={<Users className="size-5" />} label={t("menu.friends")} tint="sky" onClick={() => nav(isAuthed ? "friends" : "auth")} />
|
<Tile icon={<Users className="size-5" />} label={t("menu.friends")} tint="sky" onClick={() => nav(isAuthed ? "friends" : "auth")} />
|
||||||
<Tile icon={<Trophy className="size-5" />} label={t("menu.leaderboard")} tint="gold" onClick={() => nav("leaderboard")} />
|
<Tile icon={<Trophy className="size-5" />} label={t("menu.leaderboard")} tint="gold" onClick={() => nav("leaderboard")} />
|
||||||
<Tile icon={<ShoppingBag className="size-5" />} label={t("menu.shop")} tint="rose" onClick={() => nav("shop")} />
|
<Tile icon={<ShoppingBag className="size-5" />} label={t("menu.shop")} tint="rose" onClick={() => nav("shop")} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1 landscape:hidden" />
|
||||||
|
|
||||||
{/* footer */}
|
{/* footer */}
|
||||||
<div className="flex items-center justify-between gap-2 pt-4 sm:pt-6 pb-2">
|
<div className="flex items-center justify-between gap-2 pt-4 sm:pt-6 pb-2 landscape:pt-0 landscape:mt-4">
|
||||||
{isAuthed ? (
|
{isAuthed ? (
|
||||||
<button
|
<button
|
||||||
onClick={signOut}
|
onClick={signOut}
|
||||||
@@ -197,6 +208,10 @@ export function HomeScreen() {
|
|||||||
{t("home.lang")}
|
{t("home.lang")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* ===== end column B ===== */}
|
||||||
|
</div>
|
||||||
|
{/* ===== end content columns ===== */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { GameTable } from "@/components/GameTable";
|
import { GameTable } from "@/components/GameTable";
|
||||||
import { PostMatchRewardsModal } from "@/components/online/PostMatchRewardsModal";
|
import { PostMatchRewardsModal } from "@/components/online/PostMatchRewardsModal";
|
||||||
import { MatchIntroOverlay } from "@/components/online/MatchIntroOverlay";
|
import { MatchIntroOverlay } from "@/components/online/MatchIntroOverlay";
|
||||||
import { RotatePrompt } from "@/components/online/RotatePrompt";
|
|
||||||
import { useGameStore } from "@/lib/game-store";
|
import { useGameStore } from "@/lib/game-store";
|
||||||
import { useSessionStore } from "@/lib/session-store";
|
import { useSessionStore } from "@/lib/session-store";
|
||||||
import { useUIStore } from "@/lib/ui-store";
|
import { useUIStore } from "@/lib/ui-store";
|
||||||
@@ -59,20 +58,6 @@ export function GameScreen() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Landscape-first table: best-effort lock to landscape on Android/PWA (iOS &
|
|
||||||
// desktop reject it — harmless). Restored to portrait/auto when leaving.
|
|
||||||
useEffect(() => {
|
|
||||||
const o = (screen as unknown as { orientation?: { lock?: (m: string) => Promise<void>; unlock?: () => void } }).orientation;
|
|
||||||
o?.lock?.("landscape").catch(() => {});
|
|
||||||
return () => {
|
|
||||||
try {
|
|
||||||
o?.unlock?.();
|
|
||||||
} catch {
|
|
||||||
/* unsupported — ignore */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const notifyAchievements = (r: RewardResult) => {
|
const notifyAchievements = (r: RewardResult) => {
|
||||||
for (const a of r.newAchievements)
|
for (const a of r.newAchievements)
|
||||||
pushNotification({
|
pushNotification({
|
||||||
@@ -157,9 +142,6 @@ export function GameScreen() {
|
|||||||
onForfeit={canForfeit ? () => useGameStore.getState().forfeit() : undefined}
|
onForfeit={canForfeit ? () => useGameStore.getState().forfeit() : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Landscape-first: nudge to rotate when held in portrait on a phone */}
|
|
||||||
<RotatePrompt />
|
|
||||||
|
|
||||||
{/* UNO-style "players joining the table" intro (online matches, once) */}
|
{/* UNO-style "players joining the table" intro (online matches, once) */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{introPending && mode === "online" && (
|
{introPending && mode === "online" && (
|
||||||
|
|||||||
+4
-4
@@ -328,8 +328,8 @@ const fa: Dict = {
|
|||||||
"settings.sound": "افکت صدا",
|
"settings.sound": "افکت صدا",
|
||||||
"settings.music": "موسیقی پسزمینه",
|
"settings.music": "موسیقی پسزمینه",
|
||||||
"rotate.title": "گوشی را بچرخانید",
|
"rotate.title": "گوشی را بچرخانید",
|
||||||
"rotate.desc": "برای تجربهٔ بهترِ میز بازی، گوشی را افقی (چرخانده) نگه دارید.",
|
"rotate.desc": "برگ وسط در حالت افقی بهترین تجربه را دارد. گوشی را چرخانده (افقی) نگه دارید.",
|
||||||
"rotate.anyway": "همینطور عمودی بازی میکنم",
|
"rotate.anyway": "ادامه در حالت عمودی",
|
||||||
"settings.musicStyle": "سبک موسیقی",
|
"settings.musicStyle": "سبک موسیقی",
|
||||||
"settings.trackSantoor": "سنتی (سنتور)",
|
"settings.trackSantoor": "سنتی (سنتور)",
|
||||||
"settings.trackPlayful": "شاد",
|
"settings.trackPlayful": "شاد",
|
||||||
@@ -667,8 +667,8 @@ const en: Dict = {
|
|||||||
"settings.sound": "Sound effects",
|
"settings.sound": "Sound effects",
|
||||||
"settings.music": "Background music",
|
"settings.music": "Background music",
|
||||||
"rotate.title": "Rotate your phone",
|
"rotate.title": "Rotate your phone",
|
||||||
"rotate.desc": "The table plays best in landscape — turn your phone sideways for the full experience.",
|
"rotate.desc": "Barg-e Vasat is best in landscape — turn your phone sideways for the full experience.",
|
||||||
"rotate.anyway": "Keep playing in portrait",
|
"rotate.anyway": "Continue in portrait",
|
||||||
"settings.musicStyle": "Music style",
|
"settings.musicStyle": "Music style",
|
||||||
"settings.trackSantoor": "Traditional (Santoor)",
|
"settings.trackSantoor": "Traditional (Santoor)",
|
||||||
"settings.trackPlayful": "Playful",
|
"settings.trackPlayful": "Playful",
|
||||||
|
|||||||
Reference in New Issue
Block a user