Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch more potential users in the background. #42

Merged
merged 5 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ public class MatchController {
private InfobipClient infobipClient;

@GetMapping("/potential")
public List<DisplayUserDto> getPotentinalMatch(HttpServletRequest request){
public List<DisplayUserDto> getPotentialMatch(HttpServletRequest request, @RequestParam(value = "skip", required = false) Integer skip) {
String authToken = request.getHeader("Authorization");
User user = userService.getUserByToken(authToken);
List<DisplayUserDto> list = matchService.findPotentialMatches(user);
int skipValue = skip != null ? skip : 0;
List<DisplayUserDto> list = matchService.findPotentialMatches(user, skipValue);
return list;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public Match finishMatch(Match match, boolean status) {
return matchRepository.save(match);
}

public List<DisplayUserDto> findPotentialMatches(User user) {
public List<DisplayUserDto> findPotentialMatches(User user, int skip) {

return DisplayUserConverter.convertToDtoList(mongoTemplate.aggregate(
Aggregation.newAggregation(
Expand All @@ -81,7 +81,8 @@ public List<DisplayUserDto> 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());
}

Expand Down
38 changes: 22 additions & 16 deletions frontend/src/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import useAuth from "@/hooks/useAuth";
import useAxiosPrivate from "@/hooks/useAxiosPrivate";
import type { Match, MatchData } from "@/types/Match";
import type {
Expand All @@ -15,9 +16,10 @@ import {

export const UsersService = {
useGetCurrentUser() {
const { auth } = useAuth();
const axios = useAxiosPrivate();
return useQuery<User, Error>({
queryKey: ["UsersService.getCurrentUser"],
queryKey: ["UsersService.getCurrentUser", auth?.accessToken],
queryFn: () => axios.get("/users/me").then((response) => response.data),
staleTime: Infinity,
});
Expand All @@ -41,33 +43,43 @@ export const UsersService = {
});
},

useGetPotentailUsers: () => {
useGetPotentailUsers: (page: number) => {
const axios = useAxiosPrivate();
return useQuery<User[], Error>({
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: <T = Match>(
mutationOptions?: Omit<
UseMutationOptions<T, Error, MatchData>,
"mutationFn"
>
) => {
const axios = useAxiosPrivate();
const queryClient = useQueryClient();
return useMutation<T, Error, MatchData>({
mutationFn: async (data) => {
const response = await axios.post<T>("/match/accept", data);
return response.data;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ["UsersService.getPotentialUsers"],
});
},
...mutationOptions,
});
},
Expand All @@ -79,17 +91,11 @@ export const UsersService = {
>
) => {
const axios = useAxiosPrivate();
const queryClient = useQueryClient();
return useMutation<T, Error, MatchData>({
mutationFn: async (data) => {
const response = await axios.post<T>("/match/reject", data);
return response.data;
},
onSuccess: () => {
return queryClient.invalidateQueries({
queryKey: ["UsersService.getCurrentUser"],
});
},
...mutationOptions,
});
},
Expand Down
111 changes: 93 additions & 18 deletions frontend/src/components/MatchCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,99 @@ 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 { useEffect, useMemo, useState } from "react";
import { ProfileCard } from "./ProfileCard";
import { useQueryClient } from "@tanstack/react-query";
import { CircularProgress } from "@mui/material";

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 queryClient = useQueryClient();

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);
}
};

// 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"],
});
};
}, []);

// TODO: Add loading spinner or something
if (result.isLoading || acceptMatch.isPending || rejectMatch.isPending) {
return null;
const user = useMemo(() => {
if (!result.data) return undefined;
if (result.data.length <= currentUserIndex) return undefined;
return result.data[currentUserIndex];
}, [currentUserIndex, result.data]);

if (result.isLoading || result.isFetching) {
return (
<MatchCardContainer>
<div className="flex justify-center items-center h-full w-full text-red-500">
<CircularProgress size="3rem" color="inherit" />
</div>
</MatchCardContainer>
);
}

if (result.isError || !result.isSuccess) {
return <div>Error: {result.error?.message}</div>;
return (
<MatchCardContainer>
<div className="flex justify-center flex-col items-center h-full w-full text-center px-4">
<h2 className="text-red-500 mb-2 text-3xl">Whoops.</h2>
<span className="text-lg">
Something went wrong, try refreshing the page
</span>
</div>
</MatchCardContainer>
);
}

// TODO: Nicer message when there are no more users
if (result.data.length === 0) {
return <div>No more users</div>;
if (result.data.length === 0 || !user) {
return (
<MatchCardContainer>
<div className="flex justify-center items-center h-full w-full text-center px-4 text-lg sm:text-2xl">
Oops! You&apos;ve swiped everyone away. Try not to look so surprised.
</div>
</MatchCardContainer>
);
}

const user = result.data[0];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const openProfile = () => {
setIsProfileOpen(true);
};

const closeProfile = () => {
setIsProfileOpen(false);
};
//Slice the description to 100 characters
const truncatedDescription = user.description.slice(0, 100);
return (
<div className="flex justify-center">
<>
{!isProfileOpen && (
<div className="relative flex flex-col items-center rounded-[25px] border-[1px] border-black-200 h-[580px] sm:h-[600] w-[340px] sm:w-[24rem] p-4 bg-white dark:bg-[#343030] bg-clip-border border-[#acabab33] shadow-xl shadow-black">
<MatchCardContainer>
<div className="relative flex h-72 w-full justify-center rounded-xl bg-cover">
<div
className={cx(
Expand Down Expand Up @@ -80,21 +135,41 @@ const MatchCard = () => {
<div className="flex mt-16 flex-row justify-between w-full text-white">
<button
className="btn hover:bg-green-600 bg-green-500 transition-color duration-300 sm:ml-4 mb-2 border-green-700 rounded-full w-24 h-24 shadow-md shadow-black"
onClick={() => acceptMatch.mutate({ userId: user.id })}
onClick={() => handleNextUser(true, user.id)}
>
<CheckIcon fontSize="large" />
</button>
<button
className="btn bg-red-500 hover:bg-red-600 transition-color duration-300 rounded-full sm:mr-2 border-red-700 btn-circle w-24 h-24 shadow-md shadow-black"
onClick={() => rejectMatch.mutate({ userId: user.id })}
onClick={() => handleNextUser(false, user.id)}
>
<ClearIcon fontSize="large" />
</button>
</div>
</div>
</div>
</MatchCardContainer>
)}
{isProfileOpen && <ProfileCard user={user} onClose={closeProfile} />}
</>
);
};

interface MatchCardContainerProps {
children?: React.ReactNode;
}

const MatchCardContainer = ({
children,
...props
}: MatchCardContainerProps) => {
return (
<div className="flex justify-center">
<div
className="relative flex flex-col items-center rounded-[25px] border-[1px] border-black-200 h-[580px] sm:h-[600] w-[340px] sm:w-[24rem] p-4 bg-white dark:bg-[#343030] bg-clip-border border-[#acabab33] shadow-xl shadow-black"
{...props}
>
{children}
</div>
</div>
);
};
Expand Down
15 changes: 11 additions & 4 deletions frontend/src/views/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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<LoginValues>*/
) => {
return login(values, {
onSuccess: () => {
navigate(from, { replace: true });
Expand Down Expand Up @@ -61,7 +64,7 @@ const LoginForm = () => {
<ErrorMessage
component="div"
name="email"
className="text-red-500"
className="pl-2 text-sm text-red-500"
/>
<div className="flex items-center bg-white border-2 py-2 sm:py-3 px-2 sm:px-4 rounded-2xl mt-4 sm:mt-6">
<LockIcon />
Expand All @@ -76,14 +79,18 @@ const LoginForm = () => {
<ErrorMessage
component="div"
name="password"
className="text-red-500"
className="pl-2 text-sm text-red-500"
/>
<button
disabled={isSubmitting}
type="submit"
className="block w-full bg-red-600 transition duration-300 ease-in-out hover:bg-red-800 mt-4 py-2 sm:py-3 rounded-2xl text-white font-semibold"
>
Login
{!isSubmitting ? (
"Login"
) : (
<CircularProgress size="1rem" color="inherit" />
)}
</button>
<span className="text-sm sm:text-base text-white ml-2 flex my-3 justify-center hover:text-gray-500 cursor-pointer">
Forgot Password ?
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/MatchesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const MatchesPage = () => {
if (query.isLoading) {
return (
<MatchHeader>
<div className="flex justify-center items-center h-full w-full">
<CircularProgress />
<div className="flex justify-center items-center h-full w-full text-red-500">
<CircularProgress size="3rem" color="inherit" />
</div>
</MatchHeader>
);
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/views/RegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"
) : (
<CircularProgress size="1rem" color="inherit" />
)}
</button>
</div>
</Form>
Expand Down
Loading