feat(UserMatch): Logique des matchs et notifications

Affichage d'un modal lorsqu'une notification de match n'est pas consulté
This commit is contained in:
Laurian-Dufrechou
2023-05-16 00:59:05 +02:00
parent 9b4676f769
commit 01e70ceb0a
9 changed files with 4041 additions and 3801 deletions
+5
View File
@@ -61,6 +61,8 @@ model User {
Match Match[] @relation(fields: [MatchID], references: [id])
Notification Notification[]
NotificationMatchedUser Notification[] @relation("matchedUser")
}
model Notification {
@@ -71,6 +73,9 @@ model Notification {
UserID String @db.ObjectId
User User @relation(fields: [UserID], references: [id])
MatchedUserID String? @db.ObjectId
MatchedUser User? @relation("matchedUser", fields: [MatchedUserID], references: [id])
}
model Match {
@@ -8,20 +8,24 @@ import {
Box,
CardHeader,
useToast,
} from '@chakra-ui/react';
import Carousel from '../../../Carousel';
import {BiHeart} from 'react-icons/bi';
import {RxCross1} from 'react-icons/rx';
import {useMutation, useQuery} from '@tanstack/react-query';
import PassionTagList
from '@/components/layout/dashboard/card_user/PassionTagList';
import {useState} from 'react';
import SearchFailCard from './SearchFailCard';
import LoadingPage from '@/components/LoadingPage';
} from "@chakra-ui/react";
import Carousel from "../../../Carousel";
import { BiHeart } from "react-icons/bi";
import { RxCross1 } from "react-icons/rx";
import { useMutation, useQuery } from "@tanstack/react-query";
import PassionTagList from "@/components/layout/dashboard/card_user/PassionTagList";
import { useState } from "react";
import SearchFailCard from "./SearchFailCard";
import LoadingPage from "@/components/LoadingPage";
export default function CardUser({users, loggedUser, setMatch}) {
export default function CardUser({
users,
loggedUser,
setMatch,
refetchLoggedUser,
}) {
const toast = useToast({
position: 'top',
position: "top",
duration: 2000,
isClosable: true,
});
@@ -30,8 +34,8 @@ export default function CardUser({users, loggedUser, setMatch}) {
const likeMutation = useMutation({
mutationFn: async (id) => {
const likePostOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
idUser: loggedUser.id,
idUserLiked: id,
@@ -49,20 +53,20 @@ export default function CardUser({users, loggedUser, setMatch}) {
onSuccess: (data) => {
if (data.error) {
toast({
title: 'Erreur',
description: 'Une erreur est survenue',
status: 'error',
title: "Erreur",
description: "Une erreur est survenue",
status: "error",
});
return;
}
if (data.match) {
setMatch(true);
refetchLoggedUser();
}
setListUsers(listUsers.slice(1));
toast({
title: 'J\'aime',
description: 'Votre action a bien été prise en compte',
status: 'success',
title: "J'aime",
description: "Votre action a bien été prise en compte",
status: "success",
});
//tester si match et afficher un truc
@@ -72,8 +76,8 @@ export default function CardUser({users, loggedUser, setMatch}) {
const dislikeMutation = useMutation({
mutationFn: async (id) => {
const dislikePostOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
idUser: loggedUser.id,
idUserDisliked: id,
@@ -91,18 +95,18 @@ export default function CardUser({users, loggedUser, setMatch}) {
onSuccess: (data) => {
if (data.error) {
toast({
title: 'Erreur',
description: 'Une erreur est survenue',
status: 'error',
title: "Erreur",
description: "Une erreur est survenue",
status: "error",
duration: 2000,
});
return;
}
setListUsers(listUsers.slice(1));
toast({
title: 'J\'aime pas',
description: 'Votre action a bien été prise en compte',
status: 'success',
title: "J'aime pas",
description: "Votre action a bien été prise en compte",
status: "success",
duration: 2000,
});
},
@@ -114,11 +118,11 @@ export default function CardUser({users, loggedUser, setMatch}) {
data: listPassions,
error: passionError,
} = useQuery({
queryKey: ['passions'],
queryKey: ["passions"],
queryFn: async () => {
return fetch(`/api/passions/`)
.then(res => res.json())
.catch(err => err);
.then((res) => res.json())
.catch((err) => err);
},
});
@@ -129,57 +133,64 @@ export default function CardUser({users, loggedUser, setMatch}) {
return Math.abs(ageDate.getUTCFullYear() - 1970);
};
if (listUsers.length === 0) return <SearchFailCard/>;
if (likeMutation.isLoading ||
dislikeMutation.isLoading) return <LoadingPage/>;
if (listUsers.length === 0) return <SearchFailCard />;
if (likeMutation.isLoading || dislikeMutation.isLoading)
return <LoadingPage />;
return (
<Card w={'100%'} h={'100%'} borderRadius={'1rem'} overflow={'hidden'}>
<CardHeader>
<Carousel borderRadius={'1rem'} images={listUsers?.[0].images}/>
</CardHeader>
<Card w={"100%"} h={"100%"} borderRadius={"1rem"} overflow={"hidden"}>
<CardHeader>
<Carousel borderRadius={"1rem"} images={listUsers?.[0].images} />
</CardHeader>
<CardBody>
<Flex justify={'space-between'} mb={'20px'}>
<Heading fontSize={'1.5rem'} fontWeight={'bold'} flexBasis={'70%'}>
{listUsers[0].firstName} {listUsers[0].lastName},{' '}
{formateDateToAge(listUsers[0].birthdate)} ans
</Heading>
<CardBody>
<Flex justify={"space-between"} mb={"20px"}>
<Heading fontSize={"1.5rem"} fontWeight={"bold"} flexBasis={"70%"}>
{listUsers[0].firstName} {listUsers[0].lastName},{" "}
{formateDateToAge(listUsers[0].birthdate)} ans
</Heading>
<Flex gap={1}>
<IconButton icon={<BiHeart/>} aria-label="like"
borderRadius={'1rem'}
onClick={() => likeMutation.mutate(listUsers[0].id)
}/>
<IconButton icon={<RxCross1/>} aria-label="dislike"
borderRadius={'1rem'} variant={'outline'}
onClick={() => dislikeMutation.mutate(
listUsers[0].id)}/>
</Flex>
<Flex gap={1}>
<IconButton
icon={<BiHeart />}
aria-label="like"
borderRadius={"1rem"}
onClick={() => likeMutation.mutate(listUsers[0].id)}
/>
<IconButton
icon={<RxCross1 />}
aria-label="dislike"
borderRadius={"1rem"}
variant={"outline"}
onClick={() => dislikeMutation.mutate(listUsers[0].id)}
/>
</Flex>
</Flex>
<Box mb={'20px'}>
<Heading size={'sm'} fontWeight={'bold'} mb="0.5rem">
A propos :
</Heading>
<Text as="i">&quot;{listUsers[0].bio}&quot;</Text>
</Box>
<Box mb={"20px"}>
<Heading size={"sm"} fontWeight={"bold"} mb="0.5rem">
A propos :
</Heading>
<Text as="i">&quot;{listUsers[0].bio}&quot;</Text>
</Box>
<Box>
<Heading size={'sm'} fontWeight={'bold'}>
Passions :
</Heading>
{passionLoading
? <Text>Chargement des passions...</Text>
: passionIsError
? <Text>Erreur lors du chargement des passions</Text>
:
<PassionTagList passions={listUsers[0].PassionID}
userPassions={loggedUser.PassionID}
listPassions={listPassions}/>
}
</Box>
</CardBody>
</Card>
<Box>
<Heading size={"sm"} fontWeight={"bold"}>
Passions :
</Heading>
{passionLoading ? (
<Text>Chargement des passions...</Text>
) : passionIsError ? (
<Text>Erreur lors du chargement des passions</Text>
) : (
<PassionTagList
passions={listUsers[0].PassionID}
userPassions={loggedUser.PassionID}
listPassions={listPassions}
/>
)}
</Box>
</CardBody>
</Card>
);
}
@@ -1,67 +1,95 @@
import {Card, Flex, Box, Image, Text, Spacer, Divider} from '@chakra-ui/react';
import {useRouter} from 'next/router';
import {
Card,
Flex,
Box,
Image,
Text,
Spacer,
Divider,
} from "@chakra-ui/react";
import { useRouter } from "next/router";
import {AiFillMessage} from 'react-icons/ai';
import {BsFillPersonFill} from 'react-icons/bs';
import {BiLogOut} from 'react-icons/bi';
import {FaMapMarkedAlt} from 'react-icons/fa';
import { AiFillMessage } from "react-icons/ai";
import { BsFillPersonFill } from "react-icons/bs";
import { BiLogOut } from "react-icons/bi";
import { FaMapMarkedAlt } from "react-icons/fa";
import LeftPanelButton
from '@/components/layout/dashboard/left_panel/LeftPanelButton';
import {signOut} from 'next-auth/react';
import LeftPanelButton from "@/components/layout/dashboard/left_panel/LeftPanelButton";
import { signOut } from "next-auth/react";
import { formateDate } from "@/lib/formateDate";
export default function LeftPanel(props) {
const router = useRouter();
const {user} = props;
const formateDate = (dateString) => {
const options = {year: 'numeric', month: 'long', day: 'numeric'};
return new Date(dateString).toLocaleDateString([], options);
};
const { user } = props;
return (
<Card width={'20vw'} height={'100%'} borderRadius={0} padding={'0px'}
bg={'#faf9ff'}>
<Flex direction={'column'} height={'100%'} margin={'10%'}>
<Box>
<Image src={user.images[0] ?? '/blank_profile_picture.webp'}
objectFit={'contain'} borderRadius={'1rem'}/>
<Box mt={'2rem'}>
<Flex align={'center'} justifyContent="space-between" mb={'1rem'}>
<Text fontSize={'1.5rem'} fontWeight={'bold'}>
{user.firstName} {user.lastName}
</Text>
<Text fontSize={'1rem'} fontWeight={'bold'}>
{formateDate(user.birthdate)}
</Text>
</Flex>
<Divider mb={'1rem'}/>
<Text as="i" fontWeight={'bold'}>&quot;{user.bio}&quot;</Text>
</Box>
<Card
width={"20vw"}
height={"100%"}
borderRadius={0}
padding={"0px"}
bg={"#faf9ff"}
>
<Flex direction={"column"} height={"100%"} margin={"10%"}>
<Box>
<Image
src={user.images[0] ?? "/blank_profile_picture.webp"}
objectFit={"contain"}
borderRadius={"1rem"}
/>
<Box mt={"2rem"}>
<Flex align={"center"} justifyContent="space-between" mb={"1rem"}>
<Text fontSize={"1.5rem"} fontWeight={"bold"}>
{user.firstName} {user.lastName}
</Text>
<Text fontSize={"1rem"} fontWeight={"bold"}>
{formateDate(user.birthdate)}
</Text>
</Flex>
<Divider mb={"1rem"} />
<Text as="i" fontWeight={"bold"}>
&quot;{user.bio}&quot;
</Text>
</Box>
<Spacer/>
<Flex gap={2} direction={'column'} mt={'1vh'}
spacing={3.5} alignContent={'bottom'}>
<LeftPanelButton leftIcon={<FaMapMarkedAlt/>}
onClickHandler={() => router.push('/map')}>
Carte
</LeftPanelButton>
</Box>
<Spacer />
<Flex
gap={2}
direction={"column"}
mt={"1vh"}
spacing={3.5}
alignContent={"bottom"}
>
<LeftPanelButton
leftIcon={<FaMapMarkedAlt />}
onClickHandler={() => router.push("/map")}
>
Carte
</LeftPanelButton>
<LeftPanelButton leftIcon={<AiFillMessage/>}
onClickHandler={() => router.push('/dashboard')}>
Messages
</LeftPanelButton>
<LeftPanelButton
leftIcon={<AiFillMessage />}
onClickHandler={() => router.push("/dashboard")}
>
Messages
</LeftPanelButton>
<LeftPanelButton leftIcon={<BsFillPersonFill/>}
onClickHandler={() => router.push('/profile')}>
Profil
</LeftPanelButton>
<LeftPanelButton variant={'outline'} leftIcon={<BiLogOut/>}
onClickHandler={() => signOut({callbackUrl: '/'})}>
Déconnexion
</LeftPanelButton>
</Flex>
<LeftPanelButton
leftIcon={<BsFillPersonFill />}
onClickHandler={() => router.push("/profile")}
>
Profil
</LeftPanelButton>
<LeftPanelButton
variant={"outline"}
leftIcon={<BiLogOut />}
onClickHandler={() => signOut({ callbackUrl: "/" })}
>
Déconnexion
</LeftPanelButton>
</Flex>
</Card>
</Flex>
</Card>
);
}
@@ -0,0 +1,147 @@
import {
Box,
Button,
Divider,
Flex,
Heading,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { SetStateAction } from "react";
import { useQuery } from "@tanstack/react-query";
import { User } from "@prisma/client";
import Carousel from "@/components/Carousel";
import { MdWavingHand } from "react-icons/md";
import { formateDate } from "@/lib/formateDate";
import { Notification } from "@prisma/client";
import PassionTagList from "../card_user/PassionTagList";
type modalMatchProps = {
key: string;
loggedUser: User;
notif: Notification;
};
export default function ModalMatch({ notif, loggedUser }: modalMatchProps) {
const handleClose = () => {
const options = {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ notificationId: notif.id }),
};
fetch(`/api/user/consultNotification`, options)
.then((res) => res.json())
.catch((err) => console.log(err));
};
const { isOpen, onClose } = useDisclosure({
defaultIsOpen: true,
onClose: () => {
handleClose();
},
});
const {
isLoading,
isError,
data: matchedUser,
error,
} = useQuery({
enabled: notif !== undefined,
queryKey: ["matchUser"],
queryFn: async () => {
return fetch(`/api/users/${notif.MatchedUserID}`)
.then((res) => {
return res.json();
})
.catch((err) => {
return err;
});
},
});
const {
isLoading: passionLoading,
isError: passionIsError,
data: listPassions,
error: passionError,
} = useQuery({
queryKey: ["passions"],
queryFn: async () => {
return fetch(`/api/passions/`)
.then((res) => res.json())
.catch((err) => err);
},
});
return (
<>
{matchedUser && (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay backdropBlur="2px" />
<ModalContent>
<ModalHeader>Vous avez un nouveau match</ModalHeader>
<ModalBody>
<Carousel borderRadius={"1rem"} images={matchedUser.images} />
<Box mt={"2rem"}>
<Flex
align={"center"}
justifyContent="space-between"
mb={"1rem"}
>
<Text fontSize={"1.5rem"} fontWeight={"bold"}>
{matchedUser.firstName} {matchedUser.lastName}
</Text>
<Text fontSize={"1rem"} fontWeight={"bold"}>
{formateDate(matchedUser.birthdate)}
</Text>
</Flex>
<Divider mb={"1rem"} />
<Text as="i" fontWeight={"bold"}>
&quot;{matchedUser.bio}&quot;
</Text>
</Box>
<Box>
<Heading size={"sm"} fontWeight={"bold"}>
Passions :
</Heading>
{passionLoading ? (
<Text>Chargement des passions...</Text>
) : passionIsError ? (
<Text>Erreur lors du chargement des passions</Text>
) : (
<PassionTagList
passions={matchedUser.PassionID}
userPassions={loggedUser.PassionID}
listPassions={listPassions}
/>
)}
</Box>
</ModalBody>
<ModalFooter>
<Flex gap={"1rem"}>
<Button onClick={onClose}>Fermer</Button>
<Button
leftIcon={<MdWavingHand />}
variant={"ghost"}
onClick={onClose}
>
Saluer
</Button>
</Flex>
</ModalFooter>
</ModalContent>
</Modal>
)}
</>
);
}
+4
View File
@@ -0,0 +1,4 @@
export const formateDate = (dateString: Date) => {
const options = { year: "numeric", month: "long", day: "numeric" };
return new Date(dateString).toLocaleDateString([], options);
};
+30
View File
@@ -0,0 +1,30 @@
const { PrismaClient } = require("@prisma/client");
const patch = async (req: any, res: any) => {
const prisma = new PrismaClient();
const { notificationId } = req.body;
try {
const notification = await prisma.notification.update({
where: {
id: notificationId,
},
data: {
hasBeenConsulted: true,
},
});
res.status(200).json(notification);
} catch (error) {
res.status(400).json({ message: "Something went wrong" });
} finally {
await prisma.$disconnect();
}
};
export default (req: any, res: any) => {
req.method === "PATCH"
? patch(req, res)
: res.status(404).send({ message: "Wrong method, please use POST" });
};
+14 -4
View File
@@ -21,23 +21,33 @@ const post = async (req, res) => {
if (match) {
await prisma.notification.create({
data: {
type: NotificationType.MATCH,
user: {
type: NotificationType.NEW_MATCH,
User: {
connect: {
id: idUser,
},
},
MatchedUser: {
connect: {
id: idUserLiked,
},
},
},
});
await prisma.notification.create({
data: {
type: NotificationType.MATCH,
user: {
type: NotificationType.NEW_MATCH,
User: {
connect: {
id: idUserLiked,
},
},
MatchedUser: {
connect: {
id: idUser,
},
},
},
});
}
+22 -17
View File
@@ -1,4 +1,4 @@
import { Grid, GridItem, Text, Box, useToast } from "@chakra-ui/react";
import { Grid, GridItem, Box, useToast, useDisclosure } from "@chakra-ui/react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useState } from "react";
@@ -8,15 +8,16 @@ import CardUser from "../components/layout/dashboard/card_user/CardUser";
import SearchFailCard from "../components/layout/dashboard/card_user/SearchFailCard";
import LeftPanel from "../components/layout/dashboard/left_panel/LeftPanel";
import ModalMatch from "@/components/layout/dashboard/match_notification/ModalMatch";
import Head from "next/head";
import { websiteName } from "@/lib/constants";
import { useQuery } from "@tanstack/react-query";
import LoadingPage from "@/components/LoadingPage";
import { Notification, NotificationType } from "@prisma/client";
export default function Dashboard() {
const router = useRouter();
const toast = useToast({ position: "top", isClosable: true });
const [newMatch, setNewMatch] = useState(false);
const { data: session, status } = useSession();
@@ -25,13 +26,14 @@ export default function Dashboard() {
isError,
data: loggedUser,
error,
refetch: refetchLoggedUser,
} = useQuery({
queryKey: ["LoggedUser"],
enabled: status === "authenticated",
queryFn: async () => {
const { user } = session as unknown as Session;
return fetch(`/api/users/${user.id}`)
return fetch(`/api/users/${user.id}?include=Notification`)
.then((res) => {
return res.json();
})
@@ -72,21 +74,25 @@ export default function Dashboard() {
if (status === "unauthenticated") router.push("/");
}
/**
* UTILISER refetch (image sur discord)
* https://tanstack.com/query/v3/docs/react/reference/useQuery ---> tout en bas
*
* dans onSuccess de useQuery
* on filtre l'objet Notification de loggedUser par le type (match)
* setMatchNotification = [...filteredNotification],
*
* et on passe dans le modal la premiere notif de type match
* et on enleve au fur et a mesure (sur la BD aussi)
*
*/
let modal;
if (loggedUser?.Notification?.length > 0) {
const matchNotification = loggedUser.Notification.filter(
(notif: Notification) =>
notif.type === NotificationType.NEW_MATCH &&
notif.hasBeenConsulted === false
);
modal = matchNotification.map((notif: Notification) => {
return (
<ModalMatch notif={notif} key={notif.id} loggedUser={loggedUser} />
);
});
}
return (
<>
{modal}
<Head>
<title>{websiteName} | Dashboard</title>
</Head>
@@ -110,11 +116,10 @@ export default function Dashboard() {
listUsers.users.length === 0 ? (
<SearchFailCard />
) : (
// dans cardUser, mettre une liste de user, utiliser le user[0] et quand like ou dislike, supprimer le user[0] de la liste
<CardUser
users={listUsers.users}
loggedUser={loggedUser}
setMatch={setNewMatch}
refetchLoggedUser={refetchLoggedUser}
/>
)}
</Box>
+3649 -3649
View File
File diff suppressed because it is too large Load Diff