Skip to content

Commit c57cbc9

Browse files
feat: habit info page
1 parent d5bbc80 commit c57cbc9

File tree

6 files changed

+190
-12
lines changed

6 files changed

+190
-12
lines changed

src/api/habits/mock-habits.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ export const mockHabits: { id: HabitIdT; data: DbHabitT }[] = [
8888
displayName: 'John Doe',
8989
username: 'john_doe',
9090
lastActivity: new Date('2024-12-01T00:00:00'),
91+
},
92+
['6' as UserIdT]: {
93+
displayName: 'Sarah Wilson',
94+
username: 'sarah_wilson',
95+
lastActivity: new Date('2024-12-15T00:00:00'),
9196
isOwner: true,
9297
},
9398
},
@@ -161,6 +166,13 @@ export const mockHabitCompletions: Record<HabitIdT, AllCompletionsT> = {
161166
'2024-12-06': { numberOfCompletions: 1 },
162167
},
163168
},
169+
['6' as UserIdT]: {
170+
entries: {
171+
'2024-12-13': { numberOfCompletions: 1 },
172+
'2024-12-14': { numberOfCompletions: 1 },
173+
'2024-12-15': { numberOfCompletions: 1, note: 'Great book tonight!' },
174+
},
175+
},
164176
},
165177
};
166178
export const setMockHabitCompletions = (

src/api/users/mock-users.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export const mockUsers: UserT[] = [
3131
username: 'lorem_ipsum',
3232
createdAt: new Date(),
3333
},
34+
{
35+
id: '6' as UserIdT,
36+
displayName: 'Sarah Wilson',
37+
username: 'sarah_wilson',
38+
createdAt: new Date(),
39+
},
3440
];
3541

3642
export const mockPictures: Record<UserIdT, string> = {
@@ -39,6 +45,7 @@ export const mockPictures: Record<UserIdT, string> = {
3945
['3' as UserIdT]: 'https://randomuser.me/api/portraits/women/4.jpg',
4046
['4' as UserIdT]: 'https://randomuser.me/api/portraits/men/5.jpg',
4147
['5' as UserIdT]: 'https://randomuser.me/api/portraits/men/6.jpg',
48+
['6' as UserIdT]: 'https://randomuser.me/api/portraits/women/7.jpg',
4249
};
4350

4451
export const mockRelationships: Record<

src/app/habits/view-habit.tsx

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ import {
1616
type HabitT,
1717
type ParticipantWithIdT,
1818
useAllUsersHabitCompletions,
19+
type UserIdT,
1920
type UserT,
2021
} from '@/api';
2122
import { ErrorMessage } from '@/components/error-message';
2223
import { HabitIcon, type habitIcons } from '@/components/habit-icon';
2324
import ModifyHabitEntry from '@/components/modify-habit-entry';
2425
import { DayNavigation } from '@/components/modify-habit-entry/day-navigation';
25-
import { UserImageNameAndUsername } from '@/components/user-card';
26+
import UserCard, { UserImageNameAndUsername } from '@/components/user-card';
2627
import { getTranslucentColor } from '@/core/get-translucent-color';
2728
import {
2829
Button,
@@ -180,6 +181,21 @@ interface HabitHeaderProps {
180181
const HabitHeader = ({ habit }: HabitHeaderProps) => {
181182
const { colorScheme } = useColorScheme();
182183
const modal = useModal();
184+
const participants = Object.entries(habit.participants)
185+
.filter(
186+
(entry): entry is [string, NonNullable<(typeof entry)[1]>] =>
187+
entry[1] !== undefined,
188+
)
189+
.map(([_, p]) => p);
190+
const isOwner = habit.participants['1' as UserIdT]?.isOwner;
191+
192+
const handleTransferOwnership = (userId: string) => {
193+
alert('TODO: Transfer ownership to ' + userId);
194+
};
195+
196+
const handleKickUser = (userId: string) => {
197+
alert('TODO: Kick user ' + userId);
198+
};
183199

184200
return (
185201
<>
@@ -214,23 +230,101 @@ const HabitHeader = ({ habit }: HabitHeaderProps) => {
214230
colorScheme === 'dark' ? colors.neutral[800] : colors.white,
215231
}}
216232
>
217-
<View className="flex-1 px-4">
218-
<Header title={habit.title} />
219-
<View className="flex flex-col gap-4">
233+
<ScrollView className="flex-1 px-4">
234+
<View className="flex flex-col gap-6">
235+
<View
236+
className="flex flex-col gap-2 rounded-xl p-4"
237+
style={{
238+
backgroundColor:
239+
colorScheme === 'dark'
240+
? getTranslucentColor(habit.color.base, 0.05)
241+
: habit.color.light,
242+
borderColor:
243+
colorScheme === 'dark' ? habit.color.base : 'transparent',
244+
borderWidth: colorScheme === 'dark' ? 1 : 0,
245+
}}
246+
>
247+
<View className="flex flex-row items-center gap-2">
248+
<HabitIcon
249+
icon={habit.icon}
250+
size={24}
251+
color={colorScheme === 'dark' ? colors.white : colors.black}
252+
strokeWidth={2}
253+
/>
254+
<Text className="text-xl font-semibold">{habit.title}</Text>
255+
</View>
256+
{habit.description && (
257+
<Text className="text-base">{habit.description}</Text>
258+
)}
259+
<Text
260+
className="text-sm"
261+
style={{
262+
color:
263+
colorScheme === 'dark'
264+
? colors.stone[400]
265+
: habit.color.text,
266+
}}
267+
>
268+
{habit.settings.allowMultipleCompletions
269+
? 'Multiple completions allowed per day'
270+
: 'One completion per day limit'}
271+
</Text>
272+
<Text
273+
className="text-sm"
274+
style={{
275+
color:
276+
colorScheme === 'dark'
277+
? colors.stone[400]
278+
: habit.color.text,
279+
}}
280+
>
281+
Created{' '}
282+
{new Date(habit.createdAt).toLocaleDateString(undefined, {
283+
year: 'numeric',
284+
month: 'long',
285+
day: 'numeric',
286+
})}
287+
</Text>
288+
</View>
289+
220290
<Button
221291
label="Invite Friends"
222-
variant="outline"
223292
icon={UserPlusIcon}
224293
onPress={() => alert('todo')}
225294
/>
295+
296+
{participants.length >= 2 && (
297+
<View className="flex flex-col">
298+
<Text className="font-medium">
299+
{participants.length} participants:
300+
</Text>
301+
{participants.map((p) => (
302+
<UserCard
303+
key={p.id}
304+
data={{
305+
id: p.id,
306+
displayName: p.displayName,
307+
username: p.username,
308+
createdAt: p.lastActivity,
309+
}}
310+
showOwnerBadge={p.isOwner}
311+
showManageOptions={isOwner && p.id !== '1'}
312+
onTransferOwnership={() => handleTransferOwnership(p.id)}
313+
onKickUser={() => handleKickUser(p.id)}
314+
onPress={modal.dismiss}
315+
/>
316+
))}
317+
</View>
318+
)}
319+
226320
<Button
227321
label="Leave Habit"
228322
variant="destructive"
229323
icon={Trash2Icon}
230324
onPress={() => alert('todo')}
231325
/>
232326
</View>
233-
</View>
327+
</ScrollView>
234328
</Modal>
235329
</>
236330
);
@@ -328,7 +422,7 @@ const UserWithEntry = ({
328422
{modifyEntryButton ||
329423
// for other users, show nudge button if no completions and it's today
330424
(entry.numberOfCompletions === 0 && isToday && (
331-
<View className="flex flex-row">
425+
<View className="flex flex-row justify-end">
332426
<Button
333427
variant="outline"
334428
size="sm"

src/components/modify-habit-entry/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function ModifyHabitEntry({
2424

2525
return (
2626
<>
27-
<View className="flex flex-row">
27+
<View className="flex flex-row justify-end">
2828
{showAsNormalButton ? (
2929
<Button
3030
variant="outline"

src/components/user-card.tsx

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,38 @@
11
import { Link } from 'expo-router';
2+
import { EllipsisIcon } from 'lucide-react-native';
3+
import { useColorScheme } from 'nativewind';
24

35
import { type UserT } from '@/api';
4-
import { Pressable, Text, View } from '@/ui';
6+
import { colors, Pressable, Text, View } from '@/ui';
7+
import {
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuItemTitle,
11+
DropdownMenuRoot,
12+
DropdownMenuTrigger,
13+
} from '@/ui/dropdown-menu';
514

615
import UserPicture from './picture';
716

8-
export default function UserCard({ data }: { data: UserT }) {
17+
interface UserCardProps {
18+
data: UserT;
19+
showOwnerBadge?: boolean;
20+
showManageOptions?: boolean;
21+
onTransferOwnership?: () => void;
22+
onKickUser?: () => void;
23+
onPress?: () => void;
24+
}
25+
26+
export default function UserCard({
27+
data,
28+
showOwnerBadge,
29+
showManageOptions,
30+
onTransferOwnership,
31+
onKickUser,
32+
onPress,
33+
}: UserCardProps) {
34+
const { colorScheme } = useColorScheme();
35+
936
return (
1037
<Link
1138
push
@@ -18,8 +45,46 @@ export default function UserCard({ data }: { data: UserT }) {
1845
}}
1946
asChild
2047
>
21-
<Pressable className="my-1 rounded-3xl border border-stone-200 bg-white px-4 py-[10px] dark:border-stone-700 dark:bg-transparent">
48+
<Pressable
49+
className="my-1 flex-row items-center justify-between rounded-3xl border border-stone-200 bg-white px-4 py-[10px] dark:border-stone-700 dark:bg-transparent"
50+
onPress={() => onPress?.()}
51+
>
2252
<UserImageNameAndUsername data={data} />
53+
{showOwnerBadge && (
54+
<Text className="text-xs text-stone-400 dark:text-stone-500">
55+
Admin
56+
</Text>
57+
)}
58+
{showManageOptions && (
59+
<DropdownMenuRoot>
60+
<DropdownMenuTrigger>
61+
<Pressable>
62+
<EllipsisIcon
63+
size={24}
64+
color={colorScheme === 'dark' ? colors.white : colors.black}
65+
/>
66+
</Pressable>
67+
</DropdownMenuTrigger>
68+
<DropdownMenuContent>
69+
<DropdownMenuItem
70+
key={'transfer'}
71+
onSelect={onTransferOwnership}
72+
destructive
73+
>
74+
<DropdownMenuItemTitle>
75+
Transfer ownership
76+
</DropdownMenuItemTitle>
77+
</DropdownMenuItem>
78+
<DropdownMenuItem
79+
key={'remove'}
80+
onSelect={onKickUser}
81+
destructive
82+
>
83+
<DropdownMenuItemTitle>Remove from habit</DropdownMenuItemTitle>
84+
</DropdownMenuItem>
85+
</DropdownMenuContent>
86+
</DropdownMenuRoot>
87+
)}
2388
</Pressable>
2489
</Link>
2590
);

src/ui/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const button = tv({
3434
},
3535
destructive: {
3636
container:
37-
'border border-red-200 bg-white dark:border-red-500 dark:bg-transparent',
37+
'border border-red-200 bg-white dark:border-red-500 dark:bg-red-500/5',
3838
label: 'font-medium text-red-500',
3939
indicator: 'text-red-500',
4040
},

0 commit comments

Comments
 (0)