@vixoniccom/aqi 0.0.1 → 0.0.2-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/build/index.html +1 -1
- package/build/main.js +2 -39
- package/build/main.js.LICENSE.txt +46 -0
- package/build/test/downloads/123.ttf +0 -0
- package/build/test/downloads/7953953c-7029-4730-ae4d-cff4abd5288f.ttf +0 -0
- package/build/test/downloads/Earthquakes.png +0 -0
- package/build/test/downloads/c705a739-312e-4334-9231-e73a05e9d382.ttf +0 -0
- package/build/test/downloads/futura-font.ttf +0 -0
- package/build/test/parameters.json +36 -0
- package/build.zip +0 -0
- package/package.json +8 -8
- package/sonar-project.properties +1 -0
- package/src/App.tsx +17 -68
- package/src/components/Card.tsx +1 -1
- package/src/components/FontLoader.tsx +1 -1
- package/src/components/FormattedText.tsx +10 -11
- package/src/index.html +13 -10
- package/src/main.ts +3 -3
- package/src/services/index.ts +64 -0
- package/src/utils.ts +2 -6
- package/tsconfig.json +27 -36
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
localForage -- Offline Storage, Improved
|
|
3
|
+
Version 1.10.0
|
|
4
|
+
https://localforage.github.io/localForage
|
|
5
|
+
(c) 2013-2017 Mozilla, Apache License 2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @license React
|
|
10
|
+
* react-dom.production.min.js
|
|
11
|
+
*
|
|
12
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
13
|
+
*
|
|
14
|
+
* This source code is licensed under the MIT license found in the
|
|
15
|
+
* LICENSE file in the root directory of this source tree.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @license React
|
|
20
|
+
* react-jsx-runtime.production.min.js
|
|
21
|
+
*
|
|
22
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
23
|
+
*
|
|
24
|
+
* This source code is licensed under the MIT license found in the
|
|
25
|
+
* LICENSE file in the root directory of this source tree.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @license React
|
|
30
|
+
* react.production.min.js
|
|
31
|
+
*
|
|
32
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
33
|
+
*
|
|
34
|
+
* This source code is licensed under the MIT license found in the
|
|
35
|
+
* LICENSE file in the root directory of this source tree.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @license React
|
|
40
|
+
* scheduler.production.min.js
|
|
41
|
+
*
|
|
42
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
43
|
+
*
|
|
44
|
+
* This source code is licensed under the MIT license found in the
|
|
45
|
+
* LICENSE file in the root directory of this source tree.
|
|
46
|
+
*/
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"parameters": {
|
|
3
|
+
"backgroundImage": {
|
|
4
|
+
"id": "earthquakes",
|
|
5
|
+
"filename": "Earthquakes.png",
|
|
6
|
+
"__isAsset": true
|
|
7
|
+
},
|
|
8
|
+
"cityInput": "Santiago",
|
|
9
|
+
"topSeparationCard": 200,
|
|
10
|
+
"leftSeparationCard": 600,
|
|
11
|
+
"cardWidth": 700,
|
|
12
|
+
"cardHeight": 600,
|
|
13
|
+
"cardGap": 2,
|
|
14
|
+
"aqiFormat": {
|
|
15
|
+
"fontSize": 18,
|
|
16
|
+
"fontColor": "#000000",
|
|
17
|
+
"alignment": {
|
|
18
|
+
"horizontal": "center"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"stationFormat": {
|
|
22
|
+
"fontSize": 8,
|
|
23
|
+
"fontColor": "#000000",
|
|
24
|
+
"alignment": {
|
|
25
|
+
"horizontal": "center"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"qualityFormat": {
|
|
29
|
+
"fontSize": 4,
|
|
30
|
+
"fontColor": "#000000",
|
|
31
|
+
"alignment": {
|
|
32
|
+
"horizontal": "center"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/build.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Daniel Alvayay"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.0.
|
|
11
|
+
"version": "0.0.2-dev.0",
|
|
12
12
|
"scripts": {
|
|
13
13
|
"prepublishOnly": "vixonic-module-packager --mode=build",
|
|
14
14
|
"watch": "vixonic-module-packager --mode=watch",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"axios": "^1.6.0",
|
|
24
24
|
"localforage": "^1.10.0",
|
|
25
|
-
"react": "^
|
|
26
|
-
"react-dom": "^
|
|
25
|
+
"react": "^18.3.1",
|
|
26
|
+
"react-dom": "^18.3.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@types/react": "^
|
|
30
|
-
"@types/react-dom": "^
|
|
31
|
-
"@vixoniccom/module-packager": "^2.
|
|
32
|
-
"@vixoniccom/modules": "^2.
|
|
33
|
-
"standard-version": "^9.
|
|
29
|
+
"@types/react": "^18.3.23",
|
|
30
|
+
"@types/react-dom": "^18.3.7",
|
|
31
|
+
"@vixoniccom/module-packager": "^2.13.0-dev.1",
|
|
32
|
+
"@vixoniccom/modules": "^2.20.5-dev.1",
|
|
33
|
+
"standard-version": "^9.5.0"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sonar.projectKey=Vixonic_store-air-quality-index_de6b8e55-8f87-4d53-b9d5-7e514a2702d9
|
package/src/App.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
|
-
import localforage from 'localforage'
|
|
3
1
|
import React, { useEffect, useState } from 'react'
|
|
4
2
|
import { Card } from './components/Card'
|
|
5
3
|
import { FontLoader } from './components/FontLoader'
|
|
6
4
|
import { FormattedText } from './components/FormattedText'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { AqiData } from './types'
|
|
6
|
+
import { getData } from './services'
|
|
9
7
|
|
|
10
8
|
interface Props {
|
|
11
9
|
data: VixonicData
|
|
@@ -19,65 +17,25 @@ export const App: React.FunctionComponent<Props> = ({ data, start }) => {
|
|
|
19
17
|
const cityInput = parameters?.cityInput || "santiago"
|
|
20
18
|
const updateTime = 600000
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const sameYear = referenceDate.getFullYear() === now.getFullYear()
|
|
26
|
-
const sameMonth = referenceDate.getMonth() === now.getMonth()
|
|
27
|
-
const sameDay = referenceDate.getDate() === now.getDate()
|
|
28
|
-
|
|
29
|
-
if (sameYear && sameMonth && sameDay) {
|
|
30
|
-
const differenceMinutes = Math.abs(now.getTime() - referenceDate.getTime())
|
|
31
|
-
return differenceMinutes >= updateTime
|
|
32
|
-
}
|
|
33
|
-
return true
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const requestData = async (): Promise<AirQuality | null> => {
|
|
37
|
-
const URL = `https://api.waqi.info/feed/${cityInput}/?token=${TOKEN}`
|
|
38
|
-
try {
|
|
39
|
-
const response = await axios.get<AirQuality>(URL)
|
|
40
|
-
if (response?.status === 200) {
|
|
41
|
-
return response.data
|
|
42
|
-
}
|
|
43
|
-
return null
|
|
44
|
-
} catch (error) {
|
|
45
|
-
return null
|
|
46
|
-
}
|
|
47
|
-
}
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!start) return
|
|
48
22
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
23
|
+
const fetchData = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const data = await getData(cityInput, updateTime)
|
|
26
|
+
setFormattedData(data)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Error al obtener datos:', error)
|
|
29
|
+
setFormattedData(undefined)
|
|
56
30
|
}
|
|
57
31
|
}
|
|
58
|
-
return data
|
|
59
|
-
}
|
|
60
32
|
|
|
61
|
-
|
|
62
|
-
const storageData: StorageData | null = await localforage.getItem('aqi')
|
|
63
|
-
let data
|
|
64
|
-
if (!storageData?.item || storageData?.item.city !== parameters.cityInput || !storageData?.date || refetchData(new Date(storageData.date))) {
|
|
65
|
-
data = await requestData()
|
|
66
|
-
const processedData = processData(data)
|
|
67
|
-
await localforage.setItem('aqi', { item: processedData, date: new Date() })
|
|
68
|
-
setFormattedData(processedData)
|
|
69
|
-
} else {
|
|
70
|
-
data = storageData.item
|
|
71
|
-
setFormattedData(data)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
33
|
+
fetchData()
|
|
74
34
|
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
if (!start) return
|
|
77
|
-
getData()
|
|
78
35
|
const interval = setInterval(() => {
|
|
79
|
-
|
|
36
|
+
fetchData()
|
|
80
37
|
}, updateTime)
|
|
38
|
+
|
|
81
39
|
return () => clearInterval(interval)
|
|
82
40
|
}, [start, updateTime, parameters.cityInput])
|
|
83
41
|
|
|
@@ -92,11 +50,7 @@ export const App: React.FunctionComponent<Props> = ({ data, start }) => {
|
|
|
92
50
|
backgroundImage: backgroundImageState,
|
|
93
51
|
backgroundSize: '100% 100%',
|
|
94
52
|
}}>
|
|
95
|
-
<div style={{
|
|
96
|
-
width: "100%",
|
|
97
|
-
height: "100%",
|
|
98
|
-
position: "relative"
|
|
99
|
-
}}>
|
|
53
|
+
<div style={{ width: "100%", height: "100%", position: "relative" }}>
|
|
100
54
|
{formattedData ? (
|
|
101
55
|
<div style={{
|
|
102
56
|
position: "absolute",
|
|
@@ -122,12 +76,7 @@ export const App: React.FunctionComponent<Props> = ({ data, start }) => {
|
|
|
122
76
|
left: `${parameters.leftSeparationCard || 0}px`
|
|
123
77
|
}}>
|
|
124
78
|
<FontLoader paths={['formatMjs.font']} parameters={parameters} downloadsPath={downloadsPath} />
|
|
125
|
-
<div style={{
|
|
126
|
-
display: 'flex',
|
|
127
|
-
position: 'relative',
|
|
128
|
-
flex: '1 1 0%',
|
|
129
|
-
flexDirection: 'column',
|
|
130
|
-
}}>
|
|
79
|
+
<div style={{ display: 'flex', position: 'relative', flex: '1 1 0%', flexDirection: 'column' }}>
|
|
131
80
|
<FormattedText text={parameters?.msj0 || 'No hay datos para mostrar'} format={parameters?.formatMjs} />
|
|
132
81
|
</div>
|
|
133
82
|
</div>
|
|
@@ -135,4 +84,4 @@ export const App: React.FunctionComponent<Props> = ({ data, start }) => {
|
|
|
135
84
|
</div>
|
|
136
85
|
</div>
|
|
137
86
|
)
|
|
138
|
-
}
|
|
87
|
+
}
|
package/src/components/Card.tsx
CHANGED
|
@@ -23,10 +23,10 @@ interface Props {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export const FormattedText: React.FunctionComponent<Props> = ({ text, format, maxChar, lineHeight, style, unit, paddingBottom, paddingTop }) => {
|
|
26
|
-
const trimText = (text:
|
|
26
|
+
const trimText = (text: string, maxChar?: number) => {
|
|
27
27
|
const isValid = maxChar && maxChar >= 3
|
|
28
|
-
if (isValid && (text && text.length > maxChar)
|
|
29
|
-
|
|
28
|
+
if (isValid && (text && text.length > maxChar)) {
|
|
29
|
+
const returnText = text.substring(0, maxChar - 3)
|
|
30
30
|
returnText.substr(-1, 3)
|
|
31
31
|
return `${returnText.trim()}...`
|
|
32
32
|
}
|
|
@@ -34,7 +34,7 @@ export const FormattedText: React.FunctionComponent<Props> = ({ text, format, ma
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const checkNested = (obj: any, path: any): any => {
|
|
37
|
-
|
|
37
|
+
const arr = path.split('.')
|
|
38
38
|
if (arr.length > 0) {
|
|
39
39
|
if (obj.hasOwnProperty(arr[0])) {
|
|
40
40
|
if (arr.length > 1) return checkNested(obj[arr[0]], arr.splice(1).join('.'))
|
|
@@ -43,12 +43,11 @@ export const FormattedText: React.FunctionComponent<Props> = ({ text, format, ma
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const getHorizontalAlignment = (alignment
|
|
47
|
-
if (alignment)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return 'flex-start'
|
|
46
|
+
const getHorizontalAlignment = (alignment?: Aligment) => {
|
|
47
|
+
if (!alignment) return 'flex-start'
|
|
48
|
+
|
|
49
|
+
const hA = alignment.horizontal
|
|
50
|
+
return alignments.hasOwnProperty(hA) ? alignments[hA] : 'flex-start'
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
const renderText = maxChar ? trimText(text, maxChar) : text
|
|
@@ -78,4 +77,4 @@ FormattedText.defaultProps = {
|
|
|
78
77
|
lineHeight: 1,
|
|
79
78
|
unit: 'vh',
|
|
80
79
|
maxChar: undefined
|
|
81
|
-
}
|
|
80
|
+
}
|
package/src/index.html
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
<html
|
|
3
|
+
lang="en"
|
|
4
|
+
style="position: absolute; height: 100%; width: 100%; overflow: hidden">
|
|
5
|
+
<head>
|
|
6
|
+
<title></title>
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
</head>
|
|
9
|
+
<body style="margin: 0; overflow: hidden">
|
|
10
|
+
<div
|
|
11
|
+
id="root"
|
|
12
|
+
style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; overflow: hidden"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/src/main.ts
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import localforage from 'localforage'
|
|
3
|
+
import { AirQuality, AqiData, StorageData } from '../types'
|
|
4
|
+
import { API_RESPONSE_STATUS } from '../utils'
|
|
5
|
+
|
|
6
|
+
const TOKEN = process.env.AQI_TOKEN ?? ''
|
|
7
|
+
|
|
8
|
+
const refetchData = (referenceDate: Date, updateTime: number): boolean => {
|
|
9
|
+
const now = new Date()
|
|
10
|
+
|
|
11
|
+
const sameYear = referenceDate.getFullYear() === now.getFullYear()
|
|
12
|
+
const sameMonth = referenceDate.getMonth() === now.getMonth()
|
|
13
|
+
const sameDay = referenceDate.getDate() === now.getDate()
|
|
14
|
+
|
|
15
|
+
if (sameYear && sameMonth && sameDay) {
|
|
16
|
+
const differenceMinutes = Math.abs(now.getTime() - referenceDate.getTime())
|
|
17
|
+
return differenceMinutes >= updateTime
|
|
18
|
+
}
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const requestData = async (cityInput: string): Promise<AirQuality | null> => {
|
|
23
|
+
if (!TOKEN) {
|
|
24
|
+
console.warn('VITE_AQI_TOKEN no está definido en el entorno. Se omitirá la llamada a la API.')
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
const URL = `https://api.waqi.info/feed/${cityInput}/?token=${TOKEN}`
|
|
28
|
+
try {
|
|
29
|
+
const response = await axios.get<AirQuality>(URL)
|
|
30
|
+
if (response?.status === 200) {
|
|
31
|
+
return response.data
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error al obtener datos de AQI:', error)
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const processData = (response: AirQuality | null, cityInput: string): AqiData => {
|
|
41
|
+
let data: AqiData = { city: cityInput, station: "Estación desconocida" }
|
|
42
|
+
if (response?.status === API_RESPONSE_STATUS.OK) {
|
|
43
|
+
data = {
|
|
44
|
+
aqi: response.data.aqi,
|
|
45
|
+
city: cityInput,
|
|
46
|
+
station: response.data.city.name
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const getData = async (cityInput: string, updateTime: number): Promise<AqiData> => {
|
|
53
|
+
const storageData: StorageData | null = await localforage.getItem('aqi')
|
|
54
|
+
let data
|
|
55
|
+
if (!storageData?.item || storageData?.item.city !== cityInput || !storageData?.date || refetchData(new Date(storageData.date), updateTime)) {
|
|
56
|
+
data = await requestData(cityInput)
|
|
57
|
+
const processedData = processData(data, cityInput)
|
|
58
|
+
await localforage.setItem('aqi', { item: processedData, date: new Date() })
|
|
59
|
+
return processedData
|
|
60
|
+
} else {
|
|
61
|
+
data = storageData.item
|
|
62
|
+
return data
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
const API_RESPONSE_STATUS = {
|
|
1
|
+
export const API_RESPONSE_STATUS = {
|
|
4
2
|
OK: 'ok',
|
|
5
3
|
ERROR: 'error',
|
|
6
4
|
}
|
|
7
5
|
|
|
8
|
-
const assingAirQuality = (magnitude: number): { color: string; quality: string } => {
|
|
6
|
+
export const assingAirQuality = (magnitude: number): { color: string; quality: string } => {
|
|
9
7
|
let color = '#9D9F93'
|
|
10
8
|
let quality = ''
|
|
11
9
|
if (0 <= magnitude && magnitude <= 50) {
|
|
@@ -29,5 +27,3 @@ const assingAirQuality = (magnitude: number): { color: string; quality: string }
|
|
|
29
27
|
}
|
|
30
28
|
return { color, quality }
|
|
31
29
|
}
|
|
32
|
-
|
|
33
|
-
export { TOKEN, API_RESPONSE_STATUS, assingAirQuality }
|
package/tsconfig.json
CHANGED
|
@@ -1,37 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
]
|
|
30
|
-
},
|
|
31
|
-
"include": [
|
|
32
|
-
"./src/**/*"
|
|
33
|
-
],
|
|
34
|
-
"exclude": [
|
|
35
|
-
"./node_modules/**/*"
|
|
36
|
-
]
|
|
37
|
-
}
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es6",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"checkJs": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"emitDecoratorMetadata": true,
|
|
11
|
+
"experimentalDecorators": true,
|
|
12
|
+
"downlevelIteration": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noImplicitReturns": true,
|
|
17
|
+
"noImplicitAny": true,
|
|
18
|
+
"noImplicitThis": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"skipLibCheck": true,
|
|
23
|
+
"types": ["node"],
|
|
24
|
+
"typeRoots": ["./node_modules/@types", "./src"]
|
|
25
|
+
},
|
|
26
|
+
"include": ["./src/**/*"],
|
|
27
|
+
"exclude": ["./node_modules/**/*"]
|
|
28
|
+
}
|