diff --git a/src/App.flow.test.jsx b/src/App.flow.test.jsx index 396ab37..aaa89fc 100644 --- a/src/App.flow.test.jsx +++ b/src/App.flow.test.jsx @@ -391,3 +391,53 @@ describe('Window title', () => { await waitFor(() => expect(document.title).toMatch(/^[A-Za-z0-9 ]+ \d+ · Bible Study$/)); }); }); + +describe('Login page', () => { + async function openLogin() { + stubServer({ user: null }); + render(); + return screen.findByRole('button', { name: /^sign in$/i }); + } + + test('tells you what the app can do, in groups', async () => { + await openLogin(); + expect(screen.getByRole('heading', { level: 1, name: /read it\. study it\./i })).toBeInTheDocument(); + for (const group of ['Read', 'Study', 'Teach', 'Everywhere']) expect(screen.getByRole('region', { name: group })).toBeInTheDocument(); + expect(screen.getByText('A real outline')).toBeInTheDocument(); + expect(screen.getByText('Compare them')).toBeInTheDocument(); + }); + + test('switches between signing in and creating an account', async () => { + const user = userEvent.setup(); + await openLogin(); + expect(screen.getByRole('heading', { name: 'Sign in' })).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: /need an account/i })); + expect(screen.getByRole('heading', { name: 'Create an account' })).toBeInTheDocument(); + expect(screen.getByText(/at least 8 characters/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^create account$/i })).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: /already have an account/i })); + expect(screen.getByRole('heading', { name: 'Sign in' })).toBeInTheDocument(); + }); + + test('a theme can be picked before signing in', async () => { + const user = userEvent.setup(); + await openLogin(); + await user.click(screen.getByRole('button', { name: 'Dark' })); + expect(screen.getByRole('button', { name: 'Dark' })).toHaveAttribute('aria-pressed', 'true'); + expect(localStorage.getItem('reader-theme')).toBe('dark'); + }); + + test('a wrong password shows the error', async () => { + const user = userEvent.setup(); + vi.stubGlobal('fetch', vi.fn((url, opts = {}) => { + if (url === '/api/auth/me') return json(401, {}); + if (url === '/api/auth/login') return json(401, { error: 'Incorrect email or password.' }); + return Promise.resolve({ ok: false }); + })); + render(); + await user.type(await screen.findByLabelText(/email/i), 'a@b.co'); + await user.type(screen.getByLabelText(/^password/i), 'wrong-password'); + await user.click(screen.getByRole('button', { name: /^sign in$/i })); + expect(await screen.findByRole('alert')).toHaveTextContent(/incorrect email or password/i); + }); +}); diff --git a/src/pages/AuthPage.jsx b/src/pages/AuthPage.jsx index fe27752..68eccea 100644 --- a/src/pages/AuthPage.jsx +++ b/src/pages/AuthPage.jsx @@ -1,8 +1,91 @@ import { useState } from 'react'; import { useApp } from '../context/AppContext.js'; -import { themeVars } from '../components/reader/readerThemes.js'; +import { themeVars, READER_THEMES } from '../components/reader/readerThemes.js'; import { loginUser, registerUser, verifyMfaLogin, logoutUser } from '../syncService.js'; +// What the app does, in four groups. Everything listed here exists in the app. +const FEATURES = [ + { + title: 'Read', + points: [ + ['Six translations', 'BSB, ESV, NLT, CSB, NASB and NET, with audio and whole-Bible search.'], + ['Compare them', 'Read two side by side and see which words differ, verse by verse.'], + ['Mark it up', 'Highlight in colours you name, bookmark, and draw notes on the page.'], + ['Reading plans', 'Daily readings that open straight into the text, with streaks and reminders.'], + ], + }, + { + title: 'Study', + points: [ + ['Chunk a passage', 'Break it into sections and write Observe, Interpret and Apply notes.'], + ['Original languages', 'Greek and Hebrew word studies with pronunciation, from the interlinear.'], + ['Beside the text', 'Commentary, cross-references, and people and places for each chunk.'], + ], + }, + { + title: 'Teach', + points: [ + ['A real outline', 'Main points and sub-points for the whole study and for each chunk, with notes, verse text and minutes.'], + ['Scripts', 'Turn your notes into a script for a lesson, episode or sermon.'], + ['Share it', 'Word, PDF and Markdown files, or a read-only link anyone can open.'], + ], + }, + { + title: 'Everywhere', + points: [ + ['Your devices', 'Studies, marks and plans sync to your account, and nothing is overwritten silently.'], + ['Offline and backup', 'Save books to read without a connection, and download everything as one file.'], + ['Private', 'Your studies stay in your account, with optional two-factor sign-in.'], + ], + }, +]; + +// Small, decorative pictures of two real views, so you can see what you're signing in to. +function OutlinePreview() { + const row = (label, text, depth = 0, tag = '') => ( +
+ {label} + + {tag && {tag}} + {text} + +
+ ); + return ( + + ); +} + +function ComparePreview() { + const mark = (t) => {t}; + return ( + + ); +} + +function ThemeSwitch() { + const { readerTheme, setReaderTheme } = useApp(); + if (!setReaderTheme) return null; + return ( +
+ {Object.entries(READER_THEMES).map(([id, t]) => ( + + ))} +
+ ); +} + export default function AuthPage() { const { signIn, readerTheme } = useApp(); const shell = { style: themeVars(readerTheme) }; @@ -53,8 +136,8 @@ export default function AuthPage() { return (
-

Bible Study Project

-

Two-factor verification

+

Bible Study Project

+

Two-factor verification

{authMfaUseBackup ? 'Enter one of your saved backup codes.' @@ -109,83 +192,107 @@ export default function AuthPage() { } return ( -

-
- {/* Branding / feature panel — hidden on small screens to keep the form front and center there */} -
-

Bible Study Project

-

- Deep Bible study,
one chunk at a time. -

-

- Read Scripture in six translations, follow a reading plan into the text, and study a passage one chunk - at a time with commentary, word studies, and cross-references beside it. -

-
    -
  • Read in BSB, ESV, NLT, CSB, NASB, or NET, with audio and search
  • -
  • Reading plans that open straight into the Reader
  • -
  • Observe, Interpret, Apply notes with Greek & Hebrew word studies
  • -
  • Turn your notes into a script, then export or share
  • -
  • Your studies stay private to your account, with optional 2FA
  • -
-
- "Make every effort to present yourself approved to God, an unashamed workman who accurately - handles the word of truth." -
— 2 Timothy 2:15 (BSB)
-
+
+
+

+ + Bible Study Project +

+ +
+ +
+ {/* Sign in / create account. First on a phone; on a wide screen it stays in view beside the tour. */} +
+
+
+

{authMode === 'login' ? 'Sign in' : 'Create an account'}

+

+ {authMode === 'login' ? 'Pick up where you left off.' : 'Free to start. Your studies stay in your account.'} +

+
+ + + {authError &&

{authError}

} + +
+ +
+
+ “Make every effort to present yourself approved to God, an unashamed workman who accurately handles the word of truth.” +
— 2 Timothy 2:15 (BSB)
+
+
- {/* Sign in / register form */} -
-
-

Bible Study Project

-

- {authMode === 'login' ? 'Sign in' : 'Create an account'} + {/* What the app does */} +
+

Bible study, start to finish

+

+ Read it. Study it.
Teach it.

-
- - - {authError &&

{authError}

} - -
- -
+

+ A place to read Scripture in six translations, work through a passage one chunk at a time, and turn what you learn into an outline, + a script, and something you can share. +

+ +
+ + +
+ +
+ {FEATURES.map((group) => ( +
+

{group.title}

+
    + {group.points.map(([name, text]) => ( +
  • +

    {name}

    +

    {text}

    +
  • + ))} +
+
+ ))} +