Wave 2B: sportsbook wordmarks + team-logo coverage
MISSION 1 — real sportsbook wordmarks (kills lowercase "betmgm"):
- books.js: add the 6 missing ALLOWED_BOOKS keys (fanatics/bet365/
hardrockbet/betrivers/pointsbet/pinnacle) with real brand names +
colors — no live book falls to neutral gray. Add `slug` fields +
bookSlug()/hasBookSvg() + BUNDLED_BOOK_SVGS.
- Bundle 8 self-authored styled-text wordmark SVGs under
web/public/books/{slug}.svg (draftkings/fanduel/betmgm/caesars/
bet365/pinnacle/hardrockbet/betrivers). NOT copied trademarked logo
glyphs — the book's NAME in brand weight+color; official press-kit
art can drop into the same paths with zero code change.
- BookWordmark: render the local SVG when bundled, else the brand-color
styled-text fallback (never a broken image; never a lowercase key).
- Import BookWordmark into the ledger row (page.tsx:368) + the identical
public-profile row, replacing bare {row.book} text. vyndr/GameCard
line-grid book cell now proper-cases via bookInfo().name (keeps the
preferred-book green highlight).
MISSION 2 — team-logo coverage gaps:
- teamMeta.js: add ESPN-schedule ball-sport abbr aliases the feed emits
that fell to monograms — SA→SAS, NY→NYK, WSH→WAS, BRK→BKN (NBA),
CONN→CON (WNBA). Real-abbr-first lookup means MLB WSH (Nationals) +
WNBA NY (Liberty) still resolve directly; NY in MLB stays null.
Tests: new tests/unit/bookWordmark.test.js (all 10 ALLOWED_BOOKS resolve
to a real brand+non-gray color; 8 bundled SVGs exist; BookWordmark
SVG-first + no-lowercase-leak; ledger/profile import + use BookWordmark).
entityLayer.test.js extended for the new aliases. Full suite green
(239 suites / 2891 tests); next build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { bookInfo } from '@/lib/books';
|
||||
import { bookInfo, bookSlug, hasBookSvg } from '@/lib/books';
|
||||
|
||||
/**
|
||||
* BookWordmark (DS0) — a sportsbook renders as its brand: the real name in
|
||||
* the book's brand color, properly cased (DraftKings, FanDuel), NEVER a bare
|
||||
* lowercase "draftkings" string (DESIGN-SPEC Part 2). For inline contexts
|
||||
* where the BookChip tile is too heavy. `best` gives it the signal ring.
|
||||
* BookWordmark (DS0 · Wave 2B) — a sportsbook renders as its brand. When a
|
||||
* bundled local wordmark SVG exists (`web/public/books/{slug}.svg`, for the ~8
|
||||
* major books) it renders that; otherwise it falls back to the real book NAME
|
||||
* in the book's brand color, properly cased (DraftKings, FanDuel), NEVER a bare
|
||||
* lowercase "draftkings" string (DESIGN-SPEC Part 2).
|
||||
*
|
||||
* The bundled SVGs are self-authored styled text wordmarks — not copied
|
||||
* trademarked logo glyphs — so the founder can drop official press-kit art into
|
||||
* the same paths later with zero code change. The files are local + always
|
||||
* present, so no broken-image state is possible. `best` gives the signal ring.
|
||||
*/
|
||||
export default function BookWordmark({
|
||||
book,
|
||||
@@ -16,6 +22,8 @@ export default function BookWordmark({
|
||||
size?: number;
|
||||
}) {
|
||||
const b = bookInfo(book);
|
||||
const slug = bookSlug(book);
|
||||
const svg = hasBookSvg(book) && slug ? `/books/${slug}.svg` : null;
|
||||
return (
|
||||
<span
|
||||
className="mono"
|
||||
@@ -29,7 +37,12 @@ export default function BookWordmark({
|
||||
}}
|
||||
>
|
||||
{best && <span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--g-a)', boxShadow: '0 0 6px var(--g-a)' }} />}
|
||||
{b.name}
|
||||
{svg ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={svg} alt={b.name} height={Math.round(size * 1.35)} style={{ display: 'block', width: 'auto', height: Math.round(size * 1.35) }} />
|
||||
) : (
|
||||
b.name
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import StatStrip, { type StatCell, type StripProp, type StripArchetype } from '@
|
||||
import TeamLogo from '@/components/vyndr/TeamLogo';
|
||||
import { accentColor } from '@/lib/teamMeta';
|
||||
import { playerHref } from '@/lib/playerHref';
|
||||
import { isPreferredBook } from '@/lib/books';
|
||||
import { isPreferredBook, bookInfo } from '@/lib/books';
|
||||
import { pendingSummary, topReadForCard } from '@/lib/slateAdapter';
|
||||
import { nextRunLabelET } from '@/lib/pipelineSchedule';
|
||||
import { useParlay, legKey } from '@/contexts/ParlayContext';
|
||||
@@ -301,7 +301,7 @@ export default function GameCard({ game: g, onAddParlay, onOpen, preferredBooks
|
||||
<div className="label" style={{ fontSize: 10, textAlign: 'center' }}>O/U</div>
|
||||
{g.lines.map((ln, i) => (
|
||||
<span key={i} style={{ display: 'contents' }}>
|
||||
<div className="mono" style={{ fontSize: 12, fontWeight: 700, color: isPreferredBook(ln.book, preferredBooks) ? 'var(--g-a)' : 'var(--text-1)', paddingLeft: 2, textShadow: isPreferredBook(ln.book, preferredBooks) ? '0 0 8px rgba(0,212,160,.5)' : 'none' }} title={isPreferredBook(ln.book, preferredBooks) ? 'Your book' : undefined}>{ln.book}</div>
|
||||
<div className="mono" style={{ fontSize: 12, fontWeight: 700, color: isPreferredBook(ln.book, preferredBooks) ? 'var(--g-a)' : 'var(--text-1)', paddingLeft: 2, textShadow: isPreferredBook(ln.book, preferredBooks) ? '0 0 8px rgba(0,212,160,.5)' : 'none' }} title={isPreferredBook(ln.book, preferredBooks) ? 'Your book' : undefined}>{bookInfo(ln.book).name}</div>
|
||||
<LineCell value={ln.awayML} best={ln.bestAway} worst={ln.worstAway} />
|
||||
<LineCell value={ln.homeML} best={ln.bestHome} worst={ln.worstHome} />
|
||||
<LineCell value={ln.ou} best={ln.bestOU} />
|
||||
|
||||
Reference in New Issue
Block a user