From 99eea586dba02bf86b5717ded383df57cc164328 Mon Sep 17 00:00:00 2001 From: Jake Date: Mon, 4 May 2026 22:44:10 -0400 Subject: [PATCH 1/3] feat: MTG-themed design system, onboarding overhaul, and reliability improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design system: - Replace flat gold (#e8b836) with MTG card-frame amber (#c9a84c / #e2c468) - Add Cinzel display font (Beleren-adjacent) for logo via next/font - Gold focus rings across the app (was purple) - Glow vars: --glow-gold, --glow-keep, --glow-mull used on interactive elements - MTG mana palette tokens (--mana-white/blue/black/red/green) - Header gets 2px gold top border + gold active tab underline - Card slot highlights use mana-green (playable) and mana-blue (castable) - Keep/Mulligan/New Hand buttons glow with their semantic colors on hover Onboarding (Phase 2): - Quick-start CTA in DeckInput when textarea is empty ("Try demo deck →") - Visible label added to random deck type selector - Desktop input panel is now two-column: form + feature description sidebar - Goldfish idle state now explains the mode instead of just gating access - Removed ghost demoBtn; demo flow consolidated into DeckInput Reliability (Phase 1): - fetchCards onProgress wired: shows "Fetching cards… X/Y" during Scryfall fetch - notFound warning truncated to 3 + "+N more" (was unlimited join) - Status message cleared on successful load (was left at last progress state) - Fallback warning more actionable: clarifies simulation still works Co-Authored-By: Claude Sonnet 4.6 --- src/app/globals.css | 57 +++++++++++++++------- src/app/layout.tsx | 10 +++- src/components/ActionBar.module.css | 15 +++++- src/components/CardSlot.module.css | 14 +++--- src/components/DeckInput.module.css | 43 +++++++++++++++-- src/components/DeckInput.tsx | 57 +++++++++++++++------- src/components/Goldfish.module.css | 11 ++++- src/components/Goldfish.tsx | 14 ++++-- src/components/HandTest.module.css | 73 ++++++++++++++++++++++++----- src/components/HandTest.tsx | 45 ++++++++++++------ src/components/Header.module.css | 16 +++++-- src/hooks/useHandTest.ts | 21 ++++++--- 12 files changed, 283 insertions(+), 93 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 8d52d21..1926c3d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,27 +1,48 @@ /* ── Design Tokens ─────────────────────────────────────── */ :root { + /* Backgrounds — deep navy, MTG Arena-coded */ --bg-root: #07090f; - --bg-surface: #0f1424; - --bg-card: #151b2e; - --bg-input: #0d1020; - --bg-elevated: #181f35; + --bg-surface: #0c1020; + --bg-card: #131826; + --bg-input: #0a0e1c; + --bg-elevated: #161e30; - --text-primary: #e9ecf5; - --text-secondary: #9198b5; - --text-muted: #5a6282; + /* Text */ + --text-primary: #eceef8; + --text-secondary: #8892b8; + --text-muted: #505878; - --keep: #00d080; - --keep-dim: rgba(0, 208, 128, 0.15); - --mull: #f04f4f; - --mull-dim: rgba(240, 79, 79, 0.15); - --gold: #e8b836; - --gold-dim: rgba(232, 184, 54, 0.15); - --purple: #6c5ce7; + /* Semantic: green = keep, red = mull — matches MTG color philosophy */ + --keep: #00c877; + --keep-dim: rgba(0, 200, 119, 0.12); + --mull: #e03c3c; + --mull-dim: rgba(224, 60, 60, 0.12); - --border-faint: #1e2438; - --border-subtle: #222840; - --border-mid: #2d3455; - --border-focus: #6c5ce7; + /* Gold — MTG card-frame amber, warmer than warning yellow */ + --gold: #c9a84c; + --gold-bright: #e2c468; + --gold-dim: rgba(201, 168, 76, 0.12); + + /* MTG mana palette — used for card highlight states */ + --mana-white: #f0e6c0; + --mana-blue: #1e6eaa; + --mana-black: #3d2b5e; + --mana-red: #cc3b28; + --mana-green: #1a7a47; + + /* Borders */ + --border-faint: #181f38; + --border-subtle: #1f2842; + --border-mid: #293356; + --border-focus: #c9a84c; /* gold focus — replaces purple */ + + /* Glow effects — MTG Arena interactive signature */ + --glow-gold: 0 0 14px rgba(201, 168, 76, 0.22), 0 0 4px rgba(201, 168, 76, 0.12); + --glow-keep: 0 0 12px rgba(0, 200, 119, 0.25); + --glow-mull: 0 0 12px rgba(224, 60, 60, 0.25); + + /* Display font — Cinzel (Beleren-adjacent serif for logo/headings) */ + --font-display: 'Cinzel', serif; } /* ── Reset ────────────────────────────────────────────── */ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d8f4b8d..ce563f1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,14 @@ import type { Metadata } from 'next'; +import { Cinzel } from 'next/font/google'; import './globals.css'; +const cinzel = Cinzel({ + subsets: ['latin'], + weight: ['700'], + variable: '--font-display', + display: 'swap', +}); + export const metadata: Metadata = { title: 'Keep7 — MTG Deck Consistency Lab', description: 'Test opening hands, goldfish turns 1–5, and compare two decks side-by-side. Free, instant, no signup.', @@ -19,7 +27,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + {children} ); diff --git a/src/components/ActionBar.module.css b/src/components/ActionBar.module.css index af63abb..7c2eb23 100644 --- a/src/components/ActionBar.module.css +++ b/src/components/ActionBar.module.css @@ -40,16 +40,27 @@ .btnMull { background: var(--mull); color: #fff; + box-shadow: var(--glow-mull); +} + +.btnMull:hover { + box-shadow: 0 0 20px rgba(224, 60, 60, 0.45); } .btnKeep { background: var(--keep); color: #07090f; + box-shadow: var(--glow-keep); +} + +.btnKeep:hover { + box-shadow: 0 0 20px rgba(0, 200, 119, 0.45); } .btnNew { - background: var(--purple); - color: #fff; + background: var(--gold); + color: #07090f; + box-shadow: var(--glow-gold); } .btnReset { diff --git a/src/components/CardSlot.module.css b/src/components/CardSlot.module.css index a29331f..9774d76 100644 --- a/src/components/CardSlot.module.css +++ b/src/components/CardSlot.module.css @@ -30,18 +30,19 @@ outline-offset: 2px; } -/* Highlight states */ +/* Highlight states — mana-color coded */ .playable { - box-shadow: 0 0 0 2px var(--keep), 0 0 12px rgba(0, 208, 128, 0.3); + box-shadow: 0 0 0 2px var(--mana-green), 0 0 14px rgba(26, 122, 71, 0.5); } .castable { - box-shadow: 0 0 0 2px var(--purple), 0 0 12px rgba(108, 92, 231, 0.3); + box-shadow: 0 0 0 2px var(--mana-blue), 0 0 14px rgba(30, 110, 170, 0.5); } .tapped { - opacity: 0.5; + opacity: 0.55; transform: rotate(6deg); + filter: brightness(0.8); } /* Skeleton */ @@ -60,8 +61,9 @@ height: 100%; padding: 0.75rem; text-align: center; - background: linear-gradient(160deg, var(--bg-elevated), var(--bg-card)); - border: 1px dashed var(--border-mid); + background: linear-gradient(160deg, var(--bg-elevated) 0%, var(--bg-card) 100%); + border: 1px solid var(--border-mid); + border-top-color: rgba(201, 168, 76, 0.2); border-radius: 8px; } diff --git a/src/components/DeckInput.module.css b/src/components/DeckInput.module.css index 0392264..53ef0e6 100644 --- a/src/components/DeckInput.module.css +++ b/src/components/DeckInput.module.css @@ -123,17 +123,54 @@ border-color: var(--keep); color: #07090f; font-weight: 700; + box-shadow: var(--glow-keep); } .btnPrimary:hover:not(:disabled) { - background: #00c070; - border-color: #00c070; + background: #00da84; + border-color: #00da84; + box-shadow: 0 0 20px rgba(0, 200, 119, 0.4); +} + +.quickStart { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + background: var(--bg-elevated); + border: 1px solid var(--border-mid); + border-radius: 10px; + padding: 0.875rem 1rem; + margin-bottom: 1rem; +} + +.quickStartText { + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.4; +} + +.quickStartText strong { + color: var(--text-primary); } .webDeckRow { + margin-top: 0.75rem; +} + +.webDeckLabel { + display: block; + font-size: 0.7rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 0.375rem; +} + +.webDeckControls { display: flex; gap: 0.5rem; - margin-top: 0.75rem; } .select { diff --git a/src/components/DeckInput.tsx b/src/components/DeckInput.tsx index a16e784..a78af2f 100644 --- a/src/components/DeckInput.tsx +++ b/src/components/DeckInput.tsx @@ -11,6 +11,7 @@ interface DeckInputProps { value: string; onChange: (text: string) => void; onSubmit: (text?: string) => void; + onLoadDemo: () => void; loading: boolean; error: string | null; } @@ -23,7 +24,7 @@ const DECK_TYPES = [ { value: '__any__', label: 'Any Type' }, ]; -export function DeckInput({ value, onChange, onSubmit, loading, error }: DeckInputProps) { +export function DeckInput({ value, onChange, onSubmit, onLoadDemo, loading, error }: DeckInputProps) { const [webDeckType, setWebDeckType] = useState(loadWebDeckType() ?? 'Commander Deck'); const [webLoading, setWebLoading] = useState(false); const [webError, setWebError] = useState(null); @@ -55,8 +56,25 @@ export function DeckInput({ value, onChange, onSubmit, loading, error }: DeckInp saveWebDeckType(v); }, []); + const isEmpty = !value.trim(); + return (
+ {isEmpty && ( +
+
+ New here? Try it with a sample deck — no account needed. +
+ +
+ )} +
@@ -101,23 +119,26 @@ export function DeckInput({ value, onChange, onSubmit, loading, error }: DeckInp
- - + +
+ + +
{webError &&
{webError}
}
diff --git a/src/components/Goldfish.module.css b/src/components/Goldfish.module.css index f3fad4f..cffab7c 100644 --- a/src/components/Goldfish.module.css +++ b/src/components/Goldfish.module.css @@ -14,10 +14,17 @@ text-align: center; } +.idleTitle { + font-size: 1.1rem; + font-weight: 700; + color: var(--text-primary); +} + .idleText { - color: var(--text-muted); + color: var(--text-secondary); font-size: 0.9rem; - max-width: 360px; + max-width: 380px; + line-height: 1.55; } /* Header bar */ diff --git a/src/components/Goldfish.tsx b/src/components/Goldfish.tsx index 2a816f9..14f3e42 100644 --- a/src/components/Goldfish.tsx +++ b/src/components/Goldfish.tsx @@ -16,14 +16,18 @@ export function Goldfish() { if (!gf.started || !gf.gf) { return (
-

Load a deck in Hand Test first, then come here to goldfish.

- - {masterDeck.length > 0 && ( +
Goldfish Mode
+

+ Play out your deck for turns 1–5 against no opposition. Tap lands for mana, cast your spells, and see how consistently your deck develops. +

+ {masterDeck.length > 0 ? ( + ) : ( + )}
); diff --git a/src/components/HandTest.module.css b/src/components/HandTest.module.css index e61ace8..ee4a78d 100644 --- a/src/components/HandTest.module.css +++ b/src/components/HandTest.module.css @@ -4,9 +4,16 @@ } .inputPanel { + padding: 1rem; +} + +.inputPanelMain { max-width: 560px; margin: 0 auto; - padding: 1rem; +} + +.inputPanelSidebar { + display: none; } .statusBanner { @@ -35,22 +42,62 @@ border-color: rgba(232, 184, 54, 0.2); } -.demoBtn { - display: block; - margin: 1rem auto 0; - background: none; - border: 1px dashed var(--border-mid); - border-radius: 8px; - padding: 0.5rem 1.25rem; - font-size: 0.82rem; +.featureList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.featureHeading { + font-size: 0.68rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; color: var(--text-muted); - cursor: pointer; - transition: color 0.15s, border-color 0.15s; + margin-bottom: 0.25rem; +} + +.feature { + padding: 0.875rem 1rem; + background: var(--bg-surface); + border: 1px solid var(--border-faint); + border-radius: 10px; } -.demoBtn:hover { +.featureName { + font-size: 0.85rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.3rem; +} + +.featureDesc { + font-size: 0.78rem; color: var(--text-secondary); - border-color: var(--border-subtle); + line-height: 1.55; +} + +/* Desktop: two-column input layout */ +@media (min-width: 1024px) { + .inputPanel { + display: grid; + grid-template-columns: 520px 1fr; + gap: 3rem; + align-items: start; + max-width: 1100px; + margin: 0 auto; + padding: 2rem 1.5rem; + } + + .inputPanelMain { + max-width: none; + margin: 0; + } + + .inputPanelSidebar { + display: block; + padding-top: 0.25rem; + } } /* Test layout: single-column mobile → two-column desktop */ diff --git a/src/components/HandTest.tsx b/src/components/HandTest.tsx index e54b1ff..2e025a6 100644 --- a/src/components/HandTest.tsx +++ b/src/components/HandTest.tsx @@ -27,21 +27,38 @@ export function HandTest() {
{state.view === 'input' ? (
- {state.statusMessage && ( -
- {state.statusMessage} +
+ {state.statusMessage && ( +
+ {state.statusMessage} +
+ )} + +
+
) : (
diff --git a/src/components/Header.module.css b/src/components/Header.module.css index a3f33ea..6f1d7f6 100644 --- a/src/components/Header.module.css +++ b/src/components/Header.module.css @@ -5,20 +5,25 @@ padding: 0.75rem 1rem; background: var(--bg-surface); border-bottom: 1px solid var(--border-faint); + border-top: 2px solid var(--gold); position: sticky; top: 0; z-index: 100; + box-shadow: 0 2px 20px rgba(0, 0, 0, 0.4); } .logo { - font-size: 1.25rem; + font-family: var(--font-display); + font-size: 1.2rem; font-weight: 700; color: var(--text-primary); - letter-spacing: -0.02em; + letter-spacing: 0.04em; + text-transform: uppercase; } .seven { - color: var(--keep); + color: var(--gold); + text-shadow: var(--glow-gold); } .tabs { @@ -45,8 +50,9 @@ } .active { - color: var(--keep); - background: var(--keep-dim); + color: var(--gold-bright); + background: var(--gold-dim); + box-shadow: inset 0 -2px 0 var(--gold); } .tab:focus-visible { diff --git a/src/hooks/useHandTest.ts b/src/hooks/useHandTest.ts index d2a6c85..7949f78 100644 --- a/src/hooks/useHandTest.ts +++ b/src/hooks/useHandTest.ts @@ -93,7 +93,7 @@ export function useHandTest(onDeckReady?: (deck: Card[]) => void) { return; } - setPartial({ loading: true, error: null, warnings: [], statusMessage: null }); + setPartial({ loading: true, error: null, warnings: [], statusMessage: 'Parsing decklist…', statusTone: 'info' }); const { cardMap, errors } = parseDecklist(text); const deckSize = [...cardMap.values()].reduce((a, b) => a + b, 0); @@ -127,7 +127,9 @@ export function useHandTest(onDeckReady?: (deck: Card[]) => void) { cardData = cached; } else { try { - cardData = await fetchCards([...cardMap.keys()]); + cardData = await fetchCards([...cardMap.keys()], (loaded, total) => { + setPartial({ statusMessage: `Fetching cards… ${loaded}/${total}`, statusTone: 'info' }); + }); saveCardCacheByHash(hash, cardData); } catch { cardData = []; @@ -137,11 +139,18 @@ export function useHandTest(onDeckReady?: (deck: Card[]) => void) { const { deck, notFound } = buildDeck(cardMap, cardData); const warnings: string[] = []; - if (usedFallback) warnings.push('Scryfall unavailable — using placeholder cards.'); - if (notFound.length > 0) warnings.push(`Not found: ${notFound.join(', ')}`); + if (usedFallback) { + warnings.push('Scryfall unreachable — placeholder cards in use. All simulation mechanics still work.'); + } else if (notFound.length > 0) { + const preview = notFound.slice(0, 3).join(', '); + const tail = notFound.length > 3 ? ` +${notFound.length - 3} more` : ''; + warnings.push(`${notFound.length} card(s) not found on Scryfall: ${preview}${tail}`); + } samplesRef.current = []; - const statusMessage = usedFallback ? 'Running in offline mode — card images unavailable' : null; + const statusMessage = usedFallback + ? 'Offline mode — card images unavailable. All simulation mechanics still work.' + : null; setPartial({ masterDeck: deck, @@ -152,7 +161,7 @@ export function useHandTest(onDeckReady?: (deck: Card[]) => void) { warnings, usedFallback, statusMessage, - statusTone: usedFallback ? 'warn' : 'ok', + statusTone: usedFallback ? 'warn' : '', }); onDeckReady?.(deck); From 530c3618291d43c1dee4c076aad84d6039aa4a26 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 5 May 2026 21:42:48 -0400 Subject: [PATCH 2/3] feat: larger card images using responsive auto-fill grid Switch CardGrid and Goldfish zone grid from fixed column counts to auto-fill minmax, increasing card width from ~105px to ~160px on desktop. Cards now scale naturally with container width rather than being divided into too many narrow columns. Co-Authored-By: Claude Sonnet 4.6 --- src/components/CardGrid.module.css | 11 ++++++----- src/components/Goldfish.module.css | 14 +++++++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/CardGrid.module.css b/src/components/CardGrid.module.css index 963c46d..2816e26 100644 --- a/src/components/CardGrid.module.css +++ b/src/components/CardGrid.module.css @@ -1,19 +1,20 @@ .grid { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 6px; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: 8px; padding: 0.5rem 0; } @media (min-width: 640px) { .grid { - grid-template-columns: repeat(7, 1fr); - gap: 8px; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 10px; } } @media (min-width: 1024px) { .grid { - grid-template-columns: repeat(8, 1fr); + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 12px; } } diff --git a/src/components/Goldfish.module.css b/src/components/Goldfish.module.css index cffab7c..ab4edf8 100644 --- a/src/components/Goldfish.module.css +++ b/src/components/Goldfish.module.css @@ -113,13 +113,21 @@ .zoneGrid { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 6px; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: 8px; } @media (min-width: 640px) { .zoneGrid { - grid-template-columns: repeat(7, 1fr); + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 10px; + } +} + +@media (min-width: 1024px) { + .zoneGrid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 12px; } } From ec353d020379a7b0fddbbe5129bc5fac9283fcca Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 30 May 2026 21:29:59 -0400 Subject: [PATCH 3/3] feat: footer, SEO improvements, and README rewrite Footer: - Add Footer component with bg-root background, gold hover links - Privacy note, bug report, donate, and GitHub links - Sticky to bottom via flex column body layout SEO: - Expand meta description with mobile + no-account callouts - Add keywords array (MTG, Commander, goldfish, deck tester, Scryfall) - Add Twitter card metadata - Add robots index/follow README: - Rewrite to reflect Next.js 15 + React 19 + TypeScript stack (was describing the old vanilla JS version) - Add correct setup instructions (npm run dev, not static server) - Add project layout, import methods, reliability, and privacy sections - Link to live site Co-Authored-By: Claude Sonnet 4.6 --- README.md | 196 ++++++++++--------------------- src/app/globals.css | 6 + src/app/layout.tsx | 28 ++++- src/components/Footer.module.css | 55 +++++++++ src/components/Footer.tsx | 26 ++++ 5 files changed, 174 insertions(+), 137 deletions(-) create mode 100644 src/components/Footer.module.css create mode 100644 src/components/Footer.tsx diff --git a/README.md b/README.md index 4cd8117..70096e0 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,99 @@ # Keep7 -Testing a 100-card Magic deck on a phone is a pain. +MTG deck testing tool for opening hands, early turns, and deck comparison. -Most tools feel cramped, slow, or built for desktop first. Keep7 fixes that. +Paste a decklist. Draw hands. Goldfish turns 1–5. Compare two decks side by side. Works on mobile. -Paste your list. Tap through opening hands. Goldfish turns. Compare two decks. Do it fast on mobile. +**[→ Try it at keep7.vercel.app](https://keep7.vercel.app)** -## 60-Second Recruiter Scan +--- -**What it is:** -Mobile-first MTG deck testing app for opening-hand consistency and early-turn simulation. +## What it does -**Problem solved:** -Commander deck testing on a phone is usually slow and clumsy. +Keep7 helps you answer real play questions before you sit down at a table: -**What I built:** -- Hand tester with keep/mull flow and draw reveal -- Turn 1-5 goldfish simulator -- Deck-vs-deck race mode with 20-hand trial verdicts -- Web deck loader (MTGJSON) with retries and fallback behavior -- Parser cleanup for noisy Moxfield export lines -- Full-screen tap-to-zoom card modal for mobile use +- **Is this opener keepable?** Draw 7-card hands, keep or mulligan, see your next 3 draws. +- **How does my curve play out?** Goldfish turns 1–5 — tap lands, cast spells, see available mana. +- **Which version of this deck starts better?** Run 20 opener trials for each list, get a plain verdict. -**Stack:** -Vanilla JS (ES modules), HTML, CSS, Scryfall API, MTGJSON API +## Modes -**Why this matters:** -Shows product thinking, practical front-end architecture, API resilience, and mobile UX execution in one project. +### Hand Test +- Parse any standard decklist format +- Draw a 7-card opener, keep or mulligan +- Reveal next 3 draws after you keep +- Track session stats: keep rate, land distribution, flood/screw risk -## What This App Does +### Goldfish +- Simulate turns 1–5 against an empty board +- Play lands from hand, tap for mana, cast spells +- Action log tracks what happened each turn -Keep7 is a client-side MTG deck testing app focused on Commander-sized lists. +### Deck Race +- Compare two decklists across 20 simulated openers each +- Stats: keep rate, avg lands, T3 land %, flood/screw risk +- Plain-English verdict you can act on -It helps you answer real play questions: +## Stack -- Is this opener keepable? -- How often do I hit land drops? -- Which version of this deck starts better? +- **Next.js 15** (App Router) + **React 19** +- **TypeScript** throughout +- **CSS Modules** — no CSS-in-JS, no utility framework +- **Scryfall API** — card data and images, with retry/fallback +- **MTGJSON** — random web deck loading +- Fully client-side — no custom backend, no auth, no database -## Why People Use Keep7 - -- **You can test in seconds, not minutes.** - Paste a decklist and start drawing hands right away. - -- **It works well on a phone.** - Big tap targets, quick flows, full-screen card zoom. - -- **You can compare deck versions side by side.** - Run 20 opener trials for each list and read a plain verdict. - -- **You can load random web decks for fun testing.** - Pull from MTGJSON by deck type, then tap "Try another." - -- **It keeps your choices.** - Deck text, web deck type, and card cache are saved in local storage. - -## Features - -### Test Opening Hands - -- Parse decklists from plain text -- Draw a 7-card opener -- Mulligan or keep -- Reveal the next 3 draws -- Track session stats (keep rate, land distribution, flood/screw signals) - -### Goldfish Turns 1-5 - -- Simulate early turns with your actual list -- Play lands from hand -- See available mana and castable cards - -### Compare Two Decks - -- Run 20 opener simulations for Deck A and Deck B -- See both opening hands -- Review core stats for each deck -- Get a short verdict line you can act on - -### Load Random Web Decks - -- Load random decks from MTGJSON -- Filter by deck type -- Fallback flow: selected type -> any type -> local sample deck -- One-tap "Try another" - -## Setup (Low Pressure) - -No account. No backend setup. No database. - -1. Clone this repo. -2. Start a local static server. -3. Open the app in your browser. - -### Option A: Python - -```bash -python -m http.server 5173 -``` - -### Option B: Node +## Setup ```bash -npx http-server -p 5173 +git clone https://github.com/fuzmaster/keep7.git +cd keep7 +npm install +npm run dev ``` -Then open: +Open [http://localhost:3000](http://localhost:3000). -`http://localhost:5173` +## Decklist format -## Decklist Input Format +One card per line: -Use one card per line: - -```text -1 Sol Ring -1 Arcane Signet -35 Plains +``` +4 Lightning Bolt +4 Goblin Guide +20 Mountain ``` -The parser handles common export noise, including set codes and collector numbers from Moxfield-style lines. - -## Tech and APIs - -- Vanilla HTML/CSS/JavaScript (ES modules) -- Scryfall API for card data and images -- MTGJSON API for random web deck loading -- Fully client-side (no custom backend) - -## Project Layout - -- `index.html` - app shell and mode panels -- `styles.css` - visual system and responsive layout -- `js/ui.js` - hand test flow and interactions -- `js/goldfishUi.js` - goldfish mode UI -- `js/raceUi.js` - deck race mode UI -- `js/remoteDeck.js` - MTGJSON deck loading logic -- `js/parser.js` - decklist parsing and cleanup -- `js/storage.js` - local cache and saved settings - -## Common Issues - -### App does not load modules +Handles Moxfield export format automatically (set codes, collector numbers, etc.). -Run from a local server, not `file://`. +## Importing decks -### Card images do not appear +- **Paste a decklist** — plain text, one card per line +- **Moxfield / Archidekt URLs** — import via the URL field +- **Random web deck** — loads from MTGJSON by deck type -Check internet access and Scryfall availability. +## Reliability -### Random web deck fails +- Scryfall requests are batched, rate-limited, and retried on failure +- If Scryfall is unavailable, the app falls back to placeholder cards — simulation still works +- Card data is cached in `localStorage` by deck hash (7-day TTL, up to 5 decks) +- `localStorage` failures are handled gracefully -MTGJSON may be down. Keep7 falls back to a local sample deck. +## Project layout -## Portfolio Note +``` +src/ + app/ Next.js App Router pages and layout + components/ UI components with colocated CSS modules + hooks/ State logic for each mode (useHandTest, useGoldfish, useRace) + lib/ Business logic: parser, engine, scryfall, storage, metrics + types/ TypeScript interfaces +``` -This project shows product thinking plus front-end execution: +## Privacy -- Mobile-first UX for a niche workflow -- Real-world parser cleanup for noisy deck exports -- Resilient API fallbacks -- Fast, no-framework JavaScript architecture +Keep7 runs entirely in your browser. Deck text and session data never leave your device. The only external requests are to Scryfall (card images/data) and MTGJSON (optional random deck loading). ## License -MIT License. See `LICENSE`. +MIT diff --git a/src/app/globals.css b/src/app/globals.css index 1926c3d..252e063 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -64,6 +64,12 @@ body { color: var(--text-primary); line-height: 1.5; min-height: 100dvh; + display: flex; + flex-direction: column; +} + +main { + flex: 1; } img { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ce563f1..d6912d3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next'; import { Cinzel } from 'next/font/google'; +import { Footer } from '@/components/Footer'; import './globals.css'; const cinzel = Cinzel({ @@ -10,25 +11,42 @@ const cinzel = Cinzel({ }); export const metadata: Metadata = { - title: 'Keep7 — MTG Deck Consistency Lab', - description: 'Test opening hands, goldfish turns 1–5, and compare two decks side-by-side. Free, instant, no signup.', + title: 'Keep7 — MTG Deck Testing Tool', + description: 'Test Magic: The Gathering opening hands, goldfish turns 1–5, and compare two decks side by side. Free, instant, no account needed. Works on mobile.', + keywords: [ + 'MTG deck tester', 'Magic the Gathering', 'opening hand simulator', + 'goldfish MTG', 'deck consistency', 'Commander deck testing', + 'MTG hand test', 'mulligan helper', 'deck race', 'Scryfall', + ], openGraph: { - title: 'Keep7 — MTG Deck Consistency Lab', - description: 'Test MTG opening hands, goldfish, and compare decks.', + title: 'Keep7 — MTG Deck Testing Tool', + description: 'Test MTG opening hands, goldfish turns 1–5, and compare two decks side by side. Free and instant — no account needed.', url: 'https://keep7.vercel.app', siteName: 'Keep7', type: 'website', }, + twitter: { + card: 'summary', + title: 'Keep7 — MTG Deck Testing Tool', + description: 'Test MTG opening hands, goldfish, and compare decks. Free, instant, no signup.', + }, icons: { icon: '/favicon-card.svg', }, manifest: '/manifest.json', + robots: { + index: true, + follow: true, + }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} +