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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
44 changes: 44 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env
.env*.local

# typescript
*.tsbuildinfo

app-example

# generated native folders
/ios
/android
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
2 changes: 2 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
npx tsc --noEmit
npm audit --audit-level=high
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}
61 changes: 61 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Tria — Claude Instructions

## Project

Triathlon training app ("Runna for triathlons"). React Native + Expo SDK 54, expo-router, TypeScript.

## Stack

- **UI:** react-native-paper (MD3)
- **State:** Zustand + TanStack Query
- **Backend:** Supabase (auth, db) with PKCE OAuth
- **Auth:** Google + Strava via expo-web-browser — requires a native dev build, not Expo Go

## Commands

```bash
npx expo run:ios # native build (required for auth)
npx expo start # JS bundler only (after first build)
npx tsc --noEmit # type check — must pass with 0 errors
supabase gen types typescript --linked > types/database.ts # regenerate DB types
```

## Directory structure

Feature-based architecture:

```
app/ — expo-router screens only (no logic)
(auth)/ — sign-in, callback
(tabs)/ — today, plan, settings
_layout.tsx — PaperProvider + QueryClient + AuthGate

features/ — one folder per product feature
auth/
components/ — UI components specific to auth
lib/ — auth.ts (signIn, signOut, OAuth helpers)
store/ — auth-store.ts (Zustand)

shared/ — cross-feature utilities, no feature-specific logic
components/ — generic UI (HapticTab, ThemedText, etc.)
constants/ — theme, colors
hooks/ — useColorScheme, useThemeColor
lib/ — supabase.ts, query-client.ts

supabase/ — Supabase CLI only (config.toml, migrations)
types/ — database.ts (generated), training.ts, user.ts
```

New features go in `features/<name>/` with `components/`, `lib/`, `store/` subfolders as needed. Only put things in `shared/` if they are genuinely used by 2+ features.

## Key conventions

- `session?.user` — never store `user` separately from `session` in Zustand
- Auth navigation is owned entirely by `AuthGate` in `app/_layout.tsx` via `onAuthStateChange` — don't add redirect logic elsewhere
- `supabase/migrations/` owns the schema — do not write ad-hoc SQL files

## What to run after changes

- JS-only changes: press `r` in the Expo terminal
- New native dependencies: `npx expo run:ios`
- Schema changes: regenerate types with the command above
89 changes: 88 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,89 @@
# Tria
Runna, but for Triathlons

A triathlon training app — Runna, but for triathlons.

## Prerequisites

- Node.js 18+
- Xcode (for iOS builds)
- A Supabase project with the schema applied

## Setup

1. **Install dependencies**

```bash
npm install
```

2. **Configure environment variables**

Copy `.env.example` to `.env` and fill in your Supabase credentials:

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

```
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
```

3. **Set up the database**

Run the migrations against your Supabase project:

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

## Running the app

### iOS development build (required for Google Sign-In)

Google OAuth uses the `tria://` custom URL scheme for the redirect, which only works in a native build — not Expo Go.

```bash
npx expo run:ios
```

This installs a development build directly on your device or simulator. Re-run this command whenever native dependencies change.

### Start the JS bundler (after initial build)

Once the native build is installed, you only need to restart the bundler for JS changes:

```bash
npx expo start
```

Then press `i` to open in the iOS simulator, or scan the QR code to open on your device.

> **Note:** Expo Go does not support Google Sign-In. Always use the development build.

## Supabase

### Regenerate TypeScript types

After any schema changes, regenerate `types/database.ts`:

```bash
supabase gen types typescript --linked > types/database.ts
```

### Migrations

```bash
supabase migration new <migration-name> # create a new migration
supabase db push # push migrations to remote
supabase db pull # pull remote schema changes
```

## Tech stack

- **Framework:** React Native + Expo (SDK 54) with expo-router
- **UI:** react-native-paper (Material Design 3)
- **State:** Zustand + TanStack Query
- **Backend:** Supabase (auth, database, storage)
- **Auth:** Google OAuth via PKCE + expo-web-browser
59 changes: 59 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"expo": {
"name": "Tria",
"slug": "Tria",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "tria",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.jacobcurtis786.Tria"
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#000000"
}
}
],
"expo-secure-store",
[
"expo-build-properties",
{
"ios": {
"useFrameworks": "static"
}
}
],
"@react-native-community/datetimepicker"
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}
10 changes: 10 additions & 0 deletions app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Stack } from 'expo-router';

export default function AuthLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="sign-in" />
<Stack.Screen name="callback" />
</Stack>
);
}
32 changes: 32 additions & 0 deletions app/(auth)/callback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { View, StyleSheet } from 'react-native';
import { ActivityIndicator, Text } from 'react-native-paper';

/**
* Deep-link target for OAuth redirects.
* In the openAuthSessionAsync flow, iOS intercepts the tria:// redirect before
* it fires as a deep link, so this screen is never rendered in the normal path.
* AuthGate in _layout.tsx handles all post-auth navigation via onAuthStateChange.
*/
export default function CallbackScreen() {
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
<Text variant="bodyMedium" style={styles.text}>
Completing sign in…
</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 16,
backgroundColor: '#0F172A',
},
text: {
color: '#94A3B8',
},
});
Loading
Loading