Skip to content

Commit bf19435

Browse files
committed
feat: first pass on score slider
1 parent bd712ab commit bf19435

10 files changed

+465
-7
lines changed

apps/expo/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@rn-primitives/label": "^1.0.3",
4141
"@rn-primitives/portal": "^1.0.3",
4242
"@rn-primitives/progress": "^1.0.3",
43+
"@rn-primitives/separator": "^1.0.3",
4344
"@rn-primitives/slot": "^1.0.3",
4445
"@rn-primitives/tooltip": "^1.0.3",
4546
"@rn-primitives/types": "^1.0.3",
+6-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { SafeAreaView } from "react-native-safe-area-context";
22

3+
import { DaySlider } from "~/components/home/day-slider";
34
import HomeHeader from "~/components/home/home-header";
5+
import { Separator } from "~/components/ui/separator";
46

57
export default function HomeScreen() {
68
return (
7-
<SafeAreaView>
9+
<SafeAreaView style={{ flex: 1 }}>
810
<HomeHeader />
11+
<DaySlider />
12+
<Separator className="mb-4" />
13+
<Separator className="mx-auto mb-4 bg-red-500" orientation="vertical" />
914
</SafeAreaView>
1015
);
1116
}

apps/expo/src/components/basic-calendar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function BasicCalendar(props: BasicCalendarProps) {
3838
>
3939
<ChevronLeft className="text-foreground" size={48} />
4040
</Button>
41-
<Text>{calendarRowMonth.split(" ")[0]?.toUpperCase()}</Text>
41+
<Text>{calendarRowMonth.toUpperCase()}</Text>
4242
<Button
4343
onPress={props.onNextMonthPress}
4444
size={"icon"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { useCallback, useRef } from "react";
2+
import { Dimensions, Pressable, View } from "react-native";
3+
import { FlashList } from "@shopify/flash-list";
4+
import { format } from "date-fns";
5+
6+
import { Text } from "~/components/ui/text";
7+
import { cn } from "~/lib/utils";
8+
import { useDateStore } from "~/stores/dateStore";
9+
10+
const mockScoreData = [
11+
{ date: new Date(2024, 6, 29), score: 75 },
12+
{ date: new Date(2024, 6, 30), score: 82 },
13+
{ date: new Date(2024, 6, 31), score: 58 },
14+
{ date: new Date(2024, 7, 1), score: 93 },
15+
{ date: new Date(2024, 7, 2), score: 69 },
16+
{ date: new Date(2024, 7, 3), score: 87 },
17+
{ date: new Date(2024, 7, 4), score: 45 },
18+
{ date: new Date(2024, 7, 5), score: 91 },
19+
{ date: new Date(2024, 7, 6), score: 63 },
20+
{ date: new Date(2024, 7, 7), score: 79 },
21+
];
22+
23+
export type ScoreData = (typeof mockScoreData)[number];
24+
25+
const screenWidth = Dimensions.get("window").width;
26+
const itemWidth = 48; // 12 (width) + 4 (separator) * 3
27+
const centerOffset = (screenWidth - itemWidth) / 2;
28+
const lastIndex = mockScoreData.length - 1;
29+
30+
const DayItem = React.memo(
31+
({
32+
item,
33+
isSelected,
34+
onPress,
35+
}: {
36+
item: ScoreData;
37+
isSelected: boolean;
38+
onPress: () => void;
39+
}) => (
40+
<Pressable onPress={onPress} className="h-20 w-16">
41+
<View className="flex-col items-center gap-1">
42+
<View
43+
className={cn(
44+
"h-6 w-6 items-center justify-center rounded-full",
45+
isSelected && "bg-primary",
46+
)}
47+
>
48+
<Text
49+
className={cn(
50+
"text-xs font-semibold text-gray-400",
51+
isSelected ? "text-black" : "text-gray-400",
52+
)}
53+
>
54+
{format(item.date, "EEEEE").toUpperCase()}
55+
</Text>
56+
</View>
57+
58+
<View className="h-12 w-12 items-center justify-center rounded-full bg-green-900">
59+
<Text className="text-xl font-semibold">{item.score}</Text>
60+
</View>
61+
</View>
62+
</Pressable>
63+
),
64+
);
65+
66+
export function DaySlider() {
67+
const { selectedDate, setSelectedDate } = useDateStore();
68+
const listRef = useRef<FlashList<ScoreData> | null>(null);
69+
70+
const scrollToIndex = (index: number) => {
71+
listRef.current?.scrollToIndex({
72+
index,
73+
animated: true,
74+
viewPosition: 0.5, // This centers the item
75+
});
76+
};
77+
78+
const renderItem = useCallback(
79+
({ item, index }: { item: ScoreData; index: number }) => {
80+
const isSelected =
81+
item.date.toDateString() === selectedDate.toDateString();
82+
return (
83+
<DayItem
84+
item={item}
85+
isSelected={isSelected}
86+
onPress={() => {
87+
setSelectedDate(item.date);
88+
scrollToIndex(index);
89+
}}
90+
/>
91+
);
92+
},
93+
[selectedDate, setSelectedDate],
94+
);
95+
96+
const keyExtractor = useCallback(
97+
(item: ScoreData) => item.date.toISOString(),
98+
[],
99+
);
100+
101+
return (
102+
<FlashList
103+
ref={listRef}
104+
data={mockScoreData}
105+
extraData={selectedDate}
106+
renderItem={renderItem}
107+
keyExtractor={keyExtractor}
108+
estimatedItemSize={48}
109+
horizontal
110+
showsHorizontalScrollIndicator={false}
111+
initialScrollIndex={lastIndex}
112+
// ItemSeparatorComponent={() => <View className="w-4" />}
113+
contentContainerStyle={{
114+
paddingBottom: 4,
115+
paddingTop: 4,
116+
paddingLeft: 8,
117+
paddingRight: centerOffset,
118+
}}
119+
/>
120+
);
121+
}

apps/expo/src/components/home/home-header.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { View } from "react-native";
2+
import { format, isToday } from "date-fns";
23

34
import { HomeCalendar } from "~/components/home/home-calendar";
45
import { Button } from "~/components/ui/button";
@@ -11,15 +12,22 @@ import {
1112
import { Text } from "~/components/ui/text";
1213
import { Bell } from "~/lib/icons/bell";
1314
import { Calendar } from "~/lib/icons/calendar";
15+
import { useDateStore } from "~/stores/dateStore";
1416

1517
export default function HomeHeader() {
18+
const { selectedDate } = useDateStore();
19+
20+
const formattedDate = isToday(selectedDate)
21+
? "Today"
22+
: format(selectedDate, "MMMM do, yyyy");
23+
1624
return (
1725
<View className="flex-row items-center justify-between px-2">
1826
<Dialog>
1927
<DialogTrigger asChild>
2028
<Button variant="ghost" className="flex-row items-center gap-2">
2129
<Calendar className="text-foreground" size={24} />
22-
<Text>TODAY</Text>
30+
<Text>{formattedDate}</Text>
2331
</Button>
2432
</DialogTrigger>
2533
<DialogContent
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as React from 'react';
2+
import * as SeparatorPrimitive from '@rn-primitives/separator';
3+
import { cn } from '~/lib/utils';
4+
5+
const Separator = React.forwardRef<
6+
React.ElementRef<typeof SeparatorPrimitive.Root>,
7+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
8+
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
9+
<SeparatorPrimitive.Root
10+
ref={ref}
11+
decorative={decorative}
12+
orientation={orientation}
13+
className={cn(
14+
'shrink-0 bg-border',
15+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
16+
className
17+
)}
18+
{...props}
19+
/>
20+
));
21+
Separator.displayName = SeparatorPrimitive.Root.displayName;
22+
23+
export { Separator };
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as React from 'react';
2+
import Animated, {
3+
useAnimatedStyle,
4+
useSharedValue,
5+
withRepeat,
6+
withSequence,
7+
withTiming,
8+
} from 'react-native-reanimated';
9+
import { cn } from '~/lib/utils';
10+
11+
const duration = 1000;
12+
13+
function Skeleton({
14+
className,
15+
...props
16+
}: Omit<React.ComponentPropsWithoutRef<typeof Animated.View>, 'style'>) {
17+
const sv = useSharedValue(1);
18+
19+
React.useEffect(() => {
20+
sv.value = withRepeat(
21+
withSequence(withTiming(0.5, { duration }), withTiming(1, { duration })),
22+
-1
23+
);
24+
}, []);
25+
26+
const style = useAnimatedStyle(() => ({
27+
opacity: sv.value,
28+
}));
29+
30+
return (
31+
<Animated.View
32+
style={style}
33+
className={cn('rounded-md bg-secondary dark:bg-muted', className)}
34+
{...props}
35+
/>
36+
);
37+
}
38+
39+
export { Skeleton };
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from 'react';
2+
import { TextInput } from 'react-native';
3+
import { cn } from '~/lib/utils';
4+
5+
const Textarea = React.forwardRef<
6+
React.ElementRef<typeof TextInput>,
7+
React.ComponentPropsWithoutRef<typeof TextInput>
8+
>(({ className, multiline = true, numberOfLines = 4, placeholderClassName, ...props }, ref) => {
9+
return (
10+
<TextInput
11+
ref={ref}
12+
className={cn(
13+
'web:flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base lg:text-sm native:text-lg native:leading-[1.25] text-foreground web:ring-offset-background placeholder:text-muted-foreground web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2',
14+
props.editable === false && 'opacity-50 web:cursor-not-allowed',
15+
className
16+
)}
17+
placeholderClassName={cn('text-muted-foreground', placeholderClassName)}
18+
multiline={multiline}
19+
numberOfLines={numberOfLines}
20+
textAlignVertical='top'
21+
{...props}
22+
/>
23+
);
24+
});
25+
26+
Textarea.displayName = 'Textarea';
27+
28+
export { Textarea };

0 commit comments

Comments
 (0)