Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/components/student-space/EngineHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,17 @@ export function EngineHost({
children,
showOnboardingFlow = true,
hideCompanion = false,
landingShowcase = false,
}: {
className?: string
children?: ReactNode
showOnboardingFlow?: boolean
hideCompanion?: boolean
// When true, populate the island (all flowers, the tree, butterflies)
// so signed-out visitors see a mature island instead of the sparse
// one. Reverts on cleanup so a sign-in transition lands cleanly on
// the empty onboarding stage.
landingShowcase?: boolean
}) {
const containerRef = useRef<HTMLDivElement | null>(null)
const [error, setError] = useState<Error | null>(null)
Expand Down Expand Up @@ -156,6 +162,38 @@ export function EngineHost({
view?.fruits?.hideAll?.()
}, [game])

// Landing-page showcase: when a signed-out visitor is on /onboarding the
// engine renders behind the login surface. Surface every flower, the
// tree, and the full butterfly count so the preview reads as "what a
// mature island looks like" rather than the sparse onboarding start
// state. Cleanup re-hides them so a sign-in transition drops cleanly
// onto the empty onboarding stage.
useEffect(() => {
if (!game || !landingShowcase) return
const view = (
game as unknown as {
view?: {
flowers?: { showAll?: () => void; hideAll?: () => void }
tree?: { showAll?: () => void; hideAll?: () => void }
butterflies?: {
showAll?: () => void
hideAll?: () => void
showCount?: (n: number) => void
}
}
}
).view
if (!view) return
view.flowers?.showAll?.()
view.tree?.showAll?.()
view.butterflies?.showAll?.()
return () => {
view.flowers?.hideAll?.()
view.tree?.hideAll?.()
view.butterflies?.hideAll?.()
}
}, [game, landingShowcase])

useEffect(() => {
const container = containerRef.current
if (!container) return
Expand Down
9 changes: 6 additions & 3 deletions src/lib/student-space/camera-tuner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,12 @@ export const DEFAULT_PRESETS: Readonly<PresetMap> = Object.freeze({
durationMs: 800,
},
'login-orbit': {
azimuthDegPerSec: 1,
distance: 33.9,
pitchDeg: 35,
// Slow, cinematic arc — ~3°/s reads as ambient sweep without the
// dizziness a faster rotation would introduce; the populated island
// gets to show off over the dwell time on the login surface.
azimuthDegPerSec: 3,
distance: 32,
pitchDeg: 28,
},
})

Expand Down
23 changes: 21 additions & 2 deletions src/routes/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createFileRoute, Outlet, redirect, useLocation } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { EngineHost } from '~/components/student-space/EngineHost'
import { EdupassLogin } from '~/components/student-space/onboarding/EdupassLogin'
import { useEngine } from '~/lib/student-space/use-engine'
import { loadAuthMenu } from '~/server/auth-menu.functions'
import type { AuthMenuState } from '~/server/auth-menu.handler.server'

Expand Down Expand Up @@ -51,7 +53,7 @@ function AppLayout() {

if (isOnboardingPath(location.pathname) && authMenu?.status !== 'signed-in') {
return (
<EngineHost showOnboardingFlow={false} hideCompanion>
<EngineHost showOnboardingFlow={false} hideCompanion landingShowcase>
<SignedOutOnboarding />
</EngineHost>
)
Expand All @@ -73,9 +75,26 @@ export function isOnboardingPath(pathname: string) {
}

function SignedOutOnboarding() {
const game = useEngine()
// biome-ignore lint/suspicious/noExplicitAny: engine view bag is untyped.
const camera = (game as any)?.view?.camera ?? null
const reducedMotion = useReducedMotion()
return (
<main aria-label="Sign in" className="fixed inset-0 z-50 block overflow-hidden">
<EdupassLogin reducedMotion camera={null} />
<EdupassLogin reducedMotion={reducedMotion} camera={camera} />
</main>
)
}

function useReducedMotion() {
const [reduced, setReduced] = useState(false)
useEffect(() => {
if (typeof window === 'undefined' || !window.matchMedia) return
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
setReduced(mq.matches)
const handler = (e: MediaQueryListEvent) => setReduced(e.matches)
mq.addEventListener('change', handler)
return () => mq.removeEventListener('change', handler)
}, [])
return reduced
}
Loading