├── .github
├── CODE_OF_CONDUCT.md
├── ISSUE_TEMPLATE
│ ├── BUG.md
│ ├── FEATURE_REQUEST.md
│ └── QUESTION.md
├── backup.sql
├── images
│ └── FunctionProject.png
└── workflows
│ ├── commitlint.yml
│ └── nodejs.yml
├── LICENSE
├── README.md
├── api
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── app.js
├── assets
│ ├── config
│ │ ├── config.js
│ │ ├── emails.js
│ │ ├── errors.js
│ │ └── transporter.js
│ ├── functions
│ │ ├── functionObject.js
│ │ ├── main
│ │ │ ├── armstrongNumber.js
│ │ │ ├── calculateAge.js
│ │ │ ├── convertCurrency.js
│ │ │ ├── convertDistance.js
│ │ │ ├── convertEncoding.js
│ │ │ ├── convertRomanArabicNumbers.js
│ │ │ ├── convertTemperature.js
│ │ │ ├── fibonacci.js
│ │ │ ├── findLongestWord.js
│ │ │ ├── heapAlgorithm.js
│ │ │ ├── isPalindrome.js
│ │ │ ├── randomNumber.js
│ │ │ ├── randomQuote.js
│ │ │ ├── rightPrice.js
│ │ │ ├── sortArray.js
│ │ │ └── weatherRequest.js
│ │ └── secondary
│ │ │ ├── capitalize.js
│ │ │ ├── dateTimeManagement.js
│ │ │ └── formatNumberResult.js
│ ├── images
│ │ ├── functions
│ │ │ ├── armstrongNumber.png
│ │ │ ├── arrayMethods.png
│ │ │ ├── calculateAge.png
│ │ │ ├── chronometerTimer.png
│ │ │ ├── convertCurrency.png
│ │ │ ├── convertDistance.png
│ │ │ ├── convertEncoding.png
│ │ │ ├── convertMarkdown.png
│ │ │ ├── convertRomanArabicNumbers.png
│ │ │ ├── convertTemperature.png
│ │ │ ├── default.png
│ │ │ ├── fibonacci.png
│ │ │ ├── findLongestWord.png
│ │ │ ├── heapAlgorithm.png
│ │ │ ├── isPalindrome.png
│ │ │ ├── linkShortener.png
│ │ │ ├── randomNumber.png
│ │ │ ├── randomQuote.png
│ │ │ ├── rightPrice.png
│ │ │ ├── sortArray.png
│ │ │ ├── toDoList.png
│ │ │ └── weatherRequest.png
│ │ └── users
│ │ │ └── default.png
│ └── utils
│ │ ├── database.js
│ │ ├── deleteFilesNameStartWith.js
│ │ ├── errorHandling.js
│ │ ├── getPagesHelper.js
│ │ └── helperQueryNumber.js
├── controllers
│ ├── admin.js
│ ├── categories.js
│ ├── comments.js
│ ├── favorites.js
│ ├── functions.js
│ ├── links_shortener.js
│ ├── quotes.js
│ ├── tasks.js
│ └── users.js
├── middlewares
│ ├── isAdmin.js
│ └── isAuth.js
├── models
│ ├── categories.js
│ ├── comments.js
│ ├── favorites.js
│ ├── functions.js
│ ├── quotes.js
│ ├── short_links.js
│ ├── tasks.js
│ └── users.js
├── package-lock.json
├── package.json
└── routes
│ ├── admin.js
│ ├── categories.js
│ ├── comments.js
│ ├── favorites.js
│ ├── functions.js
│ ├── links_shortener.js
│ ├── quotes.js
│ ├── tasks.js
│ └── users.js
├── docker-compose.yml
├── s.divlo.fr
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── README.md
├── app.js
├── package-lock.json
├── package.json
├── public
│ └── images
│ │ ├── error404.png
│ │ └── linkShortener.png
└── views
│ ├── errors.ejs
│ └── index.ejs
└── website
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── components
├── CodeBlock.jsx
├── Footer.jsx
├── FunctionAdmin
│ ├── AddEditFunction.jsx
│ ├── EditArticleFunction.jsx
│ └── EditFormFunction.jsx
├── FunctionCard
│ └── FunctionCard.jsx
├── FunctionPage
│ ├── CommentCard
│ │ └── CommentCard.jsx
│ ├── FunctionArticle.jsx
│ ├── FunctionComments
│ │ └── FunctionComments.jsx
│ ├── FunctionComponentTop.jsx
│ ├── FunctionForm.jsx
│ ├── FunctionPage.jsx
│ ├── FunctionTabs.jsx
│ └── FunctionTabsTop.jsx
├── FunctionsList
│ └── FunctionsList.jsx
├── HeadTag.jsx
├── Header
│ ├── Header.jsx
│ └── NavigationLink.jsx
├── Loader.jsx
├── Modal.jsx
└── UserCard
│ └── UserCard.jsx
├── contexts
└── UserContext.jsx
├── hoc
└── withoutAuth.jsx
├── hooks
└── useAPI.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── 404.jsx
├── _app.jsx
├── _document.jsx
├── about.jsx
├── admin
│ ├── [slug].jsx
│ ├── index.jsx
│ ├── manageCategories.jsx
│ └── manageQuotes.jsx
├── functions
│ ├── [slug].jsx
│ ├── chronometerTimer.jsx
│ ├── index.jsx
│ ├── linkShortener.jsx
│ ├── randomQuote.jsx
│ ├── rightPrice.jsx
│ └── toDoList.jsx
├── index.jsx
└── users
│ ├── [name].jsx
│ ├── forgotPassword.jsx
│ ├── index.jsx
│ ├── login.jsx
│ ├── newPassword.jsx
│ └── register.jsx
├── public
├── fonts
│ └── Montserrat
│ │ ├── Montserrat.css
│ │ └── files
│ │ ├── montserrat-latin-100.woff
│ │ ├── montserrat-latin-100.woff2
│ │ ├── montserrat-latin-100italic.woff
│ │ ├── montserrat-latin-100italic.woff2
│ │ ├── montserrat-latin-200.woff
│ │ ├── montserrat-latin-200.woff2
│ │ ├── montserrat-latin-200italic.woff
│ │ ├── montserrat-latin-200italic.woff2
│ │ ├── montserrat-latin-300.woff
│ │ ├── montserrat-latin-300.woff2
│ │ ├── montserrat-latin-300italic.woff
│ │ ├── montserrat-latin-300italic.woff2
│ │ ├── montserrat-latin-400.woff
│ │ ├── montserrat-latin-400.woff2
│ │ ├── montserrat-latin-400italic.woff
│ │ ├── montserrat-latin-400italic.woff2
│ │ ├── montserrat-latin-500.woff
│ │ ├── montserrat-latin-500.woff2
│ │ ├── montserrat-latin-500italic.woff
│ │ ├── montserrat-latin-500italic.woff2
│ │ ├── montserrat-latin-600.woff
│ │ ├── montserrat-latin-600.woff2
│ │ ├── montserrat-latin-600italic.woff
│ │ ├── montserrat-latin-600italic.woff2
│ │ ├── montserrat-latin-700.woff
│ │ ├── montserrat-latin-700.woff2
│ │ ├── montserrat-latin-700italic.woff
│ │ ├── montserrat-latin-700italic.woff2
│ │ ├── montserrat-latin-800.woff
│ │ ├── montserrat-latin-800.woff2
│ │ ├── montserrat-latin-800italic.woff
│ │ ├── montserrat-latin-800italic.woff2
│ │ ├── montserrat-latin-900.woff
│ │ ├── montserrat-latin-900.woff2
│ │ ├── montserrat-latin-900italic.woff
│ │ └── montserrat-latin-900italic.woff2
├── images
│ ├── FunctionProject_brand-logo.png
│ ├── FunctionProject_icon.png
│ ├── FunctionProject_icon_small.png
│ ├── GitHub.png
│ ├── error404.png
│ └── icons
│ │ ├── icon-128x128.png
│ │ ├── icon-144x144.png
│ │ ├── icon-152x152.png
│ │ ├── icon-192x192.png
│ │ ├── icon-384x384.png
│ │ ├── icon-512x512.png
│ │ ├── icon-72x72.png
│ │ └── icon-96x96.png
├── js
│ ├── extraHeightCSS.js
│ └── preloader.js
└── manifest.json
├── server.js
├── styles
├── components
│ ├── CommentCard.css
│ ├── FunctionComments.css
│ ├── FunctionTabs.css
│ ├── FunctionsList.css
│ ├── Header.css
│ └── UserCard.css
├── general.css
├── grid.css
├── normalize.css
├── nprogress.css
├── pages
│ ├── 404.css
│ ├── FunctionComponent.css
│ ├── admin.css
│ ├── functions
│ │ ├── chronometerTimer.css
│ │ ├── rightPrice.css
│ │ └── toDoList.css
│ ├── index.css
│ ├── profile.css
│ ├── register-login.css
│ └── users.css
└── suneditor.min.css
└── utils
├── api.js
├── copyToClipboard.js
├── redirect.js
└── sunEditorConfig.js
/.github/ISSUE_TEMPLATE/BUG.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: '🐛 Rapport de bug'
3 | about: 'Signalez un problème inattendu ou un comportement involontaire.'
4 | labels: 'bug'
5 | ---
6 |
7 | ## Étapes à suivre pour reproduire le bug
8 |
9 | 1. Étape 1
10 | 2. Étape 2
11 |
12 | ## Comportement actuel
13 |
14 | ## Comportement attendu
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: '✨ Ajout d'une fonctionnalité'
3 | about: 'Suggérer une nouvelle fonctionnalité.'
4 | labels: 'feature request'
5 | ---
6 |
7 | ### Description
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/QUESTION.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: '🙋 Question'
3 | about: 'Des informations complémentaires sont demandées.'
4 | labels: 'question'
5 | ---
6 |
7 | ### Question
8 |
--------------------------------------------------------------------------------
/.github/images/FunctionProject.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/.github/images/FunctionProject.png
--------------------------------------------------------------------------------
/.github/workflows/commitlint.yml:
--------------------------------------------------------------------------------
1 | # For more information see: https://github.com/marketplace/actions/commit-linter
2 |
3 | name: 'Lint Commit Messages'
4 |
5 | on:
6 | push:
7 | branches: [master]
8 | pull_request:
9 | branches: [master]
10 |
11 | jobs:
12 | commitlint:
13 | runs-on: 'ubuntu-latest'
14 | steps:
15 | - uses: 'actions/checkout@v2'
16 | with:
17 | fetch-depth: 0
18 | - uses: 'wagoid/commitlint-github-action@v2'
19 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
2 |
3 | name: 'Node.js CI'
4 |
5 | on:
6 | push:
7 | branches: [master]
8 | pull_request:
9 | branches: [master]
10 |
11 | jobs:
12 | ci_website:
13 | runs-on: 'ubuntu-latest'
14 | defaults:
15 | run:
16 | working-directory: 'website'
17 | strategy:
18 | matrix:
19 | node-version: [14.x]
20 | steps:
21 | - uses: 'actions/checkout@v2'
22 |
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: 'actions/setup-node@v2.1.2'
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 |
28 | - name: 'Cache dependencies'
29 | uses: 'actions/cache@v2'
30 | with:
31 | path: '**/node_modules'
32 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
33 |
34 | - run: 'npm install'
35 | - run: 'npm run lint'
36 | - run: 'npm run build'
37 |
38 | ci_api:
39 | runs-on: 'ubuntu-latest'
40 | defaults:
41 | run:
42 | working-directory: 'api'
43 | strategy:
44 | matrix:
45 | node-version: [14.x]
46 | steps:
47 | - uses: 'actions/checkout@v2'
48 |
49 | - name: Use Node.js ${{ matrix.node-version }}
50 | uses: 'actions/setup-node@v2.1.2'
51 | with:
52 | node-version: ${{ matrix.node-version }}
53 |
54 | - name: 'Cache dependencies'
55 | uses: 'actions/cache@v2'
56 | with:
57 | path: '**/node_modules'
58 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
59 |
60 | - run: 'npm install'
61 | - run: 'npm run lint'
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Divlo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ⚠️ Le projet n'est plus maintenu.
5 |
6 |
7 |
8 | Apprenez la programmation grâce à l'apprentissage par projet alias fonction.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## ⚙️ À propos
22 |
23 | **FunctionProject** regroupe plein de **fonctions** sous différentes catégories. Chaque fonction dispose d'une partie "**Utilisation**", et d'une partie "**Article**" pour expliquer le code de celle-çi (le plus souvent, le code est rédigé en **Javascript**).
24 |
25 | En plus de présenter des fonctions, FunctionProject est un **blog** ce qui permet la publication d'article à propos du **développement web** et plus généralement de la **programmation informatique**.
26 |
27 | Si vous aimez le projet, vous pouvez aider à **le faire connaître** en utilisant [#FunctionProject](https://twitter.com/hashtag/FunctionProject) sur **Twitter**. 🐦
28 |
29 | Les dernières versions publiées : [https://github.com/Divlo/FunctionProject/releases](https://github.com/Divlo/FunctionProject/releases)
30 |
31 | Le projet est disponible sur [function.divlo.fr](https://function.divlo.fr/) (actuellement en version 2.3).
32 |
33 | ## 🚀 Open Source
34 |
35 | Le partage est essentiel afin de progresser, l'**Open Source** permet l'entraide et le **partage de connaissance** entre développeurs.
36 |
37 | Si vous voulez **contribuer**, avant toute chose écrivez une **"[issue](https://github.com/Divlo/FunctionProject/issues)" sur GitHub** à propos des changements que vous voulez apporter et on pourra en **discuter avec grand plaisir**. 😉
38 |
39 | ## 🌐 Installation
40 |
41 | **Note :** En installant, la version locale vous n'aurez pas accès aux données. Seulement une **base de donnée vide**.
42 |
43 | Si vous voulez avoir les données des catégories et des fonctions, vous pouvez d'abord lancer l'API pour que Sequelize crée les tables SQl et ensuite exécuter le fichier SQL [backup.sql](./.github/backup.sql).
44 |
45 | ### Prérequis
46 |
47 | - [Node.js](https://nodejs.org/) >= 14
48 | - [npm](https://www.npmjs.com/) >= 7
49 | - [MySQL](https://www.mysql.com/) >= 8
50 |
51 | ### Commandes (à suivre dans l'ordre)
52 |
53 | ```sh
54 | # Cloner le projet
55 | git clone https://github.com/Divlo/FunctionProject.git FunctionProject
56 |
57 | # Aller à la racine du projet
58 | cd FunctionProject
59 |
60 | # Installer les packages/dépendances
61 | cd ./api
62 | npm install
63 | cd ../website
64 | npm install
65 | ```
66 |
67 | Vous devrez ensuite configurer les variables d'environnements en créant un fichier `.env` à la racine du dossier `/api`, `/website` et `s.divlo.fr` pour prendre exemple du fichier `.env.example` avec votre configuration.
68 |
69 | ### Lancer l'environnement de développement
70 |
71 | #### Avec [docker](https://www.docker.com/)
72 |
73 | ```sh
74 | # Setup and run all the services for you
75 | docker-compose up --build
76 | ```
77 |
78 | **Services started :**
79 |
80 | - api : `http://localhost:8080`
81 | - s.divlo.fr : `http://localhost:7000`
82 | - website : `http://localhost:3000`
83 | - [phpmyadmin](https://www.phpmyadmin.net/) : `http://localhost:8000`
84 | - [MailDev](https://maildev.github.io/maildev/) : `http://localhost:1080`
85 | - [MySQL database](https://www.mysql.com/) (with PORT 3006)
86 |
87 | #### Sans docker
88 |
89 | Dans deux terminals séparés :
90 |
91 | - Lancer le front-end en allant dans `/website`
92 |
93 | ```sh
94 | npm run dev # front-end lancé sur http://localhost:3000
95 | ```
96 |
97 | - Lancer l'api en allant dans `/api`
98 |
99 | ```sh
100 | npm run dev # API lancé sur http://localhost:8080
101 | ```
102 |
103 | Enjoy! 😃
104 |
105 | ## 📄 License
106 |
107 | [MIT](./LICENSE)
108 |
--------------------------------------------------------------------------------
/api/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
--------------------------------------------------------------------------------
/api/.env.example:
--------------------------------------------------------------------------------
1 | COMPOSE_PROJECT_NAME="function.divlo.fr-api"
2 | HOST="http://localhost:8080"
3 | FRONT_END_HOST="http://localhost:3000"
4 | OpenWeatherMap_API_KEY=""
5 | Scraper_API_KEY=""
6 | DATABASE_HOST="functionproject-database"
7 | DATABASE_NAME="functionproject"
8 | DATABASE_USER="root"
9 | DATABASE_PASSWORD="password"
10 | DATABASE_PORT=3306
11 | JWT_SECRET=""
12 | EMAIL_HOST="functionproject-maildev"
13 | EMAIL_USER="no-reply@functionproject.fr"
14 | EMAIL_PASSWORD="password"
15 | EMAIL_PORT=25
16 |
--------------------------------------------------------------------------------
/api/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # envs
15 | .env
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .env.production
21 |
22 | # misc
23 | .DS_Store
24 | /temp
25 | /assets/images/users
26 |
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
--------------------------------------------------------------------------------
/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.16.1
2 |
3 | WORKDIR /app
4 |
5 | COPY ./package*.json ./
6 | RUN npm install
7 | COPY ./ ./
8 |
9 | # docker-compose-wait
10 | ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
11 | RUN chmod +x /wait
12 |
13 | CMD /wait && npm run dev
14 |
--------------------------------------------------------------------------------
/api/app.js:
--------------------------------------------------------------------------------
1 | /* Modules */
2 | require('dotenv').config()
3 | const path = require('path')
4 | const express = require('express')
5 | const helmet = require('helmet')
6 | const cors = require('cors')
7 | const morgan = require('morgan')
8 | const { redirectToHTTPS } = require('express-http-to-https')
9 | const rateLimit = require('express-rate-limit')
10 |
11 | /* Files Imports & Variables */
12 | const sequelize = require('./assets/utils/database')
13 | const { PORT } = require('./assets/config/config')
14 | const errorHandling = require('./assets/utils/errorHandling')
15 | const isAuth = require('./middlewares/isAuth')
16 | const isAdmin = require('./middlewares/isAdmin')
17 | const app = express()
18 |
19 | /* Middlewares */
20 | if (process.env.NODE_ENV === 'development') {
21 | app.use(morgan('dev'))
22 | } else if (process.env.NODE_ENV === 'production') {
23 | app.use(redirectToHTTPS())
24 | const requestPerSecond = 2
25 | const seconds = 60
26 | const windowMs = seconds * 1000
27 | app.enable('trust proxy')
28 | app.use(
29 | rateLimit({
30 | windowMs,
31 | max: seconds * requestPerSecond,
32 | handler: (_req, res) => {
33 | return res.status(429).json({ message: 'Too many requests' })
34 | }
35 | })
36 | )
37 | }
38 | app.use(helmet())
39 | app.use(cors())
40 | app.use(express.json())
41 |
42 | /* Routes */
43 | app.use('/images', express.static(path.join(__dirname, 'assets', 'images')))
44 | app.use('/functions', require('./routes/functions'))
45 | app.use('/categories', require('./routes/categories'))
46 | app.use('/users', require('./routes/users'))
47 | app.use('/admin', isAuth, isAdmin, require('./routes/admin'))
48 | app.use('/favorites', require('./routes/favorites'))
49 | app.use('/comments', require('./routes/comments'))
50 | app.use('/quotes', require('./routes/quotes'))
51 | app.use('/tasks', require('./routes/tasks'))
52 | app.use('/links', require('./routes/links_shortener'))
53 |
54 | /* Errors Handling */
55 | app.use((_req, _res, next) =>
56 | errorHandling(next, { statusCode: 404, message: "La route n'existe pas!" })
57 | )
58 | app.use((error, _req, res, _next) => {
59 | console.log(error)
60 | const { statusCode, message } = error
61 | return res.status(statusCode || 500).json({ message })
62 | })
63 |
64 | /* Database Relations */
65 | const Functions = require('./models/functions')
66 | const Categories = require('./models/categories')
67 | const Users = require('./models/users')
68 | const Favorites = require('./models/favorites')
69 | const Comments = require('./models/comments')
70 | const Quotes = require('./models/quotes')
71 | const Tasks = require('./models/tasks')
72 | const ShortLinks = require('./models/short_links')
73 |
74 | // A function has a category
75 | Categories.hasOne(Functions, { constraints: true, onDelete: 'CASCADE' })
76 | Functions.belongsTo(Categories)
77 |
78 | // Users can have favorites functions
79 | Users.hasMany(Favorites)
80 | Favorites.belongsTo(Users, { constraints: false })
81 | Functions.hasMany(Favorites)
82 | Favorites.belongsTo(Functions, { constraints: false })
83 |
84 | // Users can post comments on functions
85 | Users.hasMany(Comments)
86 | Comments.belongsTo(Users, { constraints: false })
87 | Functions.hasMany(Comments)
88 | Comments.belongsTo(Functions, { constraints: false })
89 |
90 | // Users can suggest new quotes
91 | Users.hasMany(Quotes)
92 | Quotes.belongsTo(Users, { constraints: false })
93 |
94 | // Users can have tasks
95 | Users.hasMany(Tasks)
96 | Tasks.belongsTo(Users, { constraints: false })
97 |
98 | // Users can have links
99 | Users.hasMany(ShortLinks)
100 | ShortLinks.belongsTo(Users, { constraints: false })
101 |
102 | /* Server */
103 | sequelize
104 | .sync()
105 | .then(() => {
106 | app.listen(PORT, () =>
107 | console.log('\x1b[36m%s\x1b[0m', `Started on port ${PORT}.`)
108 | )
109 | })
110 | .catch(error => console.log(error))
111 |
--------------------------------------------------------------------------------
/api/assets/config/config.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv')
2 |
3 | dotenv.config()
4 | const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)
5 |
6 | const config = {
7 | PORT: process.env.PORT || 8080,
8 | HOST: process.env.HOST,
9 | FRONT_END_HOST: process.env.FRONT_END_HOST,
10 | WEATHER_API_KEY: process.env.OpenWeatherMap_API_KEY,
11 | SCRAPER_API_KEY: process.env.Scraper_API_KEY,
12 | DATABASE: {
13 | host: process.env.DATABASE_HOST,
14 | name: process.env.DATABASE_NAME,
15 | user: process.env.DATABASE_USER,
16 | password: process.env.DATABASE_PASSWORD,
17 | port: parseInt(process.env.DATABASE_PORT ?? '3306', 10)
18 | },
19 | JWT_SECRET: process.env.JWT_SECRET,
20 | EMAIL_INFO: {
21 | host: process.env.EMAIL_HOST,
22 | port: EMAIL_PORT,
23 | secure: EMAIL_PORT === 465,
24 | auth: {
25 | user: process.env.EMAIL_USER,
26 | pass: process.env.EMAIL_PASSWORD
27 | },
28 | ignoreTLS: process.env.NODE_ENV !== 'production'
29 | },
30 | TOKEN_LIFE: '1 week'
31 | }
32 |
33 | module.exports = config
34 |
--------------------------------------------------------------------------------
/api/assets/config/errors.js:
--------------------------------------------------------------------------------
1 | const errors = {
2 | generalError: {
3 | message: "Vous n'avez pas rentré de valeur valide.",
4 | statusCode: 400
5 | },
6 |
7 | serverError: {
8 | message: "Le serveur n'a pas pu traiter votre requête.",
9 | statusCode: 500
10 | },
11 |
12 | requiredFields: {
13 | message: 'Vous devez remplir tous les champs...',
14 | statusCode: 400
15 | }
16 | }
17 |
18 | module.exports = errors
19 |
--------------------------------------------------------------------------------
/api/assets/config/transporter.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer')
2 | const { EMAIL_INFO } = require('./config')
3 |
4 | const transporter = nodemailer.createTransport(EMAIL_INFO)
5 |
6 | module.exports = transporter
7 |
--------------------------------------------------------------------------------
/api/assets/functions/functionObject.js:
--------------------------------------------------------------------------------
1 | const { randomNumberOutput } = require('./main/randomNumber')
2 | const convertRomanArabicNumbersOutput = require('./main/convertRomanArabicNumbers')
3 | const convertDistanceOutput = require('./main/convertDistance')
4 | const convertTemperatureOutput = require('./main/convertTemperature')
5 | const armstrongNumberOutput = require('./main/armstrongNumber')
6 | const weatherRequestOutput = require('./main/weatherRequest')
7 | const convertCurrencyOutput = require('./main/convertCurrency')
8 | const calculateAgeOutput = require('./main/calculateAge')
9 | const heapAlgorithmOutput = require('./main/heapAlgorithm')
10 | const convertEncodingOutput = require('./main/convertEncoding')
11 | const randomQuote = require('./main/randomQuote')
12 | const rightPriceOutput = require('./main/rightPrice')
13 | const isPalindromeOutput = require('./main/isPalindrome')
14 | const findLongestWordOutput = require('./main/findLongestWord')
15 | const fibonacciOutput = require('./main/fibonacci')
16 | const sortArrayOutput = require('./main/sortArray')
17 |
18 | const functionObject = {
19 | randomNumber: randomNumberOutput,
20 | convertRomanArabicNumbers: convertRomanArabicNumbersOutput,
21 | convertDistance: convertDistanceOutput,
22 | convertTemperature: convertTemperatureOutput,
23 | armstrongNumber: armstrongNumberOutput,
24 | weatherRequest: weatherRequestOutput,
25 | convertCurrency: convertCurrencyOutput,
26 | calculateAge: calculateAgeOutput,
27 | heapAlgorithm: heapAlgorithmOutput,
28 | convertEncoding: convertEncodingOutput,
29 | randomQuote: randomQuote,
30 | rightPrice: rightPriceOutput,
31 | isPalindrome: isPalindromeOutput,
32 | findLongestWord: findLongestWordOutput,
33 | fibonacci: fibonacciOutput,
34 | sortArray: sortArrayOutput
35 | }
36 |
37 | // Choisi la fonction à exécuter
38 | function functionToExecute (option) {
39 | return functionObject[option]
40 | }
41 |
42 | module.exports = functionToExecute
43 |
--------------------------------------------------------------------------------
/api/assets/functions/main/armstrongNumber.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | /**
6 | * @description Vérifie si un nombre fait partie des nombres d'Armstrong.
7 | * @param {Number} number - Le nombre à tester
8 | * @returns {Object} Un objet contenant l'explication en html et le booléen si oui ou non c'est un nombre d'armstrong
9 | * @examples armstrongNumber(153) → 153 est un nombre d'Armstrong, car 13 + 53 + 33 = 153.
10 | */
11 | function armstrongNumber (number) {
12 | const numberString = number.toString()
13 | const numberStringLength = numberString.length
14 |
15 | let result = 0
16 | let resultString = ''
17 | for (let index = 0; index < numberStringLength; index++) {
18 | result = result + parseInt(numberString[index]) ** numberStringLength
19 | resultString =
20 | resultString +
21 | ' + ' +
22 | numberString[index] +
23 | '' +
24 | numberStringLength +
25 | ' '
26 | }
27 |
28 | const formattedNumber = formatNumberResult(number)
29 | const isArmstrongNumber = result === number
30 | return {
31 | isArmstrongNumber,
32 | resultHTML: `${formattedNumber} ${
33 | isArmstrongNumber ? 'est' : "n'est pas"
34 | } un nombre d'Armstrong, car ${resultString.slice(
35 | 2
36 | )} = ${formatNumberResult(result)}.
`
37 | }
38 | }
39 |
40 | /* OUTPUTS */
41 | module.exports = ({ res, next }, argsObject) => {
42 | let { number } = argsObject
43 |
44 | // S'il n'y a pas les champs obligatoire
45 | if (!number) {
46 | return errorHandling(next, requiredFields)
47 | }
48 |
49 | // Si ce n'est pas un nombre
50 | number = parseInt(number)
51 | if (isNaN(number) || number <= 0) {
52 | return errorHandling(next, {
53 | message: 'Veuillez rentré un nombre valide.',
54 | statusCode: 400
55 | })
56 | }
57 |
58 | return res.status(200).json(armstrongNumber(number))
59 | }
60 |
--------------------------------------------------------------------------------
/api/assets/functions/main/calculateAge.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const moment = require('moment')
3 | const { requiredFields } = require('../../config/errors')
4 |
5 | function calculateAge (
6 | currentDate,
7 | { birthDateDay, birthDateMonth, birthDateYear }
8 | ) {
9 | const day = currentDate.getDate()
10 | const month = currentDate.getMonth()
11 | const currentDateMoment = moment([currentDate.getFullYear(), month, day])
12 | const birthDateMoment = moment([birthDateYear, birthDateMonth, birthDateDay])
13 |
14 | // Calcule l'âge - Moment.js
15 | const ageYears = currentDateMoment.diff(birthDateMoment, 'year')
16 | birthDateMoment.add(ageYears, 'years')
17 | const ageMonths = currentDateMoment.diff(birthDateMoment, 'months')
18 | birthDateMoment.add(ageMonths, 'months')
19 | const ageDays = currentDateMoment.diff(birthDateMoment, 'days')
20 |
21 | const isBirthday = birthDateDay === day && birthDateMonth === month
22 | return { ageYears, ageMonths, ageDays, isBirthday }
23 | }
24 |
25 | /* OUTPUTS */
26 | module.exports = ({ res, next }, argsObject) => {
27 | const { birthDate } = argsObject
28 |
29 | // S'il n'y a pas les champs obligatoire
30 | if (!birthDate) {
31 | return errorHandling(next, requiredFields)
32 | }
33 |
34 | const birthDateDay = parseInt(birthDate.substring(0, 2))
35 | const birthDateMonth = parseInt(birthDate.substring(3, 5) - 1)
36 | const birthDateYear = parseInt(birthDate.substring(6, 10))
37 |
38 | // Si ce n'est pas une date valide
39 | const currentDate = new Date()
40 | const birthDateObject = new Date(birthDateYear, birthDateMonth, birthDateDay)
41 | const result = calculateAge(currentDate, {
42 | birthDateYear,
43 | birthDateMonth,
44 | birthDateDay
45 | })
46 | if (currentDate < birthDateObject || isNaN(result.ageYears)) {
47 | return errorHandling(next, {
48 | message: 'Veuillez rentré une date valide...',
49 | statusCode: 400
50 | })
51 | }
52 |
53 | let resultHTML
54 | if (result.isBirthday) {
55 | resultHTML = `Vous avez ${result.ageYears} ans. Joyeux Anniversaire! 🥳
`
56 | } else {
57 | resultHTML = `Vous avez ${result.ageYears} ans, ${result.ageMonths} mois et ${result.ageDays} jour(s).
`
58 | }
59 | return res.status(200).json({ ...result, resultHTML })
60 | }
61 |
--------------------------------------------------------------------------------
/api/assets/functions/main/convertCurrency.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const errorHandling = require('../../utils/errorHandling')
3 | const { requiredFields } = require('../../config/errors')
4 | const formatNumberResult = require('../secondary/formatNumberResult')
5 |
6 | /* OUTPUTS */
7 | module.exports = ({ res, next }, argsObject) => {
8 | let { number, baseCurrency, finalCurrency } = argsObject
9 |
10 | // S'il n'y a pas les champs obligatoire
11 | if (!(number && baseCurrency && finalCurrency)) {
12 | return errorHandling(next, requiredFields)
13 | }
14 |
15 | // Si ce n'est pas un nombre
16 | number = parseFloat(number)
17 | if (isNaN(number)) {
18 | return errorHandling(next, {
19 | message: 'Veuillez rentré un nombre valide.',
20 | statusCode: 400
21 | })
22 | }
23 |
24 | axios
25 | .get(`https://api.exchangeratesapi.io/latest?base=${baseCurrency}`)
26 | .then(response => {
27 | const rate = response.data.rates[finalCurrency]
28 | if (!rate) {
29 | return errorHandling(next, {
30 | message: "La devise n'existe pas.",
31 | statusCode: 404
32 | })
33 | }
34 | const result = rate * number
35 | const dateObject = new Date(response.data.date)
36 | const year = dateObject.getFullYear()
37 | const day = ('0' + dateObject.getDate()).slice(-2)
38 | const month = ('0' + (dateObject.getMonth() + 1)).slice(-2)
39 | const date = `${day}/${month}/${year}`
40 | const resultHTML = `${formatNumberResult(number)} ${
41 | response.data.base
42 | } = ${formatNumberResult(
43 | result.toFixed(2)
44 | )} ${finalCurrency}
Dernier rafraîchissement du taux d'échange : ${date}
`
45 | return res.status(200).json({ date, result, resultHTML })
46 | })
47 | .catch(() =>
48 | errorHandling(next, {
49 | message: "La devise n'existe pas.",
50 | statusCode: 404
51 | })
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/api/assets/functions/main/convertDistance.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields, generalError } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | const correspondancesDistance = [
6 | 'pm',
7 | null,
8 | null,
9 | 'nm',
10 | null,
11 | null,
12 | 'µm',
13 | null,
14 | null,
15 | 'mm',
16 | 'cm',
17 | 'dm',
18 | 'm',
19 | 'dam',
20 | 'hm',
21 | 'km',
22 | null,
23 | null,
24 | 'Mm',
25 | null,
26 | null,
27 | 'Gm',
28 | null,
29 | null,
30 | 'Tm'
31 | ]
32 |
33 | /**
34 | * @description Convertis la longueur (distance) avec les unités allant de picomètre au Téramètre.
35 | * @requires {@link correspondancesDistance}
36 | * @param {Number} firstValue - Le nombre que vous voulez convertir
37 | * @param {String} unitFirstValue - L'unité du nombre que vous voulez convertir
38 | * @param {String} unitFinalValue - L'unité de votre nombre après la conversion
39 | * @returns {Object|Boolean} → false si arguments non valides et sinon un objet contenant la string et le nombre résultat
40 | * @examples convertDistance(500, 'cm', 'm') → { resultNumber: 5, resultString: "5 m" }
41 | */
42 | function convertDistance (firstValue, unitFirstValue, unitFinalValue) {
43 | const index1 = correspondancesDistance.indexOf(unitFirstValue)
44 | const index2 = correspondancesDistance.indexOf(unitFinalValue)
45 | if (index1 !== -1 && index2 !== -1) {
46 | const difference = index1 - index2
47 | const result = firstValue * Math.pow(10, difference)
48 | return {
49 | result,
50 | resultHTML: `${formatNumberResult(
51 | firstValue
52 | )} ${unitFirstValue} = ${formatNumberResult(
53 | result
54 | )} ${unitFinalValue}
`
55 | }
56 | }
57 | return false
58 | }
59 |
60 | /* OUTPUTS */
61 | module.exports = ({ res, next }, argsObject) => {
62 | let { number, numberUnit, finalUnit } = argsObject
63 |
64 | // S'il n'y a pas les champs obligatoire
65 | if (!(number && numberUnit && finalUnit)) {
66 | return errorHandling(next, requiredFields)
67 | }
68 |
69 | // Si ce n'est pas un nombre
70 | number = parseFloat(number)
71 | if (isNaN(number)) {
72 | return errorHandling(next, {
73 | message: 'Veuillez rentré un nombre valide.',
74 | statusCode: 400
75 | })
76 | }
77 |
78 | const result = convertDistance(number, numberUnit, finalUnit)
79 | if (!result) {
80 | return errorHandling(next, generalError)
81 | }
82 |
83 | return res.status(200).json(result)
84 | }
85 |
--------------------------------------------------------------------------------
/api/assets/functions/main/convertRomanArabicNumbers.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields, generalError } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | /* Variable pour convertRomanArabicNumbers */
6 | const correspondancesRomainArabe = [
7 | [1000, 'M'],
8 | [900, 'CM'],
9 | [500, 'D'],
10 | [400, 'CD'],
11 | [100, 'C'],
12 | [90, 'XC'],
13 | [50, 'L'],
14 | [40, 'XL'],
15 | [10, 'X'],
16 | [9, 'IX'],
17 | [5, 'V'],
18 | [4, 'IV'],
19 | [1, 'I']
20 | ]
21 |
22 | /**
23 | * @description Convertis un nombre arabe en nombre romain.
24 | * @param {number} nombre - Le nombre arabe à convertir
25 | * @returns {string}
26 | * @examples convertArabicToRoman(24) → 'XXIV'
27 | */
28 | function convertArabicToRoman (nombre) {
29 | // Initialisation de la variable qui va contenir le résultat de la conversion
30 | let chiffresRomains = ''
31 |
32 | function extraireChiffreRomain (valeurLettre, lettres) {
33 | while (nombre >= valeurLettre) {
34 | chiffresRomains = chiffresRomains + lettres
35 | nombre = nombre - valeurLettre
36 | }
37 | }
38 |
39 | correspondancesRomainArabe.forEach(correspondance => {
40 | extraireChiffreRomain(correspondance[0], correspondance[1])
41 | })
42 |
43 | return chiffresRomains
44 | }
45 |
46 | /**
47 | * @description Convertis un nombre romain en nombre arabe.
48 | * @param {string} string - Le nombre romain à convertir
49 | * @return {number}
50 | * @example convertRomanToArabic('XXIV') → 24
51 | */
52 | function convertRomanToArabic (string) {
53 | let result = 0
54 | correspondancesRomainArabe.forEach(correspondance => {
55 | while (string.indexOf(correspondance[1]) === 0) {
56 | // Ajout de la valeur décimale au résultat
57 | result += correspondance[0]
58 | // Supprimer la lettre romaine correspondante du début
59 | string = string.replace(correspondance[1], '')
60 | }
61 | })
62 | if (string !== '') {
63 | result = 0
64 | }
65 | return result
66 | }
67 |
68 | /* OUTPUTS */
69 | const convertRomanToArabicOutput = ({ res, next }, number) => {
70 | // S'il n'y a pas les champs obligatoire
71 | if (!number) {
72 | return errorHandling(next, requiredFields)
73 | }
74 |
75 | // Formate le paramètre
76 | number = number.toUpperCase()
77 |
78 | const result = convertRomanToArabic(number)
79 | if (result === 0) {
80 | return errorHandling(next, generalError)
81 | }
82 |
83 | return res
84 | .status(200)
85 | .json({
86 | result,
87 | resultHTML: `${number} s'écrit ${result} en chiffres arabes.
`
88 | })
89 | }
90 |
91 | const convertArabicToRomanOutput = ({ res, next }, number) => {
92 | // S'il n'y a pas les champs obligatoire
93 | if (!number) {
94 | return errorHandling(next, requiredFields)
95 | }
96 |
97 | // Si ce n'est pas un nombre
98 | number = parseInt(number)
99 | if (isNaN(number)) {
100 | return errorHandling(next, {
101 | message: 'Veuillez rentré un nombre valide.',
102 | statusCode: 400
103 | })
104 | }
105 |
106 | const result = convertArabicToRoman(number)
107 | return res
108 | .status(200)
109 | .json({
110 | result,
111 | resultHTML: `${formatNumberResult(
112 | number
113 | )} s'écrit ${result} en chiffres romains.
`
114 | })
115 | }
116 |
117 | const convertRomanArabicObject = {
118 | convertRomanToArabicOutput,
119 | convertArabicToRomanOutput
120 | }
121 | function executeFunction (option, value, { res, next }) {
122 | return convertRomanArabicObject[option]({ res, next }, value)
123 | }
124 |
125 | module.exports = ({ res, next }, argsObject) => {
126 | const { value, functionName } = argsObject
127 |
128 | // S'il n'y a pas les champs obligatoire
129 | if (!(value && functionName)) {
130 | return errorHandling(next, requiredFields)
131 | }
132 |
133 | // Si la fonction n'existe pas
134 | // eslint-disable-next-line
135 | if (!convertRomanArabicObject.hasOwnProperty(functionName)) {
136 | return errorHandling(next, {
137 | message: "Cette conversion n'existe pas.",
138 | statusCode: 400
139 | })
140 | }
141 |
142 | executeFunction(functionName, value, { res, next })
143 | }
144 |
--------------------------------------------------------------------------------
/api/assets/functions/main/convertTemperature.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields, generalError } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | /**
6 | * @description Convertis des °C en °F et l'inverse aussi.
7 | * @param {Number} degree - Nombre de degrès
8 | * @param {String} unit - Unité du nombre (°C ou °F) après conversion
9 | * @returns {Object} false si arguments non valides et sinon un objet contenant la string et le nombre résultat
10 | * @examples convertTemperature(23, '°F') → { result: 73.4, resultHTML: "73.4 °F" }
11 | */
12 | function convertTemperature (degree, unit) {
13 | let temperatureValue = 0
14 | if (unit === '°C') {
15 | temperatureValue = ((degree - 32) * 5) / 9
16 | } else if (unit === '°F') {
17 | temperatureValue = (degree * 9) / 5 + 32
18 | } else {
19 | return false
20 | }
21 | return {
22 | result: temperatureValue,
23 | resultHTML: `${formatNumberResult(degree)} ${
24 | unit === '°C' ? '°F' : '°C'
25 | } = ${formatNumberResult(temperatureValue)} ${unit}
`
26 | }
27 | }
28 |
29 | /* OUTPUTS */
30 | module.exports = ({ res, next }, argsObject) => {
31 | let { degree, unitToConvert } = argsObject
32 |
33 | // S'il n'y a pas les champs obligatoire
34 | if (!(degree && unitToConvert)) {
35 | return errorHandling(next, requiredFields)
36 | }
37 |
38 | // Si ce n'est pas un nombre
39 | degree = parseFloat(degree)
40 | if (isNaN(degree)) {
41 | return errorHandling(next, {
42 | message: 'Veuillez rentré un nombre valide.',
43 | statusCode: 400
44 | })
45 | }
46 |
47 | const result = convertTemperature(degree, unitToConvert)
48 | if (!result) {
49 | return errorHandling(next, generalError)
50 | }
51 |
52 | return res.status(200).json(result)
53 | }
54 |
--------------------------------------------------------------------------------
/api/assets/functions/main/fibonacci.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | /**
6 | * @description Calcule les counter premiers nombres de la suite de fibonacci.
7 | * @param {number} counter
8 | */
9 | function fibonacci (counter, result = [], a = 0, b = 1) {
10 | if (counter === 0) {
11 | return result
12 | }
13 | counter--
14 | result.push(a)
15 | return fibonacci(counter, result, b, a + b)
16 | }
17 |
18 | /* OUTPUTS */
19 | module.exports = ({ res, next }, argsObject) => {
20 | let { counter } = argsObject
21 |
22 | // S'il n'y a pas les champs obligatoire
23 | if (!counter) {
24 | return errorHandling(next, requiredFields)
25 | }
26 |
27 | // Si ce n'est pas un nombre
28 | counter = parseInt(counter)
29 | if (isNaN(counter)) {
30 | return errorHandling(next, {
31 | message: 'Veuillez rentré un nombre valide.',
32 | statusCode: 400
33 | })
34 | }
35 |
36 | // Si le nombre dépasse LIMIT_COUNTER
37 | const LIMIT_COUNTER = 51
38 | if (counter >= LIMIT_COUNTER) {
39 | return errorHandling(next, {
40 | message: `Par souci de performance, vous ne pouvez pas exécuter cette fonction avec un compteur dépassant ${LIMIT_COUNTER -
41 | 1}.`,
42 | statusCode: 400
43 | })
44 | }
45 |
46 | const result = fibonacci(counter)
47 | const resultFormatted = result.map(number => formatNumberResult(number))
48 | return res.status(200).json({
49 | result,
50 | resultFormatted,
51 | resultHTML: `Les ${counter} premiers nombres de la suite de fibonacci : ${resultFormatted.join(
52 | ', '
53 | )}
`
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/api/assets/functions/main/findLongestWord.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields } = require('../../config/errors')
3 |
4 | /**
5 | * @description Renvoie le mot le plus long d'une chaîne de caractères
6 | * @param {string} string
7 | * @returns {string}
8 | * @example findLongestWord('Chaîne de caractères') → 'caractères'
9 | */
10 | function findLongestWord (string) {
11 | const arrayString = string.split(' ')
12 | let stringLength = 0
13 | let result = ''
14 |
15 | arrayString.forEach(element => {
16 | if (element.length > stringLength) {
17 | result = element
18 | stringLength = element.length
19 | }
20 | })
21 |
22 | return result
23 | }
24 |
25 | /* OUTPUTS */
26 | module.exports = ({ res, next }, argsObject) => {
27 | const { string } = argsObject
28 |
29 | // S'il n'y a pas les champs obligatoire
30 | if (!string) {
31 | return errorHandling(next, requiredFields)
32 | }
33 |
34 | const result = findLongestWord(string)
35 | return res.status(200).json({
36 | result,
37 | resultHTML: `Le mot le plus long est : "${result}"
`
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/api/assets/functions/main/heapAlgorithm.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | /**
6 | * @description Retourne un tableau contenant toutes les possibilités d'anagramme d'un mot.
7 | * @param {String} string - La chaîne de caractère à permuter
8 | * @returns {Array}
9 | * @examples heapAlgorithm('abc') → ["abc", "acb", "bac", "bca", "cab", "cba"]
10 | */
11 | function heapAlgorithm (string) {
12 | const results = []
13 |
14 | if (string.length === 1) {
15 | results.push(string)
16 | return results
17 | }
18 |
19 | for (let indexString = 0; indexString < string.length; indexString++) {
20 | const firstChar = string[indexString]
21 | const charsLeft =
22 | string.substring(0, indexString) + string.substring(indexString + 1)
23 | const innerPermutations = heapAlgorithm(charsLeft)
24 | for (
25 | let indexPermutation = 0;
26 | indexPermutation < innerPermutations.length;
27 | indexPermutation++
28 | ) {
29 | results.push(firstChar + innerPermutations[indexPermutation])
30 | }
31 | }
32 | return results
33 | }
34 |
35 | /* OUTPUTS */
36 | module.exports = ({ res, next }, argsObject) => {
37 | const { string } = argsObject
38 |
39 | // S'il n'y a pas les champs obligatoire
40 | if (!string) {
41 | return errorHandling(next, requiredFields)
42 | }
43 |
44 | // Si la chaîne de caractère dépasse LIMIT_CHARACTERS caractères
45 | const LIMIT_CHARACTERS = 7
46 | if (string.length > LIMIT_CHARACTERS) {
47 | return errorHandling(next, {
48 | message: `Par souci de performance, vous ne pouvez pas exécuter cette fonction avec un mot dépassant ${LIMIT_CHARACTERS} caractères.`,
49 | statusCode: 400
50 | })
51 | }
52 |
53 | const result = heapAlgorithm(string)
54 | let resultHTML = `Il y a ${formatNumberResult(
55 | result.length
56 | )} possibilités d'anagramme pour le mot "${string}" qui contient ${
57 | string.length
58 | } caractères, la liste : `
59 | result.forEach(string => {
60 | resultHTML += string + ' '
61 | })
62 | resultHTML += '
'
63 | return res.status(200).json({ result, resultHTML })
64 | }
65 |
--------------------------------------------------------------------------------
/api/assets/functions/main/isPalindrome.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields } = require('../../config/errors')
3 |
4 | /**
5 | * @description Inverse la chaîne de caractère
6 | * @param {string} string
7 | * @returns {string}
8 | * @example reverseString('Hello') → 'olleH'
9 | */
10 | function reverseString (string) {
11 | return string
12 | .split('')
13 | .reverse()
14 | .join('')
15 | }
16 |
17 | /**
18 | * @description Vérifie si un mot est un palindrome (un mot qui peut s'écrire dans les deux sens)
19 | * @requires reverseString
20 | * @param {string} string
21 | * @param {string} reverseStringResult La chaîne de caractères inversée
22 | * @returns {boolean}
23 | * @example isPalindrome('kayak') → true
24 | */
25 | function isPalindrome (string, reverseStringResult) {
26 | return string === reverseStringResult
27 | }
28 |
29 | /* OUTPUTS */
30 | module.exports = ({ res, next }, argsObject) => {
31 | let { string } = argsObject
32 |
33 | // S'il n'y a pas les champs obligatoire
34 | if (!string) {
35 | return errorHandling(next, requiredFields)
36 | }
37 |
38 | if (typeof string !== 'string') {
39 | return errorHandling(next, {
40 | message: 'Vous devez rentré une chaîne de caractère valide.',
41 | statusCode: 400
42 | })
43 | }
44 |
45 | string = string.toLowerCase()
46 |
47 | const reverseStringResult = reverseString(string)
48 | const isPalindromeResult = isPalindrome(string, reverseStringResult)
49 | return res.status(200).json({
50 | isPalindrome: isPalindromeResult,
51 | reverseString: reverseStringResult,
52 | resultHTML: `"${string}" ${
53 | isPalindromeResult ? 'est' : "n'est pas"
54 | } un palindrome car "${string}" ${
55 | isPalindromeResult ? '===' : '!=='
56 | } "${reverseStringResult}"
`
57 | })
58 | }
59 |
--------------------------------------------------------------------------------
/api/assets/functions/main/randomNumber.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | /**
6 | * @description Génère un nombre aléatoire entre un minimum inclus et un maximum inclus.
7 | * @param {Number} min Nombre Minimum
8 | * @param {Number} max Nombre Maximum
9 | * @returns {Number} Nombre aléatoire
10 | * @examples randomNumber(1, 2) → retourne soit 1 ou 2
11 | */
12 | function randomNumber (min, max) {
13 | return Math.floor(Math.random() * (max - min + 1)) + min
14 | }
15 |
16 | /* OUTPUTS */
17 | const randomNumberOutput = ({ res, next }, argsObject) => {
18 | let { min, max } = argsObject
19 |
20 | // S'il n'y a pas les champs obligatoire
21 | if (!(min && max)) {
22 | return errorHandling(next, requiredFields)
23 | }
24 |
25 | // Si ce ne sont pas des nombres
26 | min = parseInt(min)
27 | max = parseInt(max)
28 | if (isNaN(min) || isNaN(max)) {
29 | return errorHandling(next, {
30 | message: 'Les paramètres min et max doivent être des nombres...',
31 | statusCode: 400
32 | })
33 | }
34 |
35 | const result = randomNumber(min, max)
36 | return res
37 | .status(200)
38 | .json({
39 | result,
40 | resultHTML: `Nombre aléatoire compris entre ${min} inclus et ${max} inclus : ${formatNumberResult(
41 | result
42 | )}
`
43 | })
44 | }
45 |
46 | exports.randomNumber = randomNumber
47 | exports.randomNumberOutput = randomNumberOutput
48 |
--------------------------------------------------------------------------------
/api/assets/functions/main/randomQuote.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { serverError } = require('../../config/errors')
3 | const Quotes = require('../../../models/quotes')
4 | const Users = require('../../../models/users')
5 | const sequelize = require('../../utils/database')
6 |
7 | module.exports = async ({ res, next }, _argsObject) => {
8 | try {
9 | const quote = await Quotes.findOne({
10 | order: sequelize.random(),
11 | include: [{ model: Users, attributes: ['name', 'logo'] }],
12 | attributes: {
13 | exclude: ['isValidated']
14 | },
15 | where: {
16 | isValidated: 1
17 | }
18 | })
19 | return res.status(200).json(quote)
20 | } catch (error) {
21 | console.log(error)
22 | return errorHandling(next, serverError)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/api/assets/functions/main/rightPrice.js:
--------------------------------------------------------------------------------
1 | const { randomNumber } = require('./randomNumber')
2 | const errorHandling = require('../../utils/errorHandling')
3 | const { serverError } = require('../../config/errors')
4 | const { SCRAPER_API_KEY } = require('../../config/config')
5 | const axios = require('axios')
6 | const { JSDOM } = require('jsdom')
7 |
8 | const subjectList = [
9 | 'smartphone',
10 | 'pc+gamer',
11 | 'pc+portable',
12 | 'TV',
13 | 'casque',
14 | 'clavier',
15 | 'souris',
16 | 'ecran',
17 | 'jeux+vidéos'
18 | ]
19 |
20 | function getRandomArrayElement (array) {
21 | return array[randomNumber(0, array.length - 1)]
22 | }
23 |
24 | async function getAmazonProductList (subject) {
25 | const url = `https://www.amazon.fr/s?k=${subject}`
26 | const { data } = await axios.get(
27 | `http://api.scraperapi.com/?api_key=${SCRAPER_API_KEY}&url=${url}`
28 | )
29 | const { document } = new JSDOM(data).window
30 | const amazonProductList = document.querySelectorAll('.s-result-item')
31 | const productsList = []
32 | for (const indexProduct in amazonProductList) {
33 | try {
34 | const elementProduct = amazonProductList[indexProduct]
35 | const productImage = elementProduct.querySelector('.s-image')
36 | const originalPrice = elementProduct.querySelector('.a-price-whole')
37 | .innerHTML
38 | productsList.push({
39 | name: productImage.alt,
40 | image: productImage.src,
41 | price: Number(originalPrice.replace(',', '.').replace(' ', ''))
42 | })
43 | } catch (_error) {
44 | continue
45 | }
46 | }
47 | return productsList
48 | }
49 |
50 | module.exports = async ({ res, next }, _argsObject) => {
51 | const subject = getRandomArrayElement(subjectList)
52 | try {
53 | const productsList = await getAmazonProductList(subject)
54 | const randomProduct = getRandomArrayElement(productsList)
55 | return res.status(200).json({ subject, ...randomProduct })
56 | } catch (error) {
57 | console.error(error)
58 | return errorHandling(next, serverError)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/api/assets/functions/main/sortArray.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../../utils/errorHandling')
2 | const { requiredFields } = require('../../config/errors')
3 | const formatNumberResult = require('../secondary/formatNumberResult')
4 |
5 | function minNumber (array) {
6 | let minNumber = { index: 0, value: array[0] }
7 | for (let index = 1; index < array.length; index++) {
8 | const number = array[index]
9 | if (number < minNumber.value) {
10 | minNumber = { index: index, value: array[index] }
11 | }
12 | }
13 | return minNumber
14 | }
15 |
16 | function sortArray (array) {
17 | const arrayDuplicated = [...array]
18 | const resultArray = []
19 | while (array.length !== resultArray.length) {
20 | const min = minNumber(arrayDuplicated)
21 | resultArray.push(min.value)
22 | arrayDuplicated.splice(min.index, 1)
23 | }
24 | return resultArray
25 | }
26 |
27 | /* OUTPUTS */
28 | module.exports = ({ res, next }, argsObject) => {
29 | const { numbersList } = argsObject
30 |
31 | // S'il n'y a pas les champs obligatoire
32 | if (!numbersList) {
33 | return errorHandling(next, requiredFields)
34 | }
35 |
36 | const numbersListArray = numbersList
37 | .split(',')
38 | .map(number => number.trim().replace(' ', ''))
39 | .map(Number)
40 |
41 | // Si ce n'est pas une liste de nombres
42 | if (numbersListArray.includes(NaN)) {
43 | return errorHandling(next, {
44 | message:
45 | 'Vous devez rentrer une liste de nombres séparée par des virgules valide.',
46 | statusCode: 400
47 | })
48 | }
49 |
50 | // Si la taille du tableau dépasse LIMIT_ARRAY_LENGTH
51 | const LIMIT_ARRAY_LENGTH = 31
52 | if (numbersListArray.length >= LIMIT_ARRAY_LENGTH) {
53 | return errorHandling(next, {
54 | message: `Par souci de performance, vous ne pouvez pas exécuter cette fonction avec une liste de nombres dépassant ${LIMIT_ARRAY_LENGTH -
55 | 1} nombres.`,
56 | statusCode: 400
57 | })
58 | }
59 |
60 | const result = sortArray(numbersListArray)
61 | const resultFormatted = result.map(number => formatNumberResult(number))
62 | return res.status(200).json({
63 | result,
64 | resultFormatted,
65 | resultHTML: `La liste de nombres dans l'ordre croissant : ${resultFormatted.join(
66 | ', '
67 | )}
`
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/api/assets/functions/main/weatherRequest.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const Queue = require('smart-request-balancer')
3 | const errorHandling = require('../../utils/errorHandling')
4 | const { requiredFields } = require('../../config/errors')
5 | const { WEATHER_API_KEY } = require('../../config/config')
6 | const dateTimeUTC = require('../secondary/dateTimeManagement')
7 | const capitalize = require('../secondary/capitalize')
8 |
9 | const queue = new Queue({
10 | /*
11 | rate: number of requests
12 | per
13 | limit: number of seconds
14 | */
15 | rules: {
16 | weatherRequest: {
17 | rate: 50,
18 | limit: 60,
19 | priority: 1
20 | }
21 | }
22 | })
23 |
24 | /* OUTPUTS */
25 | module.exports = ({ res, next }, argsObject) => {
26 | let { cityName } = argsObject
27 |
28 | // S'il n'y a pas les champs obligatoire
29 | if (!cityName) {
30 | return errorHandling(next, requiredFields)
31 | }
32 |
33 | cityName = cityName.split(' ').join('+')
34 |
35 | // Récupère les données météo grâce à l'API : openweathermap.org. (→ avec limite de 50 requêtes par minute)
36 | queue.request(
37 | () => {
38 | axios
39 | .get(
40 | `https://api.openweathermap.org/data/2.5/weather?q=${cityName}&lang=fr&units=metric&appid=${WEATHER_API_KEY}`
41 | )
42 | .then(response => {
43 | const json = response.data
44 | const showDateTimeValue = dateTimeUTC(
45 | (json.timezone / 60 / 60).toString()
46 | ).showDateTimeValue
47 | const resultHTML = `🌎 Position : ${
50 | json.name
51 | }, ${
52 | json.sys.country
53 | } ⏰ Date et heure : ${showDateTimeValue} ☁️ Météo : ${capitalize(
54 | json.weather[0].description
55 | )} 🌡️ Température : ${json.main.temp} °C 💧 Humidité : ${
56 | json.main.humidity
57 | }%
`
60 | return res.status(200).json({ result: json, resultHTML })
61 | })
62 | .catch(() =>
63 | errorHandling(next, {
64 | message:
65 | "La ville n'existe pas (dans l'API de openweathermap.org).",
66 | statusCode: 404
67 | })
68 | )
69 | },
70 | 'everyone',
71 | 'weatherRequest'
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/api/assets/functions/secondary/capitalize.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Majuscule à la 1ère lettre d'une string.
3 | * @param {String} s
4 | * @returns {String}
5 | * @examples capitalize('hello world!') → 'Hello world!'
6 | */
7 | function capitalize (s) {
8 | if (typeof s !== 'string') return ''
9 | return s.charAt(0).toUpperCase() + s.slice(1)
10 | }
11 |
12 | module.exports = capitalize
13 |
--------------------------------------------------------------------------------
/api/assets/functions/secondary/dateTimeManagement.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Donne la date et l'heure selon l'UTC (Universal Time Coordinated).
3 | * @param {String} utc Heure de décalage par rapport à l'UTC
4 | * @returns {Function} → showDateTime(enteredOffset) → Retourne l'exécution de la fonction showDateTime
5 | * @examples dateTimeUTC('0')
6 | */
7 | function dateTimeUTC (utc) {
8 | const timeNow = new Date()
9 | const utcOffset = timeNow.getTimezoneOffset()
10 | timeNow.setMinutes(timeNow.getMinutes() + utcOffset)
11 | const enteredOffset = parseFloat(utc) * 60
12 | timeNow.setMinutes(timeNow.getMinutes() + enteredOffset)
13 | return showDateTime(timeNow)
14 | }
15 |
16 | /**
17 | * @description Affiche la date et l'heure (format : dd/mm/yyyy - 00:00:00).
18 | * @requires {@link fonctions_annexes.js: showDateTime}
19 | * @param {String} utc Heure de décalage par rapport à l'UTC
20 | * @returns {Object} Retourne un objet contenant l'année, le mois, le jour, l'heure, les minutes, les secondes et la date formaté
21 | * @examples dateTimeUTC('0') → dateTimeUTC vous renvoie l'exécution de showDateTime
22 | */
23 | function showDateTime (timeNow) {
24 | const year = timeNow.getFullYear()
25 | const month = ('0' + (timeNow.getMonth() + 1)).slice(-2)
26 | const day = ('0' + timeNow.getDate()).slice(-2)
27 | const hour = ('0' + timeNow.getHours()).slice(-2)
28 | const minute = ('0' + timeNow.getMinutes()).slice(-2)
29 | const second = ('0' + timeNow.getSeconds()).slice(-2)
30 | const showDateTimeValue =
31 | day + '/' + month + '/' + year + ' - ' + hour + ':' + minute + ':' + second
32 | const objectDateTime = {
33 | year: year,
34 | month: month,
35 | day: day,
36 | hour: hour,
37 | minute: minute,
38 | second: second,
39 | showDateTimeValue: showDateTimeValue
40 | }
41 | return objectDateTime
42 | }
43 |
44 | module.exports = dateTimeUTC
45 |
--------------------------------------------------------------------------------
/api/assets/functions/secondary/formatNumberResult.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Formate un nombre avec des espaces.
3 | * @param {Number} number
4 | * @param {String} separator Le séparateur utilisé pour la virgule (exemple: "." ou ",")
5 | * @returns {String} - Le nombre formaté
6 | * @examples formatNumberResult(76120) → '76 120'
7 | */
8 | function formatNumberResult (number, separator = '.') {
9 | const parts = number.toString().split(separator)
10 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
11 | return parts.join(separator)
12 | }
13 |
14 | module.exports = formatNumberResult
15 |
--------------------------------------------------------------------------------
/api/assets/images/functions/armstrongNumber.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/armstrongNumber.png
--------------------------------------------------------------------------------
/api/assets/images/functions/arrayMethods.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/arrayMethods.png
--------------------------------------------------------------------------------
/api/assets/images/functions/calculateAge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/calculateAge.png
--------------------------------------------------------------------------------
/api/assets/images/functions/chronometerTimer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/chronometerTimer.png
--------------------------------------------------------------------------------
/api/assets/images/functions/convertCurrency.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/convertCurrency.png
--------------------------------------------------------------------------------
/api/assets/images/functions/convertDistance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/convertDistance.png
--------------------------------------------------------------------------------
/api/assets/images/functions/convertEncoding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/convertEncoding.png
--------------------------------------------------------------------------------
/api/assets/images/functions/convertMarkdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/convertMarkdown.png
--------------------------------------------------------------------------------
/api/assets/images/functions/convertRomanArabicNumbers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/convertRomanArabicNumbers.png
--------------------------------------------------------------------------------
/api/assets/images/functions/convertTemperature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/convertTemperature.png
--------------------------------------------------------------------------------
/api/assets/images/functions/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/default.png
--------------------------------------------------------------------------------
/api/assets/images/functions/fibonacci.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/fibonacci.png
--------------------------------------------------------------------------------
/api/assets/images/functions/findLongestWord.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/findLongestWord.png
--------------------------------------------------------------------------------
/api/assets/images/functions/heapAlgorithm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/heapAlgorithm.png
--------------------------------------------------------------------------------
/api/assets/images/functions/isPalindrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/isPalindrome.png
--------------------------------------------------------------------------------
/api/assets/images/functions/linkShortener.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/linkShortener.png
--------------------------------------------------------------------------------
/api/assets/images/functions/randomNumber.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/randomNumber.png
--------------------------------------------------------------------------------
/api/assets/images/functions/randomQuote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/randomQuote.png
--------------------------------------------------------------------------------
/api/assets/images/functions/rightPrice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/rightPrice.png
--------------------------------------------------------------------------------
/api/assets/images/functions/sortArray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/sortArray.png
--------------------------------------------------------------------------------
/api/assets/images/functions/toDoList.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/toDoList.png
--------------------------------------------------------------------------------
/api/assets/images/functions/weatherRequest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/functions/weatherRequest.png
--------------------------------------------------------------------------------
/api/assets/images/users/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/api/assets/images/users/default.png
--------------------------------------------------------------------------------
/api/assets/utils/database.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const { DATABASE } = require('../config/config')
3 |
4 | const sequelize = new Sequelize(
5 | DATABASE.name,
6 | DATABASE.user,
7 | DATABASE.password,
8 | {
9 | dialect: 'mysql',
10 | host: DATABASE.host,
11 | port: DATABASE.port
12 | }
13 | )
14 |
15 | module.exports = sequelize
16 |
--------------------------------------------------------------------------------
/api/assets/utils/deleteFilesNameStartWith.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 |
4 | function deleteFilesNameStartWith (pattern, dirPath, callback) {
5 | fs.readdir(path.resolve(dirPath), (_error, fileNames) => {
6 | for (const name of fileNames) {
7 | const splitedName = name.split('.')
8 | if (splitedName.length === 2) {
9 | const fileName = splitedName[0]
10 | if (fileName === pattern && name !== 'default.png') {
11 | return fs.unlink(path.join(dirPath, name), callback)
12 | }
13 | }
14 | }
15 | return callback()
16 | })
17 | }
18 |
19 | module.exports = deleteFilesNameStartWith
20 |
--------------------------------------------------------------------------------
/api/assets/utils/errorHandling.js:
--------------------------------------------------------------------------------
1 | function errorHandling (next, { statusCode, message }) {
2 | const error = new Error(message)
3 | error.statusCode = statusCode
4 | next(error)
5 | }
6 |
7 | module.exports = errorHandling
8 |
--------------------------------------------------------------------------------
/api/assets/utils/getPagesHelper.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../utils/errorHandling')
2 | const { serverError } = require('../config/errors')
3 | const helperQueryNumber = require('../utils/helperQueryNumber')
4 |
5 | const DEFAULT_OPTIONS = {
6 | order: [['createdAt', 'DESC']]
7 | }
8 |
9 | /**
10 | * @description Permet de faire un système de pagination sur un model Sequelize
11 | * @param {Object} Object { req, res, next }
12 | * @param {*} Model Model Sequelize
13 | * @param {Object} options Options avec clause where etc.
14 | */
15 | async function getPagesHelper (
16 | { req, res, next },
17 | Model,
18 | options = DEFAULT_OPTIONS
19 | ) {
20 | const page = helperQueryNumber(req.query.page, 1)
21 | const limit = helperQueryNumber(req.query.limit, 10)
22 | const offset = (page - 1) * limit
23 | try {
24 | const result = await Model.findAndCountAll({
25 | limit,
26 | offset,
27 | ...options
28 | })
29 | const { count, rows } = result
30 | const hasMore = page * limit < count
31 | return res.status(200).json({ totalItems: count, hasMore, rows })
32 | } catch (error) {
33 | console.log(error)
34 | return errorHandling(next, serverError)
35 | }
36 | }
37 |
38 | module.exports = getPagesHelper
39 |
--------------------------------------------------------------------------------
/api/assets/utils/helperQueryNumber.js:
--------------------------------------------------------------------------------
1 | function helperQueryNumber (value, defaultValue) {
2 | if (value && !isNaN(value)) return parseInt(value)
3 | return defaultValue
4 | }
5 |
6 | module.exports = helperQueryNumber
7 |
--------------------------------------------------------------------------------
/api/controllers/categories.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../assets/utils/errorHandling')
2 | const Categories = require('../models/categories')
3 | const { serverError } = require('../assets/config/errors')
4 |
5 | exports.getCategories = (_req, res, next) => {
6 | Categories.findAll()
7 | .then(result => {
8 | res.status(200).json(result)
9 | })
10 | .catch(error => {
11 | console.log(error)
12 | return errorHandling(next, serverError)
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/api/controllers/comments.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../assets/utils/errorHandling')
2 | const Comments = require('../models/comments')
3 | const Users = require('../models/users')
4 | const Functions = require('../models/functions')
5 | const getPagesHelper = require('../assets/utils/getPagesHelper')
6 | const { serverError } = require('../assets/config/errors')
7 |
8 | exports.getCommentsByFunctionId = async (req, res, next) => {
9 | const { functionId } = req.params
10 | const options = {
11 | where: { functionId },
12 | include: [{ model: Users, attributes: ['name', 'logo'] }],
13 | order: [['createdAt', 'DESC']]
14 | }
15 | return await getPagesHelper({ req, res, next }, Comments, options)
16 | }
17 |
18 | exports.postCommentsByFunctionId = async (req, res, next) => {
19 | const { functionId } = req.params
20 | const { message } = req.body
21 | try {
22 | const resultFunction = await Functions.findOne({
23 | where: { id: functionId }
24 | })
25 | if (!resultFunction) {
26 | return errorHandling(next, {
27 | message: "La fonction n'existe pas.",
28 | statusCode: 404
29 | })
30 | }
31 | if (!message) {
32 | return errorHandling(next, {
33 | message: 'Vous ne pouvez pas poster de commentaire vide.',
34 | statusCode: 400
35 | })
36 | }
37 | const comment = await Comments.create({
38 | message,
39 | userId: req.userId,
40 | functionId
41 | })
42 | return res.status(201).json(comment)
43 | } catch (error) {
44 | console.log(error)
45 | return errorHandling(next, serverError)
46 | }
47 | }
48 |
49 | exports.deleteCommentById = async (req, res, next) => {
50 | const { commentId } = req.params
51 | try {
52 | const comment = await Comments.findOne({
53 | where: { userId: req.userId, id: parseInt(commentId) }
54 | })
55 | if (!comment) {
56 | return errorHandling(next, {
57 | message: "Le commentaire n'existe pas.",
58 | statusCode: 404
59 | })
60 | }
61 | await comment.destroy()
62 | return res
63 | .status(200)
64 | .json({ message: 'Le commentaire a bien été supprimé.' })
65 | } catch (error) {
66 | console.log(error)
67 | return errorHandling(next, serverError)
68 | }
69 | }
70 |
71 | exports.putCommentsById = async (req, res, next) => {
72 | const { commentId } = req.params
73 | const { message } = req.body
74 | if (!message) {
75 | return errorHandling(next, {
76 | message: 'Vous ne pouvez pas poster de commentaire vide.',
77 | statusCode: 400
78 | })
79 | }
80 | try {
81 | const comment = await Comments.findOne({
82 | where: { userId: req.userId, id: parseInt(commentId) }
83 | })
84 | if (!comment) {
85 | return errorHandling(next, {
86 | message: "Le commentaire n'existe pas.",
87 | statusCode: 404
88 | })
89 | }
90 | comment.message = message
91 | await comment.save()
92 | return res
93 | .status(200)
94 | .json({ message: 'Le commentaire a bien été modifié.' })
95 | } catch (error) {
96 | console.log(error)
97 | return errorHandling(next, serverError)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/api/controllers/favorites.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../assets/utils/errorHandling')
2 | const { serverError } = require('../assets/config/errors')
3 | const Favorites = require('../models/favorites')
4 | const Functions = require('../models/functions')
5 |
6 | exports.getFavoriteByFunctionId = async (req, res, next) => {
7 | const { functionId } = req.params
8 | const { userId } = req
9 | try {
10 | const favorite = await Favorites.findOne({
11 | where: {
12 | userId,
13 | functionId
14 | }
15 | })
16 | if (!favorite) {
17 | return res.status(200).json({ isFavorite: false })
18 | }
19 | return res.status(200).json({ isFavorite: true })
20 | } catch (error) {
21 | console.log(error)
22 | return errorHandling(next, serverError)
23 | }
24 | }
25 |
26 | exports.postFavoriteByFunctionId = async (req, res, next) => {
27 | const { functionId } = req.params
28 | const { userId } = req
29 | try {
30 | const resultFunction = await Functions.findOne({
31 | where: { id: functionId }
32 | })
33 | if (!resultFunction) {
34 | return errorHandling(next, {
35 | message: "La fonction n'existe pas.",
36 | statusCode: 404
37 | })
38 | }
39 | const favorite = await Favorites.findOne({
40 | where: {
41 | userId,
42 | functionId
43 | }
44 | })
45 | if (!favorite) {
46 | await Favorites.create({ userId, functionId })
47 | return res.status(201).json({ result: 'Le favoris a bien été ajouté!' })
48 | }
49 | return errorHandling(next, {
50 | message: 'La fonction est déjà en favoris.',
51 | statusCode: 400
52 | })
53 | } catch (error) {
54 | console.log(error)
55 | return errorHandling(next, serverError)
56 | }
57 | }
58 |
59 | exports.deleteFavoriteByFunctionId = async (req, res, next) => {
60 | const { functionId } = req.params
61 | const { userId } = req
62 | try {
63 | const resultFunction = await Functions.findOne({
64 | where: { id: functionId }
65 | })
66 | if (!resultFunction) {
67 | return errorHandling(next, {
68 | message: "La fonction n'existe pas.",
69 | statusCode: 404
70 | })
71 | }
72 | const favorite = await Favorites.findOne({
73 | where: {
74 | userId,
75 | functionId
76 | }
77 | })
78 | if (!favorite) {
79 | return errorHandling(next, {
80 | message: "Le fonction n'est pas en favoris.",
81 | statusCode: 400
82 | })
83 | }
84 | await favorite.destroy()
85 | return res
86 | .status(200)
87 | .json({ message: 'Le fonction a bien été supprimé des favoris.' })
88 | } catch (error) {
89 | console.log(error)
90 | return errorHandling(next, serverError)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/api/controllers/functions.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../assets/utils/errorHandling')
2 | const { serverError } = require('../assets/config/errors')
3 | const Functions = require('../models/functions')
4 | const Categories = require('../models/categories')
5 | const functionToExecute = require('../assets/functions/functionObject')
6 | const helperQueryNumber = require('../assets/utils/helperQueryNumber')
7 | const getPagesHelper = require('../assets/utils/getPagesHelper')
8 | const Sequelize = require('sequelize')
9 |
10 | exports.getFunctions = async (req, res, next) => {
11 | const categoryId = helperQueryNumber(req.query.categoryId, 0)
12 | let { search } = req.query
13 | try {
14 | search = search.toLowerCase()
15 | } catch {}
16 | const options = {
17 | where: {
18 | isOnline: 1,
19 | // Trie par catégorie
20 | ...(categoryId !== 0 && { categorieId: categoryId }),
21 | // Recherche
22 | ...(search != null && {
23 | [Sequelize.Op.or]: [
24 | {
25 | title: Sequelize.where(
26 | Sequelize.fn('LOWER', Sequelize.col('title')),
27 | 'LIKE',
28 | `%${search}%`
29 | )
30 | },
31 | {
32 | slug: Sequelize.where(
33 | Sequelize.fn('LOWER', Sequelize.col('slug')),
34 | 'LIKE',
35 | `%${search}%`
36 | )
37 | },
38 | {
39 | description: Sequelize.where(
40 | Sequelize.fn('LOWER', Sequelize.col('description')),
41 | 'LIKE',
42 | `%${search}%`
43 | )
44 | }
45 | ]
46 | })
47 | },
48 | include: [{ model: Categories, attributes: ['name', 'color'] }],
49 | attributes: {
50 | exclude: ['updatedAt', 'utilizationForm', 'article', 'isOnline']
51 | },
52 | order: [['createdAt', 'DESC']]
53 | }
54 | return await getPagesHelper({ req, res, next }, Functions, options)
55 | }
56 |
57 | exports.getFunctionBySlug = (req, res, next) => {
58 | const { slug } = req.params
59 | Functions.findOne({
60 | where: { slug, isOnline: 1 },
61 | attributes: {
62 | exclude: ['updatedAt', 'isOnline']
63 | },
64 | include: [{ model: Categories, attributes: ['name', 'color'] }]
65 | })
66 | .then(result => {
67 | if (!result) {
68 | return errorHandling(next, {
69 | message: "La fonction n'existe pas.",
70 | statusCode: 404
71 | })
72 | }
73 | try {
74 | result.utilizationForm = JSON.parse(result.utilizationForm)
75 | } catch {}
76 | return res.status(200).json(result)
77 | })
78 | .catch(error => {
79 | console.log(error)
80 | return errorHandling(next, serverError)
81 | })
82 | }
83 |
84 | exports.executeFunctionBySlug = (req, res, next) => {
85 | const functionOutput = functionToExecute(req.params.slug)
86 | if (functionOutput !== undefined) {
87 | return functionOutput({ res, next }, req.body)
88 | }
89 | return errorHandling(next, {
90 | message: "La fonction n'existe pas.",
91 | statusCode: 404
92 | })
93 | }
94 |
--------------------------------------------------------------------------------
/api/controllers/quotes.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../assets/utils/errorHandling')
2 | const { serverError, requiredFields } = require('../assets/config/errors')
3 | const Quotes = require('../models/quotes')
4 | const Users = require('../models/users')
5 | const getPagesHelper = require('../assets/utils/getPagesHelper')
6 |
7 | exports.getQuotes = async (req, res, next) => {
8 | const options = {
9 | where: {
10 | isValidated: 1
11 | },
12 | include: [{ model: Users, attributes: ['name', 'logo'] }],
13 | attributes: {
14 | exclude: ['isValidated']
15 | },
16 | order: [['createdAt', 'DESC']]
17 | }
18 | return await getPagesHelper({ req, res, next }, Quotes, options)
19 | }
20 |
21 | exports.postQuote = (req, res, next) => {
22 | const { quote, author } = req.body
23 | // S'il n'y a pas les champs obligatoire
24 | if (!(quote && author)) {
25 | return errorHandling(next, requiredFields)
26 | }
27 | Quotes.create({ quote, author, userId: req.userId })
28 | .then(_result => {
29 | return res
30 | .status(200)
31 | .json({
32 | message:
33 | "La citation a bien été ajoutée, elle est en attente de confirmation d'un administrateur."
34 | })
35 | })
36 | .catch(error => {
37 | console.log(error)
38 | return errorHandling(next, serverError)
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/api/controllers/tasks.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../assets/utils/errorHandling')
2 | const { serverError, requiredFields } = require('../assets/config/errors')
3 | const Tasks = require('../models/tasks')
4 |
5 | exports.getTasks = async (req, res, next) => {
6 | try {
7 | const tasks = await Tasks.findAll({
8 | where: {
9 | userId: req.userId
10 | },
11 | order: [['createdAt', 'DESC']]
12 | })
13 | return res.status(200).json(tasks)
14 | } catch (error) {
15 | console.log(error)
16 | return errorHandling(next, serverError)
17 | }
18 | }
19 |
20 | exports.postTask = async (req, res, next) => {
21 | const { task } = req.body
22 | try {
23 | if (!task) {
24 | return errorHandling(next, requiredFields)
25 | }
26 | const taskResult = await Tasks.create({ task, userId: req.userId })
27 | return res.status(201).json(taskResult)
28 | } catch (error) {
29 | console.log(error)
30 | return errorHandling(next, serverError)
31 | }
32 | }
33 |
34 | exports.putTask = async (req, res, next) => {
35 | const { id } = req.params
36 | const { isCompleted } = req.body
37 | try {
38 | if (typeof isCompleted !== 'boolean') {
39 | return errorHandling(next, {
40 | message: 'isCompleted doit être un booléen.',
41 | statusCode: 400
42 | })
43 | }
44 |
45 | const taskResult = await Tasks.findOne({
46 | where: { id, userId: req.userId }
47 | })
48 | if (!taskResult) {
49 | return errorHandling(next, {
50 | message: 'La "tâche à faire" n\'existe pas.',
51 | statusCode: 404
52 | })
53 | }
54 | taskResult.isCompleted = isCompleted
55 | const taskSaved = await taskResult.save()
56 | return res.status(200).json(taskSaved)
57 | } catch (error) {
58 | console.log(error)
59 | return errorHandling(next, serverError)
60 | }
61 | }
62 |
63 | exports.deleteTask = async (req, res, next) => {
64 | const { id } = req.params
65 | try {
66 | const taskResult = await Tasks.findOne({
67 | where: { id, userId: req.userId }
68 | })
69 | if (!taskResult) {
70 | return errorHandling(next, {
71 | message: 'La "tâche à faire" n\'existe pas.',
72 | statusCode: 404
73 | })
74 | }
75 | await taskResult.destroy()
76 | return res
77 | .status(200)
78 | .json({ message: 'La "tâche à faire" a bien été supprimée!' })
79 | } catch (error) {
80 | console.log(error)
81 | return errorHandling(next, serverError)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/api/middlewares/isAdmin.js:
--------------------------------------------------------------------------------
1 | const errorHandling = require('../assets/utils/errorHandling')
2 | const { serverError } = require('../assets/config/errors')
3 | const Users = require('../models/users')
4 |
5 | module.exports = (req, _res, next) => {
6 | if (!req.userId) {
7 | return errorHandling(next, {
8 | message: "Vous n'êtes pas connecté.",
9 | statusCode: 403
10 | })
11 | }
12 | Users.findOne({ where: { id: req.userId } })
13 | .then(user => {
14 | if (!user) {
15 | return errorHandling(next, {
16 | message: "Le mot de passe ou l'adresse email n'est pas valide.",
17 | statusCode: 403
18 | })
19 | }
20 | if (!user.isAdmin) {
21 | return errorHandling(next, {
22 | message: "Vous n'êtes pas administrateur.",
23 | statusCode: 403
24 | })
25 | }
26 | next()
27 | })
28 | .catch(error => {
29 | console.log(error)
30 | return errorHandling(next, serverError)
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/api/middlewares/isAuth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 | const errorHandling = require('../assets/utils/errorHandling')
3 | const { JWT_SECRET } = require('../assets/config/config')
4 |
5 | module.exports = (req, _res, next) => {
6 | const token = req.get('Authorization')
7 | if (!token) {
8 | return errorHandling(next, {
9 | message: 'Vous devez être connecter pour effectuer cette opération.',
10 | statusCode: 403
11 | })
12 | }
13 |
14 | let decodedToken
15 | try {
16 | decodedToken = jwt.verify(token, JWT_SECRET)
17 | } catch (error) {
18 | return errorHandling(next, {
19 | message: 'Vous devez être connecter pour effectuer cette opération.',
20 | statusCode: 403
21 | })
22 | }
23 |
24 | if (!decodedToken) {
25 | return errorHandling(next, {
26 | message: 'Vous devez être connecter pour effectuer cette opération.',
27 | statusCode: 403
28 | })
29 | }
30 |
31 | req.userId = decodedToken.userId
32 | next()
33 | }
34 |
--------------------------------------------------------------------------------
/api/models/categories.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const sequelize = require('../assets/utils/database')
3 |
4 | module.exports = sequelize.define('categorie', {
5 | name: {
6 | type: Sequelize.STRING,
7 | allowNull: false
8 | },
9 | color: {
10 | type: Sequelize.STRING,
11 | allowNull: false
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/api/models/comments.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const sequelize = require('../assets/utils/database')
3 |
4 | module.exports = sequelize.define('comment', {
5 | message: {
6 | type: Sequelize.TEXT,
7 | allowNull: false
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/api/models/favorites.js:
--------------------------------------------------------------------------------
1 | const sequelize = require('../assets/utils/database')
2 |
3 | module.exports = sequelize.define('favorite', {})
4 |
--------------------------------------------------------------------------------
/api/models/functions.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const sequelize = require('../assets/utils/database')
3 |
4 | module.exports = sequelize.define('function', {
5 | title: {
6 | type: Sequelize.STRING,
7 | allowNull: false
8 | },
9 | slug: {
10 | type: Sequelize.STRING,
11 | allowNull: false
12 | },
13 | description: {
14 | type: Sequelize.STRING,
15 | allowNull: false
16 | },
17 | image: {
18 | type: Sequelize.STRING,
19 | allowNull: false,
20 | defaultValue: '/images/functions/default.png'
21 | },
22 | type: {
23 | type: Sequelize.STRING,
24 | allowNull: false
25 | },
26 | article: {
27 | type: Sequelize.TEXT,
28 | allowNull: true
29 | },
30 | utilizationForm: {
31 | type: Sequelize.TEXT,
32 | allowNull: true
33 | },
34 | isOnline: {
35 | type: Sequelize.BOOLEAN,
36 | allowNull: false,
37 | defaultValue: 0
38 | }
39 | })
40 |
--------------------------------------------------------------------------------
/api/models/quotes.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const sequelize = require('../assets/utils/database')
3 |
4 | module.exports = sequelize.define('quote', {
5 | quote: {
6 | type: Sequelize.STRING,
7 | allowNull: false
8 | },
9 | author: {
10 | type: Sequelize.STRING,
11 | allowNull: false
12 | },
13 | isValidated: {
14 | type: Sequelize.BOOLEAN,
15 | allowNull: false,
16 | defaultValue: 0
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/api/models/short_links.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const sequelize = require('../assets/utils/database')
3 |
4 | module.exports = sequelize.define('short_link', {
5 | url: {
6 | type: Sequelize.TEXT,
7 | allowNull: false
8 | },
9 | shortcut: {
10 | type: Sequelize.TEXT,
11 | allowNull: false
12 | },
13 | count: {
14 | type: Sequelize.INTEGER,
15 | allowNull: false,
16 | defaultValue: 0
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/api/models/tasks.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const sequelize = require('../assets/utils/database')
3 |
4 | module.exports = sequelize.define('task', {
5 | task: {
6 | type: Sequelize.STRING,
7 | allowNull: false
8 | },
9 | isCompleted: {
10 | type: Sequelize.BOOLEAN,
11 | allowNull: false,
12 | defaultValue: 0
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/api/models/users.js:
--------------------------------------------------------------------------------
1 | const Sequelize = require('sequelize')
2 | const sequelize = require('../assets/utils/database')
3 |
4 | module.exports = sequelize.define('user', {
5 | name: {
6 | type: Sequelize.STRING,
7 | allowNull: false
8 | },
9 | email: {
10 | type: Sequelize.STRING,
11 | allowNull: false
12 | },
13 | password: {
14 | type: Sequelize.STRING,
15 | allowNull: false
16 | },
17 | biography: {
18 | type: Sequelize.TEXT,
19 | defaultValue: ''
20 | },
21 | logo: {
22 | type: Sequelize.STRING,
23 | defaultValue: '/images/users/default.png'
24 | },
25 | isConfirmed: {
26 | type: Sequelize.BOOLEAN,
27 | defaultValue: false
28 | },
29 | isPublicEmail: {
30 | type: Sequelize.BOOLEAN,
31 | defaultValue: false
32 | },
33 | isAdmin: {
34 | type: Sequelize.BOOLEAN,
35 | defaultValue: false
36 | },
37 | tempToken: {
38 | type: Sequelize.TEXT,
39 | allowNull: true
40 | },
41 | tempExpirationToken: {
42 | type: Sequelize.DATE,
43 | allowNull: true
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "version": "2.3.0",
4 | "description": "Backend REST API for FunctionProject",
5 | "standard": {
6 | "files": [
7 | "./**/*.js"
8 | ],
9 | "envs": [
10 | "node"
11 | ]
12 | },
13 | "scripts": {
14 | "start": "node app.js",
15 | "dev": "nodemon app.js",
16 | "lint": "standard | snazzy"
17 | },
18 | "dependencies": {
19 | "axios": "^0.21.1",
20 | "bcryptjs": "^2.4.3",
21 | "cors": "^2.8.5",
22 | "dotenv": "^8.2.0",
23 | "express": "^4.17.1",
24 | "express-fileupload": "^1.2.1",
25 | "express-http-to-https": "^1.1.4",
26 | "express-rate-limit": "^5.2.6",
27 | "express-validator": "^6.10.0",
28 | "helmet": "^4.4.1",
29 | "jsdom": "^16.5.3",
30 | "jsonwebtoken": "^8.5.1",
31 | "moment": "^2.29.1",
32 | "morgan": "^1.10.0",
33 | "ms": "^2.1.3",
34 | "mysql2": "^2.2.5",
35 | "nodemailer": "^6.5.0",
36 | "sequelize": "^6.6.2",
37 | "smart-request-balancer": "^2.1.1",
38 | "uuid": "^8.3.2",
39 | "validator": "^13.5.2"
40 | },
41 | "devDependencies": {
42 | "nodemon": "^2.0.7",
43 | "snazzy": "^9.0.0",
44 | "standard": "^16.0.3"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/api/routes/categories.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const categoriesController = require('../controllers/categories')
3 |
4 | const CategoriesRouter = Router()
5 |
6 | CategoriesRouter.route('/')
7 |
8 | // Récupère les catégories
9 | .get(categoriesController.getCategories)
10 |
11 | module.exports = CategoriesRouter
12 |
--------------------------------------------------------------------------------
/api/routes/comments.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const commentsController = require('../controllers/comments')
3 | const isAuth = require('../middlewares/isAuth')
4 |
5 | const CommentsRouter = Router()
6 |
7 | CommentsRouter.route('/:commentId')
8 |
9 | // Modifier un commentaire
10 | .put(isAuth, commentsController.putCommentsById)
11 |
12 | // Supprime un commentaire
13 | .delete(isAuth, commentsController.deleteCommentById)
14 |
15 | CommentsRouter.route('/:functionId')
16 |
17 | // Récupère les commentaires
18 | .get(commentsController.getCommentsByFunctionId)
19 |
20 | // Permet à un utilisateur de poster un commentaire sur une fonction
21 | .post(isAuth, commentsController.postCommentsByFunctionId)
22 |
23 | module.exports = CommentsRouter
24 |
--------------------------------------------------------------------------------
/api/routes/favorites.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const favoritesController = require('../controllers/favorites')
3 | const isAuth = require('../middlewares/isAuth')
4 |
5 | const FavoritesRouter = Router()
6 |
7 | FavoritesRouter.route('/:functionId')
8 |
9 | // Récupère si une fonction est en favoris (d'un utilisateur)
10 | .get(isAuth, favoritesController.getFavoriteByFunctionId)
11 |
12 | // Permet à un utilisateur d'ajouter une fonction aux favoris
13 | .post(isAuth, favoritesController.postFavoriteByFunctionId)
14 |
15 | // Supprime une fonction des favoris d'un utilisateur
16 | .delete(isAuth, favoritesController.deleteFavoriteByFunctionId)
17 |
18 | module.exports = FavoritesRouter
19 |
--------------------------------------------------------------------------------
/api/routes/functions.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const functionsController = require('../controllers/functions')
3 |
4 | const FunctionsRouter = Router()
5 |
6 | FunctionsRouter.route('/')
7 |
8 | // Récupère les fonctions
9 | .get(functionsController.getFunctions)
10 |
11 | FunctionsRouter.route('/:slug')
12 |
13 | // Récupère les informations de la fonction par son slug
14 | .get(functionsController.getFunctionBySlug)
15 |
16 | // Exécute la fonction demandée en paramètre
17 | .post(functionsController.executeFunctionBySlug)
18 |
19 | module.exports = FunctionsRouter
20 |
--------------------------------------------------------------------------------
/api/routes/links_shortener.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const linksShortenerController = require('../controllers/links_shortener')
3 | const isAuth = require('../middlewares/isAuth')
4 |
5 | const LinksShortenerRouter = Router()
6 |
7 | LinksShortenerRouter.route('/')
8 |
9 | // Récupère les liens d'un utilisateur
10 | .get(isAuth, linksShortenerController.getLinks)
11 |
12 | // Ajouter un lien à raccourcir d'un utilisateur
13 | .post(isAuth, linksShortenerController.postLink)
14 |
15 | LinksShortenerRouter.route('/:id')
16 |
17 | // Permet de modifier le lien raccourci d'un utilisateur
18 | .put(isAuth, linksShortenerController.putLink)
19 |
20 | // Supprimer un lien d'un utilisateur
21 | .delete(isAuth, linksShortenerController.deleteLink)
22 |
23 | module.exports = LinksShortenerRouter
24 |
--------------------------------------------------------------------------------
/api/routes/quotes.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const quotesController = require('../controllers/quotes')
3 | const isAuth = require('../middlewares/isAuth')
4 |
5 | const QuotesRouter = Router()
6 |
7 | QuotesRouter.route('/')
8 |
9 | // Récupère les citations
10 | .get(quotesController.getQuotes)
11 |
12 | // Proposer une citation
13 | .post(isAuth, quotesController.postQuote)
14 |
15 | module.exports = QuotesRouter
16 |
--------------------------------------------------------------------------------
/api/routes/tasks.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const tasksController = require('../controllers/tasks')
3 | const isAuth = require('../middlewares/isAuth')
4 |
5 | const TasksRouter = Router()
6 |
7 | TasksRouter.route('/')
8 |
9 | // Récupère les tâches à faire d'un user
10 | .get(isAuth, tasksController.getTasks)
11 |
12 | // Poster une nouvelle tâche à faire
13 | .post(isAuth, tasksController.postTask)
14 |
15 | TasksRouter.route('/:id')
16 |
17 | // Permet de mettre une tâche à faire en isCompleted ou !isCompleted
18 | .put(isAuth, tasksController.putTask)
19 |
20 | // Supprimer une tâche à faire
21 | .delete(isAuth, tasksController.deleteTask)
22 |
23 | module.exports = TasksRouter
24 |
--------------------------------------------------------------------------------
/api/routes/users.js:
--------------------------------------------------------------------------------
1 | const { Router } = require('express')
2 | const { body } = require('express-validator')
3 | const fileUpload = require('express-fileupload')
4 | const usersController = require('../controllers/users')
5 | const { requiredFields } = require('../assets/config/errors')
6 | const Users = require('../models/users')
7 | const isAuth = require('../middlewares/isAuth')
8 |
9 | const UsersRouter = Router()
10 |
11 | UsersRouter.route('/')
12 |
13 | // Récupère les utilisateurs
14 | .get(usersController.getUsers)
15 |
16 | // Permet de modifier son profil
17 | .put(
18 | isAuth,
19 | fileUpload({
20 | useTempFiles: true,
21 | safeFileNames: true,
22 | preserveExtension: Number,
23 | limits: { fileSize: 5 * 1024 * 1024 }, // 5mb,
24 | parseNested: true
25 | }),
26 | [
27 | body('email')
28 | .isEmail()
29 | .withMessage('Veuillez rentré une adresse mail valide.')
30 | .custom(async email => {
31 | try {
32 | const user = await Users.findOne({ where: { email } })
33 | if (user && user.email !== email) {
34 | return Promise.reject(new Error("L'adresse email existe déjà..."))
35 | }
36 | } catch (error) {
37 | return console.log(error)
38 | }
39 | return true
40 | })
41 | .normalizeEmail(),
42 | body('name')
43 | .trim()
44 | .not()
45 | .isEmpty()
46 | .withMessage('Vous devez avoir un nom (ou pseudo).')
47 | .isAlphanumeric()
48 | .withMessage(
49 | 'Votre nom ne peut contenir que des lettres ou/et des nombres.'
50 | )
51 | .isLength({ max: 30 })
52 | .withMessage('Votre nom est trop long')
53 | .custom(async name => {
54 | try {
55 | const user = await Users.findOne({ where: { name } })
56 | if (user && user.name !== name) {
57 | return Promise.reject(new Error('Le nom existe déjà...'))
58 | }
59 | } catch (error) {
60 | console.log(error)
61 | }
62 | return true
63 | }),
64 | body('isPublicEmail')
65 | .isBoolean()
66 | .withMessage(
67 | "L'adresse email peut être public ou privé, rien d'autre."
68 | ),
69 | body('biography')
70 | .trim()
71 | .escape()
72 | ],
73 | usersController.putUser
74 | )
75 |
76 | // Permet de se connecter
77 | UsersRouter.post(
78 | '/login',
79 | [
80 | body('email')
81 | .not()
82 | .isEmpty()
83 | .withMessage(requiredFields.message),
84 | body('password')
85 | .not()
86 | .isEmpty()
87 | .withMessage(requiredFields.message)
88 | ],
89 | usersController.login
90 | )
91 |
92 | // Récupère les informations public d'un profil
93 | UsersRouter.get('/:name', usersController.getUserInfo)
94 |
95 | // Permet de s'inscrire
96 | UsersRouter.post(
97 | '/register',
98 | [
99 | body('email')
100 | .isEmail()
101 | .withMessage('Veuillez rentré une adresse mail valide.')
102 | .custom(async email => {
103 | try {
104 | const user = await Users.findOne({ where: { email } })
105 | if (user) {
106 | return Promise.reject(new Error("L'adresse email existe déjà..."))
107 | }
108 | } catch (error) {
109 | return console.log(error)
110 | }
111 | return true
112 | }),
113 | body('password')
114 | .isLength({ min: 4 })
115 | .withMessage('Votre mot de passe est trop court!'),
116 | body('name')
117 | .trim()
118 | .not()
119 | .isEmpty()
120 | .withMessage('Vous devez avoir un nom (ou pseudo).')
121 | .isAlphanumeric()
122 | .withMessage(
123 | 'Votre nom ne peut contenir que des lettres ou/et des nombres.'
124 | )
125 | .isLength({ max: 30 })
126 | .withMessage('Votre nom est trop long')
127 | .custom(async name => {
128 | try {
129 | const user = await Users.findOne({ where: { name } })
130 | if (user) {
131 | return Promise.reject(new Error('Le nom existe déjà...'))
132 | }
133 | } catch (error) {
134 | console.log(error)
135 | }
136 | return true
137 | })
138 | ],
139 | usersController.register
140 | )
141 |
142 | // Confirme l'inscription
143 | UsersRouter.get('/confirm-email/:tempToken', usersController.confirmEmail)
144 |
145 | UsersRouter.route('/reset-password')
146 |
147 | // Demande une réinitialisation du mot de passe
148 | .post(
149 | [
150 | body('email')
151 | .isEmail()
152 | .withMessage('Veuillez rentré une adresse mail valide.')
153 | ],
154 | usersController.resetPassword
155 | )
156 |
157 | // Nouveau mot de passe
158 | .put(
159 | [
160 | body('password')
161 | .isLength({ min: 4 })
162 | .withMessage('Votre mot de passe est trop court!')
163 | ],
164 | usersController.newPassword
165 | )
166 |
167 | module.exports = UsersRouter
168 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.0'
2 | services:
3 | functionproject-api:
4 | build:
5 | context: './api'
6 | ports:
7 | - '8080:8080'
8 | depends_on:
9 | - 'functionproject-database'
10 | - 'functionproject-maildev'
11 | volumes:
12 | - './api:/app'
13 | - '/app/node_modules'
14 | environment:
15 | WAIT_HOSTS: 'functionproject-database:3306'
16 | container_name: 'functionproject-api'
17 |
18 | s.divlo.fr-website:
19 | build:
20 | context: './s.divlo.fr'
21 | ports:
22 | - '7000:7000'
23 | depends_on:
24 | - 'functionproject-database'
25 | volumes:
26 | - './s.divlo.fr:/app'
27 | - '/app/node_modules'
28 | environment:
29 | WAIT_HOSTS: 'functionproject-database:3306'
30 | container_name: 's.divlo.fr-website'
31 |
32 | functionproject-website:
33 | build:
34 | context: './website'
35 | ports:
36 | - '3000:3000'
37 | volumes:
38 | - './website:/app'
39 | - '/app/node_modules'
40 | container_name: 'functionproject-website'
41 |
42 | functionproject-phpmyadmin:
43 | image: 'phpmyadmin/phpmyadmin:5.1.0'
44 | environment:
45 | PMA_HOST: 'functionproject-database'
46 | PMA_USER: 'root'
47 | PMA_PASSWORD: 'password'
48 | ports:
49 | - '8000:80'
50 | depends_on:
51 | - 'functionproject-database'
52 | container_name: 'functionproject-phpmyadmin'
53 |
54 | functionproject-database:
55 | image: 'mysql:8.0.23'
56 | command: '--default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci'
57 | environment:
58 | MYSQL_ROOT_PASSWORD: 'password'
59 | MYSQL_DATABASE: 'functionproject'
60 | ports:
61 | - '3306:3306'
62 | volumes:
63 | - 'database-volume:/var/lib/mysql'
64 | container_name: 'functionproject-database'
65 |
66 | functionproject-maildev:
67 | image: 'maildev/maildev:1.1.0'
68 | ports:
69 | - '1080:80'
70 | container_name: 'functionproject-maildev'
71 |
72 | volumes:
73 | database-volume:
74 |
--------------------------------------------------------------------------------
/s.divlo.fr/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
--------------------------------------------------------------------------------
/s.divlo.fr/.env.example:
--------------------------------------------------------------------------------
1 | COMPOSE_PROJECT_NAME="s.divlo.fr-website"
2 | DATABASE_HOST="functionproject-database"
3 | DATABASE_NAME="functionproject"
4 | DATABASE_USER="root"
5 | DATABASE_PASSWORD="password"
6 | DATABASE_PORT=3306
7 |
--------------------------------------------------------------------------------
/s.divlo.fr/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 |
--------------------------------------------------------------------------------
/s.divlo.fr/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.16.1
2 |
3 | WORKDIR /app
4 |
5 | COPY ./package*.json ./
6 | RUN npm install
7 | COPY ./ ./
8 |
9 | # docker-compose-wait
10 | ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
11 | RUN chmod +x /wait
12 |
13 | CMD /wait && npm run dev
14 |
--------------------------------------------------------------------------------
/s.divlo.fr/README.md:
--------------------------------------------------------------------------------
1 | # s.divlo.fr
2 |
3 | Site web qui permet de rediriger les utilisateurs vers leurs liens raccourcis sur [function.divlo.fr](https://function.divlo.fr/).
4 |
--------------------------------------------------------------------------------
/s.divlo.fr/app.js:
--------------------------------------------------------------------------------
1 | /* Modules */
2 | require('dotenv').config()
3 | const path = require('path')
4 | const express = require('express')
5 | const helmet = require('helmet')
6 | const morgan = require('morgan')
7 | const { redirectToHTTPS } = require('express-http-to-https')
8 | const mysql = require('mysql')
9 |
10 | /* Files Imports & Variables */
11 | const app = express()
12 | const database = mysql.createPool({
13 | host: process.env.DATABASE_HOST,
14 | user: process.env.DATABASE_USER,
15 | password: process.env.DATABASE_PASSWORD,
16 | database: process.env.DATABASE_NAME,
17 | port: process.env.DATABASE_PORT
18 | })
19 |
20 | /* Middlewares */
21 | if (process.env.NODE_ENV === 'development') {
22 | app.use(morgan('dev'))
23 | } else if (process.env.NODE_ENV === 'production') {
24 | app.use(redirectToHTTPS())
25 | }
26 | app.use(helmet())
27 | app.use(express.json())
28 |
29 | /* EJS Template Engines */
30 | app.set('view engine', 'ejs')
31 | app.set('views', path.join(__dirname, 'views'))
32 |
33 | /* Routes */
34 | app.use(express.static(path.join(__dirname, 'public')))
35 |
36 | app.get('/', (_req, res) => {
37 | return res.render('index')
38 | })
39 |
40 | app.get('/:shortcut', (req, res, next) => {
41 | const { shortcut } = req.params
42 | if (shortcut == null) {
43 | return res.redirect('/errors/404')
44 | }
45 | database.query(
46 | 'SELECT * FROM short_links WHERE shortcut = ?',
47 | [shortcut],
48 | (error, [result]) => {
49 | if (error != null) {
50 | return next(error)
51 | }
52 |
53 | if (result == null) {
54 | return res.redirect('/error/404')
55 | }
56 |
57 | const count = (result.count += 1)
58 | database.query(
59 | 'UPDATE short_links SET count = ? WHERE id = ?',
60 | [count, result.id],
61 | error => {
62 | if (error != null) {
63 | return next(error)
64 | }
65 |
66 | return res.redirect(result.url)
67 | }
68 | )
69 | }
70 | )
71 | })
72 |
73 | /* Errors */
74 | app.use((_req, res) => {
75 | return res.status(404).render('errors')
76 | })
77 | app.use((error, _req, res) => {
78 | console.log(error)
79 | return res.status(500).render('errors')
80 | })
81 |
82 | /* Server */
83 | const PORT = process.env.PORT || 7000
84 | app.listen(PORT, () => {
85 | console.log('\x1b[36m%s\x1b[0m', `Started on port ${PORT}.`)
86 | })
87 |
--------------------------------------------------------------------------------
/s.divlo.fr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "short.divlo.fr",
3 | "version": "1.0.0",
4 | "description": "Link shortener for FunctionProject",
5 | "standard": {
6 | "files": [
7 | "./**/*.js"
8 | ],
9 | "envs": [
10 | "node"
11 | ]
12 | },
13 | "scripts": {
14 | "start": "node app.js",
15 | "dev": "nodemon app.js",
16 | "lint": "standard | snazzy"
17 | },
18 | "dependencies": {
19 | "dotenv": "^8.2.0",
20 | "ejs": "^3.1.6",
21 | "express": "^4.17.1",
22 | "express-http-to-https": "^1.1.4",
23 | "helmet": "^4.4.1",
24 | "morgan": "^1.10.0",
25 | "mysql": "^2.18.1"
26 | },
27 | "devDependencies": {
28 | "nodemon": "^2.0.7",
29 | "snazzy": "^9.0.0",
30 | "standard": "^16.0.3"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/s.divlo.fr/public/images/error404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/s.divlo.fr/public/images/error404.png
--------------------------------------------------------------------------------
/s.divlo.fr/public/images/linkShortener.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/s.divlo.fr/public/images/linkShortener.png
--------------------------------------------------------------------------------
/s.divlo.fr/views/errors.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Short-links
8 |
9 |
10 |
11 |
18 | Adresse url non connue
19 |
20 |
21 |
--------------------------------------------------------------------------------
/s.divlo.fr/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Short-links
8 |
9 |
10 |
11 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/website/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | dist
4 | .next
5 |
--------------------------------------------------------------------------------
/website/.env.example:
--------------------------------------------------------------------------------
1 | COMPOSE_PROJECT_NAME="function.divlo.fr-website"
2 | NEXT_PUBLIC_API_URL="http://localhost:8080"
3 | CONTAINER_API_URL="http://functionproject-api:8080"
4 |
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 | .yarn
8 |
9 | # next.js
10 | .next
11 | out
12 |
13 | # production
14 | build
15 | dist
16 |
17 | # PWA
18 | **/public/workbox-*.js
19 | **/public/sw.js
20 |
21 | # envs
22 | .env
23 | .env.production
24 |
25 | # debug
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
30 | # lockfiles
31 | package-lock.json
32 | yarn.lock
33 | pnpm-lock.yaml
34 |
35 | # editors
36 | .vscode
37 | .theia
38 | .idea
39 |
40 | # misc
41 | .DS_Store
42 | .lighthouseci
43 |
--------------------------------------------------------------------------------
/website/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14.16.1
2 |
3 | WORKDIR /app
4 |
5 | COPY ./package*.json ./
6 | RUN npm install
7 | COPY ./ ./
8 |
9 | CMD ["npm", "run", "dev"]
10 |
--------------------------------------------------------------------------------
/website/components/CodeBlock.jsx:
--------------------------------------------------------------------------------
1 | import SyntaxHighlighter from 'react-syntax-highlighter'
2 | import { atomOneDark as styles } from 'react-syntax-highlighter/dist/cjs/styles/hljs'
3 |
4 | const CodeBlock = ({ language, value }) => {
5 | return (
6 |
7 | {value}
8 |
9 | )
10 | }
11 |
12 | export default CodeBlock
13 |
--------------------------------------------------------------------------------
/website/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export default function Footer () {
4 | return (
5 | <>
6 |
18 |
19 |
32 | >
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/website/components/FunctionAdmin/EditArticleFunction.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import dynamic from 'next/dynamic'
3 | import { complex } from '../../utils/sunEditorConfig'
4 | import api from '../../utils/api'
5 | import FunctionArticle from '../FunctionPage/FunctionArticle'
6 |
7 | const SunEditor = dynamic(() => import('suneditor-react'), { ssr: false })
8 |
9 | const EditArticleFunction = props => {
10 | const [htmlContent, setHtmlContent] = useState('')
11 |
12 | const handleEditorChange = content => {
13 | setHtmlContent(content)
14 | }
15 |
16 | const handleSave = async content => {
17 | let Notyf
18 | if (typeof window !== 'undefined') {
19 | Notyf = require('notyf')
20 | }
21 | const notyf = new Notyf.Notyf({
22 | duration: 5000
23 | })
24 | try {
25 | await api.put(
26 | `/admin/functions/article/${props.functionInfo.id}`,
27 | { article: content },
28 | { headers: { Authorization: props.user.token } }
29 | )
30 | notyf.success('Sauvegardé!')
31 | } catch {
32 | notyf.error('Erreur!')
33 | }
34 | }
35 |
36 | return (
37 |
38 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default EditArticleFunction
50 |
--------------------------------------------------------------------------------
/website/components/FunctionCard/FunctionCard.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useState, forwardRef, memo } from 'react'
3 | import date from 'date-and-time'
4 | import Loader from '../Loader'
5 | import { API_URL } from '../../utils/api'
6 |
7 | const FunctionCard = memo(
8 | forwardRef((props, ref) => {
9 | const [isLoading, setIsLoading] = useState(true)
10 |
11 | const handleLoad = () => {
12 | setIsLoading(false)
13 | }
14 |
15 | const handleError = (event) => {
16 | event.target.src = API_URL + '/images/functions/default.png'
17 | }
18 |
19 | const isFormOrArticle = props.type === 'form' || props.type === 'article'
20 |
21 | return (
22 | <>
23 |
36 | {/* FunctionCard a une hauteur pendant chargement */}
37 |
44 | {isLoading && }
45 |
46 |
49 |
50 |
57 |
{props.title}
58 |
59 | {props.description}
60 |
61 |
62 |
63 |
67 | {props.categorie.name}
68 |
69 |
70 | {date.format(new Date(props.createdAt), 'DD/MM/YYYY', false)}
71 |
72 |
73 |
74 |
75 |
76 |
77 |
147 | >
148 | )
149 | })
150 | )
151 |
152 | export default FunctionCard
153 |
--------------------------------------------------------------------------------
/website/components/FunctionPage/FunctionArticle.jsx:
--------------------------------------------------------------------------------
1 | import htmlParser from 'html-react-parser'
2 |
3 | const FunctionArticle = ({ article }) => {
4 | return (
5 |
6 | {article != null
7 | ? (
8 | htmlParser(article)
9 | )
10 | : (
11 |
L'article n'est pas encore disponible.
12 | )}
13 |
14 | )
15 | }
16 |
17 | export default FunctionArticle
18 |
--------------------------------------------------------------------------------
/website/components/FunctionPage/FunctionComponentTop.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext } from 'react'
2 | import Link from 'next/link'
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4 | import { faStar } from '@fortawesome/free-solid-svg-icons'
5 | import { faStar as farStar } from '@fortawesome/free-regular-svg-icons'
6 | import date from 'date-and-time'
7 | import { UserContext } from '../../contexts/UserContext'
8 | import api, { API_URL } from '../../utils/api'
9 |
10 | const FunctionComponentTop = props => {
11 | const { isAuth, user } = useContext(UserContext)
12 | const [isFavorite, setIsFavorite] = useState(false)
13 |
14 | useEffect(() => {
15 | if (isAuth && user.token != null) {
16 | fetchFavorite()
17 | }
18 | }, [isAuth])
19 |
20 | const fetchFavorite = async () => {
21 | try {
22 | const favoriteResponse = await api.get(`/favorites/${props.id}`, {
23 | headers: { Authorization: user.token }
24 | })
25 | setIsFavorite(favoriteResponse.data.isFavorite)
26 | } catch {}
27 | }
28 |
29 | const toggleFavorite = async () => {
30 | if (isAuth && user.token != null) {
31 | try {
32 | if (isFavorite) {
33 | const response = await api.delete(`/favorites/${props.id}`, {
34 | headers: { Authorization: user.token }
35 | })
36 | if (response.status === 200) return setIsFavorite(false)
37 | }
38 | const response = await api.post(
39 | `/favorites/${props.id}`,
40 | {},
41 | { headers: { Authorization: user.token } }
42 | )
43 | if (response.status === 201) return setIsFavorite(true)
44 | } catch {}
45 | }
46 | }
47 |
48 | const handleError = event => {
49 | event.target.src = API_URL + '/images/functions/default.png'
50 | }
51 |
52 | return (
53 |
54 |
55 |
56 | {isAuth && (
57 |
67 | )}
68 |
69 |
75 |
76 | {props.title}
77 |
78 |
{props.description}
79 |
95 |
96 |
97 |
98 | )
99 | }
100 |
101 | export default FunctionComponentTop
102 |
--------------------------------------------------------------------------------
/website/components/FunctionPage/FunctionPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { API_URL } from '../../utils/api'
3 | import HeadTag from '../HeadTag'
4 | import FunctionTabsTop from './FunctionTabsTop'
5 | import FunctionComponentTop from './FunctionComponentTop'
6 |
7 | const FunctionPage = props => {
8 | const [slideIndex, setSlideIndex] = useState(0)
9 |
10 | return (
11 | <>
12 |
17 |
18 |
31 | >
32 | )
33 | }
34 |
35 | export default FunctionPage
36 |
--------------------------------------------------------------------------------
/website/components/FunctionPage/FunctionTabs.jsx:
--------------------------------------------------------------------------------
1 | import SwipeableViews from 'react-swipeable-views'
2 |
3 | const FunctionTabs = props => {
4 | return (
5 |
6 | props.setSlideIndex(index)}
8 | index={props.slideIndex}
9 | >
10 | {props.children}
11 |
12 |
13 | )
14 | }
15 |
16 | export default FunctionTabs
17 |
--------------------------------------------------------------------------------
/website/components/FunctionPage/FunctionTabsTop.jsx:
--------------------------------------------------------------------------------
1 | const FunctionTabsTop = props => {
2 | return (
3 |
25 | )
26 | }
27 |
28 | export default FunctionTabsTop
29 |
--------------------------------------------------------------------------------
/website/components/FunctionsList/FunctionsList.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback } from 'react'
2 | import { useRouter } from 'next/router'
3 | import FunctionCard from '../FunctionCard/FunctionCard'
4 | import Loader from '../Loader'
5 | import api from '../../utils/api'
6 | import useAPI from '../../hooks/useAPI'
7 |
8 | let pageFunctions = 1
9 | const FunctionsList = props => {
10 | const { categoryId } = useRouter().query
11 |
12 | // State de recherche et de catégories
13 | const [, categories] = useAPI('/categories')
14 | const [inputSearch, setInputSearch] = useState({
15 | search: '',
16 | selectedCategory: categoryId || '0'
17 | })
18 |
19 | // State pour afficher les fonctions
20 | const [functionsData, setFunctionsData] = useState({
21 | hasMore: true,
22 | rows: []
23 | })
24 | const [isLoadingFunctions, setLoadingFunctions] = useState(true)
25 |
26 | // Récupère la catégorie avec la query categoryId
27 | useEffect(() => {
28 | if (categoryId) {
29 | handleChange({ target: { name: 'selectedCategory', value: categoryId } })
30 | }
31 | }, [categoryId])
32 |
33 | // Récupère les fonctions si la catégorie/recherche change
34 | useEffect(() => {
35 | pageFunctions = 1
36 | getFunctionsData().then(data => setFunctionsData(data))
37 | }, [inputSearch])
38 |
39 | // Permet la pagination au scroll
40 | const observer = useRef()
41 | const lastFunctionCardRef = useCallback(
42 | node => {
43 | if (isLoadingFunctions) return
44 | if (observer.current) observer.current.disconnect()
45 | observer.current = new window.IntersectionObserver(
46 | entries => {
47 | if (entries[0].isIntersecting && functionsData.hasMore) {
48 | pageFunctions += 1
49 | getFunctionsData().then(data => {
50 | setFunctionsData(oldData => {
51 | return {
52 | hasMore: data.hasMore,
53 | rows: [...oldData.rows, ...data.rows]
54 | }
55 | })
56 | })
57 | }
58 | },
59 | { threshold: 1 }
60 | )
61 | if (node) observer.current.observe(node)
62 | },
63 | [isLoadingFunctions, functionsData.hasMore]
64 | )
65 |
66 | const getFunctionsData = async () => {
67 | setLoadingFunctions(true)
68 | const URL = `${
69 | props.isAdmin ? '/admin/functions' : '/functions'
70 | }?page=${pageFunctions}&limit=10&categoryId=${
71 | inputSearch.selectedCategory
72 | }&search=${inputSearch.search}`
73 | const { data } = await api.get(URL, {
74 | headers: {
75 | ...(props.isAdmin &&
76 | props.token != null && { Authorization: props.token })
77 | }
78 | })
79 | setLoadingFunctions(false)
80 | return data
81 | }
82 |
83 | const handleChange = event => {
84 | const inputSearchNew = { ...inputSearch }
85 | inputSearchNew[event.target.name] = event.target.value
86 | setInputSearch(inputSearchNew)
87 | }
88 |
89 | return (
90 |
91 |
{props.children}
92 |
93 |
94 |
100 | Toutes catégories
101 | {categories.map(category => (
102 |
108 | {category.name}
109 |
110 | ))}
111 |
112 |
121 |
122 |
123 |
124 | {functionsData.rows.map((currentFunction, index) => {
125 | // Si c'est le dernier élément
126 | if (functionsData.rows.length === index + 1) {
127 | return (
128 |
134 | )
135 | }
136 | return (
137 |
142 | )
143 | })}
144 |
145 | {isLoadingFunctions &&
}
146 |
147 | )
148 | }
149 |
150 | export default FunctionsList
151 |
--------------------------------------------------------------------------------
/website/components/HeadTag.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 |
3 | const HeadTag = (props) => {
4 | const {
5 | title = 'FunctionProject',
6 | image = '/images/FunctionProject_icon_small.png',
7 | description = "Apprenez la programmation grâce à l'apprentissage par projet alias fonction.",
8 | url = 'https://function.divlo.fr/'
9 | } = props
10 |
11 | return (
12 |
13 | {title}
14 |
15 |
16 | {/* Meta Tag */}
17 |
18 |
19 |
20 |
21 |
22 | {/* Open Graph Metadata */}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {/* Twitter card Metadata */}
32 |
33 |
34 |
35 |
36 |
37 | {/* PWA Data */}
38 |
39 |
40 |
41 |
42 |
43 | {/* Preloader script */}
44 |
45 |
46 | )
47 | }
48 |
49 | export default HeadTag
50 |
--------------------------------------------------------------------------------
/website/components/Header/Header.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useContext } from 'react'
2 | import { UserContext } from '../../contexts/UserContext'
3 | import Link from 'next/link'
4 | import { useRouter } from 'next/router'
5 | import NavigationLink from './NavigationLink'
6 |
7 | export default function Header () {
8 | const { isAuth, logoutUser, user } = useContext(UserContext)
9 | const [isActive, setIsActive] = useState(false)
10 | const { pathname } = useRouter()
11 |
12 | const toggleNavbar = () => {
13 | setIsActive(!isActive)
14 | }
15 |
16 | return (
17 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/website/components/Header/NavigationLink.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useRouter } from 'next/router'
3 |
4 | export default function NavigationLink (props) {
5 | const { pathname } = useRouter()
6 |
7 | return (
8 |
9 |
10 |
15 | {props.name}
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/website/components/Loader.jsx:
--------------------------------------------------------------------------------
1 | const Loader = ({ width, height, speed }) => (
2 |
3 |
4 |
15 |
16 |
17 | )
18 |
19 | Loader.defaultProps = {
20 | width: '100px',
21 | height: '100px',
22 | speed: '.9s'
23 | }
24 |
25 | export default Loader
26 |
--------------------------------------------------------------------------------
/website/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | const Modal = props => (
2 |
5 | )
6 |
7 | export default Modal
8 |
--------------------------------------------------------------------------------
/website/components/UserCard/UserCard.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { forwardRef, memo } from 'react'
3 | import { API_URL } from '../../utils/api'
4 |
5 | const UserCard = memo(
6 | forwardRef((props, ref) => {
7 | return (
8 |
20 | )
21 | })
22 | )
23 |
24 | export default UserCard
25 |
--------------------------------------------------------------------------------
/website/contexts/UserContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useEffect } from 'react'
2 | import Cookies from 'universal-cookie'
3 | import api from '../utils/api'
4 |
5 | const cookies = new Cookies()
6 |
7 | export const UserContext = createContext()
8 |
9 | function UserContextProvider (props) {
10 | const [user, setUser] = useState(null)
11 | const [isAuth, setIsAuth] = useState(false)
12 | const [loginLoading, setLoginLoading] = useState(false)
13 | const [messageLogin, setMessageLogin] = useState('')
14 |
15 | useEffect(() => {
16 | const newUser = cookies.get('user')
17 | if (newUser != null) {
18 | setIsAuth(true)
19 | setUser(newUser)
20 | }
21 | }, [])
22 |
23 | useEffect(() => {
24 | if (isAuth) {
25 | setMessageLogin(
26 | 'Erreur: Vous devez être déconnecter avant de vous connecter.
'
27 | )
28 | } else {
29 | setMessageLogin('')
30 | }
31 | }, [isAuth])
32 |
33 | const logoutUser = () => {
34 | cookies.remove('user', { path: '/' })
35 | setUser(null)
36 | setIsAuth(false)
37 | }
38 |
39 | const loginUser = async ({ email, password }) => {
40 | setLoginLoading(true)
41 | try {
42 | const response = await api.post('/users/login', { email, password })
43 | const newUser = response.data
44 | cookies.remove('user', { path: '/' })
45 | cookies.set('user', newUser, { path: '/', maxAge: newUser.expiresIn })
46 | setUser(newUser)
47 | setIsAuth(true)
48 | setMessageLogin(
49 | 'Succès: Connexion réussi!
'
50 | )
51 | setLoginLoading(false)
52 | } catch (error) {
53 | setMessageLogin(
54 | `Erreur: ${error.response.data.message}
`
55 | )
56 | setLoginLoading(false)
57 | setIsAuth(false)
58 | setUser(null)
59 | }
60 | }
61 |
62 | return (
63 |
74 | {props.children}
75 |
76 | )
77 | }
78 |
79 | export default UserContextProvider
80 |
--------------------------------------------------------------------------------
/website/hoc/withoutAuth.jsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react'
2 | import { UserContext } from '../contexts/UserContext'
3 | import redirect from '../utils/redirect'
4 |
5 | const withoutAuth = WrappedComponent => {
6 | const Component = props => {
7 | const { isAuth, user } = useContext(UserContext)
8 |
9 | if (isAuth) return redirect({}, `/users/${user.name}`)
10 |
11 | return
12 | }
13 |
14 | return Component
15 | }
16 |
17 | export default withoutAuth
18 |
--------------------------------------------------------------------------------
/website/hooks/useAPI.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import api from '../utils/api'
3 |
4 | /**
5 | * @param {String} url
6 | * @param {*} defaultData
7 | * @param {String} method
8 | * @param {Object} options
9 | */
10 | function useAPI (url, defaultData = [], method = 'get', options = {}) {
11 | const [isLoading, setIsLoading] = useState(true)
12 | const [data, setData] = useState(defaultData)
13 | const [hasError, setHasError] = useState(false)
14 |
15 | useEffect(() => {
16 | api[method](url, options)
17 | .then(result => {
18 | setData(result.data)
19 | setIsLoading(false)
20 | })
21 | .catch(error => {
22 | setHasError(true)
23 | console.error(error)
24 | })
25 | }, [])
26 |
27 | return [isLoading, data, hasError]
28 | }
29 |
30 | export default useAPI
31 |
--------------------------------------------------------------------------------
/website/next.config.js:
--------------------------------------------------------------------------------
1 | const withFonts = require('next-fonts')
2 | const withPWA = require('next-pwa')
3 |
4 | module.exports = withFonts(
5 | withPWA({
6 | pwa: {
7 | disable: process.env.NODE_ENV !== 'production',
8 | dest: 'public'
9 | }
10 | })
11 | )
12 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "website",
3 | "version": "2.3.0",
4 | "description": "Website frontend for FunctionProject",
5 | "main": "server.js",
6 | "standard": {
7 | "files": [
8 | "./**/*.{js,jsx}"
9 | ],
10 | "envs": [
11 | "node"
12 | ]
13 | },
14 | "scripts": {
15 | "dev:custom": "cross-env NODE_ENV=development node server",
16 | "start:custom": "cross-env NODE_ENV=production node server",
17 | "dev": "next",
18 | "start": "next start",
19 | "build": "next build",
20 | "export": "next export",
21 | "lint": "standard | snazzy"
22 | },
23 | "dependencies": {
24 | "@fortawesome/fontawesome-svg-core": "^1.2.35",
25 | "@fortawesome/free-brands-svg-icons": "^5.15.3",
26 | "@fortawesome/free-regular-svg-icons": "^5.15.3",
27 | "@fortawesome/free-solid-svg-icons": "^5.15.3",
28 | "@fortawesome/react-fontawesome": "^0.1.14",
29 | "axios": "^0.21.1",
30 | "date-and-time": "^1.0.0",
31 | "date-fns": "^2.21.1",
32 | "express": "^4.17.1",
33 | "express-http-to-https": "^1.1.4",
34 | "html-react-parser": "^1.2.5",
35 | "next": "^10.1.3",
36 | "next-fonts": "^1.5.1",
37 | "next-pwa": "^5.2.9",
38 | "notyf": "^3.9.0",
39 | "nprogress": "^0.2.0",
40 | "react": "16.13.1",
41 | "react-codepen-embed": "^1.0.2",
42 | "react-color": "^2.19.3",
43 | "react-datepicker": "^3.3.0",
44 | "react-dom": "16.13.1",
45 | "react-markdown": "^5.0.3",
46 | "react-swipeable-views": "^0.13.9",
47 | "react-swipeable-views-utils": "^0.13.9",
48 | "react-syntax-highlighter": "^15.4.3",
49 | "suneditor-react": "^2.8.0",
50 | "universal-cookie": "^4.0.4"
51 | },
52 | "devDependencies": {
53 | "cross-env": "^7.0.3",
54 | "snazzy": "^9.0.0",
55 | "standard": "^16.0.3"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/website/pages/404.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import HeadTag from '../components/HeadTag'
3 |
4 | const Error404 = () => (
5 | <>
6 |
11 |
22 | >
23 | )
24 |
25 | export default Error404
26 |
--------------------------------------------------------------------------------
/website/pages/_app.jsx:
--------------------------------------------------------------------------------
1 | /* Libraries Imports */
2 | import Router from 'next/router'
3 | import NProgress from 'nprogress'
4 |
5 | /* Components Imports */
6 | import Header from '../components/Header/Header'
7 | import Footer from '../components/Footer'
8 |
9 | /* Contexts Imports */
10 | import UserContextProvider from '../contexts/UserContext'
11 |
12 | /* CSS Imports */
13 | import 'notyf/notyf.min.css'
14 | import 'react-datepicker/dist/react-datepicker.css'
15 | import '../public/fonts/Montserrat/Montserrat.css'
16 | import '../styles/suneditor.min.css'
17 | import '../styles/normalize.css'
18 | import '../styles/grid.css'
19 | import '../styles/general.css'
20 | import '../styles/nprogress.css'
21 | import '../styles/pages/admin.css'
22 | import '../styles/pages/404.css'
23 | import '../styles/pages/index.css'
24 | import '../styles/pages/profile.css'
25 | import '../styles/pages/register-login.css'
26 | import '../styles/pages/users.css'
27 | import '../styles/pages/FunctionComponent.css'
28 | import '../styles/pages/functions/chronometerTimer.css'
29 | import '../styles/pages/functions/rightPrice.css'
30 | import '../styles/pages/functions/toDoList.css'
31 | import '../styles/components/Header.css'
32 | import '../styles/components/FunctionTabs.css'
33 | import '../styles/components/CommentCard.css'
34 | import '../styles/components/FunctionComments.css'
35 | import '../styles/components/FunctionsList.css'
36 | import '../styles/components/UserCard.css'
37 |
38 | Router.events.on('routeChangeStart', () => NProgress.start())
39 | Router.events.on('routeChangeComplete', () => NProgress.done())
40 | Router.events.on('routeChangeError', () => NProgress.done())
41 |
42 | const App = ({ Component, pageProps }) => (
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | )
51 |
52 | export default App
53 |
--------------------------------------------------------------------------------
/website/pages/_document.jsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 | import Loader from '../components/Loader'
3 |
4 | class MyDocument extends Document {
5 | static async getInitialProps (ctx) {
6 | const initialProps = await Document.getInitialProps(ctx)
7 | return { ...initialProps }
8 | }
9 |
10 | render () {
11 | return (
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 | }
27 |
28 | export default MyDocument
29 |
--------------------------------------------------------------------------------
/website/pages/about.jsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import ReactMarkdown from 'react-markdown/with-html'
3 | import HeadTag from '../components/HeadTag'
4 |
5 | const About = props => {
6 | return (
7 | <>
8 |
12 |
13 |
14 |
15 |
16 |
À-propos
17 |
24 | (README.md du{' '}
25 |
30 | GitHub
31 |
32 | )
33 |
34 |
35 |
36 |
37 |
38 |
39 | {
44 | if (uri.startsWith('./')) {
45 | return `https://github.com/Divlo/FunctionProject/blob/master/${uri.slice(
46 | 2
47 | )}`
48 | }
49 | return uri
50 | }}
51 | />
52 |
53 |
54 |
55 | >
56 | )
57 | }
58 |
59 | export async function getServerSideProps (_context) {
60 | const { data } = await axios.get(
61 | 'https://raw.githubusercontent.com/Divlo/FunctionProject/master/README.md'
62 | )
63 | return {
64 | props: { data }
65 | }
66 | }
67 |
68 | export default About
69 |
--------------------------------------------------------------------------------
/website/pages/admin/[slug].jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import Cookies from 'universal-cookie'
3 | import SwipeableViews from 'react-swipeable-views'
4 | import HeadTag from '../../components/HeadTag'
5 | import AddEditFunction from '../../components/FunctionAdmin/AddEditFunction'
6 | import EditArticleFunction from '../../components/FunctionAdmin/EditArticleFunction'
7 | import EditFormFunction from '../../components/FunctionAdmin/EditFormFunction'
8 | import redirect from '../../utils/redirect'
9 | import api, { API_URL } from '../../utils/api'
10 |
11 | const AdminFunctionComponent = props => {
12 | const [slideIndex, setSlideIndex] = useState(0)
13 |
14 | const handleDeleteFunction = async () => {
15 | await api.delete(`/admin/functions/${props.functionInfo.id}`, {
16 | headers: { Authorization: props.user.token }
17 | })
18 | redirect({}, '/admin')
19 | }
20 |
21 | return (
22 | <>
23 |
28 |
29 |
30 |
63 |
64 |
65 |
setSlideIndex(index)}
67 | index={slideIndex}
68 | >
69 |
70 |
75 |
76 |
77 | Supprimer la fonction
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | >
91 | )
92 | }
93 |
94 | export async function getServerSideProps (context) {
95 | const cookies = new Cookies(context.req.headers.cookie)
96 | const user = { ...cookies.get('user') }
97 | const { slug } = context.params
98 | if (!user.isAdmin) {
99 | return redirect(context, '/404')
100 | }
101 | return api
102 | .get(`/admin/functions/${slug}`, { headers: { Authorization: user.token } })
103 | .then(response => {
104 | return {
105 | props: {
106 | user,
107 | functionInfo: response.data
108 | }
109 | }
110 | })
111 | .catch(() => redirect(context, '/404'))
112 | }
113 |
114 | export default AdminFunctionComponent
115 |
--------------------------------------------------------------------------------
/website/pages/admin/index.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { useState } from 'react'
3 | import Cookies from 'universal-cookie'
4 | import HeadTag from '../../components/HeadTag'
5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
6 | import { faTimes } from '@fortawesome/free-solid-svg-icons'
7 | import Modal from '../../components/Modal'
8 | import FunctionsList from '../../components/FunctionsList/FunctionsList'
9 | import AddEditFunction from '../../components/FunctionAdmin/AddEditFunction'
10 | import redirect from '../../utils/redirect'
11 |
12 | const Admin = props => {
13 | const [isOpen, setIsOpen] = useState(false)
14 |
15 | const toggleModal = () => setIsOpen(!isOpen)
16 |
17 | return (
18 | <>
19 |
23 |
24 | {/* Création d'une fonction */}
25 | {isOpen
26 | ? (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
45 |
46 |
Crée une nouvelle fonction
47 |
48 |
49 |
50 |
51 |
57 |
58 |
59 |
60 | )
61 | : (
62 |
63 |
64 |
Administration
65 |
70 | Crée une nouvelle fonction
71 |
72 |
73 |
74 | Gérer les catégories
75 |
76 |
77 |
78 |
79 | Gérer les citations
80 |
81 |
82 |
83 |
84 | )}
85 | >
86 | )
87 | }
88 |
89 | export async function getServerSideProps (context) {
90 | const cookies = new Cookies(context.req.headers.cookie)
91 | const user = { ...cookies.get('user') }
92 | if (!user.isAdmin) {
93 | return redirect(context, '/404')
94 | }
95 | return {
96 | props: { user }
97 | }
98 | }
99 |
100 | export default Admin
101 |
--------------------------------------------------------------------------------
/website/pages/functions/[slug].jsx:
--------------------------------------------------------------------------------
1 | import FunctionTabs from '../../components/FunctionPage/FunctionTabs'
2 | import FunctionForm from '../../components/FunctionPage/FunctionForm'
3 | import FunctionArticle from '../../components/FunctionPage/FunctionArticle'
4 | import FunctionComments from '../../components/FunctionPage/FunctionComments/FunctionComments'
5 | import FunctionPage from '../../components/FunctionPage/FunctionPage'
6 | import redirect from '../../utils/redirect'
7 | import api from '../../utils/api'
8 |
9 | const FunctionTabManager = props => {
10 | if (props.type === 'form') {
11 | return (
12 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | return (
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | const FunctionComponent = props => (
48 |
57 | )
58 |
59 | export async function getServerSideProps (context) {
60 | const { slug } = context.params
61 | return api
62 | .get(`/functions/${slug}`)
63 | .then(response => ({ props: response.data }))
64 | .catch(() => redirect(context, '/404'))
65 | }
66 |
67 | export default FunctionComponent
68 |
--------------------------------------------------------------------------------
/website/pages/functions/chronometerTimer.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import Codepen from 'react-codepen-embed'
3 | import redirect from '../../utils/redirect'
4 | import FunctionPage from '../../components/FunctionPage/FunctionPage'
5 | import FunctionTabs from '../../components/FunctionPage/FunctionTabs'
6 | import FunctionArticle from '../../components/FunctionPage/FunctionArticle'
7 | import FunctionComments from '../../components/FunctionPage/FunctionComments/FunctionComments'
8 | import Loader from '../../components/Loader'
9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
10 | import { faPlay, faPause, faSync } from '@fortawesome/free-solid-svg-icons'
11 | import api from '../../utils/api'
12 |
13 | let interval
14 | function convertSeconds (seconds) {
15 | return {
16 | minutes: Math.floor(seconds / 60),
17 | seconds: seconds % 60
18 | }
19 | }
20 |
21 | const Chronometer = () => {
22 | const [timeLength, setTimeLength] = useState(0) // seconds
23 | const [isPlaying, setIsPlaying] = useState(false)
24 |
25 | const handlePlayPause = () => {
26 | if (isPlaying) {
27 | clearInterval(interval)
28 | } else {
29 | if (interval) clearInterval(interval)
30 | interval = setInterval(() => {
31 | setTimeLength(time => time + 1)
32 | }, 1000)
33 | }
34 | setIsPlaying(!isPlaying)
35 | }
36 |
37 | const handleReset = () => {
38 | if (interval) clearInterval(interval)
39 | setIsPlaying(false)
40 | setTimeLength(0)
41 | }
42 |
43 | const getFormattedValue = () => {
44 | const minutesAndSeconds = convertSeconds(timeLength)
45 | const minutes =
46 | minutesAndSeconds.minutes < 100
47 | ? ('0' + minutesAndSeconds.minutes).slice(-2)
48 | : minutesAndSeconds.minutes
49 | const seconds = ('0' + minutesAndSeconds.seconds).slice(-2)
50 | return `${minutes}:${seconds}`
51 | }
52 |
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {getFormattedValue()}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | )
87 | }
88 |
89 | const Pomodoro = () => {
90 | return (
91 |
92 | }
99 | />
100 |
101 | )
102 | }
103 |
104 | const FunctionTabManager = props => {
105 | return (
106 |
110 |
111 |
112 |
113 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | )
124 | }
125 |
126 | const chronometerTimer = props => (
127 |
137 | )
138 |
139 | export async function getServerSideProps (context) {
140 | return api
141 | .get('/functions/chronometerTimer')
142 | .then(response => ({ props: response.data }))
143 | .catch(() => redirect(context, '/404'))
144 | }
145 |
146 | export default chronometerTimer
147 |
--------------------------------------------------------------------------------
/website/pages/functions/index.jsx:
--------------------------------------------------------------------------------
1 | import HeadTag from '../../components/HeadTag'
2 | import FunctionsList from '../../components/FunctionsList/FunctionsList'
3 |
4 | const Functions = () => {
5 | return (
6 | <>
7 |
12 |
13 |
14 | Fonctions
15 |
16 | >
17 | )
18 | }
19 |
20 | export default Functions
21 |
--------------------------------------------------------------------------------
/website/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import SwipeableViews from 'react-swipeable-views'
3 | import { autoPlay } from 'react-swipeable-views-utils'
4 | import Link from 'next/link'
5 | import HeadTag from '../components/HeadTag'
6 | import Loader from '../components/Loader'
7 |
8 | const AutoPlaySwipeableViews = autoPlay(SwipeableViews)
9 |
10 | const Home = () => {
11 | useEffect(() => {
12 | console.log(
13 | '%c ⚙️ FunctionProject',
14 | 'color: #ffd800; font-weight: bold; background-color: #181818;padding: 10px;border-radius: 10px;font-size: 20px'
15 | )
16 | }, [])
17 |
18 | return (
19 | <>
20 |
21 |
22 |
23 | {/* Slide 1 */}
24 |
25 |
26 |
FunctionProject
27 |
28 | Apprenez la programmation grâce à l'apprentissage par projet
29 | alias fonction (en JavaScript
30 | ).
31 |
32 |
33 | En savoir plus ? (à-propos)
34 |
35 |
36 | Découvrez la liste des fonctions disponibles :
37 |
38 |
39 |
46 |
47 |
48 | {/* Slide 2 */}
49 |
50 |
51 |
Code Source
52 |
53 | Le partage est essentiel afin de progresser.
54 | Par conséquent chaque fonction a un article expliquant comment
55 | elle fonctionne et
56 | le code source du projet est disponible sur mon profil GitHub :
57 |
58 |
59 |
72 |
73 |
74 |
75 | >
76 | )
77 | }
78 |
79 | export default Home
80 |
--------------------------------------------------------------------------------
/website/pages/users/forgotPassword.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import htmlParser from 'html-react-parser'
3 | import Loader from '../../components/Loader'
4 | import HeadTag from '../../components/HeadTag'
5 | import api from '../../utils/api'
6 | import withoutAuth from '../../hoc/withoutAuth'
7 |
8 | const forgotPassword = () => {
9 | const [inputState, setInputState] = useState({})
10 | const [message, setMessage] = useState('')
11 | const [isLoading, setIsLoading] = useState(false)
12 |
13 | const handleChange = (event) => {
14 | const inputStateNew = { ...inputState }
15 | inputStateNew[event.target.name] = event.target.value
16 | setInputState(inputStateNew)
17 | }
18 |
19 | const handleSubmit = (event) => {
20 | setIsLoading(true)
21 | event.preventDefault()
22 | api.post('/users/reset-password', inputState)
23 | .then(({ data }) => {
24 | setMessage(`Succès: ${data.result}
`)
25 | setIsLoading(false)
26 | setInputState({})
27 | })
28 | .catch((error) => {
29 | setMessage(`Erreur: ${error.response.data.message}
`)
30 | setIsLoading(false)
31 | })
32 | }
33 |
34 | return (
35 | <>
36 |
40 |
41 |
42 |
43 |
Mot de passe oublié ?
44 |
45 |
Demandez une demande de réinitialisation de mot de passe par email.
46 |
47 |
56 |
57 | {
58 | (isLoading)
59 | ?
60 | : htmlParser(message)
61 | }
62 |
63 |
64 |
65 |
66 | >
67 | )
68 | }
69 |
70 | export default withoutAuth(forgotPassword)
71 |
--------------------------------------------------------------------------------
/website/pages/users/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback } from 'react'
2 | import HeadTag from '../../components/HeadTag'
3 | import Loader from '../../components/Loader'
4 | import UserCard from '../../components/UserCard/UserCard'
5 | import api from '../../utils/api'
6 |
7 | const Users = () => {
8 | let pageUsers = 1
9 |
10 | const [inputSearch, setInputSearch] = useState('')
11 | const [usersData, setUsersData] = useState({
12 | totalItems: 0,
13 | hasMore: true,
14 | rows: []
15 | })
16 | const [isLoadingUsers, setLoadingUsers] = useState(true)
17 |
18 | // Récupère les users si la recherche change
19 | useEffect(() => {
20 | pageUsers = 1
21 | getUsersData().then(data => setUsersData(data))
22 | }, [inputSearch])
23 |
24 | const getUsersData = async () => {
25 | setLoadingUsers(true)
26 | const { data } = await api.get(
27 | `/users?page=${pageUsers}&limit=15&search=${inputSearch}`
28 | )
29 | setLoadingUsers(false)
30 | return data
31 | }
32 |
33 | const handleSearchChange = event => {
34 | setInputSearch(event.target.value)
35 | }
36 |
37 | // Permet la pagination au scroll
38 | const observer = useRef()
39 | const lastUserCardRef = useCallback(
40 | node => {
41 | if (isLoadingUsers) return
42 | if (observer.current) observer.current.disconnect()
43 | observer.current = new window.IntersectionObserver(
44 | entries => {
45 | if (entries[0].isIntersecting && usersData.hasMore) {
46 | pageUsers += 1
47 | getUsersData().then(data => {
48 | setUsersData(oldData => {
49 | return {
50 | totalItems: data.totalItems,
51 | hasMore: data.hasMore,
52 | rows: [...oldData.rows, ...data.rows]
53 | }
54 | })
55 | })
56 | }
57 | },
58 | { threshold: 1 }
59 | )
60 | if (node) observer.current.observe(node)
61 | },
62 | [isLoadingUsers, usersData.hasMore]
63 | )
64 |
65 | return (
66 | <>
67 |
68 |
69 |
70 |
71 |
72 |
73 | Utilisateurs
74 |
75 |
76 | La liste des utilisateurs - Total de {usersData.totalItems}{' '}
77 | utilisateurs :
78 |
79 |
80 |
81 |
82 |
83 |
92 |
93 |
94 |
95 | {usersData.rows.map((user, index) => {
96 | // Si c'est le dernier élément
97 | if (usersData.rows.length === index + 1) {
98 | return (
99 |
100 | )
101 | }
102 | return (
103 |
104 | )
105 | })}
106 |
107 |
108 | {isLoadingUsers &&
}
109 |
110 | >
111 | )
112 | }
113 |
114 | export default Users
115 |
--------------------------------------------------------------------------------
/website/pages/users/login.jsx:
--------------------------------------------------------------------------------
1 | import { useContext, useState } from 'react'
2 | import { useRouter } from 'next/router'
3 | import Link from 'next/link'
4 | import htmlParser from 'html-react-parser'
5 | import Loader from '../../components/Loader'
6 | import HeadTag from '../../components/HeadTag'
7 | import { UserContext } from '../../contexts/UserContext'
8 | import withoutAuth from '../../hoc/withoutAuth'
9 |
10 | const Login = () => {
11 | const router = useRouter()
12 | const [inputState, setInputState] = useState({})
13 | const { loginUser, messageLogin, loginLoading, isAuth } = useContext(UserContext)
14 |
15 | const handleChange = (event) => {
16 | const inputStateNew = { ...inputState }
17 | inputStateNew[event.target.name] = event.target.value
18 | setInputState(inputStateNew)
19 | }
20 |
21 | const handleSubmit = async (event) => {
22 | event.preventDefault()
23 | if (!isAuth) {
24 | await loginUser(inputState)
25 | }
26 | }
27 |
28 | return (
29 | <>
30 |
34 |
35 |
36 |
37 |
Se connecter
38 |
58 |
59 | {(router.query.isConfirmed !== undefined && messageLogin === '') &&
Succès: Votre compte a bien été confirmé, vous pouvez maintenant vous connectez!
}
60 | {(router.query.isSuccessEdit !== undefined && messageLogin === '') &&
Succès: Votre profil a bien été modifié, vous pouvez maintenant vous connectez!
}
61 | {
62 | (loginLoading)
63 | ?
64 | : htmlParser(messageLogin)
65 | }
66 |
67 |
68 |
69 |
70 | >
71 | )
72 | }
73 |
74 | export default withoutAuth(Login)
75 |
--------------------------------------------------------------------------------
/website/pages/users/newPassword.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import htmlParser from 'html-react-parser'
3 | import Loader from '../../components/Loader'
4 | import HeadTag from '../../components/HeadTag'
5 | import api from '../../utils/api'
6 | import redirect from '../../utils/redirect'
7 | import withoutAuth from '../../hoc/withoutAuth'
8 |
9 | const newPassword = (props) => {
10 | const [inputState, setInputState] = useState({})
11 | const [message, setMessage] = useState('')
12 | const [isLoading, setIsLoading] = useState(false)
13 |
14 | const handleChange = (event) => {
15 | const inputStateNew = { ...inputState }
16 | inputStateNew[event.target.name] = event.target.value
17 | setInputState(inputStateNew)
18 | }
19 |
20 | const handleSubmit = (event) => {
21 | setIsLoading(true)
22 | event.preventDefault()
23 | api.put('/users/reset-password', { ...inputState, tempToken: props.token })
24 | .then(({ data }) => {
25 | setMessage(`Succès: ${data.result}
`)
26 | setIsLoading(false)
27 | setInputState({})
28 | })
29 | .catch((error) => {
30 | setMessage(`Erreur: ${error.response.data.message}
`)
31 | setIsLoading(false)
32 | })
33 | }
34 |
35 | return (
36 | <>
37 |
41 |
42 |
43 |
44 |
Nouveau mot de passe
45 |
54 |
55 | {
56 | (isLoading)
57 | ?
58 | : htmlParser(message)
59 | }
60 |
61 |
62 |
63 |
64 | >
65 | )
66 | }
67 |
68 | export async function getServerSideProps (context) {
69 | if (context.query.token != null) {
70 | return {
71 | props: {
72 | token: context.query.token
73 | }
74 | }
75 | }
76 | return redirect(context, '/404')
77 | }
78 |
79 | export default withoutAuth(newPassword)
80 |
--------------------------------------------------------------------------------
/website/pages/users/register.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import htmlParser from 'html-react-parser'
3 | import Loader from '../../components/Loader'
4 | import HeadTag from '../../components/HeadTag'
5 | import api from '../../utils/api'
6 | import withoutAuth from '../../hoc/withoutAuth'
7 |
8 | const Register = () => {
9 | const [inputState, setInputState] = useState({ name: '', email: '', password: '' })
10 | const [message, setMessage] = useState('')
11 | const [isLoading, setIsLoading] = useState(false)
12 |
13 | const handleChange = (event) => {
14 | const inputStateNew = { ...inputState }
15 | inputStateNew[event.target.name] = event.target.value
16 | setInputState(inputStateNew)
17 | }
18 |
19 | const handleSubmit = (event) => {
20 | setIsLoading(true)
21 | event.preventDefault()
22 | api.post('/users/register', inputState)
23 | .then(({ data }) => {
24 | setInputState({ name: '', email: '', password: '' })
25 | setMessage(`Succès: ${data.result}
`)
26 | setIsLoading(false)
27 | })
28 | .catch((error) => {
29 | setMessage(`Erreur: ${error.response.data.message}
`)
30 | setIsLoading(false)
31 | })
32 | }
33 |
34 | return (
35 | <>
36 |
40 |
41 |
42 |
43 |
S'inscrire
44 |
45 |
En vous inscrivant, vous accéderez à de nombreuses fonctionnalités : publier des commentaires, ajouter des fonctions aux favoris, utiliser certaines fonctions disponibles qu'aux membres (exemple: La To Do list) etc.
46 |
47 |
67 |
68 | {
69 | (isLoading)
70 | ?
71 | : htmlParser(message)
72 | }
73 |
74 |
75 |
76 |
77 | >
78 | )
79 | }
80 | export default withoutAuth(Register)
81 |
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-100.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-100.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-100.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-100.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-100italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-100italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-100italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-100italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-200.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-200.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-200.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-200.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-200italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-200italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-200italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-200italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-300.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-300.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-300.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-300italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-300italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-300italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-300italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-400.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-400.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-400italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-400italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-400italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-400italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-500.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-500.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-500italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-500italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-500italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-500italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-600.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-600.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-600.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-600.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-600italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-600italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-600italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-600italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-700.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-700.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-700italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-700italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-700italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-700italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-800.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-800.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-800.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-800.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-800italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-800italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-800italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-800italic.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-900.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-900.woff2
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-900italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-900italic.woff
--------------------------------------------------------------------------------
/website/public/fonts/Montserrat/files/montserrat-latin-900italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/fonts/Montserrat/files/montserrat-latin-900italic.woff2
--------------------------------------------------------------------------------
/website/public/images/FunctionProject_brand-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/FunctionProject_brand-logo.png
--------------------------------------------------------------------------------
/website/public/images/FunctionProject_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/FunctionProject_icon.png
--------------------------------------------------------------------------------
/website/public/images/FunctionProject_icon_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/FunctionProject_icon_small.png
--------------------------------------------------------------------------------
/website/public/images/GitHub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/GitHub.png
--------------------------------------------------------------------------------
/website/public/images/error404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/error404.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-128x128.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-144x144.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-152x152.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-192x192.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-384x384.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-512x512.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-72x72.png
--------------------------------------------------------------------------------
/website/public/images/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theoludwig/FunctionProject/36f41da7262e0f3374c7cd6c3bdfd92966fffdc5/website/public/images/icons/icon-96x96.png
--------------------------------------------------------------------------------
/website/public/js/extraHeightCSS.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | reactSwipeableExtraHeight = document.querySelector(
4 | '.react-swipeable-view-container'
5 | )
6 | reactSwipeableExtraHeight.parentElement.style =
7 | 'height: 400px; overflow-x: hidden;'
8 |
--------------------------------------------------------------------------------
/website/public/js/preloader.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('load', () => {
2 | document.querySelector('.isLoading').classList.remove('isLoading')
3 | document.getElementById('preloader').remove()
4 | })
5 |
--------------------------------------------------------------------------------
/website/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "FunctionProject",
3 | "short_name": "FunctionProject",
4 | "icons": [
5 | {
6 | "src": "images/icons/icon-72x72.png",
7 | "sizes": "72x72",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "images/icons/icon-96x96.png",
12 | "sizes": "96x96",
13 | "type": "image/png"
14 | },
15 | {
16 | "src": "images/icons/icon-128x128.png",
17 | "sizes": "128x128",
18 | "type": "image/png"
19 | },
20 | {
21 | "src": "images/icons/icon-144x144.png",
22 | "sizes": "144x144",
23 | "type": "image/png"
24 | },
25 | {
26 | "src": "images/icons/icon-152x152.png",
27 | "sizes": "152x152",
28 | "type": "image/png"
29 | },
30 | {
31 | "src": "images/icons/icon-192x192.png",
32 | "sizes": "192x192",
33 | "type": "image/png",
34 | "purpose": "maskable"
35 | },
36 | {
37 | "src": "images/icons/icon-384x384.png",
38 | "sizes": "384x384",
39 | "type": "image/png"
40 | },
41 | {
42 | "src": "images/icons/icon-512x512.png",
43 | "sizes": "512x512",
44 | "type": "image/png"
45 | }
46 | ],
47 | "theme_color": "#ffd800",
48 | "background_color": "#181818",
49 | "start_url": "/",
50 | "display": "standalone"
51 | }
52 |
--------------------------------------------------------------------------------
/website/server.js:
--------------------------------------------------------------------------------
1 | /* Modules */
2 | const next = require('next')
3 | const express = require('express')
4 | const redirectToHTTPS = require('express-http-to-https').redirectToHTTPS
5 |
6 | /* Variables */
7 | const PORT = process.env.PORT || 3000
8 | const dev = process.env.NODE_ENV !== 'production'
9 | const app = next({ dev })
10 | const handle = app.getRequestHandler()
11 |
12 | app.prepare().then(() => {
13 | const server = express()
14 |
15 | /* Middlewares */
16 | server.use(redirectToHTTPS([/localhost:(\d{4})/]))
17 |
18 | /* Routes */
19 | server.all('*', (req, res) => {
20 | return handle(req, res)
21 | })
22 |
23 | /* Server */
24 | server.listen(PORT, error => {
25 | if (error) throw error
26 | console.log(`> Ready on http://localhost:${PORT}`)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/website/styles/components/CommentCard.css:
--------------------------------------------------------------------------------
1 | .CommentCard {
2 | display: flex;
3 | flex-direction: column;
4 | word-wrap: break-word;
5 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
6 | border: 1px solid black;
7 | border-radius: 0.7em;
8 | margin: 15px 0 15px 0;
9 | }
10 | .CommentCard__container {
11 | height: 100%;
12 | width: 100%;
13 | display: flex;
14 | flex-direction: column;
15 | padding: 20px;
16 | }
17 | .CommentCard__user-logo {
18 | border-radius: 50%;
19 | object-fit: cover;
20 | width: 50px;
21 | height: 50px;
22 | cursor: pointer;
23 | }
24 | .CommentCard__message-info {
25 | display: flex;
26 | align-items: center;
27 | margin-left: 10px;
28 | font-size: 16px;
29 | }
30 | .CommentCard__message {
31 | line-height: 1.8;
32 | margin: 15px 0 0 0;
33 | }
34 |
--------------------------------------------------------------------------------
/website/styles/components/FunctionComments.css:
--------------------------------------------------------------------------------
1 | .FunctionComments__row {
2 | margin-bottom: 20px;
3 | }
4 | .FunctionComments__textarea {
5 | height: auto;
6 | resize: vertical;
7 | }
8 |
--------------------------------------------------------------------------------
/website/styles/components/FunctionTabs.css:
--------------------------------------------------------------------------------
1 | .FunctionTabs__nav {
2 | display: flex;
3 | flex-wrap: wrap;
4 | padding-left: 0;
5 | margin-bottom: 0;
6 | list-style: none;
7 | border-bottom: 1px solid #d9e2ef;
8 | margin-bottom: -1px;
9 | margin-top: 30px;
10 | }
11 | .FunctionTabs__nav-item {
12 | margin-bottom: -1px;
13 | cursor: pointer;
14 | }
15 | .FunctionTabs__nav-link {
16 | color: var(--text-color);
17 | border: 1px solid #0c0b0b38;
18 | border-bottom: 0px;
19 | border-top-left-radius: 0.375rem;
20 | border-top-right-radius: 0.375rem;
21 | display: block;
22 | padding: 0.5rem 1rem;
23 | transition: 0.2s;
24 | }
25 | .FunctionTabs__nav-link-active {
26 | border-color: #d9e2ef #d9e2ef #fff;
27 | color: var(--important);
28 | }
29 | .FunctionTabs__nav-link:hover {
30 | border-color: #f1f4f8 #f1f4f8 #d9e2ef;
31 | text-decoration: none;
32 | }
33 |
34 | @media (max-width: 490px) {
35 | .FunctionTabs__nav {
36 | flex-direction: column;
37 | }
38 | .FunctionTabs__nav-link {
39 | border-color: #f1f4f8 #f1f4f8 #d9e2ef;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/website/styles/components/FunctionsList.css:
--------------------------------------------------------------------------------
1 | .Functions__title {
2 | padding: 20px 0 20px 0;
3 | margin-bottom: 0;
4 | }
5 | .Functions__form-control {
6 | display: block;
7 | height: calc(1.5em + 0.75rem + 2px);
8 | padding: 0.375rem 0.75rem;
9 | font-size: 1rem;
10 | font-weight: 400;
11 | line-height: 1.5;
12 | color: #495057;
13 | background-color: #fff;
14 | background-clip: padding-box;
15 | border: 1px solid #ced4da;
16 | border-radius: 0.5em;
17 | }
18 | .Functions__search-container {
19 | margin-bottom: 50px;
20 | }
21 | .Functions__select-option {
22 | color: rgb(221, 220, 220);
23 | }
24 | .Functions__search-input {
25 | width: 40%;
26 | }
27 | /* col-sm */
28 | @media (max-width: 576px) {
29 | .Functions__search-container {
30 | flex-direction: column;
31 | align-items: center;
32 | }
33 | .Functions__select {
34 | width: 90%;
35 | margin-bottom: 5px;
36 | }
37 | .Functions__search-input {
38 | width: 90%;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/website/styles/components/Header.css:
--------------------------------------------------------------------------------
1 | /* HEADER */
2 | .Header {
3 | position: fixed;
4 | width: 100%;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | z-index: 100;
9 |
10 | display: flex;
11 | flex-flow: row wrap;
12 | align-items: center;
13 | justify-content: space-between;
14 | padding: 0.5rem 1rem;
15 |
16 | border-bottom: var(--border-header-footer);
17 | background-color: var(--background-color);
18 | }
19 | @media (min-width: 992px) {
20 | .Header {
21 | flex-flow: row nowrap;
22 | justify-content: flex-start;
23 | }
24 | }
25 | .Header > .container {
26 | display: flex;
27 | flex-wrap: wrap;
28 | align-items: center;
29 | justify-content: space-between;
30 | }
31 | @media (min-width: 992px) {
32 | .Header > .container {
33 | flex-wrap: nowrap;
34 | }
35 | }
36 | /* Brand */
37 | .Header__brand-link {
38 | display: inline-block;
39 | padding-top: 0.3125rem;
40 | padding-bottom: 0.3125rem;
41 | margin-right: 1rem;
42 | font-size: 1.25rem;
43 | line-height: inherit;
44 | white-space: nowrap;
45 | }
46 | #brand-link__logo-small-screen {
47 | display: none;
48 | }
49 | @media (max-width: 496px) {
50 | #brand-link__logo {
51 | display: none;
52 | }
53 | .Header__brand-link {
54 | width: 30%;
55 | }
56 | #brand-link__logo-small-screen {
57 | display: inline-block;
58 | }
59 | }
60 |
61 | @media (min-width: 992px) {
62 | .Header .Header__navbar {
63 | display: flex;
64 | flex-basis: auto;
65 | }
66 | }
67 | .Header__navbar {
68 | flex-basis: 100%;
69 | flex-grow: 1;
70 | align-items: center;
71 | }
72 | .navbar__list {
73 | display: flex;
74 | flex-direction: row;
75 | margin-left: auto;
76 | }
77 | .navbar__list.navbar__list-active {
78 | margin: 0 !important;
79 | display: flex;
80 | }
81 | @media (max-width: 992px) {
82 | .navbar__list {
83 | display: none;
84 | flex-direction: column;
85 | align-items: center;
86 | padding-left: 0;
87 | list-style: none;
88 | }
89 | }
90 | .navbar-link {
91 | display: block;
92 | padding: 0.5rem 1rem;
93 | }
94 |
95 | /* Details Styling */
96 | .navbar-link:hover {
97 | text-decoration: none;
98 | color: rgba(255, 255, 255, 0.75);
99 | }
100 | .navbar-link,
101 | .navbar-link-active {
102 | color: rgba(255, 255, 255, 0.5);
103 | }
104 | .navbar-link-active,
105 | .navbar-link-active:hover,
106 | .Header__brand-link {
107 | color: var(--text-color);
108 | }
109 | .navbar-item {
110 | list-style: none;
111 | }
112 | .navbar-link {
113 | font-size: 16px;
114 | padding: 0.5rem;
115 | }
116 |
117 | /* Hamburger Icon */
118 | .Header__hamburger {
119 | display: none;
120 | width: 56px;
121 | height: 40px;
122 | cursor: pointer;
123 | background-color: transparent;
124 | border: 1px solid rgba(255, 255, 255, 0.1);
125 | border-radius: 0.25rem;
126 | position: relative;
127 | }
128 | .Header__hamburger > span,
129 | .Header__hamburger > span::before,
130 | .Header__hamburger > span::after {
131 | position: absolute;
132 | width: 22px;
133 | height: 1.3px;
134 | background-color: rgba(255, 255, 255);
135 | }
136 | .Header__hamburger > span {
137 | top: 50%;
138 | left: 50%;
139 | transform: translate(-50%, -50%);
140 | transition: background-color 0.3s ease-in-out;
141 | }
142 | .Header__hamburger > span::before,
143 | .Header__hamburger > span::after {
144 | content: '';
145 | transition: transform 0.3s ease-in-out;
146 | }
147 | .Header__hamburger > span::before {
148 | transform: translateY(-8px);
149 | }
150 | .Header__hamburger > span::after {
151 | transform: translateY(8px);
152 | }
153 | .Header__hamburger-active span {
154 | background-color: transparent;
155 | }
156 | .Header__hamburger-active > span::before {
157 | transform: translateY(0px) rotateZ(45deg);
158 | }
159 | .Header__hamburger-active > span::after {
160 | transform: translateY(0px) rotateZ(-45deg);
161 | }
162 | /* Apparition du hamburger */
163 | @media (max-width: 992px) {
164 | .Header__hamburger {
165 | display: flex;
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/website/styles/components/UserCard.css:
--------------------------------------------------------------------------------
1 | .UserCard {
2 | display: flex;
3 | align-items: center;
4 | position: relative;
5 | flex-direction: column;
6 | word-wrap: break-word;
7 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
8 | border: 1px solid black;
9 | border-radius: 1rem;
10 | margin: 0 0 50px 0;
11 | cursor: pointer;
12 | transition: all 0.3s;
13 | padding: 10px;
14 | }
15 | .UserCard__container {
16 | height: 100%;
17 | width: 100%;
18 | display: flex;
19 | flex-direction: column;
20 | align-items: center;
21 | color: var(--text-color);
22 | text-decoration: none !important;
23 | }
24 | .UserCard:hover {
25 | transform: translateY(-7px);
26 | }
27 | /* col-md */
28 | @media (min-width: 768px) {
29 | .UserCard {
30 | margin: 0 30px 50px 30px;
31 | }
32 | }
33 | /* col-xl */
34 | @media (min-width: 1200px) {
35 | .UserCard {
36 | margin: 0 20px 50px 20px;
37 | }
38 | }
39 | .UserCard__logo {
40 | width: 150px;
41 | height: 150px;
42 | border-radius: 50%;
43 | object-fit: cover;
44 | }
45 | .UserCard__name {
46 | margin: 30px 0 10px 0;
47 | }
48 |
--------------------------------------------------------------------------------
/website/styles/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: var(--important);
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px var(--important), 0 0 5px var(--important);
26 | opacity: 1;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 15px;
39 | right: 15px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: var(--important);
49 | border-left-color: var(--important);
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% {
68 | -webkit-transform: rotate(0deg);
69 | }
70 | 100% {
71 | -webkit-transform: rotate(360deg);
72 | }
73 | }
74 | @keyframes nprogress-spinner {
75 | 0% {
76 | transform: rotate(0deg);
77 | }
78 | 100% {
79 | transform: rotate(360deg);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/website/styles/pages/404.css:
--------------------------------------------------------------------------------
1 | .Error404__container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | min-width: 100%;
7 | }
8 |
--------------------------------------------------------------------------------
/website/styles/pages/FunctionComponent.css:
--------------------------------------------------------------------------------
1 | .FunctionComponent__top {
2 | display: flex;
3 | align-items: center;
4 | position: relative;
5 | flex-direction: column;
6 | word-wrap: break-word;
7 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
8 | border: 1px solid black;
9 | border-radius: 1rem;
10 | margin-top: 40px;
11 | }
12 | .FunctionComponent__title {
13 | margin: 0;
14 | }
15 | .FunctionComponent__description {
16 | word-break: break-all;
17 | margin-bottom: 0;
18 | }
19 | .FunctionComponent__slide {
20 | margin-top: 30px;
21 | }
22 | .FunctionComponent__image {
23 | width: 150px;
24 | }
25 | .FunctionComponent__star-favorite {
26 | color: var(--important);
27 | width: 2em !important;
28 | height: 2em !important;
29 | position: absolute;
30 | right: 0;
31 | top: 15px;
32 | margin-right: 15px;
33 | cursor: pointer;
34 | }
35 |
--------------------------------------------------------------------------------
/website/styles/pages/admin.css:
--------------------------------------------------------------------------------
1 | .Admin__Modal__container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | margin: 30px 0 0 0;
6 | }
7 | .Admin__Modal__row {
8 | display: flex;
9 | align-items: center;
10 | flex-direction: column;
11 | word-wrap: break-word;
12 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
13 | border: 1px solid black;
14 | border-radius: 1rem;
15 | margin-bottom: 50px;
16 | }
17 | .Admin__Modal-top-container {
18 | margin: 20px 0;
19 | }
20 | .Admin__Modal-select-option {
21 | color: rgb(221, 220, 220);
22 | }
23 | .Admin__Function-slide {
24 | margin-top: 40px;
25 | }
26 | .Admin__Input-group {
27 | display: flex;
28 | flex-direction: column;
29 | word-wrap: break-word;
30 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
31 | border: 1px solid black;
32 | border-radius: 1rem;
33 | margin-top: 40px;
34 | padding: 40px;
35 | }
36 |
--------------------------------------------------------------------------------
/website/styles/pages/functions/chronometerTimer.css:
--------------------------------------------------------------------------------
1 | .Chronometer__container {
2 | display: flex;
3 | flex-flow: row wrap;
4 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
5 | border: 1px solid black;
6 | border-radius: 1rem;
7 | padding: 20px;
8 | margin-bottom: 40px;
9 | }
10 | .Chronometer__item {
11 | width: 100%;
12 | }
13 | .Chronomter__row {
14 | margin: 8px 0 16px 0;
15 | text-align: center;
16 | }
17 | .Chronometer__time-left {
18 | font-size: 3rem;
19 | font-weight: 700;
20 | }
21 | .Chronometer__buttons {
22 | border-top: solid 3px var(--important);
23 | padding-top: 12px;
24 | }
25 | .Chronometer__row-button {
26 | display: flex;
27 | justify-content: center;
28 | }
29 | .Chronometer-btn {
30 | color: var(--text-color);
31 | cursor: pointer;
32 | background-color: transparent;
33 | border: none;
34 | outline: none;
35 | margin-left: 14px;
36 | margin-right: 14px;
37 | width: 2em;
38 | }
39 |
--------------------------------------------------------------------------------
/website/styles/pages/functions/rightPrice.css:
--------------------------------------------------------------------------------
1 | .Product__image {
2 | max-width: 150px;
3 | }
4 | .Price__result {
5 | height: 50px;
6 | margin: 5px 0 10px 0;
7 | display: flex;
8 | align-items: center;
9 | padding-left: 10px;
10 | color: #fff;
11 | }
12 | .Price__result-success {
13 | background-color: #28a745;
14 | }
15 | .Price__result-plus {
16 | background-color: #dc3545;
17 | }
18 | .Price__result-moins {
19 | background-color: #343a40;
20 | }
21 |
--------------------------------------------------------------------------------
/website/styles/pages/functions/toDoList.css:
--------------------------------------------------------------------------------
1 | .ManageToDo__container {
2 | display: flex;
3 | flex-direction: column;
4 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
5 | border: 1px solid black;
6 | border-radius: 1rem;
7 | margin: 40px 40px;
8 | }
9 | .ManageToDo__list {
10 | overflow: hidden;
11 | }
12 | .ManageToDo__list-item {
13 | display: flex;
14 | justify-content: space-between;
15 | margin: 25px 0;
16 | }
17 | .ManageToDo__list-item.isCompleted {
18 | text-decoration: line-through;
19 | }
20 | .ManageToDo__task-btn {
21 | color: var(--text-color);
22 | cursor: pointer;
23 | background-color: transparent;
24 | border: none;
25 | outline: none;
26 | margin-left: 7px;
27 | margin-right: 7px;
28 | width: 1.75em;
29 | }
30 | .ManageToDo__list-item-span {
31 | width: calc(100% - 120px);
32 | overflow: hidden;
33 | text-overflow: ellipsis;
34 | }
35 |
--------------------------------------------------------------------------------
/website/styles/pages/index.css:
--------------------------------------------------------------------------------
1 | .Home__container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | }
7 | .Home__logo-spin {
8 | cursor: pointer;
9 | }
10 | .Home__image-width {
11 | width: 13em;
12 | }
13 | div[aria-hidden='false'] {
14 | overflow: hidden !important;
15 | }
16 |
--------------------------------------------------------------------------------
/website/styles/pages/profile.css:
--------------------------------------------------------------------------------
1 | .Profile__container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | margin: 30px 0 0 0;
6 | }
7 | .Profile__row {
8 | display: flex;
9 | align-items: center;
10 | flex-direction: column;
11 | word-wrap: break-word;
12 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
13 | border: 1px solid black;
14 | border-radius: 1rem;
15 | margin-bottom: 50px;
16 | }
17 | .Profile__logo {
18 | border-radius: 50%;
19 | object-fit: cover;
20 | width: 150px;
21 | height: 150px;
22 | }
23 | .Profile__comment {
24 | margin: 0 0 50px 0;
25 | }
26 | .Profile__Modal-top-container {
27 | margin: 20px 0;
28 | }
29 |
--------------------------------------------------------------------------------
/website/styles/pages/register-login.css:
--------------------------------------------------------------------------------
1 | .Register-Login__container {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | }
6 | .Register-Login__row {
7 | padding: 30px;
8 | box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25);
9 | border: 1px solid black;
10 | border-radius: 1rem;
11 | }
12 | .Register-Login__title {
13 | text-align: center;
14 | }
15 | .Register-Login__Forgot-password {
16 | color: var(--text-color);
17 | font-size: 16px;
18 | }
19 | .Register-Login__Forgot-password:hover {
20 | color: var(--important);
21 | text-decoration: none;
22 | }
23 |
--------------------------------------------------------------------------------
/website/styles/pages/users.css:
--------------------------------------------------------------------------------
1 | .Users__form-control {
2 | display: block;
3 | height: calc(1.5em + 0.75rem + 2px);
4 | padding: 0.375rem 0.75rem;
5 | font-size: 1rem;
6 | font-weight: 400;
7 | line-height: 1.5;
8 | color: #495057;
9 | background-color: #fff;
10 | background-clip: padding-box;
11 | border: 1px solid #ced4da;
12 | border-radius: 0.5em;
13 | }
14 | .Users__search-container {
15 | margin-bottom: 50px;
16 | }
17 | .Users__search-input {
18 | width: 50%;
19 | }
20 |
--------------------------------------------------------------------------------
/website/utils/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export const API_URL = process.env.NEXT_PUBLIC_API_URL
4 |
5 | const api = (() => {
6 | const baseURL =
7 | typeof window === 'undefined' ? process.env.CONTAINER_API_URL : API_URL
8 | return axios.create({
9 | baseURL,
10 | headers: {
11 | 'Content-Type': 'application/json'
12 | }
13 | })
14 | })()
15 |
16 | export default api
17 |
--------------------------------------------------------------------------------
/website/utils/copyToClipboard.js:
--------------------------------------------------------------------------------
1 | function copyToClipboard (text) {
2 | const element = document.createElement('textarea')
3 | element.value = text
4 | document.body.appendChild(element)
5 | element.select()
6 | document.execCommand('copy')
7 | document.body.removeChild(element)
8 | }
9 |
10 | export default copyToClipboard
11 |
--------------------------------------------------------------------------------
/website/utils/redirect.js:
--------------------------------------------------------------------------------
1 | function redirect (ctx, path) {
2 | if (ctx.res != null) {
3 | ctx.res.writeHead(302, { Location: path })
4 | ctx.res.end()
5 | } else {
6 | document.location.href = path
7 | }
8 | }
9 |
10 | module.exports = redirect
11 |
--------------------------------------------------------------------------------
/website/utils/sunEditorConfig.js:
--------------------------------------------------------------------------------
1 | export const complex = [
2 | ['undo', 'redo'],
3 | ['font', 'fontSize', 'formatBlock'],
4 | ['bold', 'underline', 'italic', 'strike', 'subscript', 'superscript'],
5 | ['removeFormat'],
6 | '/',
7 | ['fontColor', 'hiliteColor'],
8 | ['outdent', 'indent'],
9 | ['align', 'horizontalRule', 'list', 'table'],
10 | ['link', 'image', 'video'],
11 | ['fullScreen', 'showBlocks', 'codeView'],
12 | ['preview', 'print'],
13 | ['save', 'template']
14 | ]
15 |
--------------------------------------------------------------------------------