From 70e8890fc62d2aa08d3102234b0e1c2e9924470b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Wed, 31 Jan 2024 19:15:05 +0100 Subject: [PATCH 1/5] Add paginated potential user fetching. --- .../controller/MatchController.java | 5 ++- .../rimatchbackend/service/MatchService.java | 5 ++- frontend/src/api/users.ts | 34 +++++++++------- frontend/src/components/MatchCard.tsx | 40 ++++++++++++++----- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java index 8dc7b13..8625691 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/controller/MatchController.java @@ -36,10 +36,11 @@ public class MatchController { private InfobipClient infobipClient; @GetMapping("/potential") - public List getPotentinalMatch(HttpServletRequest request){ + public List getPotentialMatch(HttpServletRequest request, @RequestParam(value = "skip", required = false) Integer skip) { String authToken = request.getHeader("Authorization"); User user = userService.getUserByToken(authToken); - List list = matchService.findPotentialMatches(user); + int skipValue = skip != null ? skip : 0; + List list = matchService.findPotentialMatches(user, skipValue); return list; } diff --git a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java index 01fb929..fe543ff 100644 --- a/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java +++ b/backend/src/main/java/com/rimatch/rimatchbackend/service/MatchService.java @@ -66,7 +66,7 @@ public Match finishMatch(Match match, boolean status) { return matchRepository.save(match); } - public List findPotentialMatches(User user) { + public List findPotentialMatches(User user, int skip) { return DisplayUserConverter.convertToDtoList(mongoTemplate.aggregate( Aggregation.newAggregation( @@ -81,7 +81,8 @@ public List findPotentialMatches(User user) { // Second age parametar needs to defined separatenly because of limitations of // the org.bson.Document Aggregation.match(Criteria.where("age").gte(user.getPreferences().getAgeGroupMin())), - Aggregation.limit(10)), + Aggregation.skip(skip), + Aggregation.limit(5)), "users", User.class).getMappedResults()); } diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index f061a47..b758e38 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -41,15 +41,31 @@ export const UsersService = { }); }, - useGetPotentailUsers: () => { + useGetPotentailUsers: (page: number) => { const axios = useAxiosPrivate(); return useQuery({ - queryKey: ["UsersService.getPotentialUsers"], + queryKey: ["UsersService.getPotentialUsers", page], queryFn: () => axios.get("/match/potential").then((res) => res.data), - staleTime: Infinity, + staleTime: 60e3, }); }, + usePrefetchPotentialUsers: (page: number) => { + const axios = useAxiosPrivate(); + const queryClient = useQueryClient(); + + const prefetch = () => { + queryClient.prefetchQuery({ + queryKey: ["UsersService.getPotentialUsers", page + 1], + queryFn: () => + axios.get(`/match/potential?skip=3`).then((res) => res.data), + staleTime: 60e3, + }); + }; + + return prefetch; + }, + useAcceptMatch: ( mutationOptions?: Omit< UseMutationOptions, @@ -57,17 +73,11 @@ export const UsersService = { > ) => { const axios = useAxiosPrivate(); - const queryClient = useQueryClient(); return useMutation({ mutationFn: async (data) => { const response = await axios.post("/match/accept", data); return response.data; }, - onSuccess: () => { - return queryClient.invalidateQueries({ - queryKey: ["UsersService.getPotentialUsers"], - }); - }, ...mutationOptions, }); }, @@ -79,17 +89,11 @@ export const UsersService = { > ) => { const axios = useAxiosPrivate(); - const queryClient = useQueryClient(); return useMutation({ mutationFn: async (data) => { const response = await axios.post("/match/reject", data); return response.data; }, - onSuccess: () => { - return queryClient.invalidateQueries({ - queryKey: ["UsersService.getCurrentUser"], - }); - }, ...mutationOptions, }); }, diff --git a/frontend/src/components/MatchCard.tsx b/frontend/src/components/MatchCard.tsx index 9024009..7bcb318 100644 --- a/frontend/src/components/MatchCard.tsx +++ b/frontend/src/components/MatchCard.tsx @@ -2,16 +2,42 @@ import ClearIcon from "@mui/icons-material/Clear"; import CheckIcon from "@mui/icons-material/Check"; import { UsersService } from "@/api/users"; import cx from "classnames"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { ProfileCard } from "./ProfileCard"; + +const PAGE_SIZE = 5; + const MatchCard = () => { - const result = UsersService.useGetPotentailUsers(); + const [page, setPage] = useState(0); + const [currentUserIndex, setCurrentUserIndex] = useState(0); + const result = UsersService.useGetPotentailUsers(page); const acceptMatch = UsersService.useAcceptMatch(); const rejectMatch = UsersService.useRejectMatch(); const [isProfileOpen, setIsProfileOpen] = useState(false); + const prefetchPotential = UsersService.usePrefetchPotentialUsers(page); + + const handleNextUser = (matchAccept: boolean, userId: string) => { + const match = matchAccept ? acceptMatch.mutate : rejectMatch.mutate; + if (currentUserIndex === PAGE_SIZE - 3) { + prefetchPotential(); + } + match({ userId }); + if (currentUserIndex === PAGE_SIZE - 1) { + setPage((prev) => prev + 1); + setCurrentUserIndex(0); + } else { + setCurrentUserIndex((prev) => prev + 1); + } + }; + + const user = useMemo(() => { + if (!result.data) return undefined; + if (result.data.length <= currentUserIndex) return undefined; + return result.data[currentUserIndex]; + }, [currentUserIndex, result.data]); // TODO: Add loading spinner or something - if (result.isLoading || acceptMatch.isPending || rejectMatch.isPending) { + if (result.isLoading || !user || result.isFetching) { return null; } @@ -24,9 +50,6 @@ const MatchCard = () => { return
No more users
; } - const user = result.data[0]; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars const openProfile = () => { setIsProfileOpen(true); }; @@ -34,7 +57,6 @@ const MatchCard = () => { const closeProfile = () => { setIsProfileOpen(false); }; - //Slice the description to 100 characters const truncatedDescription = user.description.slice(0, 100); return (
@@ -80,13 +102,13 @@ const MatchCard = () => {
From 087dc7d9e7ac449e97dcd119fe217c6e9bb3090c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Wed, 31 Jan 2024 19:29:13 +0100 Subject: [PATCH 2/5] Invalidating queries when user leaves the potential users. --- frontend/src/components/MatchCard.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/MatchCard.tsx b/frontend/src/components/MatchCard.tsx index 7bcb318..d3361c2 100644 --- a/frontend/src/components/MatchCard.tsx +++ b/frontend/src/components/MatchCard.tsx @@ -2,8 +2,9 @@ import ClearIcon from "@mui/icons-material/Clear"; import CheckIcon from "@mui/icons-material/Check"; import { UsersService } from "@/api/users"; import cx from "classnames"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ProfileCard } from "./ProfileCard"; +import { useQueryClient } from "@tanstack/react-query"; const PAGE_SIZE = 5; @@ -15,6 +16,7 @@ const MatchCard = () => { const rejectMatch = UsersService.useRejectMatch(); const [isProfileOpen, setIsProfileOpen] = useState(false); const prefetchPotential = UsersService.usePrefetchPotentialUsers(page); + const queryClient = useQueryClient(); const handleNextUser = (matchAccept: boolean, userId: string) => { const match = matchAccept ? acceptMatch.mutate : rejectMatch.mutate; @@ -30,6 +32,17 @@ const MatchCard = () => { } }; + // When the component unmounts it is necessary to invalidate the query + // because the query is cached and it will return the same data + // but the new component will start from index 0 of the array + useEffect(() => { + return () => { + queryClient.invalidateQueries({ + queryKey: ["UsersService.getPotentialUsers"], + }); + }; + }, []); + const user = useMemo(() => { if (!result.data) return undefined; if (result.data.length <= currentUserIndex) return undefined; From ea218a91bfb66bab330cc2c519e6c53658f73544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Wed, 31 Jan 2024 20:44:20 +0100 Subject: [PATCH 3/5] Fix current user not being reset on logout. --- frontend/src/api/users.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index b758e38..bc19260 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -1,3 +1,4 @@ +import useAuth from "@/hooks/useAuth"; import useAxiosPrivate from "@/hooks/useAxiosPrivate"; import type { Match, MatchData } from "@/types/Match"; import type { @@ -15,9 +16,10 @@ import { export const UsersService = { useGetCurrentUser() { + const { auth } = useAuth(); const axios = useAxiosPrivate(); return useQuery({ - queryKey: ["UsersService.getCurrentUser"], + queryKey: ["UsersService.getCurrentUser", auth?.accessToken], queryFn: () => axios.get("/users/me").then((response) => response.data), staleTime: Infinity, }); From 670fa35b2f773f5c4d20a0df14e9c6148915d113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Wed, 31 Jan 2024 21:26:19 +0100 Subject: [PATCH 4/5] Add loading spinner, message when there are no more users and error message. --- frontend/src/components/MatchCard.tsx | 60 ++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/MatchCard.tsx b/frontend/src/components/MatchCard.tsx index d3361c2..e30342d 100644 --- a/frontend/src/components/MatchCard.tsx +++ b/frontend/src/components/MatchCard.tsx @@ -5,6 +5,7 @@ import cx from "classnames"; import { useEffect, useMemo, useState } from "react"; import { ProfileCard } from "./ProfileCard"; import { useQueryClient } from "@tanstack/react-query"; +import { CircularProgress } from "@mui/material"; const PAGE_SIZE = 5; @@ -49,18 +50,37 @@ const MatchCard = () => { return result.data[currentUserIndex]; }, [currentUserIndex, result.data]); - // TODO: Add loading spinner or something - if (result.isLoading || !user || result.isFetching) { - return null; + if (result.isLoading || result.isFetching) { + return ( + +
+ +
+
+ ); } if (result.isError || !result.isSuccess) { - return
Error: {result.error?.message}
; + return ( + +
+

Whoops.

+ + Something went wrong, try refreshing the page + +
+
+ ); } - // TODO: Nicer message when there are no more users - if (result.data.length === 0) { - return
No more users
; + if (result.data.length === 0 || !user) { + return ( + +
+ Oops! You've swiped everyone away. Try not to look so surprised. +
+
+ ); } const openProfile = () => { @@ -72,9 +92,9 @@ const MatchCard = () => { }; const truncatedDescription = user.description.slice(0, 100); return ( -
+ <> {!isProfileOpen && ( -
+
{
-
+ )} {isProfileOpen && } + + ); +}; + +interface MatchCardContainerProps { + children?: React.ReactNode; +} + +const MatchCardContainer = ({ + children, + ...props +}: MatchCardContainerProps) => { + return ( +
+
+ {children} +
); }; From 5a6c1d5d295c710bc67ad33d5b307ef062ef0cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Bo=C5=BEi=C4=87?= Date: Wed, 31 Jan 2024 21:58:53 +0100 Subject: [PATCH 5/5] Improve circular progress styling. --- frontend/src/components/MatchCard.tsx | 4 ++-- frontend/src/views/LoginForm.tsx | 15 +++++++++++---- frontend/src/views/MatchesPage.tsx | 4 ++-- frontend/src/views/RegisterForm.tsx | 7 ++++++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/MatchCard.tsx b/frontend/src/components/MatchCard.tsx index e30342d..1e01117 100644 --- a/frontend/src/components/MatchCard.tsx +++ b/frontend/src/components/MatchCard.tsx @@ -53,8 +53,8 @@ const MatchCard = () => { if (result.isLoading || result.isFetching) { return ( -
- +
+
); diff --git a/frontend/src/views/LoginForm.tsx b/frontend/src/views/LoginForm.tsx index d75983a..70bea64 100644 --- a/frontend/src/views/LoginForm.tsx +++ b/frontend/src/views/LoginForm.tsx @@ -3,6 +3,7 @@ import * as Yup from "yup"; import { EmailAtIcon, LockIcon } from "../assets"; import { Link, useLocation, useNavigate } from "react-router-dom"; import AuthService from "@/api/auth"; +import { CircularProgress } from "@mui/material"; const LoginSchema = Yup.object({ email: Yup.string().required("Required").email("Must be valid email"), @@ -23,7 +24,9 @@ const LoginForm = () => { const { mutateAsync: login } = AuthService.useLogin(); const from = location?.state?.from?.pathname ?? "/"; - const handleSubmit = (values: LoginValues) => { + const handleSubmit = ( + values: LoginValues /*helpers: FormikHelpers*/ + ) => { return login(values, { onSuccess: () => { navigate(from, { replace: true }); @@ -61,7 +64,7 @@ const LoginForm = () => {
@@ -76,14 +79,18 @@ const LoginForm = () => { Forgot Password ? diff --git a/frontend/src/views/MatchesPage.tsx b/frontend/src/views/MatchesPage.tsx index 5187bbf..42f2e61 100644 --- a/frontend/src/views/MatchesPage.tsx +++ b/frontend/src/views/MatchesPage.tsx @@ -8,8 +8,8 @@ const MatchesPage = () => { if (query.isLoading) { return ( -
- +
+
); diff --git a/frontend/src/views/RegisterForm.tsx b/frontend/src/views/RegisterForm.tsx index e6bc357..ad12bbe 100644 --- a/frontend/src/views/RegisterForm.tsx +++ b/frontend/src/views/RegisterForm.tsx @@ -7,6 +7,7 @@ import { EmailAtIcon, LockIcon } from "../assets"; import { Link, useNavigate } from "react-router-dom"; import AuthService from "@/api/auth"; import DateRangeIcon from "@mui/icons-material/DateRange"; +import { CircularProgress } from "@mui/material"; const RegisterSchema = Yup.object({ email: Yup.string().required("Required").email("Must be a valid email"), password: Yup.string() @@ -220,7 +221,11 @@ const RegisterForm = () => { type="submit" className="block w-full bg-red-600 transition duration-300 ease-in-out hover:bg-red-800 mt-4 py-3 rounded-2xl text-white font-semibold" > - Register + {!isSubmitting ? ( + "Login" + ) : ( + + )}