@vixoniccom/footbal-score 1.4.6-dev.0 → 1.4.6-dev.10
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 +25 -0
- package/build/main.js +1 -1
- package/build/main.js.LICENSE.txt +0 -7
- package/build/test/parameters.json +2 -2
- package/build/test/services.json +109 -0
- package/build.zip +0 -0
- package/configuration/dataGroup/dataInputs.ts +5 -44
- package/configuration.json +4 -282
- package/package.json +2 -2
- package/src/App.tsx +62 -160
- package/src/components/Table/components/Results.tsx +2 -2
- package/src/components/Table/components/Row.tsx +2 -1
- package/src/components/Table/components/Time.tsx +4 -3
- package/src/components/Table/index.tsx +7 -1
- package/src/football-loader.ts +27 -0
- package/src/globals.d.ts +28 -0
- package/src/helpers/getFixture.ts +1 -44
- package/src/main.ts +39 -10
- package/src/parameters.d.ts +1 -1
- package/src/test/parameters.json +2 -2
- package/src/test/services.json +109 -0
- package/src/helpers/mockFixtures.ts +0 -74
package/src/App.tsx
CHANGED
|
@@ -1,183 +1,85 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import moment from 'moment'
|
|
3
3
|
import { FontLoader, FormattedText, Table } from './components'
|
|
4
|
-
import { getFixture } from './helpers/getFixture'
|
|
5
4
|
|
|
6
5
|
export type Props = {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
appData?: VixonicData
|
|
7
|
+
fixtures: FootballFixture[]
|
|
8
|
+
serviceError?: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
export const App: React.FunctionComponent<Props> = ({ appData, fixtures }) => {
|
|
12
|
+
if (!appData) return null
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const { parameters, downloadsPath } = appData
|
|
15
|
+
const { backgroundImage, padding, msj0 = '', matchesYesterday, matchesTomorrow } = parameters
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
console.log(`[⚽ football-score ${new Date().toLocaleTimeString()}]`, msg, ...args)
|
|
17
|
+
console.log('[App] fixtures received:', fixtures.length, fixtures)
|
|
18
18
|
|
|
19
|
-
// Returns the timestamp (ms) when the next fetch should happen based on fixture states.
|
|
20
|
-
// Persisted in localStorage so re-mounts don't reset the schedule.
|
|
21
|
-
function computeNextFetchTime(fixtures: any[], pollIntervalMs: number): number {
|
|
22
|
-
const now = Date.now()
|
|
23
|
-
|
|
24
|
-
const liveMatches = fixtures.filter(f => LIVE_STATUSES.includes(f.fixture?.status?.short))
|
|
25
|
-
if (liveMatches.length > 0) {
|
|
26
|
-
const next = new Date(now + pollIntervalMs)
|
|
27
|
-
log(`🟢 ${liveMatches.length} partido(s) en vivo → próximo fetch en ${next.toLocaleTimeString()} (${pollIntervalMs / 1000}s)`)
|
|
28
|
-
return now + pollIntervalMs
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const nextMatchAt = fixtures
|
|
32
|
-
.filter(f => f.fixture?.status?.short === 'NS')
|
|
33
|
-
.map(f => new Date(f.fixture.date).getTime())
|
|
34
|
-
.filter(t => t > now)
|
|
35
|
-
.sort((a, b) => a - b)[0]
|
|
36
|
-
|
|
37
|
-
if (nextMatchAt !== undefined) {
|
|
38
|
-
const wakeUp = nextMatchAt - PRE_MATCH_MS
|
|
39
|
-
if (wakeUp <= now) {
|
|
40
|
-
const next = new Date(now + pollIntervalMs)
|
|
41
|
-
log(`🟡 Dentro de ventana pre-partido → próximo fetch en ${next.toLocaleTimeString()}`)
|
|
42
|
-
return now + pollIntervalMs
|
|
43
|
-
}
|
|
44
|
-
log(`⏰ Próximo partido: ${new Date(nextMatchAt).toLocaleTimeString()} → fetch a las ${new Date(wakeUp).toLocaleTimeString()} (10 min antes)`)
|
|
45
|
-
return wakeUp
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const tomorrow = new Date()
|
|
49
|
-
tomorrow.setDate(tomorrow.getDate() + 1)
|
|
50
|
-
tomorrow.setHours(0, 15, 0, 0)
|
|
51
|
-
log(`💤 Sin partidos pendientes → próximo check: ${tomorrow.toLocaleString()}`)
|
|
52
|
-
return tomorrow.getTime()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const App: React.FunctionComponent<Props> = ({ data, start }) => {
|
|
56
|
-
const [dataFixture, setDataFixture] = useState<any[]>([])
|
|
57
|
-
const [leagueState, setLeagueState] = useState<string>('')
|
|
58
|
-
const { parameters, downloadsPath } = data
|
|
59
|
-
const { backgroundImage, padding, msj0 = '', league = '9' } = parameters
|
|
60
19
|
const backgroundImageState = backgroundImage?.filename
|
|
61
20
|
? `url("${downloadsPath}/${backgroundImage.filename}")`
|
|
62
21
|
: ''
|
|
63
22
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const paramsRef = useRef(parameters)
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
leagueRef.current = league
|
|
70
|
-
paramsRef.current = parameters
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const clearSchedule = () => {
|
|
74
|
-
if (timeoutRef.current !== null) {
|
|
75
|
-
clearTimeout(timeoutRef.current)
|
|
76
|
-
timeoutRef.current = null
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const fetchAndSchedule = async () => {
|
|
81
|
-
const { matchesYesterday, matchesTomorrow, updateTime: ut = 300000 } = paramsRef.current
|
|
82
|
-
const cached: any = await localforage.getItem('football-score')
|
|
23
|
+
const serviceUtcOffset = fixtures[0]
|
|
24
|
+
? moment.parseZone(fixtures[0].fixture.date).utcOffset()
|
|
25
|
+
: 0
|
|
83
26
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
fixtures = await getFixture(leagueRef.current, matchesYesterday, matchesTomorrow)
|
|
88
|
-
log(`✅ ${fixtures.length} fixture(s) recibidos`)
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error('[football-score] API error:', err)
|
|
91
|
-
fixtures = cached?.data ?? []
|
|
92
|
-
log(`❌ API falló → usando cache (${fixtures.length} fixtures)`)
|
|
93
|
-
}
|
|
27
|
+
const today = moment().utcOffset(serviceUtcOffset).format('YYYY-MM-DD')
|
|
28
|
+
const yesterdayStr = moment().utcOffset(serviceUtcOffset).subtract(1, 'days').format('YYYY-MM-DD')
|
|
29
|
+
const tomorrowStr = moment().utcOffset(serviceUtcOffset).add(1, 'days').format('YYYY-MM-DD')
|
|
94
30
|
|
|
95
|
-
|
|
31
|
+
console.log('[App] date context — today:', today, 'yesterday:', yesterdayStr, 'tomorrow:', tomorrowStr, 'utcOffset:', serviceUtcOffset)
|
|
32
|
+
console.log('[App] filter config — matchesYesterday:', matchesYesterday, 'matchesTomorrow:', matchesTomorrow)
|
|
96
33
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
clearSchedule()
|
|
106
|
-
timeoutRef.current = setTimeout(fetchAndSchedule, Math.max(0, nextFetchTime - Date.now()))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const init = async () => {
|
|
110
|
-
const cached: any = await localforage.getItem('football-score')
|
|
111
|
-
const now = Date.now()
|
|
112
|
-
|
|
113
|
-
if (!cached?.data) {
|
|
114
|
-
log(`🆕 Sin cache → fetch inmediato`)
|
|
115
|
-
fetchAndSchedule()
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
setDataFixture(cached.data)
|
|
120
|
-
|
|
121
|
-
if (!cached.nextFetchTime || cached.nextFetchTime <= now) {
|
|
122
|
-
log(`🔁 Mount check: nextFetchTime ya pasó (${cached.nextFetchTime ? new Date(cached.nextFetchTime).toLocaleTimeString() : 'null'}) → fetch inmediato`)
|
|
123
|
-
fetchAndSchedule()
|
|
124
|
-
} else {
|
|
125
|
-
const mins = Math.round((cached.nextFetchTime - now) / 60000)
|
|
126
|
-
log(`💾 Mount check: usando cache → próximo fetch en ${new Date(cached.nextFetchTime).toLocaleTimeString()} (~${mins} min)`)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (!start) return
|
|
132
|
-
setLeagueState(league)
|
|
133
|
-
init()
|
|
134
|
-
return clearSchedule
|
|
135
|
-
}, [data])
|
|
34
|
+
const filteredFixtures = fixtures.filter(f => {
|
|
35
|
+
const fixtureDate = moment.parseZone(f.fixture.date).format('YYYY-MM-DD')
|
|
36
|
+
if (fixtureDate === today) return true
|
|
37
|
+
if (fixtureDate === yesterdayStr && matchesYesterday) return true
|
|
38
|
+
if (fixtureDate === tomorrowStr && matchesTomorrow) return true
|
|
39
|
+
return false
|
|
40
|
+
})
|
|
136
41
|
|
|
137
|
-
|
|
138
|
-
if (leagueState === '' || leagueState === league) return
|
|
139
|
-
setLeagueState(league)
|
|
140
|
-
clearSchedule()
|
|
141
|
-
localforage.clear()
|
|
142
|
-
fetchAndSchedule()
|
|
143
|
-
}, [league])
|
|
42
|
+
console.log('[App] filteredFixtures count:', filteredFixtures.length)
|
|
144
43
|
|
|
145
44
|
return (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
45
|
+
<>
|
|
46
|
+
{filteredFixtures.length > 0 ?
|
|
47
|
+
<div style={{
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
top: 0,
|
|
50
|
+
bottom: 0,
|
|
51
|
+
left: 0,
|
|
52
|
+
right: 0,
|
|
53
|
+
backgroundImage: backgroundImageState,
|
|
54
|
+
backgroundSize: '100% 100%',
|
|
55
|
+
padding: padding
|
|
56
|
+
}}>
|
|
57
|
+
<FontLoader paths={['nameFormat.font', 'statusFormat.font', 'timeFormat.font', 'titleFormat.font', 'descriptionFormat.font', 'dateDayFormat.font', 'dateMonthFormat.font', 'goalsFormat.font']} parameters={parameters} downloadsPath={downloadsPath} />
|
|
58
|
+
{parameters.titleEnabled &&
|
|
59
|
+
<div style={{ display: 'flex', position: 'relative', flex: '1 1 0%', flexDirection: 'column' }}>
|
|
60
|
+
<FormattedText text={parameters.title0 || 'Partidos Mundial'} format={parameters.titleFormat} />
|
|
61
|
+
</div>
|
|
62
|
+
}
|
|
63
|
+
<Table dataFixture={filteredFixtures} parameters={parameters} />
|
|
64
|
+
</div> :
|
|
65
|
+
<div style={{
|
|
66
|
+
position: 'absolute',
|
|
67
|
+
top: 0,
|
|
68
|
+
bottom: 0,
|
|
69
|
+
left: 0,
|
|
70
|
+
right: 0,
|
|
71
|
+
backgroundImage: backgroundImageState,
|
|
72
|
+
backgroundSize: '100% 100%',
|
|
73
|
+
padding: padding,
|
|
74
|
+
display: 'flex',
|
|
75
|
+
alignItems: parameters.alignVerticalMsj
|
|
76
|
+
}}>
|
|
77
|
+
<FontLoader paths={['nameFormat.font', 'descriptionFormat.font', 'dateDayFormat.font', 'dateMonthFormat.font', 'formatMjs.font']} parameters={parameters} downloadsPath={downloadsPath} />
|
|
159
78
|
<div style={{ display: 'flex', position: 'relative', flex: '1 1 0%', flexDirection: 'column' }}>
|
|
160
|
-
<FormattedText text={
|
|
79
|
+
<FormattedText text={msj0 || 'Sin partidos'} format={parameters.formatMjs} />
|
|
161
80
|
</div>
|
|
162
|
-
}
|
|
163
|
-
<Table dataFixture={dataFixture} parameters={parameters} />
|
|
164
|
-
</div> :
|
|
165
|
-
<div style={{
|
|
166
|
-
position: 'absolute',
|
|
167
|
-
top: 0,
|
|
168
|
-
bottom: 0,
|
|
169
|
-
left: 0,
|
|
170
|
-
right: 0,
|
|
171
|
-
backgroundImage: backgroundImageState,
|
|
172
|
-
backgroundSize: '100% 100%',
|
|
173
|
-
padding: padding,
|
|
174
|
-
display: 'flex',
|
|
175
|
-
alignItems: parameters.alignVerticalMsj
|
|
176
|
-
}}>
|
|
177
|
-
<FontLoader paths={['nameFormat.font', 'descriptionFormat.font', 'dateDayFormat.font', 'dateMonthFormat.font', 'formatMjs.font']} parameters={parameters} downloadsPath={downloadsPath} />
|
|
178
|
-
<div style={{ display: 'flex', position: 'relative', flex: '1 1 0%', flexDirection: 'column' }}>
|
|
179
|
-
<FormattedText text={msj0 || 'Sin partidos'} format={parameters.formatMjs} />
|
|
180
81
|
</div>
|
|
181
|
-
|
|
82
|
+
}
|
|
83
|
+
</>
|
|
182
84
|
)
|
|
183
85
|
}
|
|
@@ -11,11 +11,11 @@ interface Props {
|
|
|
11
11
|
export const Results: React.FunctionComponent<Props> = ({ parameters, goals, score }) => {
|
|
12
12
|
return (
|
|
13
13
|
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
|
14
|
-
{isNumber(score.penalty
|
|
14
|
+
{isNumber(score.penalty?.home) ? <FormattedText text={`(${score.penalty.home})`} format={parameters.goalsFormat} /> : ''}
|
|
15
15
|
<FormattedText text={goals.home ? goals.home : 0} format={parameters.goalsFormat} />
|
|
16
16
|
<FormattedText text={'-'} format={parameters.goalsFormat} />
|
|
17
17
|
<FormattedText text={goals.away ? goals.away : 0} format={parameters.goalsFormat} />
|
|
18
|
-
{isNumber(score.penalty
|
|
18
|
+
{isNumber(score.penalty?.away) ? <FormattedText text={`(${score.penalty.away})`} format={parameters.goalsFormat} /> : ''}
|
|
19
19
|
</div>
|
|
20
20
|
)
|
|
21
21
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import moment from 'moment'
|
|
1
2
|
import React, { useEffect, useState } from 'react'
|
|
2
3
|
import { Results, Status, Time } from './'
|
|
3
4
|
import { FormattedText } from '../../'
|
|
@@ -71,7 +72,7 @@ export const Row: React.FunctionComponent<Props> = ({ item, parameters }) => {
|
|
|
71
72
|
justifyContent: 'center'
|
|
72
73
|
}}>
|
|
73
74
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
74
|
-
<Time time={
|
|
75
|
+
<Time time={moment.parseZone(fixture.date).unix()} utcOffset={moment.parseZone(fixture.date).utcOffset()} parameters={parameters} />
|
|
75
76
|
</div>
|
|
76
77
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
77
78
|
<Status status={fixture.status.short} parameters={parameters} />
|
|
@@ -4,14 +4,15 @@ import { FormattedText } from '../../'
|
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
time: number
|
|
7
|
+
utcOffset: number
|
|
7
8
|
parameters: VixonicParameters
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export const Time: React.FunctionComponent<Props> = ({ time, parameters }) => {
|
|
11
|
+
export const Time: React.FunctionComponent<Props> = ({ time, utcOffset, parameters }) => {
|
|
11
12
|
return (
|
|
12
13
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
|
13
|
-
<FormattedText text={moment.unix(time).format('DD-MM')} format={parameters.timeFormat} />
|
|
14
|
-
<FormattedText text={moment.unix(time).format('h:mm a')} format={parameters.timeFormat} />
|
|
14
|
+
<FormattedText text={moment.unix(time).utcOffset(utcOffset).format('DD-MM')} format={parameters.timeFormat} />
|
|
15
|
+
<FormattedText text={moment.unix(time).utcOffset(utcOffset).format('h:mm a')} format={parameters.timeFormat} />
|
|
15
16
|
</div>
|
|
16
17
|
)
|
|
17
18
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import moment from 'moment'
|
|
1
2
|
import React, { useEffect, useState } from 'react'
|
|
2
3
|
import { Row } from './components'
|
|
3
4
|
|
|
@@ -9,11 +10,16 @@ type Props = {
|
|
|
9
10
|
export const Table: React.FunctionComponent<Props> = ({ dataFixture, parameters }) => {
|
|
10
11
|
const { itemsPerPage, durationPerPage } = parameters
|
|
11
12
|
const rows = Number(itemsPerPage || 4)
|
|
12
|
-
const orderedData = dataFixture.sort((a, b) => a.fixture.
|
|
13
|
+
const orderedData = dataFixture.sort((a, b) => moment.parseZone(a.fixture.date).unix() - moment.parseZone(b.fixture.date).unix())
|
|
13
14
|
const [page, setPage] = useState<number>(1)
|
|
14
15
|
const [items, setItems] = useState<string[][]>(orderedData.slice(0, rows))
|
|
15
16
|
const total = orderedData.length
|
|
16
17
|
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
setPage(1)
|
|
20
|
+
setItems(orderedData.slice(0, rows))
|
|
21
|
+
}, [dataFixture])
|
|
22
|
+
|
|
17
23
|
useEffect(() => {
|
|
18
24
|
const timer = setTimeout(() => {
|
|
19
25
|
handlePagination()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class FootballLoader {
|
|
2
|
+
public onData?: (fixtures: FootballFixture[]) => void = undefined
|
|
3
|
+
public onError?: (error: string) => void = undefined
|
|
4
|
+
|
|
5
|
+
update(appData: VixonicData) {
|
|
6
|
+
const serviceId = appData.parameters.footballService?.id
|
|
7
|
+
console.log('[FootballLoader] serviceId:', serviceId)
|
|
8
|
+
|
|
9
|
+
if (!serviceId || !appData.services[serviceId]) {
|
|
10
|
+
console.log('[FootballLoader] service not found in appData.services. Available keys:', Object.keys(appData.services))
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const rawData = appData.services[serviceId].data as FootballServiceData | undefined
|
|
15
|
+
console.log('[FootballLoader] rawData:', JSON.stringify(rawData, null, 2))
|
|
16
|
+
|
|
17
|
+
const fixtures = rawData?.fixtures ?? []
|
|
18
|
+
console.log('[FootballLoader] fixtures count:', fixtures.length)
|
|
19
|
+
if (fixtures.length > 0) {
|
|
20
|
+
console.log('[FootballLoader] first fixture:', JSON.stringify(fixtures[0], null, 2))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.onData?.(fixtures)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
stop() {}
|
|
27
|
+
}
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
declare interface FootballFixture {
|
|
2
|
+
fixture: {
|
|
3
|
+
id: number
|
|
4
|
+
date: string
|
|
5
|
+
status: { long: string; short: string; elapsed: number | null }
|
|
6
|
+
venue: { name: string; city: string }
|
|
7
|
+
}
|
|
8
|
+
league: {
|
|
9
|
+
id: number
|
|
10
|
+
name: string
|
|
11
|
+
country: string
|
|
12
|
+
logo: string
|
|
13
|
+
round: string
|
|
14
|
+
}
|
|
15
|
+
teams: {
|
|
16
|
+
home: { id: number; name: string; logo: string; winner: boolean | null }
|
|
17
|
+
away: { id: number; name: string; logo: string; winner: boolean | null }
|
|
18
|
+
}
|
|
19
|
+
goals: { home: number | null; away: number | null }
|
|
20
|
+
score: {
|
|
21
|
+
halftime: { home: number | null; away: number | null }
|
|
22
|
+
fulltime: { home: number | null; away: number | null }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare interface FootballServiceData {
|
|
27
|
+
fixtures: FootballFixture[]
|
|
28
|
+
}
|
|
@@ -1,44 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import axios from 'axios'
|
|
3
|
-
import { mockFixtures } from './mockFixtures'
|
|
4
|
-
|
|
5
|
-
if (!process.env.FOOTBALL_API_KEY || !process.env.FOOTBALL_API_HOST || !process.env.FOOTBALL_API_URL)
|
|
6
|
-
console.warn('Missing API credentials')
|
|
7
|
-
const FOOTBALL_API_KEY = process.env.FOOTBALL_API_KEY
|
|
8
|
-
const FOOTBALL_API_HOST = process.env.FOOTBALL_API_HOST
|
|
9
|
-
const FOOTBALL_API_URL = process.env.FOOTBALL_API_URL
|
|
10
|
-
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000
|
|
11
|
-
|
|
12
|
-
const USE_MOCK = false
|
|
13
|
-
|
|
14
|
-
export const getFixture = async (league: string, yesterday?: boolean, tomorrow?: boolean) => {
|
|
15
|
-
if (USE_MOCK) return mockFixtures
|
|
16
|
-
|
|
17
|
-
const today = moment().format('YYYY-MM-DD')
|
|
18
|
-
const year = moment().format('YYYY')
|
|
19
|
-
const from = yesterday ? moment(Date.now() - MILLISECONDS_PER_DAY).format('YYYY-MM-DD') : today
|
|
20
|
-
const to = tomorrow ? moment(Date.now() + MILLISECONDS_PER_DAY).format('YYYY-MM-DD') : today
|
|
21
|
-
const timezone = (() => {
|
|
22
|
-
try {
|
|
23
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
24
|
-
return tz
|
|
25
|
-
} catch (error) {
|
|
26
|
-
return 'UTC'
|
|
27
|
-
}
|
|
28
|
-
})()
|
|
29
|
-
|
|
30
|
-
const options = {
|
|
31
|
-
method: 'GET',
|
|
32
|
-
url: FOOTBALL_API_URL,
|
|
33
|
-
params: { from, to, league, season: year, timezone },
|
|
34
|
-
headers: { 'X-RapidAPI-Key': FOOTBALL_API_KEY, 'X-RapidAPI-Host': FOOTBALL_API_HOST },
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const response = await axios.request(options)
|
|
39
|
-
return response.data.response
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error(error)
|
|
42
|
-
return []
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
export const resolvedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
package/src/main.ts
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
|
-
import ReactDOM from 'react-dom'
|
|
2
1
|
import React from 'react'
|
|
3
|
-
import
|
|
4
|
-
|
|
2
|
+
import ReactDOM from 'react-dom'
|
|
3
|
+
import { App } from './App'
|
|
4
|
+
import { FootballLoader } from './football-loader'
|
|
5
5
|
const { ipcRenderer } = require('electron')
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
let appData: VixonicData | undefined = undefined
|
|
8
|
+
let fixtures: FootballFixture[] = []
|
|
9
|
+
let serviceError: string | undefined = undefined
|
|
10
|
+
|
|
11
|
+
const footballLoader = new FootballLoader()
|
|
12
|
+
|
|
13
|
+
footballLoader.onData = (update) => {
|
|
14
|
+
fixtures = update
|
|
15
|
+
render()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
footballLoader.onError = (error) => {
|
|
19
|
+
serviceError = error
|
|
20
|
+
render()
|
|
21
|
+
}
|
|
7
22
|
|
|
8
23
|
ipcRenderer.on('preload', (_event: any, data: VixonicData) => {
|
|
9
|
-
|
|
24
|
+
console.log('[main] preload received. parameters:', JSON.stringify(data.parameters, null, 2))
|
|
25
|
+
console.log('[main] preload services keys:', Object.keys(data.services))
|
|
26
|
+
appData = data
|
|
27
|
+
footballLoader.update(data)
|
|
28
|
+
render()
|
|
10
29
|
})
|
|
11
30
|
|
|
12
31
|
ipcRenderer.on('start', (_event: any, data: VixonicData) => {
|
|
13
|
-
start
|
|
14
|
-
|
|
32
|
+
console.log('[main] start received. parameters:', JSON.stringify(data.parameters, null, 2))
|
|
33
|
+
console.log('[main] start services keys:', Object.keys(data.services))
|
|
34
|
+
appData = data
|
|
35
|
+
footballLoader.update(data)
|
|
36
|
+
render()
|
|
15
37
|
})
|
|
16
38
|
|
|
17
39
|
ipcRenderer.on('update', (_event: any, data: VixonicData) => {
|
|
18
|
-
|
|
40
|
+
console.log('[main] update received. services keys:', Object.keys(data.services))
|
|
41
|
+
appData = data
|
|
42
|
+
footballLoader.update(data)
|
|
43
|
+
render()
|
|
19
44
|
})
|
|
20
45
|
|
|
21
46
|
ipcRenderer.on('finish', (_event: any) => {
|
|
47
|
+
footballLoader.stop()
|
|
22
48
|
console.log('finish')
|
|
23
49
|
})
|
|
24
50
|
|
|
25
|
-
function render(
|
|
26
|
-
ReactDOM.render(
|
|
51
|
+
function render() {
|
|
52
|
+
ReactDOM.render(
|
|
53
|
+
React.createElement(App, { appData, fixtures, serviceError }),
|
|
54
|
+
document.getElementById('root')
|
|
55
|
+
)
|
|
27
56
|
}
|
package/src/parameters.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ declare type VixonicData = {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
declare type VixonicParameters = Partial<{
|
|
8
|
-
|
|
8
|
+
footballService: { id: string }, matchesYesterday: boolean, matchesTomorrow: boolean, titleEnabled: boolean, title0: string, titleFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, msj0: string, formatMjs: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, alignVerticalMsj: 'flex-start' | 'center' | 'flex-end', itemsPerPage: number, durationPerPage: number, backgroundImage: {id?: string, filename?: string, extension?: string}, padding: string, columnGap: string, rowGap: string, timeFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, widthTime: string, statusFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, widthStatus: string, goalsFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, widthScore: string, descriptionEnabled: boolean, descriptionFormat: { fontSize?: number, fontColor?: string, alignment?: { horizontal?: 'left' | 'right' | 'center' }, font?: { filename: string, id: string, __isAsset: true } }, widthDescription: string, imageSize: number, widthImage: string
|
|
9
9
|
}>
|
package/src/test/parameters.json
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"services": {
|
|
3
|
+
"f7e3a1b2-c4d5-6789-abcd-ef0123456789": {
|
|
4
|
+
"id": "f7e3a1b2-c4d5-6789-abcd-ef0123456789",
|
|
5
|
+
"data": {
|
|
6
|
+
"fixtures": [
|
|
7
|
+
{
|
|
8
|
+
"fixture": {
|
|
9
|
+
"id": 1001,
|
|
10
|
+
"date": "2026-06-22T18:00:00+00:00",
|
|
11
|
+
"status": { "long": "Match Finished", "short": "FT", "elapsed": 90 },
|
|
12
|
+
"venue": { "name": "Camp Nou", "city": "Barcelona" }
|
|
13
|
+
},
|
|
14
|
+
"league": {
|
|
15
|
+
"id": 140,
|
|
16
|
+
"name": "La Liga",
|
|
17
|
+
"country": "Spain",
|
|
18
|
+
"logo": "https://media.api-sports.io/football/leagues/140.png",
|
|
19
|
+
"round": "Regular Season - 38"
|
|
20
|
+
},
|
|
21
|
+
"teams": {
|
|
22
|
+
"home": { "id": 529, "name": "Barcelona", "logo": "https://media.api-sports.io/football/teams/529.png", "winner": true },
|
|
23
|
+
"away": { "id": 541, "name": "Real Madrid", "logo": "https://media.api-sports.io/football/teams/541.png", "winner": false }
|
|
24
|
+
},
|
|
25
|
+
"goals": { "home": 3, "away": 1 },
|
|
26
|
+
"score": {
|
|
27
|
+
"halftime": { "home": 1, "away": 0 },
|
|
28
|
+
"fulltime": { "home": 3, "away": 1 }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"fixture": {
|
|
33
|
+
"id": 1002,
|
|
34
|
+
"date": "2026-06-22T20:45:00+00:00",
|
|
35
|
+
"status": { "long": "Second Half", "short": "2H", "elapsed": 67 },
|
|
36
|
+
"venue": { "name": "Anfield", "city": "Liverpool" }
|
|
37
|
+
},
|
|
38
|
+
"league": {
|
|
39
|
+
"id": 39,
|
|
40
|
+
"name": "Premier League",
|
|
41
|
+
"country": "England",
|
|
42
|
+
"logo": "https://media.api-sports.io/football/leagues/39.png",
|
|
43
|
+
"round": "Regular Season - 38"
|
|
44
|
+
},
|
|
45
|
+
"teams": {
|
|
46
|
+
"home": { "id": 40, "name": "Liverpool", "logo": "https://media.api-sports.io/football/teams/40.png", "winner": null },
|
|
47
|
+
"away": { "id": 42, "name": "Arsenal", "logo": "https://media.api-sports.io/football/teams/42.png", "winner": null }
|
|
48
|
+
},
|
|
49
|
+
"goals": { "home": 2, "away": 2 },
|
|
50
|
+
"score": {
|
|
51
|
+
"halftime": { "home": 1, "away": 1 },
|
|
52
|
+
"fulltime": { "home": null, "away": null }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"fixture": {
|
|
57
|
+
"id": 1003,
|
|
58
|
+
"date": "2026-06-23T19:00:00+00:00",
|
|
59
|
+
"status": { "long": "Not Started", "short": "NS", "elapsed": null },
|
|
60
|
+
"venue": { "name": "Allianz Arena", "city": "Munich" }
|
|
61
|
+
},
|
|
62
|
+
"league": {
|
|
63
|
+
"id": 78,
|
|
64
|
+
"name": "Bundesliga",
|
|
65
|
+
"country": "Germany",
|
|
66
|
+
"logo": "https://media.api-sports.io/football/leagues/78.png",
|
|
67
|
+
"round": "Regular Season - 34"
|
|
68
|
+
},
|
|
69
|
+
"teams": {
|
|
70
|
+
"home": { "id": 157, "name": "Bayern Munich", "logo": "https://media.api-sports.io/football/teams/157.png", "winner": null },
|
|
71
|
+
"away": { "id": 165, "name": "Borussia Dortmund", "logo": "https://media.api-sports.io/football/teams/165.png", "winner": null }
|
|
72
|
+
},
|
|
73
|
+
"goals": { "home": null, "away": null },
|
|
74
|
+
"score": {
|
|
75
|
+
"halftime": { "home": null, "away": null },
|
|
76
|
+
"fulltime": { "home": null, "away": null }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"fixture": {
|
|
81
|
+
"id": 1004,
|
|
82
|
+
"date": "2026-06-22T17:30:00+00:00",
|
|
83
|
+
"status": { "long": "Match Finished", "short": "FT", "elapsed": 90 },
|
|
84
|
+
"venue": { "name": "San Siro", "city": "Milan" }
|
|
85
|
+
},
|
|
86
|
+
"league": {
|
|
87
|
+
"id": 135,
|
|
88
|
+
"name": "Serie A",
|
|
89
|
+
"country": "Italy",
|
|
90
|
+
"logo": "https://media.api-sports.io/football/leagues/135.png",
|
|
91
|
+
"round": "Regular Season - 38"
|
|
92
|
+
},
|
|
93
|
+
"teams": {
|
|
94
|
+
"home": { "id": 489, "name": "AC Milan", "logo": "https://media.api-sports.io/football/teams/489.png", "winner": false },
|
|
95
|
+
"away": { "id": 505, "name": "Inter", "logo": "https://media.api-sports.io/football/teams/505.png", "winner": true }
|
|
96
|
+
},
|
|
97
|
+
"goals": { "home": 0, "away": 2 },
|
|
98
|
+
"score": {
|
|
99
|
+
"halftime": { "home": 0, "away": 1 },
|
|
100
|
+
"fulltime": { "home": 0, "away": 2 }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"updatedAt": 1750608000000,
|
|
106
|
+
"__typename": "FootballAppService"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|