Skip to content

Commit

Permalink
feat/profile-page (#143)
Browse files Browse the repository at this point in the history
* feat: create header of profil page

* fix: added props to avatar (radius & noCache)

* chore: added noCaches in Avatar form

* feat: added history in profile page

* fix: history card request

* feat: added Font constants

* chore: using constant instance of arbitrary values

* fix: review
  • Loading branch information
GaspardBBY authored Apr 4, 2024
1 parent b73e1bc commit c62caf3
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 139 deletions.
7 changes: 6 additions & 1 deletion expo/app/(tabs)/profile/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Stack } from "expo-router";

import { ProfileHeader } from "../../../components/profile/HeaderProfile";

export default function ProfileTabLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Profil" }} />
<Stack.Screen
name="index"
options={{ title: "Profil", header: () => <ProfileHeader /> }}
/>
<Stack.Screen name="account" options={{ headerShown: false }} />
</Stack>
);
Expand Down
37 changes: 10 additions & 27 deletions expo/app/(tabs)/profile/index.tsx
Original file line number Diff line number Diff line change
@@ -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 && (
<View style={styles.elements}>
<Button
prependIcon={<SignOut />}
onPress={() => supabase.auth.signOut()}
>
Se déconnecter
</Button>
<Button href="/(tabs)/profile/account">Gérer mon compte</Button>
</View>
)}
</>
<ScrollView
contentContainerStyle={{
paddingVertical: 32,
paddingHorizontal: 18,
}}
>
<UserRoomHistory limit={10} />
</ScrollView>
);
}

const styles = StyleSheet.create({
elements: {
gap: 10,
},
});
58 changes: 17 additions & 41 deletions expo/components/RoomHistoryInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link href={`/rooms/${room.id}`}>
<Link href={`/rooms/${roomId}`}>
<View style={styles.layout}>
<View style={styles.infos}>
<Text style={styles.roomName}>{room.name}</Text>
<Text style={styles.roomName}>{roomName}</Text>
<Text style={styles.roomInfo}>
par {username} le {formatedDate}
par {hostUsername} le {formattedDate}
</Text>
</View>
<MaterialIcons name="keyboard-arrow-right" size={32} color="black" />
Expand All @@ -67,11 +43,11 @@ const styles = StyleSheet.create({
justifyContent: "space-between",
alignItems: "center",
width: "100%",
marginVertical: 8,
},

infos: {
flexDirection: "column",
marginVertical: 8,
gap: 4,
},

Expand Down
97 changes: 62 additions & 35 deletions expo/components/UserRoomHistory.tsx
Original file line number Diff line number Diff line change
@@ -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<Room[]>([]);
const historyRoom = supabase
.from("room_users")
.select("rooms!inner(created_at, name, id, host:user_profile(username))");

type HistoryRoomType = QueryData<typeof historyRoom>;

export default function UserRoomHistory({ limit = 5 }: { limit?: number }) {
const [rooms, setRooms] = useState<HistoryRoomType | null>([]);
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 <Subtitle>Impossible de charger l'historique des salles</Subtitle>;
}

return (
<View>
<Text style={styles.title}>Historique</Text>
<View style={{ gap: 10 }}>
<H2>Historique</H2>
{rooms == null && (
<Subtitle>Vous n'avez aucune salle dans votre historique</Subtitle>
)}
<FlatList
data={rooms}
keyExtractor={(item) => item.id}
keyExtractor={(item) => item.rooms!.id}
renderItem={({ item }) => {
return <RoomHistoryInfoCard room={item} />;
if (!item.rooms)
return <Subtitle>Impossible de charger cette salle</Subtitle>;
return (
<RoomHistoryInfoCard
createdAt={item.rooms.created_at}
hostUsername={
item.rooms.host?.username ??
"Impossible de récupérer le nom de l'hôte"
}
roomId={item.rooms.id}
roomName={item.rooms.name}
/>
);
}}
/>
</View>
);
}

const styles = StyleSheet.create({
title: {
fontFamily: "Outfit-Bold",
fontSize: 24,
marginBottom: 10,
},
});
53 changes: 31 additions & 22 deletions expo/components/profile/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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<ImageStyle>;
noCache?: boolean;
radius?: number;
};

const Avatar = forwardRef<AvatarRemote, AvatarProps>(
({ id, tempoAvatarImage, style }, ref) => {
({ id, tempoAvatarImage, noCache, radius = 9999, style }, ref) => {
const [avatarUrl, setAvatarUrl] = useState<string>();

useImperativeHandle(ref, () => ({
Expand All @@ -30,35 +31,43 @@ const Avatar = forwardRef<AvatarRemote, AvatarProps>(
}, [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 (
<Image
source={{ uri: tempoAvatarImage ?? avatarUrl }}
aria-aria-label="Avatar"
style={[styles.avatar, styles.image, style]}
/>
);
} else {
return (
<View style={[styles.avatar, styles.noImage, style]}>
<User />
</View>
);
}
return (
<>
{avatarUrl ? (
<Image
source={{ uri: tempoAvatarImage ?? avatarUrl }}
aria-aria-label="Avatar"
style={[
{ borderRadius: radius },
styles.avatar,
styles.image,
style,
]}
/>
) : (
<View
style={[
{ borderRadius: radius },
styles.avatar,
styles.noImage,
style,
]}
/>
)}
</>
);
}
);

const styles = StyleSheet.create({
avatar: {
borderRadius: 9999,
overflow: "hidden",
maxWidth: "100%",
aspectRatio: 1,
Expand Down
Loading

0 comments on commit c62caf3

Please sign in to comment.