feat: add docker support and update deps

This commit is contained in:
divlo 2020-10-30 16:58:27 +01:00
parent f5a8be5000
commit ffec0058e5
35 changed files with 5006 additions and 4502 deletions

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

2
api/.dockerignore Normal file
View File

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

View File

@ -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
View File

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

@ -16,11 +16,14 @@ 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())
}
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 +40,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 +86,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(() => {

View File

@ -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'
} }

View File

@ -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
} }
) )

1649
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,33 +5,33 @@
"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": "^4.17.1",
"express-fileupload": "^1.1.6", "express-fileupload": "^1.2.0",
"express-http-to-https": "^1.1.4", "express-http-to-https": "^1.1.4",
"express-validator": "^6.4.0", "express-validator": "^6.6.1",
"helmet": "^3.21.3", "helmet": "^4.1.1",
"jsdom": "^16.2.2", "jsdom": "^16.4.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"moment": "^2.24.0", "moment": "^2.29.1",
"ms": "^2.1.2", "ms": "^2.1.2",
"mysql2": "^2.1.0", "mysql2": "^2.2.5",
"nodemailer": "^6.4.6", "nodemailer": "^6.4.14",
"sequelize": "^5.21.5", "sequelize": "^6.3.5",
"smart-request-balancer": "^2.1.1", "smart-request-balancer": "^2.1.1",
"uuid": "^7.0.2", "uuid": "^8.3.1",
"validator": "^13.0.0", "validator": "^13.1.17",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"morgan": "^1.9.1" "morgan": "^1.10.0"
}, },
"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
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 = "" 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"

View File

@ -1,2 +1,2 @@
node_modules 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 */ /* 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}.`)
}) })

File diff suppressed because it is too large Load Diff

View File

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

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

View File

@ -82,62 +82,68 @@ const CommentCard = forwardRef((props, ref) => {
<a>{props.user.name}</a> <a>{props.user.name}</a>
</Link> </Link>
&nbsp;-{' '} &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> </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>
&nbsp;-&nbsp; &nbsp;-&nbsp;
<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>

View File

@ -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>
) )
} }

View File

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

View File

@ -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' />
)} )}

4268
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,44 +4,46 @@
"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"
} }
} }

View File

@ -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>
)}
</> </>
) )
} }

View File

@ -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> )}
)}
</> </>
) )
} }

View File

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

View File

@ -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>
) )
} }

View File

@ -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> &nbsp; É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&nbsp;
style={{ cursor: 'pointer', width: '1rem' }} <Link
/> href={
&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={
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>
&nbsp;le{' '} &nbsp;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>
)}
</> </>
) )
} }

View File

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