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=="],