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
45 changes: 45 additions & 0 deletions _specs/passphrase-front-page.md
Original file line number Diff line number Diff line change
@@ -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"`.
96 changes: 96 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
12 changes: 11 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('')
Expand All @@ -27,6 +28,15 @@ function App() {
setView('input')
}

function handleUnlock() {
sessionStorage.setItem('asl-unlocked', '1')
setView('input')
}

if (view === 'gate') {
return <PassphrasePage onUnlock={handleUnlock} />
}

return (
<div className="flashcard-app">
{view === 'input' ? (
Expand Down
9 changes: 9 additions & 0 deletions src/components/AppBanner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function AppBanner() {
return (
<div className="app-banner">
<div className="app-banner__icon">🤟</div>
<h1 className="app-banner__title">ASL Flashcards</h1>
<p className="app-banner__subtitle">American Sign Language practice made easy</p>
</div>
)
}
50 changes: 50 additions & 0 deletions src/components/PassphrasePage.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="passphrase-page">
<div className="passphrase-page__body">
<AppBanner />
<form className="passphrase-page__form" onSubmit={handleSubmit}>
<label className="passphrase-page__label" htmlFor="passphrase-input">
Enter passphrase to continue
</label>
<input
id="passphrase-input"
type="password"
className="passphrase-page__input"
value={value}
onChange={e => { setValue(e.target.value); setError('') }}
autoComplete="off"
autoFocus
/>
{error && <p className="passphrase-page__error">{error}</p>}
<button type="submit" className="btn-primary passphrase-page__submit">
Enter
</button>
</form>
</div>
<Footer />
</div>
)
}
Loading