Compare commits
8 Commits
58dd6d72b3
...
d08c0a38fb
| Author | SHA1 | Date |
|---|---|---|
|
|
d08c0a38fb | |
|
|
37d4ee82be | |
|
|
6ac28c7357 | |
|
|
63588e0500 | |
|
|
85a3231c50 | |
|
|
bc1f1731d2 | |
|
|
b9d2f0a156 | |
|
|
f994e2ca5d |
24
README.md
24
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue