Skip to content

Commit d4789c3

Browse files
authored
Merge pull request #3 from Mookiies/002-provider-settings
Support The Trivia API
2 parents ba92315 + 85f48f1 commit d4789c3

32 files changed

Lines changed: 1538 additions & 84 deletions

.specify/memory/constitution.md

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,3 @@
1-
<!--
2-
SYNC IMPACT REPORT
3-
==================
4-
Version change: 1.0.0 → 1.1.0
5-
Bump rationale: MINOR — new principle (V. Live Verification via Maestro) added
6-
Modified principles: None renamed
7-
Added sections: V. Live Verification via Maestro
8-
Removed sections: None
9-
Templates requiring updates:
10-
- .specify/templates/plan-template.md ✅ Constitution Check guidance updated for Principle V
11-
- .specify/templates/spec-template.md ✅ Independent Test guidance updated for Maestro
12-
- .specify/templates/tasks-template.md ✅ Checkpoints and Polish phase updated for Maestro
13-
Deferred TODOs: None
14-
-->
15-
161
# Cirquiz Constitution
172

183
## Core Principles
@@ -41,36 +26,16 @@ Deferred TODOs: None
4126
- New features MUST NOT introduce obvious regressions to existing flows.
4227
- N+1 data-fetching patterns are not allowed.
4328

44-
### V. Live Verification via Maestro
45-
46-
All P1 user journeys MUST be verified against the running app using the Maestro MCP before
47-
a feature is considered done. Required steps:
48-
49-
- Launch the app with `mcp__maestro__launch_app` on a simulator or real device.
50-
- Confirm expected UI state via `mcp__maestro__inspect_view_hierarchy` and/or
51-
`mcp__maestro__take_screenshot`.
52-
- Run scenario flows with `mcp__maestro__run_flow` or `mcp__maestro__run_flow_files`
53-
where a repeatable flow exists.
54-
55-
Unit tests and static analysis alone do NOT satisfy verification for any UI-facing feature.
56-
At least one Maestro-based check MUST be performed per P1 user story, and its result
57-
(screenshot or passing flow output) MUST be documented in the feature's `specs/` directory
58-
before merge.
59-
60-
**Rationale**: Cirquiz is a mobile app. Static tests can miss rendering issues, navigation
61-
bugs, and platform-specific behavior. Maestro MCP provides direct inspection of the running
62-
app, making acceptance verification concrete and reproducible.
63-
6429
## Governance
6530

6631
This constitution guides technical and implementation decisions for the Cirquiz monorepo.
6732

6833
- **Constitution Check**: Each feature plan (`plan.md`) MUST include a brief note on how
69-
all five principles apply or are intentionally excepted.
34+
all four principles apply or are intentionally excepted.
7035
- **Exceptions**: Deviations from a principle MUST be noted in `plan.md` with a one-line
7136
rationale.
7237
- **Amendments**: Use semantic versioning — MAJOR for removals/redefinitions, MINOR for
7338
additions, PATCH for wording. Update `LAST_AMENDED_DATE` on any change.
7439
- This is a personal project; governance is lightweight by design. Common sense prevails.
7540

76-
**Version**: 1.1.0 | **Ratified**: 2026-03-07 | **Last Amended**: 2026-03-07
41+
**Version**: 2.0.0 | **Ratified**: 2026-03-07 | **Last Amended**: 2026-03-11

.specify/templates/plan-template.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,11 @@
3131

3232
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
3333

34-
[Gates determined based on constitution file. Must address all five principles:]
34+
[Gates determined based on constitution file. Must address all four principles:]
3535
[I. Code Quality — readability, no duplication, lint passes]
3636
[II. Testing Standards — happy-path tests for P1 journeys, written alongside code]
3737
[III. UX Consistency — navigation/error/loading patterns match existing screens]
3838
[IV. Performance — <2s interactions, no regressions, no N+1 fetching]
39-
[V. Live Verification via Maestro — each P1 story verified by launching the app and]
40-
[ inspecting with Maestro MCP (inspect_view_hierarchy / take_screenshot / run_flow).]
41-
[ Screenshot or passing flow output MUST be saved to specs/ before merge.]
4239

4340
## Project Structure
4441

.specify/templates/spec-template.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@
2626

2727
**Why this priority**: [Explain the value and why it has this priority level]
2828

29-
**Independent Test**: [Describe how this can be tested independently. For mobile UI stories,
30-
include the Maestro MCP steps: launch app → inspect view hierarchy / take screenshot →
31-
run flow. E.g., "Launch app, navigate to [screen], verify [element] visible via
32-
inspect_view_hierarchy; run maestro flow flows/[story].yaml"]
29+
**Independent Test**: [Describe how this can be tested independently.]
3330

3431
**Acceptance Scenarios**:
3532

@@ -44,8 +41,7 @@ inspect_view_hierarchy; run maestro flow flows/[story].yaml"]
4441

4542
**Why this priority**: [Explain the value and why it has this priority level]
4643

47-
**Independent Test**: [Describe how this can be tested independently. For mobile UI stories,
48-
include Maestro MCP steps: launch app → inspect / screenshot → run flow.]
44+
**Independent Test**: [Describe how this can be tested independently.]
4945

5046
**Acceptance Scenarios**:
5147

.specify/templates/tasks-template.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,7 @@ Examples of foundational tasks (adjust based on your project):
9595
- [ ] T016 [US1] Add validation and error handling
9696
- [ ] T017 [US1] Add logging for user story 1 operations
9797

98-
**Checkpoint**: User Story 1 complete — verify live with Maestro MCP:
99-
1. `mcp__maestro__launch_app` → navigate to the relevant screen
100-
2. `mcp__maestro__inspect_view_hierarchy` or `mcp__maestro__take_screenshot`
101-
3. Save screenshot/flow result to `specs/[###-feature-name]/verification/us1/`
98+
**Checkpoint**: User Story 1 complete — verify independently per spec.md Independent Test.
10299

103100
---
104101

@@ -120,8 +117,7 @@ Examples of foundational tasks (adjust based on your project):
120117
- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
121118
- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
122119

123-
**Checkpoint**: User Story 2 complete — verify live with Maestro MCP and save result to
124-
`specs/[###-feature-name]/verification/us2/`
120+
**Checkpoint**: User Story 2 complete — verify independently per spec.md Independent Test.
125121

126122
---
127123

@@ -142,8 +138,7 @@ Examples of foundational tasks (adjust based on your project):
142138
- [ ] T027 [US3] Implement [Service] in src/services/[service].py
143139
- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
144140

145-
**Checkpoint**: User Story 3 complete — verify live with Maestro MCP and save result to
146-
`specs/[###-feature-name]/verification/us3/`
141+
**Checkpoint**: User Story 3 complete — verify independently per spec.md Independent Test.
147142

148143
---
149144

@@ -161,8 +156,7 @@ Examples of foundational tasks (adjust based on your project):
161156
- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
162157
- [ ] TXXX Security hardening
163158
- [ ] TXXX Run quickstart.md validation
164-
- [ ] TXXX [P] Maestro live verification — run all P1 story flows, save screenshots/results
165-
to `specs/[###-feature-name]/verification/` before merge
159+
- [ ] TXXX Final validation — verify all P1 story acceptance scenarios pass
166160

167161
---
168162

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ specs/ # Feature specs and plans
2020
```
2121

2222
## Active Technologies
23+
- TypeScript ~5.9 + React Native 0.83.2, Expo SDK ~55, Expo Router ~55, Zustand ^5, @react-native-async-storage/async-storage, react-native-reanimated, i18next + react-i18nex (002-provider-settings)
24+
- AsyncStorage — `@cirquiz/settings` (new) and `@cirquiz/active_game` (existing) (002-provider-settings)
2325

2426
- **Runtime**: React Native 0.83.2, React 19.2.0
2527
- **Framework**: Expo SDK ~55, Expo Router ~55
@@ -62,8 +64,9 @@ yarn workspace cirquiz typecheck
6264
- Zustand for global state (stores in `apps/cirquiz/src/state/`)
6365

6466
## Recent Changes
67+
- 002-provider-settings: Added TypeScript ~5.9 + React Native 0.83.2, Expo SDK ~55, Expo Router ~55, Zustand ^5, @react-native-async-storage/async-storage, react-native-reanimated, i18next + react-i18nex
6568

6669
- 001-pass-and-play-trivia: Pass-and-play local multiplayer trivia game with player setup, question flow, handoff screens, reveal, and standings
6770

6871
<!-- MANUAL ADDITIONS START -->
69-
<!-- MANUAL ADDITIONS END -->
72+
<!-- MANUAL ADDITIONS END -->

apps/.TODO.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
This file is purely for notes and tracking things I want to implement going forward.
22

33
- replace when going to the home screen
4-
- Cache and prefetch the categories. Categories shouldn't be changing while the app is running. We can just hard code these.
5-
- Add support for trivia API and a settings page to be able to change them.
6-
- Along with this we should then support having multiple category selection because this api support that. also multiple difficulty
4+
- Instead of refresshing the session for otdb when we run out of questions we should show some sort of UI to reset the session or allow changing the category selection
5+
- On taking a long time to load for continue, if someone quits the game it should cancel the pending network request and navigaiton
76

87
OFFLINE SUPPORT
98
- Store the list of categories from otdb and return that

apps/cirquiz/app/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Animated, {
1313
import { useSafeAreaInsets } from 'react-native-safe-area-context';
1414
import { Button } from '../src/components/Button';
1515
import { GradientScreen } from '../src/components/GradientScreen';
16+
import { IconButton } from '../src/components/IconButton';
1617
import { ShineButton } from '../src/components/ShineButton';
1718
import { useGameStore } from '../src/state/gameStore';
1819
import { colors, fontSize, spacing } from '../src/theme';
@@ -156,6 +157,13 @@ export default function HomeScreen() {
156157
return (
157158
<GradientScreen>
158159
<View style={[styles.screen, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
160+
<View style={styles.topBar}>
161+
<IconButton
162+
icon="settings-outline"
163+
onPress={() => router.push('/settings')}
164+
color={colors.text}
165+
/>
166+
</View>
159167
<Animated.View style={[styles.hero, heroStyle]}>
160168
<View style={styles.logoWrapper}>
161169
<CirclequizLogo width={340} height={95} />
@@ -206,6 +214,10 @@ const styles = StyleSheet.create({
206214
flex: 1,
207215
flexDirection: 'column',
208216
},
217+
topBar: {
218+
alignItems: 'flex-end',
219+
paddingHorizontal: spacing.sm,
220+
},
209221
avatarRow: {
210222
flexDirection: 'row',
211223
flexWrap: 'wrap',

apps/cirquiz/app/settings.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { router } from 'expo-router';
2+
import { useTranslation } from 'react-i18next';
3+
import { StyleSheet, Text, View } from 'react-native';
4+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
5+
import { GradientScreen } from '../src/components/GradientScreen';
6+
import { IconButton } from '../src/components/IconButton';
7+
import { SelectableRow } from '../src/components/SelectableRow';
8+
import { useSettingsStore } from '../src/state/settingsStore';
9+
import { colors, fontSize, fontWeight, spacing } from '../src/theme';
10+
11+
export default function SettingsScreen() {
12+
const { t } = useTranslation();
13+
const insets = useSafeAreaInsets();
14+
const questionSource = useSettingsStore((s) => s.questionSource);
15+
const setQuestionSource = useSettingsStore((s) => s.setQuestionSource);
16+
17+
return (
18+
<GradientScreen>
19+
<View style={[styles.screen, { paddingBottom: insets.bottom }]}>
20+
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
21+
<IconButton icon="arrow-back" onPress={() => router.back()} color={colors.text} />
22+
<Text style={styles.headerTitle}>{t('settings.title')}</Text>
23+
<View style={styles.headerSpacer} />
24+
</View>
25+
26+
<View style={styles.content}>
27+
<Text style={styles.sectionLabel}>{t('settings.questionSource')}</Text>
28+
<SelectableRow
29+
label={t('settings.otdb')}
30+
active={questionSource === 'otdb'}
31+
onPress={() => setQuestionSource('otdb')}
32+
/>
33+
<SelectableRow
34+
label={t('settings.theTriviaApi')}
35+
active={questionSource === 'the-trivia-api'}
36+
onPress={() => setQuestionSource('the-trivia-api')}
37+
/>
38+
</View>
39+
</View>
40+
</GradientScreen>
41+
);
42+
}
43+
44+
const styles = StyleSheet.create({
45+
screen: {
46+
flex: 1,
47+
},
48+
header: {
49+
flexDirection: 'row',
50+
alignItems: 'center',
51+
paddingHorizontal: spacing.lg,
52+
paddingBottom: spacing.sm,
53+
},
54+
headerTitle: {
55+
flex: 1,
56+
textAlign: 'center',
57+
fontSize: fontSize['2xl'],
58+
fontWeight: fontWeight.bold,
59+
color: colors.text,
60+
},
61+
headerSpacer: { width: 44 },
62+
content: {
63+
paddingHorizontal: spacing.xl,
64+
paddingTop: spacing.lg,
65+
},
66+
sectionLabel: {
67+
fontSize: fontSize.md,
68+
fontWeight: fontWeight.semibold,
69+
color: colors.textSecondary,
70+
marginBottom: spacing.sm,
71+
},
72+
});

apps/cirquiz/app/setup.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import { Button } from '../src/components/Button';
2222
import { AVATAR_LIST, type AvatarKey } from '../src/avatars';
2323
import { type Difficulty } from '../src/providers';
2424
import { useCategoryLoader } from '../src/hooks/useCategoryLoader';
25+
import { useToast } from '../src/hooks/useToast';
2526
import { useGameStore } from '../src/state/gameStore';
27+
import { useSettingsStore } from '../src/state/settingsStore';
2628
import { colors, fontSize, fontWeight, radius, spacing } from '../src/theme';
2729
import { IconButton } from '../src/components/IconButton';
2830
import LightningSVG from '../assets/lightning.svg';
@@ -77,6 +79,32 @@ export default function SetupScreen() {
7779
const [pickerIndex, setPickerIndex] = useState<number | null>(null);
7880
const [stickyHeight, setStickyHeight] = useState(0);
7981

82+
const questionSource = useSettingsStore((s) => s.questionSource);
83+
const isFirstRender = useRef(true);
84+
const { showToast, ToastNode } = useToast();
85+
86+
// Refs let us read latest values in the effect without adding them as deps
87+
const categoryRef = useRef(category);
88+
categoryRef.current = category;
89+
const quickPlayRef = useRef(quickPlay);
90+
quickPlayRef.current = quickPlay;
91+
const loadCategoriesRef = useRef(loadCategories);
92+
loadCategoriesRef.current = loadCategories;
93+
94+
useEffect(() => {
95+
if (isFirstRender.current) {
96+
isFirstRender.current = false;
97+
return;
98+
}
99+
if (categoryRef.current !== undefined) {
100+
setCategory(undefined);
101+
showToast(t('settings.categoryResetNotice'));
102+
}
103+
if (!quickPlayRef.current) {
104+
loadCategoriesRef.current();
105+
}
106+
}, [questionSource, t, showToast]);
107+
80108
const usedAvatars = players.map((p) => p.avatar);
81109

82110
const addPlayer = () => {
@@ -175,7 +203,11 @@ export default function SetupScreen() {
175203
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
176204
<IconButton icon="arrow-back" onPress={() => router.back()} color={colors.text} />
177205
<Text style={styles.headerTitle}>{t('setup.title', 'NEW GAME')}</Text>
178-
<View style={styles.headerSpacer} />
206+
<IconButton
207+
icon="settings-outline"
208+
onPress={() => router.push('/settings')}
209+
color={colors.text}
210+
/>
179211
</View>
180212

181213
<KeyboardAwareScrollView
@@ -332,6 +364,7 @@ export default function SetupScreen() {
332364
</View>
333365
</Pressable>
334366
</Modal>
367+
{ToastNode}
335368
</GradientScreen>
336369
);
337370
}

apps/cirquiz/src/components/GradientScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface Props {
1010
showBlobs?: boolean;
1111
mode?: keyof typeof GRADIENT_COLORS;
1212
}
13-
const LIGHTEN_AMMOUNT = 0.06;
13+
const LIGHTEN_AMMOUNT = 0.045;
1414
const GRADIENT_COLORS = {
1515
standard: [colors.primaryFaint, colors.white, colors.difficultyFaint] as const,
1616
lighter: [

0 commit comments

Comments
 (0)