This commit is contained in:
pro.boooooo 2024-03-27 01:37:35 +01:00
commit 3ac9cb675d
46 changed files with 9876 additions and 0 deletions

20
.eslintrc.cjs Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
.idea
.DS_Store
.env
node_modules/
yarn-error.log
build/
dist/
ecosystem.config.js
deploy.key
coverage
coverage.txt
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

3
.lintstagedrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"*.{js,jsx}": "prettier --write"
}

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# Projet BUT 3
## Run Locally
Clone the project
```bash
git clone gitea@dwarves.iut-fbleau.fr:legrelle/Projet-dev-but3-2024.git
```
Go to the project directory
```bash
cd Projet-dev-but3-2024
```
Install dependencies
```bash
npm i
```
Start the front-end
```bash
npm run dev
```
## API Reference
API Documentation : [Postman](https://but-3-dev-project-back.onrender.com/api/) documentation url

19
__tests__/App.test.jsx Normal file
View File

@ -0,0 +1,19 @@
// src/__ tests __/App.test.tsx
import { expect, test } from "vitest";
import { render } from "@testing-library/react";
import App from "../src/App.jsx";
import { BrowserRouter } from "react-router-dom";
test("demo", () => {
expect(true).toBe(true);
});
test("Renders the main page", () => {
render(
<BrowserRouter>
<App />
</BrowserRouter>,
);
expect(true).toBeTruthy();
});

View File

@ -0,0 +1,51 @@
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import { render, fireEvent, screen, cleanup } from "@testing-library/react";
import { Login } from "../../../src/pages/index.js";
import { AuthenticationContext } from "../../../src/contexts/index.js";
describe("Login Component", () => {
let loginFunction;
beforeEach(() => {
loginFunction = vi.fn(); // Mock login function
render(
<AuthenticationContext.Provider value={{ login: loginFunction }}>
<Login />
</AuthenticationContext.Provider>,
);
});
afterEach(() => {
cleanup();
});
it("renders without errors", () => {
expect(screen.getByText("Login page")).toBeTruthy();
});
it("allows entering a username and password", () => {
fireEvent.change(screen.getByPlaceholderText("username"), {
target: { value: "testuser" },
});
fireEvent.change(screen.getByPlaceholderText("password"), {
target: { value: "password123" },
});
expect(screen.getByPlaceholderText("username").value).toBe("testuser");
expect(screen.getByPlaceholderText("password").value).toBe("password123");
});
it("handles submit event", async () => {
fireEvent.change(screen.getByPlaceholderText("username"), {
target: { value: "testuser" },
});
fireEvent.change(screen.getByPlaceholderText("password"), {
target: { value: "password123" },
});
fireEvent.click(screen.getByText("submit"));
expect(loginFunction).toHaveBeenCalledWith("testuser", "password123");
});
//TODO add more tests please :)
});

View File

@ -0,0 +1,9 @@
// @ts-check
import { test, expect } from "@playwright/test";
test("has title on my project", async ({ page }) => {
await page.goto("/");
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle("Vite + React");
});

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5453
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "projet-but3",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "vitest",
"test-e2e": "npx playwright test",
"show-e2e": "npx playwright show-report",
"preview": "vite preview",
"format": "prettier --write 'src/**/*.{js,jsx}'",
"prepare": "husky install"
},
"dependencies": {
"axios": "^1.6.7",
"react": "^18.2.0",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"sass": "^1.72.0"
},
"devDependencies": {
"@playwright/test": "^1.41.1",
"@testing-library/react": "^14.1.2",
"@types/node": "^20.11.9",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.55.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"happy-dom": "^13.3.1",
"husky": "^9.0.6",
"lint-staged": "^15.2.0",
"prettier": "^3.2.4",
"vite": "^5.0.8",
"vitest": "^1.2.2"
}
}

42
playwright.config.js Normal file
View File

@ -0,0 +1,42 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
// Look for test files in the "tests" directory, relative to this configuration file.
testDir: "e2e-tests",
// Run all tests in parallel.
fullyParallel: true,
// Fail the build on CI if you accidentally left test.only in the source code.
forbidOnly: !!import.meta.env.process.env.CI,
// Retry on CI only.
retries: import.meta.env.process.env.CI ? 2 : 0,
// Opt out of parallel tests on CI.
workers: import.meta.env.process.env.CI ? 1 : undefined,
// Reporter to use
reporter: "html",
use: {
// Base URL to use in actions like `await page.goto('/')`.
baseURL: "http://localhost:5173/",
// Collect trace when retrying the failed test.
trace: "on-first-retry",
},
// Configure projects for major browsers.
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
// Run your local dev server before starting the tests.
webServer: {
command: "npm run dev",
url: "http://localhost:5173/",
reuseExistingServer: !import.meta.env.process.env.CI,
},
});

3337
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

15
src/App.jsx Normal file
View File

@ -0,0 +1,15 @@
import React from "react";
import { Authenticated } from "./components";
import Layout from "./layout/Layout";
import { Router } from "./router";
const App = () => (
<Authenticated>
<Layout>
<Router />
</Layout>
</Authenticated>
);
export default App;

34
src/api/authentication.js Normal file
View File

@ -0,0 +1,34 @@
import axios from "axios";
export const isLoggedIn = async () => {
try {
const response = await axios.get("/authenticate");
return response.data;
} catch (error) {
return error.response.data;
}
};
export const login = async (username, password) => {
try {
const response = await axios.post("/authenticate", {
username,
password,
});
return response.data;
} catch (error) {
return error.response.data;
}
};
export const logout = async () => {
try {
const response = await axios.delete("/authenticate");
return response.data;
} catch (error) {
return error.response.data;
}
};

3
src/api/index.js Normal file
View File

@ -0,0 +1,3 @@
export * from "./authentication";
export * from "./user";
export * from "./rooms";

23
src/api/rooms.js Normal file
View File

@ -0,0 +1,23 @@
import axios from "axios";
export const createRoom = async (name) => {
const response = await axios.post("/room", {
name,
});
return response.data;
};
export const deleteRoom = async (id) => {
const response = await axios.delete(`/room/${id}`);
return response.data;
};
export const getRooms = async () => {
const response = await axios.get(`/room`);
return response.data;
};
export const getRoom = async (id) => {
const response = await axios.get(`/room/${id}`);
return response.data;
};

14
src/api/user.js Normal file
View File

@ -0,0 +1,14 @@
import axios from "axios";
export const createUser = async (username, password, confirmation) => {
try {
const response = await axios.post("/user", {
username,
password,
confirmation,
});
return response.data;
} catch (error) {
return error.response.data;
}
};

View File

@ -0,0 +1,9 @@
body {
height: 100vh;
font-family: Arial, Helvetica, sans-serif;
}
* {
margin: 0;
padding: 0;
}

View File

@ -0,0 +1,50 @@
import { useAuth } from "../../hooks";
import "./NavBar.scss";
import { logout } from "../../api";
import { Link } from "react-router-dom/";
export default function NavBar() {
const { user } = useAuth();
console.log(user);
const onLogout = () => {
logout().then((res) => {
if (res === "Ok") {
window.location.reload();
}
});
};
return (
<nav id="nav-container">
<ul id="leafs-container">
<li className="leaf">
<Link to="/">Accueil</Link>
</li>
<li className="leaf">
<Link to="rooms">Pieces</Link>
</li>
{user ? (
<div className="leaf-into">
<li className="leaf">
<Link to="profile">Profile</Link>
</li>
<button className="leaf-btn" onClick={onLogout}>
Deconnexion
</button>
</div>
) : (
<li className="leaf-into">
<span className="leaf-txt">
<Link to="login">Connexion</Link>&nbsp;/&nbsp;
<Link to="register">Inscription</Link>
</span>
</li>
)}
</ul>
</nav>
);
}

View File

@ -0,0 +1,41 @@
#nav-container {
background: rgb(123, 106, 156);
padding: 15px;
a {
font-weight: bold;
color: white;
text-decoration: none;
&:hover {
border-bottom: 1.5px solid white;
}
}
#leafs-container {
list-style-type: none;
display: flex;
flex-direction: row;
flex-wrap: wrap;
color: white;
gap: 50px;
align-items: center;
justify-content: center;
padding: 0;
.leaf-into {
.leaf-btn {
}
.leaf {
}
.leaf-txt {
}
}
.leaf {
font-weight: bold;
}
}
}

View File

@ -0,0 +1,7 @@
import React from "react";
import { AuthenticationProvider } from "../contexts";
export const Authenticated = ({ children }) => {
return <AuthenticationProvider>{children}</AuthenticationProvider>;
};

1
src/components/index.js Normal file
View File

@ -0,0 +1 @@
export * from "./authenticated";

View File

@ -0,0 +1,100 @@
import React, { useEffect, useState } from "react";
import {
createUser as registerApi,
isLoggedIn,
login as loginApi,
logout as logoutApi,
} from "../api";
import { useQuery } from "../hooks";
import { useLocation, useNavigate } from "react-router-dom";
const initState = {
user: undefined,
};
export const AuthenticationContext = React.createContext({
...initState,
login: () => {},
register: () => {},
logout: () => {},
});
export const AuthenticationProvider = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
const query = useQuery();
const [authState, setAuthState] = useState(initState);
const [isLoading, setIsLoading] = useState(true);
const redirect = () => {
navigate(query.get("redirect_uri") || "/");
};
const login = async (email, password) => {
loginApi(email, password)
.then((user) => {
setAuthState({ user });
redirect();
})
.catch(() => {
setAuthState({ user: undefined });
});
};
const register = async (username, password, confirmation) => {
registerApi(username, password, confirmation).then((user) => {
setAuthState({ user });
redirect();
});
};
const logout = () => {
logoutApi().then(() => {
setAuthState(initState);
navigate(`/login`);
});
};
useEffect(() => {
isLoggedIn()
.then((user) => {
if (user === "Unauthorized") throw new Error("Unauthorized");
setAuthState({ user });
if (location.pathname === "/login") {
redirect();
}
})
.catch(() => {
setAuthState({ user: undefined });
if (!location.pathname.match(/^(\/|\/login|\/register)$/)) {
navigate(
`/login?redirect_uri=${encodeURI(location.pathname)}`
);
}
})
.finally(() => {
setIsLoading(false);
});
}, []);
useEffect(() => {
if (location.pathname === "/logout") {
logout();
}
}, [location]);
if (isLoading) {
return <p>Loading...</p>;
}
return (
<AuthenticationContext.Provider
value={{ ...authState, login, logout, register }}
>
{children}
</AuthenticationContext.Provider>
);
};

1
src/contexts/index.js Normal file
View File

@ -0,0 +1 @@
export * from './auth-context'

2
src/hooks/index.js Normal file
View File

@ -0,0 +1,2 @@
export * from "./use-auth";
export * from "./use-query";

7
src/hooks/use-auth.js Normal file
View File

@ -0,0 +1,7 @@
import React from "react";
import { AuthenticationContext } from "../contexts";
export function useAuth() {
return React.useContext(AuthenticationContext);
}

9
src/hooks/use-query.js Normal file
View File

@ -0,0 +1,9 @@
import React from 'react'
import { useLocation } from 'react-router-dom'
export const useQuery = () => {
const { search } = useLocation()
return React.useMemo(() => new URLSearchParams(search), [search])
}

9
src/layout/Layout.css Normal file
View File

@ -0,0 +1,9 @@
#layout-container {
display: flex;
flex-direction: column;
height: 100vh;
main {
height: 100%;
}
}

11
src/layout/Layout.jsx Normal file
View File

@ -0,0 +1,11 @@
import NavBar from "../components/NavBar/NavBar";
import "./Layout.css";
export default function Layout({ children }) {
return (
<div id="layout-container">
<NavBar />
<main>{children}</main>
</div>
);
}

19
src/main.jsx Normal file
View File

@ -0,0 +1,19 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./assets/styles/index.css";
import { BrowserRouter } from "react-router-dom";
import { CookiesProvider } from "react-cookie";
import setupAxios from "./setupAxios";
setupAxios();
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<CookiesProvider>
<App />
</CookiesProvider>
</BrowserRouter>
</React.StrictMode>
);

View File

@ -0,0 +1,2 @@
export * from './login'
export * from './register'

View File

@ -0,0 +1,42 @@
import React, { useState } from "react";
import { useAuth } from "../../hooks";
export const Login = () => {
const { login } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState();
const onSubmit = async (e) => {
e.preventDefault();
const response = await login(username, password);
if (response && !response.success) {
setError(response.error);
}
};
return (
<div>
<h1>Login page</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={onSubmit}>
<input
type="text"
value={username}
placeholder="username"
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
value={password}
placeholder="password"
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">submit</button>
</form>
</div>
);
};

View File

@ -0,0 +1,50 @@
import React, { useState } from "react";
import { useAuth } from "../../hooks";
export const Register = () => {
const { register } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmation, setConfirmation] = useState("");
const [error, setError] = useState();
const onSubmit = async (e) => {
e.preventDefault();
const response = await register(username, password, confirmation);
console.log(response);
if (response && !response.success) {
setError(response.error);
}
};
return (
<div>
<h1>Register page</h1>
{error && <p className="text-red-500">{error}</p>}
<form onSubmit={onSubmit}>
<input
type="text"
value={username}
placeholder="username"
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
value={password}
placeholder="password"
onChange={(e) => setPassword(e.target.value)}
/>
<input
type="password"
value={confirmation}
placeholder="confirmation"
onChange={(e) => setConfirmation(e.target.value)}
/>
<button type="submit">submit</button>
</form>
</div>
);
};

15
src/pages/home.jsx Normal file
View File

@ -0,0 +1,15 @@
import React from "react";
import { useAuth } from "../hooks";
export const Home = () => {
const { user } = useAuth();
console.log(user);
return (
<div>
<h1>Home page</h1>
{user && <h2>Hello {user.user.username}</h2>}
</div>
);
};

2
src/pages/index.js Normal file
View File

@ -0,0 +1,2 @@
export * from "./authenticated";
export * from "./home";

View File

@ -0,0 +1,50 @@
import { useAuth } from "../../hooks";
import "./Profile.scss";
//Bilouuuuuuu94!@@
export default function Profile() {
const { user } = useAuth();
return (
<div id="profile-container">
<div id="title-container">
<h3 id="title">
Heureux de vous voir <b>{user.user.username}</b> !
</h3>
</div>
<div className="profile-modifier-container">
<span className="profile-modifier-title">
Change ton pseudo
</span>
<input
className="profile-modifier-ipt"
type="text"
placeholder="Nouveau nom"
/>
<button>OK</button>
</div>
<div className="profile-modifier-container">
<span className="profile-modifier-title">
Change ton mot de passe
</span>
<input
className="profile-modifier-ipt"
type="password"
placeholder="Nouveau mot de passe"
/>
<input
className="profile-modifier-ipt"
type="password"
placeholder="Confirmation"
/>
<button>OK</button>
</div>
</div>
);
}

View File

@ -0,0 +1,35 @@
#profile-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
margin-top: 15px;
#title-container {
#title {
}
}
.profile-modifier-container {
border: 1px dashed black;
width: 500px;
display: flex;
flex-direction: column;
padding: 10px;
justify-content: space-between;
align-items: center;
gap: 10px;
.profile-modifier-title {
color: rgb(70, 70, 70);
font-weight: bold;
}
.profile-modifier-ipt {
text-align: center;
width: 50%;
height: 30px;
}
}
}

21
src/pages/room/Room.jsx Normal file
View File

@ -0,0 +1,21 @@
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { getRoom } from "../../api";
export default function Room() {
const { id } = useParams();
const [data, setData] = useState();
useEffect(() => {
getRoom(id).then((res) => {
setData(res);
console.log(res);
});
}, []);
return (
<div>
<span>Piece n{id}</span>
</div>
);
}

0
src/pages/room/Room.scss Normal file
View File

87
src/pages/rooms/Rooms.jsx Normal file
View File

@ -0,0 +1,87 @@
import "./Rooms.scss";
import { useState, useEffect } from "react";
import { createRoom, getRooms, deleteRoom } from "../../api";
import { Link } from "react-router-dom";
export default function Rooms() {
const [rooms, setRooms] = useState([]);
const onClickCreate = () => {
const name = prompt("Nom de la piece ?");
createRoom(name).then((res) => {
const values = [...rooms];
values.push(res);
setRooms(values);
});
};
const onClickDelete = (id, name) => {
const confirmation = prompt(
`Etes-vous sur de vouloir supprimer ${name} ? (Oui ou non)`
);
if (confirmation.toLocaleLowerCase() !== "oui") return;
deleteRoom(id).then((res) => {
const values = rooms.filter((e) => e._id !== id);
setRooms(values);
});
};
useEffect(() => {
getRooms().then((res) => {
setRooms(res);
});
}, []);
return (
<div id="rooms-container">
{rooms.length === 0 ? (
<span id="err-no-rooms">Aucune piece enregistree</span>
) : (
<div id="rooms-list-container">
{rooms.map((i, j) => (
<div className="room" key={j}>
<div
className="room-delete"
onClick={() => {
onClickDelete(i._id, i.name);
}}
>
<span className="room-delete-ascii">×</span>
</div>
<div className="room-id-container">
<span className="label-id">ID</span>
<span className="room-id">
{i._id[0]}
{i._id[1]}
{i._id[2]}
{i._id[3]}
{i._id[4]}
{i._id[5]}
...
</span>
</div>
<div className="room-name-container">
<span className="label-name">Nom&nbsp;</span>
<Link to={`/room/${i._id}`}>
<span className="room-name">{i.name}</span>
</Link>
</div>
</div>
))}
</div>
)}
<div id="rooms-add-container">
<div id="rooms-text-on">Creer une nouvelle piece</div>
<button id="add-rooms" onClick={onClickCreate}>
+
</button>
</div>
</div>
);
}

119
src/pages/rooms/Rooms.scss Normal file
View File

@ -0,0 +1,119 @@
#rooms-container {
height: 100%;
width: 100%;
#err-no-rooms {
height: 100%;
display: flex;
color: red;
font-weight: bold;
justify-content: center;
align-items: center;
}
#rooms-list-container {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 50px;
margin-top: 20px;
.room {
width: 200px;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
border: 1px dashed rgb(54, 54, 54);
position: relative;
&:hover {
.room-delete {
display: block;
}
}
.room-delete {
width: 25px;
height: 25px;
position: absolute;
top: 0;
right: 0;
background: red;
display: none;
.room-delete-ascii {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
color: white;
font-weight: bold;
}
}
.room-id-container {
display: flex;
justify-content: space-between;
width: 60%;
.label-id {
}
.room-id {
}
}
.room-name-container {
width: 60%;
display: flex;
justify-content: space-between;
.label-name {
}
.room-name {
}
}
&:hover {
cursor: pointer;
transform: scale(1.02);
transition: 0.2s;
}
}
}
#rooms-add-container {
position: absolute;
bottom: 20px;
right: 20px;
gap: 25px;
#rooms-text-on {
width: 100px;
background: rgb(123, 106, 156);
color: white;
display: none;
}
#add-rooms {
text-align: center;
width: 40px;
height: 40px;
border-radius: 50px;
border: none;
background: rgb(123, 106, 156);
color: white;
font-size: 1em;
font-weight: bold;
&:hover {
cursor: pointer;
}
}
}
}

17
src/router.jsx Normal file
View File

@ -0,0 +1,17 @@
import React from "react";
import { Route, Routes } from "react-router-dom";
import Profile from "./pages/profile/Profile";
import Rooms from "./pages/rooms/Rooms";
import Room from "./pages/room/Room";
import { Home, Login, Register } from "./pages";
export const Router = () => (
<Routes>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="rooms" element={<Rooms />} />
<Route path="room/:id" element={<Room />} />
<Route path="profile" element={<Profile />} />
</Routes>
);

8
src/setupAxios.js Normal file
View File

@ -0,0 +1,8 @@
import axios from "axios";
const setupAxios = () => {
axios.defaults.baseURL = import.meta.env.VITE_API_URL;
axios.defaults.withCredentials = true;
};
export default setupAxios;

14
vite.config.js Normal file
View File

@ -0,0 +1,14 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 3000,
},
test: {
// Use happy-dom for a more lightweight browser environment
environment: "happy-dom",
},
plugins: [react()],
});

14
vitest.config.js Normal file
View File

@ -0,0 +1,14 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
server: {
port: 3001,
},
test: {
include: ["__tests__/**/*.test.{js,jsx}"],
environment: "happy-dom",
},
plugins: [react()],
});