Use browser navigation for reliable production page links

This commit is contained in:
Quintine committed 2026-09-21 19:25:32 +10:00
1 parent b14593fa40
commit c5661ec255
15 files changed
+40 -42

No files matched your search

+1 -2
View File
@@ -1,6 +1,5 @@
'use client';
import { useId, useState } from 'react';
import Link from 'next/link';
import type { Action, GameView } from '@/game/engine';
import { bribeAction } from '@/game/bribe-options';
import { faction } from '@/game/catalog';
@@ -47,7 +46,7 @@ export function Bribes({ game: g, act, busy }: {
<p>{recipient ? `You may pay up to ${recipient.maximum} spice to this recipient.` : `Uncommitted spice: ${options.available}. Choose a recipient to see the payment limit.`}</p>
{recipient?.limitation ? <p>{recipient.limitation}</p> : null}
{recipient && !options.blocked && !action ? <output className="bribe-status">{recipient.maximum < 1 ? 'No spice is available for this payment.' : `Enter a whole number from 1 to ${recipient.maximum}.`}</output> : null}
<p>Eligible Bureaucrats may divert two spice to the Bank. AI opponents can receive spice, but do not negotiate deals yet. <Link href="/rules?topic=bribes#bribes">Bribe rules</Link></p>
<p>Eligible Bureaucrats may divert two spice to the Bank. AI opponents can receive spice, but do not negotiate deals yet. <a href="/rules?topic=bribes#bribes">Bribe rules</a></p>
</details>
);
}
+1 -2
View File
@@ -1,4 +1,3 @@
import Link from 'next/link';
/** Counts only: this component never receives deck contents or private cards. */
export function DrawPiles({ piles }: {
@@ -19,7 +18,7 @@ export function DrawPiles({ piles }: {
</dl>
<p>Draw piles exclude hands, auction cards and discards.
{(piles.treachery === 0 || piles.spice === 0) && ' An empty pile may be replenished from eligible discards.'}
{' '}<Link href="/rules?topic=draw-piles#draw-piles">About these piles</Link>
{' '}<a href="/rules?topic=draw-piles#draw-piles">About these piles</a>
</p>
</section>
);
+1 -2
View File
@@ -1,4 +1,3 @@
import Link from 'next/link';
import { leaders } from '@/game/cards';
import type { GameView } from '@/game/engine';
import { LeaderInspector } from './leader-inspector';
@@ -16,7 +15,7 @@ export function EcazLoyaltyCard({ loyalty }: { loyalty: GameView['ecazLoyalty']
<p className="text-base! leading-7!">This Ecaz Traitor Card was randomly set aside face up before dealing. It stays outside the deck for this game and cannot be drawn as a Traitor or Face Dancer. Its leader remains in play under the normal leader rules.</p>
<div className="flex flex-wrap items-center gap-3">
<LeaderInspector kind="loyalty" identity={{ id: leader.id, name: leader.name, factionName: 'Ecaz', strength: leader.strength }} />
<Link className="py-3 text-base underline" href="/rules?topic=ecaz-loyalty#ecaz-loyalty">Loyalty rules</Link>
<a className="py-3 text-base underline" href="/rules?topic=ecaz-loyalty#ecaz-loyalty">Loyalty rules</a>
</div>
</div>
</section>;
+3 -4
View File
@@ -2,7 +2,6 @@
import { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
import { Eye, X } from 'lucide-react';
import { FACTIONS, faction, type FactionId } from '@/game/catalog';
@@ -34,8 +33,8 @@ export function FactionSheet({ factionId, advanced }: { factionId: FactionId; ad
</section>}
<footer>
<p>The rules and game are still being completed. This sheet describes faction powers; table controls show the choices currently available. Optional modules can add or change powers.</p>
<Link href={`/rules?topic=faction-${factionId}#faction-${factionId}`}>Full {house.name} reference</Link>
{advanced && <Link href={`/rules?topic=advanced-${factionId}#advanced-${factionId}`}>Advanced {house.name} reference</Link>}
<a href={`/rules?topic=faction-${factionId}#faction-${factionId}`}>Full {house.name} reference</a>
{advanced && <a href={`/rules?topic=advanced-${factionId}#advanced-${factionId}`}>Advanced {house.name} reference</a>}
</footer>
</article>;
}
@@ -64,7 +63,7 @@ export function FactionInspector({ factionId, advanced }: { factionId: FactionId
<DialogTitle>{house.name} faction sheet</DialogTitle>
<DialogDescription>Read public faction powers and alliance guidance without changing the game.</DialogDescription>
<FactionSheetReader key={`${factionId}:${advanced}`} factionId={factionId} tableAdvanced={advanced} />
<Link className="faction-sheet-help" href="/rules?topic=faction-sheets#faction-sheets">Faction sheet help</Link>
<a className="faction-sheet-help" href="/rules?topic=faction-sheets#faction-sheets">Faction sheet help</a>
</section>
</DialogPrimitive.Popup>
</DialogPortal>
+1 -2
View File
@@ -1,7 +1,6 @@
'use client';
import { useState, type CSSProperties } from 'react';
import Link from 'next/link';
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
import { Eye, Star, X } from 'lucide-react';
import { FACTIONS, faction, type FactionId } from '@/game/catalog';
@@ -64,7 +63,7 @@ export function ForceInventoryDetails({ player, worlds = null }: { player: Publi
<div><h3>Concealed No-Field</h3><p>{inventory.marker.name}</p><p>Counts as one force for board presence until revealed. It is a separate marker, not an extra physical force counter. Its hidden force value is not shown here. Revelation draws from the reserves available then.</p></div>
</section>}
<p>Inspecting counters does not move them or commit a Battle Plan. Spice support, temporary powers and leaders may change battle strength without adding counters.</p>
<Link href="/rules?topic=force-counters#force-counters">Force counter guide</Link>
<a href="/rules?topic=force-counters#force-counters">Force counter guide</a>
</div>;
}
+6 -7
View File
@@ -137,7 +137,6 @@ import {
import { validBattleSlotPair } from '@/game/battle-card-slots';
import { stoneBurnerPlanBlock } from '@/game/stone-burner';
import { TECH_TOKENS, ownedTech } from '@/game/tech-tokens';
import Link from 'next/link';
import {
canUseAsKaramaRole,
canUseAsTruthtranceRole,
@@ -2627,9 +2626,9 @@ export function GameTable({
comparison. Other weapon effects and traitor precedence
still apply.
</p>
<Link href="/rules?topic=stone-burner">
<a href="/rules?topic=stone-burner">
Read Stone Burner rules
</Link>
</a>
{actionButton('Kill both leaders', {
type: 'decision',
event: g.decision.event,
@@ -3286,13 +3285,13 @@ export function GameTable({
<details className="phase-guidance">
<summary>What happens in this phase?</summary>
<p>{PHASE_HELP[g.phase]}</p>
<Link
<a
href={`/rules#${phaseRuleId(g.phase)}`}
target="_blank"
rel="noreferrer"
>
Open phase guide ↗
</Link>
</a>
</details>
<ChoamBaliset game={g} act={act} busy={busy} />
<ChoamGamont game={g} act={act} busy={busy} />
@@ -5529,9 +5528,9 @@ export function GameTable({
several special cards, optional faction reactions, ally support,
auction deadlines, and a printed-board connectivity audit.
</p>
<Link href="/rules">
<a href="/rules">
Open the rules coverage ledger <ArrowRight size={14} />
</Link>
</a>
</div>
)}
</section>
+1 -2
View File
@@ -1,6 +1,5 @@
'use client';
import { Fragment, useId, useState, type ReactNode } from 'react';
import Link from 'next/link';
import type { Card } from '@/game/cards';
import { browseHand, type HandCategory, type HandSort } from '@/game/hand-browsing';
import { Button } from './ui/button';
@@ -48,7 +47,7 @@ export function HandBrowser({ cards, empty, children }: {
<div className="hand-browse-results">
<output aria-live="polite">{shown.length} of {cards.length} {cards.length === 1 ? 'card' : 'cards'} shown</output>
{filtered && <Button type="button" variant="outline" onClick={() => { setQuery(''); setCategory('all'); }}>Show all cards</Button>}
<Link href="/rules?topic=hand-browsing#hand-browsing">Hand browsing help</Link>
<a href="/rules?topic=hand-browsing#hand-browsing">Hand browsing help</a>
</div>
</form>
</search>
+3 -4
View File
@@ -1,6 +1,5 @@
'use client';
import Link from 'next/link';
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
import { BattleWheel } from './battle-wheel';
import { CardInspector, CardRules } from './card-inspector';
@@ -25,7 +24,7 @@ export function Introduction() {
const hydrated = useSyncExternalStore(subscribeHydration, clientSnapshot, serverSnapshot);
return <main className={styles.page}>
<header className={styles.header}>
<Link href="/">← Return to Arrakis</Link>
<a href="/">← Return to Arrakis</a>
<p className="eyebrow">LEARN TO PLAY · BASIC GAME</p>
<h1>Your first steps on Arrakis</h1>
<p>Practice the Basic phases, from the storm and spice blow to alliances, resources, bidding, movement, battle and collection, then take a seat at a real table. These are separate examples; they do not change any saved game.</p>
@@ -164,11 +163,11 @@ function IntroductionLessons() {
<li>Use <strong>Protect your saved seat</strong> and keep its recovery kit private. An invitation identifies the room; the kit restores your seat after cookie loss.</li>
</ul>
<p>Refresh restores a live game through its saved seat. If a request has an uncertain result, use the offered retry rather than repeating the game action.</p>
<div className={styles.links}><Link href="/">Create or join a game →</Link><Link href="/rules">Explore the full reference →</Link></div>
<div className={styles.links}><a href="/">Create or join a game →</a><a href="/rules">Explore the full reference →</a></div>
</>}
<footer className={styles.footer}>
<button disabled={state.step === 0} onClick={() => go(state.step - 1)}>Previous lesson</button>
<Link href={`/rules?topic=${lesson.topic}#${lesson.topic}`}>Read the related rules</Link>
<a href={`/rules?topic=${lesson.topic}#${lesson.topic}`}>Read the related rules</a>
{state.step < INTRODUCTION_STEPS.length - 1 && <button onClick={() => go(state.step + 1)}>Next lesson</button>}
</footer>
</section>
+1 -2
View File
@@ -1,5 +1,4 @@
import type { GameView } from '@/game/engine';
import Link from 'next/link';
import { faction } from '@/game/catalog';
import { leaderStrengthLabel } from '@/game/cards';
import { isStoneBurner } from '@/game/battle-cards';
@@ -37,7 +36,7 @@ export function RevealedBattle({ game }: { game: GameView }) {
<section className="revealed-plans" id="revealed-battle-plans" tabIndex={-1} aria-label="Revealed battle plans">
<h3>Revealed battle plans</h3>
<a className="battle-display-link" href="#table-decisions">Return to battle decisions</a>
<p className="revealed-plans-help">Both plans are public. Inspect any played component; use the decision controls to resolve the battle. <Link href="/rules?topic=revealed-battle-components#revealed-battle-components">Component guide</Link> · <Link href="/rules?topic=battle#battle">Battle rules</Link></p>
<p className="revealed-plans-help">Both plans are public. Inspect any played component; use the decision controls to resolve the battle. <a href="/rules?topic=revealed-battle-components#revealed-battle-components">Component guide</a> · <a href="/rules?topic=battle#battle">Battle rules</a></p>
<div className="revealed-plan-pair">
{[battle.attacker, battle.defender].map((id) => {
const plan = battle.plans[id];
+6 -7
View File
@@ -1,5 +1,4 @@
'use client';
import Link from 'next/link';
import { StrongholdCardGallery } from './stronghold-cards';
import { HomeworldCardGallery } from './homeworld-cards';
import { NexusCardGallery } from './nexus-cards';
@@ -94,7 +93,7 @@ function RulesReferenceContent() {
return (
<main className="reference-page">
<header className="reference-header">
<Link href="/">← Return to Arrakis</Link>
<a href="/">← Return to Arrakis</a>
<span className="eyebrow">
<BookOpen size={14} /> THE MENTAT’S REFERENCE
</span>
@@ -107,17 +106,17 @@ function RulesReferenceContent() {
Find a phase, card or faction. Follow the action guides, inspect
examples and move directly between related rules.
</p>
<p><Link href="/learn">New to Dune? Try the interactive introduction →</Link></p>
<p><a href="/learn">New to Dune? Try the interactive introduction →</a></p>
<p className="reference-status">
This reference is growing with the game. Advanced play and expansion
rules are unfinished.{' '}
<Link href="/rules/coverage">
<a href="/rules/coverage">
View the implementation ledger <ArrowUpRight size={13} />
</Link>
</a>
{' · '}
<Link href="/rules?topic=implementation-checklist#implementation-checklist">
<a href="/rules?topic=implementation-checklist#implementation-checklist">
Open feature checklists <ArrowUpRight size={13} />
</Link>
</a>
</p>
</header>
<div className="reference-layout">
+1 -2
View File
@@ -7,7 +7,6 @@ import { ForceCountFields, forceCountInputError } from './truthtrance-force-coun
import { isKnowledgeFact, knowledgeFactInputError } from '@/game/truthtrance-knowledge';
import type { PlanClaim } from '@/game/battle-promises';
import { useId, useState } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
@@ -130,7 +129,7 @@ export function Truthtrance({
<section className="truthtrance-panel" aria-label="Truthtrance question">
<h2>Truthtrance</h2>
<p className="fine">
<Link href="/rules#card-truthtrance">Question rules</Link> · The table
<a href="/rules#card-truthtrance">Question rules</a> · The table
resumes its previous decision after these questions.
</p>
{w.stage === 'priority' ? (