commit
cbe82f74a9
2
.github/backup.sql
vendored
2
.github/backup.sql
vendored
File diff suppressed because one or more lines are too long
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
.github/backup
|
.github/backup
|
||||||
|
2
LICENSE
2
LICENSE
@ -5,4 +5,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
28
README.md
28
README.md
@ -5,10 +5,9 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitmoji.carloscuesta.me/"><img src="https://camo.githubusercontent.com/2a4924a23bd9ef18afe793f4999b1b9ec474e48f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6769746d6f6a692d253230f09f989c253230f09f988d2d4646444436372e7376673f7374796c653d666c61742d737175617265" alt="Gitmoji"/></a>
|
|
||||||
<a href="https://standardjs.com"><img alt="JavaScript Style Guide" src="https://img.shields.io/badge/code_style-standard-brightgreen.svg"/></a>
|
<a href="https://standardjs.com"><img alt="JavaScript Style Guide" src="https://img.shields.io/badge/code_style-standard-brightgreen.svg"/></a>
|
||||||
|
<a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a>
|
||||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a>
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a>
|
||||||
<img src="https://img.shields.io/github/stars/Divlo/FunctionProject?style=social" alt="Stars">
|
|
||||||
<br/> <br/>
|
<br/> <br/>
|
||||||
<a href="https://function.divlo.fr/"><img src="https://raw.githubusercontent.com/Divlo/FunctionProject/master/.github/FunctionProject.png" alt="FunctionProject" /></a>
|
<a href="https://function.divlo.fr/"><img src="https://raw.githubusercontent.com/Divlo/FunctionProject/master/.github/FunctionProject.png" alt="FunctionProject" /></a>
|
||||||
</p>
|
</p>
|
||||||
@ -23,7 +22,7 @@ Si vous aimez le projet, vous pouvez aider à **le faire connaître** en utilisa
|
|||||||
|
|
||||||
Les dernières versions publiées : [https://github.com/Divlo/FunctionProject/releases](https://github.com/Divlo/FunctionProject/releases)
|
Les dernières versions publiées : [https://github.com/Divlo/FunctionProject/releases](https://github.com/Divlo/FunctionProject/releases)
|
||||||
|
|
||||||
Le projet est disponible sur [function.divlo.fr](https://function.divlo.fr/) (actuellement en version 2.1).
|
Le projet est disponible sur [function.divlo.fr](https://function.divlo.fr/) (actuellement en version 2.2).
|
||||||
|
|
||||||
## 🚀 Open Source
|
## 🚀 Open Source
|
||||||
|
|
||||||
@ -39,8 +38,9 @@ Si vous voulez avoir les données des catégories et des fonctions, vous pouvez
|
|||||||
|
|
||||||
### Prérequis :
|
### Prérequis :
|
||||||
|
|
||||||
- NodeJS (et npm) → version récente
|
- [Node.js](https://nodejs.org/) >= 14
|
||||||
- Base de donnée MySQL → J'utilise Wamp ce qui me permet d'avoir phpmyadmin.
|
- [npm](https://www.npmjs.com/) >= 6
|
||||||
|
- [MySQL](https://www.mysql.com/) >= 5.7
|
||||||
|
|
||||||
### Commandes (à suivre dans l'ordre) :
|
### Commandes (à suivre dans l'ordre) :
|
||||||
|
|
||||||
@ -62,6 +62,24 @@ Vous devrez ensuite configurer l'API en créant un fichier `.env` à la racine d
|
|||||||
|
|
||||||
### Lancer l'environnement de développement :
|
### Lancer l'environnement de développement :
|
||||||
|
|
||||||
|
#### Avec [docker](https://www.docker.com/) :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Setup and run all the services for you
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Services started :**
|
||||||
|
|
||||||
|
- api : `http://localhost:8080`
|
||||||
|
- s.divlo.fr : `http://localhost:7000`
|
||||||
|
- website : `http://localhost:3000`
|
||||||
|
- [phpmyadmin](https://www.phpmyadmin.net/) : `http://localhost:8000`
|
||||||
|
- [MailDev](https://maildev.github.io/maildev/) : `http://localhost:1080`
|
||||||
|
- [MySQL database](https://www.mysql.com/) (with PORT 3006)
|
||||||
|
|
||||||
|
#### Sans docker :
|
||||||
|
|
||||||
Dans deux terminals séparés :
|
Dans deux terminals séparés :
|
||||||
|
|
||||||
- Lancer le front-end en allant dans `/website`
|
- Lancer le front-end en allant dans `/website`
|
||||||
|
2
api/.dockerignore
Normal file
2
api/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
@ -1,12 +1,15 @@
|
|||||||
HOST = "http://localhost:8080"
|
HOST="http://localhost:8080"
|
||||||
FRONT_END_HOST = "http://localhost:3000"
|
FRONT_END_HOST="http://localhost:3000"
|
||||||
OpenWeatherMap_API_KEY = ""
|
OpenWeatherMap_API_KEY=""
|
||||||
Scraper_API_KEY = ""
|
Scraper_API_KEY=""
|
||||||
DB_HOST = ""
|
DATABASE_HOST="functionproject-database"
|
||||||
DB_NAME = ""
|
DATABASE_NAME="functionproject"
|
||||||
DB_USER = ""
|
DATABASE_USER="root"
|
||||||
DB_PASS = ""
|
DATABASE_PASSWORD="password"
|
||||||
JWT_SECRET = ""
|
DATABASE_PORT=3306
|
||||||
EMAIL_HOST = ""
|
JWT_SECRET=""
|
||||||
EMAIL_USER = ""
|
EMAIL_HOST="functionproject-maildev"
|
||||||
EMAIL_PASSWORD = ""
|
EMAIL_USER="no-reply@functionproject.fr"
|
||||||
|
EMAIL_PASSWORD="password"
|
||||||
|
EMAIL_PORT=25
|
||||||
|
COMPOSE_PROJECT_NAME="function.divlo.fr-api"
|
||||||
|
6
api/.gitignore
vendored
6
api/.gitignore
vendored
@ -11,14 +11,16 @@
|
|||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
# misc
|
# envs
|
||||||
.DS_Store
|
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.production
|
.env.production
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
/temp
|
/temp
|
||||||
/assets/images/
|
/assets/images/
|
||||||
|
|
||||||
|
13
api/Dockerfile
Normal file
13
api/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM node:14.15.0-alpine3.12
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY ./ ./
|
||||||
|
|
||||||
|
# docker-compose-wait
|
||||||
|
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
|
||||||
|
RUN chmod +x /wait
|
||||||
|
|
||||||
|
CMD /wait && npm run dev
|
24
api/app.js
24
api/app.js
@ -6,6 +6,7 @@ const helmet = require('helmet')
|
|||||||
const cors = require('cors')
|
const cors = require('cors')
|
||||||
const morgan = require('morgan')
|
const morgan = require('morgan')
|
||||||
const { redirectToHTTPS } = require('express-http-to-https')
|
const { redirectToHTTPS } = require('express-http-to-https')
|
||||||
|
const rateLimit = require('express-rate-limit')
|
||||||
|
|
||||||
/* Files Imports & Variables */
|
/* Files Imports & Variables */
|
||||||
const sequelize = require('./assets/utils/database')
|
const sequelize = require('./assets/utils/database')
|
||||||
@ -16,11 +17,27 @@ const isAdmin = require('./middlewares/isAdmin')
|
|||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
/* Middlewares */
|
/* Middlewares */
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
app.use(morgan('dev'))
|
||||||
|
} else if (process.env.NODE_ENV === 'production') {
|
||||||
|
app.use(redirectToHTTPS())
|
||||||
|
const requestPerSecond = 2
|
||||||
|
const seconds = 60
|
||||||
|
const windowMs = seconds * 1000
|
||||||
|
app.enable('trust proxy')
|
||||||
|
app.use(
|
||||||
|
rateLimit({
|
||||||
|
windowMs,
|
||||||
|
max: seconds * requestPerSecond,
|
||||||
|
handler: (_req, res) => {
|
||||||
|
return res.status(429).json({ message: 'Too many requests' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
app.use(helmet())
|
app.use(helmet())
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
app.use(morgan('dev'))
|
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(redirectToHTTPS([/localhost:(\d{4})/]))
|
|
||||||
|
|
||||||
/* Routes */
|
/* Routes */
|
||||||
app.use('/images', express.static(path.join(__dirname, 'assets', 'images')))
|
app.use('/images', express.static(path.join(__dirname, 'assets', 'images')))
|
||||||
@ -37,7 +54,7 @@ app.use('/links', require('./routes/links_shortener'))
|
|||||||
/* Errors Handling */
|
/* Errors Handling */
|
||||||
app.use((_req, _res, next) =>
|
app.use((_req, _res, next) =>
|
||||||
errorHandling(next, { statusCode: 404, message: "La route n'existe pas!" })
|
errorHandling(next, { statusCode: 404, message: "La route n'existe pas!" })
|
||||||
) // 404
|
)
|
||||||
app.use((error, _req, res, _next) => {
|
app.use((error, _req, res, _next) => {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
const { statusCode, message } = error
|
const { statusCode, message } = error
|
||||||
@ -83,7 +100,6 @@ Users.hasMany(ShortLinks)
|
|||||||
ShortLinks.belongsTo(Users, { constraints: false })
|
ShortLinks.belongsTo(Users, { constraints: false })
|
||||||
|
|
||||||
/* Server */
|
/* Server */
|
||||||
// sequelize.sync({ force: true })
|
|
||||||
sequelize
|
sequelize
|
||||||
.sync()
|
.sync()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
const dotenv = require('dotenv')
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
PORT: process.env.PORT || 8080,
|
PORT: process.env.PORT || 8080,
|
||||||
HOST: process.env.HOST,
|
HOST: process.env.HOST,
|
||||||
@ -5,20 +10,22 @@ const config = {
|
|||||||
WEATHER_API_KEY: process.env.OpenWeatherMap_API_KEY,
|
WEATHER_API_KEY: process.env.OpenWeatherMap_API_KEY,
|
||||||
SCRAPER_API_KEY: process.env.Scraper_API_KEY,
|
SCRAPER_API_KEY: process.env.Scraper_API_KEY,
|
||||||
DATABASE: {
|
DATABASE: {
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DATABASE_HOST,
|
||||||
name: process.env.DB_NAME,
|
name: process.env.DATABASE_NAME,
|
||||||
user: process.env.DB_USER,
|
user: process.env.DATABASE_USER,
|
||||||
password: process.env.DB_PASS
|
password: process.env.DATABASE_PASSWORD,
|
||||||
|
port: parseInt(process.env.DATABASE_PORT ?? '3306', 10)
|
||||||
},
|
},
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
EMAIL_INFO: {
|
EMAIL_INFO: {
|
||||||
host: process.env.EMAIL_HOST,
|
host: process.env.EMAIL_HOST,
|
||||||
port: 465,
|
port: EMAIL_PORT,
|
||||||
secure: true, // true for 465, false for other ports
|
secure: EMAIL_PORT === 465,
|
||||||
auth: {
|
auth: {
|
||||||
user: process.env.EMAIL_USER,
|
user: process.env.EMAIL_USER,
|
||||||
pass: process.env.EMAIL_PASSWORD
|
pass: process.env.EMAIL_PASSWORD
|
||||||
}
|
},
|
||||||
|
ignoreTLS: process.env.NODE_ENV !== 'production'
|
||||||
},
|
},
|
||||||
TOKEN_LIFE: '1 week'
|
TOKEN_LIFE: '1 week'
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ const sequelize = new Sequelize(
|
|||||||
DATABASE.password,
|
DATABASE.password,
|
||||||
{
|
{
|
||||||
dialect: 'mysql',
|
dialect: 'mysql',
|
||||||
host: DATABASE.host
|
host: DATABASE.host,
|
||||||
|
port: DATABASE.port
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
1656
api/package-lock.json
generated
1656
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,37 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"description": "Backend REST API for FunctionProject",
|
"description": "Backend REST API for FunctionProject",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
"dev": "nodemon app.js",
|
"dev": "nodemon app.js",
|
||||||
"format": "standard \"./**/*.js\" --fix | snazzy || exit 0"
|
"format": "standard \"./**/*.js\" --fix | snazzy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.21.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.17.1",
|
|
||||||
"express-fileupload": "^1.1.6",
|
|
||||||
"express-http-to-https": "^1.1.4",
|
|
||||||
"express-validator": "^6.4.0",
|
|
||||||
"helmet": "^3.21.3",
|
|
||||||
"jsdom": "^16.2.2",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
|
||||||
"moment": "^2.24.0",
|
|
||||||
"ms": "^2.1.2",
|
|
||||||
"mysql2": "^2.1.0",
|
|
||||||
"nodemailer": "^6.4.6",
|
|
||||||
"sequelize": "^5.21.5",
|
|
||||||
"smart-request-balancer": "^2.1.1",
|
|
||||||
"uuid": "^7.0.2",
|
|
||||||
"validator": "^13.0.0",
|
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"morgan": "^1.9.1"
|
"express": "^4.17.1",
|
||||||
|
"express-fileupload": "^1.2.0",
|
||||||
|
"express-http-to-https": "^1.1.4",
|
||||||
|
"express-rate-limit": "^5.1.3",
|
||||||
|
"express-validator": "^6.6.1",
|
||||||
|
"helmet": "^4.1.1",
|
||||||
|
"jsdom": "^16.4.0",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"ms": "^2.1.2",
|
||||||
|
"mysql2": "^2.2.5",
|
||||||
|
"nodemailer": "^6.4.14",
|
||||||
|
"sequelize": "^6.3.5",
|
||||||
|
"smart-request-balancer": "^2.1.1",
|
||||||
|
"uuid": "^8.3.1",
|
||||||
|
"validator": "^13.1.17"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.6",
|
||||||
"snazzy": "^8.0.0",
|
"snazzy": "^9.0.0",
|
||||||
"standard": "^14.3.4"
|
"standard": "^16.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
73
docker-compose.yml
Normal file
73
docker-compose.yml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
version: '3.0'
|
||||||
|
services:
|
||||||
|
functionproject-api:
|
||||||
|
build:
|
||||||
|
context: './api'
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
|
depends_on:
|
||||||
|
- 'functionproject-database'
|
||||||
|
- 'functionproject-maildev'
|
||||||
|
volumes:
|
||||||
|
- './api:/app'
|
||||||
|
- '/app/node_modules'
|
||||||
|
environment:
|
||||||
|
WAIT_HOSTS: 'functionproject-database:3306'
|
||||||
|
container_name: 'functionproject-api'
|
||||||
|
|
||||||
|
s.divlo.fr-website:
|
||||||
|
build:
|
||||||
|
context: './s.divlo.fr'
|
||||||
|
ports:
|
||||||
|
- '7000:7000'
|
||||||
|
depends_on:
|
||||||
|
- 'functionproject-database'
|
||||||
|
volumes:
|
||||||
|
- './s.divlo.fr:/app'
|
||||||
|
- '/app/node_modules'
|
||||||
|
environment:
|
||||||
|
WAIT_HOSTS: 'functionproject-database:3306'
|
||||||
|
container_name: 's.divlo.fr-website'
|
||||||
|
|
||||||
|
functionproject-website:
|
||||||
|
build:
|
||||||
|
context: './website'
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
volumes:
|
||||||
|
- './website:/app'
|
||||||
|
- '/app/node_modules'
|
||||||
|
container_name: 'functionproject-website'
|
||||||
|
|
||||||
|
functionproject-phpmyadmin:
|
||||||
|
image: 'phpmyadmin/phpmyadmin:5.0.2'
|
||||||
|
environment:
|
||||||
|
PMA_HOST: 'functionproject-database'
|
||||||
|
PMA_USER: 'root'
|
||||||
|
PMA_PASSWORD: 'password'
|
||||||
|
ports:
|
||||||
|
- '8000:80'
|
||||||
|
depends_on:
|
||||||
|
- 'functionproject-database'
|
||||||
|
container_name: 'functionproject-phpmyadmin'
|
||||||
|
|
||||||
|
functionproject-database:
|
||||||
|
image: 'mysql:5.7'
|
||||||
|
command: '--default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci'
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: 'password'
|
||||||
|
MYSQL_DATABASE: 'functionproject'
|
||||||
|
ports:
|
||||||
|
- '3306:3306'
|
||||||
|
volumes:
|
||||||
|
- 'database-volume:/var/lib/mysql'
|
||||||
|
container_name: 'functionproject-database'
|
||||||
|
|
||||||
|
functionproject-maildev:
|
||||||
|
image: 'maildev/maildev:1.1.0'
|
||||||
|
ports:
|
||||||
|
- '1080:80'
|
||||||
|
container_name: 'functionproject-maildev'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
database-volume:
|
2
s.divlo.fr/.dockerignore
Normal file
2
s.divlo.fr/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
@ -1,4 +1,6 @@
|
|||||||
DB_HOST = ""
|
DATABASE_HOST="functionproject-database"
|
||||||
DB_NAME = ""
|
DATABASE_NAME="functionproject"
|
||||||
DB_USER = ""
|
DATABASE_USER="root"
|
||||||
DB_PASS = ""
|
DATABASE_PASSWORD="password"
|
||||||
|
DATABASE_PORT=3306
|
||||||
|
COMPOSE_PROJECT_NAME="s.divlo.fr-website"
|
||||||
|
2
s.divlo.fr/.gitignore
vendored
2
s.divlo.fr/.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
|
13
s.divlo.fr/Dockerfile
Normal file
13
s.divlo.fr/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM node:14.15.0-alpine3.12
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY ./ ./
|
||||||
|
|
||||||
|
# docker-compose-wait
|
||||||
|
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
|
||||||
|
RUN chmod +x /wait
|
||||||
|
|
||||||
|
CMD /wait && npm run dev
|
@ -10,18 +10,21 @@ const mysql = require('mysql')
|
|||||||
/* Files Imports & Variables */
|
/* Files Imports & Variables */
|
||||||
const app = express()
|
const app = express()
|
||||||
const database = mysql.createPool({
|
const database = mysql.createPool({
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DATABASE_HOST,
|
||||||
user: process.env.DB_USER,
|
user: process.env.DATABASE_USER,
|
||||||
password: process.env.DB_PASS,
|
password: process.env.DATABASE_PASS,
|
||||||
database: process.env.DB_NAME,
|
database: process.env.DATABASE_NAME,
|
||||||
port: process.env.DB_PORT
|
port: process.env.DATABASE_PORT
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Middlewares */
|
/* Middlewares */
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
app.use(morgan('dev'))
|
||||||
|
} else if (process.env.NODE_ENV === 'production') {
|
||||||
|
app.use(redirectToHTTPS())
|
||||||
|
}
|
||||||
app.use(helmet())
|
app.use(helmet())
|
||||||
app.use(morgan('dev'))
|
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(redirectToHTTPS([/localhost:(\d{4})/]))
|
|
||||||
|
|
||||||
/* EJS Template Engines */
|
/* EJS Template Engines */
|
||||||
app.set('view engine', 'ejs')
|
app.set('view engine', 'ejs')
|
||||||
@ -77,7 +80,7 @@ app.use((error, _req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/* Server */
|
/* Server */
|
||||||
const PORT = process.env.PORT || 8000
|
const PORT = process.env.PORT || 7000
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log('\x1b[36m%s\x1b[0m', `Started on port ${PORT}.`)
|
console.log('\x1b[36m%s\x1b[0m', `Started on port ${PORT}.`)
|
||||||
})
|
})
|
||||||
|
1707
s.divlo.fr/package-lock.json
generated
1707
s.divlo.fr/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,16 +9,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"ejs": "^3.1.3",
|
"ejs": "^3.1.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-http-to-https": "^1.1.4",
|
"express-http-to-https": "^1.1.4",
|
||||||
"helmet": "^4.0.0",
|
"helmet": "^4.1.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"mysql": "^2.18.1"
|
"mysql": "^2.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.6",
|
||||||
"snazzy": "^8.0.0",
|
"snazzy": "^9.0.0",
|
||||||
"standard": "^14.3.4"
|
"standard": "^16.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
website/.dockerignore
Normal file
2
website/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
@ -1 +1,3 @@
|
|||||||
NEXT_PUBLIC_API_URL = "http://localhost:8080"
|
NEXT_PUBLIC_API_URL = "http://localhost:8080"
|
||||||
|
CONTAINER_API_URL="http://functionproject-api:8080"
|
||||||
|
COMPOSE_PROJECT_NAME="function.divlo.fr-website"
|
||||||
|
9
website/Dockerfile
Normal file
9
website/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM node:14.15.0-alpine3.12
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY ./ ./
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev"]
|
@ -8,7 +8,7 @@ export default function Footer () {
|
|||||||
<Link href='/about'>
|
<Link href='/about'>
|
||||||
<a>FunctionProject</a>
|
<a>FunctionProject</a>
|
||||||
</Link>
|
</Link>
|
||||||
- Version 2.1 <br />
|
- Version 2.2 <br />
|
||||||
<a href='https://divlo.fr/' target='_blank' rel='noopener noreferrer'>
|
<a href='https://divlo.fr/' target='_blank' rel='noopener noreferrer'>
|
||||||
Divlo
|
Divlo
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
|
@ -23,15 +23,15 @@ const FunctionCard = memo(
|
|||||||
<Link
|
<Link
|
||||||
{...(props.isAdmin
|
{...(props.isAdmin
|
||||||
? {
|
? {
|
||||||
href: '/admin/[slug]',
|
href: '/admin/[slug]',
|
||||||
as: `/admin/${props.slug}`
|
as: `/admin/${props.slug}`
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
href: isFormOrArticle
|
href: isFormOrArticle
|
||||||
? '/functions/[slug]'
|
? '/functions/[slug]'
|
||||||
: `/functions/${props.slug}`,
|
: `/functions/${props.slug}`,
|
||||||
as: `/functions/${props.slug}`
|
as: `/functions/${props.slug}`
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* FunctionCard a une hauteur pendant chargement */}
|
{/* FunctionCard a une hauteur pendant chargement */}
|
||||||
<a
|
<a
|
||||||
|
@ -82,62 +82,68 @@ const CommentCard = forwardRef((props, ref) => {
|
|||||||
<a>{props.user.name}</a>
|
<a>{props.user.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
-{' '}
|
-{' '}
|
||||||
{date.format(new Date(props.createdAt), 'DD/MM/YYYY à HH:mm', false)}
|
{date.format(
|
||||||
|
new Date(props.createdAt),
|
||||||
|
'DD/MM/YYYY à HH:mm',
|
||||||
|
false
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
<div className='col-24'>
|
<div className='col-24'>
|
||||||
{!isEditing ? (
|
{!isEditing
|
||||||
<>
|
? (
|
||||||
<div className='CommentCard__message'>
|
<>
|
||||||
<ReactMarkdown
|
<div className='CommentCard__message'>
|
||||||
source={editInput}
|
<ReactMarkdown
|
||||||
renderers={{ code: CodeBlock }}
|
source={editInput}
|
||||||
/>
|
renderers={{ code: CodeBlock }}
|
||||||
</div>
|
/>
|
||||||
{isAuth && user.name === props.user.name && (
|
</div>
|
||||||
<p
|
{isAuth && user.name === props.user.name && (
|
||||||
style={{
|
<p
|
||||||
fontSize: '15px',
|
style={{
|
||||||
margin: '15px 0 0 0',
|
fontSize: '15px',
|
||||||
fontStyle: 'italic'
|
margin: '15px 0 0 0',
|
||||||
}}
|
fontStyle: 'italic'
|
||||||
>
|
}}
|
||||||
<a onClick={deleteCommentById} href='#'>
|
>
|
||||||
supprimer
|
<a onClick={deleteCommentById} href='#'>
|
||||||
</a>
|
supprimer
|
||||||
|
</a>
|
||||||
-
|
-
|
||||||
<a style={{ cursor: 'pointer' }} onClick={editComment}>
|
<a style={{ cursor: 'pointer' }} onClick={editComment}>
|
||||||
modifier
|
modifier
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className='form-group FunctionComments__post-group'>
|
||||||
|
<label className='form-label' htmlFor='commentEdit'>
|
||||||
|
Modifier le commentaire :
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
style={{ height: 'auto' }}
|
||||||
|
value={editInput}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='commentEdit'
|
||||||
|
id='commentEdit'
|
||||||
|
className='form-control'
|
||||||
|
rows='5'
|
||||||
|
placeholder="Idée d'amélioration, avis, remarque, partage d'expérience personnel, ... (Markdown autorisé)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='form-group' style={{ marginTop: '0.7em' }}>
|
||||||
|
<button type='submit' className='btn btn-dark'>
|
||||||
|
Envoyer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='text-center'>{htmlParser(message)}</div>
|
||||||
|
</form>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className='form-group FunctionComments__post-group'>
|
|
||||||
<label className='form-label' htmlFor='commentEdit'>
|
|
||||||
Modifier le commentaire :
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
style={{ height: 'auto' }}
|
|
||||||
value={editInput}
|
|
||||||
onChange={handleChange}
|
|
||||||
name='commentEdit'
|
|
||||||
id='commentEdit'
|
|
||||||
className='form-control'
|
|
||||||
rows='5'
|
|
||||||
placeholder="Idée d'amélioration, avis, remarque, partage d'expérience personnel, ... (Markdown autorisé)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='form-group' style={{ marginTop: '0.7em' }}>
|
|
||||||
<button type='submit' className='btn btn-dark'>
|
|
||||||
Envoyer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='text-center'>{htmlParser(message)}</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,11 +3,13 @@ import htmlParser from 'html-react-parser'
|
|||||||
const FunctionArticle = ({ article }) => {
|
const FunctionArticle = ({ article }) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: '50px' }} className='container-fluid'>
|
<div style={{ marginBottom: '50px' }} className='container-fluid'>
|
||||||
{article != null ? (
|
{article != null
|
||||||
htmlParser(article)
|
? (
|
||||||
) : (
|
htmlParser(article)
|
||||||
<p className='text-center'>L'article n'est pas encore disponible.</p>
|
)
|
||||||
)}
|
: (
|
||||||
|
<p className='text-center'>L'article n'est pas encore disponible.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -90,36 +90,38 @@ const FunctionComments = ({ functionId }) => {
|
|||||||
<div className='FunctionComments__post container-fluid'>
|
<div className='FunctionComments__post container-fluid'>
|
||||||
<div className='row FunctionComments__row'>
|
<div className='row FunctionComments__row'>
|
||||||
<div className='col-24'>
|
<div className='col-24'>
|
||||||
{isAuth ? (
|
{isAuth
|
||||||
<form onSubmit={handleSubmit}>
|
? (
|
||||||
<div className='form-group FunctionComments__post-group'>
|
<form onSubmit={handleSubmit}>
|
||||||
<label className='form-label' htmlFor='commentPost'>
|
<div className='form-group FunctionComments__post-group'>
|
||||||
Ajouter un commentaire :
|
<label className='form-label' htmlFor='commentPost'>
|
||||||
</label>
|
Ajouter un commentaire :
|
||||||
<textarea
|
</label>
|
||||||
className='FunctionComments__textarea form-control'
|
<textarea
|
||||||
value={inputState.commentPost}
|
className='FunctionComments__textarea form-control'
|
||||||
onChange={handleChange}
|
value={inputState.commentPost}
|
||||||
name='commentPost'
|
onChange={handleChange}
|
||||||
id='commentPost'
|
name='commentPost'
|
||||||
placeholder="Idée d'amélioration, avis, remarque, partage d'expérience personnel, ... (Markdown autorisé)"
|
id='commentPost'
|
||||||
/>
|
placeholder="Idée d'amélioration, avis, remarque, partage d'expérience personnel, ... (Markdown autorisé)"
|
||||||
</div>
|
/>
|
||||||
<div className='form-group' style={{ marginTop: '0.7em' }}>
|
</div>
|
||||||
<button type='submit' className='btn btn-dark'>
|
<div className='form-group' style={{ marginTop: '0.7em' }}>
|
||||||
Envoyer
|
<button type='submit' className='btn btn-dark'>
|
||||||
</button>
|
Envoyer
|
||||||
</div>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
) : (
|
</form>
|
||||||
<p className='text-center'>
|
)
|
||||||
Vous devez être{' '}
|
: (
|
||||||
<Link href='/users/login'>
|
<p className='text-center'>
|
||||||
<a>connecté</a>
|
Vous devez être{' '}
|
||||||
</Link>{' '}
|
<Link href='/users/login'>
|
||||||
pour poster un commentaire.
|
<a>connecté</a>
|
||||||
</p>
|
</Link>{' '}
|
||||||
)}
|
pour poster un commentaire.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,35 +51,37 @@ export default function Header () {
|
|||||||
<NavigationLink name='Accueil' path='/' />
|
<NavigationLink name='Accueil' path='/' />
|
||||||
<NavigationLink name='Fonctions' path='/functions' />
|
<NavigationLink name='Fonctions' path='/functions' />
|
||||||
<NavigationLink name='Utilisateurs' path='/users' />
|
<NavigationLink name='Utilisateurs' path='/users' />
|
||||||
{!isAuth ? (
|
{!isAuth
|
||||||
<>
|
? (
|
||||||
<NavigationLink name="S'inscrire" path='/users/register' />
|
<>
|
||||||
<NavigationLink name='Connexion' path='/users/login' />
|
<NavigationLink name="S'inscrire" path='/users/register' />
|
||||||
</>
|
<NavigationLink name='Connexion' path='/users/login' />
|
||||||
) : (
|
</>
|
||||||
<>
|
)
|
||||||
<li className='navbar-item'>
|
: (
|
||||||
<Link href='/users/[name]' as={`/users/${user.name}`}>
|
<>
|
||||||
<a
|
<li className='navbar-item'>
|
||||||
className={`navbar-link ${
|
<Link href='/users/[name]' as={`/users/${user.name}`}>
|
||||||
|
<a
|
||||||
|
className={`navbar-link ${
|
||||||
pathname === '/users/[name]'
|
pathname === '/users/[name]'
|
||||||
? 'navbar-link-active'
|
? 'navbar-link-active'
|
||||||
: null
|
: null
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Mon Profil
|
Mon Profil
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className='navbar-item'>
|
<li className='navbar-item'>
|
||||||
<Link href='/'>
|
<Link href='/'>
|
||||||
<a onClick={logoutUser} className='navbar-link'>
|
<a onClick={logoutUser} className='navbar-link'>
|
||||||
Se déconnecter
|
Se déconnecter
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isAuth && user.isAdmin && (
|
{isAuth && user.isAdmin && (
|
||||||
<NavigationLink name='Admin' path='/admin' />
|
<NavigationLink name='Admin' path='/admin' />
|
||||||
)}
|
)}
|
||||||
|
4270
website/package-lock.json
generated
4270
website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,47 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"description": "Website frontend for FunctionProject",
|
"description": "Website frontend for FunctionProject",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_ENV=development node server",
|
"dev:custom": "cross-env NODE_ENV=development node server",
|
||||||
|
"start:custom": "cross-env NODE_ENV=production node server",
|
||||||
|
"dev": "next",
|
||||||
|
"start": "next start",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"export": "next export",
|
"export": "next export",
|
||||||
"start": "cross-env NODE_ENV=production node server",
|
"format": "standard \"./**/*.{js,jsx}\" --fix | snazzy"
|
||||||
"format": "standard \"./**/*.{js,jsx}\" --fix | snazzy || exit 0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.13.0",
|
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.13.0",
|
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.9",
|
"@fortawesome/react-fontawesome": "^0.1.12",
|
||||||
"@zeit/next-css": "^1.0.1",
|
"@zeit/next-css": "^1.0.1",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.21.0",
|
||||||
"date-and-time": "^0.13.1",
|
"date-and-time": "^0.14.1",
|
||||||
"date-fns": "^2.12.0",
|
"date-fns": "^2.16.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-http-to-https": "^1.1.4",
|
"express-http-to-https": "^1.1.4",
|
||||||
"html-react-parser": "^0.10.2",
|
"html-react-parser": "^0.14.0",
|
||||||
"next": "^9.5.4",
|
"next": "^10.0.0",
|
||||||
"next-fonts": "^1.0.3",
|
"next-fonts": "^1.4.0",
|
||||||
"notyf": "^3.6.0",
|
"notyf": "^3.9.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"react": "16.13.0",
|
"react": "17.0.1",
|
||||||
"react-codepen-embed": "^1.0.1",
|
"react-codepen-embed": "^1.0.1",
|
||||||
"react-color": "^2.18.0",
|
"react-color": "^2.19.3",
|
||||||
"react-datepicker": "^2.14.1",
|
"react-datepicker": "^3.3.0",
|
||||||
"react-dom": "16.13.0",
|
"react-dom": "17.0.1",
|
||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^5.0.2",
|
||||||
"react-swipeable-views": "^0.13.9",
|
"react-swipeable-views": "^0.13.9",
|
||||||
"react-swipeable-views-utils": "^0.13.9",
|
"react-swipeable-views-utils": "^0.13.9",
|
||||||
"react-syntax-highlighter": "^12.2.1",
|
"react-syntax-highlighter": "^15.3.0",
|
||||||
"suneditor-react": "^2.8.0",
|
"suneditor-react": "^2.8.0",
|
||||||
"universal-cookie": "^4.0.3"
|
"universal-cookie": "^4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"snazzy": "^8.0.0",
|
"snazzy": "^9.0.0",
|
||||||
"standard": "^14.3.4"
|
"standard": "^16.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,64 +23,66 @@ const Admin = props => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Création d'une fonction */}
|
{/* Création d'une fonction */}
|
||||||
{isOpen ? (
|
{isOpen
|
||||||
<Modal toggleModal={toggleModal}>
|
? (
|
||||||
<div className='Admin__Modal__container container-fluid'>
|
<Modal toggleModal={toggleModal}>
|
||||||
<div className='Admin__Modal__row row'>
|
<div className='Admin__Modal__container container-fluid'>
|
||||||
<div className='col-24'>
|
<div className='Admin__Modal__row row'>
|
||||||
<div className='Admin__Modal-top-container row'>
|
<div className='col-24'>
|
||||||
<div className='col-24'>
|
<div className='Admin__Modal-top-container row'>
|
||||||
<span
|
<div className='col-24'>
|
||||||
onClick={toggleModal}
|
<span
|
||||||
style={{
|
onClick={toggleModal}
|
||||||
cursor: 'pointer',
|
style={{
|
||||||
position: 'absolute',
|
cursor: 'pointer',
|
||||||
left: 0
|
position: 'absolute',
|
||||||
}}
|
left: 0
|
||||||
>
|
}}
|
||||||
<FontAwesomeIcon
|
>
|
||||||
icon={faTimes}
|
<FontAwesomeIcon
|
||||||
style={{ width: '1.5rem', color: 'red' }}
|
icon={faTimes}
|
||||||
/>
|
style={{ width: '1.5rem', color: 'red' }}
|
||||||
</span>
|
/>
|
||||||
<h2 className='text-center'>Crée une nouvelle fonction</h2>
|
</span>
|
||||||
|
<h2 className='text-center'>Crée une nouvelle fonction</h2>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='col-24'>
|
<div className='col-24'>
|
||||||
<AddEditFunction
|
<AddEditFunction
|
||||||
defaultInputState={{ type: 'form' }}
|
defaultInputState={{ type: 'form' }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</Modal>
|
)
|
||||||
) : (
|
: (
|
||||||
<FunctionsList isAdmin token={props.user.token}>
|
<FunctionsList isAdmin token={props.user.token}>
|
||||||
<div className='col-24'>
|
<div className='col-24'>
|
||||||
<h1 className='Functions__title'>Administration</h1>
|
<h1 className='Functions__title'>Administration</h1>
|
||||||
<button
|
<button
|
||||||
onClick={toggleModal}
|
onClick={toggleModal}
|
||||||
style={{ margin: '0 0 40px 0' }}
|
style={{ margin: '0 0 40px 0' }}
|
||||||
className='btn btn-dark'
|
className='btn btn-dark'
|
||||||
>
|
>
|
||||||
Crée une nouvelle fonction
|
Crée une nouvelle fonction
|
||||||
</button>
|
|
||||||
<Link href='/admin/manageCategories'>
|
|
||||||
<button style={{ margin: '0 0 0 20px' }} className='btn btn-dark'>
|
|
||||||
Gérer les catégories
|
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
<Link href='/admin/manageCategories'>
|
||||||
<Link href='/admin/manageQuotes'>
|
<button style={{ margin: '0 0 0 20px' }} className='btn btn-dark'>
|
||||||
<button style={{ margin: '0 0 0 20px' }} className='btn btn-dark'>
|
Gérer les catégories
|
||||||
Gérer les citations
|
</button>
|
||||||
</button>
|
</Link>
|
||||||
</Link>
|
<Link href='/admin/manageQuotes'>
|
||||||
</div>
|
<button style={{ margin: '0 0 0 20px' }} className='btn btn-dark'>
|
||||||
</FunctionsList>
|
Gérer les citations
|
||||||
)}
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</FunctionsList>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -159,119 +159,121 @@ const manageCategories = props => {
|
|||||||
description="Page d'administration de FunctionProject. Gérer les catégories."
|
description="Page d'administration de FunctionProject. Gérer les catégories."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isOpen ? (
|
{isOpen
|
||||||
<Modal>
|
? (
|
||||||
<AddEditCategory
|
<Modal>
|
||||||
handleToggleModal={toggleModal}
|
<AddEditCategory
|
||||||
defaultInputState={defaultInputState}
|
handleToggleModal={toggleModal}
|
||||||
{...props}
|
defaultInputState={defaultInputState}
|
||||||
isEditing={isEditing}
|
{...props}
|
||||||
/>
|
isEditing={isEditing}
|
||||||
</Modal>
|
/>
|
||||||
) : (
|
</Modal>
|
||||||
<div className='container-fluid text-center'>
|
)
|
||||||
<div className='row justify-content-center'>
|
: (
|
||||||
<div className='col-24'>
|
<div className='container-fluid text-center'>
|
||||||
<h1>Gérer les catégories</h1>
|
<div className='row justify-content-center'>
|
||||||
<button
|
<div className='col-24'>
|
||||||
onClick={() => {
|
<h1>Gérer les catégories</h1>
|
||||||
setDefaultInputState(defaultCategoryState)
|
<button
|
||||||
toggleModal()
|
onClick={() => {
|
||||||
setIsEditing(false)
|
setDefaultInputState(defaultCategoryState)
|
||||||
}}
|
toggleModal()
|
||||||
style={{ margin: '0 0 40px 0' }}
|
setIsEditing(false)
|
||||||
className='btn btn-dark'
|
}}
|
||||||
>
|
style={{ margin: '0 0 40px 0' }}
|
||||||
Ajouter une catégorie
|
className='btn btn-dark'
|
||||||
</button>
|
>
|
||||||
|
Ajouter une catégorie
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='row justify-content-center'>
|
||||||
<div className='row justify-content-center'>
|
<div className='container-fluid'>
|
||||||
<div className='container-fluid'>
|
<div className='col-24 table-column'>
|
||||||
<div className='col-24 table-column'>
|
<table className='table'>
|
||||||
<table className='table'>
|
<thead>
|
||||||
<thead>
|
<tr>
|
||||||
<tr>
|
<th className='table-row' scope='col'>
|
||||||
<th className='table-row' scope='col'>
|
id
|
||||||
id
|
</th>
|
||||||
</th>
|
<th className='table-row' scope='col'>
|
||||||
<th className='table-row' scope='col'>
|
name
|
||||||
name
|
</th>
|
||||||
</th>
|
<th className='table-row' scope='col'>
|
||||||
<th className='table-row' scope='col'>
|
color
|
||||||
color
|
</th>
|
||||||
</th>
|
<th className='table-row' scope='col'>
|
||||||
<th className='table-row' scope='col'>
|
createdAt
|
||||||
createdAt
|
</th>
|
||||||
</th>
|
<th className='table-row' scope='col'>
|
||||||
<th className='table-row' scope='col'>
|
updatedAt
|
||||||
updatedAt
|
</th>
|
||||||
</th>
|
<th className='table-row' scope='col'>
|
||||||
<th className='table-row' scope='col'>
|
Modifier
|
||||||
Modifier
|
</th>
|
||||||
</th>
|
<th className='table-row' scope='col'>
|
||||||
<th className='table-row' scope='col'>
|
Supprimer
|
||||||
Supprimer
|
</th>
|
||||||
</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{categories.map(category => {
|
||||||
{categories.map(category => {
|
return (
|
||||||
return (
|
<tr
|
||||||
<tr
|
key={category.id}
|
||||||
key={category.id}
|
style={{ backgroundColor: category.color }}
|
||||||
style={{ backgroundColor: category.color }}
|
|
||||||
>
|
|
||||||
<td className='table-row'>{category.id}</td>
|
|
||||||
<td className='table-row'>{category.name}</td>
|
|
||||||
<td className='table-row'>{category.color}</td>
|
|
||||||
<td className='table-row'>
|
|
||||||
{date.format(
|
|
||||||
new Date(category.createdAt),
|
|
||||||
'DD/MM/YYYY à HH:mm',
|
|
||||||
false
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className='table-row'>
|
|
||||||
{date.format(
|
|
||||||
new Date(category.updatedAt),
|
|
||||||
'DD/MM/YYYY à HH:mm',
|
|
||||||
false
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() =>
|
|
||||||
handleEditCategory({
|
|
||||||
name: category.name,
|
|
||||||
color: category.color,
|
|
||||||
id: category.id
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<td className='table-row'>{category.id}</td>
|
||||||
icon={faPen}
|
<td className='table-row'>{category.name}</td>
|
||||||
style={{ width: '1.5rem' }}
|
<td className='table-row'>{category.color}</td>
|
||||||
/>
|
<td className='table-row'>
|
||||||
</td>
|
{date.format(
|
||||||
<td
|
new Date(category.createdAt),
|
||||||
style={{ cursor: 'pointer' }}
|
'DD/MM/YYYY à HH:mm',
|
||||||
onClick={() => handleRemoveCategory(category.id)}
|
false
|
||||||
>
|
)}
|
||||||
<FontAwesomeIcon
|
</td>
|
||||||
icon={faTrash}
|
<td className='table-row'>
|
||||||
style={{ width: '1.5rem' }}
|
{date.format(
|
||||||
/>
|
new Date(category.updatedAt),
|
||||||
</td>
|
'DD/MM/YYYY à HH:mm',
|
||||||
</tr>
|
false
|
||||||
)
|
)}
|
||||||
})}
|
</td>
|
||||||
</tbody>
|
<td
|
||||||
</table>
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() =>
|
||||||
|
handleEditCategory({
|
||||||
|
name: category.name,
|
||||||
|
color: category.color,
|
||||||
|
id: category.id
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faPen}
|
||||||
|
style={{ width: '1.5rem' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => handleRemoveCategory(category.id)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTrash}
|
||||||
|
style={{ width: '1.5rem' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -266,144 +266,146 @@ const LinksList = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='row justify-content-center'>
|
<div className='row justify-content-center'>
|
||||||
<div className='container-fluid'>
|
<div className='container-fluid'>
|
||||||
{!isEditing ? (
|
{!isEditing
|
||||||
<div className='col-24 table-column'>
|
? (
|
||||||
<table className='table' style={{ marginBottom: '40px' }}>
|
<div className='col-24 table-column'>
|
||||||
<thead>
|
<table className='table' style={{ marginBottom: '40px' }}>
|
||||||
<tr>
|
<thead>
|
||||||
<th className='table-row' scope='col'>
|
<tr>
|
||||||
Liens
|
<th className='table-row' scope='col'>
|
||||||
</th>
|
Liens
|
||||||
<th className='table-row' scope='col'>
|
</th>
|
||||||
Nom
|
<th className='table-row' scope='col'>
|
||||||
</th>
|
Nom
|
||||||
<th className='table-row' scope='col'>
|
</th>
|
||||||
Compteur de clics
|
<th className='table-row' scope='col'>
|
||||||
</th>
|
Compteur de clics
|
||||||
<th className='table-row' scope='col'>
|
</th>
|
||||||
Modifier
|
<th className='table-row' scope='col'>
|
||||||
</th>
|
Modifier
|
||||||
<th className='table-row' scope='col'>
|
</th>
|
||||||
Supprimer
|
<th className='table-row' scope='col'>
|
||||||
</th>
|
Supprimer
|
||||||
</tr>
|
</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{linksData.rows.map((link, index) => {
|
<tbody>
|
||||||
const linkJSX = (
|
{linksData.rows.map((link, index) => {
|
||||||
<>
|
const linkJSX = (
|
||||||
<td className='table-row'>
|
<>
|
||||||
<a href={link.url}>{link.url}</a>
|
<td className='table-row'>
|
||||||
</td>
|
<a href={link.url}>{link.url}</a>
|
||||||
<td className='table-row'>
|
</td>
|
||||||
<a href={`https://s.divlo.fr/${link.shortcut}`}>
|
<td className='table-row'>
|
||||||
{link.shortcut}
|
<a href={`https://s.divlo.fr/${link.shortcut}`}>
|
||||||
</a>
|
{link.shortcut}
|
||||||
</td>
|
</a>
|
||||||
<td className='table-row'>{link.count}</td>
|
</td>
|
||||||
<td
|
<td className='table-row'>{link.count}</td>
|
||||||
style={{ cursor: 'pointer' }}
|
<td
|
||||||
onClick={() => handleEditLink(link)}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
onClick={() => handleEditLink(link)}
|
||||||
<FontAwesomeIcon
|
>
|
||||||
icon={faPen}
|
<FontAwesomeIcon
|
||||||
style={{ width: '1.5rem' }}
|
icon={faPen}
|
||||||
/>
|
style={{ width: '1.5rem' }}
|
||||||
</td>
|
/>
|
||||||
<td
|
</td>
|
||||||
style={{ cursor: 'pointer' }}
|
<td
|
||||||
onClick={() => handleRemoveLink(link.id)}
|
style={{ cursor: 'pointer' }}
|
||||||
>
|
onClick={() => handleRemoveLink(link.id)}
|
||||||
<FontAwesomeIcon
|
>
|
||||||
icon={faTrash}
|
<FontAwesomeIcon
|
||||||
style={{ width: '1.5rem' }}
|
icon={faTrash}
|
||||||
/>
|
style={{ width: '1.5rem' }}
|
||||||
</td>
|
/>
|
||||||
</>
|
</td>
|
||||||
)
|
</>
|
||||||
// Si c'est le dernier élément
|
|
||||||
if (linksData.rows.length === index + 1) {
|
|
||||||
return (
|
|
||||||
<tr key={index} ref={lastLinkRef}>
|
|
||||||
{linkJSX}
|
|
||||||
</tr>
|
|
||||||
)
|
)
|
||||||
}
|
// Si c'est le dernier élément
|
||||||
return <tr key={index}>{linkJSX}</tr>
|
if (linksData.rows.length === index + 1) {
|
||||||
})}
|
return (
|
||||||
</tbody>
|
<tr key={index} ref={lastLinkRef}>
|
||||||
</table>
|
{linkJSX}
|
||||||
</div>
|
</tr>
|
||||||
) : (
|
)
|
||||||
<Modal>
|
}
|
||||||
<div className='Admin__Modal__container container-fluid'>
|
return <tr key={index}>{linkJSX}</tr>
|
||||||
<div className='Admin__Modal__row row'>
|
})}
|
||||||
<div className='col-24'>
|
</tbody>
|
||||||
<div className='Admin__Modal-top-container row'>
|
</table>
|
||||||
<div className='col-24'>
|
</div>
|
||||||
<span
|
)
|
||||||
onClick={toggleModal}
|
: (
|
||||||
style={{
|
<Modal>
|
||||||
cursor: 'pointer',
|
<div className='Admin__Modal__container container-fluid'>
|
||||||
position: 'absolute',
|
<div className='Admin__Modal__row row'>
|
||||||
left: 0
|
<div className='col-24'>
|
||||||
}}
|
<div className='Admin__Modal-top-container row'>
|
||||||
>
|
<div className='col-24'>
|
||||||
<FontAwesomeIcon
|
<span
|
||||||
icon={faTimes}
|
onClick={toggleModal}
|
||||||
style={{ width: '1.5rem', color: 'red' }}
|
style={{
|
||||||
/>
|
cursor: 'pointer',
|
||||||
</span>
|
position: 'absolute',
|
||||||
|
left: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTimes}
|
||||||
|
style={{ width: '1.5rem', color: 'red' }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='col-24'>
|
<div className='col-24'>
|
||||||
<form onSubmit={handleEditSubmit}>
|
<form onSubmit={handleEditSubmit}>
|
||||||
<div className='form-group'>
|
<div className='form-group'>
|
||||||
<label className='form-label' htmlFor='url'>
|
<label className='form-label' htmlFor='url'>
|
||||||
Entrez le lien à raccourcir :
|
Entrez le lien à raccourcir :
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={defaultInputState.url}
|
value={defaultInputState.url}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
type='text'
|
type='text'
|
||||||
name='url'
|
name='url'
|
||||||
id='url'
|
id='url'
|
||||||
placeholder='(e.g : https://divlo.fr)'
|
placeholder='(e.g : https://divlo.fr)'
|
||||||
className='form-control'
|
className='form-control'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='form-group'>
|
<div className='form-group'>
|
||||||
<label className='form-label' htmlFor='shortcutName'>
|
<label className='form-label' htmlFor='shortcutName'>
|
||||||
Entrez le nom du raccourci :
|
Entrez le nom du raccourci :
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={defaultInputState.shortcutName}
|
value={defaultInputState.shortcutName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
type='text'
|
type='text'
|
||||||
name='shortcutName'
|
name='shortcutName'
|
||||||
id='shortcutName'
|
id='shortcutName'
|
||||||
placeholder='(e.g : divlo)'
|
placeholder='(e.g : divlo)'
|
||||||
className='form-control'
|
className='form-control'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='form-group text-center'>
|
<div className='form-group text-center'>
|
||||||
<button type='submit' className='btn btn-dark'>
|
<button type='submit' className='btn btn-dark'>
|
||||||
Envoyer
|
Envoyer
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className='form-result text-center'>
|
||||||
|
{isLoading ? <Loader /> : htmlParser(message)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<div className='form-result text-center'>
|
|
||||||
{isLoading ? <Loader /> : htmlParser(message)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
</Modal>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,104 +54,110 @@ const PlayRightPrice = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='container-fluid'>
|
<div className='container-fluid'>
|
||||||
{!isPlaying ? (
|
{!isPlaying
|
||||||
<div className='row justify-content-center'>
|
? (
|
||||||
<div className='form-group text-center'>
|
|
||||||
<button
|
|
||||||
onClick={handlePlaying}
|
|
||||||
type='submit'
|
|
||||||
className='btn btn-dark'
|
|
||||||
>
|
|
||||||
Jouer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isLoadingProduct ? (
|
|
||||||
<div className='row justify-content-center'>
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className='row justify-content-center'>
|
<div className='row justify-content-center'>
|
||||||
<div
|
<div className='form-group text-center'>
|
||||||
style={{ marginBottom: '20px' }}
|
<button
|
||||||
className='col-24 text-center'
|
onClick={handlePlaying}
|
||||||
>
|
type='submit'
|
||||||
<h4>{productToGuess.name}</h4>
|
className='btn btn-dark'
|
||||||
<img
|
>
|
||||||
src={productToGuess.image}
|
Jouer
|
||||||
alt={productToGuess.name}
|
</button>
|
||||||
className='Product__image'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
<div className='row justify-content-center'>
|
: isLoadingProduct
|
||||||
<div style={{ marginBottom: '25px' }} className='col-24'>
|
? (
|
||||||
{attemptsArray.length > 0 &&
|
<div className='row justify-content-center'>
|
||||||
attemptsArray[0].message ===
|
<Loader />
|
||||||
'Bravo, vous avez trouvé le juste prix !' ? (
|
|
||||||
<div className='form-group text-center'>
|
|
||||||
<button
|
|
||||||
onClick={handlePlaying}
|
|
||||||
type='submit'
|
|
||||||
className='btn btn-dark'
|
|
||||||
>
|
|
||||||
Rejouer ?
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className='text-center'>
|
|
||||||
<input
|
|
||||||
value={enteredPrice}
|
|
||||||
onChange={handleChange}
|
|
||||||
name='enteredPrice'
|
|
||||||
id='enteredPrice'
|
|
||||||
type='number'
|
|
||||||
step='0.01'
|
|
||||||
className='form-control'
|
|
||||||
autoComplete='off'
|
|
||||||
placeholder='Devinez le prix (prix à virgule possible!)'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='form-group text-center'>
|
|
||||||
<button type='submit' className='btn btn-dark'>
|
|
||||||
Deviner
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
|
: (
|
||||||
<div
|
<>
|
||||||
style={{ marginBottom: '30px' }}
|
<div className='row justify-content-center'>
|
||||||
className='row justify-content-center'
|
|
||||||
>
|
|
||||||
{attemptsArray.map((attempt, index) => {
|
|
||||||
const { message } = attempt
|
|
||||||
let priceResultClass
|
|
||||||
if (message === "C'est moins !") {
|
|
||||||
priceResultClass = 'Price__result-moins'
|
|
||||||
} else if (message === "C'est plus !") {
|
|
||||||
priceResultClass = 'Price__result-plus'
|
|
||||||
} else {
|
|
||||||
priceResultClass = 'Price__result-success'
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
key={index}
|
style={{ marginBottom: '20px' }}
|
||||||
className={`col-24 Price__result ${priceResultClass}`}
|
className='col-24 text-center'
|
||||||
>
|
>
|
||||||
# {attempt.numberTry} ({attempt.guessedPrice}) {message}
|
<h4>{productToGuess.name}</h4>
|
||||||
|
<img
|
||||||
|
src={productToGuess.image}
|
||||||
|
alt={productToGuess.name}
|
||||||
|
className='Product__image'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
})}
|
|
||||||
</div>
|
<div className='row justify-content-center'>
|
||||||
</>
|
<div style={{ marginBottom: '25px' }} className='col-24'>
|
||||||
)}
|
{attemptsArray.length > 0 &&
|
||||||
|
attemptsArray[0].message ===
|
||||||
|
'Bravo, vous avez trouvé le juste prix !'
|
||||||
|
? (
|
||||||
|
<div className='form-group text-center'>
|
||||||
|
<button
|
||||||
|
onClick={handlePlaying}
|
||||||
|
type='submit'
|
||||||
|
className='btn btn-dark'
|
||||||
|
>
|
||||||
|
Rejouer ?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className='text-center'>
|
||||||
|
<input
|
||||||
|
value={enteredPrice}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='enteredPrice'
|
||||||
|
id='enteredPrice'
|
||||||
|
type='number'
|
||||||
|
step='0.01'
|
||||||
|
className='form-control'
|
||||||
|
autoComplete='off'
|
||||||
|
placeholder='Devinez le prix (prix à virgule possible!)'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group text-center'>
|
||||||
|
<button type='submit' className='btn btn-dark'>
|
||||||
|
Deviner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{ marginBottom: '30px' }}
|
||||||
|
className='row justify-content-center'
|
||||||
|
>
|
||||||
|
{attemptsArray.map((attempt, index) => {
|
||||||
|
const { message } = attempt
|
||||||
|
let priceResultClass
|
||||||
|
if (message === "C'est moins !") {
|
||||||
|
priceResultClass = 'Price__result-moins'
|
||||||
|
} else if (message === "C'est plus !") {
|
||||||
|
priceResultClass = 'Price__result-plus'
|
||||||
|
} else {
|
||||||
|
priceResultClass = 'Price__result-success'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`col-24 Price__result ${priceResultClass}`}
|
||||||
|
>
|
||||||
|
# {attempt.numberTry} ({attempt.guessedPrice}) {message}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -84,299 +84,301 @@ const Profile = props => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Édition du profil */}
|
{/* Édition du profil */}
|
||||||
{isOpen ? (
|
{isOpen
|
||||||
<Modal toggleModal={toggleModal}>
|
? (
|
||||||
<div className='Profile__container container-fluid'>
|
<Modal toggleModal={toggleModal}>
|
||||||
<div className='Profile__row row'>
|
<div className='Profile__container container-fluid'>
|
||||||
<div className='col-24'>
|
<div className='Profile__row row'>
|
||||||
<div className='Profile__Modal-top-container row'>
|
<div className='col-24'>
|
||||||
<div className='col-24'>
|
<div className='Profile__Modal-top-container row'>
|
||||||
<span
|
<div className='col-24'>
|
||||||
|
<span
|
||||||
|
onClick={toggleModal}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTimes}
|
||||||
|
style={{ width: '1.5rem', color: 'red' }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<h2 className='text-center'>Éditer le profil</h2>
|
||||||
|
<p className='text-center'>
|
||||||
|
<em>
|
||||||
|
(Vous devrez vous reconnecter après la sauvegarde){' '}
|
||||||
|
<br /> Si vous changez votre adresse email, vous devrez
|
||||||
|
la confirmer comme à l'inscription (vérifier vos
|
||||||
|
emails).
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='col-24'>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label className='form-label' htmlFor='name'>
|
||||||
|
Nom :
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={inputState.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
type='text'
|
||||||
|
name='name'
|
||||||
|
id='name'
|
||||||
|
className='form-control'
|
||||||
|
placeholder='Divlo'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label className='form-label' htmlFor='email'>
|
||||||
|
Email :
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
value={inputState.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
type='email'
|
||||||
|
name='email'
|
||||||
|
id='email'
|
||||||
|
className='form-control'
|
||||||
|
placeholder='email@gmail.com'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group custom-control custom-switch'>
|
||||||
|
<input
|
||||||
|
onChange={event => handleChange(event, true)}
|
||||||
|
type='checkbox'
|
||||||
|
name='isPublicEmail'
|
||||||
|
checked={inputState.isPublicEmail}
|
||||||
|
className='custom-control-input'
|
||||||
|
id='isPublicEmail'
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className='custom-control-label'
|
||||||
|
htmlFor='isPublicEmail'
|
||||||
|
>
|
||||||
|
Email Public
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label className='form-label' htmlFor='biography'>
|
||||||
|
Biographie :
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
style={{ height: 'auto' }}
|
||||||
|
value={inputState.biography}
|
||||||
|
onChange={handleChange}
|
||||||
|
name='biography'
|
||||||
|
id='biography'
|
||||||
|
className='form-control'
|
||||||
|
rows='5'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label className='form-label' htmlFor='logo'>
|
||||||
|
Logo <em>(400x400 recommandé)</em> :
|
||||||
|
</label>
|
||||||
|
<p style={{ margin: 0 }}>
|
||||||
|
<em>
|
||||||
|
Si aucun fichier est choisi, le logo ne sera pas
|
||||||
|
modifié.
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
onChange={handleChange}
|
||||||
|
accept='image/jpeg,image/jpg,image/png,image/gif'
|
||||||
|
type='file'
|
||||||
|
name='logo'
|
||||||
|
id='logo'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group text-center'>
|
||||||
|
<button type='submit' className='btn btn-dark'>
|
||||||
|
Sauvegarder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className='form-result text-center'>
|
||||||
|
{isLoading ? <Loader /> : htmlParser(message)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className='container-fluid Profile__container'>
|
||||||
|
<div className='row Profile__row'>
|
||||||
|
<div className='col-20'>
|
||||||
|
<div className='text-center'>
|
||||||
|
<h1>
|
||||||
|
Profil de <span className='important'>{props.name}</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className='row justify-content-center'>
|
||||||
|
<div className='col-24 text-center'>
|
||||||
|
<img
|
||||||
|
className='Profile__logo'
|
||||||
|
src={API_URL + props.logo}
|
||||||
|
alt={props.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='col-24 text-center'>
|
||||||
|
{props.biography != null && <p>{props.biography}</p>}
|
||||||
|
{props.email != null && (
|
||||||
|
<p>
|
||||||
|
<span className='important'>Email :</span> {props.email}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<span className='important'>
|
||||||
|
Date de création du compte :
|
||||||
|
</span>{' '}
|
||||||
|
{date.format(
|
||||||
|
new Date(props.createdAt),
|
||||||
|
'DD/MM/YYYY à HH:mm',
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAuth && user.name === props.name && (
|
||||||
|
<button
|
||||||
onClick={toggleModal}
|
onClick={toggleModal}
|
||||||
style={{
|
style={{ marginBottom: '25px' }}
|
||||||
cursor: 'pointer',
|
className='btn btn-dark'
|
||||||
position: 'absolute',
|
|
||||||
left: 0
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faTimes}
|
icon={faPen}
|
||||||
style={{ width: '1.5rem', color: 'red' }}
|
style={{ cursor: 'pointer', width: '1rem' }}
|
||||||
/>
|
/>
|
||||||
</span>
|
Éditez le profil
|
||||||
<h2 className='text-center'>Éditer le profil</h2>
|
|
||||||
<p className='text-center'>
|
|
||||||
<em>
|
|
||||||
(Vous devrez vous reconnecter après la sauvegarde){' '}
|
|
||||||
<br /> Si vous changez votre adresse email, vous devrez
|
|
||||||
la confirmer comme à l'inscription (vérifier vos
|
|
||||||
emails).
|
|
||||||
</em>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='col-24'>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className='form-group'>
|
|
||||||
<label className='form-label' htmlFor='name'>
|
|
||||||
Nom :
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={inputState.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
type='text'
|
|
||||||
name='name'
|
|
||||||
id='name'
|
|
||||||
className='form-control'
|
|
||||||
placeholder='Divlo'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='form-group'>
|
|
||||||
<label className='form-label' htmlFor='email'>
|
|
||||||
Email :
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={inputState.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
type='email'
|
|
||||||
name='email'
|
|
||||||
id='email'
|
|
||||||
className='form-control'
|
|
||||||
placeholder='email@gmail.com'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='form-group custom-control custom-switch'>
|
|
||||||
<input
|
|
||||||
onChange={event => handleChange(event, true)}
|
|
||||||
type='checkbox'
|
|
||||||
name='isPublicEmail'
|
|
||||||
checked={inputState.isPublicEmail}
|
|
||||||
className='custom-control-input'
|
|
||||||
id='isPublicEmail'
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className='custom-control-label'
|
|
||||||
htmlFor='isPublicEmail'
|
|
||||||
>
|
|
||||||
Email Public
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='form-group'>
|
|
||||||
<label className='form-label' htmlFor='biography'>
|
|
||||||
Biographie :
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
style={{ height: 'auto' }}
|
|
||||||
value={inputState.biography}
|
|
||||||
onChange={handleChange}
|
|
||||||
name='biography'
|
|
||||||
id='biography'
|
|
||||||
className='form-control'
|
|
||||||
rows='5'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='form-group'>
|
|
||||||
<label className='form-label' htmlFor='logo'>
|
|
||||||
Logo <em>(400x400 recommandé)</em> :
|
|
||||||
</label>
|
|
||||||
<p style={{ margin: 0 }}>
|
|
||||||
<em>
|
|
||||||
Si aucun fichier est choisi, le logo ne sera pas
|
|
||||||
modifié.
|
|
||||||
</em>
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
onChange={handleChange}
|
|
||||||
accept='image/jpeg,image/jpg,image/png,image/gif'
|
|
||||||
type='file'
|
|
||||||
name='logo'
|
|
||||||
id='logo'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='form-group text-center'>
|
|
||||||
<button type='submit' className='btn btn-dark'>
|
|
||||||
Sauvegarder
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
</form>
|
|
||||||
<div className='form-result text-center'>
|
|
||||||
{isLoading ? <Loader /> : htmlParser(message)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Modal>
|
{props.favoritesArray.length > 0 && (
|
||||||
) : (
|
|
||||||
<div className='container-fluid Profile__container'>
|
|
||||||
<div className='row Profile__row'>
|
|
||||||
<div className='col-20'>
|
|
||||||
<div className='text-center'>
|
|
||||||
<h1>
|
|
||||||
Profil de <span className='important'>{props.name}</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className='row justify-content-center'>
|
<div className='row justify-content-center'>
|
||||||
<div className='col-24 text-center'>
|
<div className='col-24 text-center'>
|
||||||
<img
|
<h2>
|
||||||
className='Profile__logo'
|
Dernières fonctions ajoutées aux{' '}
|
||||||
src={API_URL + props.logo}
|
<span className='important'>favoris</span> :
|
||||||
alt={props.name}
|
</h2>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className='col-24'>
|
||||||
|
<div className='row justify-content-center'>
|
||||||
|
{props.favoritesArray.map(favorite => {
|
||||||
|
return <FunctionCard key={favorite.id} {...favorite} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.commentsArray.length > 0 && (
|
||||||
|
<div className='row justify-content-center'>
|
||||||
<div className='col-24 text-center'>
|
<div className='col-24 text-center'>
|
||||||
{props.biography != null && <p>{props.biography}</p>}
|
<h2>
|
||||||
{props.email != null && (
|
Derniers <span className='important'>commentaires</span> :
|
||||||
<p>
|
</h2>
|
||||||
<span className='important'>Email :</span> {props.email}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p>
|
|
||||||
<span className='important'>
|
|
||||||
Date de création du compte :
|
|
||||||
</span>{' '}
|
|
||||||
{date.format(
|
|
||||||
new Date(props.createdAt),
|
|
||||||
'DD/MM/YYYY à HH:mm',
|
|
||||||
false
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className='col-24'>
|
||||||
{isAuth && user.name === props.name && (
|
{props.commentsArray.map(comment => (
|
||||||
<button
|
<div
|
||||||
onClick={toggleModal}
|
key={comment.id}
|
||||||
style={{ marginBottom: '25px' }}
|
className='row Profile__row Profile__comment'
|
||||||
className='btn btn-dark'
|
>
|
||||||
>
|
<div className='col-20'>
|
||||||
<FontAwesomeIcon
|
<p style={{ textAlign: 'center', marginTop: '30px' }}>
|
||||||
icon={faPen}
|
Posté sur la fonction
|
||||||
style={{ cursor: 'pointer', width: '1rem' }}
|
<Link
|
||||||
/>
|
href={
|
||||||
Éditez le profil
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{props.favoritesArray.length > 0 && (
|
|
||||||
<div className='row justify-content-center'>
|
|
||||||
<div className='col-24 text-center'>
|
|
||||||
<h2>
|
|
||||||
Dernières fonctions ajoutées aux{' '}
|
|
||||||
<span className='important'>favoris</span> :
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className='col-24'>
|
|
||||||
<div className='row justify-content-center'>
|
|
||||||
{props.favoritesArray.map(favorite => {
|
|
||||||
return <FunctionCard key={favorite.id} {...favorite} />
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{props.commentsArray.length > 0 && (
|
|
||||||
<div className='row justify-content-center'>
|
|
||||||
<div className='col-24 text-center'>
|
|
||||||
<h2>
|
|
||||||
Derniers <span className='important'>commentaires</span> :
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className='col-24'>
|
|
||||||
{props.commentsArray.map(comment => (
|
|
||||||
<div
|
|
||||||
key={comment.id}
|
|
||||||
className='row Profile__row Profile__comment'
|
|
||||||
>
|
|
||||||
<div className='col-20'>
|
|
||||||
<p style={{ textAlign: 'center', marginTop: '30px' }}>
|
|
||||||
Posté sur la fonction
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
comment.function.type === 'form' ||
|
comment.function.type === 'form' ||
|
||||||
comment.function.type === 'article'
|
comment.function.type === 'article'
|
||||||
? '/functions/[slug]'
|
? '/functions/[slug]'
|
||||||
: `/functions/${comment.function.slug}`
|
: `/functions/${comment.function.slug}`
|
||||||
}
|
}
|
||||||
as={`/functions/${comment.function.slug}`}
|
as={`/functions/${comment.function.slug}`}
|
||||||
>
|
>
|
||||||
<a>{comment.function.title}</a>
|
<a>{comment.function.title}</a>
|
||||||
</Link>
|
</Link>
|
||||||
le{' '}
|
le{' '}
|
||||||
{date.format(
|
{date.format(
|
||||||
new Date(comment.createdAt),
|
new Date(comment.createdAt),
|
||||||
'DD/MM/YYYY à HH:mm',
|
'DD/MM/YYYY à HH:mm',
|
||||||
false
|
false
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
source={comment.message}
|
source={comment.message}
|
||||||
renderers={{ code: CodeBlock }}
|
renderers={{ code: CodeBlock }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{props.quotesArray.length > 0 && (
|
{props.quotesArray.length > 0 && (
|
||||||
<div className='row justify-content-center'>
|
<div className='row justify-content-center'>
|
||||||
<div className='col-24 text-center'>
|
<div className='col-24 text-center'>
|
||||||
<h2>
|
<h2>
|
||||||
Dernières <span className='important'>citations</span>{' '}
|
Dernières <span className='important'>citations</span>{' '}
|
||||||
proposées (et validées) :
|
proposées (et validées) :
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Citations pour la fonction{' '}
|
Citations pour la fonction{' '}
|
||||||
<Link href='/functions/randomQuote'>
|
<Link href='/functions/randomQuote'>
|
||||||
<a>Générateur de citations</a>
|
<a>Générateur de citations</a>
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='col-24 table-column'>
|
||||||
|
<table style={{ marginBottom: '50px' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className='table-row' scope='col'>
|
||||||
|
Citation/Proverbe
|
||||||
|
</th>
|
||||||
|
<th className='table-row' scope='col'>
|
||||||
|
Auteur
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{props.quotesArray.map((currentQuote, index) => {
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td className='table-row text-center'>
|
||||||
|
{currentQuote.quote}
|
||||||
|
</td>
|
||||||
|
<td className='table-row text-center'>
|
||||||
|
{currentQuote.author}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-24 table-column'>
|
)}
|
||||||
<table style={{ marginBottom: '50px' }}>
|
</div>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className='table-row' scope='col'>
|
|
||||||
Citation/Proverbe
|
|
||||||
</th>
|
|
||||||
<th className='table-row' scope='col'>
|
|
||||||
Auteur
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{props.quotesArray.map((currentQuote, index) => {
|
|
||||||
return (
|
|
||||||
<tr key={index}>
|
|
||||||
<td className='table-row text-center'>
|
|
||||||
{currentQuote.quote}
|
|
||||||
</td>
|
|
||||||
<td className='table-row text-center'>
|
|
||||||
{currentQuote.author}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,15 @@ import axios from 'axios'
|
|||||||
|
|
||||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL
|
export const API_URL = process.env.NEXT_PUBLIC_API_URL
|
||||||
|
|
||||||
const api = axios.create({
|
const api = (() => {
|
||||||
baseURL: API_URL,
|
const baseURL =
|
||||||
headers: {
|
typeof window === 'undefined' ? process.env.CONTAINER_API_URL : API_URL
|
||||||
'Content-Type': 'application/json'
|
return axios.create({
|
||||||
}
|
baseURL,
|
||||||
})
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
Reference in New Issue
Block a user