@vixoniccom/menu-daily 0.1.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/assets/framed.ai +3330 -4
  3. package/assets/modern.ai +421 -0
  4. package/build.zip +0 -0
  5. package/configuration.json +207 -0
  6. package/icon.png +0 -0
  7. package/package.json +31 -0
  8. package/src/dataLoader.ts +168 -0
  9. package/src/global.d.ts +59 -0
  10. package/src/index.html +33 -0
  11. package/src/logger.ts +11 -0
  12. package/src/main.ts +47 -0
  13. package/src/scenes/App.tsx +103 -0
  14. package/src/scenes/components/FontLoader.tsx +52 -0
  15. package/src/scenes/components/FormattedText.tsx +56 -0
  16. package/src/scenes/components/Grid/Grid.tsx +161 -0
  17. package/src/scenes/components/Grid/GridItem.tsx +79 -0
  18. package/src/scenes/components/Grid/animation.ts +105 -0
  19. package/src/scenes/components/Grid/index.ts +2 -0
  20. package/src/scenes/components/MealContainer/components/OptionItem.tsx +25 -0
  21. package/src/scenes/components/MealContainer/components/Title/index.tsx +82 -0
  22. package/src/scenes/components/MealContainer/components/Title/styles/Framed.tsx +52 -0
  23. package/src/scenes/components/MealContainer/components/Title/styles/Modern.tsx +47 -0
  24. package/src/scenes/components/MealContainer/components/Title/styles/index.tsx +13 -0
  25. package/src/scenes/components/MealContainer/components/index.ts +2 -0
  26. package/src/scenes/components/MealContainer/index.tsx +59 -0
  27. package/src/static/menu-daily-example.xlsx +0 -0
  28. package/src/test/downloads/1234.ttf +0 -0
  29. package/src/test/downloads/background.jpg +0 -0
  30. package/src/test/downloads/futura-font.ttf +0 -0
  31. package/src/test/parameters.json +37 -0
  32. package/tsconfig.json +36 -0
  33. package/tslint.json +5 -0
@@ -0,0 +1,207 @@
1
+ {
2
+ "schema": [
3
+ {
4
+ "type": "group",
5
+ "id": "data",
6
+ "label": "Datos",
7
+ "items": [
8
+ {
9
+ "type": "text-input",
10
+ "id": "url",
11
+ "label": "Enlace",
12
+ "description": "Dirección donde se encuentran los datos en formato excel.",
13
+ "required": true
14
+ },
15
+ {
16
+ "type": "select-input",
17
+ "id": "pollingInterval",
18
+ "label": "Actualización",
19
+ "description": "Frecuencia de consulta de los datos.",
20
+ "items": [
21
+ {
22
+ "label": "5 minutos",
23
+ "value": 300000
24
+ },
25
+ {
26
+ "label": "15 minutos",
27
+ "value": 900000
28
+ },
29
+ {
30
+ "label": "30 minutos",
31
+ "value": 1.8e+6
32
+ },
33
+ {
34
+ "label": "1 hora",
35
+ "value": 3.6e+6
36
+ },
37
+ {
38
+ "label": "6 horas",
39
+ "value": 2.16e+7
40
+ },
41
+ {
42
+ "label": "12 horas",
43
+ "value": 4.32e+7
44
+ },
45
+ {
46
+ "label": "Diaria",
47
+ "value": 8.64e+7
48
+ }
49
+ ],
50
+ "defaultValue": 900000,
51
+ "required": true
52
+ },
53
+ {
54
+ "type": "text-input",
55
+ "id": "mealType",
56
+ "label": "Tipo de comida",
57
+ "description": "El nombre de la pestaña en el excel. No requerido."
58
+ },
59
+ {
60
+ "type": "text-input",
61
+ "id": "msj0",
62
+ "label": "Sin menú",
63
+ "description": "Mensaje cuando no hay menu."
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "type": "group",
69
+ "id": "design",
70
+ "label": "Apariencia",
71
+ "items": [
72
+ {
73
+ "type": "label",
74
+ "label": "Animación"
75
+ },
76
+ {
77
+ "type": "number-input",
78
+ "id": "animationDuration",
79
+ "label": "Duración",
80
+ "description": "En segundos. Por defecto 15s."
81
+ },
82
+ {
83
+ "type": "select-input",
84
+ "id": "animationMode",
85
+ "label": "Modo",
86
+ "items": [
87
+ {
88
+ "label": "Fundido",
89
+ "value": "fade"
90
+ },
91
+ {
92
+ "label": "Izquieda-derecha",
93
+ "value": "slideRight"
94
+ },
95
+ {
96
+ "label": "Derecha-izquierda",
97
+ "value": "slideLeft"
98
+ }
99
+ ],
100
+ "defaultValue": "fade"
101
+ },
102
+ {
103
+ "type": "label",
104
+ "label": "Contenedor General"
105
+ },
106
+ {
107
+ "type": "number-input",
108
+ "id": "containerGridColumns",
109
+ "label": "Columnas",
110
+ "range": "[1:99]"
111
+ },
112
+ {
113
+ "type": "number-input",
114
+ "id": "containerGridColumnsGap",
115
+ "label": "Separación",
116
+ "description": "En porcentaje",
117
+ "range": "[0:100]"
118
+ },
119
+ {
120
+ "type": "number-input",
121
+ "id": "containerGridRows",
122
+ "label": "Filas",
123
+ "range": "[1:99]"
124
+ },
125
+ {
126
+ "type": "number-input",
127
+ "id": "containerGridRowsGap",
128
+ "label": "Separación",
129
+ "description": "En porcentaje",
130
+ "range": "[0:100]"
131
+ },
132
+ {
133
+ "type": "text-input",
134
+ "id": "containerGridMargins",
135
+ "label": "Margenes",
136
+ "description": "En formato CSS."
137
+ },
138
+ {
139
+ "type": "select-asset-kna-input",
140
+ "id": "backgroundImage",
141
+ "label": "Imagen de fondo"
142
+ },
143
+ {
144
+ "type": "label",
145
+ "label": "Contenedor de comida"
146
+ },
147
+ {
148
+ "type": "number-input",
149
+ "id": "itemGridRows",
150
+ "label": "Filas"
151
+ },
152
+ {
153
+ "type": "text-input",
154
+ "id": "itemGridMargins",
155
+ "label": "Margenes",
156
+ "description": "En formato CSS."
157
+ },
158
+ {
159
+ "type": "number-input",
160
+ "id": "itemAnimationDuration",
161
+ "label": "Duración de animación",
162
+ "description": "En segundos. Por defecto calcula automaticamente."
163
+ },
164
+ {
165
+ "type": "label",
166
+ "label": "Comida"
167
+ },
168
+ {
169
+ "type": "text-format",
170
+ "id": "itemTitleTextFormat",
171
+ "label": "Formato del tíltulo"
172
+ },
173
+ {
174
+ "type": "color-picker",
175
+ "id": "itemTitleBackgroundColor",
176
+ "label": "Fondo",
177
+ "description": "Color de fondo del título"
178
+ },
179
+ {
180
+ "type": "text-format",
181
+ "id": "itemOptionsTextFormat",
182
+ "label": "Formato de las opciones"
183
+ },
184
+ {
185
+ "type": "select-input",
186
+ "id": "itemStyle",
187
+ "label": "Estilo",
188
+ "items": [
189
+ {
190
+ "label": "Estandard",
191
+ "value": "standard"
192
+ },
193
+ {
194
+ "label": "Moderno",
195
+ "value": "modern"
196
+ },
197
+ {
198
+ "label": "Enmarcado",
199
+ "value": "framed"
200
+ }
201
+ ],
202
+ "defaultValue": "standard"
203
+ }
204
+ ]
205
+ }
206
+ ]
207
+ }
package/icon.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@vixoniccom/menu-daily",
3
+ "alias": "Menú diario",
4
+ "description": "App que muestra el menú del día.",
5
+ "color": "#3395FF",
6
+ "tags": [
7
+ "menu",
8
+ "app"
9
+ ],
10
+ "author": {
11
+ "name": ""
12
+ },
13
+ "version": "0.1.0",
14
+ "scripts": {
15
+ "prepublish": "vixonic-module-packager --mode build",
16
+ "watch": "vixonic-module-packager --mode watch",
17
+ "run": "vixonic-module-packager --mode run"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^8.0.14",
21
+ "@types/react": "^15.0.38",
22
+ "@types/react-dom": "^15.5.1"
23
+ },
24
+ "dependencies": {
25
+ "animejs": "^2.0.2",
26
+ "localforage": "^1.5.0",
27
+ "react": "^15.6.1",
28
+ "react-dom": "^15.6.1",
29
+ "xlsx": "^0.10.8"
30
+ }
31
+ }
@@ -0,0 +1,168 @@
1
+ import xlsx from 'xlsx'
2
+ import localforage from 'localforage'
3
+ import { logger } from './logger'
4
+
5
+ const getStoreKey = (id: string) => (`urlrequest-menu:${id}`)
6
+
7
+ export class DataLoader {
8
+ onUpdate?: (data: MenuData) => void
9
+ onError?: (e: Error) => void
10
+
11
+ private _url: string | undefined
12
+ get url (): string | undefined { return this._url }
13
+
14
+ private _data: MenuData | undefined
15
+ get data (): MenuData | undefined { return this._data }
16
+
17
+ private _mealType?: string
18
+ private _pollingInterval: number = 60000
19
+ private _pollingTimer: number | undefined
20
+
21
+ private _req: XMLHttpRequest | undefined
22
+
23
+ private _started: boolean = false
24
+
25
+ constructor (url?: string, interval?: number, mealType?: string) {
26
+ this.setup(url, interval, mealType)
27
+ }
28
+
29
+ setup (url?: string, interval: number = 300000, mealType?: string) {
30
+ if (!url) {
31
+ this.stop()
32
+ return
33
+ }
34
+
35
+ this._pollingInterval = interval >= 60000 ? interval : 60000
36
+ this._mealType = mealType
37
+ if (this._url !== url) {
38
+ this.stop()
39
+ this._url = url
40
+ // Get cache data
41
+ if (!this._data) {
42
+ localforage.getItem<CacheData>(getStoreKey(this._url)).then(
43
+ (data) => {
44
+ if (!data || !data.updateTime || !data.menuData) {
45
+ this.startPolling()
46
+ } else {
47
+ logger.log('Got cached data!', data.updateTime)
48
+ this.onUpdate && this.onUpdate(data.menuData)
49
+ let timeDiff = Date.now() - data.updateTime
50
+ if (timeDiff >= this._pollingInterval) {
51
+ this.startPolling()
52
+ } else {
53
+ this.startPollingTimer(this._pollingInterval - timeDiff)
54
+ }
55
+ }
56
+ },
57
+ (_e) => {
58
+ this.startPolling()
59
+ }
60
+ )
61
+ } else {
62
+ this.startPolling()
63
+ }
64
+ }
65
+ }
66
+
67
+ private startPolling () {
68
+ if (this._started) return
69
+ if (!this._url) return
70
+ this._started = true
71
+ this.load()
72
+ }
73
+
74
+ private stop (): void {
75
+ window.clearTimeout(this._pollingTimer)
76
+ this._started = false
77
+ if (this._req) this._req.abort()
78
+ }
79
+
80
+ private load (): void {
81
+ if (!this._url) return
82
+
83
+ this._req = new XMLHttpRequest()
84
+ this._req.open('GET', this._url, true)
85
+ this._req.responseType = 'arraybuffer'
86
+
87
+ this._req.onload = this.onLoad.bind(this)
88
+
89
+ this._req.onerror = (e) => {
90
+ if (e) {
91
+ this.onError && this.onError(e as any)
92
+ this.startPollingTimer(30000)
93
+ }
94
+ }
95
+
96
+ this._req.send()
97
+ }
98
+
99
+ private onLoad (_ev: Event) {
100
+ if (!(this._req && this._url)) return
101
+
102
+ logger.log('Data loaded', Date.now())
103
+ let arraybuffer = this._req.response
104
+
105
+ /* convert data to binary string */
106
+ let data = new Uint8Array(arraybuffer)
107
+ let arr = new Array()
108
+ for (let i = 0; i !== data.length; ++i) arr[i] = String.fromCharCode(data[i])
109
+ let bstr = arr.join('')
110
+
111
+ /* Call XLSX */
112
+ let workbook = xlsx.read(bstr, { type: 'binary' })
113
+ this._pollingTimer = window.setTimeout(() => { this.load() }, this._pollingInterval)
114
+
115
+ let menuData = this.parseData(workbook)
116
+ if (menuData) {
117
+ this._data = menuData
118
+ // Save cache
119
+ localforage.setItem<CacheData>(getStoreKey(this._url), {
120
+ updateTime: Date.now(),
121
+ menuData: this._data
122
+ })
123
+ // Dispatch update
124
+ this.onUpdate && this.onUpdate(this._data)
125
+ }
126
+ }
127
+
128
+ private startPollingTimer (interval?: number) {
129
+ if (this._pollingTimer) window.clearTimeout(this._pollingInterval)
130
+ this._pollingTimer = window.setTimeout(() => { this.load() }, interval || this._pollingInterval)
131
+ }
132
+
133
+ private parseData (workbook: xlsx.WorkBook): MenuData | undefined {
134
+ try {
135
+ if (this._mealType) workbook.SheetNames.find((name) => name.toUpperCase() === this._mealType!.toUpperCase())
136
+ let menuData: MenuData = workbook.SheetNames.reduce((menu: MenuData, name) => {
137
+ let newMenu = {...menu}
138
+ let dailyMenu = this.parseSheet(workbook.Sheets[name])
139
+ if (dailyMenu) newMenu[name.toUpperCase()] = dailyMenu
140
+ return newMenu
141
+ }, {})
142
+ logger.log(menuData)
143
+ return menuData
144
+ } catch (e) {
145
+ this.onError && this.onError(e)
146
+ return undefined
147
+ }
148
+ }
149
+
150
+ private parseSheet (sheet: xlsx.WorkSheet): MenuByDate | undefined {
151
+ if (!sheet) return undefined
152
+ let meals: MenuByDate | {} = xlsx.utils.sheet_to_json(sheet, {raw: true}).reduce((menu: MenuByDate, row: any) => {
153
+ let newMenu = {...menu}
154
+ let keys = Object.keys(row)
155
+ let date: {d: number, m: number, y: number} = xlsx.SSF.parse_date_code(row[keys[0]] as any)
156
+ if (date.d && date.m && date.y) {
157
+ newMenu[new Date(date.y,date.m - 1,date.d).getTime()] = keys.slice(1).map((key) => {
158
+ return {
159
+ type: key,
160
+ options: row[key] && row[key].split(';') || []
161
+ } as Meal
162
+ })
163
+ }
164
+ return newMenu
165
+ }, {})
166
+ return meals
167
+ }
168
+ }
@@ -0,0 +1,59 @@
1
+ declare type VixonicData = {
2
+ downloadsPath: string
3
+ parameters: VixonicParameters
4
+ }
5
+
6
+ declare type VixonicFile = Partial<{
7
+ filename: string
8
+ }>
9
+
10
+ declare type VixonicTextFormat = Partial<{
11
+ fontSize: number,
12
+ fontColor: string,
13
+ alignment: {
14
+ horizontal: 'left' | 'right' | 'center'
15
+ }
16
+ font: VixonicFile
17
+ }>
18
+
19
+ declare type VixonicParameters = Partial<{
20
+ url: string
21
+ pollingInterval: number
22
+ mealType: string
23
+ animationDuration: number
24
+ animationMode: 'fade' | 'slideLeft' | 'slideRight'
25
+ containerGridColumns: number
26
+ containerGridColumnsGap: number
27
+ containerGridRows: number
28
+ containerGridRowsGap: number
29
+ containerGridMargins: number
30
+ backgroundImage: VixonicFile
31
+ msj0: string
32
+ itemStyle: 'standard' | 'modern' | 'framed'
33
+ itemTitleTextFormat: VixonicTextFormat
34
+ itemTitleBackgroundColor: string
35
+ itemAnimationDuration: number
36
+ itemGridMargins: string
37
+ itemGridRows: number
38
+ itemOptionsTextFormat: VixonicTextFormat
39
+ }>
40
+
41
+ declare type CacheData = {
42
+ updateTime: number
43
+ menuData: MenuData
44
+ }
45
+
46
+ declare type MenuData = {
47
+ [type: string]: MenuByDate
48
+ }
49
+
50
+ declare type MenuByDate = {
51
+ [date: number]: Menu
52
+ }
53
+
54
+ declare type Menu = Meal[]
55
+
56
+ declare type Meal = {
57
+ type: string
58
+ options: string[]
59
+ }
package/src/index.html ADDED
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title></title>
5
+ <meta charset="utf-8">
6
+ <style>
7
+ @font-face {
8
+ font-family: 'FrutigerLTStdBoldCn';
9
+ src: url('./static/fonts/FrutigerLTStdBoldCn.ttf');
10
+ }
11
+
12
+ @font-face {
13
+ font-family: 'FrutigerLTStdCn';
14
+ src: url('./static/fonts/FrutigerLTStdCn.otf');
15
+ }
16
+
17
+ @font-face {
18
+ font-family: 'FrutigerLTStdBlackCn';
19
+ src: url('./static/fonts/FrutigerLTStdBlackCn.ttf');
20
+ }
21
+
22
+ @font-face {
23
+ font-family: 'FrutigerLTStdLightCn';
24
+ src: url('./static/fonts/FrutigerLTStdLightCn.ttf');
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div id="root" style="position: absolute; top: 0; left: 0;
30
+ bottom: 0; right: 0; display: flex; justify-content: center;
31
+ align-items: center; overflow: hidden;"></div>
32
+ </body>
33
+ </html>
package/src/logger.ts ADDED
@@ -0,0 +1,11 @@
1
+ export const LOGGER_PROPERTY = 'vxAppLogger'
2
+ class Logger {
3
+ log (message: any, ...optionalParams: any[]) {
4
+ if ((window as any)[LOGGER_PROPERTY] === true) {
5
+ if (optionalParams && optionalParams.length > 0) console.log(message, optionalParams)
6
+ else console.log(message)
7
+ }
8
+ }
9
+ }
10
+
11
+ export let logger = new Logger()
package/src/main.ts ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+ import React from 'react'
3
+ import ReactDOM from 'react-dom'
4
+
5
+ import App, { AppProps } from './scenes/App'
6
+ import { logger } from './logger'
7
+
8
+ let { ipcRenderer } = require('electron')
9
+
10
+ let start = false
11
+ let vixonicData: VixonicData
12
+
13
+ type VixonicData = {
14
+ downloadsPath: string,
15
+ parameters: VixonicParameters
16
+ }
17
+
18
+ ipcRenderer.on('preload', (_event: any, _data: VixonicData) => {
19
+ // Preload command
20
+ logger.log('Preload', _data)
21
+ vixonicData = _data
22
+ doRender()
23
+ })
24
+
25
+ ipcRenderer.on('start', (_event: any, _data: VixonicData) => {
26
+ // Start command
27
+ logger.log('Start', _data)
28
+ vixonicData = _data
29
+ start = true
30
+ doRender()
31
+ })
32
+
33
+ ipcRenderer.on('update', (_event: any, _data: VixonicData) => {
34
+ // Update command
35
+ logger.log('Update', _data)
36
+ vixonicData = _data
37
+ doRender()
38
+ })
39
+
40
+ ipcRenderer.on('finish', (_event: any) => {
41
+ // Finish command.
42
+ })
43
+
44
+ function doRender () {
45
+ if (!vixonicData) return
46
+ ReactDOM.render(React.createElement<AppProps>(App as any, { start, vixonicData }), document.getElementById('root'))
47
+ }
@@ -0,0 +1,103 @@
1
+ import * as React from 'react'
2
+
3
+ import { DataLoader } from '../dataLoader'
4
+ import Grid from './components/Grid'
5
+ import MealContainer from './components/MealContainer'
6
+ import FontLoader, { fontParser } from './components/FontLoader'
7
+ import FormattedText from './components/FormattedText'
8
+
9
+ export type AppProps = {
10
+ start: boolean,
11
+ vixonicData: VixonicData
12
+ }
13
+
14
+ export type AppState = {
15
+ loading: boolean
16
+ updateTime?: number
17
+ data?: Menu
18
+ }
19
+
20
+ class App extends React.Component<AppProps, AppState> {
21
+ private readonly dataLoader: DataLoader
22
+
23
+ constructor (props: AppProps) {
24
+ super(props)
25
+ let { parameters } = props.vixonicData
26
+ this.state = {
27
+ loading: true
28
+ }
29
+ this.dataLoader = new DataLoader(parameters.url, parameters.pollingInterval)
30
+ }
31
+
32
+ componentDidMount () {
33
+ this.dataLoader.onUpdate = (data) => {
34
+ const { mealType } = this.props.vixonicData.parameters
35
+ const today = new Date()
36
+ today.setHours(0,0,0,0)
37
+ const menu = mealType && data[mealType.toUpperCase()] || data[Object.keys(data)[0]]
38
+ this.setState({loading: false, data: menu[today.getTime()]})
39
+ }
40
+
41
+ this.dataLoader.onError = (e) => {
42
+ console.error(e)
43
+ }
44
+ }
45
+
46
+ componentWillReceiveProps (nextProps: AppProps) {
47
+ if (this.props.vixonicData.parameters.url !== nextProps.vixonicData.parameters.url) {
48
+ let { parameters } = nextProps.vixonicData
49
+ parameters.url && this.dataLoader.setup(parameters.url, parameters.pollingInterval)
50
+ }
51
+ }
52
+
53
+ render () {
54
+ const { parameters, downloadsPath } = this.props.vixonicData
55
+ let cycle = parameters.animationDuration && parameters.animationDuration * 1000 || 15000
56
+ return <div style={{
57
+ position: 'absolute',
58
+ top: 0, right: 0, bottom: 0, left: 0,
59
+ backgroundImage: parameters.backgroundImage &&
60
+ `url("${downloadsPath + '/' + parameters.backgroundImage.filename}")` || undefined,
61
+ backgroundSize: '100% 100%',
62
+ padding: parameters.containerGridMargins
63
+ }}>
64
+ {
65
+ this.state.loading || !this.state.data || !this.state.data[0] ?
66
+ <div style={{width: '100%', height: '100%', display: 'flex', fontSize: `${100 / (parameters.itemGridRows || 1)}vmin`, justifyContent: 'center', alignItems: 'center'}}>
67
+ <FormattedText
68
+ text={this.state.loading ? 'Cargando...' : parameters.msj0 || 'No hay menu.'}
69
+ downloadsPath={downloadsPath}
70
+ defaults={{
71
+ alignment: 'center',
72
+ fontColor: 'black',
73
+ fontSize: 10
74
+ }}
75
+ style={{width: '100%'}}
76
+ format={parameters.itemOptionsTextFormat || {}}
77
+ unit='%'/>
78
+ </div> :
79
+ <Grid
80
+ id='container'
81
+ animate={this.props.start}
82
+ style={{width: '100%', height: '100%'}}
83
+ items={this.state.data.map((meal) => (<MealContainer data={meal} interval={cycle} vixonicData={this.props.vixonicData} />))}
84
+ animation={{
85
+ mode: parameters.animationMode || 'fade',
86
+ duration: cycle
87
+ }} layout={{
88
+ alignment: {
89
+ h: 'start',
90
+ v: 'center'
91
+ },
92
+ columns: parameters.containerGridColumns || 1,
93
+ columnsGap: parameters.containerGridColumnsGap || 0,
94
+ rows: parameters.containerGridRows || 1,
95
+ rowsGap: parameters.containerGridRowsGap || 0
96
+ }}/>
97
+ }
98
+ <FontLoader downloadPath={downloadsPath} fonts={fontParser(parameters)}/>
99
+ </div>
100
+ }
101
+ }
102
+
103
+ export default App