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

add real testimonial on feedbackPage #23

Merged
merged 11 commits into from
Mar 29, 2024
72 changes: 56 additions & 16 deletions src/components/feedback/FormFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,41 @@ import { useForm } from '@mantine/form';
import { zodResolver } from 'mantine-form-zod-resolver';
import { z } from 'zod';
import { StarLikeComponent } from '../starComponent/StarLikeComponent';
import useNotify, { NotifyDto } from '../../hooks/useNotify';
import { AuthStore, useAuthStore } from '../../store/useAuthStore';
import { TestimonialsController } from '../../services/BaseApi';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AddTestimonialDto } from '@FullStackMap/from-a2b';

export const FormFeedback = () => {
const MAX_CHARS = 500;

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

const feedbackSchema = z.object({
comment: z
feedBack: z
.string()
.min(10, 'Le commentaire doit contenir au moins 10 caractères'),
rating: z
.min(10, 'Le commentaire doit contenir au moins 10 caractères')
.refine(
(value) => {
return !/\s{6,}/.test(value);
},
{
message:
'Le commentaire ne peut pas contenir plus de 5 espaces de suite',
path: ['feedBack'],
}
)
.refine((value) => /\S/.test(value), {
message:
'Le commentaire ne peut pas être vide ou contenir uniquement des espaces',
path: ['feedBack'],
}),
rate: z
.number()
.min(1, 'La note minimale est de 1')
.max(5, 'La note maximale est de 5'),
Expand All @@ -25,35 +48,51 @@ export const FormFeedback = () => {
const feedbackForm = useForm({
validateInputOnChange: true,
initialValues: {
comment: '',
rating: 0,
feedBack: '',
rate: 0,
testimonialDate: new Date().toISOString().split('T')[0],
},
validate: zodResolver(feedbackSchema),
});

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
const sendTestimonialMutation = useMutation({
mutationFn: async ([id, dto]: [string, AddTestimonialDto]) =>
await TestimonialsController.createTestimonialAsyncPOST(id, dto),
onSuccess: () => {
SuccessNotify({
title: 'Avis envoyé',
message: 'Votre avis a bien été envoyé, merci !',
autoClose: 5000,
} as NotifyDto);
feedbackForm.reset();
queryClient.invalidateQueries({ queryKey: ['testimonials'] });
},
});

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
throw new Error('Not implemented');
if (!feedbackForm.isValid()) return;
sendTestimonialMutation.mutate([userId!, feedbackForm.values]);
};

const handleChangeRating = (rating: number) => {
feedbackForm.setValues({ ...feedbackForm.values, rating });
const handleChangeRating = (rate: number) => {
feedbackForm.setValues({ ...feedbackForm.values, rate });
};

const handleCommentChange = (
event: React.ChangeEvent<HTMLTextAreaElement>
) => {
const comment = event.target.value;
setCharCount(comment.length);
if (comment.length > MAX_CHARS) {
event.target.value = comment.slice(0, MAX_CHARS);
const feedBack = event.target.value;
setCharCount(feedBack.length);
if (feedBack.length > MAX_CHARS) {
event.target.value = feedBack.slice(0, MAX_CHARS);
setCharCount(MAX_CHARS);
}
feedbackForm.setFieldValue('comment', comment.slice(0, MAX_CHARS));
feedbackForm.setFieldValue('feedBack', feedBack.slice(0, MAX_CHARS));
};

return (
<form onSubmit={handleSubmit} onReset={() => feedbackForm.reset()}>
<form onSubmit={handleSubmit}>
<Title order={2} mt={50} ta="center">
Laissez votre avis
</Title>
Expand All @@ -63,7 +102,7 @@ export const FormFeedback = () => {
resize={!isMobile ? 'vertical' : 'none'}
required
placeholder='Ex: "J’ai adoré mon séjour, je recommande vivement !"'
{...feedbackForm.getInputProps('comment')}
{...feedbackForm.getInputProps('feedBack')}
onChange={handleCommentChange}
/>
<Text
Expand All @@ -79,6 +118,7 @@ export const FormFeedback = () => {
<Center mt="xl">
<Button
type="submit"
loading={sendTestimonialMutation.isPending}
disabled={!feedbackForm.isValid()}
variant="filled"
mb="lg">
Expand Down
6 changes: 3 additions & 3 deletions src/components/header/DefaultHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const DefaultHeader = (props: DefaultHeaderProps) => {

return (
<>
<Group py={10} mx={10} justify="space-between">
<Group py={10} mx={2} justify="space-between">
<Burger
opened={props.burgerOpened}
onClick={props.toggleBurgerState}
Expand All @@ -71,8 +71,8 @@ const DefaultHeader = (props: DefaultHeaderProps) => {
alt="Logo du site"
onClick={handleClickLogo}
className="cursor-pointer"
height={50}
width={50}
height={40}
width={40}
/>
<Group>
<OfflineComponent />
Expand Down
180 changes: 98 additions & 82 deletions src/pages/feedback/FeedbackPage.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,57 @@
import { useRef } from 'react';
import { TestimonialDto } from '@FullStackMap/from-a2b';
import { Carousel } from '@mantine/carousel';
import {
Badge,
Card,
Center,
Container,
Title,
Text,
Group,
Loader,
Rating,
Center,
Card,
Badge,
Text,
Title,
} from '@mantine/core';
import { Carousel } from '@mantine/carousel';
import { useQuery } from '@tanstack/react-query';
import Autoplay from 'embla-carousel-autoplay';
import { useEffect, useRef, useState } from 'react';
import { FormFeedback } from '../../components/feedback/FormFeedback';

const reviews = [
{
pseudo: 'John Doe',
date: '2023-02-01',
comment: 'La plateforme est très intuitive, je suis très satisfait !',
rating: 1,
},
{
pseudo: 'Jane Doe',
date: '2023-01-15',
comment:
'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.',
rating: 1,
},
{
pseudo: 'Bob Smith',
date: '2022-12-20',
comment:
'Je suis un peu déçu par la qualité des services proposés, mais le rapport qualité-prix est correct.',
rating: 3,
},
{
pseudo: 'Alice Smith',
date: '2022-12-01',
comment:
'Je recommande vivement cette plateforme, j’ai passé un excellent séjour !',
rating: 5,
},
{
pseudo: 'Eva Brown',
date: '2022-11-15',
comment:
'Je suis très satisfaite de ma réservation, je n’hésiterai pas à revenir !',
rating: 5,
},
];

const averageRating =
reviews.reduce((acc, review) => acc + review.rating, 0) / reviews.length;
import useNotify, { NotifyDto } from '../../hooks/useNotify';
import { AnoTestimonialsController } from '../../services/BaseApi';
import { AuthStore, useAuthStore } from '../../store/useAuthStore';

const FeedbackPage = () => {
const autoplay = useRef(Autoplay({ delay: 5000 }));
const { ErrorNotify } = useNotify();
const isLogged: () => boolean = useAuthStore((s: AuthStore) => s.isLogged);

const { data: reviews } = useQuery({
queryKey: ['Testimonials'],
queryFn: async () => {
return AnoTestimonialsController.getAllTestimonialGET()
.then((res) => res.data)
.catch(() => {
ErrorNotify({
title: "Une erreur s'est produite",
message: 'Impossible de récupérer les avis',
} as NotifyDto);
return [];
});
},
});

const [averageRating, setAverageRating] = useState(0);

useEffect(() => {
if (!reviews) return;

const avgRating: number =
reviews.reduce(
(acc: number, review: TestimonialDto) => acc + review.rate!,
0
) / reviews.length;

setAverageRating(avgRating);
}, [reviews]);

return (
<Container size="md">
Expand All @@ -64,42 +61,55 @@ const FeedbackPage = () => {
<Text mt="sm" ta="center" c="teal" fw={700}>
Découvrez ce que nos clients disent à propos de nous.
</Text>
<Carousel
slideSize="70%"
plugins={[autoplay.current]}
onMouseEnter={autoplay.current.stop}
onMouseLeave={autoplay.current.reset}
height={200}
slideGap="md"
loop
mt="md"
mb="md">
{reviews.map((review) => (
<Carousel.Slide key={review.pseudo}>
<Card shadow="sm" radius="md" withBorder h={190}>
<Card.Section mah="70%">
<Text size="md" m={20}>
{review.comment}
</Text>
</Card.Section>
<Group mt="auto" justify="space-between">
<Group>
<Badge bg="dark">{review.pseudo}</Badge>
<Text size="sm" c="dimmed">
{new Date(review.date).toLocaleDateString()}
{reviews && reviews.length === 0 ? (
<>
<Text ta="center" pt="lg">
Soyez le premier à donner votre avis !
</Text>
</>
) : null}
{reviews && reviews.length > 0 ? (
<Carousel
slideSize="70%"
plugins={[autoplay.current]}
onMouseEnter={autoplay.current.stop}
onMouseLeave={autoplay.current.reset}
height={200}
slideGap="md"
loop
mt="md"
mb="md">
{reviews?.map((review: any) => (
<Carousel.Slide key={review.userId}>
<Card shadow="sm" radius="md" withBorder h={190}>
<Card.Section mah="70%">
<Text size="md" m={20}>
{review.feedBack}
</Text>
</Card.Section>
<Group mt="auto" justify="space-between">
<Group>
<Badge bg="dark">{review.user?.userName}</Badge>
<Text size="sm" c="dimmed">
{review.testimonialDate}
</Text>
</Group>
<Rating
color="teal"
value={review.rate || 0}
readOnly
title="Note attribuée par le client"
/>
</Group>
<Rating
color="teal"
value={review.rating}
readOnly
title="Note attribuée par le client"
/>
</Group>
</Card>
</Carousel.Slide>
))}
</Carousel>
</Card>
</Carousel.Slide>
))}
</Carousel>
) : (
<Center>
<Loader size="xl" mt="xl" />
</Center>
)}
<Title order={3} mt="xl" ta="center">
La note attribuée par nos clients
</Title>
Expand All @@ -112,7 +122,13 @@ const FeedbackPage = () => {
title="Note moyenne"
/>
</Center>
<FormFeedback />
{isLogged() ? (
<FormFeedback />
) : (
<Text ta="center" c="teal" fw="bold">
Connectez-vous pour donner votre avis
</Text>
)}
</Container>
);
};
Expand Down
17 changes: 17 additions & 0 deletions src/services/BaseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ const UserControllerFunc = () => {
return client.UserApiFactory(configLogged);
};

const TestimonialsControllerFunc = () => {
const configLogged = new client.Configuration({
basePath: basePath,
baseOptions: {
headers: {
Authorization: `Bearer ${token}`,
},
},
});

return client.TestimonialApiFactory(configLogged);
};

export const AnoAxiosClient = axios.create({
baseURL: basePath,
});
Expand All @@ -53,4 +66,8 @@ export const TripController = TripControllerFunc();

export const AnoAuthController = client.AuthApiFactory(configAno);

export const AnoTestimonialsController = client.TestimonialApiFactory(configAno);

export const TestimonialsController = TestimonialsControllerFunc();

export const UserController = UserControllerFunc();
Loading