diff --git a/expo/app/(tabs)/profile/_layout.tsx b/expo/app/(tabs)/profile/_layout.tsx index cd789bfc..5b225d7e 100644 --- a/expo/app/(tabs)/profile/_layout.tsx +++ b/expo/app/(tabs)/profile/_layout.tsx @@ -1,9 +1,14 @@ import { Stack } from "expo-router"; +import { ProfileHeader } from "../../../components/profile/HeaderProfile"; + export default function ProfileTabLayout() { return ( - + }} + /> ); diff --git a/expo/app/(tabs)/profile/index.tsx b/expo/app/(tabs)/profile/index.tsx index 140a0eb3..5016a158 100644 --- a/expo/app/(tabs)/profile/index.tsx +++ b/expo/app/(tabs)/profile/index.tsx @@ -1,34 +1,17 @@ -import SignOut from "phosphor-react-native/src/icons/SignOut"; import React from "react"; -import { StyleSheet } from "react-native"; +import { ScrollView } from "react-native"; -import Button from "../../../components/Button"; -import { View } from "../../../components/Tamed"; -import { supabase } from "../../../lib/supabase"; -import { useSupabaseUserHook } from "../../../lib/useSupabaseUser"; +import UserRoomHistory from "../../../components/UserRoomHistory"; export default function TabsProfile() { - const user = useSupabaseUserHook(); - return ( - <> - {user && ( - - - - - )} - + + + ); } - -const styles = StyleSheet.create({ - elements: { - gap: 10, - }, -}); diff --git a/expo/components/RoomHistoryInfoCard.tsx b/expo/components/RoomHistoryInfoCard.tsx index 5833ce17..dd85c936 100644 --- a/expo/components/RoomHistoryInfoCard.tsx +++ b/expo/components/RoomHistoryInfoCard.tsx @@ -1,58 +1,34 @@ import { MaterialIcons } from "@expo/vector-icons"; -import { Room } from "commons/database-types-utils"; import { Link } from "expo-router"; -import { useEffect, useState } from "react"; import { StyleSheet } from "react-native"; -import Alert from "./Alert"; import { Text, View } from "./Themed"; -import { getUserProfileFromUserProfileId } from "../lib/userProfile"; interface RoomHistoryInfoCardProps { - room: Room; + createdAt: string; + hostUsername: string; + roomId: string; + roomName: string; } export default function RoomHistoryInfoCard({ - room, + createdAt, + hostUsername, + roomId, + roomName, }: RoomHistoryInfoCardProps) { - const [username, setUsername] = useState(""); - const [formatedDate, setFormatedDate] = useState(""); - - const getUsername = async (userProfileId: string) => { - const user = await getUserProfileFromUserProfileId(userProfileId); - if (!user) { - Alert.alert("Erreur lors de la récupération de l'utilisateur"); - return; - } - return user.username; - }; - useEffect(() => { - (async () => { - const username = await getUsername(room.host_user_profile_id || ""); - if (!username) { - Alert.alert("Erreur lors de la récupération du nom d'utilisateur"); - return; - } - const formattedDate = new Date(room.created_at).toLocaleDateString( - "fr-FR", - { - year: "numeric", - month: "long", - day: "numeric", - } - ); - - setUsername(username); - setFormatedDate(formattedDate); - })(); - }, []); + const formattedDate = new Date(createdAt).toLocaleDateString("fr-FR", { + year: "numeric", + month: "long", + day: "numeric", + }); return ( - + - {room.name} + {roomName} - par {username} le {formatedDate} + par {hostUsername} le {formattedDate} @@ -67,11 +43,11 @@ const styles = StyleSheet.create({ justifyContent: "space-between", alignItems: "center", width: "100%", + marginVertical: 8, }, infos: { flexDirection: "column", - marginVertical: 8, gap: 4, }, diff --git a/expo/components/UserRoomHistory.tsx b/expo/components/UserRoomHistory.tsx index deed20a5..0f77a516 100644 --- a/expo/components/UserRoomHistory.tsx +++ b/expo/components/UserRoomHistory.tsx @@ -1,57 +1,84 @@ -import { Room } from "commons/database-types-utils"; +import { QueryData } from "@supabase/supabase-js/dist/module/lib/types"; import { useEffect, useState } from "react"; import { FlatList, StyleSheet } from "react-native"; +import RoomHistoryInfoCard from "./RoomHistoryInfoCard"; +import { View } from "./Themed"; +import H2 from "./text/H2"; +import Subtitle from "./text/Subtitle"; import { supabase } from "../lib/supabase"; import { useUserProfile } from "../lib/userProfile"; -import Alert from "./Alert"; -import RoomHistoryInfoCard from "./RoomHistoryInfoCard"; -import { Text, View } from "./Themed"; -export default function UserRoomHistory() { - const [rooms, setRooms] = useState([]); +const historyRoom = supabase + .from("room_users") + .select("rooms!inner(created_at, name, id, host:user_profile(username))"); + +type HistoryRoomType = QueryData; + +export default function UserRoomHistory({ limit = 5 }: { limit?: number }) { + const [rooms, setRooms] = useState([]); const user = useUserProfile(); + useEffect(() => { - (async () => { - if (!user) return; - const userId = user.user_profile_id; - - const { error, data } = await supabase - .from("rooms") - .select("*, room_users(*)") - .eq("room_users.profile_id", userId) - .eq("is_active", false) - .order("created_at", { ascending: false }) - .limit(5); - - if (error) { - return Alert.alert( - "Erreur lors de la récupération des salles d'écoute" - ); + if (!user) return; + /** + * Fetch all rooms where user is a member and the room is not active + * Tips : If the filter on a referenced table's column is not satisfied, the referenced + * table returns [] or null but the parent table is not filtered out. + * If you want to filter out the parent table rows, use the !inner hint + */ + const fetchRoomHistory = async () => { + const { data } = await supabase + .from("room_users") + .select( + "rooms!inner(created_at, name, id, host:user_profile(username))" + ) + .eq("rooms.is_active", false) + .eq("profile_id", user.user_profile_id) + .order("rooms(created_at)", { ascending: false }) + .limit(limit); + + if (!data || data.length === 0) { + return setRooms(null); } setRooms(data); - })(); + }; + fetchRoomHistory(); }, [user]); + if (rooms) { + const everyoneHaveRoom = rooms.every((room) => room.rooms); + const everyoneHaveId = rooms.every((room) => room.rooms?.name); + if (!everyoneHaveId || !everyoneHaveRoom) + return Impossible de charger l'historique des salles; + } + return ( - - Historique + +

Historique

+ {rooms == null && ( + Vous n'avez aucune salle dans votre historique + )} item.id} + keyExtractor={(item) => item.rooms!.id} renderItem={({ item }) => { - return ; + if (!item.rooms) + return Impossible de charger cette salle; + return ( + + ); }} />
); } - -const styles = StyleSheet.create({ - title: { - fontFamily: "Outfit-Bold", - fontSize: 24, - marginBottom: 10, - }, -}); diff --git a/expo/components/profile/Avatar.tsx b/expo/components/profile/Avatar.tsx index f4d837c9..cc2b8743 100644 --- a/expo/components/profile/Avatar.tsx +++ b/expo/components/profile/Avatar.tsx @@ -1,5 +1,4 @@ import { ImageStyle } from "expo-image"; -import User from "phosphor-react-native/src/regular/User"; import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { Image, StyleProp, StyleSheet, View } from "react-native"; @@ -12,10 +11,12 @@ type AvatarProps = { id: string | undefined; tempoAvatarImage?: string; // if we want to pass an image url directly (on edit profile) style?: StyleProp; + noCache?: boolean; + radius?: number; }; const Avatar = forwardRef( - ({ id, tempoAvatarImage, style }, ref) => { + ({ id, tempoAvatarImage, noCache, radius = 9999, style }, ref) => { const [avatarUrl, setAvatarUrl] = useState(); useImperativeHandle(ref, () => ({ @@ -30,35 +31,43 @@ const Avatar = forwardRef( }, [id]); async function downloadUserImage() { - const path = `${id}.jpg`; - const { data } = supabase.storage - .from("avatars") - .getPublicUrl(path + "?avoidCache=" + Math.random()); + let path = `${id}.jpg`; + if (noCache) path += "?avoidCache=" + Math.random(); + const { data } = supabase.storage.from("avatars").getPublicUrl(path); setAvatarUrl(data.publicUrl); } - if (avatarUrl) { - return ( - - ); - } else { - return ( - - - - ); - } + return ( + <> + {avatarUrl ? ( + + ) : ( + + )} + + ); } ); const styles = StyleSheet.create({ avatar: { - borderRadius: 9999, overflow: "hidden", maxWidth: "100%", aspectRatio: 1, diff --git a/expo/components/profile/AvatarForm.tsx b/expo/components/profile/AvatarForm.tsx index 26842192..ffaa74dc 100644 --- a/expo/components/profile/AvatarForm.tsx +++ b/expo/components/profile/AvatarForm.tsx @@ -6,7 +6,7 @@ import { Alert, Platform, Pressable, View } from "react-native"; import Avatar, { AvatarRemote } from "./Avatar"; import { supabase } from "../../lib/supabase"; -import { useSupabaseUserHook } from "../../lib/useSupabaseUser"; +import { useUserProfile } from "../../lib/userProfile"; import Button from "../Button"; import { formStyles } from "../ControlledInput"; import { Text } from "../Themed"; @@ -21,7 +21,7 @@ interface AvatarProps { */ const AvatarForm = forwardRef((props: AvatarProps, ref) => { const { onImageLoad } = props; - const user = useSupabaseUserHook(); + const user = useUserProfile(); const [uploading, setUploading] = useState(false); const [avatarUrl, setAvatarUrl] = useState(undefined); @@ -41,7 +41,8 @@ const AvatarForm = forwardRef((props: AvatarProps, ref) => { error: "No user or avatar", }; } - const fileName = `${user.id}.jpg`; + + const fileName = `${user.user_profile_id}.jpg`; const image = await getBase64Image(avatarUrl); const { error } = await supabase.storage @@ -64,7 +65,7 @@ const AvatarForm = forwardRef((props: AvatarProps, ref) => { async function selectAvatar() { if (!user) return; - setUploading(true); + // setUploading(true); const file = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, @@ -81,14 +82,19 @@ const AvatarForm = forwardRef((props: AvatarProps, ref) => { setAvatarUrl(image.uri); onImageLoad(); } - setUploading(false); + // setUploading(false); } return ( Photo de profil - + + + + + + + + + + {userProfile ? userProfile.username : "chargement"} + + @{profile?.nickname} + + +
+ ); +}; + +const profileStyles = StyleSheet.create({ + personalityView: { + flexDirection: "column", + marginHorizontal: 12, + }, + username: { + fontSize: 16, + fontFamily: Font.Outfit.Medium, + letterSpacing: 0.32, + }, + nickname: { + fontSize: 14, + fontFamily: Font.Outfit.Regular, + color: Colors.light.gray, + }, +}); diff --git a/expo/components/text/Subtitle.tsx b/expo/components/text/Subtitle.tsx new file mode 100644 index 00000000..6f15a8ce --- /dev/null +++ b/expo/components/text/Subtitle.tsx @@ -0,0 +1,23 @@ +import { StyleSheet } from "react-native"; + +import Colors from "../../constants/Colors"; +import Font from "../../constants/Font"; +import { Text } from "../Themed"; + +type SubtitleProps = { + children: string | string[]; +}; + +const Subtitle: React.FC = ({ children }) => { + return {children}; +}; + +const styles = StyleSheet.create({ + Subtitle: { + fontFamily: Font.Outfit.Regular, + fontSize: 16, + color: Colors.light.gray, + }, +}); + +export default Subtitle; diff --git a/expo/constants/Colors.ts b/expo/constants/Colors.ts index 58e6f7e5..a8645b45 100644 --- a/expo/constants/Colors.ts +++ b/expo/constants/Colors.ts @@ -11,5 +11,6 @@ export default { tint: tintColorLight, tabIconDefault: fakeBlack, tabIconSelected: fakeBlack, + gray: "#B2B2B2", }, }; diff --git a/expo/constants/Font.ts b/expo/constants/Font.ts new file mode 100644 index 00000000..81e3f262 --- /dev/null +++ b/expo/constants/Font.ts @@ -0,0 +1,26 @@ +/** + * Font constants + */ +export default { + Outfit: { + Thin: "Outfit-Thin", + ExtraLight: "Outfit-ExtraLight", + Light: "Outfit-Light", + Medium: "Outfit-Medium", + Regular: "Outfit-Regular", + SemiBold: "Outfit-SemiBold", + Bold: "Outfit-Bold", + ExtraBold: "Outfit-ExtraBold", + Black: "Outfit-Black", + }, + Unbounded: { + Black: "Unbounded-Black", + Bold: "Unbounded-Bold", + ExtraBold: "Unbounded-ExtraBold", + ExtraLight: "Unbounded-ExtraLight", + Light: "Unbounded-Light", + Medium: "Unbounded-Medium", + Regular: "Unbounded-Regular", + SemiBold: "Unbounded-SemiBold", + }, +};