Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
}}
/>
<View style={{ position: "absolute", top: -5, left: 10 }}>
<BookmarkButton occurrenceId={occurrence.id} />
<BookmarkButton eventId={occurrence.event.id} />
</View>
<Text
style={{
Expand Down
222 changes: 98 additions & 124 deletions frontend/apps/mobile/app/(app)/(tabs)/index.tsx

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions frontend/apps/mobile/app/(app)/(tabs)/saved.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { SavedEventCard } from '@/components/SavedEventCard';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { getGetSavedByGuardianIdQueryKey, Saved, useDeleteSaved, useGetSavedByGuardianId } from '@skillspark/api-client';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'expo-router';
import React from 'react';
import { ActivityIndicator, Alert, FlatList, TouchableOpacity, useColorScheme, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAuthContext } from '@/hooks/use-auth-context';
import { ErrorScreen } from '@/components/ErrorScreen';

export default function SavedScreen() {
const insets = useSafeAreaInsets();
const colorScheme = useColorScheme();
const router = useRouter();
const theme = Colors[colorScheme ?? 'light'];
const queryClient = useQueryClient();
const { guardianId } = useAuthContext();

const { data: response, isLoading, error } = useGetSavedByGuardianId(guardianId!, undefined, {
query: { enabled: !!guardianId },
});
const deleteSavedMutation = useDeleteSaved();

if (!guardianId) {
return <ErrorScreen message="Illegal state: no guardian ID retrieved" />;
}

if (isLoading) {
return (
<View className="flex-1 items-center justify-center gap-2">
<ActivityIndicator size="large" />
<ThemedText>Loading events...</ThemedText>
</View>
);
}

if (error) {
return (
<View className="flex-1 items-center justify-center p-4">
<ThemedText className="text-red-500 font-semibold">Error loading events</ThemedText>
<ThemedText>{error.detail || "An error occurred"}</ThemedText>
</View>
);
}

if (!response || !Array.isArray(response.data)) {
return (
<View className="flex-1 items-center justify-center p-4">
<ThemedText>No events available</ThemedText>
</View>
);
}

const savedEvents: Saved[] = response.status === 200 && Array.isArray(response.data)
? response.data
: [];

const handleDeleteSaved = (savedId: string) => {
Alert.alert(
'Delete saved event',
'Are you sure you want remove this saved event?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete', style: 'destructive',
onPress: async () => {
deleteSavedMutation.mutate(
{ id: savedId },
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: getGetSavedByGuardianIdQueryKey(guardianId),
});
},
onError: (err) => console.error('Failed to delete saved event', err),
}
);
},
},
]
);
};

return (
<ThemedView className="flex-1" style={{ paddingTop: insets.top }}>
<View className="flex-row items-center justify-between px-5 py-[14px]">
<TouchableOpacity
onPress={() => router.navigate('/profile')}
className="w-10 justify-center items-start"
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<IconSymbol name="chevron.left" size={24} color={theme.text} />
</TouchableOpacity>
<ThemedText className="text-xl text-center font-nunito-bold">Saved</ThemedText>
<View className="w-10" />
</View>
<ThemedView className="flex-1">
{savedEvents.length === 0 ? (
<View className="flex-1 justify-center items-center p-5">
<ThemedText className="text-center text-lg text-gray-500">
You have no saved events.
</ThemedText>
</View>
) : (
<FlatList
data={savedEvents}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<SavedEventCard
event={item.event}
onBookmarkPress={() => handleDeleteSaved(item.id)}
/>
)}
contentContainerStyle={{ paddingTop: 10, paddingBottom: 20 }}
showsVerticalScrollIndicator={false}
/>
)}
</ThemedView>
</ThemedView>
);
}
122 changes: 62 additions & 60 deletions frontend/apps/mobile/app/(auth)/signup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { router } from "expo-router";
import { useState } from "react";
import { View } from "react-native";
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
import { useAuthContext } from "@/hooks/use-auth-context";
import { Controller, useForm } from "react-hook-form";
import { ErrorMessage } from "@/components/ErrorMessage";
Expand All @@ -23,7 +23,6 @@ type SignupFormData = {
export default function SignupScreen() {
const [errorText, setErrorText] = useState("");
const { signup } = useAuthContext();

const { control, handleSubmit } = useForm<SignupFormData>({
defaultValues: {
name: "",
Expand Down Expand Up @@ -62,65 +61,68 @@ export default function SignupScreen() {
};

return (
<ThemedView
className="flex-1 items-center justify-center"
>
<ThemedText
type="title"
className="text-3xl font-bold mb-8"
>
Sign Up
</ThemedText>
<View
className="w-full px-6 gap-4 items-center"
<ThemedView className="flex-1">
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1"
>
<AuthFormInput
control={control}
name="name"
placeholder="Full Name"
autoCapitalize="none"
/>
<AuthFormInput
control={control}
name="email"
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
/>
<AuthFormInput
control={control}
name="username"
placeholder="Username"
autoCapitalize="none"
/>
<AuthFormInput
control={control}
name="password"
placeholder="Password"
secureTextEntry={true}
/>
<Controller
control={control}
name="language_preference"
render={({ field: { onChange, value } }) => (
<Dropdown
value={value}
onChange={onChange}
options={[
{ label: 'English', value: 'en' },
{ label: 'Thai', value: 'th' },
]}
placeholder="Select a language..."
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
>
<View className="flex-1 items-center justify-center px-6 gap-4">
<ThemedText type="title" className="text-3xl font-bold mb-8">
Sign Up
</ThemedText>
<AuthFormInput
control={control}
name="name"
placeholder="Full Name"
autoCapitalize="none"
/>
<AuthFormInput
control={control}
name="email"
placeholder="Email"
keyboardType="email-address"
autoCapitalize="none"
/>
<AuthFormInput
control={control}
name="username"
placeholder="Username"
autoCapitalize="none"
/>
<AuthFormInput
control={control}
name="password"
placeholder="Password"
secureTextEntry={true}
/>
<Controller
control={control}
name="language_preference"
render={({ field: { onChange, value } }) => (
<Dropdown
value={value}
onChange={onChange}
options={[
{ label: "English", value: "en" },
{ label: "Thai", value: "th" },
]}
placeholder="Select a language..."
/>
)}
/>
<Button label="Sign Up" onPress={handleSubmit(onSubmit)} />
<PageRedirectButton
label="Already have an account? Log in"
onPress={handleGoToLogIn}
/>
)}
/>
<Button label="Sign Up" onPress={handleSubmit(onSubmit)} />
<PageRedirectButton
label="Already have an account? Log in"
onPress={handleGoToLogIn}
/>
<ErrorMessage message={errorText} />
</View>
<ErrorMessage message={errorText} />
</View>
</ScrollView>
</KeyboardAvoidingView>
</ThemedView>
);
}
}
Loading
Loading