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

FunctionProject

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 | Node.js CI 13 | JavaScript Style Guide 14 | Licence MIT 15 | Conventional Commits 16 | Contributor Covenant 17 |

18 | FunctionProject 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 | {props.title} 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 | {props.title} 75 |

76 | {props.title} 77 |

78 |

{props.description}

79 |
80 | 81 | 88 | {props.categorie.name} 89 | 90 | 91 |

92 | {date.format(new Date(props.createdAt), 'DD/MM/YYYY', false)} 93 |

94 |
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 |
19 | 24 | 25 | 30 |
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 |
4 |
5 | 23 |
24 |
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 | 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 |