$
This commit is contained in:
commit
3ac9cb675d
20
.eslintrc.cjs
Normal file
20
.eslintrc.cjs
Normal 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
20
.gitignore
vendored
Normal 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
4
.husky/pre-commit
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
3
.lintstagedrc.json
Normal file
3
.lintstagedrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"*.{js,jsx}": "prettier --write"
|
||||
}
|
31
README.md
Normal file
31
README.md
Normal 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
19
__tests__/App.test.jsx
Normal 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();
|
||||
});
|
51
__tests__/pages/authenticated/Login.test.jsx
Normal file
51
__tests__/pages/authenticated/Login.test.jsx
Normal 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 :)
|
||||
});
|
9
e2e-tests/example.spec.js
Normal file
9
e2e-tests/example.spec.js
Normal 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
13
index.html
Normal 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
5453
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal 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
42
playwright.config.js
Normal 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
3337
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
src/App.jsx
Normal file
15
src/App.jsx
Normal 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
34
src/api/authentication.js
Normal 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
3
src/api/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./authentication";
|
||||
export * from "./user";
|
||||
export * from "./rooms";
|
23
src/api/rooms.js
Normal file
23
src/api/rooms.js
Normal 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
14
src/api/user.js
Normal 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;
|
||||
}
|
||||
};
|
9
src/assets/styles/index.css
Normal file
9
src/assets/styles/index.css
Normal file
@ -0,0 +1,9 @@
|
||||
body {
|
||||
height: 100vh;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
50
src/components/NavBar/NavBar.jsx
Normal file
50
src/components/NavBar/NavBar.jsx
Normal 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> /
|
||||
<Link to="register">Inscription</Link>
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
41
src/components/NavBar/NavBar.scss
Normal file
41
src/components/NavBar/NavBar.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
7
src/components/authenticated.jsx
Normal file
7
src/components/authenticated.jsx
Normal 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
1
src/components/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./authenticated";
|
100
src/contexts/auth-context.jsx
Normal file
100
src/contexts/auth-context.jsx
Normal 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
1
src/contexts/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from './auth-context'
|
2
src/hooks/index.js
Normal file
2
src/hooks/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./use-auth";
|
||||
export * from "./use-query";
|
7
src/hooks/use-auth.js
Normal file
7
src/hooks/use-auth.js
Normal 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
9
src/hooks/use-query.js
Normal 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
9
src/layout/Layout.css
Normal file
@ -0,0 +1,9 @@
|
||||
#layout-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
11
src/layout/Layout.jsx
Normal file
11
src/layout/Layout.jsx
Normal 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
19
src/main.jsx
Normal 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>
|
||||
);
|
2
src/pages/authenticated/index.js
Normal file
2
src/pages/authenticated/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './login'
|
||||
export * from './register'
|
42
src/pages/authenticated/login.jsx
Normal file
42
src/pages/authenticated/login.jsx
Normal 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>
|
||||
);
|
||||
};
|
50
src/pages/authenticated/register.jsx
Normal file
50
src/pages/authenticated/register.jsx
Normal 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
15
src/pages/home.jsx
Normal 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
2
src/pages/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./authenticated";
|
||||
export * from "./home";
|
50
src/pages/profile/Profile.jsx
Normal file
50
src/pages/profile/Profile.jsx
Normal 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>
|
||||
);
|
||||
}
|
35
src/pages/profile/Profile.scss
Normal file
35
src/pages/profile/Profile.scss
Normal 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
21
src/pages/room/Room.jsx
Normal 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
0
src/pages/room/Room.scss
Normal file
87
src/pages/rooms/Rooms.jsx
Normal file
87
src/pages/rooms/Rooms.jsx
Normal 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 </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
119
src/pages/rooms/Rooms.scss
Normal 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
17
src/router.jsx
Normal 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
8
src/setupAxios.js
Normal 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
14
vite.config.js
Normal 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
14
vitest.config.js
Normal 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()],
|
||||
});
|
Loading…
Reference in New Issue
Block a user