├── js ├── app.js ├── constants.js ├── utils │ ├── findComponentsByPath │ │ └── index.js │ ├── api │ │ ├── weatherCastApi │ │ │ └── index.js │ │ ├── sensorsApi │ │ │ ├── index.test.js │ │ │ └── index.js │ │ └── sensorsDetailsApi │ │ │ └── index.js │ ├── env │ │ ├── index.js │ │ └── index.test.js │ └── signInForm │ │ ├── index.js │ │ └── index.test.js ├── pages │ ├── 404 │ │ └── index.js │ ├── common │ │ ├── pagination │ │ │ ├── index.unit.test.js │ │ │ ├── index.js │ │ │ └── index.integration.test.js │ │ └── header │ │ │ ├── index.js │ │ │ └── index.test.js │ ├── signIn │ │ └── index.js │ ├── home │ │ └── index.js │ ├── addSensor │ │ └── index.js │ └── facadeDetails │ │ └── index.js ├── samples │ ├── unit │ │ ├── unit1.js │ │ ├── toBeDefined.test.js │ │ ├── toEqual.test.js │ │ ├── toContain.test.js │ │ ├── toBeNan.test.js │ │ ├── toBe.test.js │ │ └── unit1.test.js │ ├── integration │ │ └── sample1.test.js │ └── course │ │ ├── lessComplicatedFunction.js │ │ └── complicatedFunction.js └── router │ ├── index.js │ └── index.test.js ├── assets ├── 212130.jpg ├── 212131.jpg ├── 212312.jpg ├── 214100.jpg ├── 214110.jpg ├── 214120.jpg ├── 214200.jpg ├── 215100.jpg ├── 215200.jpg ├── 215300.jpg ├── 215400.jpg ├── 216100.jpg ├── 216200.jpg ├── 216300.jpg ├── 217100.jpg └── 217200.jpg ├── README.md ├── babel.config.cjs ├── .travis.yml ├── data ├── facade-detail-data.json ├── mock-facade-detail-data.js ├── mock-weather-api-data.js ├── homepage-data.json └── mock-homepage-data.js ├── package.json ├── index.html ├── .gitignore └── css ├── main.css └── normalize.css /js/app.js: -------------------------------------------------------------------------------- 1 | import { router } from './router/index.js' 2 | 3 | const app = () => router() 4 | 5 | export default app -------------------------------------------------------------------------------- /js/constants.js: -------------------------------------------------------------------------------- 1 | export const USER_EMAIL = 'thomas@facadia.com' 2 | export const USER_PASSWORD = 'azerty' 3 | export const ITEMS_PER_PAGE = 8 4 | -------------------------------------------------------------------------------- /assets/212130.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/212130.jpg -------------------------------------------------------------------------------- /assets/212131.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/212131.jpg -------------------------------------------------------------------------------- /assets/212312.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/212312.jpg -------------------------------------------------------------------------------- /assets/214100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/214100.jpg -------------------------------------------------------------------------------- /assets/214110.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/214110.jpg -------------------------------------------------------------------------------- /assets/214120.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/214120.jpg -------------------------------------------------------------------------------- /assets/214200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/214200.jpg -------------------------------------------------------------------------------- /assets/215100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/215100.jpg -------------------------------------------------------------------------------- /assets/215200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/215200.jpg -------------------------------------------------------------------------------- /assets/215300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/215300.jpg -------------------------------------------------------------------------------- /assets/215400.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/215400.jpg -------------------------------------------------------------------------------- /assets/216100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/216100.jpg -------------------------------------------------------------------------------- /assets/216200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/216200.jpg -------------------------------------------------------------------------------- /assets/216300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/216300.jpg -------------------------------------------------------------------------------- /assets/217100.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/217100.jpg -------------------------------------------------------------------------------- /assets/217200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenClassrooms-Student-Center/testez-vos-applications-front-end-avec-javascript/HEAD/assets/217200.jpg -------------------------------------------------------------------------------- /js/utils/findComponentsByPath/index.js: -------------------------------------------------------------------------------- 1 | export const findComponentByPath = (path, routes) => routes.find(r => r.path.match(new RegExp(`^\\${path}$`, 'gm'))) || undefined; 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testez-vos-applications-front-end-avec-javascript 2 | Code source de Façadia - le projet fil rouge du cours "Testez vos applications Front End avec Javascript" 3 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | } -------------------------------------------------------------------------------- /js/utils/api/weatherCastApi/index.js: -------------------------------------------------------------------------------- 1 | import { isInTestEnv } from '../../env/index.js' 2 | import { data } from '../../../../data/mock-weather-api-data.js' 3 | 4 | 5 | export const retrieveWeatherForecastData = async (coordinates) => { 6 | return data 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '14' 5 | branches: 6 | only: 7 | - main 8 | cache: 9 | directories: 10 | - node_modules 11 | before_install: 12 | - npm update 13 | install: 14 | - npm install 15 | script: 16 | - npm test -------------------------------------------------------------------------------- /js/utils/env/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Verify if we are in dev|prod or test env. 3 | * We use this block of code in order to pass the tests on the CI 4 | * @returns {boolean} 5 | */ 6 | export const isInTestEnv = () => typeof process !== 'undefined' && process.env.NODE_ENV === 'test' -------------------------------------------------------------------------------- /js/pages/404/index.js: -------------------------------------------------------------------------------- 1 | const ErrorPage = { 2 | render: () => { 3 | return ` 4 |
5 |

Error Page

6 |

This is just a test of the error page

7 |
8 | ` 9 | } 10 | } 11 | 12 | export default ErrorPage 13 | -------------------------------------------------------------------------------- /js/samples/unit/unit1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {string} name 4 | */ 5 | export const sayHello = name => { 6 | if (!name) { 7 | return "Hello, World" 8 | } 9 | 10 | if (name === "Alexandra") { 11 | return "Bonjour Alexandra" 12 | } 13 | 14 | return `Hello, ${name}` 15 | } 16 | -------------------------------------------------------------------------------- /js/utils/api/sensorsApi/index.test.js: -------------------------------------------------------------------------------- 1 | import { data } from '../../../../data/mock-homepage-data' 2 | 3 | import { retrieveSensorsData } from './index' 4 | 5 | describe('Sensors API Unit Test Suites', () => { 6 | it('should return the mocked data', () => { 7 | expect(retrieveSensorsData()).toBe(data.facades) 8 | }) 9 | }) -------------------------------------------------------------------------------- /js/utils/env/index.test.js: -------------------------------------------------------------------------------- 1 | import { isInTestEnv } from './index' 2 | 3 | 4 | describe('Env Utils Unit Test Suite', () => { 5 | it('should be in test env', () => { 6 | expect(isInTestEnv()).toBe(true) 7 | }) 8 | 9 | it('should not be in test env', () => { 10 | process.env.NODE_ENV = 'dev' 11 | expect(isInTestEnv()).not.toBe(true) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /js/samples/unit/toBeDefined.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @returns {string} 4 | */ 5 | const getName = () => "thomas" 6 | 7 | const userAge = 31 8 | 9 | describe('toBeDefined Unit Test Suites', () => { 10 | it('should return something', () => ( 11 | expect(getName()).toBeDefined() 12 | )) 13 | 14 | it('should also return something', () => { 15 | expect(userAge).toBeDefined() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /js/samples/unit/toEqual.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {number} a 4 | * @param {number} b 5 | * @returns number 6 | */ 7 | const makeSum = (a, b) => a + b 8 | 9 | 10 | describe('toBe Unit Test Suites', () => { 11 | it('should return 4', () => { 12 | expect(makeSum(2, 2)).toEqual(4) 13 | }) 14 | 15 | it('should not return 6', () => { 16 | expect(makeSum(2, 3)).not.toEqual(4) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /js/utils/api/sensorsApi/index.js: -------------------------------------------------------------------------------- 1 | import { data } from '../../../../data/mock-homepage-data.js' 2 | 3 | import { isInTestEnv } from '../../env/index.js' 4 | 5 | export const retrieveSensorsData = () => isInTestEnv() 6 | ? data.facades 7 | : fetch('http://localhost:5500/data/homepage-data.json') 8 | .then(res => res.json()) 9 | .then(data => data.facades) 10 | .catch(err => console.log("Oh no", err)) 11 | -------------------------------------------------------------------------------- /js/samples/unit/toContain.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @returns {array} 4 | */ 5 | const getFriends = () => ["mike", "john", "lucie", "anna"] 6 | 7 | describe('toContain Unit Test Suite', () => { 8 | it('should contain the name "john"', () => { 9 | expect(getFriends()).toContain('john') 10 | }) 11 | 12 | it('should not contain the name "thomas"', () => { 13 | expect(getFriends()).not.toContain('thomas') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /js/utils/api/sensorsDetailsApi/index.js: -------------------------------------------------------------------------------- 1 | import { isInTestEnv } from '../../env/index.js' 2 | import { data } from '../../../../data/mock-facade-detail-data.js' 3 | 4 | 5 | export const retrieveSensorsDetailsData = () => { 6 | if (isInTestEnv()) { 7 | return data.facade 8 | } 9 | 10 | return fetch('http://localhost:5500/data/facade-detail-data.json') 11 | .then(res => res.json()) 12 | .then(data => data.facade) 13 | .catch(err => console.log("Oh no", err)) 14 | } -------------------------------------------------------------------------------- /js/samples/unit/toBeNan.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {number} a 4 | * @param {number} b 5 | * @returns {number} 6 | */ 7 | const makeSum = (a, b) => a + b 8 | 9 | /** 10 | * 11 | * @param {string} a 12 | * @param {number} b 13 | * @returns {string} 14 | */ 15 | const makeString = (a, b) => a * b 16 | 17 | 18 | describe("ToBeNan Unit Test Suites", () => { 19 | it('should return a NaN', () => { 20 | expect(makeSum(2, 2)).not.toBeNaN() 21 | }) 22 | 23 | it('should not return a NaN', () => { 24 | expect(makeString("hi", 3)).toBeNaN() 25 | }) 26 | }) -------------------------------------------------------------------------------- /data/facade-detail-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "facade": { 3 | "id": 232124, 4 | "marque": "facadiaRT", 5 | "isActive": true, 6 | "coordinates": { 7 | "lat": 12.23, 8 | "lng": -18.23 9 | }, 10 | "temperature": 17, 11 | "moisturePercentage": 0.3, 12 | "inspection": { 13 | "lastInspectionDate": "23/02/2021", 14 | "engineerId": 123456 15 | }, 16 | "medias": [ 17 | "212312.jpg", 18 | "concept-house-2.jpeg", 19 | "concept-house-3.jpeg" 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testez-vos-applications-front-end-avec-javascript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "@babel/preset-env": "^7.14.4", 8 | "@testing-library/dom": "^7.31.0", 9 | "@testing-library/jest-dom": "^5.12.0", 10 | "@testing-library/user-event": "^13.1.9", 11 | "babel-jest": "^27.0.2", 12 | "jest": "^27.0.3", 13 | "jest-html-reporter": "^3.4.1" 14 | }, 15 | "scripts": { 16 | "test": "jest" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "type": "module" 22 | } 23 | -------------------------------------------------------------------------------- /js/samples/unit/toBe.test.js: -------------------------------------------------------------------------------- 1 | const me = { 2 | firstName: "Thomas", 3 | lastName: "Dimnet", 4 | age: 31 5 | } 6 | 7 | const color = "tomato" 8 | 9 | describe('toBe Unit Test Suites', () => { 10 | it('should be my firstName', () => { 11 | expect(me.firstName).toBe("Thomas") 12 | }) 13 | 14 | it('should be my age', () => { 15 | expect(me.age).toBe(31) 16 | }) 17 | 18 | it('should be the color tomato', () => { 19 | expect(color).toBe('tomato') 20 | }) 21 | 22 | it('should not be the color tomato', () => { 23 | expect(color).not.toBe('lightblue') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /data/mock-facade-detail-data.js: -------------------------------------------------------------------------------- 1 | export const data = { 2 | "facade": { 3 | "id": 232124, 4 | "marque": "facadiaRT", 5 | "isActive": true, 6 | "coordinates": { 7 | "lat": 12.23, 8 | "lng": -18.23 9 | }, 10 | "temperature": 17, 11 | "moisturePercentage": 0.3, 12 | "inspection": { 13 | "lastInspectionDate": "23/02/2021", 14 | "engineerId": 123456 15 | }, 16 | "medias": [ 17 | "212312.jpg", 18 | "concept-house-2.jpeg", 19 | "concept-house-3.jpeg" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /js/samples/unit/unit1.test.js: -------------------------------------------------------------------------------- 1 | // Vous importez la fonction à tester 2 | import { sayHello } from './unit1.js' 3 | 4 | 5 | // Puis, vous créez le bloc de séries de test (ou Test Suite) 6 | describe('sayHello Unit Test Suites', () => { 7 | it('should display "Hello, World"', () => { 8 | expect(sayHello()).toEqual("Hello, World") 9 | }) 10 | 11 | it('should display "Bonjour Alexandra"', () => { 12 | expect(sayHello("Alexandra")).toEqual("Bonjour Alexandra") 13 | }) 14 | 15 | it('should display "Hello, Thomas"', () => { 16 | expect(sayHello("Thomas")).toEqual("Hello, Thomas") 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Façadia - Se connecter 9 | 10 | 11 | 12 | 13 | 14 |
15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /js/pages/common/pagination/index.unit.test.js: -------------------------------------------------------------------------------- 1 | import Pagination from "./index" 2 | 3 | /** 4 | * @function Pagination.getNumberOfPages 5 | */ 6 | describe('Pagination Unit Test Suites', () => { 7 | it('should return something', () => ( 8 | expect(Pagination.getNumberOfPages(12)).toBeDefined() 9 | )) 10 | 11 | it('should return 0', () => ( 12 | expect(Pagination.getNumberOfPages(0)).toEqual(0) 13 | )) 14 | 15 | it('should return 1', () => ( 16 | expect(Pagination.getNumberOfPages(7)).toEqual(1) 17 | )) 18 | 19 | it('should return 5', () => ( 20 | expect(Pagination.getNumberOfPages(34)).toEqual(5) 21 | )) 22 | }) 23 | -------------------------------------------------------------------------------- /js/samples/integration/sample1.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | // Ici j'importe DOM Test Librairy 5 | import { 6 | getByTestId 7 | } from '@testing-library/dom' 8 | 9 | // Je crée ma suite de test 10 | describe('Sample 1 Integration Test Suites', () => { 11 | // Je crée mon test 12 | it('should display "Hello, Thomas"', () => { 13 | // Je crée un nouveau noeud 14 | const $wrapper = document.createElement('div') 15 | 16 | // Je lui injecte du HTML 17 | $wrapper.innerHTML = ` 18 |
19 |

Hello, Thomas

20 |
21 | ` 22 | 23 | // Je test le resultat 24 | expect(getByTestId($wrapper, "hello").textContent).toEqual("Hello, Thomas") 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /js/samples/course/lessComplicatedFunction.js: -------------------------------------------------------------------------------- 1 | const isArrayOfNumbers = data => data.some(isNaN) 2 | 3 | const findLargestNumberInArray = data => { 4 | let largestNumber = 0; 5 | 6 | for (let i = 0; i < data.length; i++) { 7 | if (largestNumber < data[i]) { 8 | largestNumber = data[i] 9 | } 10 | } 11 | 12 | return largestNumber 13 | } 14 | 15 | const findLargestWordInArray = data => { 16 | let largestWord = "" 17 | for (let i = 0; i < data.length; i++) { 18 | if (largestWord.length < data[i].length) { 19 | largestWord = data[i] 20 | } 21 | } 22 | 23 | return largestWord 24 | } 25 | 26 | const findLargestInArray = data => { 27 | if (isArrayOfNumbers(data)) { 28 | return findLargestNumberInArray(data) 29 | } 30 | 31 | return findLargestWordInArray(data) 32 | } 33 | -------------------------------------------------------------------------------- /js/pages/common/header/index.js: -------------------------------------------------------------------------------- 1 | const Header = { 2 | render: () => { 3 | return ` 4 |
5 |

6 | Façadia 7 |

8 | 22 |
23 | ` 24 | } 25 | } 26 | 27 | export default Header 28 | -------------------------------------------------------------------------------- /js/samples/course/complicatedFunction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description une fonction trop complexe 3 | * @param {*} data 4 | * @returns 5 | */ 6 | const findLargestInArray = data => { 7 | // On regarde si le tableau est composé uniquement de chiffres 8 | if (!data.some(isNaN)) { 9 | // Si oui, alors on récupère le nombre le plus grand 10 | let largestNumber = 0; 11 | 12 | for (let i = 0; i < data.length; i++) { 13 | if (largestNumber < data[i]) { 14 | largestNumber = data[i] 15 | } 16 | } 17 | 18 | // On retourne le nombre le plus grand 19 | return largestNumber 20 | 21 | } else { 22 | // Sinon, ça veut dire qu'on a un tableau de mots 23 | // On cherche le plus grand 24 | let largestWord = "" 25 | for (let i = 0; i < data.length; i++) { 26 | if (largestWord.length < data[i].length) { 27 | largestWord = data[i] 28 | } 29 | } 30 | 31 | // On retourne le mot le plus grand 32 | return largestWord 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /js/pages/common/header/index.test.js: -------------------------------------------------------------------------------- 1 | import Header from "./index.js" 2 | 3 | describe('Header Snapshot Test Suites', () => { 4 | it('should match snapshot', () => { 5 | expect(Header.render()).toMatchInlineSnapshot(` 6 | " 7 |
8 |

9 | Façadia 10 |

11 | 25 |
26 | " 27 | `) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /data/mock-weather-api-data.js: -------------------------------------------------------------------------------- 1 | export const data = { 2 | "request": { 3 | "type": "LatLon", 4 | "query": "Lat 48.88 and Lon 2.38", 5 | "language": "en", 6 | "unit": "m" 7 | }, 8 | "location": { 9 | "name": "Villette", 10 | "country": "France", 11 | "region": "Ile-de-France", 12 | "lat": "48.883", 13 | "lon": "2.367", 14 | "timezone_id": "Europe/Paris", 15 | "localtime": "2021-05-02 18:33", 16 | "localtime_epoch": 1619980380, 17 | "utc_offset": "2.0" 18 | }, 19 | "current": { 20 | "observation_time": "04:33 PM", 21 | "temperature": 10, 22 | "weather_code": 389, 23 | "weather_icons": [ 24 | "http://assets.weatherstack.com/images/wsymbols01_png_64/wsymbol_0024_thunderstorms.png" 25 | ], 26 | "weather_descriptions": [ 27 | "Rain With Thunderstorm" 28 | ], 29 | "wind_speed": 15, 30 | "wind_degree": 360, 31 | "wind_dir": "N", 32 | "pressure": 1022, 33 | "precip": 0.5, 34 | "humidity": 66, 35 | "cloudcover": 75, 36 | "feelslike": 9, 37 | "uv_index": 3, 38 | "visibility": 10, 39 | "is_day": "yes" 40 | } 41 | } -------------------------------------------------------------------------------- /js/pages/common/pagination/index.js: -------------------------------------------------------------------------------- 1 | import { ITEMS_PER_PAGE } from '../../../constants.js' 2 | import Home from '../../home/index.js' 3 | 4 | 5 | const Pagination = { 6 | /** 7 | * 8 | * @param {number} numberOfSensors 9 | * @returns {number} 10 | */ 11 | getNumberOfPages: numberOfSensors => Math.ceil(numberOfSensors / ITEMS_PER_PAGE), 12 | render: numberOfSensors => { 13 | const numberOfPages = Pagination.getNumberOfPages(numberOfSensors) 14 | 15 | let $paginationList = '' 28 | 29 | return $paginationList 30 | }, 31 | handlePagination: () => { 32 | const $pagination = document.querySelector('.pagination-list') 33 | 34 | $pagination.addEventListener('click', (e) => { 35 | const page = Number(e.target.textContent.trim()) 36 | Home.onChangePage(ITEMS_PER_PAGE * (page - 1)) 37 | }) 38 | } 39 | } 40 | 41 | export default Pagination 42 | -------------------------------------------------------------------------------- /js/utils/signInForm/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | USER_EMAIL, 3 | USER_PASSWORD 4 | } from '../../constants.js' 5 | 6 | 7 | const checkUserEmailInput = () => { 8 | const $userMailInput = document.querySelector('#user-email') 9 | const $userEmailErrorMSg = document.querySelector('.user-email-error-msg') 10 | 11 | const isUserEmailValid = $userMailInput.value.toLowerCase() === USER_EMAIL 12 | 13 | if (isUserEmailValid) { 14 | $userEmailErrorMSg.classList.add('hidden') 15 | } else { 16 | $userEmailErrorMSg.classList.remove('hidden') 17 | } 18 | 19 | return isUserEmailValid 20 | } 21 | 22 | 23 | const checkUserPasswordInput = () => { 24 | const $userPasswordInput = document.querySelector('#user-password') 25 | const $userPasswordErrorMSg = document.querySelector('.user-password-error-msg') 26 | 27 | const isUserPasswordValid = $userPasswordInput.value.toLowerCase() === USER_PASSWORD 28 | 29 | if (isUserPasswordValid) { 30 | $userPasswordErrorMSg.classList.add('hidden') 31 | } else { 32 | $userPasswordErrorMSg.classList.remove('hidden') 33 | } 34 | 35 | return isUserPasswordValid 36 | } 37 | 38 | 39 | const isFormValid = () => checkUserEmailInput() && checkUserPasswordInput() 40 | 41 | 42 | export const handleSignInForm = () => { 43 | const $formWrapper = document.querySelector('.sign-in-form') 44 | 45 | $formWrapper.addEventListener('submit', (e) => { 46 | e.preventDefault() 47 | 48 | if (isFormValid()) { 49 | window.location = '/#/home' 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /js/router/index.js: -------------------------------------------------------------------------------- 1 | import AddSensor from '../pages/addSensor/index.js' 2 | import ErrorPage from '../pages/404/index.js' 3 | import FacadeDetails from '../pages/facadeDetails/index.js' 4 | import Home from '../pages/home/index.js' 5 | import Pagination from '../pages/common/pagination/index.js' 6 | import SignIn from '../pages/signIn/index.js' 7 | 8 | import { handleSignInForm } from '../utils/signInForm/index.js' 9 | import { findComponentByPath } from '../utils/findComponentsByPath/index.js' 10 | 11 | 12 | const routes = [ 13 | { 14 | path: "/", 15 | component: SignIn 16 | }, 17 | { 18 | path: "/home", 19 | component: Home 20 | }, 21 | { 22 | path: "/add-sensor", 23 | component: AddSensor 24 | }, 25 | { 26 | path: "/facade-details", 27 | component: FacadeDetails 28 | }, 29 | ] 30 | 31 | const parseLocation = () => location.hash.slice(1).toLocaleLowerCase() || '/' 32 | 33 | const bindEventListener = () => { 34 | if (parseLocation() === '/') { 35 | handleSignInForm() 36 | } else if (parseLocation() === '/home') { 37 | Pagination.handlePagination() 38 | } 39 | } 40 | 41 | 42 | export const router = async () => { 43 | // Find the component based on the current path 44 | const path = parseLocation() 45 | 46 | // If there is not matching route, get the "Error" Component 47 | const { component = ErrorPage } = findComponentByPath(path, routes) || {} 48 | 49 | // Render the component in the app placeholder 50 | document.querySelector('#root').innerHTML = await component.render() 51 | 52 | // Finally bind the app event listeners 53 | bindEventListener() 54 | } 55 | -------------------------------------------------------------------------------- /js/pages/common/pagination/index.integration.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { 6 | getByTestId 7 | } from '@testing-library/dom' 8 | 9 | import Pagination from "./index" 10 | 11 | 12 | let $wrapper 13 | 14 | beforeEach(() => { 15 | $wrapper = document.createElement('div') 16 | }) 17 | 18 | afterEach(() => { 19 | $wrapper = null 20 | }) 21 | 22 | /** 23 | * @function Pagination.render 24 | */ 25 | describe('Pagination Integration Test Suites', () => { 26 | it('should render 0 pagination list item', () => { 27 | const NUMBER_OF_SENSORS = 0 28 | 29 | const $paginationList = Pagination.render(NUMBER_OF_SENSORS) 30 | $wrapper.innerHTML = $paginationList 31 | 32 | expect( 33 | getByTestId($wrapper,"pagination-list").querySelectorAll('.pagination-list-item') 34 | ).toHaveLength(0) 35 | }) 36 | 37 | it('should render 2 pagination list item', () => { 38 | const NUMBER_OF_SENSORS = 10 39 | 40 | const $paginationList = Pagination.render(NUMBER_OF_SENSORS) 41 | $wrapper.innerHTML = $paginationList 42 | 43 | expect( 44 | getByTestId($wrapper,"pagination-list").querySelectorAll('.pagination-list-item') 45 | ).toHaveLength(2) 46 | }) 47 | 48 | it('should render 3 pagination list item', () => { 49 | const NUMBER_OF_SENSORS = 20 50 | 51 | const $paginationList = Pagination.render(NUMBER_OF_SENSORS) 52 | $wrapper.innerHTML = $paginationList 53 | 54 | expect( 55 | getByTestId($wrapper,"pagination-list").querySelectorAll('.pagination-list-item') 56 | ).toHaveLength(3) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /js/pages/signIn/index.js: -------------------------------------------------------------------------------- 1 | const SignIn = { 2 | render: () => { 3 | return ` 4 |
5 |
6 |

Façadia

7 | 26 |
27 |
28 | ` 29 | } 30 | } 31 | 32 | export default SignIn -------------------------------------------------------------------------------- /js/router/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import '@testing-library/jest-dom' 6 | 7 | import { 8 | getByTestId 9 | } from '@testing-library/dom' 10 | 11 | import { router } from './index' 12 | 13 | 14 | describe('Router Integration Test Suites', () => { 15 | it('should render the sign in page', async () => { 16 | 17 | document.body.innerHTML = ` 18 |
19 | ` 20 | 21 | await router() 22 | 23 | expect( 24 | getByTestId(document.body, 'sign-in-form-title') 25 | ).toHaveTextContent('Veuillez vous connecter') 26 | }) 27 | 28 | it('should render the sensors home page', async () => { 29 | document.body.innerHTML = ` 30 |
31 | ` 32 | 33 | document.location = '/#/home' 34 | 35 | await router() 36 | 37 | expect( 38 | getByTestId(document.body, 'home-sensors-title') 39 | ).toHaveTextContent('Vos capteurs') 40 | }) 41 | 42 | it('should render the sensor page', async () => { 43 | document.body.innerHTML = ` 44 |
45 | ` 46 | 47 | document.location = '/#/facade-details' 48 | 49 | await router() 50 | 51 | expect( 52 | getByTestId(document.body, 'sensor-detail-title') 53 | ).toHaveTextContent('Détails du capteur #7') 54 | }) 55 | 56 | it('should render the add sensor page', async () => { 57 | document.body.innerHTML = ` 58 |
59 | ` 60 | 61 | document.location = '/#/add-sensor' 62 | 63 | await router() 64 | 65 | expect( 66 | getByTestId(document.body, 'add-sensor-title') 67 | ).toHaveTextContent("Ajout d'un nouveau capteur") 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # NightWatch test 107 | tests_output 108 | -------------------------------------------------------------------------------- /js/pages/home/index.js: -------------------------------------------------------------------------------- 1 | import Header from '../common/header/index.js' 2 | import Pagination from '../common/pagination/index.js' 3 | 4 | import { retrieveSensorsData } from '../../utils/api/sensorsApi/index.js' 5 | 6 | import { ITEMS_PER_PAGE } from '../../constants.js' 7 | 8 | const Home = { 9 | offset: 0, 10 | sensors: null, 11 | renderSensorsCard: sensors => { 12 | let $sensorsWrapper = '
' 13 | 14 | for (let i = Home.offset; i < ITEMS_PER_PAGE + Home.offset; i++) { 15 | $sensorsWrapper += ` 16 |
17 | Capteur numéro 1 22 |
23 |

Capteur #${sensors[i].id}

24 | 25 | Localisation : ${sensors[i].location} 26 | 27 | Status : 28 | actif 29 | 30 | 31 | Voir les détails 32 | 33 |
34 |
35 | ` 36 | } 37 | 38 | $sensorsWrapper += '
' 39 | return $sensorsWrapper 40 | }, 41 | 42 | render: async () => { 43 | const sensors = await retrieveSensorsData() 44 | Home.sensors = sensors 45 | 46 | return await ` 47 |
48 |
49 | ${Header.render()} 50 |
51 |
52 |

Vos capteurs

53 |
54 | ${Home.renderSensorsCard(sensors)} 55 | ${Pagination.render(sensors.length)} 56 |
57 |
58 |
59 | ` 60 | }, 61 | 62 | onChangePage: async offset => { 63 | Home.offset = offset 64 | 65 | const $sensorsWrapper = document.querySelector('.sensors-wrapper') 66 | $sensorsWrapper.innerHTML = '' 67 | 68 | document.querySelector('#root').innerHTML = await Home.render() 69 | 70 | Pagination.handlePagination() 71 | } 72 | } 73 | 74 | export default Home -------------------------------------------------------------------------------- /js/utils/signInForm/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import '@testing-library/jest-dom' 6 | import { 7 | getByRole, 8 | getByTestId, 9 | getByLabelText 10 | } from '@testing-library/dom' 11 | import userEvent from '@testing-library/user-event' 12 | 13 | import { handleSignInForm } from './index' 14 | import SignInPage from '../../pages/signIn/index' 15 | 16 | 17 | beforeEach(() => { 18 | document.body.innerHTML = SignInPage.render() 19 | handleSignInForm() 20 | }) 21 | 22 | afterEach(() => { 23 | document.body.innerHTML = '' 24 | }) 25 | 26 | 27 | describe('SignInForm Integration Test Suites', () => { 28 | it('should display the error message when the e-mail is not correct', () => { 29 | userEvent.type( 30 | getByLabelText(document.body, 'Votre addresse e-mail'), 31 | 'thomas@thomas.com' 32 | ) 33 | 34 | userEvent.click(getByRole(document.body, 'button')) 35 | 36 | expect( 37 | getByTestId(document.body, 'user-email-error-msg') 38 | ).not.toHaveClass('hidden') 39 | }) 40 | 41 | it('should not display the error message when the e-mail is correct but it should display the password error message', () => { 42 | userEvent.type( 43 | getByLabelText(document.body, 'Votre addresse e-mail'), 44 | 'thomas@facadia.com' 45 | ) 46 | 47 | userEvent.click(getByRole(document.body, 'button')) 48 | 49 | expect( 50 | getByTestId(document.body, 'user-email-error-msg') 51 | ).toHaveClass('hidden') 52 | 53 | expect( 54 | getByTestId(document.body, 'user-password-error-msg') 55 | ).not.toHaveClass('hidden') 56 | }) 57 | 58 | it('should display the error message when the password is not correct', () => { 59 | userEvent.type( 60 | getByLabelText(document.body, 'Votre addresse e-mail'), 61 | 'thomas@facadia.com' 62 | ) 63 | 64 | userEvent.type( 65 | getByLabelText(document.body, 'Votre mot de passe'), 66 | 'thomas' 67 | ) 68 | 69 | userEvent.click(getByRole(document.body, 'button')) 70 | 71 | expect( 72 | getByTestId(document.body, 'user-password-error-msg') 73 | ).not.toHaveClass('hidden') 74 | }) 75 | 76 | it('should not display any error messages since both email and password are correct', () => { 77 | userEvent.type( 78 | getByLabelText(document.body, 'Votre addresse e-mail'), 79 | 'thomas@facadia.com' 80 | ) 81 | 82 | userEvent.type( 83 | getByLabelText(document.body, 'Votre mot de passe'), 84 | 'azerty' 85 | ) 86 | 87 | userEvent.click(getByRole(document.body, 'button')) 88 | 89 | expect( 90 | getByTestId(document.body, 'user-email-error-msg') 91 | ).toHaveClass('hidden') 92 | 93 | expect( 94 | getByTestId(document.body, 'user-password-error-msg') 95 | ).toHaveClass('hidden') 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /js/pages/addSensor/index.js: -------------------------------------------------------------------------------- 1 | import Header from '../common/header/index.js' 2 | 3 | const AddSensor = { 4 | render: () => { 5 | return ` 6 |
7 |
8 | ${Header.render()} 9 |
10 |
11 |

Ajout d'un nouveau capteur

12 |
13 |
14 |
15 | Informations de base du capteur 16 |
17 | 18 | 19 |
20 |
21 | 22 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | Coordonnées géographiques du capteur 34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 | Technicien rattaché 45 |
46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 |
57 | ` 58 | } 59 | } 60 | 61 | export default AddSensor 62 | -------------------------------------------------------------------------------- /data/homepage-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "facades": [ 3 | { 4 | "id": 1, 5 | "isActive": true, 6 | "img": "212312.jpg", 7 | "location": "24, Rue Georges Lardennois" 8 | }, 9 | { 10 | "id": 2, 11 | "isActive": false, 12 | "img": "212130.jpg", 13 | "location": "8, Rue Georges Lardennois" 14 | }, 15 | { 16 | "id": 3, 17 | "isActive": false, 18 | "img": "212131.jpg", 19 | "location": "4, Rue Georges Lardennois" 20 | }, 21 | { 22 | "id": 4, 23 | "isActive": true, 24 | "img": "214200.jpg", 25 | "location": "81, Rue Georges Lardennois" 26 | }, 27 | { 28 | "id": 5, 29 | "isActive": false, 30 | "img": "214100.jpg", 31 | "location": "2, Rue Rémy de Gourmont" 32 | }, 33 | { 34 | "id": 6, 35 | "isActive": true, 36 | "img": "214110.jpg", 37 | "location": "12, Rue Rémy de Gourmont" 38 | }, 39 | { 40 | "id": 7, 41 | "isActive": true, 42 | "img": "214120.jpg", 43 | "location": "18, Rue Rémy de Gourmont" 44 | }, 45 | { 46 | "id": 8, 47 | "isActive": true, 48 | "img": "215100.jpg", 49 | "location": "4, Rue Edgar Poe" 50 | }, 51 | { 52 | "id": 9, 53 | "isActive": true, 54 | "img": "215200.jpg", 55 | "location": "7, Rue Edgar Poe" 56 | }, 57 | { 58 | "id": 10, 59 | "isActive": true, 60 | "img": "215300.jpg", 61 | "location": "17, Rue Edgar Poe" 62 | }, 63 | { 64 | "id": 11, 65 | "isActive": true, 66 | "img": "215400.jpg", 67 | "location": "24, Rue Edgar Poe" 68 | }, 69 | { 70 | "id": 12, 71 | "isActive": false, 72 | "img": "216100.jpg", 73 | "location": "2, Rue Philippe Hetch" 74 | }, 75 | { 76 | "id": 13, 77 | "isActive": true, 78 | "img": "216200.jpg", 79 | "location": "6, Rue Philippe Hetch" 80 | }, 81 | { 82 | "id": 14, 83 | "isActive": true, 84 | "img": "216300.jpg", 85 | "location": "9, Rue Philippe Hetch" 86 | }, 87 | { 88 | "id": 15, 89 | "isActive": false, 90 | "img": "217100.jpg", 91 | "location": "2, Rue Barrelet de Ricou" 92 | }, 93 | { 94 | "id": 16, 95 | "isActive": true, 96 | "img": "217200.jpg", 97 | "location": "4, Rue Barrelet de Ricou" 98 | }, 99 | { 100 | "id": 17, 101 | "isActive": true, 102 | "img": "concept-house.jpeg", 103 | "location": "Rue Georges Lardennois" 104 | }, 105 | { 106 | "id": 18, 107 | "isActive": false, 108 | "img": "concept-house.jpeg", 109 | "location": "Rue Georges Lardennois" 110 | }, 111 | { 112 | "id": 19, 113 | "isActive": true, 114 | "img": "concept-house.jpeg", 115 | "location": "Rue Georges Lardennois" 116 | }, 117 | { 118 | "id": 20, 119 | "isActive": true, 120 | "img": "concept-house.jpeg", 121 | "location": "Rue Georges Lardennois" 122 | }, 123 | { 124 | "id": 21, 125 | "isActive": true, 126 | "img": "concept-house.jpeg", 127 | "location": "Rue Georges Lardennois" 128 | }, 129 | { 130 | "id": 22, 131 | "isActive": true, 132 | "img": "concept-house.jpeg", 133 | "location": "Rue Georges Lardennois" 134 | }, 135 | { 136 | "id": 23, 137 | "isActive": true, 138 | "img": "concept-house.jpeg", 139 | "location": "Rue Georges Lardennois" 140 | }, 141 | { 142 | "id": 24, 143 | "isActive": true, 144 | "img": "concept-house.jpeg", 145 | "location": "Rue Georges Lardennois" 146 | }, 147 | { 148 | "id": 25, 149 | "isActive": true, 150 | "img": "concept-house.jpeg", 151 | "location": "Rue Georges Lardennois" 152 | }, 153 | { 154 | "id": 26, 155 | "isActive": true, 156 | "img": "concept-house.jpeg", 157 | "location": "Rue Georges Lardennois" 158 | }, 159 | { 160 | "id": 27, 161 | "isActive": true, 162 | "img": "concept-house.jpeg", 163 | "location": "Rue Georges Lardennois" 164 | }, 165 | { 166 | "id": 28, 167 | "isActive": true, 168 | "img": "concept-house.jpeg", 169 | "location": "Rue Georges Lardennois" 170 | }, 171 | { 172 | "id": 29, 173 | "isActive": true, 174 | "img": "concept-house.jpeg", 175 | "location": "Rue Georges Lardennois" 176 | }, 177 | { 178 | "id": 30, 179 | "isActive": true, 180 | "img": "concept-house.jpeg", 181 | "location": "Rue Georges Lardennois" 182 | }, 183 | { 184 | "id": 31, 185 | "isActive": true, 186 | "img": "concept-house.jpeg", 187 | "location": "Rue Georges Lardennois" 188 | }, 189 | { 190 | "id": 32, 191 | "isActive": true, 192 | "img": "concept-house.jpeg", 193 | "location": "Rue Georges Lardennois" 194 | }, 195 | { 196 | "id": 33, 197 | "isActive": true, 198 | "img": "concept-house.jpeg", 199 | "location": "Rue Georges Lardennois" 200 | }, 201 | { 202 | "id": 34, 203 | "isActive": true, 204 | "img": "concept-house.jpeg", 205 | "location": "Rue Georges Lardennois" 206 | } 207 | ] 208 | } -------------------------------------------------------------------------------- /data/mock-homepage-data.js: -------------------------------------------------------------------------------- 1 | export const data = { 2 | "facades": [ 3 | { 4 | "id": 1, 5 | "isActive": true, 6 | "img": "212312.jpg", 7 | "location": "24, Rue Georges Lardennois" 8 | }, 9 | { 10 | "id": 2, 11 | "isActive": false, 12 | "img": "212130.jpg", 13 | "location": "8, Rue Georges Lardennois" 14 | }, 15 | { 16 | "id": 3, 17 | "isActive": false, 18 | "img": "212131.jpg", 19 | "location": "4, Rue Georges Lardennois" 20 | }, 21 | { 22 | "id": 4, 23 | "isActive": true, 24 | "img": "214200.jpg", 25 | "location": "81, Rue Georges Lardennois" 26 | }, 27 | { 28 | "id": 5, 29 | "isActive": false, 30 | "img": "214100.jpg", 31 | "location": "2, Rue Rémy de Gourmont" 32 | }, 33 | { 34 | "id": 6, 35 | "isActive": true, 36 | "img": "214110.jpg", 37 | "location": "12, Rue Rémy de Gourmont" 38 | }, 39 | { 40 | "id": 7, 41 | "isActive": true, 42 | "img": "214120.jpg", 43 | "location": "18, Rue Rémy de Gourmont" 44 | }, 45 | { 46 | "id": 8, 47 | "isActive": true, 48 | "img": "215100.jpg", 49 | "location": "4, Rue Edgar Poe" 50 | }, 51 | { 52 | "id": 9, 53 | "isActive": true, 54 | "img": "215200.jpg", 55 | "location": "7, Rue Edgar Poe" 56 | }, 57 | { 58 | "id": 10, 59 | "isActive": true, 60 | "img": "215300.jpg", 61 | "location": "17, Rue Edgar Poe" 62 | }, 63 | { 64 | "id": 11, 65 | "isActive": true, 66 | "img": "215400.jpg", 67 | "location": "24, Rue Edgar Poe" 68 | }, 69 | { 70 | "id": 12, 71 | "isActive": false, 72 | "img": "216100.jpg", 73 | "location": "2, Rue Philippe Hetch" 74 | }, 75 | { 76 | "id": 13, 77 | "isActive": true, 78 | "img": "216200.jpg", 79 | "location": "6, Rue Philippe Hetch" 80 | }, 81 | { 82 | "id": 14, 83 | "isActive": true, 84 | "img": "216300.jpg", 85 | "location": "9, Rue Philippe Hetch" 86 | }, 87 | { 88 | "id": 15, 89 | "isActive": false, 90 | "img": "217100.jpg", 91 | "location": "2, Rue Barrelet de Ricou" 92 | }, 93 | { 94 | "id": 16, 95 | "isActive": true, 96 | "img": "217200.jpg", 97 | "location": "4, Rue Barrelet de Ricou" 98 | }, 99 | { 100 | "id": 17, 101 | "isActive": true, 102 | "img": "concept-house.jpeg", 103 | "location": "Rue Georges Lardennois" 104 | }, 105 | { 106 | "id": 18, 107 | "isActive": false, 108 | "img": "concept-house.jpeg", 109 | "location": "Rue Georges Lardennois" 110 | }, 111 | { 112 | "id": 19, 113 | "isActive": true, 114 | "img": "concept-house.jpeg", 115 | "location": "Rue Georges Lardennois" 116 | }, 117 | { 118 | "id": 20, 119 | "isActive": true, 120 | "img": "concept-house.jpeg", 121 | "location": "Rue Georges Lardennois" 122 | }, 123 | { 124 | "id": 21, 125 | "isActive": true, 126 | "img": "concept-house.jpeg", 127 | "location": "Rue Georges Lardennois" 128 | }, 129 | { 130 | "id": 22, 131 | "isActive": true, 132 | "img": "concept-house.jpeg", 133 | "location": "Rue Georges Lardennois" 134 | }, 135 | { 136 | "id": 23, 137 | "isActive": true, 138 | "img": "concept-house.jpeg", 139 | "location": "Rue Georges Lardennois" 140 | }, 141 | { 142 | "id": 24, 143 | "isActive": true, 144 | "img": "concept-house.jpeg", 145 | "location": "Rue Georges Lardennois" 146 | }, 147 | { 148 | "id": 25, 149 | "isActive": true, 150 | "img": "concept-house.jpeg", 151 | "location": "Rue Georges Lardennois" 152 | }, 153 | { 154 | "id": 26, 155 | "isActive": true, 156 | "img": "concept-house.jpeg", 157 | "location": "Rue Georges Lardennois" 158 | }, 159 | { 160 | "id": 27, 161 | "isActive": true, 162 | "img": "concept-house.jpeg", 163 | "location": "Rue Georges Lardennois" 164 | }, 165 | { 166 | "id": 28, 167 | "isActive": true, 168 | "img": "concept-house.jpeg", 169 | "location": "Rue Georges Lardennois" 170 | }, 171 | { 172 | "id": 29, 173 | "isActive": true, 174 | "img": "concept-house.jpeg", 175 | "location": "Rue Georges Lardennois" 176 | }, 177 | { 178 | "id": 30, 179 | "isActive": true, 180 | "img": "concept-house.jpeg", 181 | "location": "Rue Georges Lardennois" 182 | }, 183 | { 184 | "id": 31, 185 | "isActive": true, 186 | "img": "concept-house.jpeg", 187 | "location": "Rue Georges Lardennois" 188 | }, 189 | { 190 | "id": 32, 191 | "isActive": true, 192 | "img": "concept-house.jpeg", 193 | "location": "Rue Georges Lardennois" 194 | }, 195 | { 196 | "id": 33, 197 | "isActive": true, 198 | "img": "concept-house.jpeg", 199 | "location": "Rue Georges Lardennois" 200 | }, 201 | { 202 | "id": 34, 203 | "isActive": true, 204 | "img": "concept-house.jpeg", 205 | "location": "Rue Georges Lardennois" 206 | } 207 | ] 208 | } -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Descriptive color variables */ 3 | --japanese-laurel: #008000; 4 | --mine-shaft: #333; 5 | --persimmon: #FF6347; 6 | --science-blue: #0B5ED7; 7 | --silver: #C4C4C4; 8 | --wild-sand: #F5F5F5; 9 | 10 | /* Functional color variables */ 11 | --color-primary: var(--wild-sand); 12 | --color-secondary: var(--mine-shaft); 13 | --color-tertiary: var(--silver); 14 | 15 | --button-bg-color: var(--science-blue); 16 | 17 | --is-ok-color: var(--japanese-laurel); 18 | --has-warning-color: var(--persimmon); 19 | } 20 | 21 | /* General Settings */ 22 | body { 23 | background-color: var(--color-primary); 24 | color: var(--color-secondary); 25 | } 26 | 27 | h1, 28 | h2 { 29 | margin: 0; 30 | } 31 | 32 | .hidden { 33 | display: none; 34 | } 35 | 36 | .pr { 37 | margin-right: auto !important; 38 | } 39 | 40 | img { 41 | width: 100%; 42 | } 43 | 44 | .section-title { 45 | font-size: 24px; 46 | font-weight: 300; 47 | } 48 | 49 | .on { 50 | color: var(--is-ok-color); 51 | } 52 | 53 | .off { 54 | color: var(--has-warning-color); 55 | } 56 | 57 | /* Main Header */ 58 | .main-header { 59 | align-items: center; 60 | background-color: var(--color-secondary); 61 | display: flex; 62 | height: 100px; 63 | padding-left: 40px; 64 | padding-right: 40px; 65 | margin-bottom: 40px; 66 | } 67 | 68 | .main-header-title { 69 | margin-right: 40px; 70 | } 71 | 72 | .main-header-title a, 73 | .main-nav-link { 74 | color: var(--color-primary); 75 | text-decoration: none; 76 | } 77 | 78 | .main-nav-link:hover { 79 | text-decoration: underline; 80 | } 81 | 82 | .main-nav { 83 | display: flex; 84 | list-style-type: none; 85 | padding-left: 0; 86 | width: 100%; 87 | } 88 | 89 | .main-nav-item { 90 | margin-right: 24px; 91 | } 92 | 93 | /* Sign In Page */ 94 | .sign-in-page { 95 | align-items: center; 96 | display: flex; 97 | justify-content: center; 98 | min-height: 100vh; 99 | min-width: 100vw; 100 | } 101 | 102 | .sign-in-main-wrapper { 103 | background-color: var(--color-secondary); 104 | color: var(--color-primary); 105 | padding: 60px 40px 30px; 106 | } 107 | 108 | .main-title { 109 | margin-bottom: 24px; 110 | text-align: center; 111 | } 112 | 113 | .form-title { 114 | font-weight: 300; 115 | margin-bottom: 16px; 116 | text-align: center; 117 | } 118 | 119 | .form-group { 120 | display: flex; 121 | flex-direction: column; 122 | font-size: 16px; 123 | margin-bottom: 16px; 124 | min-width: 300px; 125 | } 126 | 127 | .form-group label { 128 | margin-bottom: 8px; 129 | } 130 | 131 | .form-group input { 132 | border: none; 133 | height: 39px; 134 | padding-left: 8px; 135 | } 136 | 137 | .form-text-error { 138 | color: var(--has-warning-color); 139 | margin-top: 8px; 140 | } 141 | 142 | .submit-btn { 143 | background-color: var(--button-bg-color); 144 | border-radius: 8px; 145 | border: none; 146 | color: var(--color-primary); 147 | cursor: pointer; 148 | font-size: 16px; 149 | height: 39px; 150 | margin-top: 24px; 151 | width: 100%; 152 | } 153 | 154 | /* Home Page */ 155 | .home-page-main { 156 | width: 80%; 157 | max-width: 1024px; 158 | margin-left: auto; 159 | margin-right: auto; 160 | } 161 | 162 | 163 | .home-page-main-top-header { 164 | display: flex; 165 | } 166 | 167 | .home-page-main-top-header .section-title { 168 | margin-right: auto; 169 | } 170 | 171 | .sensors-wrapper { 172 | display: grid; 173 | grid-template-columns: 1fr 1fr 1fr 1fr; 174 | grid-row-gap: 16px; 175 | grid-column-gap: 24px; 176 | margin-bottom: 32px; 177 | } 178 | 179 | .sensor-card { 180 | background-color: var(--color-secondary); 181 | color: var(--color-primary); 182 | border-radius: 8px; 183 | overflow: hidden; 184 | } 185 | 186 | .sensor-info { 187 | display: flex; 188 | flex-direction: column; 189 | padding: 8px 16px; 190 | } 191 | 192 | .sensor-info-title { 193 | margin-bottom: 8px; 194 | } 195 | 196 | .sensor-info-location, 197 | .sensor-info-status { 198 | display: block; 199 | margin-bottom: 8px; 200 | } 201 | 202 | .sensor-info-btn { 203 | background-color: var(--button-bg-color); 204 | border-radius: 8px; 205 | color: var(--color-primary); 206 | padding-bottom: 8px; 207 | padding-top: 8px; 208 | text-align: center; 209 | text-decoration: none; 210 | } 211 | 212 | .pagination-list { 213 | display: flex; 214 | justify-content: center; 215 | list-style-type: none; 216 | padding-left: 0; 217 | } 218 | 219 | .pagination-list-item { 220 | border: 1px solid var(--button-bg-color); 221 | } 222 | 223 | .pagination-list-item:first-child { 224 | border-top-left-radius: 8px; 225 | border-bottom-left-radius: 8px; 226 | } 227 | 228 | .pagination-list-item:last-child { 229 | border-top-right-radius: 8px; 230 | border-bottom-right-radius: 8px; 231 | } 232 | 233 | .pagination-list-item a { 234 | align-items: center; 235 | color: var(--button-bg-color); 236 | display: flex; 237 | height: 38px; 238 | justify-content: center; 239 | text-decoration: none; 240 | width: 33px; 241 | } 242 | 243 | 244 | /* Sensor Detail Page */ 245 | .sensor-details-main { 246 | margin-left: auto; 247 | margin-right: auto; 248 | max-width: 1024px; 249 | width: 80%; 250 | } 251 | 252 | .sensor-details-wrapper { 253 | display: grid; 254 | grid-template-columns: 400px auto; 255 | grid-template-rows: 500px; 256 | grid-column-gap: 40px; 257 | } 258 | 259 | .data-sensor-wrapper { 260 | margin-bottom: 32px; 261 | } 262 | 263 | .sensor-details-img-gallery-wrapper img { 264 | height: 100%; 265 | object-fit: cover; 266 | } 267 | 268 | .sensor-details-info-wrapper { 269 | background-color: var(--color-tertiary); 270 | padding-left: 16px; 271 | padding-right: 16px; 272 | overflow: scroll; 273 | } 274 | 275 | .data-table { 276 | background-color: var(--color-primary); 277 | border: 1px solid var(--color-secondary); 278 | width: 100%; 279 | border-collapse: collapse; 280 | border: 1px solid var(--color-secondary); 281 | } 282 | 283 | .data-table th, 284 | .data-table td { 285 | border: 1px solid var(--color-secondary); 286 | padding: 8px; 287 | text-align: center; 288 | } 289 | 290 | .data-table th:hover, 291 | .data-table td:hover { 292 | color: var(--color-primary); 293 | background-color: var(--color-secondary); 294 | } 295 | 296 | .weather-forecast-wrapper { 297 | margin-bottom: 32px; 298 | } 299 | 300 | /* Add Sensor Page */ 301 | .add-sensor-page-main { 302 | width: 80%; 303 | max-width: 1024px; 304 | margin-left: auto; 305 | margin-right: auto; 306 | } 307 | -------------------------------------------------------------------------------- /js/pages/facadeDetails/index.js: -------------------------------------------------------------------------------- 1 | import Header from '../common/header/index.js' 2 | 3 | import { retrieveSensorsDetailsData } from '../../utils/api/sensorsDetailsApi/index.js' 4 | import { retrieveWeatherForecastData } from '../../utils/api/weatherCastApi/index.js' 5 | 6 | const FacadeDetails = { 7 | renderSensorImg: sensor => { 8 | return ` 9 | 12 | ` 13 | }, 14 | renderSensorTable: sensor => { 15 | return ` 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 82 | 85 | 86 | 87 |
#Type de donnéesValeur des données
1ID 27 | ${sensor.id} 28 |
2Marque du capteur 34 | ${sensor.marque} 35 |
3Status 41 | ${sensor.isActive ? 'actif': 'inactif'} 42 |
4Lattitude 48 | ${sensor.coordinates.lat} 49 |
5Longitude 55 | ${sensor.coordinates.lng} 56 |
6Température 62 | ${sensor.temperature} 63 |
7Degrée d'humidité 69 | ${sensor.moisturePercentage * 100} 70 |
8Date de dernière visite 76 | ${sensor.inspection.lastInspectionDate} 77 |
9ID du technicien 83 | ${sensor.inspection.engineerId} 84 |
88 | ` 89 | }, 90 | renderWeatherForecast: forecastData => { 91 | return ` 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | 126 | 127 | 128 | 129 | 130 | 134 | 135 | 136 | 137 | 138 | 141 | 142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 | 152 | 155 | 156 | 157 |
#Type de donnéesValeur des données
1Localisation${forecastData.location.name}
2Température${forecastData.current.temperature} °C
3Icône 113 | ${forecastData.current.weather_descriptions[0]} 118 |
4Description 124 | ${forecastData.current.weather_descriptions[0]} 125 |
5Vent 131 | ${forecastData.current.wind_dir} 132 | ${forecastData.current.wind_speed} km/h 133 |
6Couverture nuageuse 139 | ${forecastData.current.cloudcover} 140 |
7Index UV 146 | ${forecastData.current.uv_index} 147 |
8Jour/nuit 153 | ${forecastData.current.is_day === 'yes' ? 'Jour' : 'Nuit'} 154 |
158 | ` 159 | }, 160 | 161 | render: async () => { 162 | const sensor = await retrieveSensorsDetailsData() 163 | const weatherForecastData = await retrieveWeatherForecastData(sensor.coordinates) 164 | 165 | return ` 166 |
167 |
168 | ${Header.render()} 169 |
170 |
171 |

Détails du capteur #7

172 |
173 |
174 | ${FacadeDetails.renderSensorImg(sensor)} 175 |
176 |
177 |

Données du capteur

178 | ${FacadeDetails.renderSensorTable(sensor)} 179 |
180 |
181 |

Bulletin météo

182 | ${FacadeDetails.renderWeatherForecast(weatherForecastData)} 183 |
184 |
185 |
186 |
187 |
188 |
189 | ` 190 | } 191 | } 192 | 193 | export default FacadeDetails 194 | 195 | -------------------------------------------------------------------------------- /css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | --------------------------------------------------------------------------------