Skip to content
Open
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
7 changes: 7 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(bunx tsc:*)"
]
}
}
7 changes: 7 additions & 0 deletions apps/mobile/.claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -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/**)"
]
}
}
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<View className="bg-card border border-border rounded-2xl overflow-hidden relative">
<View className="absolute inset-0" style={{ overflow: 'hidden' }}>
<Image
source={race.image}
contentFit="cover"
blurRadius={20}
style={{
width: '100%',
height: '100%',
transform: [{ scale: 1.4 }],
}}
/>
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
experimental_backgroundImage: `linear-gradient(135deg, ${cardSoft} 0%, ${cardColor} 100%)`,
}}
/>
</View>

<View className="flex flex-row gap-4 items-center px-4 py-6">
<View
className="border-2 border-border"
style={{
width: 60,
height: 60,
borderRadius: 9999,
overflow: 'hidden',
}}
>
<Image
source={race.image}
contentFit="cover"
style={{ width: '100%', height: '100%' }}
/>
</View>
<View className="flex flex-col flex-1">
<Text className="text-muted-foreground text-xs">
Round {dummyRaceMeta.round} · {dummyRaceMeta.city}
</Text>
<Text className="text-2xl font-semibold" numberOfLines={1}>
{race.raceName}
</Text>
<Text className="text-muted-foreground text-xs mt-1">
Qualifying {dummyRaceMeta.qualifyingDate}
</Text>
</View>
</View>
</View>
)
}
160 changes: 160 additions & 0 deletions apps/mobile/src/app/(app)/home/_components/race-header/creative.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Animated.View
className={className}
style={[
{ width: size, height: size, borderRadius: size / 2 },
style,
]}
/>
)
}

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 (
<Animated.View
style={enterStyle}
className="bg-background border border-border rounded-2xl overflow-hidden relative"
>
<View
className="absolute"
style={{
top: -20,
left: -20,
right: -20,
bottom: -20,
opacity: 0.35,
transform: [{ rotate: '-6deg' }],
}}
>
<Image
source={race.image}
contentFit="cover"
blurRadius={30}
style={{ width: '100%', height: '100%' }}
/>
</View>
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
experimental_backgroundImage: `linear-gradient(180deg, ${bgFade} 0%, ${bg} 100%)`,
}}
/>

<Text
className="absolute font-black italic text-foreground"
style={{
fontSize: 96,
lineHeight: 96,
opacity: 0.07,
left: -12,
top: 24,
}}
>
R{dummyRaceMeta.round.toString().padStart(2, '0')}
</Text>

<View className="flex flex-row justify-between items-start px-4 pt-4">
<View>
<Text className="text-[10px] tracking-[0.25em] text-muted-foreground font-medium">
ROUND {dummyRaceMeta.round} / {dummyRaceMeta.totalRounds}
</Text>
</View>
<View className="border border-border rounded-md px-2 py-1.5 bg-card/60">
<View className="flex flex-row items-center gap-1.5">
<PulseDot className="bg-destructive" size={6} />
<Text className="text-[9px] tracking-[0.2em] text-muted-foreground font-medium">
QUALI LOCK
</Text>
</View>
<Text
className="text-foreground font-semibold mt-0.5"
style={{
fontSize: 13,
fontVariant: ['tabular-nums'],
letterSpacing: 0.5,
}}
>
{countdown}
</Text>
</View>
</View>

<View className="px-4 pt-8 pb-4">
<Text
className="font-black tracking-tight text-foreground"
style={{ fontSize: 36, lineHeight: 38 }}
numberOfLines={2}
>
{race.raceName.toUpperCase()}
</Text>
<Text className="text-xs text-muted-foreground mt-1 tracking-widest uppercase">
{dummyRaceMeta.circuitName}
</Text>
</View>

<View className="border-t border-border px-4 py-3 flex flex-row items-center justify-between">
<View className="flex flex-row items-center gap-1.5">
{[0, 1, 2, 3, 4].map((i) => {
const isLive = i === 4
return isLive ? (
<PulseDot key={i} className="bg-destructive" size={10} />
) : (
<View
key={i}
className="bg-muted"
style={{ width: 10, height: 10, borderRadius: 2 }}
/>
)
})}
<Text className="text-[10px] tracking-[0.2em] text-destructive font-semibold ml-2">
TIPPING OPEN
</Text>
</View>
<Text className="text-[10px] tracking-[0.2em] text-muted-foreground font-medium">
{dummyRaceMeta.raceWindowLabel}
</Text>
</View>
</Animated.View>
)
}
14 changes: 14 additions & 0 deletions apps/mobile/src/app/(app)/home/_components/race-header/dummy.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 (
<View className="bg-card border border-border rounded-2xl px-4 py-5 flex flex-col gap-4">
<View className="flex flex-row items-start justify-between gap-3">
<View className="flex flex-col flex-1 gap-2">
<Text className="text-[11px] tracking-[0.15em] text-muted-foreground font-medium uppercase">
Round {dummyRaceMeta.round} · {dummyRaceMeta.raceWindowLabel}
</Text>
<Text
className="text-3xl font-semibold tracking-tight"
numberOfLines={1}
>
{race.raceName}
</Text>
<View className="flex flex-row items-center gap-1.5">
<Icon as={MapPin} size={13} className="text-muted-foreground" />
<Text className="text-sm text-muted-foreground">
{dummyRaceMeta.circuitName}
</Text>
</View>
</View>

<View className="relative" style={{ width: 56, height: 56 }}>
<View
className="border border-border rounded-xl items-center justify-center"
style={{ width: 56, height: 56 }}
>
<Text className="text-[10px] tracking-[0.18em] text-destructive font-semibold">
{dummyRaceMeta.monthLabel}
</Text>
<Text className="text-2xl font-bold leading-7">
{dummyRaceMeta.dayLabel}
</Text>
</View>
<View
className="absolute border-2 border-card overflow-hidden"
style={{
width: 22,
height: 22,
borderRadius: 9999,
top: -6,
right: -6,
}}
>
<Image
source={race.image}
contentFit="cover"
style={{ width: '100%', height: '100%' }}
/>
</View>
</View>
</View>

<Pressable className="self-start">
{({ pressed }) => (
<View
className="flex flex-row items-center gap-1.5 rounded-full border border-border px-3 py-1.5 bg-muted/40"
style={{ opacity: pressed ? 0.7 : 1 }}
>
<Icon as={Clock} size={12} className="text-foreground" />
<Text className="text-xs font-medium">
{dummyRaceMeta.locksInLabel}
</Text>
</View>
)}
</Pressable>

<View className="flex flex-col gap-1.5">
<View
className="bg-muted rounded-full overflow-hidden"
style={{ height: 3 }}
>
<View
className="bg-primary"
style={{ width: `${progress * 100}%`, height: '100%' }}
/>
</View>
<View className="flex flex-row justify-between">
<Text className="text-[11px] text-muted-foreground">Season</Text>
<Text className="text-[11px] text-muted-foreground">
{dummyRaceMeta.round} / {dummyRaceMeta.totalRounds}
</Text>
</View>
</View>
</View>
)
}
11 changes: 9 additions & 2 deletions apps/mobile/src/app/(app)/home/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -91,6 +91,13 @@ export default function HomeScreen() {
}}
>
<ScrollView>
<Link href="/home/race-header-preview" asChild>
<Pressable className="px-4 pt-3">
<Text className="text-xs text-muted-foreground underline">
Preview header designs →
</Text>
</Pressable>
</Link>
<Content />
</ScrollView>
</View>
Expand Down
Loading