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..53dba4d --- /dev/null +++ b/plugins/WorldCup2026/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Connect to worldcup26.ir", + "dataStream": { "name": "worldcup2026-teams-import" }, + "required": true, + "error": "Could not reach the worldcup26.ir API. 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..0caa592 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js @@ -0,0 +1,67 @@ +var games = data.games || []; +var teamFilter = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; + +var groupGames = games.filter(function(g) { return g.type === 'group'; }); + +var standings = {}; + +function ensureTeam(name, id, group) { + if (!standings[name]) { + standings[name] = { id: id, group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, pts: 0 }; + } +} + +groupGames.forEach(function(g) { + ensureTeam(g.home_team_name_en, g.home_team_id, g.group); + ensureTeam(g.away_team_name_en, g.away_team_id, g.group); + + if (g.finished !== 'TRUE') return; + + var homeScore = parseInt(g.home_score, 10) || 0; + var awayScore = parseInt(g.away_score, 10) || 0; + var home = standings[g.home_team_name_en]; + var away = standings[g.away_team_name_en]; + + home.mp++; away.mp++; + home.gf += homeScore; home.ga += awayScore; + away.gf += awayScore; away.ga += homeScore; + + if (homeScore > awayScore) { + home.w++; home.pts += 3; away.l++; + } else if (awayScore > homeScore) { + away.w++; away.pts += 3; home.l++; + } else { + home.d++; home.pts++; away.d++; away.pts++; + } +}); + +var rows = Object.keys(standings).map(function(k) { + var s = standings[k]; + return { + sourceId: s.id, + team: k, + 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..f483d4b --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js @@ -0,0 +1,35 @@ +var games = data.games || []; + +var stageMap = { + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +result = games.filter(function(g) { return g.type !== 'group'; }).map(function(g) { + var homeTeam = g.home_team_id !== '0' ? g.home_team_name_en : (g.home_team_label || 'TBD'); + var awayTeam = g.away_team_id !== '0' ? g.away_team_name_en : (g.away_team_label || 'TBD'); + + var score = '-'; + if (g.finished === 'TRUE') { + score = g.home_score + '-' + g.away_score; + } else if (g.time_elapsed !== 'notstarted') { + score = g.home_score + '-' + g.away_score + ' (Live)'; + } + + var status = 'Upcoming'; + if (g.finished === 'TRUE') status = 'Finished'; + else if (g.time_elapsed !== 'notstarted') status = 'Live'; + + return { + date: g.local_date, + round: stageMap[g.type] || g.type, + home_team: homeTeam, + away_team: awayTeam, + 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..3ea951e --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js @@ -0,0 +1,51 @@ +var games = data.games || []; +var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; + +var stageMap = { + group: 'Group Stage', + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +function parseDate(d) { + var parts = d.split(' '); + var dateParts = parts[0].split('/'); + return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); +} + +var played = games.filter(function(g) { + return g.finished === 'TRUE' && + (g.home_team_id === teamId || g.away_team_id === teamId); +}); + +played.sort(function(a, b) { + return parseDate(b.local_date) - parseDate(a.local_date); +}); + +var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; + +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.home_team_id === teamId; + var opponent = isHome ? last.away_team_name_en : last.home_team_name_en; + var myScore = parseInt(isHome ? last.home_score : last.away_score, 10); + var oppScore = parseInt(isHome ? last.away_score : last.home_score, 10); + var scoreStr = isHome ? last.home_score + '-' + last.away_score : last.away_score + '-' + last.home_score; + var matchResult = myScore > oppScore ? 'Win' : myScore < oppScore ? 'Loss' : 'Draw'; + + result = [{ + date: last.local_date, + home_away: isHome ? 'Home' : 'Away', + opponent: opponent, + score: scoreStr, + result: matchResult, + stage: stageMap[last.type] || last.type, + 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..98e1adf --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/matches.js @@ -0,0 +1,56 @@ +var games = data.games || []; +var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; + +var stageMap = { + group: 'Group Stage', + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +function parseDate(d) { + var parts = d.split(' '); + var dateParts = parts[0].split('/'); + return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); +} + +function getTeamName(game, side) { + if (side === 'home') { + return game.home_team_id !== '0' ? game.home_team_name_en : (game.home_team_label || 'TBD'); + } + return game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD'); +} + +var filtered = games.filter(function(g) { + return g.home_team_id === teamId || g.away_team_id === teamId; +}); + +filtered.sort(function(a, b) { + return parseDate(a.local_date) - parseDate(b.local_date); +}); + +result = filtered.map(function(g) { + var score = '-'; + if (g.finished === 'TRUE') { + score = g.home_score + '-' + g.away_score; + } else if (g.time_elapsed !== 'notstarted') { + score = g.home_score + '-' + g.away_score + ' (Live)'; + } + + var status = 'Upcoming'; + if (g.finished === 'TRUE') status = 'Finished'; + else if (g.time_elapsed !== 'notstarted') status = 'Live'; + + return { + date: g.local_date, + home_team: getTeamName(g, 'home'), + away_team: getTeamName(g, 'away'), + score: score, + group: g.group, + stage: stageMap[g.type] || g.type, + 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..5e77c14 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js @@ -0,0 +1,51 @@ +var games = data.games || []; +var teamId = context.objects[0] ? String(context.objects[0].teamId) : ''; + +var stageMap = { + group: 'Group Stage', + r32: 'Round of 32', + r16: 'Round of 16', + qf: 'Quarter-Final', + sf: 'Semi-Final', + third: 'Third Place', + final: 'Final' +}; + +function parseDate(d) { + var parts = d.split(' '); + var dateParts = parts[0].split('/'); + return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00'); +} + +var teamGames = games.filter(function(g) { + return g.home_team_id === teamId || g.away_team_id === teamId; +}); + +var upcoming = teamGames.filter(function(g) { + return g.finished !== 'TRUE' && g.time_elapsed === 'notstarted'; +}); + +upcoming.sort(function(a, b) { + return parseDate(a.local_date) - parseDate(b.local_date); +}); + +var sourceId = context.objects[0] ? context.objects[0].sourceId : ''; + +if (upcoming.length === 0) { + result = [{ date: 'No upcoming matches', home_away: '', opponent: '', stage: '', group: '', sourceId: sourceId }]; +} else { + var next = upcoming[0]; + var isHome = next.home_team_id === teamId; + var opponent = isHome ? next.away_team_name_en : next.home_team_name_en; + if (!opponent || opponent === 'undefined') { + opponent = isHome ? (next.away_team_label || 'TBD') : (next.home_team_label || 'TBD'); + } + result = [{ + date: next.local_date, + home_away: isHome ? 'Home' : 'Away', + opponent: opponent, + stage: stageMap[next.type] || next.type, + group: next.type === 'group' ? '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..2711046 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js @@ -0,0 +1,29 @@ +var games = data.games || []; +var teamId = context.objects[0] ? String([].concat(context.objects[0].teamId)[0] || '') : ''; +var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : ''; +var group = context.objects[0] ? [].concat(context.objects[0].group)[0] || '' : ''; + +var groupGames = games.filter(function(g) { + return g.type === 'group' && + (g.home_team_id === teamId || g.away_team_id === teamId); +}); + +var mp = 0, w = 0, d = 0, l = 0, gf = 0, ga = 0; + +groupGames.forEach(function(g) { + if (g.finished !== 'TRUE') return; + mp++; + var isHome = g.home_team_id === teamId; + var myScore = parseInt(isHome ? g.home_score : g.away_score, 10) || 0; + var oppScore = parseInt(isHome ? g.away_score : g.home_score, 10) || 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: teamId }]; 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..79a815a --- /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": "/get/games", + "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..f787afe --- /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": "/get/games", + "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..00019ca --- /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": "/get/games", + "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..226bf03 --- /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": "/get/games", + "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..6768ee4 --- /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": "/get/games", + "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..506c389 --- /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": "/get/games", + "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..c1627e5 --- /dev/null +++ b/plugins/WorldCup2026/v1/dataStreams/worldcup2026-teams-import.json @@ -0,0 +1,23 @@ +{ + "name": "worldcup2026-teams-import", + "displayName": "Teams (Import)", + "description": "All 48 World Cup 2026 teams", + "tags": ["Teams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/get/teams", + "pathToData": "teams" + }, + "matches": "none", + "timeframes": false, + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "id", "shape": "string", "visible": false }, + { "name": "name_en", "displayName": "Team", "shape": "string", "role": "label" }, + { "name": "fifa_code", "displayName": "FIFA Code", "shape": "string" }, + { "name": "flag", "displayName": "Flag", "shape": "url" }, + { "name": "groups", "displayName": "Group", "shape": "string" }, + { "name": "iso2", "shape": "string", "visible": false } + ] +} 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..eab872a --- /dev/null +++ b/plugins/WorldCup2026/v1/docs/README.md @@ -0,0 +1,30 @@ +# Before you start + +This plugin connects to the community API at [worldcup26.ir](https://worldcup26.ir) — an unofficial, community-maintained 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..4a8b877 --- /dev/null +++ b/plugins/WorldCup2026/v1/indexDefinitions/default.json @@ -0,0 +1,24 @@ +{ + "steps": [ + { + "name": "teams", + "dataStream": { + "name": "worldcup2026-teams-import" + }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name_en", + "type": { "value": "World Cup Team" }, + "properties": [ + { "teamName": "name_en" }, + { "teamId": "id" }, + "fifa_code", + "flag", + { "group": "groups" }, + "iso2" + ] + } + } + ] +} diff --git a/plugins/WorldCup2026/v1/metadata.json b/plugins/WorldCup2026/v1/metadata.json new file mode 100644 index 0000000..13d352d --- /dev/null +++ b/plugins/WorldCup2026/v1/metadata.json @@ -0,0 +1,37 @@ +{ + "name": "worldcup2026", + "displayName": "FIFA World Cup 2026", + "version": "1.1.2", + "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://worldcup26.ir" + } + } +} 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 @@ +[]