diff --git a/3Design/mobile/app/navigation/StackNavigator.js b/3Design/mobile/app/navigation/StackNavigator.js index e39dc863..151d6fd0 100644 --- a/3Design/mobile/app/navigation/StackNavigator.js +++ b/3Design/mobile/app/navigation/StackNavigator.js @@ -5,6 +5,7 @@ import RegisterScreen from '../screens/RegisterScreen'; import TabNavigator from './TabNavigator'; import PostScreen from "../screens/PostScreen"; import LeaderboardScreen from '../screens/LeaderboardScreen'; +import ProfilePage from '../screens/ProfileScreen'; // Import ProfilePage const Stack = createNativeStackNavigator(); @@ -42,6 +43,12 @@ export default function StackNavigator() { component={LeaderboardScreen} options={{ title: 'Leaderboard' }} /> + {/* Add the ProfilePage route */} + ); } diff --git a/3Design/mobile/app/screens/LeaderboardScreen.js b/3Design/mobile/app/screens/LeaderboardScreen.js index 38a3de1b..631c4da7 100644 --- a/3Design/mobile/app/screens/LeaderboardScreen.js +++ b/3Design/mobile/app/screens/LeaderboardScreen.js @@ -7,11 +7,12 @@ import { ActivityIndicator, Platform, StatusBar, + Image, + TouchableOpacity } from 'react-native'; import axios from 'axios'; import { Colors } from '../constants/Colors'; import { AuthContext } from "../context/AuthContext"; -import { Categories } from "../constants/Categories"; export default function LeaderboardScreen({ route, navigation }) { const { category } = route.params; @@ -41,12 +42,46 @@ export default function LeaderboardScreen({ route, navigation }) { } }, [category]); + const handleViewPost = (item) => { + const postUrl = `${process.env.EXPO_PUBLIC_VITE_API_URL}/api/v1/posts/${item.postId}`; + axios + .get(postUrl, { + headers: { Authorization: `Bearer ${user.accessToken}` }, + }) + .then((response) => { + const postData = response.data; + navigation.navigate('Post', { + postId: item.postId, + username: item.user.nickName, + userId: item.user.id, + title: postData.title, + content: postData.text, + model: postData.fileUrl, + }); + }) + .catch((error) => console.error(error)); + }; + const renderItem = ({ item, index }) => ( {index + 1} - {item.user.nickName} + navigation.navigate('ProfilePage', { userId: item.user.id })} + > + + {item.user.nickName} + {item.score} - {item.postId} + handleViewPost(item)} + > + View Post + ); @@ -127,6 +162,22 @@ const styles = StyleSheet.create({ textAlign: 'center', fontSize: 16, }, + userCell: { + flex: 2, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + profileImage: { + width: 30, + height: 30, + borderRadius: 15, + marginRight: 8, + }, + userName: { + fontSize: 16, + textAlign: 'center', + }, loaderContainer: { flex: 1, justifyContent: 'center', @@ -141,4 +192,16 @@ const styles = StyleSheet.create({ fontSize: 18, fontWeight: '500', }, + viewPostButton: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 8, + }, + viewPostButtonText: { + color: Colors.dark, + fontSize: 16, + fontWeight: '600', + textDecorationLine: 'underline', + }, }); diff --git a/3Design/mobile/app/screens/ProfileScreen.js b/3Design/mobile/app/screens/ProfileScreen.js index df53e3cd..02b33481 100644 --- a/3Design/mobile/app/screens/ProfileScreen.js +++ b/3Design/mobile/app/screens/ProfileScreen.js @@ -7,7 +7,7 @@ import { FlatList, TouchableOpacity, Modal, - Alert, + ActivityIndicator } from 'react-native'; import Post from '../components/Post'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -19,17 +19,24 @@ import { MaterialIcons } from '@expo/vector-icons'; const ProfilePage = () => { const route = useRoute(); + const navigation = useNavigation(); + const { user } = useContext(AuthContext); + + // Use the userId from params if provided; else fallback to currently logged-in user's userId + const userId = route.params?.userId || user.userId; const [isAchievementsVisible, setAchievementsVisible] = useState(false); const [achievements, setAchievements] = useState([]); - - const [userData, setUserData] = useState({}); + const [userData, setUserData] = useState(null); const [latestPosts, setLatestPosts] = useState([]); const [filteredPosts, setFilteredPosts] = useState([]); + const [loadingPosts, setLoadingPosts] = useState(true); + const [loadingUserData, setLoadingUserData] = useState(true); + const [loadingAchievements, setLoadingAchievements] = useState(true); + const [showVisual, setShowVisual] = useState(false); - const { user } = useContext(AuthContext); - const userId = route.params?.userId || user.userId; + const flatListRef = useRef(null); const fetchUserPosts = async () => { try { @@ -41,17 +48,15 @@ const ProfilePage = () => { ); setLatestPosts(response.data); filterPosts(response.data, true); - setLoadingPosts(false); } catch (error) { - // console.error('Error fetching posts:', error); + console.error('Error fetching posts:', error); + } finally { setLoadingPosts(false); - // Alert.alert('Error', 'Failed to fetch posts'); } }; const fetchUserData = async () => { try { - // Fetch the user profile data by username const response = await axios.get( `${process.env.EXPO_PUBLIC_VITE_API_URL}/api/v1/users/${userId}`, { @@ -61,6 +66,8 @@ const ProfilePage = () => { setUserData(response.data); } catch (error) { console.error('Error fetching user data:', error); + } finally { + setLoadingUserData(false); } }; @@ -75,10 +82,13 @@ const ProfilePage = () => { setAchievements(response.data); } catch (error) { console.error('Error fetching achievements:', error); + } finally { + setLoadingAchievements(false); } }; const filterPosts = (allPosts, showVisual) => { + // Currently not filtering by showVisual, just display all setFilteredPosts(allPosts); }; @@ -88,7 +98,6 @@ const ProfilePage = () => { const clearFilteredPosts = () => { setFilteredPosts([]); - console.log('Filtered posts cleared.'); }; useEffect(() => { @@ -97,32 +106,48 @@ const ProfilePage = () => { fetchAchievements(); }, [user, userId]); - const navigation = useNavigation(); - - const flatListRef = useRef(null); - const disableScroll = (isDisabled) => { if (flatListRef.current) { flatListRef.current.setNativeProps({ scrollEnabled: !isDisabled }); } }; + // If we're still loading user data, show a loader + if (loadingUserData || loadingPosts || loadingAchievements) { + return ( + + + + ); + } + + // If userData is null even after loading, show a fallback UI + if (!userData) { + return ( + + Failed to load user profile. + + ); + } + return ( - {/* Display Username at the top of the page */} - {userData.nickName} + {/* User's Nickname (or fallback if unavailable) */} + + {userData?.nickName || 'Unknown User'} + - {/* Profile Picture */} + {/* Profile Picture (with fallback) */} - {/* Tournament Points */} + {/* Tournament Points (with fallback) */} - Tournament Points: {userData.experience} + Tournament Points: {userData?.experience ?? 0} { {/* Latest Posts */} Latest Posts: - + item.postId.toString()} - removeClippedSubviews={false} keyboardShouldPersistTaps='handled' - ListEmptyComponent={This user hasn't posted yet.} + ListEmptyComponent={ + This user hasn't posted yet. + } renderItem={({ item }) => ( { )} /> + { Achievements - {/* Table Header */} Title @@ -189,9 +215,11 @@ const ProfilePage = () => { {item.description} {item.point} - {new Intl.DateTimeFormat('en-GB').format( - new Date(item.earnedAt) - )} + {item.earnedAt + ? new Intl.DateTimeFormat('en-GB').format( + new Date(item.earnedAt) + ) + : ''} )} @@ -216,6 +244,17 @@ const ProfilePage = () => { }; const styles = StyleSheet.create({ + loaderContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + errorText: { + color: Colors.red, + fontSize: 18, + textAlign: 'center', + marginTop: 20, + }, container: { flex: 1, padding: 20, @@ -245,20 +284,10 @@ const styles = StyleSheet.create({ fontWeight: 'bold', marginBottom: 10, }, - itemContainer: { - marginBottom: 10, - padding: 10, - borderWidth: 1, - borderColor: '#ccc', - borderRadius: 8, - }, - itemType: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 4, - }, - itemTitle: { - fontSize: 16, + emptyText: { + textAlign: 'center', + marginTop: 20, + fontStyle: 'italic', }, achievementsButton: { backgroundColor: Colors.dark, @@ -284,17 +313,13 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', // Semi-transparent background + backgroundColor: 'rgba(0, 0, 0, 0.5)', }, centeredModal: { - width: '95%', // Adjust width as needed + width: '95%', backgroundColor: '#fff', borderRadius: 10, padding: 20, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, elevation: 5, }, modalTitle: { @@ -310,7 +335,6 @@ const styles = StyleSheet.create({ paddingVertical: 10, borderRadius: 5, marginBottom: 5, - height: '60', }, headerCell: { fontSize: 15,