├── 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 |
8 |
9 | -
10 | Accueil
11 |
12 | -
13 | Le projet
14 |
15 | -
16 | Ajouter un capteur
17 |
18 | -
19 | Se Déconnecter
20 |
21 |
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 |
11 |
12 | -
13 | Accueil
14 |
15 | -
16 | Le projet
17 |
18 | -
19 | Ajouter un capteur
20 |
21 | -
22 | Se Déconnecter
23 |
24 |
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 |
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 |

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

11 |
12 | `
13 | },
14 | renderSensorTable: sensor => {
15 | return `
16 |
17 |
18 | | # |
19 | Type de données |
20 | Valeur des données |
21 |
22 |
23 |
24 | | 1 |
25 | ID |
26 |
27 | ${sensor.id}
28 | |
29 |
30 |
31 | | 2 |
32 | Marque du capteur |
33 |
34 | ${sensor.marque}
35 | |
36 |
37 |
38 | | 3 |
39 | Status |
40 |
41 | ${sensor.isActive ? 'actif': 'inactif'}
42 | |
43 |
44 |
45 | | 4 |
46 | Lattitude |
47 |
48 | ${sensor.coordinates.lat}
49 | |
50 |
51 |
52 | | 5 |
53 | Longitude |
54 |
55 | ${sensor.coordinates.lng}
56 | |
57 |
58 |
59 | | 6 |
60 | Température |
61 |
62 | ${sensor.temperature}
63 | |
64 |
65 |
66 | | 7 |
67 | Degrée d'humidité |
68 |
69 | ${sensor.moisturePercentage * 100}
70 | |
71 |
72 |
73 | | 8 |
74 | Date de dernière visite |
75 |
76 | ${sensor.inspection.lastInspectionDate}
77 | |
78 |
79 |
80 | | 9 |
81 | ID du technicien |
82 |
83 | ${sensor.inspection.engineerId}
84 | |
85 |
86 |
87 |
88 | `
89 | },
90 | renderWeatherForecast: forecastData => {
91 | return `
92 |
93 |
94 | | # |
95 | Type de données |
96 | Valeur des données |
97 |
98 |
99 |
100 | | 1 |
101 | Localisation |
102 | ${forecastData.location.name} |
103 |
104 |
105 | | 2 |
106 | Température |
107 | ${forecastData.current.temperature} °C |
108 |
109 |
110 | | 3 |
111 | Icône |
112 |
113 |
118 | |
119 |
120 |
121 | | 4 |
122 | Description |
123 |
124 | ${forecastData.current.weather_descriptions[0]}
125 | |
126 |
127 |
128 | | 5 |
129 | Vent |
130 |
131 | ${forecastData.current.wind_dir}
132 | ${forecastData.current.wind_speed} km/h
133 | |
134 |
135 |
136 | | 6 |
137 | Couverture nuageuse |
138 |
139 | ${forecastData.current.cloudcover}
140 | |
141 |
142 |
143 | | 7 |
144 | Index UV |
145 |
146 | ${forecastData.current.uv_index}
147 | |
148 |
149 |
150 | | 8 |
151 | Jour/nuit |
152 |
153 | ${forecastData.current.is_day === 'yes' ? 'Jour' : 'Nuit'}
154 | |
155 |
156 |
157 |
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 |
--------------------------------------------------------------------------------