diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..92322c9
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(bunx tsc:*)"
+ ]
+ }
+}
diff --git a/apps/mobile/.claude/settings.local.json b/apps/mobile/.claude/settings.local.json
index 176e6a5..2f71d4b 100644
--- a/apps/mobile/.claude/settings.local.json
+++ b/apps/mobile/.claude/settings.local.json
@@ -1,5 +1,12 @@
{
"enabledPlugins": {
"expo@claude-plugins-official": true
+ },
+ "permissions": {
+ "allow": [
+ "Read(//Users/joschuag/Developer/Public/gridtip/**)",
+ "Read(//Users/joschuag/Developer/Public/gridtip/apps/**)",
+ "Read(//Users/joschuag/Developer/Public/gridtip/packages/**)"
+ ]
}
}
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 70aedef..03078a8 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -14,8 +14,8 @@
"lint": "expo lint"
},
"dependencies": {
- "@gridtip/shared": "workspace:*",
"@expo/ui": "~55.0.5",
+ "@gridtip/shared": "workspace:*",
"@react-navigation/bottom-tabs": "^7.15.5",
"@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.33",
diff --git a/apps/mobile/src/app/(app)/home/_components/race-header/blurred-flag.tsx b/apps/mobile/src/app/(app)/home/_components/race-header/blurred-flag.tsx
new file mode 100644
index 0000000..b888ed6
--- /dev/null
+++ b/apps/mobile/src/app/(app)/home/_components/race-header/blurred-flag.tsx
@@ -0,0 +1,69 @@
+import { View, useColorScheme } from 'react-native'
+import { Image } from 'expo-image'
+import { Text } from '@/components/ui/text'
+import { Race } from '@/types'
+import { THEME } from '@/lib/theme'
+import { dummyRaceMeta } from './dummy'
+
+export function BlurredFlagHeader({ race }: { race: Race }) {
+ const scheme = useColorScheme() === 'dark' ? 'dark' : 'light'
+ const cardColor = THEME[scheme].card
+ const cardSoft =
+ scheme === 'dark' ? 'hsla(0, 0%, 3.9%, 0.6)' : 'hsla(0, 0%, 100%, 0.6)'
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ Round {dummyRaceMeta.round} · {dummyRaceMeta.city}
+
+
+ {race.raceName}
+
+
+ Qualifying {dummyRaceMeta.qualifyingDate}
+
+
+
+
+ )
+}
diff --git a/apps/mobile/src/app/(app)/home/_components/race-header/creative.tsx b/apps/mobile/src/app/(app)/home/_components/race-header/creative.tsx
new file mode 100644
index 0000000..49faa87
--- /dev/null
+++ b/apps/mobile/src/app/(app)/home/_components/race-header/creative.tsx
@@ -0,0 +1,160 @@
+import { useEffect } from 'react'
+import { View, useColorScheme } from 'react-native'
+import { Image } from 'expo-image'
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withRepeat,
+ withTiming,
+} from 'react-native-reanimated'
+import { Text } from '@/components/ui/text'
+import { Race } from '@/types'
+import { THEME } from '@/lib/theme'
+import { dummyRaceMeta } from './dummy'
+
+function PulseDot({ className, size = 8 }: { className?: string; size?: number }) {
+ const opacity = useSharedValue(1)
+ useEffect(() => {
+ opacity.value = withRepeat(withTiming(0.25, { duration: 700 }), -1, true)
+ }, [opacity])
+ const style = useAnimatedStyle(() => ({ opacity: opacity.value }))
+ return (
+
+ )
+}
+
+export function CreativeHeader({ race }: { race: Race }) {
+ const scheme = useColorScheme() === 'dark' ? 'dark' : 'light'
+ const bg = THEME[scheme].background
+ const bgFade = scheme === 'dark' ? 'hsla(0, 0%, 3.9%, 0)' : 'hsla(0, 0%, 100%, 0)'
+
+ const enter = useSharedValue(0)
+ useEffect(() => {
+ enter.value = withTiming(1, { duration: 500 })
+ }, [enter])
+ const enterStyle = useAnimatedStyle(() => ({
+ opacity: enter.value,
+ transform: [{ translateY: (1 - enter.value) * 16 }],
+ }))
+
+ const { days, hours, minutes, seconds } = dummyRaceMeta.countdown
+ const pad = (n: number) => n.toString().padStart(2, '0')
+ const countdown = `${pad(days)}:${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
+
+ return (
+
+
+
+
+
+
+
+ R{dummyRaceMeta.round.toString().padStart(2, '0')}
+
+
+
+
+
+ ROUND {dummyRaceMeta.round} / {dummyRaceMeta.totalRounds}
+
+
+
+
+
+
+ QUALI LOCK
+
+
+
+ {countdown}
+
+
+
+
+
+
+ {race.raceName.toUpperCase()}
+
+
+ {dummyRaceMeta.circuitName}
+
+
+
+
+
+ {[0, 1, 2, 3, 4].map((i) => {
+ const isLive = i === 4
+ return isLive ? (
+
+ ) : (
+
+ )
+ })}
+
+ TIPPING OPEN
+
+
+
+ {dummyRaceMeta.raceWindowLabel}
+
+
+
+ )
+}
diff --git a/apps/mobile/src/app/(app)/home/_components/race-header/dummy.ts b/apps/mobile/src/app/(app)/home/_components/race-header/dummy.ts
new file mode 100644
index 0000000..c6eaca5
--- /dev/null
+++ b/apps/mobile/src/app/(app)/home/_components/race-header/dummy.ts
@@ -0,0 +1,14 @@
+export const dummyRaceMeta = {
+ round: 7,
+ totalRounds: 24,
+ circuitName: 'Jeddah Corniche Circuit',
+ city: 'Jeddah',
+ qualifyingDate: 'Sat 16 Aug · 19:00',
+ raceWindowLabel: '17–19 AUG',
+ monthLabel: 'AUG',
+ dayLabel: '17',
+ countdown: { days: 2, hours: 14, minutes: 37, seconds: 12 },
+ locksInLabel: 'Locks in 2d 4h',
+}
+
+export type DummyRaceMeta = typeof dummyRaceMeta
diff --git a/apps/mobile/src/app/(app)/home/_components/race-header/native-polish.tsx b/apps/mobile/src/app/(app)/home/_components/race-header/native-polish.tsx
new file mode 100644
index 0000000..c77c56c
--- /dev/null
+++ b/apps/mobile/src/app/(app)/home/_components/race-header/native-polish.tsx
@@ -0,0 +1,97 @@
+import { Pressable, View } from 'react-native'
+import { Image } from 'expo-image'
+import { Clock, MapPin } from 'lucide-react-native'
+import { Text } from '@/components/ui/text'
+import { Icon } from '@/components/ui/icon'
+import { Race } from '@/types'
+import { dummyRaceMeta } from './dummy'
+
+export function NativePolishHeader({ race }: { race: Race }) {
+ const progress = dummyRaceMeta.round / dummyRaceMeta.totalRounds
+
+ return (
+
+
+
+
+ Round {dummyRaceMeta.round} · {dummyRaceMeta.raceWindowLabel}
+
+
+ {race.raceName}
+
+
+
+
+ {dummyRaceMeta.circuitName}
+
+
+
+
+
+
+
+ {dummyRaceMeta.monthLabel}
+
+
+ {dummyRaceMeta.dayLabel}
+
+
+
+
+
+
+
+
+
+ {({ pressed }) => (
+
+
+
+ {dummyRaceMeta.locksInLabel}
+
+
+ )}
+
+
+
+
+
+
+
+ Season
+
+ {dummyRaceMeta.round} / {dummyRaceMeta.totalRounds}
+
+
+
+
+ )
+}
diff --git a/apps/mobile/src/app/(app)/home/index.tsx b/apps/mobile/src/app/(app)/home/index.tsx
index 41f14a7..dd764e7 100644
--- a/apps/mobile/src/app/(app)/home/index.tsx
+++ b/apps/mobile/src/app/(app)/home/index.tsx
@@ -1,6 +1,6 @@
-import { Stack } from 'expo-router'
+import { Link, Stack } from 'expo-router'
import { Text } from '@/components/ui/text'
-import { ScrollView, View } from 'react-native'
+import { Pressable, ScrollView, View } from 'react-native'
import { useEffect, useState } from 'react'
import { useSession } from '@/lib/ctx'
import { api } from '@/lib/api'
@@ -91,6 +91,13 @@ export default function HomeScreen() {
}}
>
+
+
+
+ Preview header designs →
+
+
+
diff --git a/apps/mobile/src/app/(app)/home/race-header-preview.tsx b/apps/mobile/src/app/(app)/home/race-header-preview.tsx
new file mode 100644
index 0000000..f018bdc
--- /dev/null
+++ b/apps/mobile/src/app/(app)/home/race-header-preview.tsx
@@ -0,0 +1,54 @@
+import { Stack } from 'expo-router'
+import { ScrollView, View } from 'react-native'
+import { Text } from '@/components/ui/text'
+import { Race } from '@/types'
+import { BlurredFlagHeader } from './_components/race-header/blurred-flag'
+import { NativePolishHeader } from './_components/race-header/native-polish'
+import { CreativeHeader } from './_components/race-header/creative'
+
+const sampleRace: Race = {
+ id: 'preview',
+ country: 'Saudi Arabia',
+ raceName: 'Saudi Arabian Grand Prix',
+ image: 'https://www.flagcolorcodes.com/data/flag-of-saudi-arabia.png',
+ isSprint: false,
+}
+
+function Section({
+ label,
+ children,
+}: {
+ label: string
+ children: React.ReactNode
+}) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ )
+}
+
+export default function RaceHeaderPreviewScreen() {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/bun.lock b/bun.lock
index 7bb14ef..ee3c94c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "gridtip-mono",
@@ -26,6 +27,7 @@
"expo-font": "~55.0.4",
"expo-glass-effect": "~55.0.8",
"expo-image": "~55.0.6",
+ "expo-linear-gradient": "^55.0.13",
"expo-linking": "~55.0.8",
"expo-router": "~55.0.7",
"expo-secure-store": "~55.0.9",
@@ -65,6 +67,7 @@
"name": "f1-tipping-react",
"version": "0.1.0",
"dependencies": {
+ "@gridtip/shared": "workspace:*",
"@hookform/resolvers": "^5.2.1",
"@libsql/client": "^0.15.14",
"@paralleldrive/cuid2": "^2.2.2",
@@ -1756,6 +1759,8 @@
"expo-keep-awake": ["expo-keep-awake@55.0.6", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-acJjeHqkNxMVckEcJhGQeIksqqsarscSHJtT559bNgyiM4r14dViQ66su7bb6qDVeBt0K7z3glXI1dHVck1Zgg=="],
+ "expo-linear-gradient": ["expo-linear-gradient@55.0.13", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-Qz2T4jpkA15RIk29DBqI1TwW+8O9AN8MyC4TJPbh/5UnihH0yNNz3waplUO8Szh5OZ3czTGvtPQU4ysF3RDxwQ=="],
+
"expo-linking": ["expo-linking@55.0.13", "", { "dependencies": { "expo-constants": "~55.0.14", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-xbOqNWQCC5RGtXSW83ZCKOjRivyxO2zBouRYy/hgbsyrHUJhztMAjlq8RKYDUL8D6QVsH9Q81SNoq4Zhcn+4HQ=="],
"expo-modules-autolinking": ["expo-modules-autolinking@55.0.17", "", { "dependencies": { "@expo/require-utils": "^55.0.4", "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-VhlEVGnP+xBjfSKDKNN7GAPKN2whIfV08jsZvNj7UGyJWpZYiO6Emx1FLP5xd1+JZVpIrt/kxR641kdcPo7Ehw=="],