diff --git a/_specs/passphrase-front-page.md b/_specs/passphrase-front-page.md new file mode 100644 index 0000000..bd13c8e --- /dev/null +++ b/_specs/passphrase-front-page.md @@ -0,0 +1,45 @@ +# Spec for passphrase-front-page + +branch: claude/feature/passphrase-front-page + +## Summary +Add a passphrase-gated front page that is the first thing a user sees when they open the application. The page displays an application banner and a passphrase entry field. Only after the correct passphrase is entered does the user proceed to the existing landing page. The `Footer` component is included on this page. + +## Functional Requirements +- The app opens to the new front page instead of the landing page. +- The front page displays an application banner (app name/logo/title). +- The front page displays a passphrase input field (type password) and a submit control. +- When the user submits the correct passphrase (`DororthASL`), the app transitions to the existing landing page. +- When the user submits an incorrect passphrase, an error message is shown and the input is cleared; the user remains on the front page. +- The `Footer` component is rendered at the bottom of the front page. +- The passphrase is not displayed in plain text at any point. +- The passphrase should not be stored in `localStorage`, `sessionStorage`, or any persistent client-side store — it only needs to gate the current session. + +## Possible Edge Cases +- Submitting an empty passphrase should show an error (not silently fail). +- The passphrase comparison is case-sensitive (`DororthASL` is the only valid value). +- Once the user has passed the gate, navigating back (browser back button) should not re-show the front page within the same session. +- The passphrase should not appear in the page source, git history, or bundle in a way that is trivially discoverable — consider keeping it out of the component render logic if possible (e.g. an env variable or a hashed comparison). + +## Acceptance Criteria +- [ ] Opening the app shows the front page with a banner and passphrase field. +- [ ] Entering `DororthASL` and submitting advances the user to the landing page. +- [ ] Entering any other value shows an inline error and clears the input. +- [ ] Submitting an empty field shows an error. +- [ ] The `Footer` component is visible on the front page. +- [ ] The front page is not shown again after a successful passphrase entry within the same session. +- [ ] The passphrase input masks the entered text. + +## Open Questions +- Should the banner be a new component or a simple styled heading on the front page? + - new component +- Should session-level gating use React state only (resets on page reload) or `sessionStorage` (survives reload within the tab)? + - use sessionStorage + +## Testing Guidelines +Create a test file in the `./tests` folder for this feature and create meaningful tests for the following cases: +- Renders the front page (banner + passphrase input + footer) on initial load. +- Entering the correct passphrase and submitting transitions to the landing page view. +- Entering an incorrect passphrase shows an error message and does not advance. +- Submitting with an empty passphrase shows a validation error. +- The passphrase input field has `type="password"`. diff --git a/src/App.css b/src/App.css index 704c892..1241c7b 100644 --- a/src/App.css +++ b/src/App.css @@ -606,3 +606,99 @@ max-width: 100%; } } + +/* ── App banner ───────────────────────────────────────── */ + +.app-banner { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-bottom: 32px; +} + +.app-banner__icon { + font-size: 48px; + line-height: 1; +} + +.app-banner__title { + margin: 0; + font-size: clamp(28px, 5vw, 42px); + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-h); +} + +.app-banner__subtitle { + margin: 0; + font-size: 16px; + color: var(--text); +} + +/* ── Passphrase page ──────────────────────────────────── */ + +.passphrase-page { + min-height: 100svh; + display: flex; + flex-direction: column; + padding: 28px 12px; + + @media (max-width: 640px) { + padding: 32px 16px; + } +} + +.passphrase-page__body { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.passphrase-page__form { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + max-width: 360px; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 12px; + padding: 28px 24px; + box-shadow: var(--shadow); +} + +.passphrase-page__label { + font-size: 15px; + font-weight: 600; + color: var(--text-h); +} + +.passphrase-page__input { + width: 100%; + padding: 10px 12px; + font-size: 15px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + color: var(--text-h); + box-sizing: border-box; + + &:focus { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-color: var(--accent); + } +} + +.passphrase-page__error { + margin: 0; + font-size: 14px; + color: var(--color-needs-fix); +} + +.passphrase-page__submit { + align-self: flex-end; +} diff --git a/src/App.jsx b/src/App.jsx index 8a4c743..f186d51 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,11 +3,12 @@ import { CardColors } from './data/card-colors' import { LandingPage } from './components/LandingPage' import { FlashcardSession } from './components/FlashcardSession' import { Footer } from './components/Footer' +import { PassphrasePage } from './components/PassphrasePage' import './App.css' import {Toaster} from "react-hot-toast"; function App() { - const [view, setView] = useState('input') + const [view, setView] = useState(() => sessionStorage.getItem('asl-unlocked') ? 'input' : 'gate') const [terms, setTerms] = useState([]) const [cardColors, setCardColors] = useState([]) const [categoryTitle, setCategoryTitle] = useState('') @@ -27,6 +28,15 @@ function App() { setView('input') } + function handleUnlock() { + sessionStorage.setItem('asl-unlocked', '1') + setView('input') + } + + if (view === 'gate') { + return + } + return (
{view === 'input' ? ( diff --git a/src/components/AppBanner.jsx b/src/components/AppBanner.jsx new file mode 100644 index 0000000..7afcafb --- /dev/null +++ b/src/components/AppBanner.jsx @@ -0,0 +1,9 @@ +export function AppBanner() { + return ( +
+
🤟
+

ASL Flashcards

+

American Sign Language practice made easy

+
+ ) +} diff --git a/src/components/PassphrasePage.jsx b/src/components/PassphrasePage.jsx new file mode 100644 index 0000000..c1194fb --- /dev/null +++ b/src/components/PassphrasePage.jsx @@ -0,0 +1,50 @@ +import { useState } from 'react' +import { AppBanner } from './AppBanner' +import { Footer } from './Footer' + +export function PassphrasePage({ onUnlock }) { + const [value, setValue] = useState('') + const [error, setError] = useState('') + + function handleSubmit(e) { + e.preventDefault() + if (!value.trim()) { + setError('Please enter the passphrase.') + return + } + if (value === import.meta.env.VITE_PASSPHRASE) { + setError('') + onUnlock() + } else { + setError('Incorrect passphrase.') + setValue('') + } + } + + return ( +
+
+ +
+ + { setValue(e.target.value); setError('') }} + autoComplete="off" + autoFocus + /> + {error &&

{error}

} + +
+
+
+ ) +}