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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Supabase (optional). Without these the app runs entirely as a guest:
# every mode stays playable, only the worldwide leaderboard needs an account.
# Create a project on https://supabase.com, then copy Settings > API.
VITE_SUPABASE_URL=https://xxxxxxxxxxxxxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ coverage

# Private specification — kept locally, never committed
cahier-des-charges.html

# Supabase CLI local artefacts
supabase/.temp
supabase/.branches
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ cahier-des-charges.html

# Vendored Stockfish build — never reformat
public/stockfish

# Supabase CLI local artefacts
supabase/.temp
supabase/.branches
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,97 @@ npm run dev

The app is served on http://localhost:5173.

## Backend setup (optional)

Every mode is playable without a backend: the app detects that no Supabase
project is configured and runs in guest mode, keeping progress in
`localStorage`. Only accounts and the worldwide leaderboard need the steps
below.

### 1. Create the project

1. Sign in on [supabase.com](https://supabase.com) and create a new project.
Note the database password somewhere safe; it is shown only once.
2. Open **Project Settings > API** and copy **Project URL** and the **anon
public** key.
3. Copy `.env.example` to `.env.local` and paste both values:

```bash
cp .env.example .env.local
```

The anon key is meant to be public — it is shipped in the browser bundle,
and Row Level Security is what actually protects the data. Never put the
`service_role` key in this file.

### 2. Create the tables

`supabase/migrations/` holds the whole schema: tables, Row Level Security
policies, privileges, the trigger that creates a profile on sign-up, and the
realtime publication the leaderboard subscribes to.

The quickest way is the dashboard: open **SQL Editor**, paste the contents of
`supabase/migrations/20260711000000_init.sql`, and run it. The script is
idempotent, so running it twice is harmless.

With the [Supabase CLI](https://supabase.com/docs/guides/cli) instead:

```bash
npx supabase link --project-ref <your-project-ref>
npx supabase db push
```

Check it worked under **Table Editor**: `profiles`, `scores`,
`puzzle_progress` and `achievements` should be listed, each marked _RLS
enabled_.

### 3. Set the URLs

Under **Authentication > URL Configuration**:

- **Site URL** — `http://localhost:5173` while developing, the deployed
address once online.
- **Redirect URLs** — add `http://localhost:5173/profile` and, once deployed,
`https://<your-domain>/profile`. Sign-in sends the player back to `/profile`
and this list is matched exactly, so a missing entry fails the sign-in.

Under **Authentication > Providers > Email**, turn **Confirm email** off while
testing, unless you want to click a confirmation link for every test account.

### 4. Google sign-in (optional)

The button is shown regardless; it only works once this is done.

1. In [Google Cloud Console](https://console.cloud.google.com), create a
project, then **APIs & Services > OAuth consent screen**: choose
**External**, fill in the app name and your email, and add your own address
under **Test users** so you can sign in before the app is published.
2. **Credentials > Create credentials > OAuth client ID**, type **Web
application**. Under **Authorised redirect URIs**, paste the callback shown
by Supabase in **Authentication > Providers > Google** — it looks like
`https://<project-ref>.supabase.co/auth/v1/callback`. Add
`http://127.0.0.1:54321/auth/v1/callback` as well if you intend to sign in
against a local stack; that is where its own auth service listens, and
Google rejects any callback not listed here.
3. Copy the generated **Client ID** and **Client secret** into that same
Supabase Google provider panel, enable it, and save.

Nothing changes in the app itself: the provider is read from the project.

### Running the backend locally

Docker is required. `supabase/config.toml` is already set up for this app —
port 5173, and the `/profile` callback in the allow-list.

```bash
npx supabase start # applies supabase/migrations automatically
npx supabase status # prints the local URL and anon key for .env.local
```

For local Google sign-in, put the same credentials in `supabase/.env` (git
ignored, see `supabase/.env.example`). Without it the stack still starts and
email sign-in works; only the Google button is inert.

## Scripts

| Script | Purpose |
Expand Down
111 changes: 111 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@fontsource/playfair-display": "^5.2.8",
"@supabase/supabase-js": "^2.110.8",
"chess.js": "^1.4.0",
"framer-motion": "^11.18.2",
"react": "^18.3.1",
Expand Down
6 changes: 5 additions & 1 deletion scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ const BUDGETS = [
{
label: 'Initial JavaScript',
match: (path) => path.endsWith('.js'),
maxGzipKb: 200,
// Raised from 200 kB for M10: the Supabase client is about 65 kB gzipped
// and is a hard requirement of the accounts and leaderboard of section 2.6.
// M9 should bring this back down with route-level lazy loading, which the
// specification assigns to it; until then the guard still catches growth.
maxGzipKb: 230,
},
{
label: 'CSS',
Expand Down
4 changes: 3 additions & 1 deletion src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ describe('routing', () => {
[ROUTES.battle, /Affrontement/],
[ROUTES.puzzle, 'Puzzles'],
[ROUTES.hunt, /Chasse aux Pièces/],
[ROUTES.leaderboard, /Classement mondial/],
// Tests run with no backend configured, so the guard explains the
// leaderboard is unavailable rather than rendering it.
[ROUTES.leaderboard, /Classement indisponible/],
[ROUTES.profile, 'Profil'],
])('renders the expected page at %s', (path, heading) => {
renderAt(path)
Expand Down
21 changes: 20 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useEffect } from 'react'
import { Route, Routes } from 'react-router-dom'
import AppLayout from '@/components/Layout/AppLayout'
import LoginPage from '@/features/auth/LoginPage'
import RegisterPage from '@/features/auth/RegisterPage'
import RequireAuth from '@/features/auth/RequireAuth'
import BattlePage from '@/features/battle/BattlePage'
import CoachPage from '@/features/coach/CoachPage'
import HomePage from '@/features/home/HomePage'
Expand All @@ -9,8 +13,14 @@ import NotFoundPage from '@/features/NotFoundPage'
import ProfilePage from '@/features/profile/ProfilePage'
import PuzzlePage from '@/features/puzzle/PuzzlePage'
import { ROUTES } from '@/routes'
import { useAuthStore } from '@/store/useAuthStore'

export default function App() {
const initialise = useAuthStore((state) => state.initialise)

// Restores the stored session and follows sign-in/sign-out for the whole app.
useEffect(() => initialise(), [initialise])

return (
<Routes>
<Route element={<AppLayout />}>
Expand All @@ -19,7 +29,16 @@ export default function App() {
<Route path={ROUTES.battle} element={<BattlePage />} />
<Route path={ROUTES.puzzle} element={<PuzzlePage />} />
<Route path={ROUTES.hunt} element={<HuntPage />} />
<Route path={ROUTES.leaderboard} element={<LeaderboardPage />} />
<Route
path={ROUTES.leaderboard}
element={
<RequireAuth>
<LeaderboardPage />
</RequireAuth>
}
/>
<Route path={ROUTES.login} element={<LoginPage />} />
<Route path={ROUTES.register} element={<RegisterPage />} />
<Route path={ROUTES.profile} element={<ProfilePage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
Expand Down
10 changes: 6 additions & 4 deletions src/components/Layout/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { BOTTOM_BAR_ITEMS, NAV_ITEMS } from '@/components/Layout/navigation'
import { ROUTES } from '@/routes'

describe('navigation', () => {
it('exposes one entry per SPA route', () => {
const navPaths = NAV_ITEMS.map((item) => item.path).sort()
const routePaths = Object.values(ROUTES).sort()
expect(navPaths).toEqual(routePaths)
it('exposes one entry per navigable route', () => {
// The auth screens are reached from a guard or a link, never from the menu.
const navigable = Object.values(ROUTES).filter(
(route) => route !== ROUTES.login && route !== ROUTES.register,
)
expect(NAV_ITEMS.map((item) => item.path).sort()).toEqual(navigable.sort())
})

it('contains no duplicate route', () => {
Expand Down
Loading
Loading