Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
up-backend up-frontend logs-backend logs-frontend \
build-backend build-frontend shell-backend shell-frontend setup-hooks generate-api \
clean-node-modules \
up-localstack up-backend-localstack logs-localstack
format-frontend \
up-localstack up-backend-localstack logs-localstack

# Default target - show help
.DEFAULT_GOAL := help
Expand Down Expand Up @@ -52,6 +53,7 @@ help:
@echo " make logs-frontend - View frontend logs"
@echo " make build-backend - Build only backend"
@echo " make build-frontend - Build only frontend"
@echo " make format-frontend - Format all frontend files with Prettier"
@echo ""
@echo "$(BLUE)Container Management:$(NC)"
@echo " make stop - Stop all services (without removing)"
Expand Down Expand Up @@ -187,6 +189,11 @@ restart-frontend:
@$(DOCKER_COMPOSE) restart frontend
@echo "$(GREEN)Frontend restarted$(NC)"

format-frontend:
@echo "$(BOLD)Formatting all frontend files with Prettier...$(NC)"
@cd frontend && bun x prettier --write "**/*.{ts,tsx,js,jsx,json,css,scss,md}"
@echo "$(GREEN)Frontend formatting complete$(NC)"

# ------------------------
# Cleanup Commands
# ------------------------
Expand Down
3 changes: 2 additions & 1 deletion frontend/apps/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ To use the Map features, you must configure Google Maps API keys for iOS and And
2. Edit `.env` and fill in the required values:

## Map Integration & Managing Locations

The map view (app/(tabs)/map.tsx) retrieves event occurrences dynamically from the backend API. It renders location pins for all events that have valid geographic coordinates.

To populate the map:

1. Ensure the backend API is running and accessible.

2. Create event occurrences with associated locations containing valid latitude and longitude.
2. Create event occurrences with associated locations containing valid latitude and longitude.
10 changes: 6 additions & 4 deletions frontend/apps/mobile/app/(app)/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function TabLayout() {
<Tabs.Screen
name="index"
options={{
title: translate('nav.home'),
title: translate("nav.home"),
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
),
Expand All @@ -31,7 +31,7 @@ export default function TabLayout() {
<Tabs.Screen
name="map"
options={{
title: translate('nav.map'),
title: translate("nav.map"),
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="map.fill" color={color} />
),
Expand All @@ -40,8 +40,10 @@ export default function TabLayout() {
<Tabs.Screen
name="profile"
options={{
title: translate('nav.profile'),
tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />,
title: translate("nav.profile"),
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="person.fill" color={color} />
),
}}
/>
<Tabs.Screen name="event" options={{ href: null }} />
Expand Down
167 changes: 134 additions & 33 deletions frontend/apps/mobile/app/(app)/(tabs)/event/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ function formatAddress(occurrence: EventOccurrence) {
return parts.join(", ") || "Location";
}

function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) {
function EventOccurrenceDetail({
occurrence,
}: {
occurrence: EventOccurrence;
}) {
const router = useRouter();
const insets = useSafeAreaInsets();
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
const [descriptionTruncated, setDescriptionTruncated] = useState(false);
const { t: translate } = useTranslation();
const duration = formatDuration(occurrence.start_time, occurrence.end_time, {
hr: translate('event.hr'),
min: translate('event.min'),
hr: translate("event.hr"),
min: translate("event.min"),
});
const address = formatAddress(occurrence);

Expand Down Expand Up @@ -66,20 +70,37 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
<View className="flex-1 bg-[#C5C5C5]" />
)}
<TouchableOpacity
onPress={() => router.navigate('/')}
onPress={() => router.navigate("/")}
activeOpacity={0.7}
className="absolute top-4 left-4 z-10 flex-row items-center bg-white rounded-full px-4 py-2.5 elevation-10"
style={{ shadowColor: "#000", shadowOpacity: 0.15, shadowRadius: 8 }}
style={{
shadowColor: "#000",
shadowOpacity: 0.15,
shadowRadius: 8,
}}
>
<MaterialIcons name="chevron-left" size={20} color={AppColors.primaryText} />
<Text className="text-[15px] font-medium" style={{ color: AppColors.primaryText }}>{translate('event.back')}</Text>
<MaterialIcons
name="chevron-left"
size={20}
color={AppColors.primaryText}
/>
<Text
className="text-[15px] font-medium"
style={{ color: AppColors.primaryText }}
>
{translate("event.back")}
</Text>
</TouchableOpacity>
</View>

{/* Content card */}
<View
className="bg-white rounded-t-[28px] -mt-7 px-[22px] pb-6 elevation-2"
style={{ shadowColor: "#000", shadowOpacity: 0.06, shadowRadius: 12 }}
style={{
shadowColor: "#000",
shadowOpacity: 0.06,
shadowRadius: 12,
}}
>
{/* Drag handle */}
<View
Expand All @@ -96,7 +117,11 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
{occurrence.event.title}
</Text>
<View className="flex-row items-center gap-1.5 mb-3.5">
<MaterialIcons name="location-on" size={16} color={AppColors.primaryText} />
<MaterialIcons
name="location-on"
size={16}
color={AppColors.primaryText}
/>
<Text
className="text-[13px] flex-1"
style={{ color: AppColors.secondaryText }}
Expand All @@ -119,9 +144,17 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
{occurrence.event.description}
</Text>
{descriptionTruncated && (
<Pressable onPress={() => setDescriptionExpanded((prev) => !prev)} className="mb-3.5">
<Text className="text-[13px] font-semibold" style={{ color: AppColors.primaryText }}>
{descriptionExpanded ? translate('event.seeLess') : translate('event.seeMore')}
<Pressable
onPress={() => setDescriptionExpanded((prev) => !prev)}
className="mb-3.5"
>
<Text
className="text-[13px] font-semibold"
style={{ color: AppColors.primaryText }}
>
{descriptionExpanded
? translate("event.seeLess")
: translate("event.seeMore")}
</Text>
</Pressable>
)}
Expand All @@ -133,13 +166,28 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
className="border-[1.5px] rounded-full px-4 py-[7px]"
style={{ borderColor: AppColors.borderLight }}
>
<Text className="text-[13px]" style={{ color: AppColors.secondaryText }}>{translate(`interests.${cat}`, { defaultValue: cat })}</Text>
<Text
className="text-[13px]"
style={{ color: AppColors.secondaryText }}
>
{translate(`interests.${cat}`, { defaultValue: cat })}
</Text>
</View>
))}
</View>
<View className="items-end ml-3.5">
<Text className="text-xl font-bold" style={{ color: AppColors.primaryText }}>{occurrence.price} THB</Text>
<Text className="text-xs" style={{ color: AppColors.subtleText }}>{translate('event.perSession')}</Text>
<Text
className="text-xl font-bold"
style={{ color: AppColors.primaryText }}
>
{occurrence.price} THB
</Text>
<Text
className="text-xs"
style={{ color: AppColors.subtleText }}
>
{translate("event.perSession")}
</Text>
</View>
</View>
</View>
Expand All @@ -153,7 +201,11 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
{/* Bottom section */}
<View
className="flex-1 bg-white px-[22px] pt-[22px] pb-7 elevation-2"
style={{ shadowColor: "#000", shadowOpacity: 0.06, shadowRadius: 12 }}
style={{
shadowColor: "#000",
shadowOpacity: 0.06,
shadowRadius: 12,
}}
>
<View className="flex-row items-center justify-between mb-[22px]">
<Text
Expand All @@ -162,13 +214,28 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
>
{duration}
</Text>
<View
className="flex-row items-center gap-[5px] bg-[#F3F4F6] rounded-full px-3 py-[7px]"
>
<MaterialIcons name="directions-walk" size={14} color={AppColors.secondaryText} />
<Text className="text-[13px]" style={{ color: AppColors.secondaryText }}>8 {translate('event.minWalk')}</Text>
<MaterialIcons name="arrow-forward" size={12} color={AppColors.subtleText} />
<MaterialIcons name="directions-bus" size={16} color={AppColors.secondaryText} />
<View className="flex-row items-center gap-[5px] bg-[#F3F4F6] rounded-full px-3 py-[7px]">
<MaterialIcons
name="directions-walk"
size={14}
color={AppColors.secondaryText}
/>
<Text
className="text-[13px]"
style={{ color: AppColors.secondaryText }}
>
8 {translate("event.minWalk")}
</Text>
<MaterialIcons
name="arrow-forward"
size={12}
color={AppColors.subtleText}
/>
<MaterialIcons
name="directions-bus"
size={16}
color={AppColors.secondaryText}
/>
</View>
</View>
<View className="flex-row items-center justify-between">
Expand All @@ -180,16 +247,45 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
>
<View className="w-1.5 h-1.5 rounded-full bg-white" />
</View>
<Text className="text-sm font-medium" style={{ color: AppColors.secondaryText }}>{translate('event.home')}</Text>
<Text
className="text-sm font-medium"
style={{ color: AppColors.secondaryText }}
>
{translate("event.home")}
</Text>
</View>
<View className="pl-1.5 py-0.5">
<Text className="text-sm leading-[10px]" style={{ color: AppColors.subtleText }}>•</Text>
<Text className="text-sm leading-[10px]" style={{ color: AppColors.subtleText }}>•</Text>
<Text className="text-sm leading-[10px]" style={{ color: AppColors.subtleText }}>•</Text>
<Text
className="text-sm leading-[10px]"
style={{ color: AppColors.subtleText }}
>
</Text>
<Text
className="text-sm leading-[10px]"
style={{ color: AppColors.subtleText }}
>
</Text>
<Text
className="text-sm leading-[10px]"
style={{ color: AppColors.subtleText }}
>
</Text>
</View>
<View className="flex-row items-center gap-2.5">
<MaterialIcons name="location-on" size={16} color={AppColors.secondaryText} />
<Text className="text-sm font-medium" style={{ color: AppColors.secondaryText }}>{translate('event.location')}</Text>
<MaterialIcons
name="location-on"
size={16}
color={AppColors.secondaryText}
/>
<Text
className="text-sm font-medium"
style={{ color: AppColors.secondaryText }}
>
{translate("event.location")}
</Text>
</View>
</View>
<TouchableOpacity
Expand All @@ -198,7 +294,9 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
className="rounded-2xl px-[26px] py-3.5"
style={{ backgroundColor: AppColors.primaryText }}
>
<Text className="text-white text-[17px] font-bold">{translate('event.register')}</Text>
<Text className="text-white text-[17px] font-bold">
{translate("event.register")}
</Text>
</TouchableOpacity>
</View>
</View>
Expand All @@ -224,12 +322,15 @@ export default function EventOccurrenceScreen() {
if (error || !response || response.status !== 200) {
return (
<View className="flex-1 items-center justify-center p-6">
<Text className="text-base font-semibold" style={{ color: AppColors.danger }}>
{translate('event.notFound')}
<Text
className="text-base font-semibold"
style={{ color: AppColors.danger }}
>
{translate("event.notFound")}
</Text>
</View>
);
}

return <EventOccurrenceDetail occurrence={response.data} />;
}
}
31 changes: 17 additions & 14 deletions frontend/apps/mobile/app/(app)/(tabs)/family/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,34 @@ import { Colors } from "@/constants/theme";

export default function FamilyLayout() {
const colorScheme = useColorScheme();
const theme = Colors[colorScheme ?? 'light'];
const theme = Colors[colorScheme ?? "light"];

return (
<Stack
screenOptions={{
headerShown: false,
headerTintColor: theme.tint,
headerStyle: { backgroundColor: theme.background },
headerTitleStyle: { fontFamily: 'NunitoSans_600SemiBold', color: theme.text },
headerBackTitle: "",
headerTitleStyle: {
fontFamily: "NunitoSans_600SemiBold",
color: theme.text,
},
headerBackTitle: "",
}}
>
<Stack.Screen
name="index"
options={{
headerShown: false
}}
<Stack.Screen
name="index"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="manage"
options={{
<Stack.Screen
name="manage"
options={{
title: "Manage Child",
headerShown: false
}}
headerShown: false,
}}
/>
</Stack>
);
}
}
Loading
Loading