Compare commits

...

11 Commits

16 changed files with 52 additions and 37 deletions

View File

@ -18,15 +18,15 @@
## Programme exécutable ## 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). 1. Téléchargez le programme exécutable pour Windows en cliquant [ici](https://github.com/polynux/groupomania-openclassrooms/releases/download/1.1/groupomania-release-1.1.zip).
2. Décompressez le fichier `groupomania-release-1.0.zip`. 2. Décompressez le fichier `groupomania-release-1.1.zip`.
3. Lancez le programme `groupomania.exe`. 3. Lancez le programme `groupomania.exe`.
Si vous souhaitez compiler le programme exécutable, vous pouvez suivre les instructions suivantes : 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`. Executez `npm run build:neu`.
Le programme exécutable se trouve dans le dossier `groupomania-openclassrooms/client/dist`. Le programme exécutable se trouve dans le dossier `groupomania-openclassrooms/client/dist`.

1
client/.env-example Normal file
View File

@ -0,0 +1 @@
VITE_API_URL=https://localhost:3000/api

View File

@ -62,6 +62,7 @@
"cli": { "cli": {
"binaryName": "groupomania", "binaryName": "groupomania",
"resourcesPath": "/dist-vite/", "resourcesPath": "/dist-vite/",
"clientLibrary": "/public/js/neutralino.js",
"extensionsPath": "/extensions/", "extensionsPath": "/extensions/",
"binaryVersion": "4.7.0", "binaryVersion": "4.7.0",
"clientVersion": "3.6.0" "clientVersion": "3.6.0"

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@ import { toastError, toastSuccess } from '@controllers/Toasts';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getMeInfo } from '@controllers/UserController'; import { getMeInfo } from '@controllers/UserController';
const Like = ({ message }: { message: any}) => { const Like = ({ message }: { message: any }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [liked, setLiked] = useState(false); const [liked, setLiked] = useState(false);
const me = useQuery(['me'], getMeInfo, { const me = useQuery(['me'], getMeInfo, {
@ -20,7 +20,7 @@ const Like = ({ message }: { message: any}) => {
useEffect(() => { useEffect(() => {
if (message.likedBy.some((like: any) => like.userId === me.data?.id)) { if (message.likedBy.some((like: any) => like.userId === me.data?.id)) {
setLiked(true); setLiked(true);
} }
}, [message]); }, [message]);
const mutateLike = useMutation(liked ? unlikePost : likePost, { const mutateLike = useMutation(liked ? unlikePost : likePost, {
@ -28,10 +28,11 @@ const Like = ({ message }: { message: any}) => {
queryClient.invalidateQueries(['messages']); queryClient.invalidateQueries(['messages']);
if (data.message === 'Post liked') { if (data.message === 'Post liked') {
setLiked(true); setLiked(true);
toastSuccess('Message aimé'); toastSuccess('Message liké');
} else { } else if (data.message === 'Post unliked') {
setLiked(false); setLiked(false);
toastSuccess('Message non aimé'); } else {
toastError(data.message);
} }
}, },
onError: (error) => { onError: (error) => {
@ -45,12 +46,16 @@ const Like = ({ message }: { message: any}) => {
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-2 mb-2 rounded-full bg-grey-dark shadow-lg shadow-slate-900 cursor-pointer"
onClick={like} onClick={like}
name="like" name="like"
> >
<FaThumbsUp className={'fill-red-light text-xl w-10 h-10 p-2.5' + (liked ? ' 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>} {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> <span className="sr-only">Aimer</span>
</button> </button>
); );

View File

@ -41,7 +41,7 @@ const Message = ({ message }: any) => {
</div> </div>
<Text text={message.content} /> <Text text={message.content} />
{message.image && <Image src={message.image} alt="image" className="w-fit rounded-lg cursor-pointer" />} {message.image && <Image src={message.image} alt="image" className="w-fit rounded-lg cursor-pointer" />}
<div className="flex justify-between"> <div className="flex justify-between gap-3 flex-wrap">
<div className="text-grey-light date"> <div className="text-grey-light date">
{new Date(message.createdAt).toLocaleDateString(undefined, { {new Date(message.createdAt).toLocaleDateString(undefined, {
year: 'numeric', year: 'numeric',
@ -51,9 +51,10 @@ const Message = ({ message }: any) => {
minute: 'numeric', minute: 'numeric',
})} })}
</div> </div>
{message.edited && <div className="text-grey-light italic">Modifié</div>} {message.edited && <div className="text-grey-light italic flex-grow">Modifié</div>}
{me.data?.id === message.author.id && message.likes > 0 && <div className="text-white">{message.likes} likes</div>}
</div> </div>
{me.data?.id === message.author.id ? null : <Like message={message} />} {me.data?.id !== message.author.id && <Like message={message} />}
</div> </div>
</div> </div>
</> </>

View File

@ -57,7 +57,7 @@ const NewMessage = () => {
)} )}
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
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" className="flex gap-2 bg-grey-dark rounded-xl p-2 mx-2 sm:p-3 md:mx-0 shadow-md shadow-grey-dark"
> >
<div className="file"> <div className="file">
<label htmlFor="image" className="cursor-pointer block p-2"> <label htmlFor="image" className="cursor-pointer block p-2">

View File

@ -1,8 +1,9 @@
import { Cookies } from "react-cookie"; import { Cookies } from "react-cookie";
import { api } from "../main";
const likePost = async (id: string) => { const likePost = async (id: string) => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch(`/api/posts/like/${id}`, { const response = await fetch(`${api}/posts/like/${id}`, {
method: 'PUT', method: 'PUT',
mode: 'cors', mode: 'cors',
headers: { headers: {
@ -19,7 +20,7 @@ const likePost = async (id: string) => {
const unlikePost = async (id: string) => { const unlikePost = async (id: string) => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch(`/api/posts/unlike/${id}`, { const response = await fetch(`${api}/posts/unlike/${id}`, {
method: 'PUT', method: 'PUT',
mode: 'cors', mode: 'cors',
headers: { headers: {

View File

@ -1,8 +1,9 @@
import { Cookies } from 'react-cookie'; import { Cookies } from 'react-cookie';
import { api } from '../main';
const getMessages = async () => { const getMessages = async () => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch('/api/posts', { const response = await fetch(api + '/posts', {
method: 'GET', method: 'GET',
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
@ -17,7 +18,7 @@ const getMessages = async () => {
const newMessage = async (data: FormData) => { const newMessage = async (data: FormData) => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch('/api/posts/new', { const response = await fetch(api + '/posts/new', {
method: 'POST', method: 'POST',
body: data, body: data,
mode: 'cors', mode: 'cors',
@ -30,7 +31,7 @@ const newMessage = async (data: FormData) => {
const deleteMessage = async (id: string) => { const deleteMessage = async (id: string) => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch(`/api/posts/delete/${id}`, { const response = await fetch(`${api}/posts/delete/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
@ -41,7 +42,7 @@ const deleteMessage = async (id: string) => {
const editMessage = async (id: string, data: FormData) => { const editMessage = async (id: string, data: FormData) => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch(`/api/posts/edit/${id}`, { const response = await fetch(`${api}/posts/edit/${id}`, {
method: 'PUT', method: 'PUT',
body: data, body: data,
mode: 'cors', mode: 'cors',

View File

@ -1,9 +1,10 @@
import { Cookies } from 'react-cookie'; import { Cookies } from 'react-cookie';
import { api } from '../main';
const getMeInfo = async () => { const getMeInfo = async () => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch('/api/me', { const response = await fetch(api + '/me', {
method: 'GET', method: 'GET',
mode: 'cors', mode: 'cors',
headers: { headers: {
@ -21,7 +22,7 @@ const getMeInfo = async () => {
const login = async ({ email, password }: { email: string; password: string }) => { const login = async ({ email, password }: { email: string; password: string }) => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch('/api/auth/login', { const response = await fetch(api + '/auth/login', {
method: 'POST', method: 'POST',
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
mode: 'cors', mode: 'cors',
@ -51,7 +52,7 @@ const signup = async (formData: FormData) => {
throw 'Passwords do not match'; throw 'Passwords do not match';
} }
const response = await fetch('/api/auth/signup', { const response = await fetch(api + '/auth/signup', {
method: 'POST', method: 'POST',
body: JSON.stringify(form), body: JSON.stringify(form),
mode: 'cors', mode: 'cors',
@ -69,7 +70,7 @@ const signup = async (formData: FormData) => {
export const giveUserRights = async (userId: string, role: string) => { export const giveUserRights = async (userId: string, role: string) => {
const token = new Cookies().get('token'); const token = new Cookies().get('token');
const response = await fetch(`/api/users/${userId}/roles`, { const response = await fetch(`${api}/users/${userId}/roles`, {
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
headers: { headers: {
@ -117,7 +118,7 @@ export const changeUserInfo = async (userId: string, formData: FormData) => {
} }
} }
const response = await fetch(`/api/users/${userId}`, { const response = await fetch(`${api}/users/${userId}`, {
method: 'PUT', method: 'PUT',
mode: 'cors', mode: 'cors',
headers: { headers: {

View File

@ -3,3 +3,10 @@ import App from './App';
import './index.css'; import './index.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />); ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);
let api = '/api';
if (import.meta.env.VITE_API_URL) {
api = import.meta.env.VITE_API_URL;
}
export { api };

View File

@ -5,7 +5,7 @@ import 'react-toastify/dist/ReactToastify.css';
const Home = () => { const Home = () => {
return ( return (
<div className="min-h-full max-h-full bg-grey flex flex-col items-center overflow-y-hidden"> <div className="min-h-full max-h-full bg-grey flex flex-col items-center justify-between overflow-y-hidden">
<AppHeader /> <AppHeader />
<MessageWrapper /> <MessageWrapper />
<NewMessage /> <NewMessage />

View File

@ -5,8 +5,11 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "nodemon --watch src -e js,ts,json --exec 'ts-node src/index.ts'", "dev": "nodemon --watch src -e js,ts,json --exec 'ts-node src/index.ts'",
"build": "cd client && npm run build", "build": "npm run build:server && npm run build:client",
"start": "ts-node src/index.ts", "build:neu": "cd client && npm run build:neu",
"build:server": "tsc && tsc-alias",
"build:client": "cd client && npm run build",
"start": "node dist/index.js",
"db": "prisma studio", "db": "prisma studio",
"db:build": "prisma generate", "db:build": "prisma generate",
"db:push": "prisma db push", "db:push": "prisma db push",

View File

@ -10,8 +10,6 @@ export default async (req: Request, res: Response) => {
} }
return res.status(200).send({ message: 'Post unliked' }); return res.status(200).send({ message: 'Post unliked' });
} catch (error) { } catch (error) {
console.log(error);
return res.status(500).send(error); return res.status(500).send(error);
} }
}; };

View File

@ -190,11 +190,6 @@ const likePost = async (id: number, userId: number): Promise<PrismaPost | Error>
id, id,
}, },
data: { data: {
likedBy: {
connect: {
id: newLike.id,
},
},
likes: { likes: {
increment: 1, increment: 1,
}, },

View File

@ -1,15 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"target": "es2017", "target": "ES2017",
"lib": [ "lib": [
"esnext" "esnext"
], ],
"moduleResolution": "node", "moduleResolution": "node",
"module": "CommonJS",
"esModuleInterop": true, "esModuleInterop": true,
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"resolveJsonModule": true,
"skipDefaultLibCheck": true, "skipDefaultLibCheck": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,