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
21 changes: 21 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ export default async function Home() {
return (
<div className="relative min-h-screen overflow-x-hidden text-ink">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(JSON_LD) }} />
{/* First-visit cover for the what's-new tour: solid midnight painted with
the initial HTML, hidden synchronously (same parse pass, before paint)
for anyone who has already seen the tour. First-timers keep it until
FeatureTour mounts and takes over with the identical color, so home
never flashes before the thank-you screen. HIDDEN, never removed: the
server and client trees must stay structurally identical or hydration
fails (suppressHydrationWarning covers the style the script mutates). */}
<div
id="gf-tour-cover"
suppressHydrationWarning
style={{ position: "fixed", inset: 0, zIndex: 120, background: "#02001e" }}
/>
<script
dangerouslySetInnerHTML={{
__html:
"try{if(localStorage.getItem('gitfut:tour:v1'))document.getElementById('gf-tour-cover').style.display='none'}catch(e){document.getElementById('gf-tour-cover').style.display='none'}",
}}
/>
<noscript>
<style>{`#gf-tour-cover{display:none}`}</style>
</noscript>
<Background />
<AppShell stars={stars} scoutCount={scoutCount} />
</div>
Expand Down
32 changes: 32 additions & 0 deletions components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ import BuyMeACoffee from "@/components/BuyMeACoffee";
import SupportProductHunt from "@/components/SupportProductHunt";
import GithubStar from "@/components/GithubStar";
import { SAMPLE_CARDS } from "@/lib/github/samples";
import { TOUR_STORAGE_KEY } from "@/components/tour/steps";

const HowItWorksModal = dynamic(() => import("@/components/HowItWorksModal"), {
ssr: false,
});
// Home-only: AppShell is rendered solely by app/page.tsx, so the TEAM NEWS
// bulletin never mounts on scout/duel pages. Lazy + ssr:false like the modal.
const WhatsNew = dynamic(() => import("@/components/WhatsNew"), { ssr: false });
// First-visit what's-new tour (thank-you + zoom showcase). Mounted over home as
// a pure overlay — no navigation in or out — and only until it has been seen
// once (localStorage; the pre-hydration cover in app/page.tsx hides home's
// first paint for exactly the visitors who will get it).
const FeatureTour = dynamic(() => import("@/components/tour/FeatureTour"), { ssr: false });

export default function AppShell({
stars,
Expand All @@ -30,6 +36,7 @@ export default function AppShell({
const [isPending, startTransition] = useTransition();
const [pending, setPending] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [tourOpen, setTourOpen] = useState(false);

// Mark this tab as "has visited home" so a scouted card shows BACK, while a
// directly-opened / shared card link (no home visit) shows a "make your card"
Expand All @@ -40,6 +47,29 @@ export default function AppShell({
} catch {}
}, []);

// The tour auto-plays exactly once. If it should NOT play, the pre-hydration
// cover was already removed synchronously by the inline script; if the tour
// chunk somehow never arrives, the safety timer clears the cover anyway so
// nobody stares at a blank screen. Deferred set (like seen-home above) so it
// can't cascade a render.
useEffect(() => {
let show = false;
try {
show = !localStorage.getItem(TOUR_STORAGE_KEY);
} catch {}
const t = setTimeout(() => {
if (show) setTourOpen(true);
}, 0);
const safety = setTimeout(() => {
const cover = document.getElementById("gf-tour-cover");
if (cover) cover.style.display = "none";
}, 4000);
return () => {
clearTimeout(t);
clearTimeout(safety);
};
}, []);

// Scouting navigates to the canonical /<username> route. The transition keeps
// the loading screen up (with the mascot + puns) while the report is fetched
// and server-rendered; the route then plays its own reveal.
Expand Down Expand Up @@ -80,6 +110,8 @@ export default function AppShell({

{modalOpen && <HowItWorksModal onClose={() => setModalOpen(false)} />}
<WhatsNew />

{tourOpen && <FeatureTour onDone={() => setTourOpen(false)} />}
</>
);
}
1 change: 1 addition & 0 deletions components/BallCursor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export default function BallCursor() {
return (
<button
type="button"
data-tour="ball"
onClick={(e) => {
posRef.current = { x: e.clientX, y: e.clientY };
setPhase("dribble");
Expand Down
16 changes: 11 additions & 5 deletions components/ResultView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ interface Props {
stars?: number | null;
/** GitHub-derived flag; share links only carry ?country= when it's overridden. */
canonicalCountry?: string;
/** Rendered as the feature tour's demo stage: drop the support pills, which
* are promo chrome with nothing to do with the tour (and float above it). */
demo?: boolean;
}

// Card width scales with the viewport but is bounded by BOTH width and height
Expand All @@ -47,6 +50,7 @@ export default function ResultView({
onCountryChange,
stars,
canonicalCountry = "",
demo = false,
}: Props) {
const captureRef = useRef<HTMLDivElement>(null);
const storyRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -193,12 +197,14 @@ export default function ResultView({
storyRef={storyRef}
canonicalCountry={canonicalCountry}
/>
<DuelButton login={card.login} />
<div data-tour="duel">
<DuelButton login={card.login} />
</div>
</div>
{/* Mobile cabinet: right under the duel CTA, before the report panels
stack — the stacked layout's most visible slot after the card. */}
{hasAwards && (
<div className="hidden w-[min(90vw,420px)] max-[980px]:block">
<div className="hidden w-[min(90vw,420px)] max-[980px]:block" data-tour="trophies">
<TrophyCabinetPanel card={card} onAwardClick={setActiveAward} />
</div>
)}
Expand All @@ -212,7 +218,7 @@ export default function ResultView({
<MetricsPanel card={card} />
{hasAwards ? (
<>
<div className="max-[980px]:hidden">
<div className="max-[980px]:hidden" data-tour="trophies">
<TrophyCabinetPanel card={card} onAwardClick={setActiveAward} />
</div>
<button
Expand Down Expand Up @@ -255,8 +261,8 @@ export default function ResultView({
</div>
</main>

<SupportProductHunt />
<BuyMeACoffee />
{!demo && <SupportProductHunt />}
{!demo && <BuyMeACoffee />}

{modalOpen && <HowItWorksModal onClose={() => setModalOpen(false)} />}

Expand Down
Loading
Loading