Merge pull request #10 from Divlo/release/v2.2

Release/v2.2
This commit is contained in:
Divlo 2020-10-30 17:28:58 +01:00 committed by GitHub
commit cbe82f74a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 5060 additions and 4518 deletions

2
.github/backup.sql vendored

File diff suppressed because one or more lines are too long

2
.gitignore vendored
View File

@ -1 +1 @@
.github/backup
.github/backup

View File

@ -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 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.

View File

@ -5,10 +5,9 @@
</p>
<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://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>
<img src="https://img.shields.io/github/stars/Divlo/FunctionProject?style=social" alt="Stars">
<br/> <br/>
<a href="https://function.divlo.fr/"><img src="https://raw.githubusercontent.com/Divlo/FunctionProject/master/.github/FunctionProject.png" alt="FunctionProject" /></a>
</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)
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
@ -39,8 +38,9 @@ Si vous voulez avoir les données des catégories et des fonctions, vous pouvez
### Prérequis :
- NodeJS (et npm) → version récente
- Base de donnée MySQL → J'utilise Wamp ce qui me permet d'avoir phpmyadmin.
- [Node.js](https://nodejs.org/) >= 14
- [npm](https://www.npmjs.com/) >= 6
- [MySQL](https://www.mysql.com/) >= 5.7
### 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 :
#### 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 :
- Lancer le front-end en allant dans `/website`

2
api/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
build

View File

@ -1,12 +1,15 @@
HOST = "http://localhost:8080"
FRONT_END_HOST = "http://localhost:3000"
OpenWeatherMap_API_KEY = ""
Scraper_API_KEY = ""
DB_HOST = ""
DB_NAME = ""
DB_USER = ""
DB_PASS = ""
JWT_SECRET = ""
EMAIL_HOST = ""
EMAIL_USER = ""
EMAIL_PASSWORD = ""
HOST="http://localhost:8080"
FRONT_END_HOST="http://localhost:3000"
OpenWeatherMap_API_KEY=""
Scraper_API_KEY=""
DATABASE_HOST="functionproject-database"
DATABASE_NAME="functionproject"
DATABASE_USER="root"
DATABASE_PASSWORD="password"
DATABASE_PORT=3306
JWT_SECRET=""
EMAIL_HOST="functionproject-maildev"
EMAIL_USER="no-reply@functionproject.fr"
EMAIL_PASSWORD="password"
EMAIL_PORT=25
COMPOSE_PROJECT_NAME="function.divlo.fr-api"

6
api/.gitignore vendored
View File

@ -11,14 +11,16 @@
# production
/build
# misc
.DS_Store
# envs
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.production
# misc
.DS_Store
/temp
/assets/images/

13
api/Dockerfile Normal file
View 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

View File

@ -6,6 +6,7 @@ const helmet = require('helmet')
const cors = require('cors')
const morgan = require('morgan')
const { redirectToHTTPS } = require('express-http-to-https')
const rateLimit = require('express-rate-limit')
/* Files Imports & Variables */
const sequelize = require('./assets/utils/database')
@ -16,11 +17,27 @@ const isAdmin = require('./middlewares/isAdmin')
const app = express()
/* 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(cors())
app.use(morgan('dev'))
app.use(express.json())
app.use(redirectToHTTPS([/localhost:(\d{4})/]))
/* Routes */
app.use('/images', express.static(path.join(__dirname, 'assets', 'images')))
@ -37,7 +54,7 @@ app.use('/links', require('./routes/links_shortener'))
/* Errors Handling */
app.use((_req, _res, next) =>
errorHandling(next, { statusCode: 404, message: "La route n'existe pas!" })
) // 404
)
app.use((error, _req, res, _next) => {
console.log(error)
const { statusCode, message } = error
@ -83,7 +100,6 @@ Users.hasMany(ShortLinks)
ShortLinks.belongsTo(Users, { constraints: false })
/* Server */
// sequelize.sync({ force: true })
sequelize
.sync()
.then(() => {

View File

@ -1,3 +1,8 @@
const dotenv = require('dotenv')
dotenv.config()
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)
const config = {
PORT: process.env.PORT || 8080,
HOST: process.env.HOST,
@ -5,20 +10,22 @@ const config = {
WEATHER_API_KEY: process.env.OpenWeatherMap_API_KEY,
SCRAPER_API_KEY: process.env.Scraper_API_KEY,
DATABASE: {
host: process.env.DB_HOST,
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASS
host: process.env.DATABASE_HOST,
name: process.env.DATABASE_NAME,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
port: parseInt(process.env.DATABASE_PORT ?? '3306', 10)
},
JWT_SECRET: process.env.JWT_SECRET,
EMAIL_INFO: {
host: process.env.EMAIL_HOST,
port: 465,
secure: true, // true for 465, false for other ports
port: EMAIL_PORT,
secure: EMAIL_PORT === 465,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
}
},
ignoreTLS: process.env.NODE_ENV !== 'production'
},
TOKEN_LIFE: '1 week'
}

View File

@ -7,7 +7,8 @@ const sequelize = new Sequelize(
DATABASE.password,
{
dialect: 'mysql',
host: DATABASE.host
host: DATABASE.host,
port: DATABASE.port
}
)

1656
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,38 @@
{
"name": "api",
"version": "2.1.0",
"version": "2.2.0",
"description": "Backend REST API for FunctionProject",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"format": "standard \"./**/*.js\" --fix | snazzy || exit 0"
"format": "standard \"./**/*.js\" --fix | snazzy"
},
"dependencies": {
"axios": "^0.19.2",
"axios": "^0.21.0",
"bcryptjs": "^2.4.3",
"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",
"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": {
"nodemon": "^2.0.4",
"snazzy": "^8.0.0",
"standard": "^14.3.4"
"nodemon": "^2.0.6",
"snazzy": "^9.0.0",
"standard": "^16.0.0"
}
}

73
docker-compose.yml Normal file
View 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
View File

@ -0,0 +1,2 @@
node_modules
build

View File

@ -1,4 +1,6 @@
DB_HOST = ""
DB_NAME = ""
DB_USER = ""
DB_PASS = ""
DATABASE_HOST="functionproject-database"
DATABASE_NAME="functionproject"
DATABASE_USER="root"
DATABASE_PASSWORD="password"
DATABASE_PORT=3306
COMPOSE_PROJECT_NAME="s.divlo.fr-website"

View File

@ -1,2 +1,2 @@
node_modules
.env
.env

13
s.divlo.fr/Dockerfile Normal file
View 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

View File

@ -10,18 +10,21 @@ const mysql = require('mysql')
/* Files Imports & Variables */
const app = express()
const database = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
port: process.env.DB_PORT
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASS,
database: process.env.DATABASE_NAME,
port: process.env.DATABASE_PORT
})
/* 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(morgan('dev'))
app.use(express.json())
app.use(redirectToHTTPS([/localhost:(\d{4})/]))
/* EJS Template Engines */
app.set('view engine', 'ejs')
@ -77,7 +80,7 @@ app.use((error, _req, res) => {
})
/* Server */
const PORT = process.env.PORT || 8000
const PORT = process.env.PORT || 7000
app.listen(PORT, () => {
console.log('\x1b[36m%s\x1b[0m', `Started on port ${PORT}.`)
})

File diff suppressed because it is too large Load Diff

View File

@ -9,16 +9,16 @@
},
"dependencies": {
"dotenv": "^8.2.0",
"ejs": "^3.1.3",
"ejs": "^3.1.5",
"express": "^4.17.1",
"express-http-to-https": "^1.1.4",
"helmet": "^4.0.0",
"helmet": "^4.1.1",
"morgan": "^1.10.0",
"mysql": "^2.18.1"
},
"devDependencies": {
"nodemon": "^2.0.4",
"snazzy": "^8.0.0",
"standard": "^14.3.4"
"nodemon": "^2.0.6",
"snazzy": "^9.0.0",
"standard": "^16.0.0"
}
}

2
website/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
build

View File

@ -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
View 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"]

View File

@ -8,7 +8,7 @@ export default function Footer () {
<Link href='/about'>
<a>FunctionProject</a>
</Link>
&nbsp;- Version 2.1 <br />
&nbsp;- Version 2.2 <br />
<a href='https://divlo.fr/' target='_blank' rel='noopener noreferrer'>
Divlo
</a>{' '}

View File

@ -23,15 +23,15 @@ const FunctionCard = memo(
<Link
{...(props.isAdmin
? {
href: '/admin/[slug]',
as: `/admin/${props.slug}`
}
href: '/admin/[slug]',
as: `/admin/${props.slug}`
}
: {
href: isFormOrArticle
? '/functions/[slug]'
: `/functions/${props.slug}`,
as: `/functions/${props.slug}`
})}
href: isFormOrArticle
? '/functions/[slug]'
: `/functions/${props.slug}`,
as: `/functions/${props.slug}`
})}
>
{/* FunctionCard a une hauteur pendant chargement */}
<a

View File

@ -82,62 +82,68 @@ const CommentCard = forwardRef((props, ref) => {
<a>{props.user.name}</a>
</Link>
&nbsp;-{' '}
{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>
</div>
<div className='row'>
<div className='col-24'>
{!isEditing ? (
<>
<div className='CommentCard__message'>
<ReactMarkdown
source={editInput}
renderers={{ code: CodeBlock }}
/>
</div>
{isAuth && user.name === props.user.name && (
<p
style={{
fontSize: '15px',
margin: '15px 0 0 0',
fontStyle: 'italic'
}}
>
<a onClick={deleteCommentById} href='#'>
supprimer
</a>
{!isEditing
? (
<>
<div className='CommentCard__message'>
<ReactMarkdown
source={editInput}
renderers={{ code: CodeBlock }}
/>
</div>
{isAuth && user.name === props.user.name && (
<p
style={{
fontSize: '15px',
margin: '15px 0 0 0',
fontStyle: 'italic'
}}
>
<a onClick={deleteCommentById} href='#'>
supprimer
</a>
&nbsp;-&nbsp;
<a style={{ cursor: 'pointer' }} onClick={editComment}>
modifier
</a>
</p>
<a style={{ cursor: 'pointer' }} onClick={editComment}>
modifier
</a>
</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>

View File

@ -3,11 +3,13 @@ import htmlParser from 'html-react-parser'
const FunctionArticle = ({ article }) => {
return (
<div style={{ marginBottom: '50px' }} className='container-fluid'>
{article != null ? (
htmlParser(article)
) : (
<p className='text-center'>L'article n'est pas encore disponible.</p>
)}
{article != null
? (
htmlParser(article)
)
: (
<p className='text-center'>L'article n'est pas encore disponible.</p>
)}
</div>
)
}

View File

@ -90,36 +90,38 @@ const FunctionComments = ({ functionId }) => {
<div className='FunctionComments__post container-fluid'>
<div className='row FunctionComments__row'>
<div className='col-24'>
{isAuth ? (
<form onSubmit={handleSubmit}>
<div className='form-group FunctionComments__post-group'>
<label className='form-label' htmlFor='commentPost'>
Ajouter un commentaire :
</label>
<textarea
className='FunctionComments__textarea form-control'
value={inputState.commentPost}
onChange={handleChange}
name='commentPost'
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' }}>
<button type='submit' className='btn btn-dark'>
Envoyer
</button>
</div>
</form>
) : (
<p className='text-center'>
Vous devez être{' '}
<Link href='/users/login'>
<a>connecté</a>
</Link>{' '}
pour poster un commentaire.
</p>
)}
{isAuth
? (
<form onSubmit={handleSubmit}>
<div className='form-group FunctionComments__post-group'>
<label className='form-label' htmlFor='commentPost'>
Ajouter un commentaire :
</label>
<textarea
className='FunctionComments__textarea form-control'
value={inputState.commentPost}
onChange={handleChange}
name='commentPost'
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' }}>
<button type='submit' className='btn btn-dark'>
Envoyer
</button>
</div>
</form>
)
: (
<p className='text-center'>
Vous devez être{' '}
<Link href='/users/login'>
<a>connecté</a>
</Link>{' '}
pour poster un commentaire.
</p>
)}
</div>
</div>
</div>

View File

@ -51,35 +51,37 @@ export default function Header () {
<NavigationLink name='Accueil' path='/' />
<NavigationLink name='Fonctions' path='/functions' />
<NavigationLink name='Utilisateurs' path='/users' />
{!isAuth ? (
<>
<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
className={`navbar-link ${
{!isAuth
? (
<>
<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
className={`navbar-link ${
pathname === '/users/[name]'
? 'navbar-link-active'
: null
}`}
>
Mon Profil
</a>
</Link>
</li>
<li className='navbar-item'>
<Link href='/'>
<a onClick={logoutUser} className='navbar-link'>
Se déconnecter
</a>
</Link>
</li>
</>
)}
>
Mon Profil
</a>
</Link>
</li>
<li className='navbar-item'>
<Link href='/'>
<a onClick={logoutUser} className='navbar-link'>
Se déconnecter
</a>
</Link>
</li>
</>
)}
{isAuth && user.isAdmin && (
<NavigationLink name='Admin' path='/admin' />
)}

4270
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,49 @@
{
"name": "website",
"version": "2.1.0",
"version": "2.2.0",
"description": "Website frontend for FunctionProject",
"main": "server.js",
"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",
"export": "next export",
"start": "cross-env NODE_ENV=production node server",
"format": "standard \"./**/*.{js,jsx}\" --fix | snazzy || exit 0"
"format": "standard \"./**/*.{js,jsx}\" --fix | snazzy"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-brands-svg-icons": "^5.13.0",
"@fortawesome/free-regular-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9",
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-brands-svg-icons": "^5.15.1",
"@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.12",
"@zeit/next-css": "^1.0.1",
"axios": "^0.19.2",
"date-and-time": "^0.13.1",
"date-fns": "^2.12.0",
"axios": "^0.21.0",
"date-and-time": "^0.14.1",
"date-fns": "^2.16.1",
"express": "^4.17.1",
"express-http-to-https": "^1.1.4",
"html-react-parser": "^0.10.2",
"next": "^9.5.4",
"next-fonts": "^1.0.3",
"notyf": "^3.6.0",
"html-react-parser": "^0.14.0",
"next": "^10.0.0",
"next-fonts": "^1.4.0",
"notyf": "^3.9.0",
"nprogress": "^0.2.0",
"react": "16.13.0",
"react": "17.0.1",
"react-codepen-embed": "^1.0.1",
"react-color": "^2.18.0",
"react-datepicker": "^2.14.1",
"react-dom": "16.13.0",
"react-markdown": "^4.3.1",
"react-color": "^2.19.3",
"react-datepicker": "^3.3.0",
"react-dom": "17.0.1",
"react-markdown": "^5.0.2",
"react-swipeable-views": "^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",
"universal-cookie": "^4.0.3"
"universal-cookie": "^4.0.4"
},
"devDependencies": {
"cross-env": "^7.0.2",
"snazzy": "^8.0.0",
"standard": "^14.3.4"
"snazzy": "^9.0.0",
"standard": "^16.0.0"
}
}

View File

@ -23,64 +23,66 @@ const Admin = props => {
/>
{/* Création d'une fonction */}
{isOpen ? (
<Modal toggleModal={toggleModal}>
<div className='Admin__Modal__container container-fluid'>
<div className='Admin__Modal__row row'>
<div className='col-24'>
<div className='Admin__Modal-top-container row'>
<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'>Crée une nouvelle fonction</h2>
{isOpen
? (
<Modal toggleModal={toggleModal}>
<div className='Admin__Modal__container container-fluid'>
<div className='Admin__Modal__row row'>
<div className='col-24'>
<div className='Admin__Modal-top-container row'>
<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'>Crée une nouvelle fonction</h2>
</div>
</div>
</div>
</div>
<div className='col-24'>
<AddEditFunction
defaultInputState={{ type: 'form' }}
{...props}
/>
<div className='col-24'>
<AddEditFunction
defaultInputState={{ type: 'form' }}
{...props}
/>
</div>
</div>
</div>
</div>
</Modal>
) : (
<FunctionsList isAdmin token={props.user.token}>
<div className='col-24'>
<h1 className='Functions__title'>Administration</h1>
<button
onClick={toggleModal}
style={{ margin: '0 0 40px 0' }}
className='btn btn-dark'
>
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
</Modal>
)
: (
<FunctionsList isAdmin token={props.user.token}>
<div className='col-24'>
<h1 className='Functions__title'>Administration</h1>
<button
onClick={toggleModal}
style={{ margin: '0 0 40px 0' }}
className='btn btn-dark'
>
Crée une nouvelle fonction
</button>
</Link>
<Link href='/admin/manageQuotes'>
<button style={{ margin: '0 0 0 20px' }} className='btn btn-dark'>
Gérer les citations
</button>
</Link>
</div>
</FunctionsList>
)}
<Link href='/admin/manageCategories'>
<button style={{ margin: '0 0 0 20px' }} className='btn btn-dark'>
Gérer les catégories
</button>
</Link>
<Link href='/admin/manageQuotes'>
<button style={{ margin: '0 0 0 20px' }} className='btn btn-dark'>
Gérer les citations
</button>
</Link>
</div>
</FunctionsList>
)}
</>
)
}

View File

@ -159,119 +159,121 @@ const manageCategories = props => {
description="Page d'administration de FunctionProject. Gérer les catégories."
/>
{isOpen ? (
<Modal>
<AddEditCategory
handleToggleModal={toggleModal}
defaultInputState={defaultInputState}
{...props}
isEditing={isEditing}
/>
</Modal>
) : (
<div className='container-fluid text-center'>
<div className='row justify-content-center'>
<div className='col-24'>
<h1>Gérer les catégories</h1>
<button
onClick={() => {
setDefaultInputState(defaultCategoryState)
toggleModal()
setIsEditing(false)
}}
style={{ margin: '0 0 40px 0' }}
className='btn btn-dark'
>
Ajouter une catégorie
</button>
{isOpen
? (
<Modal>
<AddEditCategory
handleToggleModal={toggleModal}
defaultInputState={defaultInputState}
{...props}
isEditing={isEditing}
/>
</Modal>
)
: (
<div className='container-fluid text-center'>
<div className='row justify-content-center'>
<div className='col-24'>
<h1>Gérer les catégories</h1>
<button
onClick={() => {
setDefaultInputState(defaultCategoryState)
toggleModal()
setIsEditing(false)
}}
style={{ margin: '0 0 40px 0' }}
className='btn btn-dark'
>
Ajouter une catégorie
</button>
</div>
</div>
</div>
<div className='row justify-content-center'>
<div className='container-fluid'>
<div className='col-24 table-column'>
<table className='table'>
<thead>
<tr>
<th className='table-row' scope='col'>
id
</th>
<th className='table-row' scope='col'>
name
</th>
<th className='table-row' scope='col'>
color
</th>
<th className='table-row' scope='col'>
createdAt
</th>
<th className='table-row' scope='col'>
updatedAt
</th>
<th className='table-row' scope='col'>
Modifier
</th>
<th className='table-row' scope='col'>
Supprimer
</th>
</tr>
</thead>
<tbody>
{categories.map(category => {
return (
<tr
key={category.id}
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
})}
<div className='row justify-content-center'>
<div className='container-fluid'>
<div className='col-24 table-column'>
<table className='table'>
<thead>
<tr>
<th className='table-row' scope='col'>
id
</th>
<th className='table-row' scope='col'>
name
</th>
<th className='table-row' scope='col'>
color
</th>
<th className='table-row' scope='col'>
createdAt
</th>
<th className='table-row' scope='col'>
updatedAt
</th>
<th className='table-row' scope='col'>
Modifier
</th>
<th className='table-row' scope='col'>
Supprimer
</th>
</tr>
</thead>
<tbody>
{categories.map(category => {
return (
<tr
key={category.id}
style={{ backgroundColor: category.color }}
>
<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>
<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
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>
)}
)}
</>
)
}

View File

@ -266,144 +266,146 @@ const LinksList = ({
</div>
<div className='row justify-content-center'>
<div className='container-fluid'>
{!isEditing ? (
<div className='col-24 table-column'>
<table className='table' style={{ marginBottom: '40px' }}>
<thead>
<tr>
<th className='table-row' scope='col'>
Liens
</th>
<th className='table-row' scope='col'>
Nom
</th>
<th className='table-row' scope='col'>
Compteur de clics
</th>
<th className='table-row' scope='col'>
Modifier
</th>
<th className='table-row' scope='col'>
Supprimer
</th>
</tr>
</thead>
<tbody>
{linksData.rows.map((link, index) => {
const linkJSX = (
<>
<td className='table-row'>
<a href={link.url}>{link.url}</a>
</td>
<td className='table-row'>
<a href={`https://s.divlo.fr/${link.shortcut}`}>
{link.shortcut}
</a>
</td>
<td className='table-row'>{link.count}</td>
<td
style={{ cursor: 'pointer' }}
onClick={() => handleEditLink(link)}
>
<FontAwesomeIcon
icon={faPen}
style={{ width: '1.5rem' }}
/>
</td>
<td
style={{ cursor: 'pointer' }}
onClick={() => handleRemoveLink(link.id)}
>
<FontAwesomeIcon
icon={faTrash}
style={{ width: '1.5rem' }}
/>
</td>
</>
)
// Si c'est le dernier élément
if (linksData.rows.length === index + 1) {
return (
<tr key={index} ref={lastLinkRef}>
{linkJSX}
</tr>
{!isEditing
? (
<div className='col-24 table-column'>
<table className='table' style={{ marginBottom: '40px' }}>
<thead>
<tr>
<th className='table-row' scope='col'>
Liens
</th>
<th className='table-row' scope='col'>
Nom
</th>
<th className='table-row' scope='col'>
Compteur de clics
</th>
<th className='table-row' scope='col'>
Modifier
</th>
<th className='table-row' scope='col'>
Supprimer
</th>
</tr>
</thead>
<tbody>
{linksData.rows.map((link, index) => {
const linkJSX = (
<>
<td className='table-row'>
<a href={link.url}>{link.url}</a>
</td>
<td className='table-row'>
<a href={`https://s.divlo.fr/${link.shortcut}`}>
{link.shortcut}
</a>
</td>
<td className='table-row'>{link.count}</td>
<td
style={{ cursor: 'pointer' }}
onClick={() => handleEditLink(link)}
>
<FontAwesomeIcon
icon={faPen}
style={{ width: '1.5rem' }}
/>
</td>
<td
style={{ cursor: 'pointer' }}
onClick={() => handleRemoveLink(link.id)}
>
<FontAwesomeIcon
icon={faTrash}
style={{ width: '1.5rem' }}
/>
</td>
</>
)
}
return <tr key={index}>{linkJSX}</tr>
})}
</tbody>
</table>
</div>
) : (
<Modal>
<div className='Admin__Modal__container container-fluid'>
<div className='Admin__Modal__row row'>
<div className='col-24'>
<div className='Admin__Modal-top-container row'>
<div className='col-24'>
<span
onClick={toggleModal}
style={{
cursor: 'pointer',
position: 'absolute',
left: 0
}}
>
<FontAwesomeIcon
icon={faTimes}
style={{ width: '1.5rem', color: 'red' }}
/>
</span>
// Si c'est le dernier élément
if (linksData.rows.length === index + 1) {
return (
<tr key={index} ref={lastLinkRef}>
{linkJSX}
</tr>
)
}
return <tr key={index}>{linkJSX}</tr>
})}
</tbody>
</table>
</div>
)
: (
<Modal>
<div className='Admin__Modal__container container-fluid'>
<div className='Admin__Modal__row row'>
<div className='col-24'>
<div className='Admin__Modal-top-container row'>
<div className='col-24'>
<span
onClick={toggleModal}
style={{
cursor: 'pointer',
position: 'absolute',
left: 0
}}
>
<FontAwesomeIcon
icon={faTimes}
style={{ width: '1.5rem', color: 'red' }}
/>
</span>
</div>
</div>
</div>
</div>
<div className='col-24'>
<form onSubmit={handleEditSubmit}>
<div className='form-group'>
<label className='form-label' htmlFor='url'>
Entrez le lien à raccourcir :
</label>
<input
value={defaultInputState.url}
onChange={handleChange}
type='text'
name='url'
id='url'
placeholder='(e.g : https://divlo.fr)'
className='form-control'
/>
</div>
<div className='col-24'>
<form onSubmit={handleEditSubmit}>
<div className='form-group'>
<label className='form-label' htmlFor='url'>
Entrez le lien à raccourcir :
</label>
<input
value={defaultInputState.url}
onChange={handleChange}
type='text'
name='url'
id='url'
placeholder='(e.g : https://divlo.fr)'
className='form-control'
/>
</div>
<div className='form-group'>
<label className='form-label' htmlFor='shortcutName'>
Entrez le nom du raccourci :
</label>
<input
value={defaultInputState.shortcutName}
onChange={handleChange}
type='text'
name='shortcutName'
id='shortcutName'
placeholder='(e.g : divlo)'
className='form-control'
/>
</div>
<div className='form-group'>
<label className='form-label' htmlFor='shortcutName'>
Entrez le nom du raccourci :
</label>
<input
value={defaultInputState.shortcutName}
onChange={handleChange}
type='text'
name='shortcutName'
id='shortcutName'
placeholder='(e.g : divlo)'
className='form-control'
/>
</div>
<div className='form-group text-center'>
<button type='submit' className='btn btn-dark'>
Envoyer
</button>
<div className='form-group text-center'>
<button type='submit' className='btn btn-dark'>
Envoyer
</button>
</div>
</form>
<div className='form-result text-center'>
{isLoading ? <Loader /> : htmlParser(message)}
</div>
</form>
<div className='form-result text-center'>
{isLoading ? <Loader /> : htmlParser(message)}
</div>
</div>
</div>
</div>
</Modal>
)}
</Modal>
)}
</div>
</div>
</div>

View File

@ -54,104 +54,110 @@ const PlayRightPrice = () => {
return (
<div className='container-fluid'>
{!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>
) : (
<>
{!isPlaying
? (
<div className='row justify-content-center'>
<div
style={{ marginBottom: '20px' }}
className='col-24 text-center'
>
<h4>{productToGuess.name}</h4>
<img
src={productToGuess.image}
alt={productToGuess.name}
className='Product__image'
/>
<div className='form-group text-center'>
<button
onClick={handlePlaying}
type='submit'
className='btn btn-dark'
>
Jouer
</button>
</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>
)}
)
: isLoadingProduct
? (
<div className='row justify-content-center'>
<Loader />
</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 className='row justify-content-center'>
<div
key={index}
className={`col-24 Price__result ${priceResultClass}`}
style={{ marginBottom: '20px' }}
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 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>
)
}

View File

@ -84,299 +84,301 @@ const Profile = props => {
/>
{/* Édition du profil */}
{isOpen ? (
<Modal toggleModal={toggleModal}>
<div className='Profile__container container-fluid'>
<div className='Profile__row row'>
<div className='col-24'>
<div className='Profile__Modal-top-container row'>
<div className='col-24'>
<span
{isOpen
? (
<Modal toggleModal={toggleModal}>
<div className='Profile__container container-fluid'>
<div className='Profile__row row'>
<div className='col-24'>
<div className='Profile__Modal-top-container row'>
<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}
style={{
cursor: 'pointer',
position: 'absolute',
left: 0
}}
style={{ marginBottom: '25px' }}
className='btn btn-dark'
>
<FontAwesomeIcon
icon={faTimes}
style={{ width: '1.5rem', color: 'red' }}
icon={faPen}
style={{ cursor: 'pointer', width: '1rem' }}
/>
</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
&nbsp; Éditez le profil
</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>
{props.favoritesArray.length > 0 && (
<div className='row justify-content-center'>
<div className='col-24 text-center'>
<img
className='Profile__logo'
src={API_URL + props.logo}
alt={props.name}
/>
<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'>
{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>
<h2>
Derniers <span className='important'>commentaires</span> :
</h2>
</div>
{isAuth && user.name === props.name && (
<button
onClick={toggleModal}
style={{ marginBottom: '25px' }}
className='btn btn-dark'
>
<FontAwesomeIcon
icon={faPen}
style={{ cursor: 'pointer', width: '1rem' }}
/>
&nbsp; É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&nbsp;
<Link
href={
<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&nbsp;
<Link
href={
comment.function.type === 'form' ||
comment.function.type === 'article'
? '/functions/[slug]'
: `/functions/${comment.function.slug}`
}
as={`/functions/${comment.function.slug}`}
>
<a>{comment.function.title}</a>
</Link>
as={`/functions/${comment.function.slug}`}
>
<a>{comment.function.title}</a>
</Link>
&nbsp;le{' '}
{date.format(
new Date(comment.createdAt),
'DD/MM/YYYY à HH:mm',
false
)}
</p>
<ReactMarkdown
source={comment.message}
renderers={{ code: CodeBlock }}
/>
{date.format(
new Date(comment.createdAt),
'DD/MM/YYYY à HH:mm',
false
)}
</p>
<ReactMarkdown
source={comment.message}
renderers={{ code: CodeBlock }}
/>
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
)}
)}
{props.quotesArray.length > 0 && (
<div className='row justify-content-center'>
<div className='col-24 text-center'>
<h2>
Dernières <span className='important'>citations</span>{' '}
proposées (et validées) :
</h2>
<p>
Citations pour la fonction{' '}
<Link href='/functions/randomQuote'>
<a>Générateur de citations</a>
</Link>
.
</p>
{props.quotesArray.length > 0 && (
<div className='row justify-content-center'>
<div className='col-24 text-center'>
<h2>
Dernières <span className='important'>citations</span>{' '}
proposées (et validées) :
</h2>
<p>
Citations pour la fonction{' '}
<Link href='/functions/randomQuote'>
<a>Générateur de citations</a>
</Link>
.
</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 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>
)}
</>
)
}

View File

@ -2,11 +2,15 @@ import axios from 'axios'
export const API_URL = process.env.NEXT_PUBLIC_API_URL
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json'
}
})
const api = (() => {
const baseURL =
typeof window === 'undefined' ? process.env.CONTAINER_API_URL : API_URL
return axios.create({
baseURL,
headers: {
'Content-Type': 'application/json'
}
})
})()
export default api