Skip to content

Commit aaa9728

Browse files
authored
Merge pull request #23 from FullStackMap/features/get-testimonials
add real testimonial on feedbackPage
2 parents da7c881 + bc764b9 commit aaa9728

File tree

4 files changed

+174
-101
lines changed

4 files changed

+174
-101
lines changed

src/components/feedback/FormFeedback.tsx

+56-16
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,41 @@ import { useForm } from '@mantine/form';
55
import { zodResolver } from 'mantine-form-zod-resolver';
66
import { z } from 'zod';
77
import { StarLikeComponent } from '../starComponent/StarLikeComponent';
8+
import useNotify, { NotifyDto } from '../../hooks/useNotify';
9+
import { AuthStore, useAuthStore } from '../../store/useAuthStore';
10+
import { TestimonialsController } from '../../services/BaseApi';
11+
import { useMutation, useQueryClient } from '@tanstack/react-query';
12+
import { AddTestimonialDto } from '@FullStackMap/from-a2b';
813

914
export const FormFeedback = () => {
1015
const MAX_CHARS = 500;
1116

1217
const [charCount, setCharCount] = useState(0);
1318
const isMobile = useMediaQuery('(max-width: 768px)');
19+
const { SuccessNotify } = useNotify();
20+
const queryClient = useQueryClient();
21+
const userId: string | undefined = useAuthStore((s: AuthStore) => s.user?.Id);
1422

1523
const feedbackSchema = z.object({
16-
comment: z
24+
feedBack: z
1725
.string()
18-
.min(10, 'Le commentaire doit contenir au moins 10 caractères'),
19-
rating: z
26+
.min(10, 'Le commentaire doit contenir au moins 10 caractères')
27+
.refine(
28+
(value) => {
29+
return !/\s{6,}/.test(value);
30+
},
31+
{
32+
message:
33+
'Le commentaire ne peut pas contenir plus de 5 espaces de suite',
34+
path: ['feedBack'],
35+
}
36+
)
37+
.refine((value) => /\S/.test(value), {
38+
message:
39+
'Le commentaire ne peut pas être vide ou contenir uniquement des espaces',
40+
path: ['feedBack'],
41+
}),
42+
rate: z
2043
.number()
2144
.min(1, 'La note minimale est de 1')
2245
.max(5, 'La note maximale est de 5'),
@@ -25,35 +48,51 @@ export const FormFeedback = () => {
2548
const feedbackForm = useForm({
2649
validateInputOnChange: true,
2750
initialValues: {
28-
comment: '',
29-
rating: 0,
51+
feedBack: '',
52+
rate: 0,
53+
testimonialDate: new Date().toISOString().split('T')[0],
3054
},
3155
validate: zodResolver(feedbackSchema),
3256
});
3357

34-
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
58+
const sendTestimonialMutation = useMutation({
59+
mutationFn: async ([id, dto]: [string, AddTestimonialDto]) =>
60+
await TestimonialsController.createTestimonialAsyncPOST(id, dto),
61+
onSuccess: () => {
62+
SuccessNotify({
63+
title: 'Avis envoyé',
64+
message: 'Votre avis a bien été envoyé, merci !',
65+
autoClose: 5000,
66+
} as NotifyDto);
67+
feedbackForm.reset();
68+
queryClient.invalidateQueries({ queryKey: ['testimonials'] });
69+
},
70+
});
71+
72+
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
3573
event.preventDefault();
36-
throw new Error('Not implemented');
74+
if (!feedbackForm.isValid()) return;
75+
sendTestimonialMutation.mutate([userId!, feedbackForm.values]);
3776
};
3877

39-
const handleChangeRating = (rating: number) => {
40-
feedbackForm.setValues({ ...feedbackForm.values, rating });
78+
const handleChangeRating = (rate: number) => {
79+
feedbackForm.setValues({ ...feedbackForm.values, rate });
4180
};
4281

4382
const handleCommentChange = (
4483
event: React.ChangeEvent<HTMLTextAreaElement>
4584
) => {
46-
const comment = event.target.value;
47-
setCharCount(comment.length);
48-
if (comment.length > MAX_CHARS) {
49-
event.target.value = comment.slice(0, MAX_CHARS);
85+
const feedBack = event.target.value;
86+
setCharCount(feedBack.length);
87+
if (feedBack.length > MAX_CHARS) {
88+
event.target.value = feedBack.slice(0, MAX_CHARS);
5089
setCharCount(MAX_CHARS);
5190
}
52-
feedbackForm.setFieldValue('comment', comment.slice(0, MAX_CHARS));
91+
feedbackForm.setFieldValue('feedBack', feedBack.slice(0, MAX_CHARS));
5392
};
5493

5594
return (
56-
<form onSubmit={handleSubmit} onReset={() => feedbackForm.reset()}>
95+
<form onSubmit={handleSubmit}>
5796
<Title order={2} mt={50} ta="center">
5897
Laissez votre avis
5998
</Title>
@@ -63,7 +102,7 @@ export const FormFeedback = () => {
63102
resize={!isMobile ? 'vertical' : 'none'}
64103
required
65104
placeholder='Ex: "J’ai adoré mon séjour, je recommande vivement !"'
66-
{...feedbackForm.getInputProps('comment')}
105+
{...feedbackForm.getInputProps('feedBack')}
67106
onChange={handleCommentChange}
68107
/>
69108
<Text
@@ -79,6 +118,7 @@ export const FormFeedback = () => {
79118
<Center mt="xl">
80119
<Button
81120
type="submit"
121+
loading={sendTestimonialMutation.isPending}
82122
disabled={!feedbackForm.isValid()}
83123
variant="filled"
84124
mb="lg">

src/components/header/DefaultHeader.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const DefaultHeader = (props: DefaultHeaderProps) => {
5959

6060
return (
6161
<>
62-
<Group py={10} mx={10} justify="space-between">
62+
<Group py={10} mx={2} justify="space-between">
6363
<Burger
6464
opened={props.burgerOpened}
6565
onClick={props.toggleBurgerState}
@@ -71,8 +71,8 @@ const DefaultHeader = (props: DefaultHeaderProps) => {
7171
alt="Logo du site"
7272
onClick={handleClickLogo}
7373
className="cursor-pointer"
74-
height={50}
75-
width={50}
74+
height={40}
75+
width={40}
7676
/>
7777
<Group>
7878
<OfflineComponent />

src/pages/feedback/FeedbackPage.tsx

+98-82
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,57 @@
1-
import { useRef } from 'react';
1+
import { TestimonialDto } from '@FullStackMap/from-a2b';
2+
import { Carousel } from '@mantine/carousel';
23
import {
4+
Badge,
5+
Card,
6+
Center,
37
Container,
4-
Title,
5-
Text,
68
Group,
9+
Loader,
710
Rating,
8-
Center,
9-
Card,
10-
Badge,
11+
Text,
12+
Title,
1113
} from '@mantine/core';
12-
import { Carousel } from '@mantine/carousel';
14+
import { useQuery } from '@tanstack/react-query';
1315
import Autoplay from 'embla-carousel-autoplay';
16+
import { useEffect, useRef, useState } from 'react';
1417
import { FormFeedback } from '../../components/feedback/FormFeedback';
15-
16-
const reviews = [
17-
{
18-
pseudo: 'John Doe',
19-
date: '2023-02-01',
20-
comment: 'La plateforme est très intuitive, je suis très satisfait !',
21-
rating: 1,
22-
},
23-
{
24-
pseudo: 'Jane Doe',
25-
date: '2023-01-15',
26-
comment:
27-
'J’ai eu un problème avec ma réservation, mais le service client a été très réactif et m’a aidé à trouver une solution.',
28-
rating: 1,
29-
},
30-
{
31-
pseudo: 'Bob Smith',
32-
date: '2022-12-20',
33-
comment:
34-
'Je suis un peu déçu par la qualité des services proposés, mais le rapport qualité-prix est correct.',
35-
rating: 3,
36-
},
37-
{
38-
pseudo: 'Alice Smith',
39-
date: '2022-12-01',
40-
comment:
41-
'Je recommande vivement cette plateforme, j’ai passé un excellent séjour !',
42-
rating: 5,
43-
},
44-
{
45-
pseudo: 'Eva Brown',
46-
date: '2022-11-15',
47-
comment:
48-
'Je suis très satisfaite de ma réservation, je n’hésiterai pas à revenir !',
49-
rating: 5,
50-
},
51-
];
52-
53-
const averageRating =
54-
reviews.reduce((acc, review) => acc + review.rating, 0) / reviews.length;
18+
import useNotify, { NotifyDto } from '../../hooks/useNotify';
19+
import { AnoTestimonialsController } from '../../services/BaseApi';
20+
import { AuthStore, useAuthStore } from '../../store/useAuthStore';
5521

5622
const FeedbackPage = () => {
5723
const autoplay = useRef(Autoplay({ delay: 5000 }));
24+
const { ErrorNotify } = useNotify();
25+
const isLogged: () => boolean = useAuthStore((s: AuthStore) => s.isLogged);
26+
27+
const { data: reviews } = useQuery({
28+
queryKey: ['Testimonials'],
29+
queryFn: async () => {
30+
return AnoTestimonialsController.getAllTestimonialGET()
31+
.then((res) => res.data)
32+
.catch(() => {
33+
ErrorNotify({
34+
title: "Une erreur s'est produite",
35+
message: 'Impossible de récupérer les avis',
36+
} as NotifyDto);
37+
return [];
38+
});
39+
},
40+
});
41+
42+
const [averageRating, setAverageRating] = useState(0);
43+
44+
useEffect(() => {
45+
if (!reviews) return;
46+
47+
const avgRating: number =
48+
reviews.reduce(
49+
(acc: number, review: TestimonialDto) => acc + review.rate!,
50+
0
51+
) / reviews.length;
52+
53+
setAverageRating(avgRating);
54+
}, [reviews]);
5855

5956
return (
6057
<Container size="md">
@@ -64,42 +61,55 @@ const FeedbackPage = () => {
6461
<Text mt="sm" ta="center" c="teal" fw={700}>
6562
Découvrez ce que nos clients disent à propos de nous.
6663
</Text>
67-
<Carousel
68-
slideSize="70%"
69-
plugins={[autoplay.current]}
70-
onMouseEnter={autoplay.current.stop}
71-
onMouseLeave={autoplay.current.reset}
72-
height={200}
73-
slideGap="md"
74-
loop
75-
mt="md"
76-
mb="md">
77-
{reviews.map((review) => (
78-
<Carousel.Slide key={review.pseudo}>
79-
<Card shadow="sm" radius="md" withBorder h={190}>
80-
<Card.Section mah="70%">
81-
<Text size="md" m={20}>
82-
{review.comment}
83-
</Text>
84-
</Card.Section>
85-
<Group mt="auto" justify="space-between">
86-
<Group>
87-
<Badge bg="dark">{review.pseudo}</Badge>
88-
<Text size="sm" c="dimmed">
89-
{new Date(review.date).toLocaleDateString()}
64+
{reviews && reviews.length === 0 ? (
65+
<>
66+
<Text ta="center" pt="lg">
67+
Soyez le premier à donner votre avis !
68+
</Text>
69+
</>
70+
) : null}
71+
{reviews && reviews.length > 0 ? (
72+
<Carousel
73+
slideSize="70%"
74+
plugins={[autoplay.current]}
75+
onMouseEnter={autoplay.current.stop}
76+
onMouseLeave={autoplay.current.reset}
77+
height={200}
78+
slideGap="md"
79+
loop
80+
mt="md"
81+
mb="md">
82+
{reviews?.map((review: any) => (
83+
<Carousel.Slide key={review.userId}>
84+
<Card shadow="sm" radius="md" withBorder h={190}>
85+
<Card.Section mah="70%">
86+
<Text size="md" m={20}>
87+
{review.feedBack}
9088
</Text>
89+
</Card.Section>
90+
<Group mt="auto" justify="space-between">
91+
<Group>
92+
<Badge bg="dark">{review.user?.userName}</Badge>
93+
<Text size="sm" c="dimmed">
94+
{review.testimonialDate}
95+
</Text>
96+
</Group>
97+
<Rating
98+
color="teal"
99+
value={review.rate || 0}
100+
readOnly
101+
title="Note attribuée par le client"
102+
/>
91103
</Group>
92-
<Rating
93-
color="teal"
94-
value={review.rating}
95-
readOnly
96-
title="Note attribuée par le client"
97-
/>
98-
</Group>
99-
</Card>
100-
</Carousel.Slide>
101-
))}
102-
</Carousel>
104+
</Card>
105+
</Carousel.Slide>
106+
))}
107+
</Carousel>
108+
) : (
109+
<Center>
110+
<Loader size="xl" mt="xl" />
111+
</Center>
112+
)}
103113
<Title order={3} mt="xl" ta="center">
104114
La note attribuée par nos clients
105115
</Title>
@@ -112,7 +122,13 @@ const FeedbackPage = () => {
112122
title="Note moyenne"
113123
/>
114124
</Center>
115-
<FormFeedback />
125+
{isLogged() ? (
126+
<FormFeedback />
127+
) : (
128+
<Text ta="center" c="teal" fw="bold">
129+
Connectez-vous pour donner votre avis
130+
</Text>
131+
)}
116132
</Container>
117133
);
118134
};

src/services/BaseApi.ts

+17
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ const UserControllerFunc = () => {
3434
return client.UserApiFactory(configLogged);
3535
};
3636

37+
const TestimonialsControllerFunc = () => {
38+
const configLogged = new client.Configuration({
39+
basePath: basePath,
40+
baseOptions: {
41+
headers: {
42+
Authorization: `Bearer ${token}`,
43+
},
44+
},
45+
});
46+
47+
return client.TestimonialApiFactory(configLogged);
48+
};
49+
3750
export const AnoAxiosClient = axios.create({
3851
baseURL: basePath,
3952
});
@@ -53,4 +66,8 @@ export const TripController = TripControllerFunc();
5366

5467
export const AnoAuthController = client.AuthApiFactory(configAno);
5568

69+
export const AnoTestimonialsController = client.TestimonialApiFactory(configAno);
70+
71+
export const TestimonialsController = TestimonialsControllerFunc();
72+
5673
export const UserController = UserControllerFunc();

0 commit comments

Comments
 (0)