Compare commits

...

8 Commits

10 changed files with 190 additions and 42 deletions

View File

@ -8,9 +8,29 @@
3. Pour compiler les fichiers, éxecutez `npm run build`.
4. Enfin, pour démarrer le serveur, éxecutez `npm start`.
4. Il faut ensuite générer un fichier `.env` à la racine du projet. Vous pouvez vous aider du fichier `.env.example` pour cela. Pour la base de données, vous pouvez utiliser MySQL ou MariaDB. Si vous désirez utiliser mariadb ou mondodb par exemple, alors il faut aussi modifier le 'provider' dans le fichier `prisma/schema.prisma`.
5. Puis générer la base de données en éxécutant `npm run db:build` et enfin synchroniser les tables avec `npm run db:push`.
6. Enfin, pour démarrer le serveur, éxecutez `npm start`.
7. Pour accéder à l'application, rendez-vous sur `http://localhost:5000`.
## Programme exécutable
1. Téléchargez le programme exécutable pour Windows en cliquant [ici](https://github.com/polynux/groupomania-openclassrooms/releases/download/1.0/groupomania-release-1.0.zip).
2. Décompressez le fichier `groupomania-release-1.0.zip`.
3. Lancez le programme `groupomania.exe`.
Si vous souhaitez compiler le programme exécutable, vous pouvez suivre les instructions suivantes :
Executez `cd groupomania-openclassrooms/client` puis `npm install`, puis `npm run build:neu`.
Le programme exécutable se trouve dans le dossier `groupomania-openclassrooms/client/dist`.
## TODO
- [x] Régler un problème avec prisma. Il demande un roleId pour la création de l'utilisateur mais ce n'est pas présent dans le model.
- [ ] Utiliser NeutralinoJS pour faire une application desktop
- [x] Utiliser NeutralinoJS pour faire une application desktop

View File

@ -1,13 +1,11 @@
import gravatar from 'gravatar';
import { useState } from 'react';
import Image from './Image';
const Avatar = ({ user }: any) => {
// console.log(user);
const initials = user.firstName[0] + user.lastName[0];
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=GD&background=0D8ABC&color=fff&size=64`;
const [avatar, setAvatar] = useState(avatarUi);
const [firstLoad, setFirstLoad] = useState(true);
@ -22,7 +20,7 @@ const Avatar = ({ user }: any) => {
return (
<div className="avatar shrink-0">
<img src={avatar} alt="avatar" className="rounded-full w-12 h-12 md:w-16 md:h-16 transition-all cursor-pointer" />
<Image src={avatar} alt="avatar" className="rounded-full w-12 h-12 md:w-16 md:h-16 transition-all cursor-pointer" />
</div>
);
};

View File

@ -0,0 +1,48 @@
import { useState } from 'react';
import { FaTimes } from 'react-icons/fa';
import Modal from './Modal';
const Image = ({ src, alt, className }: { src: string; alt?: string; className?: string }) => {
const [error, setError] = useState(false);
const [modal, setModal] = useState(false);
const onError = () => {
if (!error) {
setError(true);
}
};
const onClick = () => {
setModal(true);
};
return (
<>
{modal && (
<div onClick={() => setModal(false)}>
<Modal show={modal} className="w-auto flex flex-col items-end max-w-[80%]">
<FaTimes
className="fill-grey-light hover:fill-grey-dark hover:bg-grey-light cursor-pointer transition-all text-xl w-10 h-10 p-2.5 rounded-md"
onClick={() => setModal(!modal)}
/>
<img
src={error ? 'https://via.placeholder.com/150' : src}
alt={alt}
onError={onError}
className={className}
/>
</Modal>
</div>
)}
<img
src={error ? 'https://via.placeholder.com/150' : src}
alt={alt}
onError={onError}
className={className}
onClick={onClick}
/>
</>
);
};
export default Image;

View File

@ -5,18 +5,7 @@ import { getMeInfo } from '@controllers/UserController';
import { useQuery } from '@tanstack/react-query';
import { toastError } from '@controllers/Toasts';
import User from './User';
const Image = ({ image }: { image: string }) => {
if (image === '' || image === null) {
return null;
}
return (
<div className="flex justify-center ">
<img src={image} alt="image" className="w-full rounded-lg cursor-pointer" />
</div>
);
};
import Image from './Image';
const Text = ({ text }: { text: string }) => {
if (text === '') {
@ -50,7 +39,7 @@ const Message = ({ message }: any) => {
) : null}
</div>
<Text text={message.content} />
<Image image={message.image} />
{message.image && <Image src={message.image} alt="image" className="w-fit rounded-lg cursor-pointer" />}
<div className="text-grey-light date">
{new Date(message.createdAt).toLocaleDateString(undefined, {
year: 'numeric',

View File

@ -21,7 +21,7 @@ const MessageWrapper = () => {
return (
<main className="messages-wrapper rounded-md w-full max-w-3xl flex flex-col flex-shrink relative overflow-y-hidden">
<ScrollToBottom className="message-container flex flex-col gap-4 w-full max-w-3xl overflow-scroll pt-4 pb-6 px-2 md:px-0">
<ScrollToBottom className="message-container flex flex-col gap-4 w-full max-w-3xl overflow-y-scroll overflow-x-hidden pt-4 pb-6 px-2 md:px-0">
{isLoading
? ''
: isError

View File

@ -37,7 +37,7 @@ const ScrollToBottom = ({ children, className = '' }: { children: ReactNode; cla
</div>
<button
onClick={() => node?.scrollIntoView({ behavior: 'smooth' })}
className={'absolute right-3 transition-all' + (show ? ' bottom-3' : ' -bottom-10')}
className={'absolute right-3 ' + (show ? 'animate-show bottom-3 block' : 'animate-hide hidden bottom-0 -mb-10')}
>
<div className="popup-btn 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" />

View File

@ -16,13 +16,12 @@ const User = ({ author }: any) => {
toastError(error as string);
},
});
const queryClient = useQueryClient();
function handleContextMenu(e: any) {
if (messageId!== '0' && e.target.closest('.message')?.id.slice(9) !== messageId) {
setPopupPos({ posX: 0, posY: 0});
if (messageId !== '0' && e.target.closest('.message')?.id.slice(9) !== messageId) {
setPopupPos({ posX: 0, posY: 0 });
setMessageId('0');
document.removeEventListener('contextmenu', handleContextMenu);
}
@ -44,7 +43,7 @@ const User = ({ author }: any) => {
setMessageId(e.target.closest('.message').id.slice(9));
setPopupPos({ posX: e.clientX, posY: e.clientY });
};
async function changeRights() {
setPopupPos({ posX: 0, posY: 0 });
setMessageId('0');
@ -56,6 +55,12 @@ const User = ({ author }: any) => {
queryClient.invalidateQueries(['messages']);
}
const handleChange = (e: any) => {
e.preventDefault();
toastSuccess('Infos personelles changées');
setShow(!show);
};
return (
<div className="user">
<button
@ -90,20 +95,83 @@ const User = ({ author }: any) => {
)}
<Modal show={show}>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2">
<div className="text-2xl text-white">Info utilisateur</div>
<div className="text-white">Nom: {author.lastName}</div>
<div className="text-white">Prenom: {author.firstName}</div>
<div className="text-white">
Adresse mail: <a href={'mailto:' + author.email}>{author.email}</a>
{me.data?.id !== author.id ? (
<div className="flex flex-col gap-2">
<div className="text-2xl text-white">Info utilisateur</div>
<div className="text-white">Nom: {author.lastName}</div>
<div className="text-white">Prenom: {author.firstName}</div>
<div className="text-white">
Adresse mail: <a href={'mailto:' + author.email}>{author.email}</a>
</div>
<button
className="rounded-md border border-red bg-red py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
onClick={() => setShow(false)}
>
Fermer
</button>
</div>
</div>
<button
className="rounded-md border border-red bg-red py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
onClick={() => setShow(false)}
>
Fermer
</button>
) : (
<>
<div className="text-2xl text-white">Modifier ses infos personnelles</div>
<form className="flex flex-col gap-2" onSubmit={handleChange}>
<label htmlFor="firstName" className="text-white">
Prenom
</label>
<input
type="text"
name="firstName"
id="firstName"
className="rounded-lg p-2"
defaultValue={author.firstName}
/>
<label htmlFor="lastName" className="text-white">
Nom
</label>
<input
type="text"
name="lastName"
id="lastName"
className="rounded-lg p-2"
defaultValue={author.lastName}
/>
<label htmlFor="email" className="text-white">
Adresse mail
</label>
<input type="email" name="email" id="email" className="rounded-lg p-2" defaultValue={author.email} />
<label htmlFor="password" className="text-white">
Mot de passe
</label>
<input type="password" name="password" id="password" className="rounded-lg p-2" />
<div>
<div className='text-white text-xl'>Changer son mot de passe:</div>
<div className="flex flex-col gap-2">
<label htmlFor="newPassword" className="text-white">
Nouveau mot de passe
</label>
<input type="password" name="newPassword" id="newPassword" className="rounded-lg p-2" />
<label htmlFor="confirmPassword" className="text-white">
Confirmer le nouveau mot de passe
</label>
<input type="password" name="confirmPassword" id="confirmPassword" className="rounded-lg p-2" />
</div>
</div>
<div className="flex gap-4">
<button
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"
onClick={() => setShow(false)}
>
Annuler
</button>
<button
type="submit"
className="rounded-md border border-red bg-red py-2 px-4 text-sm max-w-[100px] font-medium text-white hover:bg-white hover:text-red focus:outline-none focus:ring-2 focus:ring-red focus:ring-offset-2"
>
Modifier
</button>
</div>
</form>
</>
)}
</div>
</Modal>
</div>

View File

@ -19,6 +19,22 @@ module.exports = {
padding: {
'2.5': '0.625rem',
},
keyframes: {
'hide': {
'0%': { opacity: 1, display: 'block' },
'99%': { opacity: 0, display: 'block' },
'100%': { opacity: 0, display: 'none' },
},
'show': {
'0%': { opacity: 0, display: 'none' },
'1%': { opacity: 0, display: 'block' },
'100%': { opacity: 1, display: 'block' },
},
},
animation: {
'hide': 'hide 0.2s ease-in-out',
'show': 'show 0.2s ease-in-out',
},
},
},
plugins: [],

View File

@ -5,10 +5,12 @@
"main": "index.js",
"scripts": {
"dev": "nodemon --watch src -e js,ts,json --exec 'ts-node src/index.ts'",
"build": "cd client && pnpm build",
"build": "cd client && npm run build",
"start": "ts-node src/index.ts",
"db": "prisma studio",
"db-build": "prisma generate; prisma db push"
"db:build": "prisma generate",
"db:push": "prisma db push",
"postinstall": "cd client && npm install"
},
"keywords": [],
"author": "",

View File

@ -5,6 +5,7 @@ import { config as envConfig } from 'dotenv';
import { deleteExpiredTokens } from '@/controller/AuthController';
import ms from 'ms';
import path from 'path';
import fs from 'fs';
envConfig();
@ -42,7 +43,13 @@ app.use(json({ limit: '50mb' }));
app.use(urlencoded({ extended: true, limit: '50mb' }));
app.use(express.static(path.join(__dirname, '../public')));
app.use(express.static(path.join(__dirname, '../client/dist')));
// 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);