Skip to content

Commit

Permalink
feature: Create matching menu
Browse files Browse the repository at this point in the history
- Deliver fully functional matching menu with all animations
- Integrate Google Places API
- Correct AlertProvidert to handle new format of errors.

Refs: 8696pcz16
Signed-off-by: Patryk Kłosiński <[email protected]>
  • Loading branch information
JimTheCat committed Jan 4, 2025
1 parent af1a166 commit f70bb32
Show file tree
Hide file tree
Showing 14 changed files with 437 additions and 113 deletions.
2 changes: 2 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MeowHub</title>
<script async defer
src="https://maps.googleapis.com/maps/api/js?key=%VITE_GOOGLE_PLACES_API_KEY%&libraries=places"></script>
</head>
<body>
<div id="root"></div>
Expand Down
138 changes: 109 additions & 29 deletions frontend/src/Features/Matching/components/Filters/MatchFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,121 @@
import {Card, Divider, Grid, ScrollArea, Select, Stack, Title} from "@mantine/core";
import {
Box,
Button,
Card,
Divider,
Grid,
Group,
NativeSelect,
RangeSlider,
ScrollArea,
Stack,
Text,
Title
} from "@mantine/core";
import {useState} from "react";
import {IconEye, IconFriends, IconGenderBigender, IconRulerMeasure} from "@tabler/icons-react";
import {distanceOptions, hereForOptions, sexualityOptions, showMeOptions, sliderMarks} from "../../const";
import {useForm} from "@mantine/form";

export const MatchFilters = () => {
const [range, setRange] = useState<{ from: string | number, to: string | number }>({from: 16, to: 50});
const [isChanged, setIsChanged] = useState(false);

const showMeOptions = [
{
label: 'Male',
value: 'male',
},
{
label: 'Female',
value: 'female',
const dummyFiltersData = {
showMe: 'everyone',
prefferedAge: [18, 40],
prefferedDistance: 999,
prefferedSexuality: 'any',
hereFor: 'anything',
}

const form = useForm({
initialValues: {
showMe: dummyFiltersData.showMe,
prefferedAge: dummyFiltersData.prefferedAge,
prefferedDistance: dummyFiltersData.prefferedDistance,
prefferedSexuality: dummyFiltersData.prefferedSexuality,
hereFor: dummyFiltersData.hereFor,
},
{
label: 'Everyone',
value: 'everyone',
onValuesChange: () => {
setIsChanged(true);
},
]
});

return (
<Stack gap={"md"} justify={"center"} py={"lg"} px={"xl"}>
<Card mah={"90vh"} w={'100%'} withBorder component={ScrollArea}>
{/* Filters */}
<Title order={2}>Filters</Title>

<Divider my={"md"}/>
<form
onSubmit={form.onSubmit((values) => console.log(values))}
onReset={(event) => {
form.onReset(event);
setIsChanged(false);
}}
>
<Stack gap={"md"} h={'100%'} align={'center'} justify={"center"} py={"lg"} px={"xl"}>
<Card mah={"90vh"} w={'100%'} withBorder component={ScrollArea}>
{/* Filters */}
<Title order={2}>Filters</Title>

<Grid gutter={0}>
<Grid.Col span={12}>
<Select label={'Show me'} placeholder={'Select'} data={showMeOptions}/>
</Grid.Col>
<Grid.Col span={12}>
<Divider my={"md"}/>

</Grid.Col>
</Grid>
</Card>
</Stack>
<Grid gutter={0} grow>
<Grid.Col span={6} pr={'xs'}>
<NativeSelect
label={'Show me'}
data={showMeOptions}
leftSection={<IconFriends/>}
{...form.getInputProps("showMe")}
/>
</Grid.Col>
<Grid.Col pl={'xs'} span={6}>
<NativeSelect
label={'Sexuality'}
data={sexualityOptions}
leftSection={<IconGenderBigender/>}
{...form.getInputProps("prefferedSexuality")}
/>
</Grid.Col>
<Grid.Col span={12} mt={"md"}>
<Box>
<Text size={"sm"}>Preferred age</Text>
<RangeSlider
mt={'xs'}
marks={sliderMarks}
max={100}
min={16}
minRange={1}
{...form.getInputProps("prefferedAge")}
/>
</Box>
</Grid.Col>
<Grid.Col span={12} mt={"lg"}>
<NativeSelect
label={'Distance'}
data={distanceOptions}
leftSection={<IconRulerMeasure/>}
{...form.getInputProps("prefferedDistance")}
/>
</Grid.Col>
<Grid.Col span={12} mt={"lg"}>
<NativeSelect
label={'Here for'}
data={hereForOptions}
leftSection={<IconEye/>}
{...form.getInputProps("hereFor")}
/>
</Grid.Col>
</Grid>
</Card>
{/* Buttons - show them when change is made */}
{isChanged && (
<Group justify={"space-between"}>
<Button type={"reset"} color="red" radius="lg">
Cancel
</Button>
<Button type={"submit"} color="green" radius="lg">
Apply filters
</Button>
</Group>
)}
</Stack>
</form>
);
}
15 changes: 11 additions & 4 deletions frontend/src/Features/Matching/components/Main/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {ProfileCard} from "./components";
import {useEffect, useState} from "react";

type ProfileDummy = {
id: number;
name: string;
age: number;
location: string;
Expand All @@ -17,9 +18,10 @@ export const Main = () => {
const [isAnimating, setIsAnimating] = useState(false);

useEffect(() => {
// Symulacja pobierania profili
// Dummy data
const newProfiles = [
{
id: 1,
name: "Iza",
age: 23,
location: "London",
Expand All @@ -30,6 +32,7 @@ export const Main = () => {
],
},
{
id: 2,
name: "John",
age: 29,
location: "New York",
Expand All @@ -40,6 +43,7 @@ export const Main = () => {
],
},
{
id: 3,
name: "Alice",
age: 25,
location: "Berlin",
Expand All @@ -50,6 +54,7 @@ export const Main = () => {
],
},
{
id: 4,
name: "Iza",
age: 23,
location: "London",
Expand All @@ -60,6 +65,7 @@ export const Main = () => {
],
},
{
id: 5,
name: "John",
age: 29,
location: "New York",
Expand All @@ -70,6 +76,7 @@ export const Main = () => {
],
},
{
id: 6,
name: "Alice",
age: 25,
location: "Berlin",
Expand All @@ -92,7 +99,7 @@ export const Main = () => {
setCurrentIndex((prev) => prev + 1);
setSwipeDirection("");
setIsAnimating(false);
}, 500); // Czas animacji
}, 500); // Time of animation
};

return (
Expand All @@ -105,11 +112,11 @@ export const Main = () => {
}}
>
{profiles
.slice(currentIndex, currentIndex + 2) // Tylko bieżąca karta i następna
.slice(currentIndex, currentIndex + 2) // Only two profiles at a time
.map((profile, index) => {
return (
<ProfileCard
key={currentIndex + index}
key={profile.id}
profile={profile}
isActive={index === 0}
isNext={index === 1}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Badge, Box, Button, Card, Group, Image, ScrollArea, Stack, Text} from "@mantine/core";
import {Carousel} from "@mantine/carousel";
import {Carousel, Embla, useAnimationOffsetEffect} from "@mantine/carousel";
import {IconCheck, IconX} from "@tabler/icons-react";
import {useEffect, useState} from "react";
import classes from "./carousel.module.css";
Expand Down Expand Up @@ -27,13 +27,17 @@ export const ProfileCard = ({
}: ProfileProps) => {
const [isAnimating, setIsAnimating] = useState(false);
const [styles, setStyles] = useState({
transform: "translate(-50%, -50%) scale(1)",
transform: "translate(-50%, -50%) scale(0.9)",
opacity: 1,
});
const [embla, setEmbla] = useState<Embla | null>(null);
const DURATION = 500;

useAnimationOffsetEffect(embla, DURATION);

useEffect(() => {
if (swipeDirection) {
// Animacja przesuwania aktywnej karty
// Animation for swiping
const directionTransform =
swipeDirection === "left"
? "translate(-150%, -50%)"
Expand All @@ -45,32 +49,48 @@ export const ProfileCard = ({
opacity: 0,
});

// Po zakończeniu animacji wykonaj swipe i zresetuj styl
// Reset styles after animation
const timeout = setTimeout(() => {
handleSwipe(swipeDirection);
setIsAnimating(false);
setStyles({
transform: "translate(-50%, -50%) scale(0.9)", // Nowa karta wychodzi spod poprzedniej
transform: "translate(-50%, -50%) scale(0.9)", // Default scale
opacity: 1,
});
}, 500);

return () => clearTimeout(timeout);
} else if (isNext) {
// Skalowanie nowej karty, która czeka na swoją kolej
setStyles({
// Animation for next card
setStyles((prevStyles) => ({
...prevStyles,
transform: "translate(-50%, -50%) scale(0.9)",
opacity: 1,
});
}));
} else if (isActive) {
// Reset do domyślnego stanu dla aktywnej karty
// Animation for active card
setStyles({
transform: "translate(-50%, -50%) scale(1)",
opacity: 1,
});
}

const timeout = setTimeout(() => {
if (embla) {
embla.reInit();
}
}, 300); // Run after the animation
return () => clearTimeout(timeout);
}, [swipeDirection, isActive, isNext, handleSwipe]);

const handleZIndex = () => {
if (isActive) {
return 100;
}

return isNext ? 50 : 1;
}

return (
<Card
pos={'absolute'}
Expand All @@ -84,26 +104,26 @@ export const ProfileCard = ({
transition: isAnimating
? "transform 0.5s ease, opacity 0.5s ease"
: "transform 0.3s ease, opacity 0.3s ease",
zIndex: isActive ? 10 : isNext ? 5 : 1,
zIndex: handleZIndex(),
boxShadow: "0px 8px 16px rgba(0, 0, 0, 0.2)",
overflow: "hidden",
'--card-padding': '0',
}}
component={ScrollArea}
>
<Box mah="calc(90dvh - 80px)">
{/* Sekcja karuzeli */}
{/* Carousel section */}
<Card.Section h="70vh">
<Carousel classNames={classes} withIndicators>
<Carousel classNames={classes} getEmblaApi={setEmbla} withIndicators>
{profile.photos.map((photo) => (
<Carousel.Slide key={photo.index}>
<Image src={photo.url} alt="Profile photo" h="70vh"/>
<Carousel.Slide key={photo.index} pos={'relative'} h="70vh">
<Image src={photo.url} alt="Profile photo" h={'100%'}/>
</Carousel.Slide>
))}
</Carousel>
</Card.Section>

{/* Sekcja informacji użytkownika */}
{/* Profile information */}
<Stack gap="xs" p="md">
<Group justify="apart">
<Text size="xl" fw={700}>
Expand All @@ -120,7 +140,7 @@ export const ProfileCard = ({
</Stack>
</Box>

{/* Sekcja przycisków */}
{/* Swipe buttons */}
<Group
justify="space-evenly"
pos={'absolute'}
Expand Down
Loading

0 comments on commit f70bb32

Please sign in to comment.