diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2fa7725..cd076fb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -22,6 +22,7 @@ plugins/SumoLogic/* @richpeters plugins/TransportForLondon/* @clarkd plugins/UniFi/* @adamkinniburgh plugins/UptimeRobot/* @kieranlangton +plugins/WorldCup2026/* @TimWheeler-SQUP # Fallback – if a plugin has no specified author diff --git a/plugins/WorldCup2026/v1/configValidation.json b/plugins/WorldCup2026/v1/configValidation.json new file mode 100644 index 0000000..12e84ac --- /dev/null +++ b/plugins/WorldCup2026/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Connect to openfootball", + "dataStream": { "name": "worldcup2026-teams-import" }, + "required": true, + "error": "Could not reach the openfootball data source. Check your network connection.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/WorldCup2026/v1/custom_types.json b/plugins/WorldCup2026/v1/custom_types.json new file mode 100644 index 0000000..8ea11b9 --- /dev/null +++ b/plugins/WorldCup2026/v1/custom_types.json @@ -0,0 +1,9 @@ +[ + { + "name": "World Cup Team", + "sourceType": "World Cup Team", + "icon": "flag", + "singular": "Team", + "plural": "Teams" + } +] diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js new file mode 100644 index 0000000..ba27376 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -0,0 +1,61 @@ +var matches = data.matches || []; +var teamFilter = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; + +var groupMatches = matches.filter(function(m) { return m.group; }); + +var standings = {}; + +function ensureTeam(name, group) { + if (!standings[name]) { + standings[name] = { group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, pts: 0 }; + } +} + +groupMatches.forEach(function(m) { + ensureTeam(m.team1, m.group); + ensureTeam(m.team2, m.group); + + if (!m.score || !m.score.ft) return; + + var s1 = m.score.ft[0]; + var s2 = m.score.ft[1]; + var t1 = standings[m.team1]; + var t2 = standings[m.team2]; + + t1.mp++; t2.mp++; + t1.gf += s1; t1.ga += s2; + t2.gf += s2; t2.ga += s1; + + if (s1 > s2) { + t1.w++; t1.pts += 3; t2.l++; + } else if (s2 > s1) { + t2.w++; t2.pts += 3; t1.l++; + } else { + t1.d++; t1.pts++; t2.d++; t2.pts++; + } +}); + +var rows = Object.keys(standings).map(function(name) { + var s = standings[name]; + return { + sourceId: name, + team: name, + group: s.group, + mp: s.mp, w: s.w, d: s.d, l: s.l, + gf: s.gf, ga: s.ga, gd: s.gf - s.ga, pts: s.pts + }; +}); + +if (teamFilter && standings[teamFilter]) { + var targetGroup = standings[teamFilter].group; + rows = rows.filter(function(r) { return r.group === targetGroup; }); +} + +rows.sort(function(a, b) { + if (a.group !== b.group) return a.group.localeCompare(b.group); + if (b.pts !== a.pts) return b.pts - a.pts; + if (b.gd !== a.gd) return b.gd - a.gd; + return b.gf - a.gf; +}); + +result = rows; diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js b/plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js new file mode 100644 index 0000000..f713f98 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js @@ -0,0 +1,28 @@ +var matches = data.matches || []; + +var stageMap = { + 'Round of 32': 'Round of 32', + 'Round of 16': 'Round of 16', + 'Quarter-final': 'Quarter-Final', + 'Semi-final': 'Semi-Final', + 'Third-place play-off': 'Third Place', + 'Final': 'Final' +}; + +result = matches.filter(function(m) { return !m.group; }).map(function(m) { + var score = '-'; + var status = 'Upcoming'; + if (m.score && m.score.ft) { + score = m.score.ft[0] + '-' + m.score.ft[1]; + status = 'Finished'; + } + + return { + date: m.date, + round: stageMap[m.round] || m.round, + home_team: m.team1, + away_team: m.team2, + score: score, + status: status + }; +}); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js new file mode 100644 index 0000000..0feb1b6 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -0,0 +1,41 @@ +var matches = data.matches || []; +var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; +var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; + +var knockoutStageMap = { + 'Round of 32': 'Round of 32', + 'Round of 16': 'Round of 16', + 'Quarter-final': 'Quarter-Final', + 'Semi-final': 'Semi-Final', + 'Third-place play-off': 'Third Place', + 'Final': 'Final' +}; + +var played = matches.filter(function(m) { + return m.score && (m.team1 === teamName || m.team2 === teamName); +}); + +played.sort(function(a, b) { + return new Date(b.date) - new Date(a.date); +}); + +if (played.length === 0) { + result = [{ date: 'No matches played yet', home_away: '', opponent: '', score: '', result: '', stage: '', sourceId: sourceId }]; +} else { + var last = played[0]; + var isHome = last.team1 === teamName; + var opponent = isHome ? last.team2 : last.team1; + var myScore = isHome ? last.score.ft[0] : last.score.ft[1]; + var oppScore = isHome ? last.score.ft[1] : last.score.ft[0]; + var matchResult = myScore > oppScore ? 'Win' : myScore < oppScore ? 'Loss' : 'Draw'; + + result = [{ + date: last.date, + home_away: isHome ? 'Home' : 'Away', + opponent: opponent, + score: myScore + '-' + oppScore, + result: matchResult, + stage: last.group ? 'Group Stage' : (knockoutStageMap[last.round] || last.round), + sourceId: sourceId + }]; +} diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js new file mode 100644 index 0000000..0d7d0e1 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js @@ -0,0 +1,38 @@ +var matches = data.matches || []; +var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; + +var knockoutStageMap = { + 'Round of 32': 'Round of 32', + 'Round of 16': 'Round of 16', + 'Quarter-final': 'Quarter-Final', + 'Semi-final': 'Semi-Final', + 'Third-place play-off': 'Third Place', + 'Final': 'Final' +}; + +var filtered = matches.filter(function(m) { + return m.team1 === teamName || m.team2 === teamName; +}); + +filtered.sort(function(a, b) { + return new Date(a.date) - new Date(b.date); +}); + +result = filtered.map(function(m) { + var score = '-'; + var status = 'Upcoming'; + if (m.score && m.score.ft) { + score = m.score.ft[0] + '-' + m.score.ft[1]; + status = 'Finished'; + } + + return { + date: m.date, + home_team: m.team1, + away_team: m.team2, + score: score, + group: m.group || '', + stage: m.group ? 'Group Stage' : (knockoutStageMap[m.round] || m.round), + status: status + }; +}); diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js new file mode 100644 index 0000000..da3334f --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -0,0 +1,36 @@ +var matches = data.matches || []; +var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; +var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; + +var knockoutStageMap = { + 'Round of 32': 'Round of 32', + 'Round of 16': 'Round of 16', + 'Quarter-final': 'Quarter-Final', + 'Semi-final': 'Semi-Final', + 'Third-place play-off': 'Third Place', + 'Final': 'Final' +}; + +var upcoming = matches.filter(function(m) { + return !m.score && (m.team1 === teamName || m.team2 === teamName); +}); + +upcoming.sort(function(a, b) { + return new Date(a.date) - new Date(b.date); +}); + +if (upcoming.length === 0) { + result = [{ date: 'No upcoming matches', home_away: '', opponent: '', stage: '', group: '', sourceId: sourceId }]; +} else { + var next = upcoming[0]; + var isHome = next.team1 === teamName; + var opponent = isHome ? next.team2 : next.team1; + result = [{ + date: next.date, + home_away: isHome ? 'Home' : 'Away', + opponent: opponent, + stage: next.group ? 'Group Stage' : (knockoutStageMap[next.round] || next.round), + group: next.group || '', + sourceId: sourceId + }]; +} diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js new file mode 100644 index 0000000..2cb63ab --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -0,0 +1,27 @@ +var matches = data.matches || []; +var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; +var group = context.objects[0] ? [].concat(context.objects[0].group)[0] || '' : ''; + +var groupGames = matches.filter(function(m) { + return m.group && (m.team1 === teamName || m.team2 === teamName); +}); + +var mp = 0, w = 0, d = 0, l = 0, gf = 0, ga = 0; + +groupGames.forEach(function(m) { + if (!m.score || !m.score.ft) return; + mp++; + var isHome = m.team1 === teamName; + var myScore = isHome ? m.score.ft[0] : m.score.ft[1]; + var oppScore = isHome ? m.score.ft[1] : m.score.ft[0]; + gf += myScore; + ga += oppScore; + if (myScore > oppScore) w++; + else if (myScore < oppScore) l++; + else d++; +}); + +var pts = (w * 3) + d; +var gd = gf - ga; + +result = [{ country: teamName, group: group, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, gd: gd, sourceId: teamName }]; diff --git a/plugins/WorldCup2026/v1/dataStreams/scripts/teams-import.js b/plugins/WorldCup2026/v1/dataStreams/scripts/teams-import.js new file mode 100644 index 0000000..ddc6a89 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/teams-import.js @@ -0,0 +1,14 @@ +var matches = data.matches || []; +var seen = {}; +var teams = []; + +matches.filter(function(m) { return m.group; }).forEach(function(m) { + [{ name: m.team1, group: m.group }, { name: m.team2, group: m.group }].forEach(function(t) { + if (!seen[t.name]) { + seen[t.name] = true; + teams.push({ sourceId: t.name, name_en: t.name, group: t.group }); + } + }); +}); + +result = teams; diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json new file mode 100644 index 0000000..86a31f0 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-group-standings.json @@ -0,0 +1,33 @@ +{ + "name": "worldcup2026-group-standings", + "displayName": "Group Standings", + "description": "Group stage standings calculated from match results, with optional filter to a specific team's group", + "tags": ["Standings"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/worldcup.json", + "postRequestScript": "group-standings.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false }, + { "name": "group", "displayName": "Group", "shape": "string" }, + { "name": "team", "displayName": "Team", "shape": "string", "role": "label" }, + { "name": "mp", "displayName": "Played", "shape": "number" }, + { "name": "w", "displayName": "W", "shape": "number" }, + { "name": "d", "displayName": "D", "shape": "number" }, + { "name": "l", "displayName": "L", "shape": "number" }, + { "name": "gf", "displayName": "GF", "shape": "number" }, + { "name": "ga", "displayName": "GA", "shape": "number" }, + { "name": "gd", "displayName": "GD", "shape": "number" }, + { "name": "pts", "displayName": "Points", "shape": "number" }, + { "name": "team", "sourceId": "sourceId", "sourceType": "World Cup Team" } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json new file mode 100644 index 0000000..4f14d2d --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json @@ -0,0 +1,26 @@ +{ + "name": "worldcup2026-knockout", + "displayName": "Knockout Matches", + "description": "All knockout stage fixtures, showing placeholder labels until teams are determined", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/worldcup.json", + "postRequestScript": "knockout.js" + }, + "matches": "none", + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "round", "displayName": "Round", "shape": "string" }, + { "name": "home_team", "displayName": "Home Team", "shape": "string" }, + { "name": "away_team", "displayName": "Away Team", "shape": "string" }, + { "name": "score", "displayName": "Score", "shape": "string" }, + { + "name": "status", + "displayName": "Status", + "shape": ["state", { "map": { "success": ["Finished"], "warning": ["Live"], "unknown": ["Upcoming"] } }] + } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json new file mode 100644 index 0000000..9cb4643 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json @@ -0,0 +1,32 @@ +{ + "name": "worldcup2026-last-match", + "displayName": "Last Match", + "description": "The most recently completed match for the selected team", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/worldcup.json", + "postRequestScript": "last-match.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "home_away", "displayName": "Home / Away", "shape": "string" }, + { "name": "opponent", "displayName": "Opponent", "shape": "string" }, + { "name": "score", "displayName": "Score", "shape": "string" }, + { + "name": "result", + "displayName": "Result", + "shape": ["state", { "map": { "success": ["Win"], "error": ["Loss"], "unknown": ["Draw"] } }] + }, + { "name": "stage", "displayName": "Stage", "shape": "string" }, + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json new file mode 100644 index 0000000..2453a3c --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-matches.json @@ -0,0 +1,32 @@ +{ + "name": "worldcup2026-matches", + "displayName": "Matches", + "description": "All 104 World Cup 2026 fixtures in date order, with optional filter by team", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/worldcup.json", + "postRequestScript": "matches.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "home_team", "displayName": "Home Team", "shape": "string" }, + { "name": "away_team", "displayName": "Away Team", "shape": "string" }, + { "name": "score", "displayName": "Score", "shape": "string" }, + { "name": "group", "displayName": "Group", "shape": "string" }, + { "name": "stage", "displayName": "Stage", "shape": "string" }, + { + "name": "status", + "displayName": "Status", + "shape": ["state", { "map": { "success": ["Finished"], "warning": ["Live"], "unknown": ["Upcoming"] } }] + } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-next-match.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-next-match.json new file mode 100644 index 0000000..5ed08ab --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-next-match.json @@ -0,0 +1,27 @@ +{ + "name": "worldcup2026-next-match", + "displayName": "Next Match", + "description": "The next upcoming match for the selected team", + "tags": ["Matches"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/worldcup.json", + "postRequestScript": "next-match.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "date", "displayName": "Date", "shape": "string", "role": "label" }, + { "name": "home_away", "displayName": "Home / Away", "shape": "string" }, + { "name": "opponent", "displayName": "Opponent", "shape": "string" }, + { "name": "stage", "displayName": "Stage", "shape": "string" }, + { "name": "group", "displayName": "Group", "shape": "string" }, + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json new file mode 100644 index 0000000..02e4387 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-team-standing.json @@ -0,0 +1,33 @@ +{ + "name": "worldcup2026-team-standing", + "displayName": "Team Standing", + "description": "Group stage standing and match statistics for the selected team", + "tags": ["Standings"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/worldcup.json", + "postRequestScript": "team-standing.js" + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "timeframes": false, + "metadata": [ + { "name": "country", "displayName": "Country", "shape": "string", "role": "label" }, + { "name": "group", "displayName": "Group", "shape": "string" }, + { "name": "mp", "displayName": "Played", "shape": "number" }, + { "name": "w", "displayName": "Won", "shape": "number" }, + { "name": "d", "displayName": "Drawn", "shape": "number" }, + { "name": "l", "displayName": "Lost", "shape": "number" }, + { "name": "gf", "displayName": "Goals For", "shape": "number" }, + { "name": "ga", "displayName": "Goals Against", "shape": "number" }, + { "name": "gd", "displayName": "Goal Difference", "shape": "number" }, + { "name": "pts", "displayName": "Points", "shape": "number" }, + { "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false }, + { "name": "country", "sourceId": "sourceId", "sourceType": "World Cup Team" } + ] +} diff --git a/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json new file mode 100644 index 0000000..7bc6597 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json @@ -0,0 +1,20 @@ +{ + "name": "worldcup2026-teams-import", + "displayName": "Teams (Import)", + "description": "All 48 World Cup 2026 teams, extracted from match data", + "tags": ["Teams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/worldcup.json", + "postRequestScript": "teams-import.js" + }, + "matches": "none", + "timeframes": false, + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "sourceId", "shape": "string", "visible": false }, + { "name": "name_en", "displayName": "Team", "shape": "string", "role": "label" }, + { "name": "group", "displayName": "Group", "shape": "string" } + ] +} diff --git a/plugins/WorldCup2026/v1/defaultContent/manifest.json b/plugins/WorldCup2026/v1/defaultContent/manifest.json new file mode 100644 index 0000000..cb9c715 --- /dev/null +++ b/plugins/WorldCup2026/v1/defaultContent/manifest.json @@ -0,0 +1,5 @@ +{ + "items": [ + { "name": "team", "type": "dashboard" } + ] +} diff --git a/plugins/WorldCup2026/v1/defaultContent/scopes.json b/plugins/WorldCup2026/v1/defaultContent/scopes.json new file mode 100644 index 0000000..000d47d --- /dev/null +++ b/plugins/WorldCup2026/v1/defaultContent/scopes.json @@ -0,0 +1,17 @@ +[ + { + "name": "All Teams", + "matches": { + "sourceType": { + "type": "equals", + "value": "World Cup Team" + } + }, + "variable": { + "name": "Team", + "type": "object", + "default": "none", + "allowMultipleSelection": false + } + } +] diff --git a/plugins/WorldCup2026/v1/defaultContent/team.dash.json b/plugins/WorldCup2026/v1/defaultContent/team.dash.json new file mode 100644 index 0000000..15ab1ca --- /dev/null +++ b/plugins/WorldCup2026/v1/defaultContent/team.dash.json @@ -0,0 +1,164 @@ +{ + "name": "Team Dashboard", + "schemaVersion": "1.4", + "variables": [ + "{{variables.[Team]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "9d2b7a31-0998-43fa-8479-29e61e6e9ef8", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Next Match", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-next-match}}", + "name": "worldcup2026-next-match", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["date", "home_away", "opponent", "stage", "group"] + } + } + } + } + }, + { + "i": "81d998b7-a744-4abd-8284-5e8b2bf5f8af", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Last Match", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-last-match}}", + "name": "worldcup2026-last-match", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["date", "home_away", "opponent", "score", "result", "stage"] + } + } + } + } + }, + { + "i": "b4b8fd8e-358b-46b2-9d8e-dabfb2fdff87", + "x": 0, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Group Standings", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-group-standings}}", + "name": "worldcup2026-group-standings", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["team", "mp", "w", "d", "l", "gf", "ga", "gd", "pts"], + "hiddenColumns": ["group"] + } + } + } + } + }, + { + "i": "5b8537bf-dd49-4d1a-a4b0-401b24d5bf22", + "x": 2, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Stats", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Team]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.worldcup2026-team-standing}}", + "name": "worldcup2026-team-standing", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[All Teams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Team]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["country", "group", "gf", "ga", "gd", "pts"], + "hiddenColumns": ["mp", "w", "d", "l", "sourceId"] + } + } + } + } + } + ] + } +} diff --git a/plugins/WorldCup2026/v1/docs/README.md b/plugins/WorldCup2026/v1/docs/README.md new file mode 100644 index 0000000..59ec748 --- /dev/null +++ b/plugins/WorldCup2026/v1/docs/README.md @@ -0,0 +1,30 @@ +# Before you start + +This plugin connects to the community data source at https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026/worldcup.json — an unofficial data source. It is not an official FIFA API. Data coverage and availability depends on the third party keeping the service running. + +No account, API key, or credentials are required. + +## What this plugin monitors + +Indexes all 48 FIFA World Cup 2026 teams into SquaredUp, making them available for search, scoping, and dashboard variables. Data streams cover: + +- Live and completed match results +- Group stage standings +- Knockout bracket fixtures +- Per-team statistics (next match, last match, group points) + +## What gets indexed + +| Object type | Description | +|---|---| +| World Cup Team | All 48 participating national teams, including FIFA code, group, and flag | + +## Configuration + +No configuration fields are required. Add the plugin and it connects immediately. + +## Known limitations + +- Data is sourced from an unofficial community API (`worldcup26.ir`) and may be delayed, incomplete, or unavailable during high-traffic periods +- Knockout stage fixtures show placeholder labels (e.g. "Winner Group A") until the group stage is complete and teams are determined +- Match times are in local event time — no timezone conversion is applied diff --git a/plugins/WorldCup2026/v1/icon.png b/plugins/WorldCup2026/v1/icon.png new file mode 100644 index 0000000..c370f77 Binary files /dev/null and b/plugins/WorldCup2026/v1/icon.png differ diff --git a/plugins/WorldCup2026/v1/indexDefinitions/default.json b/plugins/WorldCup2026/v1/indexDefinitions/default.json new file mode 100644 index 0000000..2f12693 --- /dev/null +++ b/plugins/WorldCup2026/v1/indexDefinitions/default.json @@ -0,0 +1,20 @@ +{ + "steps": [ + { + "name": "teams", + "dataStream": { + "name": "worldcup2026-teams-import" + }, + "timeframe": "none", + "objectMapping": { + "id": "sourceId", + "name": "name_en", + "type": { "value": "World Cup Team" }, + "properties": [ + { "teamName": "name_en" }, + "group" + ] + } + } + ] +} diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json new file mode 100644 index 0000000..3389fc4 --- /dev/null +++ b/plugins/WorldCup2026/v1/metadata.json @@ -0,0 +1,37 @@ +{ + "name": "worldcup2026", + "displayName": "FIFA World Cup 2026", + "version": "1.2.0", + "author": { + "name": "@TimWheeler-SQUP", + "type": "community" + }, + "description": "Live scores, group standings, knockout bracket, and team data for the FIFA World Cup 2026.", + "category": "Fun", + "type": "hybrid", + "schemaVersion": "2.0", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["world cup", "fifa", "football", "soccer", "2026"], + "objectTypes": ["World Cup Team"], + "links": [ + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/WorldCup2026/v1", + "label": "Repository" + }, + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/WorldCup2026/v1/docs/README.md", + "label": "Help adding this plugin" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "authMode": "none", + "baseUrl": "https://raw.githubusercontent.com/openfootball/worldcup.json/master/2026" + } + } +} diff --git a/plugins/WorldCup2026/v1/ui.json b/plugins/WorldCup2026/v1/ui.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/plugins/WorldCup2026/v1/ui.json @@ -0,0 +1 @@ +[]