Compare commits
14 Commits
3e1a2f6b5b
...
39b9f05cff
| Author | SHA1 | Date |
|---|---|---|
|
|
39b9f05cff | |
|
|
28c1832001 | |
|
|
9df7b8bc4f | |
|
|
919ac7012a | |
|
|
ddfdc29c1f | |
|
|
0b8d60fa46 | |
|
|
eb286154c4 | |
|
|
125ce7aa7a | |
|
|
b6bda6e436 | |
|
|
d69f31addc | |
|
|
ae743c074d | |
|
|
c927f3aced | |
|
|
83c1b5685a | |
|
|
e420e01e4f |
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/icon" href="/favicon.ico" />
|
||||||
<title>Groupomania</title>
|
<title>Groupomania</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full">
|
<body class="h-full">
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
|
|
@ -25,7 +25,7 @@ const AppHeader = () => {
|
||||||
<header className="sticky top-0 z-100 w-full shadow-sm shadow-slate-900">
|
<header className="sticky top-0 z-100 w-full shadow-sm shadow-slate-900">
|
||||||
<div className="min-h-80 flex items-center justify-between bg-grey-dark p-2">
|
<div className="min-h-80 flex items-center justify-between bg-grey-dark p-2">
|
||||||
<div className="app-header__logo flex items-center gap-3">
|
<div className="app-header__logo flex items-center gap-3">
|
||||||
<img src={logo} alt="logo" className="h-14" />
|
<img src={logo} alt="Logo du site" className="h-14" />
|
||||||
<span className="w-0 transition-all duration-500 sm:w-full flex-shrink overflow-hidden text-red font-bold text-4xl">Groupomania</span>
|
<span className="w-0 transition-all duration-500 sm:w-full flex-shrink overflow-hidden text-red font-bold text-4xl">Groupomania</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="right">
|
<div className="right">
|
||||||
|
|
|
||||||
|
|
@ -6,28 +6,21 @@ const Avatar = ({ user }: any) => {
|
||||||
const initials = user.firstName[0] + user.lastName[0];
|
const initials = user.firstName[0] + user.lastName[0];
|
||||||
const gravatarUrl = gravatar.url(user.email, { s: '64', r: 'x', d: '404' }, true);
|
const gravatarUrl = gravatar.url(user.email, { s: '64', r: 'x', d: '404' }, true);
|
||||||
const avatarUi = `https://ui-avatars.com/api/?name=${initials}&background=0D8ABC&color=fff&size=64`;
|
const avatarUi = `https://ui-avatars.com/api/?name=${initials}&background=0D8ABC&color=fff&size=64`;
|
||||||
const [avatar, setAvatar] = useState(avatarUi);
|
const [error, setError] = useState(false);
|
||||||
const [firstLoad, setFirstLoad] = useState(true);
|
|
||||||
|
|
||||||
if (firstLoad) {
|
const handleError = (err: any) => {
|
||||||
fetch(gravatarUrl)
|
if (!error) {
|
||||||
.then((response) => {
|
setError(true);
|
||||||
if (response.ok) {
|
}
|
||||||
setAvatar(gravatarUrl);
|
};
|
||||||
}
|
|
||||||
setFirstLoad(false);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
setFirstLoad(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="avatar shrink-0">
|
<div className="avatar shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={avatar}
|
src={!error ? gravatarUrl : avatarUi}
|
||||||
alt="avatar"
|
alt={`avatar ${user.firstName} ${user.lastName}`}
|
||||||
className="rounded-full w-12 h-12 md:w-16 md:h-16 transition-all cursor-pointer"
|
className="rounded-full w-12 h-12 md:w-16 md:h-16 transition-all cursor-pointer"
|
||||||
|
onError={handleError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,17 @@ import { useState } from 'react';
|
||||||
import { FaTimes } from 'react-icons/fa';
|
import { FaTimes } from 'react-icons/fa';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
||||||
const Image = ({ src, alt, className }: { src: string; alt?: string; className?: string }) => {
|
const Image = ({ src, alt, className, onError }: { src: string; alt?: string; className?: string, onError?: (err: any) => void }) => {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
|
|
||||||
const onError = () => {
|
const handleError = (err: any) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
|
if (onError) {
|
||||||
|
onError(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
|
|
@ -26,18 +29,18 @@ const Image = ({ src, alt, className }: { src: string; alt?: string; className?:
|
||||||
onClick={() => setModal(!modal)}
|
onClick={() => setModal(!modal)}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={error ? 'https://via.placeholder.com/150' : src}
|
src={(error && onError === undefined) ? 'https://via.placeholder.com/150' : src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
onError={onError}
|
onError={handleError}
|
||||||
className={className}
|
className={className}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<img
|
<img
|
||||||
src={error ? 'https://via.placeholder.com/150' : src}
|
src={(error && onError === undefined) ? 'https://via.placeholder.com/150' : src}
|
||||||
alt={alt}
|
alt={(error && onError === undefined) ? 'Image absente' : alt}
|
||||||
onError={onError}
|
onError={handleError}
|
||||||
className={className}
|
className={className}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,38 @@
|
||||||
import { FaThumbsUp } from 'react-icons/fa';
|
import { FaThumbsUp } from 'react-icons/fa';
|
||||||
import { likePost, unlikePost } from '@controllers/LikeController';
|
import { likePost, unlikePost } from '@controllers/LikeController';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { toastError, toastSuccess } from '@controllers/Toasts';
|
import { toastError, toastSuccess } from '@controllers/Toasts';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getMeInfo } from '@controllers/UserController';
|
||||||
|
|
||||||
const Like = ({ messageId, isLiked }: { messageId: string; isLiked: boolean }) => {
|
const Like = ({ message }: { message: any}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [liked, setLiked] = useState(false);
|
||||||
|
const me = useQuery(['me'], getMeInfo, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toastError(error as string);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const mutateLike = useMutation(isLiked ? unlikePost : likePost, {
|
useEffect(() => {
|
||||||
|
if (message.likedBy.some((like: any) => like.userId === me.data?.id)) {
|
||||||
|
setLiked(true);
|
||||||
|
}
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
const mutateLike = useMutation(liked ? unlikePost : likePost, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries(['messages']);
|
queryClient.invalidateQueries(['messages']);
|
||||||
isLiked ? toastSuccess('Message aimé') : null;
|
if (data.message === 'Post liked') {
|
||||||
|
setLiked(true);
|
||||||
|
toastSuccess('Message aimé');
|
||||||
|
} else {
|
||||||
|
setLiked(false);
|
||||||
|
toastSuccess('Message non aimé');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toastError(error as string);
|
toastError(error as string);
|
||||||
|
|
@ -17,15 +40,18 @@ const Like = ({ messageId, isLiked }: { messageId: string; isLiked: boolean }) =
|
||||||
});
|
});
|
||||||
|
|
||||||
const like = () => {
|
const like = () => {
|
||||||
mutateLike.mutate(messageId);
|
mutateLike.mutate(message.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="absolute -bottom-10 right-0 mb-2 rounded-full bg-grey-dark shadow-lg shadow-slate-900 cursor-pointer"
|
className="absolute -bottom-10 right-0 mb-2 rounded-full bg-grey-dark shadow-lg shadow-slate-900 cursor-pointer"
|
||||||
onClick={like}
|
onClick={like}
|
||||||
|
name="like"
|
||||||
>
|
>
|
||||||
<FaThumbsUp className={'fill-red-light text-xl w-10 h-10 p-2.5' + (isLiked ? ' fill-red' : '')} />
|
<FaThumbsUp className={'fill-red-light text-xl w-10 h-10 p-2.5' + (liked ? ' fill-red' : '')} />
|
||||||
|
{message.likes > 0 && <span className="absolute -top-2 right-0 text-white rounded-full bg-red w-5 h-5 text-xs text-center p-0">{message.likes}</span>}
|
||||||
|
<span className="sr-only">Aimer</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||||
import { toastError } from '@controllers/Toasts';
|
import { toastError } from '@controllers/Toasts';
|
||||||
import User from './User';
|
import User from './User';
|
||||||
import Image from './Image';
|
import Image from './Image';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
const Text = ({ text }: { text: string }) => {
|
const Text = ({ text }: { text: string }) => {
|
||||||
if (text === '') {
|
if (text === '') {
|
||||||
|
|
@ -52,14 +53,7 @@ const Message = ({ message }: any) => {
|
||||||
</div>
|
</div>
|
||||||
{message.edited && <div className="text-grey-light italic">Modifié</div>}
|
{message.edited && <div className="text-grey-light italic">Modifié</div>}
|
||||||
</div>
|
</div>
|
||||||
{me.data?.id === message.author.id ? null : (
|
{me.data?.id === message.author.id ? null : <Like message={message} />}
|
||||||
<Like
|
|
||||||
messageId={message.id}
|
|
||||||
isLiked={
|
|
||||||
message.likes > 0 && message.likedBy.find((like: any) => like.userId === me.data?.id) ? true : false
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaPlus } from 'react-icons/fa';
|
import { FaPlus, FaTimes } from 'react-icons/fa';
|
||||||
import {newMessage} from '@controllers/MessageController';
|
import { newMessage } from '@controllers/MessageController';
|
||||||
|
|
||||||
const NewMessage = () => {
|
const NewMessage = () => {
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [image, setImage] = useState('');
|
const [image, setImage] = useState('');
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [preview, setPreview] = useState('');
|
||||||
|
|
||||||
|
const handleImageSelect = (e: any) => {
|
||||||
|
setImage(e.target.value);
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const src = URL.createObjectURL(file);
|
||||||
|
setPreview(src);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { mutate: send } = useMutation(newMessage, {
|
const { mutate: send } = useMutation(newMessage, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -26,36 +36,69 @@ const NewMessage = () => {
|
||||||
send(data);
|
send(data);
|
||||||
setMessage('');
|
setMessage('');
|
||||||
setImage('');
|
setImage('');
|
||||||
|
setPreview('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveImage = () => {
|
||||||
|
setImage('');
|
||||||
|
setPreview('');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="new-message z-10 flex justify-center w-full">
|
<footer className="new-message z-10 flex justify-center w-full">
|
||||||
<form onSubmit={handleSubmit} className="flex gap-2 bg-grey-dark rounded-xl w-full max-w-3xl p-2 mx-2 sm:p-3 md:mx-0 shadow-md shadow-grey-dark">
|
<div className="w-full max-w-3xl">
|
||||||
<div className="file">
|
{preview !== '' && (
|
||||||
<label htmlFor="image" className="cursor-pointer block p-2">
|
<div className="image-preview w-fit rounded-xl bg-grey-dark p-3 absolute bottom-20 shadow-md shadow-grey-dark">
|
||||||
<div className="rounded-full text-grey-dark bg-red-light text-lg p-2">
|
<img src={preview} alt="Image" className="rounded-lg max-w-xs max-h-48" />
|
||||||
<FaPlus className="" />
|
<span className="rounded-full text-grey-light bg-grey-dark text-lg p-2 block absolute -top-4 -right-4 cursor-pointer shadow-md shadow-slate-800 hover:text-grey-dark hover:bg-grey-light transition-all" onClick={handleRemoveImage}>
|
||||||
</div>
|
<FaTimes className="" />
|
||||||
</label>
|
</span>
|
||||||
<input type="file" name="image" id="image" accept="image/*" className="hidden" value={image} onChange={e => setImage(e.target.value)} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
<form
|
||||||
<input
|
onSubmit={handleSubmit}
|
||||||
type="text"
|
className="flex gap-2 flex-wrap bg-grey-dark rounded-xl p-2 mx-2 sm:p-3 md:mx-0 shadow-md shadow-grey-dark"
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
placeholder="Message"
|
|
||||||
className="bg-grey-dark text-white rounded-xl p-2.5 w-full placeholder-red-light"
|
|
||||||
id='content'
|
|
||||||
name='content'
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="rounded-md border border-red bg-red py-2 px-4 text-sm font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
|
|
||||||
>
|
>
|
||||||
Envoyer
|
<div className="file">
|
||||||
</button>
|
<label htmlFor="image" className="cursor-pointer block p-2">
|
||||||
</form>
|
<span className="rounded-full text-grey-dark bg-red-light text-lg p-2 block">
|
||||||
|
<FaPlus className="" />
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">Ajouter une image</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="image"
|
||||||
|
id="image"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
{...(image !== '' && { value: image })}
|
||||||
|
onChange={handleImageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="new-message flex-grow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
placeholder="Message"
|
||||||
|
className="bg-grey-dark text-white rounded-xl p-2.5 w-full placeholder-red-light"
|
||||||
|
id="content"
|
||||||
|
name="content"
|
||||||
|
/>
|
||||||
|
<label htmlFor="content" className="sr-only">
|
||||||
|
Message
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-md border border-red bg-red py-2 px-4 text-sm font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Envoyer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,12 @@ const ScrollToBottom = ({ children, className = '' }: { children: ReactNode; cla
|
||||||
<button
|
<button
|
||||||
onClick={() => node?.scrollIntoView({ behavior: 'smooth' })}
|
onClick={() => node?.scrollIntoView({ behavior: 'smooth' })}
|
||||||
className={'absolute right-3 ' + (show ? 'animate-show bottom-3 block' : 'animate-hide hidden bottom-0 -mb-10')}
|
className={'absolute right-3 ' + (show ? 'animate-show bottom-3 block' : 'animate-hide hidden bottom-0 -mb-10')}
|
||||||
|
name="bottom"
|
||||||
>
|
>
|
||||||
<div className="popup-btn cursor-pointer rounded-full shadow-lg shadow-slate-900 bg-grey-dark hover:bg-grey-light transition-all">
|
<span className="popup-btn block cursor-pointer rounded-full shadow-lg shadow-slate-900 bg-grey-dark hover:bg-grey-light transition-all">
|
||||||
<FaChevronDown className="fill-grey-light hover:fill-grey-dark transition-all text-xl w-10 h-10 p-2.5" />
|
<FaChevronDown className="fill-grey-light hover:fill-grey-dark transition-all text-xl w-10 h-10 p-2.5" />
|
||||||
</div>
|
</span>
|
||||||
|
<span className='sr-only'>Descendre en bas de la page</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ const User = ({ author }: any) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div
|
<div
|
||||||
className="rounded-md border py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 bg-transparent hover:text-red"
|
className="rounded-md border py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white focus:outline-none focus:ring-2 focus:ring-offset-2 bg-transparent hover:text-red cursor-pointer"
|
||||||
onClick={(e) => setShow(false)}
|
onClick={(e) => setShow(false)}
|
||||||
>
|
>
|
||||||
Annuler
|
Annuler
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,15 @@ const Login = () => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [cookies, setCookie] = useCookies(['token']);
|
const [cookies, setCookie] = useCookies(['token']);
|
||||||
|
const [keepMeLoggedIn, setKeepMeLoggedIn] = useState(false);
|
||||||
|
|
||||||
const mutation = useMutation(login, {
|
const mutation = useMutation(login, {
|
||||||
onSuccess: (data: Token) => {
|
onSuccess: (data: Token) => {
|
||||||
setCookie('token', data.token, { path: '/', expires: new Date(data.expiresAt) });
|
setCookie('token', data.token, { path: '/', expires: keepMeLoggedIn ? new Date(data.expiresAt) : undefined });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toastError(error as string);
|
toastError(error as string);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
|
@ -68,6 +69,19 @@ const Login = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<input
|
||||||
|
id="remember_me"
|
||||||
|
name="remember_me"
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 text-red focus:ring-red border-gray-300 rounded"
|
||||||
|
checked={keepMeLoggedIn}
|
||||||
|
onChange={(e) => setKeepMeLoggedIn(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="remember_me" className="ml-2 block text-sm text-white">
|
||||||
|
Se souvenir de moi
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,38 @@ import { Token } from '@/types';
|
||||||
import logo from '@assets/images/logo.svg';
|
import logo from '@assets/images/logo.svg';
|
||||||
import { toastError, toastSuccess } from '@controllers/Toasts';
|
import { toastError, toastSuccess } from '@controllers/Toasts';
|
||||||
import { signup } from '@controllers/UserController';
|
import { signup } from '@controllers/UserController';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const checkPasswordComplexity = (password: string) => {
|
||||||
|
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\da-zA-Z]).{8,}$/;
|
||||||
|
return regex.test(password);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkEmailValidity = (email: string) => {
|
||||||
|
const regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
|
||||||
|
return regex.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target as HTMLFormElement);
|
const data = new FormData(e.target as HTMLFormElement);
|
||||||
|
const email = data.get('email') as string;
|
||||||
|
const password = data.get('password') as string;
|
||||||
|
const passwordConfirmation = data.get('password-confirm') as string;
|
||||||
|
const firstName = data.get('firstName') as string;
|
||||||
|
const lastName = data.get('lastName') as string;
|
||||||
|
|
||||||
|
if (!/^[a-zA-Z]+$/.test(firstName)) return toastError('Le prénom ne doit contenir que des lettres.');
|
||||||
|
if (!/^[a-zA-Z]+$/.test(lastName)) return toastError('Le nom ne doit contenir que des lettres.');
|
||||||
|
if (!checkEmailValidity(email)) return toastError('L\'adresse e-mail n\'est pas valide.');
|
||||||
|
if (password !== passwordConfirmation) return toastError('Les mots de passe ne correspondent pas.');
|
||||||
|
if (!checkPasswordComplexity(password)) return toastError('Le mot de passe doit contenir au moins 8 caractères, une majuscule, une minuscule, un chiffre et un caractère spécial.');
|
||||||
|
|
||||||
signup(data).then((data: Token) => {
|
signup(data).then((data: Token) => {
|
||||||
toastSuccess('You have successfully signed up!');
|
toastSuccess('Vous êtes inscrit !');
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
toastError(err as string);
|
toastError(err as string);
|
||||||
|
|
@ -23,8 +46,8 @@ export default () => {
|
||||||
<div>
|
<div>
|
||||||
<img className="mx-auto h-20 pb-2 w-auto" src={logo} alt="Groupomania" />
|
<img className="mx-auto h-20 pb-2 w-auto" src={logo} alt="Groupomania" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full max-w-md space-y-8 bg-grey rounded-lg p-5">
|
<div className="w-full max-w-md space-y-4 bg-grey rounded-lg p-5">
|
||||||
<form className="m-6 " action="#" method="POST" onSubmit={(e) => onSubmit(e)}>
|
<form className="m-6 mb-0" action="#" method="POST" onSubmit={(e) => onSubmit(e)}>
|
||||||
<div className="-space-y-px rounded-md shadow-sm">
|
<div className="-space-y-px rounded-md shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="lastName" className="sr-only">
|
<label htmlFor="lastName" className="sr-only">
|
||||||
|
|
@ -106,6 +129,11 @@ export default () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<p className="text-center text-sm text-grey-dark">
|
||||||
|
<Link to="/login" className="font-medium text-red-light hover:text-red">
|
||||||
|
Retournez à la page de connexion
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
15
src/index.ts
15
src/index.ts
|
|
@ -44,15 +44,16 @@ app.use(urlencoded({ extended: true, limit: '50mb' }));
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, '../public')));
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
|
||||||
// check if folder dist-vite exists
|
|
||||||
if (fs.existsSync(path.join(__dirname, '../client/dist-vite'))) {
|
|
||||||
app.use(express.static(path.join(__dirname, '../client/dist-vite')));
|
|
||||||
} else {
|
|
||||||
app.use(express.static(path.join(__dirname, '../client/dist')));
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use('/api', api);
|
app.use('/api', api);
|
||||||
|
|
||||||
|
const staticDist = fs.existsSync(path.join(__dirname, '../client/dist-vite')) ? express.static(path.join(__dirname, '../client/dist-vite')) : express.static(path.join(__dirname, '../client/dist'));
|
||||||
|
|
||||||
|
app.use(staticDist);
|
||||||
|
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, '../client/dist/index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server listening on port ${port}`);
|
console.log(`Server listening on port ${port}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue