various improvements to make release 1 complete

This commit is contained in:
Reindl David (IT-PTR-CEN2-SL10) 2026-03-27 13:59:28 +01:00
parent 9ea962131a
commit 6b9c3fefa1
37 changed files with 7695 additions and 1803 deletions

4
.gitignore vendored
View File

@ -46,6 +46,10 @@ dist/
builds/
*.zip
# Development Documentation
# Internal notes and summaries, not for distribution
dev-docs/
# Environment Files
.env
.env.local

View File

@ -51,7 +51,6 @@ npm run build
This command:
- Runs `wp-scripts build --output-path=assets/build`
- Uses the custom `webpack.config.js` to ensure output is named `editor-blocks.js`
- Generates additional asset files in `assets/build/`
@ -87,7 +86,7 @@ Press `Ctrl+C` to stop the server.
### Project Structure
```
```tree
swiss-football-matchdata/
├── src/
│ ├── editor-blocks.js # Main editor block components
@ -97,10 +96,11 @@ swiss-football-matchdata/
├── blocks/ # Block definitions (metadata)
│ ├── context/
│ ├── match-events/
│ ├── match-bench/
│ ├── match-referees/
│ ├── match-roster/
│ ├── schedule/
│ ├── standings/
│ ├── team-data/
│ └── shortcode-inserter/
├── assets/
│ ├── build/
@ -130,6 +130,7 @@ swiss-football-matchdata/
### Block Files
Each block is defined in its own directory under `blocks/`:
- `block.json` — Block metadata (name, icon, attributes, supports)
- The React component code lives in `src/editor-blocks.js`
@ -141,12 +142,14 @@ Each block is defined in its own directory under `blocks/`:
- Update attribute definitions
2. **Test with hot reload**:
```bash
npm run start
```
- Hard-refresh your WordPress editor page
- Make changes in `src/editor-blocks.js`
- The browser auto-updates (HMR)
- Hard-refresh your WordPress editor page
- Make changes in `src/editor-blocks.js`
- The browser auto-updates (HMR)
3. **Verify output**:
- Insert/edit the block in WordPress
@ -154,6 +157,7 @@ Each block is defined in its own directory under `blocks/`:
- Look at page source to verify shortcode is generated correctly
4. **Build for production**:
```bash
npm run build
```
@ -190,36 +194,107 @@ Translation files include:
**Workflow when adding new translatable strings:**
1. Add translation strings to PHP or JavaScript with proper functions:
1. Add translation strings to PHP or JavaScript with proper functions and translator comments:
```php
__('Text to translate', 'swi_foot_matchdata')
/* translators: %s is the API error message */
__('Error loading data: %s', 'swi_foot_matchdata')
_e('Text to display', 'swi_foot_matchdata')
```
2. Extract strings to the `.pot` template file:
```bash
wp i18n make-pot . languages/swi_foot_matchdata.pot
```
(Requires WP-CLI installed)
**Important**: Always add `/* translators: ... */` comments above i18n functions with placeholders to clarify their meaning for translators.
3. Update existing `.po` translation files from the new template (done by translators in PoEdit or similar)
2. Extract strings and update translation files using the i18n management script:
4. Generate binary `.mo` files:
```bash
wp i18n make-mo languages/
./dev-scripts/i18n-manage.sh extract
```
5. Commit both `.po` and `.mo` files to git:
This extracts all strings to `languages/swi_foot_matchdata.pot`.
3. Generate `.po` files for translation:
```bash
./dev-scripts/i18n-manage.sh translate
```
Creates/updates `.po` files for: German (de_DE), French (fr_FR), Italian (it_IT), English (en_US)
4. Provide `.po` files to translators:
- Translators edit `languages/swi_foot_matchdata-de_DE.po`, `-fr_FR.po`, `-it_IT.po`
- They can use PoEdit, Crowdin, or any PO file editor
- Translator comments help clarify placeholder meanings
5. After translations are complete, compile `.mo` files:
```bash
./dev-scripts/i18n-manage.sh build
```
Generates binary `.mo` files from `.po` files.
6. Commit both `.po` and `.mo` files to git:
```bash
git add languages/*.po languages/*.mo
git commit -m "Update translations for [feature/language]"
```
7. (Optional) Clean up regional variants:
```bash
./dev-scripts/i18n-manage.sh clean
```
Removes regional variants (de_AT, de_CH) to keep only core languages.
**Note**: Editor backup files (`.po~`) are **not** committed (ignored by `.gitignore`).
The `.mo` files are essential for distribution — they're included in both git commits and distribution packages so installations work immediately without requiring a build step.
### Complete i18n Workflow with Script
The `i18n-manage.sh` script automates the entire translation pipeline:
```bash
# Run complete workflow (extract → translate → build → clean)
./dev-scripts/i18n-manage.sh all
```
This is equivalent to running:
```bash
./dev-scripts/i18n-manage.sh extract # Extract strings to .pot
./dev-scripts/i18n-manage.sh translate # Generate .po files for each language
./dev-scripts/i18n-manage.sh build # Compile .po to .mo files
./dev-scripts/i18n-manage.sh clean # Remove regional variants
```
**Supported Languages (by default)**:
- de_DE — German
- fr_FR — French
- it_IT — Italian
- en_US — English
**Script Configuration**:
Edit `dev-scripts/i18n-manage.sh` lines 39-60 to customize:
- `WP_CLI_CMD` — WordPress CLI command (for Docker or local)
- `LANGUAGES` — Supported language codes
- `REGIONAL_VARIANTS` — Variants to remove during cleanup
**Best Practices**:
- Always add translator comments for strings with placeholders
- Use ordered placeholder syntax: `%1$d`, `%2$s` (not just `%d`, `%s`)
- Keep strings complete and translatable (don't break into fragments)
- Commit `.po` and `.mo` files to git along with source changes
For detailed documentation, see:
- `dev-docs/I18N_QUICK_REFERENCE.md` — Quick start guide
- `dev-docs/I18N_OPTIMIZATIONS.md` — Best practices and examples
## Testing in Development
### Local WordPress Installation
@ -283,10 +358,11 @@ test -f assets/build/editor-blocks.asset.php && echo "✓ Asset manifest present
A convenience bash script is included to automate distribution packaging:
```bash
./build-distribution.sh
./dev-scripts/build-distribution.sh
```
This script:
- Creates a clean working directory
- Excludes all development files and dependencies
- Includes only files needed for distribution
@ -294,11 +370,13 @@ This script:
- Displays summary information
**Excluded from the distribution**:
- `node_modules/` (development dependencies)
- `src/` (source code for building)
- `test/` (test files)
- `package.json` / `package-lock.json`
- `webpack.config.js` (build config)
- `dev-scripts/` (development scripts)
- `README-DEV.md` (developer guide)
- `.git`, `.gitignore`, `.DS_Store`, and other system files
@ -360,6 +438,7 @@ unzip -l dist/swiss-football-matchdata.zip | head -30
### 5. Size Check
The distribution ZIP should be reasonable in size:
- **With `assets/build/editor-blocks.js`**: ~150300 KB
- **Typical unzipped size**: ~800 KB2 MB
@ -440,6 +519,7 @@ jobs:
```
This ensures:
- Every push/PR has a valid build
- Missing build artifacts fail visibly
- PHP code is syntactically correct
@ -501,7 +581,8 @@ npm start
| Install deps | `npm install` |
| Build for production | `npm run build` |
| Development with hot reload | `npm start` |
| Create distribution ZIP | `./build-distribution.sh` |
| Extract and translate strings | `./dev-scripts/i18n-manage.sh all` |
| Create distribution ZIP | `./dev-scripts/build-distribution.sh` |
| Check PHP syntax | `php -l includes/class-swi-foot-blocks.php` |
| Fix linting issues | `npx eslint src/ --fix` |
@ -512,6 +593,7 @@ This plugin integrates with the Swiss Football Association Club API. The API str
**Swagger UI**: https://stg-club-api-services.football.ch/swagger/index.html
This documentation provides:
- All available REST endpoints
- Request/response schemas
- Authentication requirements
@ -519,6 +601,7 @@ This documentation provides:
- Example requests and responses
Refer to this documentation when:
- Adding new API integrations
- Understanding the data structures used in the plugin
- Debugging API-related issues

103
README.md
View File

@ -35,19 +35,7 @@ This section explains how the plugin's Gutenberg editor blocks are intended to b
- Works in any paragraph or text-based block.
- The shortcode generates dynamically at render time on the frontend.
#### 2. Team Data Block
- **Purpose**: Create and insert shortcodes using a guided block interface.
- **How to use**:
1. Insert the `Swiss Football Team Data` block in your post/page.
2. Open the Inspector Controls (right sidebar).
3. Select a team from the dropdown.
4. Choose what to display (Standings or Match).
5. If you chose Match, select the specific match.
6. Save the post — the block generates and saves the appropriate shortcode.
- **Best for**: Users who prefer a visual block interface over toolbars.
#### 3. Context Provider Block (for scoped data)
#### 2. Context Provider Block (for scoped data)
- **Purpose**: Provide shared contextual data (season, club/team selection) for child blocks placed inside it. It saves JSON to the frontend as a `data-swi-foot-context` attribute on the provider's wrapper element.
- **How to use**:
@ -56,7 +44,7 @@ This section explains how the plugin's Gutenberg editor blocks are intended to b
3. Drop child blocks inside the Context block — these child blocks will inherit the contextual settings.
4. On save, the provider writes a `data-swi-foot-context` attribute containing a compact JSON string with the configured keys and values. This attribute is used by frontend renderers.
#### 4. Specialized Blocks (Standings, Schedule, Roster, Events)
#### 3. Specialized Blocks (Standings, Schedule, Roster, Events)
- **Swiss Football Standings**
- Purpose: Display a league standings table for a team.
@ -80,15 +68,76 @@ This section explains how the plugin's Gutenberg editor blocks are intended to b
## Shortcodes
Shortcodes provide additional flexibility where blocks are not desirable. Examples:
Shortcodes provide additional flexibility where blocks are not desirable.
### Context Inheritance
Most shortcodes listed below support **context inheritance** from a parent `[swi_foot_context]` block. When a `match_id` or `team_id` is not explicitly provided as a parameter, the shortcode will attempt to use values from the current block context.
**Example with context:**
```html
[swi_foot_context match_id="12345"]
[swi_foot_events] <!-- Automatically uses match_id from context -->
[swi_foot_roster side="home"] <!-- Automatically uses match_id from context -->
[/swi_foot_context]
```
**Example without context (explicit parameters required):**
```html
[swi_foot_events match_id="12345"]
[swi_foot_roster match_id="12345" side="home"]
```
### Match Display & Elements
```html
[swi_foot_match match_id="12345"]
[swi_foot_match_home_team match_id="12345"]
[swi_foot_match_away_team match_id="12345"]
[swi_foot_match_date match_id="12345" format="d.m.Y"]
[swi_foot_match_time match_id="12345"]
[swi_foot_match_venue match_id="12345"]
[swi_foot_match_score match_id="12345"]
[swi_foot_match_status match_id="12345"]
[swi_foot_match_league match_id="12345"]
[swi_foot_match_round match_id="12345"]
```
Refer to the original `readme.txt` for a complete list of shortcodes and their parameters.
### Team & Standings
```html
[swi_foot_standings team_id="42"]
```
### Match Roster & Bench
```html
[swi_foot_roster match_id="12345" side="home|away" starting_squad="true|false" bench="true|false"]
[swi_foot_bench match_id="12345"]
```
- `[swi_foot_roster]` — Display players for a match.
- `side="home"` or `side="away"` — Show only one team (defaults to "home").
- `starting_squad="true|false"` — Include starting squad (defaults to true).
- `bench="true|false"` — Include bench/substitutes (defaults to false).
- Players are distinguished by `assignmentRoleId`: 0 = starting squad, non-zero = bench.
- When both are shown, lists are separated by a horizontal rule.
- `[swi_foot_bench]` — Display team staff and substitutes.
- Automatically shows both home and away team staff.
- `match_id` required; will auto-populate from context if in a context provider.
### Match Events & Referees
```html
[swi_foot_events match_id="12345" event_order="dynamic|newest_first|oldest_first"]
[swi_foot_referees match_id="12345"]
```
- `[swi_foot_events]` — Show match events (goals, cards, substitutions, etc.) with configurable sort order.
- `[swi_foot_referees]` — Display match referees with their roles formatted as "Role: Firstname Name" (name in bold).
Refer to the original `readme.txt` for additional shortcode details and parameters.
## Installation
@ -104,28 +153,6 @@ Refer to the original `readme.txt` for a complete list of shortcodes and their p
- **Verein ID / Season ID**: used to scope team/season data.
- **Cache Duration**: how long API responses are cached (default is 30 seconds; configurable).
## Developer Notes
- Block registrations and editor scripts live in:
- [includes/class-swi-foot-blocks.php](includes/class-swi-foot-blocks.php)
- [assets/editor-blocks.js](assets/editor-blocks.js)
- [assets/build/index.js](assets/build/index.js)
- The editor script includes defensive `safeRegisterBlockType` and `safeRegisterFormatType` wrappers to avoid runtime errors when WordPress' editor APIs are not available.
- Blocks are written to use `apiVersion: 3` and expect modern Gutenberg APIs.
## Headless / Automated Checks
For quick verification that the built bundle registers the plugin's blocks and formats, a small Node-based test helper exists at `test/register-check.js`.
Run it locally with:
```bash
node test/register-check.js
```
This script stubs a minimal `window.wp` environment to confirm that `swi-foot/context` and the inline format are registered by the built bundle.
## Support
For support or bug reports, open an issue in the project's tracker or contact the plugin author.

View File

@ -12,6 +12,35 @@
color: #23282d;
}
/* Form actions with buttons */
.swi-foot-form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
margin-bottom: 10px;
}
.swi-foot-form-actions button {
flex-shrink: 0;
}
/* Connection status messages */
#connection-status {
display: inline-block;
margin-left: 15px;
font-weight: 500;
line-height: 1.5;
min-height: 22px;
}
#connection-status.success {
color: #046b3a;
}
#connection-status.error {
color: #d63638;
}
/* Teams grid layout */
.swi-foot-teams-grid {
display: grid;

View File

@ -2,6 +2,96 @@
jQuery(document).ready(function($) {
'use strict';
// Settings form with Save and Test functionality
$('#swi-foot-settings-form').on('submit', function(e) {
var $form = $(this);
var $status = $('#connection-status');
var action = $('[name="swi_foot_action"]:focus').val() || 'save_only';
e.preventDefault();
// Get form data
var formData = new FormData($form[0]);
// Submit form via WordPress options.php
fetch($form.attr('action'), {
method: 'POST',
credentials: 'same-origin',
body: formData
}).then(function(resp) {
// After form saves successfully, test if requested
if (action === 'save_and_test') {
return test_api_connection($status);
} else {
$('<div class="notice notice-info is-dismissible"><p>Settings saved!</p></div>')
.insertAfter('.wrap h1');
}
}).catch(function(err) {
$('<div class="notice notice-error is-dismissible"><p>Error saving settings: ' + err.message + '</p></div>')
.insertAfter('.wrap h1');
});
});
// Function to test API connection
function test_api_connection($statusElement) {
// Get current field values from form
var apiUrl = $('input[name="swi_foot_api_base_url"]').val();
var username = $('input[name="swi_foot_api_username"]').val();
var password = $('input[name="swi_foot_api_password"]').val();
// Validate credentials first
if (!username || !password) {
$('<div class="notice notice-error is-dismissible"><p>API credentials not configured. Please configure Username and Password.</p></div>')
.insertAfter('.wrap h1');
return;
}
if (!apiUrl) {
$('<div class="notice notice-error is-dismissible"><p>API Base URL not configured.</p></div>')
.insertAfter('.wrap h1');
return;
}
// Step 1: Check if URL is reachable with a HEAD request
var urlTest = apiUrl.replace(/\/$/, '') + '/';
fetch(urlTest, {
method: 'HEAD',
credentials: 'same-origin',
mode: 'no-cors'
}).then(function(resp) {
// Step 2: Make actual API call via REST endpoint
return fetch(swi_foot_ajax.rest_url.replace(/\/$/, '') + '/admin/test-connection', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': swi_foot_ajax.rest_nonce
},
body: JSON.stringify({ test: true })
});
}).then(function(resp) {
return resp.json();
}).then(function(response) {
if (response && response.success) {
$('<div class="notice notice-success is-dismissible"><p>Settings saved and connection test successful!</p></div>')
.insertAfter('.wrap h1');
} else {
var errorMsg = (response.error || 'Connection failed');
var details = response.details ? ': ' + response.details : '';
$('<div class="notice notice-error is-dismissible"><p>' + errorMsg + details + '</p></div>')
.insertAfter('.wrap h1');
}
}).catch(function(err) {
$('<div class="notice notice-error is-dismissible"><p>API URL not reachable: ' + apiUrl + '</p></div>')
.insertAfter('.wrap h1');
});
}
// Handle button clicks to set which action was used
$('button[name="swi_foot_action"]').on('click', function() {
$(this).focus();
});
// Refresh teams functionality
$('#refresh-teams').on('click', function() {
var $button = $(this);
@ -18,13 +108,13 @@ jQuery(document).ready(function($) {
return resp.json();
}).then(function(response) {
if (response && response.success) {
$status.text('Teams refreshed successfully!').addClass('success');
$status.text('Teams refreshed successfully!').addClass('success');
setTimeout(function() { location.reload(); }, 1500);
} else {
$status.text('Error: ' + (response.error || 'Unknown')).addClass('error');
$status.text('Error: ' + (response.error || 'Unknown')).addClass('error');
}
}).catch(function() {
$status.text('Network error occurred.').addClass('error');
$status.text('Network error occurred.').addClass('error');
}).finally(function() {
$button.prop('disabled', false).text('Refresh Teams List');
});
@ -48,47 +138,16 @@ jQuery(document).ready(function($) {
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': swi_foot_ajax.rest_nonce }
}).then(function(resp) { return resp.json(); }).then(function(response) {
if (response && response.success) {
$status.text('Cache cleared successfully!').addClass('success');
$status.text('Cache cleared successfully!').addClass('success');
setTimeout(function() { location.reload(); }, 1000);
} else {
$status.text('Error clearing cache.').addClass('error');
$status.text('Error clearing cache.').addClass('error');
}
}).catch(function() {
$status.text('Error clearing cache.').addClass('error');
$status.text('Error clearing cache.').addClass('error');
}).finally(function() {
$button.prop('disabled', false);
});
});
// Test API connection
$('#test-connection').on('click', function() {
var $button = $(this);
var $status = $('#connection-status');
$button.prop('disabled', true).text('Testing...');
$status.text('').removeClass('success error');
fetch(swi_foot_ajax.rest_url.replace(/\/$/, '') + '/admin/test-connection', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': swi_foot_ajax.rest_nonce }
}).then(function(resp) { return resp.json(); }).then(function(response) {
if (response && response.success) {
$status.text('Connection successful!').addClass('success');
} else {
$status.text('Connection failed: ' + (response.error || 'Unknown')).addClass('error');
}
}).catch(function() {
$status.text('Network error occurred.').addClass('error');
}).finally(function() {
$button.prop('disabled', false).text('Test API Connection');
});
});
// Auto-save settings notice
$('form').on('submit', function() {
$('<div class="notice notice-info is-dismissible"><p>Settings saved! The plugin will automatically refresh API tokens as needed.</p></div>')
.insertAfter('.wrap h1');
});
});

View File

@ -143,7 +143,7 @@
font-weight: 600;
}
/* Error messages */
/* Error messages - for configuration/setup issues */
.swi-foot-error {
color: #721c24;
padding: 12px 16px;
@ -154,6 +154,39 @@
font-weight: 500;
}
/* Warning messages - yellow style for "data not yet available" (406) */
.swi-foot-data-warning {
color: #856404;
padding: 12px 16px;
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
margin: 15px 0;
font-weight: 500;
}
/* Unavailable data messages - grey style for "no data available" */
.swi-foot-data-unavailable {
color: #6c757d;
padding: 12px 16px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
margin: 15px 0;
font-weight: 500;
}
/* Notice messages - informational style (deprecated, use data-warning or data-unavailable) */
.swi-foot-notice {
color: #6c757d;
padding: 12px 16px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
margin: 15px 0;
font-weight: 500;
}
/* Responsive design */
@media (max-width: 768px) {
.swi-foot-table {
@ -379,3 +412,40 @@
max-height: 80px;
}
}
/* Roster and Bench Player Formatting */
.swi-foot-position {
color: #6c757d;
font-size: 0.9em;
font-weight: 400;
font-style: italic;
}
.swi-foot-role {
color: #6c757d;
font-size: 0.9em;
font-weight: 400;
font-style: italic;
margin-left: 4px;
}
.roster-player-captain {
font-weight: 500;
color: #d4a017;
}
.swi-foot-captain-badge {
display: inline-block;
background-color: #d4a017;
color: #fff;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
border-radius: 50%;
font-weight: 700;
font-size: 12px;
margin-right: 4px;
vertical-align: middle;
}

653
assets/editor-blocks.js Normal file
View File

@ -0,0 +1,653 @@
// Swiss Football Gutenberg Blocks - Refactored User Flow
(function (blocks, element, editor, components, i18n, data) {
'use strict';
const { registerBlockType } = blocks;
const { createElement: el, useState, useEffect } = element;
const { InspectorControls } = editor;
const { PanelBody, SelectControl, TextControl, ToggleControl, Spinner, RangeControl } = components;
const { __ } = i18n;
// Helper function to make AJAX calls
function makeAjaxCall(action, data = {}) {
return new Promise((resolve, reject) => {
jQuery.post(swiFootEditorData.ajax_url, {
action: action,
nonce: swiFootEditorData.nonce,
...data
}, function (response) {
if (response && response.success) {
resolve(response);
} else {
reject(response);
}
}).fail(reject);
});
}
// Main Swiss Football Block - Refactored Logic
registerBlockType('swi-foot/team-data', {
title: __('Swiss Football Team Data', 'swi_foot_matchdata'),
description: __('Display team standings or match data with guided selection', 'swi_foot_matchdata'),
icon: 'universal-access-alt',
category: 'widgets',
keywords: [
__('football', 'swi_foot_matchdata'),
__('soccer', 'swi_foot_matchdata'),
__('team', 'swi_foot_matchdata'),
__('swiss', 'swi_foot_matchdata'),
],
attributes: {
selectedTeam: {
type: 'string',
default: ''
},
dataType: {
type: 'string',
default: '' // 'match' or 'stats'
},
selectedMatch: {
type: 'string',
default: ''
},
shortcodeType: {
type: 'string',
default: 'match'
},
format: {
type: 'string',
default: ''
},
separator: {
type: 'string',
default: ':'
}
},
edit: function (props) {
const { attributes, setAttributes } = props;
const { selectedTeam, dataType, selectedMatch, shortcodeType, format, separator } = attributes;
// State for dynamic data
const [teams, setTeams] = useState([]);
const [matches, setMatches] = useState([]);
const [loadingTeams, setLoadingTeams] = useState(true);
const [loadingMatches, setLoadingMatches] = useState(false);
// Load teams on component mount
useEffect(() => {
makeAjaxCall('swi_foot_get_teams_for_editor')
.then(response => {
if (response.success && response.data) {
setTeams(response.data);
}
setLoadingTeams(false);
})
.catch(() => {
setLoadingTeams(false);
});
}, []);
// Load matches when team is selected and dataType is 'match'
useEffect(() => {
if (selectedTeam && dataType === 'match') {
setLoadingMatches(true);
makeAjaxCall('swi_foot_get_matches_for_team', { team_id: selectedTeam })
.then(response => {
if (response.success && response.data) {
setMatches(response.data);
}
setLoadingMatches(false);
})
.catch(() => {
setLoadingMatches(false);
});
} else {
setMatches([]);
}
}, [selectedTeam, dataType]);
// Build shortcode/content preview
let content = '';
if (selectedTeam && dataType === 'stats') {
content = '[swi_foot_standings team_id="' + selectedTeam + '"]';
} else if (selectedTeam && dataType === 'match' && selectedMatch) {
if (selectedMatch === 'current') {
content = '[swi_foot_' + shortcodeType + ' team_id="' + selectedTeam + '" show_current="true"';
} else {
content = '[swi_foot_' + shortcodeType + ' match_id="' + selectedMatch + '"';
}
if (format && (shortcodeType === 'match_date' || shortcodeType === 'match_time')) {
content += ' format="' + format + '"';
}
if (separator !== ':' && shortcodeType === 'match_score') {
content += ' separator="' + separator + '"';
}
content += ']';
}
// Data type options
const dataTypeOptions = [
{ value: '', label: __('Choose what to display...', 'swi_foot_matchdata') },
{ value: 'stats', label: __('Team Statistics (Standings/Ranking)', 'swi_foot_matchdata') },
{ value: 'match', label: __('Match Data', 'swi_foot_matchdata') }
];
// Match data type options
const matchDataOptions = [
{ value: 'match', label: __('Complete Match Information', 'swi_foot_matchdata') },
{ value: 'match_home_team', label: __('Home Team Name Only', 'swi_foot_matchdata') },
{ value: 'match_away_team', label: __('Away Team Name Only', 'swi_foot_matchdata') },
{ value: 'match_date', label: __('Match Date Only', 'swi_foot_matchdata') },
{ value: 'match_time', label: __('Match Time Only', 'swi_foot_matchdata') },
{ value: 'match_venue', label: __('Venue/Location Only', 'swi_foot_matchdata') },
{ value: 'match_score', label: __('Score Only', 'swi_foot_matchdata') },
{ value: 'match_status', label: __('Match Status Only', 'swi_foot_matchdata') },
{ value: 'match_league', label: __('League Name Only', 'swi_foot_matchdata') },
{ value: 'match_round', label: __('Round Number Only', 'swi_foot_matchdata') }
];
return el('div', { className: 'swi-foot-team-data-block' },
el(InspectorControls, {},
el(PanelBody, { title: __('Team Selection', 'swi_foot_matchdata') },
// Step 1: Team selection
loadingTeams ? el(Spinner) : el(SelectControl, {
label: __('1. Select Team', 'swi_foot_matchdata'),
value: selectedTeam,
options: [{ value: '', label: __('Choose a team...', 'swi_foot_matchdata') }].concat(
teams.map(team => ({
value: team.value,
label: team.label
}))
),
onChange: function (value) {
setAttributes({
selectedTeam: value,
dataType: '', // Reset when team changes
selectedMatch: ''
});
}
}),
// Step 2: Data type selection (only show if team is selected)
selectedTeam && el(SelectControl, {
label: __('2. What would you like to display?', 'swi_foot_matchdata'),
value: dataType,
options: dataTypeOptions,
onChange: function (value) {
setAttributes({
dataType: value,
selectedMatch: '' // Reset match when data type changes
});
}
})
),
// Match-specific settings
dataType === 'match' && el(PanelBody, { title: __('Match Settings', 'swi_foot_matchdata') },
// Match selection
loadingMatches ? el(Spinner) : el(SelectControl, {
label: __('3. Select Match', 'swi_foot_matchdata'),
value: selectedMatch,
options: [{ value: '', label: __('Choose a match...', 'swi_foot_matchdata') }].concat(
[{ value: 'current', label: __('Current Match (Next upcoming or recent)', 'swi_foot_matchdata') }],
matches.map(match => ({
value: match.value,
label: match.label
}))
),
onChange: function (value) { setAttributes({ selectedMatch: value }); }
}),
// Match data type (what part of match to show)
selectedMatch && el(SelectControl, {
label: __('4. What match information to display?', 'swi_foot_matchdata'),
value: shortcodeType,
options: matchDataOptions,
onChange: function (value) { setAttributes({ shortcodeType: value }); }
})
),
// Advanced options for match data
dataType === 'match' && selectedMatch && el(PanelBody, { title: __('Advanced Options', 'swi_foot_matchdata'), initialOpen: false },
(shortcodeType === 'match_date' || shortcodeType === 'match_time') && el(SelectControl, {
label: __('Date/Time Format', 'swi_foot_matchdata'),
value: format,
options: shortcodeType === 'match_date' ? [
{ value: '', label: __('Default (WordPress setting)', 'swi_foot_matchdata') },
{ value: 'd.m.Y', label: '31.12.2024 (European)' },
{ value: 'm/d/Y', label: '12/31/2024 (US)' },
{ value: 'F j, Y', label: 'December 31, 2024' },
{ value: 'D, M j', label: 'Tue, Dec 31' }
] : [
{ value: '', label: __('Default (WordPress setting)', 'swi_foot_matchdata') },
{ value: 'H:i', label: '14:30 (24-hour)' },
{ value: 'g:i A', label: '2:30 PM (12-hour)' },
{ value: 'g:i', label: '2:30 (12-hour no AM/PM)' }
],
onChange: function (value) { setAttributes({ format: value }); }
}),
shortcodeType === 'match_score' && el(SelectControl, {
label: __('Score Separator', 'swi_foot_matchdata'),
value: separator,
options: [
{ value: ':', label: '2 : 1 (colon with spaces)' },
{ value: '-', label: '2 - 1 (dash with spaces)' },
{ value: ' vs ', label: '2 vs 1 (versus)' },
{ value: '|', label: '2 | 1 (pipe)' }
],
onChange: function (value) { setAttributes({ separator: value }); }
})
)
),
// Main preview area
el('div', { className: 'swi-foot-preview' },
el('div', {
style: {
padding: '30px',
textAlign: 'center',
background: '#f8f9fa',
border: '2px dashed #ddd',
borderRadius: '8px'
}
},
el('h4', { style: { marginTop: 0 } }, __('Swiss Football Team Data', 'swi_foot_matchdata')),
!selectedTeam && el('p', { style: { color: '#666' } },
__('1. Please select a team to get started', 'swi_foot_matchdata')
),
selectedTeam && !dataType && el('p', { style: { color: '#666' } },
__('2. Please choose what you want to display', 'swi_foot_matchdata')
),
selectedTeam && dataType === 'stats' && el('p', { style: { color: '#28a745' } },
'✓ ' + __('Will display team standings/ranking', 'swi_foot_matchdata')
),
selectedTeam && dataType === 'match' && !selectedMatch && el('p', { style: { color: '#666' } },
__('3. Please select a match', 'swi_foot_matchdata')
),
selectedTeam && dataType === 'match' && selectedMatch && el('div', {},
el('p', { style: { color: '#28a745' } },
'✓ ' + __('Ready to display match data', 'swi_foot_matchdata')
),
el('p', { style: { fontSize: '14px', color: '#666' } },
__('Generated shortcode:', 'swi_foot_matchdata')
),
el('code', {
style: {
display: 'block',
padding: '10px',
background: '#fff',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'Monaco, Menlo, monospace',
fontSize: '12px',
wordBreak: 'break-all'
}
}, content)
)
)
)
);
},
save: function (props) {
const { attributes } = props;
const { selectedTeam, dataType, selectedMatch, shortcodeType, format, separator } = attributes;
// Build shortcode for saving
let shortcode = '';
if (selectedTeam && dataType === 'stats') {
shortcode = '[swi_foot_standings team_id="' + selectedTeam + '"]';
} else if (selectedTeam && dataType === 'match' && selectedMatch) {
if (selectedMatch === 'current') {
shortcode = '[swi_foot_' + shortcodeType + ' team_id="' + selectedTeam + '" show_current="true"';
} else {
shortcode = '[swi_foot_' + shortcodeType + ' match_id="' + selectedMatch + '"';
}
if (format && (shortcodeType === 'match_date' || shortcodeType === 'match_time')) {
shortcode += ' format="' + format + '"';
}
if (separator !== ':' && shortcodeType === 'match_score') {
shortcode += ' separator="' + separator + '"';
}
shortcode += ']';
}
return shortcode;
}
});
// Match Roster Block with Side Selector and Bench Option
registerBlockType('swi-foot/match-roster', {
title: __('Swiss Football Match Roster', 'swi_foot_matchdata'),
description: __('Display match roster for home or away team, with optional bench players', 'swi_foot_matchdata'),
icon: 'groups',
category: 'widgets',
keywords: [
__('roster', 'swi_foot_matchdata'),
__('players', 'swi_foot_matchdata'),
__('lineup', 'swi_foot_matchdata'),
__('team', 'swi_foot_matchdata'),
],
attributes: {
selectedTeam: {
type: 'string',
default: ''
},
selectedMatch: {
type: 'string',
default: ''
},
side: {
type: 'string',
default: 'home' // home or away
},
withBench: {
type: 'boolean',
default: false
}
},
edit: function (props) {
const { attributes, setAttributes } = props;
const { selectedTeam, selectedMatch, side, withBench } = attributes;
const [teams, setTeams] = wp.element.useState([]);
const [matches, setMatches] = wp.element.useState([]);
const [loadingTeams, setLoadingTeams] = wp.element.useState(true);
const [loadingMatches, setLoadingMatches] = wp.element.useState(false);
// Helper AJAX call
function makeAjaxCall(action, data = {}) {
return new Promise((resolve, reject) => {
jQuery.post(swiFootEditorData.ajax_url, {
action: action,
nonce: swiFootEditorData.nonce,
...data
}, function (response) {
if (response && response.success) {
resolve(response);
} else {
reject(response);
}
}).fail(reject);
});
}
// Load teams
wp.element.useEffect(() => {
setLoadingTeams(true);
makeAjaxCall('swi_foot_get_teams_for_editor')
.then(response => {
if (Array.isArray(response.data)) {
setTeams(response.data);
}
setLoadingTeams(false);
})
.catch(() => setLoadingTeams(false));
}, []);
// Load matches when team changes
wp.element.useEffect(() => {
if (selectedTeam) {
setLoadingMatches(true);
makeAjaxCall('swi_foot_get_matches_for_team', { team_id: selectedTeam })
.then(response => {
if (Array.isArray(response.data)) {
setMatches(response.data);
}
setLoadingMatches(false);
})
.catch(() => setLoadingMatches(false));
}
}, [selectedTeam]);
return el('div', { className: 'swi-foot-roster-block' },
el(InspectorControls, {},
el(PanelBody, { title: __('Match Selection', 'swi_foot_matchdata') },
// Team selector
loadingTeams ? el(Spinner) : el(SelectControl, {
label: __('Select Team', 'swi_foot_matchdata'),
value: selectedTeam,
options: [{ value: '', label: __('Choose a team...', 'swi_foot_matchdata') }].concat(
teams.map(team => ({ value: team.value, label: team.label }))
),
onChange: function (value) {
setAttributes({ selectedTeam: value, selectedMatch: '' });
}
}),
// Match selector (shows after team is selected)
selectedTeam && (loadingMatches ? el(Spinner) : el(SelectControl, {
label: __('Select Match', 'swi_foot_matchdata'),
value: selectedMatch,
options: [{ value: '', label: __('Choose a match...', 'swi_foot_matchdata') }].concat(
[{ value: 'current', label: __('Current Match', 'swi_foot_matchdata') }],
matches.map(match => ({ value: match.value, label: match.label }))
),
onChange: function (value) { setAttributes({ selectedMatch: value }); }
})),
// Side selector (Home/Away)
selectedMatch && el(SelectControl, {
label: __('Select Side', 'swi_foot_matchdata'),
value: side,
options: [
{ value: 'home', label: __('Home Team', 'swi_foot_matchdata') },
{ value: 'away', label: __('Away Team', 'swi_foot_matchdata') }
],
onChange: function (value) { setAttributes({ side: value }); }
}),
// Include bench toggle
selectedMatch && el(ToggleControl, {
label: __('Include Bench Players', 'swi_foot_matchdata'),
help: __('When enabled, bench players will also be displayed', 'swi_foot_matchdata'),
checked: withBench,
onChange: function (value) { setAttributes({ withBench: value }); }
})
)
),
// Block preview
el('div', {
style: {
padding: '30px',
textAlign: 'center',
background: '#f8f9fa',
border: '2px dashed #ddd',
borderRadius: '8px'
}
},
el('h4', { style: { marginTop: 0 } }, __('Match Roster', 'swi_foot_matchdata')),
(!selectedTeam || !selectedMatch) ?
el('p', { style: { color: '#666' } },
__('Please select team, match, side, and bench option', 'swi_foot_matchdata')
)
:
el('p', { style: { color: '#28a745' } },
'✓ ' + __('Roster will be displayed for ' + side + ' team' + (withBench ? ' (with bench)' : ''), 'swi_foot_matchdata')
)
)
);
},
save: function (props) {
const { selectedTeam, selectedMatch, side, withBench } = props.attributes;
if (!selectedTeam || !selectedMatch || !side) return '';
let shortcode = '[swi_foot_roster ';
if (selectedMatch === 'current') {
shortcode += 'team_id="' + selectedTeam + '" show_current="true"';
} else {
shortcode += 'match_id="' + selectedMatch + '"';
}
shortcode += ' side="' + side + '"';
if (withBench) {
shortcode += ' with_bench="true"';
}
shortcode += ']';
return shortcode;
}
});
// Match Events Block
registerBlockType('swi-foot/match-events', {
title: __('Swiss Football Match Events', 'swi_foot_matchdata'),
description: __('Display live match events with auto-refresh', 'swi_foot_matchdata'),
icon: 'list-view',
category: 'widgets',
keywords: [
__('events', 'swi_foot_matchdata'),
__('live', 'swi_foot_matchdata'),
__('timeline', 'swi_foot_matchdata'),
__('match', 'swi_foot_matchdata'),
],
attributes: {
selectedTeam: {
type: 'string',
default: ''
},
selectedMatch: {
type: 'string',
default: ''
},
refreshInterval: {
type: 'number',
default: 30
}
},
edit: function (props) {
const { attributes, setAttributes } = props;
const { selectedTeam, selectedMatch, refreshInterval } = attributes;
const [teams, setTeams] = useState([]);
const [matches, setMatches] = useState([]);
const [loadingTeams, setLoadingTeams] = useState(true);
const [loadingMatches, setLoadingMatches] = useState(false);
// Load teams
useEffect(() => {
makeAjaxCall('swi_foot_get_teams_for_editor')
.then(response => {
if (response.success && response.data) {
setTeams(response.data);
}
setLoadingTeams(false);
})
.catch(() => setLoadingTeams(false));
}, []);
// Load matches when team changes
useEffect(() => {
if (selectedTeam) {
setLoadingMatches(true);
makeAjaxCall('swi_foot_get_matches_for_team', { team_id: selectedTeam })
.then(response => {
if (response.success && response.data) {
setMatches(response.data);
}
setLoadingMatches(false);
})
.catch(() => setLoadingMatches(false));
}
}, [selectedTeam]);
return el('div', { className: 'swi-foot-events-block' },
el(InspectorControls, {},
el(PanelBody, { title: __('Match Selection', 'swi_foot_matchdata') },
loadingTeams ? el(Spinner) : el(SelectControl, {
label: __('Select Team', 'swi_foot_matchdata'),
value: selectedTeam,
options: [{ value: '', label: __('Choose a team...', 'swi_foot_matchdata') }].concat(
teams.map(team => ({ value: team.value, label: team.label }))
),
onChange: function (value) {
setAttributes({ selectedTeam: value, selectedMatch: '' });
}
}),
selectedTeam && (loadingMatches ? el(Spinner) : el(SelectControl, {
label: __('Select Match', 'swi_foot_matchdata'),
value: selectedMatch,
options: [{ value: '', label: __('Choose a match...', 'swi_foot_matchdata') }].concat(
[{ value: 'current', label: __('Current Match', 'swi_foot_matchdata') }],
matches.map(match => ({ value: match.value, label: match.label }))
),
onChange: function (value) { setAttributes({ selectedMatch: value }); }
}))
),
el(PanelBody, { title: __('Auto-Refresh Settings', 'swi_foot_matchdata') },
el(RangeControl, {
label: __('Refresh Interval (seconds)', 'swi_foot_matchdata'),
value: refreshInterval,
onChange: function (value) { setAttributes({ refreshInterval: value }); },
min: 10,
max: 300,
step: 10
})
)
),
el('div', {
style: {
padding: '30px',
textAlign: 'center',
background: '#f8f9fa',
border: '2px dashed #ddd',
borderRadius: '8px'
}
},
el('h4', { style: { marginTop: 0 } }, __('Live Match Events', 'swi_foot_matchdata')),
(!selectedTeam || !selectedMatch) ?
el('p', { style: { color: '#666' } }, __('Please select team and match', 'swi_foot_matchdata')) :
el('div', {},
el('p', { style: { color: '#28a745' } }, '✓ ' + __('Will display live match events', 'swi_foot_matchdata')),
el('p', { style: { fontSize: '12px', color: '#666' } },
__('Auto-refresh every', 'swi_foot_matchdata') + ' ' + refreshInterval + ' ' + __('seconds', 'swi_foot_matchdata'))
)
)
);
},
save: function (props) {
const { attributes } = props;
const { selectedTeam, selectedMatch, refreshInterval } = attributes;
if (!selectedTeam || !selectedMatch) return '';
let shortcode = '[swi_foot_events ';
if (selectedMatch === 'current') {
shortcode += 'team_id="' + selectedTeam + '" show_current="true"';
} else {
shortcode += 'match_id="' + selectedMatch + '"';
}
shortcode += ' refresh_interval="' + refreshInterval + '"]';
return shortcode;
}
});
})(
window.wp.blocks,
window.wp.element,
window.wp.blockEditor,
window.wp.components,
window.wp.i18n,
window.wp.data
);

View File

@ -2,7 +2,7 @@
"apiVersion": 3,
"name": "swi-foot/context",
"title": "Swiss Football Context (container)",
"category": "widgets",
"category": "swi-football",
"icon": "admin-site",
"description": "Provides a team/season/match context to child blocks. Children will inherit these settings unless they override them.",
"providesContext": {

View File

@ -0,0 +1,12 @@
{
"apiVersion": 3,
"name": "swi-foot/match-bench",
"title": "Swiss Football Match Bench",
"category": "swi-football",
"icon": "clipboard-user",
"description": "Display team staff and substitutes for a match",
"attributes": {
"side": { "type": "string", "default": "home" }
},
"usesContext": ["swi-foot/context"]
}

View File

@ -2,8 +2,8 @@
"apiVersion": 3,
"name": "swi-foot/match-events",
"title": "Swiss Football Match Events",
"category": "widgets",
"icon": "list-view",
"category": "swi-football",
"icon": "info",
"description": "Live match events with optional auto-refresh.",
"attributes": {
"selectedTeam": { "type": "string", "default": "" },

View File

@ -0,0 +1,15 @@
{
"apiVersion": 3,
"name": "swi-foot/match-referees",
"title": "Swiss Football Match Referees",
"category": "swi-football",
"icon": "whistle",
"description": "Display match officials and referees",
"attributes": {
"selectedMatch": {
"type": "string",
"default": ""
}
},
"usesContext": ["swi-foot/context"]
}

View File

@ -2,14 +2,15 @@
"apiVersion": 3,
"name": "swi-foot/match-roster",
"title": "Swiss Football Match Roster",
"category": "widgets",
"category": "swi-football",
"icon": "groups",
"description": "Display match roster for a selected match and side.",
"attributes": {
"selectedTeam": { "type": "string", "default": "" },
"selectedMatch": { "type": "string", "default": "" },
"side": { "type": "string", "default": "home" },
"withBench": { "type": "boolean", "default": false }
"showStartingSquad": { "type": "boolean", "default": true },
"showBench": { "type": "boolean", "default": false }
}
,
"usesContext": ["swi-foot/context"]

View File

@ -2,8 +2,8 @@
"apiVersion": 3,
"name": "swi-foot/schedule",
"title": "Swiss Football Schedule",
"category": "widgets",
"icon": "schedule",
"category": "swi-football",
"icon": "calendar",
"description": "Display upcoming matches for a team.",
"attributes": {
"teamId": {
@ -13,6 +13,11 @@
"limit": {
"type": "number",
"default": 5
},
"matchFilter": {
"type": "string",
"default": "all",
"enum": ["all", "home", "away"]
}
}
,

View File

@ -2,7 +2,7 @@
"apiVersion": 3,
"name": "swi-foot/standings",
"title": "Swiss Football Standings",
"category": "widgets",
"category": "swi-football",
"icon": "analytics",
"description": "Display current standings for a team.",
"attributes": {

View File

@ -1,18 +0,0 @@
{
"apiVersion": 3,
"name": "swi-foot/team-data",
"title": "Swiss Football Team Data",
"category": "widgets",
"icon": "universal-access-alt",
"description": "Guided team selector to insert shortcodes for various match/team displays.",
"attributes": {
"selectedTeam": { "type": "string", "default": "" },
"dataType": { "type": "string", "default": "" },
"selectedMatch": { "type": "string", "default": "" },
"shortcodeType": { "type": "string", "default": "match" },
"format": { "type": "string", "default": "" },
"separator": { "type": "string", "default": ":" }
}
,
"usesContext": ["swi-foot/context"]
}

View File

@ -5,8 +5,12 @@
set -e # Exit on error
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$(dirname "$SCRIPT_DIR")"
PLUGIN_NAME="swiss-football-matchdata"
DIST_DIR="dist"
DIST_DIR="$PLUGIN_ROOT/dist"
# Colors for output
GREEN='\033[0;32m'
@ -19,9 +23,9 @@ echo -e "${BLUE}=== Building Distribution Package ===${NC}"
echo "Creating distribution directory..."
mkdir -p "$DIST_DIR"
# Check if we're in the plugin root directory
if [ ! -f "package.json" ]; then
echo "Error: package.json not found. Run this script from the plugin root directory."
# Check if plugin root has package.json
if [ ! -f "$PLUGIN_ROOT/package.json" ]; then
echo "Error: package.json not found in $PLUGIN_ROOT"
exit 1
fi
@ -31,10 +35,11 @@ rm -rf "$DIST_DIR/$PLUGIN_NAME" "$DIST_DIR/$PLUGIN_NAME.zip"
# Copy all plugin files, excluding development artifacts
echo "Copying plugin files (excluding development files)..."
rsync -av \
rsync -av "$PLUGIN_ROOT/" \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.npm-cache' \
--exclude='package.json' \
--exclude='package-lock.json' \
--exclude='.gitignore' \
--exclude='README-DEV.md' \
@ -45,9 +50,10 @@ rsync -av \
--exclude='test/' \
--exclude='src/' \
--exclude='webpack.config.js' \
--exclude='build-distribution.sh' \
--exclude='dev-scripts/' \
--exclude='dev-docs/' \
--exclude='dist/' \
. "$DIST_DIR/$PLUGIN_NAME/" \
"$DIST_DIR/$PLUGIN_NAME/" \
> /dev/null 2>&1 || true
# Verify essential files are present
@ -64,9 +70,7 @@ fi
# Create ZIP archive
echo "Creating ZIP archive..."
cd "$DIST_DIR"
zip -r -q "$PLUGIN_NAME.zip" "$PLUGIN_NAME/"
cd ..
(cd "$DIST_DIR" && zip -r -q "$PLUGIN_NAME.zip" "$PLUGIN_NAME/")
# Get file sizes
DIST_SIZE=$(du -sh "$DIST_DIR/$PLUGIN_NAME" | cut -f1)
@ -95,6 +99,8 @@ echo "🚫 Excluded from distribution:"
echo " ✗ node_modules/ (development dependencies)"
echo " ✗ src/ (source files)"
echo " ✗ test/ (test files)"
echo " ✗ dev-scripts/ (scripts for development)"
echo " ✗ dev-docs/ (development documentation)"
echo " ✗ package.json / package-lock.json"
echo " ✗ webpack.config.js"
echo " ✗ README-DEV.md"

522
dev-scripts/i18n-manage.sh Executable file
View File

@ -0,0 +1,522 @@
#!/bin/bash
################################################################################
# Swiss Football Matchdata - i18n Management Script
#
# Complete i18n workflow using wp-cli:
# 1. Extract strings from PHP/JS to .pot file
# 2. Translate .pot to specified languages
# 3. Generate .mo files from .po files
# 4. Clean up regional language variants
#
# Usage:
# ./dev-scripts/i18n-manage.sh [extract|translate|build|clean|all]
#
# Commands:
# extract - Extract i18n strings and generate .pot file
# translate - Generate .po files for all languages (requires jq)
# build - Generate .mo files from .po files
# clean - Remove regional language variants
# all - Run all steps: extract → translate → build → clean
#
# Configuration:
# Modify the CONFIG section below to match your environment
#
# Dependencies:
# - wp-cli (via docker compose when WP_CLI_WRAPPER is used)
# - jq (for JSON translation data handling)
# - msgfmt (for generating .mo files)
#
################################################################################
set -e
################################################################################
# CONFIGURATION - Modify these values for your environment
################################################################################
# WordPress CLI command wrapper (for docker environments)
# Use local wp-cli: WP_CLI_CMD="wp"
# Use docker compose with working directory: WP_CLI_CMD="docker compose -f ~/Development/wordpress-dev/wp-dev-compose.yml run -w /var/www/html/wp-content/plugins/swiss-football-matchdata --rm wp-cli"
WP_CLI_CMD="docker compose -f ~/Development/wordpress-dev/wp-dev-compose.yml run -w /var/www/html/wp-content/plugins/swiss-football-matchdata --rm wp-cli"
# Plugin slug
PLUGIN_SLUG="swi_foot_matchdata"
# Text domain (must match in plugin)
TEXT_DOMAIN="swi_foot_matchdata"
# Languages to support: language_code:language_name
# Format: LANGUAGE_CODE|LANGUAGE_NAME (space-separated for compatibility)
LANGUAGES="de_DE:German fr_FR:French it_IT:Italian en_US:English"
# Regional variants to clean up (remove if they exist)
# These will be removed to keep only the main language codes
REGIONAL_VARIANTS=(
"de_AT" # Austrian German
"de_CH" # Swiss German
)
################################################################################
# COLORS & OUTPUT
################################################################################
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}${NC} $1"
}
log_success() {
echo -e "${GREEN}${NC} $1"
}
log_warning() {
echo -e "${YELLOW}${NC} $1"
}
log_error() {
echo -e "${RED}${NC} $1"
}
log_section() {
echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${CYAN} $1${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
}
################################################################################
# UTILITY FUNCTIONS
################################################################################
check_dependencies() {
local missing_deps=0
# Check for wp-cli
if ! eval "$WP_CLI_CMD --version" &>/dev/null; then
log_error "wp-cli not found or not accessible via: $WP_CLI_CMD"
missing_deps=1
fi
# Check for msgfmt (gettext tools)
if ! command -v msgfmt &>/dev/null; then
log_error "msgfmt not found. Install gettext tools:"
echo " macOS: brew install gettext"
echo " Ubuntu: sudo apt-get install gettext"
missing_deps=1
fi
# Check for jq (optional but recommended)
if ! command -v jq &>/dev/null; then
log_warning "jq not installed (optional). Install for better translation support:"
echo " macOS: brew install jq"
echo " Ubuntu: sudo apt-get install jq"
fi
if [[ $missing_deps -eq 1 ]]; then
return 1
fi
return 0
}
get_language_name() {
local code=$1
for pair in $LANGUAGES; do
local lang_code="${pair%:*}"
local lang_name="${pair#*:}"
if [[ "$lang_code" == "$code" ]]; then
echo "$lang_name"
return 0
fi
done
echo "Unknown"
}
################################################################################
# EXTRACTION
################################################################################
extract_strings() {
log_section "Extracting i18n Strings"
local pot_file="languages/${PLUGIN_SLUG}.pot"
local exclude_patterns="node_modules,vendor,assets/build,wp-json"
log_info "Extracting strings to: $pot_file"
# Create languages directory if it doesn't exist
mkdir -p languages
# Use wp-cli to extract strings
# Note: wp-cli automatically handles:
# - Translators comments (/* translators: ... */)
# - Multiple placeholders with ordered syntax (%1$d, %2$s, etc.)
# - WordPress i18n functions (__, _e, _x, _n, etc.)
# Syntax: wp i18n make-pot <source-directory> <output-file-path>
log_info "Running wp-cli extraction command..."
log_info " Working directory (Docker): /var/www/html/wp-content/plugins/swiss-football-matchdata"
log_info " Plugin directory (local): $(pwd)"
# Execute the extraction with proper output handling
# WP_CLI_CMD is already configured with -w flag, so . refers to the plugin directory
local extract_output
extract_output=$(eval "$WP_CLI_CMD i18n make-pot --exclude=$exclude_patterns --domain=$TEXT_DOMAIN . languages/${PLUGIN_SLUG}.pot 2>&1" || echo "FAILED")
# Small delay to ensure file is written
sleep 1
# Check if extraction was successful by verifying the file exists
local retries=3
local pot_exists=0
while [[ $retries -gt 0 ]] && [[ $pot_exists -eq 0 ]]; do
if [[ -f "$pot_file" ]]; then
pot_exists=1
break
fi
((retries--))
if [[ $retries -gt 0 ]]; then
log_warning "Pot file not found yet, retrying... ($retries attempts left)"
sleep 1
fi
done
if [[ $pot_exists -eq 1 ]]; then
log_success "Extracted strings to: $pot_file"
# Show statistics
local string_count=$(grep -c "^msgid \"" "$pot_file" 2>/dev/null || echo "0")
string_count=$(echo "$string_count" | tr -d ' \n')
log_info "Total strings found: $string_count"
# Check for obsolete entries
local obsolete_count=$(grep -c "^#~" "$pot_file" 2>/dev/null || echo "0")
obsolete_count=$(echo "$obsolete_count" | tr -d ' \n')
if [[ $obsolete_count -gt 0 ]]; then
log_warning "Found $obsolete_count obsolete entries (marked with #~)"
fi
return 0
else
log_error "Failed to generate .pot file"
log_error "wp-cli output: $extract_output"
log_error "Checked path: $pot_file (local filesystem)"
log_info "Directory context:"
log_info " • Local plugin dir: $(pwd)"
log_info " • Docker plugin dir: /var/www/html/wp-content/plugins/swiss-football-matchdata"
log_info " • Expected output (local): $(pwd)/languages/${PLUGIN_SLUG}.pot"
log_info ""
log_info "If the file exists in Docker but not locally, verify:"
log_info " 1. Docker volume mounting is correct"
log_info " 2. Run: docker compose -f ~/Development/wordpress-dev/wp-dev-compose.yml exec wp-cli ls -la /var/www/html/wp-content/plugins/swiss-football-matchdata/"
return 1
fi
}
################################################################################
# TRANSLATION
################################################################################
translate_strings() {
log_section "Generating .po files from .pot"
local pot_file="languages/${PLUGIN_SLUG}.pot"
if [[ ! -f "$pot_file" ]]; then
log_error ".pot file not found: $pot_file"
log_info "Run 'extract' command first"
return 1
fi
local created_count=0
for pair in $LANGUAGES; do
local lang_code="${pair%:*}"
local lang_name="${pair#*:}"
local po_file="languages/${PLUGIN_SLUG}-${lang_code}.po"
log_info "Processing: $lang_name ($lang_code)"
if [[ -f "$po_file" ]]; then
# Update existing .po file
log_info " Updating existing .po file..."
eval "$WP_CLI_CMD i18n update-po languages/${PLUGIN_SLUG}.pot languages/${PLUGIN_SLUG}-${lang_code}.po 2>/dev/null" || true
log_success " Updated: $po_file"
else
# Create new .po file from .pot
log_info " Creating new .po file..."
cp "$pot_file" "$po_file"
# Update language headers in .po file
sed -i.bak "s/Language: /Language: ${lang_code}/" "$po_file"
rm -f "${po_file}.bak"
log_success " Created: $po_file"
((created_count++))
fi
done
log_success "Created/Updated $created_count translation files"
return 0
}
################################################################################
# BUILD .MO FILES
################################################################################
build_mo_files() {
log_section "Generating .mo files"
local compiled_count=0
local errors=0
for pair in $LANGUAGES; do
local lang_code="${pair%:*}"
local lang_name="${pair#*:}"
local po_file="languages/${PLUGIN_SLUG}-${lang_code}.po"
local mo_file="languages/${PLUGIN_SLUG}-${lang_code}.mo"
if [[ -f "$po_file" ]]; then
log_info "Compiling: $lang_name ($lang_code)"
if msgfmt -o "$mo_file" "$po_file" 2>/dev/null; then
log_success " Generated: $mo_file"
((compiled_count++))
else
log_error " Failed to compile: $po_file"
((errors++))
fi
else
log_warning " .po file not found: $po_file"
fi
done
log_success "Compiled $compiled_count .mo files"
if [[ $errors -gt 0 ]]; then
log_warning "Encountered $errors errors during compilation"
return 1
fi
return 0
}
################################################################################
# CLEANUP
################################################################################
cleanup_regional_variants() {
log_section "Cleaning up regional language variants"
local removed_count=0
for variant in "${REGIONAL_VARIANTS[@]}"; do
# Check for .po files
local po_file="languages/${PLUGIN_SLUG}-${variant}.po"
if [[ -f "$po_file" ]]; then
log_info "Removing: $po_file"
rm -f "$po_file"
((removed_count++))
fi
# Check for .mo files
local mo_file="languages/${PLUGIN_SLUG}-${variant}.mo"
if [[ -f "$mo_file" ]]; then
log_info "Removing: $mo_file"
rm -f "$mo_file"
((removed_count++))
fi
# Also check for backup files created by sed
local po_backup="${po_file}.bak"
if [[ -f "$po_backup" ]]; then
rm -f "$po_backup"
fi
done
if [[ $removed_count -eq 0 ]]; then
log_info "No regional variants found to remove"
else
log_success "Removed $removed_count regional variant files"
fi
return 0
}
################################################################################
# DISPLAY SUMMARY
################################################################################
show_summary() {
log_section "i18n Management Summary"
echo -e "${YELLOW}📁 Language Files:${NC}\n"
for pair in $LANGUAGES; do
local lang_code="${pair%:*}"
local lang_name="${pair#*:}"
local po_file="languages/${PLUGIN_SLUG}-${lang_code}.po"
local mo_file="languages/${PLUGIN_SLUG}-${lang_code}.mo"
printf " %-20s %-20s " "$lang_code" "$lang_name"
if [[ -f "$po_file" ]] && [[ -f "$mo_file" ]]; then
local po_size=$(du -h "$po_file" | cut -f1)
local mo_size=$(du -h "$mo_file" | cut -f1)
echo -e "${GREEN}${NC} PO: $po_size, MO: $mo_size"
elif [[ -f "$po_file" ]]; then
local po_size=$(du -h "$po_file" | cut -f1)
echo -e "${YELLOW}${NC} PO: $po_size (no .mo)"
else
echo -e "${RED}${NC} Missing files"
fi
done
local lang_count=$(echo "$LANGUAGES" | wc -w)
echo -e "\n${YELLOW}Configuration:${NC}\n"
echo " Plugin slug: $PLUGIN_SLUG"
echo " Text domain: $TEXT_DOMAIN"
echo " Languages: $lang_count"
echo " Languages dir: languages/"
echo -e "\n${YELLOW}Next Steps:${NC}\n"
echo " • Verify .po files contain all necessary translations"
echo " • Update empty translation entries in .po files"
echo " • Re-run 'build' to regenerate .mo files after translations"
echo ""
}
################################################################################
# HELP
################################################################################
show_help() {
cat << 'EOF'
Usage: ./dev-scripts/i18n-manage.sh [COMMAND]
COMMANDS:
extract Extract i18n strings from PHP/JS files to .pot
translate Generate .po files for all configured languages
build Compile .po files to .mo files
clean Remove regional language variants
all Run all steps: extract → translate → build → clean
help Show this help message
EXAMPLES:
# Extract strings and generate translations
./dev-scripts/i18n-manage.sh extract
# Generate .po files for translation
./dev-scripts/i18n-manage.sh translate
# Compile .po to .mo files
./dev-scripts/i18n-manage.sh build
# Run complete workflow
./dev-scripts/i18n-manage.sh all
# Clean up regional variants
./dev-scripts/i18n-manage.sh clean
CONFIGURATION:
Edit the script's CONFIG section to adjust:
- WP_CLI_CMD: WordPress CLI command wrapper
- LANGUAGES: Supported language codes
- REGIONAL_VARIANTS: Language variants to remove
TRANSLATORS COMMENTS:
In PHP code, add translators comments above i18n functions:
/* translators: %s is the error message from the API */
__('Error loading teams: %s', 'swi_foot_matchdata')
For multiple placeholders, use ordered syntax:
/* translators: %1$d is count, %2$d is duration in seconds */
__('Currently caching %1$d records with %2$d second duration.', 'swi_foot_matchdata')
SUPPORTED LANGUAGES (default):
de_DE - German
fr_FR - French
it_IT - Italian
en_US - English (US)
DEPENDENCIES:
- wp-cli (WordPress CLI)
- msgfmt (gettext tools)
- jq (optional, for translation management)
EOF
}
################################################################################
# MAIN
################################################################################
main() {
local command="${1:-all}"
# Show banner
echo -e "${CYAN}╔════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ Swiss Football Matchdata - i18n Management ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════╝${NC}\n"
# Check dependencies
log_info "Checking dependencies..."
if ! check_dependencies; then
log_error "Missing required dependencies. Please install and try again."
exit 1
fi
log_success "All dependencies available\n"
# Execute command
case "$command" in
extract)
extract_strings
;;
translate)
translate_strings
show_summary
;;
build)
build_mo_files
show_summary
;;
clean)
cleanup_regional_variants
;;
all)
extract_strings && \
translate_strings && \
build_mo_files && \
cleanup_regional_variants && \
show_summary
;;
help|--help|-h)
show_help
;;
*)
log_error "Unknown command: $command"
echo ""
show_help
exit 1
;;
esac
local exit_code=$?
if [[ $exit_code -eq 0 ]]; then
log_success "\nTask completed successfully!"
else
log_error "\nTask failed with exit code $exit_code"
fi
exit $exit_code
}
# Run main function with all arguments
main "$@"

View File

@ -31,6 +31,7 @@ class Swi_Foot_Admin
register_setting('swi_foot_settings', 'swi_foot_verein_id');
register_setting('swi_foot_settings', 'swi_foot_season_id');
register_setting('swi_foot_settings', 'swi_foot_match_cache_duration');
register_setting('swi_foot_settings', 'swi_foot_api_language');
add_settings_section(
'swi_foot_api_section',
@ -44,6 +45,7 @@ class Swi_Foot_Admin
add_settings_field('swi_foot_api_password', __('API Password (Application Pass)', 'swi_foot_matchdata'), array($this, 'password_render'), 'swi_foot_settings', 'swi_foot_api_section');
add_settings_field('swi_foot_verein_id', __('Verein ID (Club ID)', 'swi_foot_matchdata'), array($this, 'verein_id_render'), 'swi_foot_settings', 'swi_foot_api_section');
add_settings_field('swi_foot_season_id', __('Season ID', 'swi_foot_matchdata'), array($this, 'season_id_render'), 'swi_foot_settings', 'swi_foot_api_section');
add_settings_field('swi_foot_api_language', __('API Response Language', 'swi_foot_matchdata'), array($this, 'language_render'), 'swi_foot_settings', 'swi_foot_api_section');
add_settings_section(
'swi_foot_cache_section',
@ -107,6 +109,17 @@ class Swi_Foot_Admin
echo '<p class="description">' . __('How long to cache match data in seconds (10-300)', 'swi_foot_matchdata') . '</p>';
}
public function language_render()
{
$language = get_option('swi_foot_api_language', '1');
echo '<select name="swi_foot_api_language">';
echo '<option value="1" ' . selected($language, '1', false) . '>' . __('German', 'swi_foot_matchdata') . '</option>';
echo '<option value="2" ' . selected($language, '2', false) . '>' . __('French', 'swi_foot_matchdata') . '</option>';
echo '<option value="3" ' . selected($language, '3', false) . '>' . __('Italian', 'swi_foot_matchdata') . '</option>';
echo '</select>';
echo '<p class="description">' . __('Select the language for API responses (German, French, or Italian)', 'swi_foot_matchdata') . '</p>';
}
public function admin_scripts($hook)
{
if ($hook === 'settings_page_swiss-football-matchdata') {
@ -248,23 +261,21 @@ class Swi_Foot_Admin
<div class="wrap">
<h1><?php _e('Swiss Football Matchdata Settings', 'swi_foot_matchdata'); ?></h1>
<form action="options.php" method="post">
<form action="options.php" method="post" id="swi-foot-settings-form">
<?php
settings_fields('swi_foot_settings');
do_settings_sections('swi_foot_settings');
submit_button();
?>
</form>
<div class="swi-foot-admin-section">
<h2><?php _e('Connection Test', 'swi_foot_matchdata'); ?></h2>
<p>
<button type="button" id="test-connection" class="button">
<?php _e('Test API Connection', 'swi_foot_matchdata'); ?>
<div class="swi-foot-form-actions">
<button type="submit" name="swi_foot_action" value="save_and_test" class="button button-primary">
<?php _e('Save and Test', 'swi_foot_matchdata'); ?>
</button>
<span id="connection-status"></span>
</p>
</div>
<button type="submit" name="swi_foot_action" value="save_only" class="button">
<?php _e('Save without Test', 'swi_foot_matchdata'); ?>
</button>
</div>
<span id="connection-status"></span>
</form>
<div class="swi-foot-admin-section">
<h2><?php _e('Team Management', 'swi_foot_matchdata'); ?></h2>
@ -325,7 +336,11 @@ class Swi_Foot_Admin
if (is_wp_error($teams)) {
echo '<p class="notice notice-error">' .
sprintf(__('Error loading teams: %s', 'swi_foot_matchdata'), $teams->get_error_message()) .
sprintf(
/* translators: %s is the error message from the API */
__('Error loading teams: %s', 'swi_foot_matchdata'),
$teams->get_error_message()
) .
'</p>';
return;
}
@ -357,7 +372,12 @@ class Swi_Foot_Admin
$cache_count = is_array($keys) ? count($keys) : 0;
$cache_duration = get_option('swi_foot_match_cache_duration', 30);
echo '<p>' . sprintf(__('Currently caching %d match records with %d second cache duration.', 'swi_foot_matchdata'), $cache_count, $cache_duration) . '</p>';
echo '<p>' . sprintf(
/* translators: %1$d is the number of cached match records, %2$d is the cache duration in seconds */
__('Currently caching %1$d match records with %2$d second cache duration.', 'swi_foot_matchdata'),
$cache_count,
$cache_duration
) . '</p>';
if ($cache_count > 0) {
$timestamps = array();
@ -460,7 +480,5 @@ class Swi_Foot_Admin
<?php
}
}
?>

View File

@ -9,6 +9,7 @@ class Swi_Foot_API
private $verein_id;
private $season_id;
private $cache_duration;
private $language;
public function __construct()
{
@ -18,6 +19,7 @@ class Swi_Foot_API
$this->verein_id = get_option('swi_foot_verein_id');
$this->season_id = get_option('swi_foot_season_id', date('Y'));
$this->cache_duration = get_option('swi_foot_match_cache_duration', 30);
$this->language = get_option('swi_foot_api_language', '1');
// AJAX actions were migrated to REST endpoints (see includes/class-swi-foot-rest.php)
@ -176,6 +178,7 @@ class Swi_Foot_API
$response = wp_remote_get($url, array(
'headers' => array(
'X-User-Token' => $token,
'X-User-Language' => $this->language,
'Content-Type' => 'application/json'
),
'timeout' => 30
@ -204,7 +207,13 @@ class Swi_Foot_API
if ($response_code !== 200) {
$body_debug = wp_remote_retrieve_body($response);
error_log('Swiss Football API: Request to ' . $url . ' returned ' . $response_code . ' - body: ' . substr($body_debug, 0, 1000));
return new WP_Error('api_error', 'API request failed with code ' . $response_code);
// Handle 406 Not Acceptable - data not yet available
if ($response_code === 406) {
return new WP_Error('data_not_available', 'Data not yet available', array('status_code' => 406));
}
return new WP_Error('api_error', 'API request failed with code ' . $response_code, array('status_code' => $response_code));
}
$body = wp_remote_retrieve_body($response);
@ -348,6 +357,11 @@ class Swi_Foot_API
return $this->api_request('/api/match/' . $match_id . '/events');
}
public function get_match_referees($match_id)
{
return $this->api_request('/api/match/' . $match_id . '/referees');
}
public function get_team_picture($team_id)
{
// Special handling for team picture endpoint which returns 200 with data or 204 No Content
@ -364,6 +378,7 @@ class Swi_Foot_API
$response = wp_remote_get($url, array(
'headers' => array(
'X-User-Token' => $token,
'X-User-Language' => $this->language,
'Content-Type' => 'application/json'
),
'timeout' => 30
@ -410,9 +425,9 @@ class Swi_Foot_API
return $body;
}
public function get_commons_ids()
public function get_common_ids($params = array())
{
return $this->api_request('/api/commons/ids');
return $this->api_request('/api/common/ids', $params);
}
/**
@ -441,17 +456,54 @@ class Swi_Foot_API
*/
public function test_connection()
{
// Actually test the connection by making an API request
$result = $this->api_request('/api/teams', array('vereinId' => $this->verein_id));
// Check if the request was successful (not an error)
if (is_wp_error($result)) {
error_log('Swiss Football API: Connection test failed - ' . $result->get_error_message());
return false;
// First, check if credentials are configured
if (empty($this->username) || empty($this->password)) {
return array(
'success' => false,
'error' => 'API credentials not configured',
'details' => 'Please configure API Username and Password in Settings → Swiss Football'
);
}
// Check if Verein ID is configured
if (empty($this->verein_id)) {
return array(
'success' => false,
'error' => 'Verein ID (Club ID) not configured',
'details' => 'Please configure Verein ID in Settings → Swiss Football'
);
}
// Verify we got a response (array means success)
return is_array($result);
// Test connection using the /api/common/ids endpoint with required parameters
$result = $this->get_common_ids(array(
'ClubId' => $this->verein_id,
'Language' => 1 // 1 = German
));
if (is_wp_error($result)) {
$error_msg = $result->get_error_message();
error_log('Swiss Football API: Connection test failed - ' . $error_msg);
return array(
'success' => false,
'error' => 'API connection failed',
'details' => $error_msg
);
}
if (!is_array($result)) {
return array(
'success' => false,
'error' => 'Invalid API response',
'details' => 'The API returned unexpected data format'
);
}
return array(
'success' => true,
'error' => null,
'details' => 'Connection successful!'
);
}
public function get_current_match($team_id)

View File

@ -17,6 +17,23 @@ class Swi_Foot_Blocks
public function register_blocks()
{
// Register custom block category for Swiss Football blocks
add_filter('block_categories_all', function($categories) {
// Check if category already exists
foreach ($categories as $cat) {
if ($cat['slug'] === 'swi-football') {
return $categories; // Already registered
}
}
// Add custom category with translated label
$categories[] = array(
'slug' => 'swi-football',
'title' => __('Swiss Football', 'swi_foot_matchdata'),
'icon' => 'soccer'
);
return $categories;
});
// Register blocks from metadata (block.json) and provide server-side render callbacks
$base = dirname(__DIR__) . '/blocks';
@ -90,14 +107,6 @@ class Swi_Foot_Blocks
) );
}
// Register additional blocks (editor-only save shortcodes)
if ( file_exists( $base . '/team-data/block.json' ) ) {
register_block_type_from_metadata( $base . '/team-data', array(
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
if ( file_exists( $base . '/match-roster/block.json' ) ) {
register_block_type_from_metadata( $base . '/match-roster', array(
'render_callback' => array( $this, 'render_match_roster_block' ),
@ -113,6 +122,22 @@ class Swi_Foot_Blocks
'editor_style' => 'swi-foot-editor-styles'
) );
}
if ( file_exists( $base . '/match-bench/block.json' ) ) {
register_block_type_from_metadata( $base . '/match-bench', array(
'render_callback' => array( $this, 'render_match_bench_block' ),
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
if ( file_exists( $base . '/match-referees/block.json' ) ) {
register_block_type_from_metadata( $base . '/match-referees', array(
'render_callback' => array( $this, 'render_match_referees_block' ),
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
}
public function enqueue_scripts()
@ -223,7 +248,11 @@ class Swi_Foot_Blocks
if (is_wp_error($standings)) {
return '<div class="swi-foot-error">' .
sprintf(__('Error loading standings: %s', 'swi_foot_matchdata'), $standings->get_error_message()) .
sprintf(
/* translators: %s is the error message from the API */
__('Error loading standings: %s', 'swi_foot_matchdata'),
$standings->get_error_message()
) .
'</div>';
}
@ -281,6 +310,7 @@ class Swi_Foot_Blocks
// Team must come from context only
$team_id = $ctx['team_id'] ?? null;
$limit = isset($attributes['limit']) ? $attributes['limit'] : 5;
$match_filter = isset($attributes['matchFilter']) ? $attributes['matchFilter'] : 'all';
if (empty($team_id)) {
return '<div class="swi-foot-error">' . __('Schedule: No team provided in container context.', 'swi_foot_matchdata') . '</div>';
@ -291,18 +321,47 @@ class Swi_Foot_Blocks
if (is_wp_error($events)) {
return '<div class="swi-foot-error">' .
sprintf(__('Error loading schedule: %s', 'swi_foot_matchdata'), $events->get_error_message()) .
sprintf(
/* translators: %s is the error message from the API */
__('Error loading schedule: %s', 'swi_foot_matchdata'),
$events->get_error_message()
) .
'</div>';
}
// Filter upcoming events and limit results
// Filter upcoming events by match type (home/away/all) and limit results
$upcoming_events = array();
if (is_array($events)) {
foreach ($events as $event) {
// Check if match is in the future
if (strtotime($event['matchDate']) >= time()) {
$upcoming_events[] = $event;
if (count($upcoming_events) >= $limit) {
break;
// Apply match filter (home/away/all)
$match_should_be_included = true;
if ($match_filter !== 'all') {
// Determine if this is a home or away match
// teamAId is home team, teamBId is away team
$is_home_match = isset($event['teamAId']) && $event['teamAId'] == $team_id;
$is_away_match = isset($event['teamBId']) && $event['teamBId'] == $team_id;
// If team IDs aren't available, try to infer from team names
if (!isset($event['teamAId']) || !isset($event['teamBId'])) {
// This is a fallback - not ideal but works if IDs aren't present
$is_home_match = true; // Default to home
}
if ($match_filter === 'home' && !$is_home_match) {
$match_should_be_included = false;
} elseif ($match_filter === 'away' && !$is_away_match) {
$match_should_be_included = false;
}
}
if ($match_should_be_included) {
$upcoming_events[] = $event;
if (count($upcoming_events) >= $limit) {
break;
}
}
}
}
@ -400,7 +459,8 @@ class Swi_Foot_Blocks
// Match must come from context
$match_id = $ctx['match_id'] ?? '';
$side = $attributes['side'] ?? 'home';
$with_bench = $attributes['withBench'] ? 'true' : 'false';
$show_starting_squad = isset($attributes['showStartingSquad']) ? $attributes['showStartingSquad'] : true;
$show_bench = isset($attributes['showBench']) ? $attributes['showBench'] : false;
if (empty($match_id)) {
return '<div class="swi-foot-error">' . __('Match Roster: No match provided in container context.', 'swi_foot_matchdata') . '</div>';
@ -408,8 +468,11 @@ class Swi_Foot_Blocks
// Build shortcode using match from context and side attribute
$shortcode = '[swi_foot_roster match_id="' . esc_attr($match_id) . '" side="' . esc_attr($side) . '"';
if ($with_bench === 'true') {
$shortcode .= ' with_bench="true"';
if ($show_starting_squad) {
$shortcode .= ' show_starting="true"';
}
if ($show_bench) {
$shortcode .= ' show_bench="true"';
}
$shortcode .= ']';
@ -446,6 +509,61 @@ class Swi_Foot_Blocks
return do_shortcode($shortcode);
}
/**
* Render match bench block
* Gets match from container context, displays team staff/substitutes
*/
public function render_match_bench_block($attributes, $content = '', $block = null)
{
// Get context from parent block container
$provided = is_object($block) && !empty($block->context['swi-foot/context']) ? $block->context['swi-foot/context'] : null;
$ctx = array();
if (is_array($provided)) $ctx = $provided;
$post_defaults = function_exists('swi_foot_resolve_context') ? swi_foot_resolve_context(get_the_ID()) : array();
$ctx = array_merge($post_defaults, $ctx);
// Match must come from context
$match_id = $ctx['match_id'] ?? '';
$side = $attributes['side'] ?? 'home';
if (empty($match_id)) {
return '<div class="swi-foot-error">' . __('Match Bench: No match provided in container context.', 'swi_foot_matchdata') . '</div>';
}
// Build shortcode using match from context and side attribute
$shortcode = '[swi_foot_bench match_id="' . esc_attr($match_id) . '" side="' . esc_attr($side) . '"]';
// Process and return the shortcode
return do_shortcode($shortcode);
}
/**
* Render match referees block
* Gets match from container context, displays match officials/referees
*/
public function render_match_referees_block($attributes, $content = '', $block = null)
{
// Get context from parent block container
$provided = is_object($block) && !empty($block->context['swi-foot/context']) ? $block->context['swi-foot/context'] : null;
$ctx = array();
if (is_array($provided)) $ctx = $provided;
$post_defaults = function_exists('swi_foot_resolve_context') ? swi_foot_resolve_context(get_the_ID()) : array();
$ctx = array_merge($post_defaults, $ctx);
// Match must come from context
$match_id = $ctx['match_id'] ?? '';
if (empty($match_id)) {
return '<div class="swi-foot-error">' . __('Match Referees: No match provided in container context.', 'swi_foot_matchdata') . '</div>';
}
// Build shortcode using match from context
$shortcode = '[swi_foot_referees match_id="' . esc_attr($match_id) . '"]';
// Process and return the shortcode
return do_shortcode($shortcode);
}
private function render_team_selector($block_type)
{
$api = new Swi_Foot_API();

View File

@ -84,18 +84,10 @@ class Swi_Foot_REST {
)
));
// Also allow team_id as a path parameter: /wp-json/swi-foot/v1/matches/123
register_rest_route('swi-foot/v1', '/matches/(?P<team_id>\d+)', array(
register_rest_route('swi-foot/v1', '/block-status/(?P<match_id>\d+)/(?P<endpoint>[a-z_]+)', array(
'methods' => 'GET',
'callback' => array($this, 'get_matches_for_team'),
'permission_callback' => '__return_true',
'args' => array(
'team_id' => array(
'validate_callback' => function($param, $request, $key) {
return is_numeric($param);
}
)
)
'callback' => array($this, 'check_block_status'),
'permission_callback' => '__return_true'
));
// Single match details: /wp-json/swi-foot/v1/match/<match_id>
@ -129,9 +121,9 @@ class Swi_Foot_REST {
)
));
register_rest_route('swi-foot/v1', '/commons-ids', array(
register_rest_route('swi-foot/v1', '/common-ids', array(
'methods' => 'GET',
'callback' => array($this, 'get_commons_ids'),
'callback' => array($this, 'get_common_ids'),
'permission_callback' => '__return_true'
));
@ -210,7 +202,7 @@ class Swi_Foot_REST {
return rest_ensure_response($normalized);
}
public function get_commons_ids($request) {
public function get_common_ids($request) {
$api = new Swi_Foot_API();
$commons = $api->get_commons_ids();
if (is_wp_error($commons)) {
@ -370,12 +362,77 @@ class Swi_Foot_REST {
public function admin_test_connection($request) {
$api = new Swi_Foot_API();
if (method_exists($api, 'test_connection')) {
$ok = $api->test_connection();
$result = $api->test_connection();
} else {
$ok = false;
$result = array(
'success' => false,
'error' => 'test_connection method not found',
'details' => 'Plugin may be incorrectly installed'
);
}
// Return response with full error details
if (is_array($result) && isset($result['success'])) {
if ($result['success']) {
return rest_ensure_response(array(
'success' => true,
'message' => isset($result['details']) ? $result['details'] : 'Connection successful!'
));
} else {
return new WP_REST_Response(array(
'success' => false,
'error' => isset($result['error']) ? $result['error'] : 'Connection failed',
'details' => isset($result['details']) ? $result['details'] : 'Unknown error'
), 400);
}
}
// Fallback for unexpected response format
return new WP_REST_Response(array(
'success' => false,
'error' => 'Unexpected response from API test',
'details' => 'Unable to determine connection status'
), 400);
}
public function check_block_status($request) {
$match_id = $request->get_param('match_id');
$endpoint = $request->get_param('endpoint');
if (empty($match_id) || empty($endpoint)) {
return rest_ensure_response(array('status' => 'error', 'message' => 'Missing parameters'));
}
$api = new Swi_Foot_API();
// Map endpoint names to API methods
$method_map = array(
'roster' => 'get_match_players',
'bench' => 'get_match_bench',
'referees' => 'get_match_referees',
'events' => 'get_match_events'
);
if (!isset($method_map[$endpoint])) {
return rest_ensure_response(array('status' => 'error', 'message' => __('Unknown endpoint', 'swi_foot_matchdata')));
}
$method = $method_map[$endpoint];
$result = $api->$method($match_id);
// Determine status based on result
if (is_wp_error($result)) {
$code = $result->get_error_code();
if ($code === 'data_not_available') {
return rest_ensure_response(array('status' => 'warning', 'message' => __('Data not yet available', 'swi_foot_matchdata')));
} else {
return rest_ensure_response(array('status' => 'error', 'message' => __('No data available', 'swi_foot_matchdata')));
}
} elseif (empty($result)) {
return rest_ensure_response(array('status' => 'error', 'message' => __('No data available', 'swi_foot_matchdata')));
} else {
return rest_ensure_response(array('status' => 'success', 'message' => __('Data available', 'swi_foot_matchdata')));
}
if ($ok) return rest_ensure_response(array('success' => true));
return new WP_REST_Response(array('error' => 'Connection failed'), 400);
}
public function admin_delete_finished_match($request) {

View File

@ -22,6 +22,8 @@ class Swi_Foot_Shortcodes
add_shortcode('swi_foot_match_round', array($this, 'match_round_shortcode'));
add_shortcode('swi_foot_standings', array($this, 'standings_shortcode'));
add_shortcode('swi_foot_roster', array($this, 'roster_shortcode'));
add_shortcode('swi_foot_bench', array($this, 'bench_shortcode'));
add_shortcode('swi_foot_referees', array($this, 'referees_shortcode'));
add_shortcode('swi_foot_events', array($this, 'events_shortcode'));
add_shortcode('swi_foot_match_data', array($this, 'match_data_shortcode'));
add_shortcode('swi_foot_match_home_team_logo', array($this, 'match_home_team_logo_shortcode'));
@ -44,6 +46,26 @@ class Swi_Foot_Shortcodes
return $vars;
}
/**
* Render error message, distinguishing between "data not available" (406) and other errors
* @param WP_Error $error The error object
* @return string HTML-formatted error message
*/
private function render_error_message($error)
{
if (!is_wp_error($error)) {
return '';
}
// Check if this is a "data not yet available" error (HTTP 406)
if ($error->get_error_code() === 'data_not_available') {
return '<div class="swi-foot-data-warning">⏳ ' . __('Data not yet available', 'swi_foot_matchdata') . '</div>';
}
// For other errors, show grey "no data available" message
return '<div class="swi-foot-data-unavailable"> ' . __('No data available', 'swi_foot_matchdata') . '</div>';
}
/**
* Handle internal requests for team logo images
* URL format: /?swi_foot_team_logo=team_id&position=home|away|event
@ -198,7 +220,7 @@ class Swi_Foot_Shortcodes
$match_data = $this->get_match_data($atts);
if (!$match_data) {
return '<span class="swi-foot-error">' . __('Match data not available', 'swi_foot_matchdata') . '</span>';
return '<div class="swi-foot-error">' . __('Match data not available', 'swi_foot_matchdata') . '</div>';
}
ob_start();
@ -388,6 +410,139 @@ class Swi_Foot_Shortcodes
return '';
}
/**
* Render match bench (team staff) list
* Usage: [swi_foot_bench match_id="12345"]
* Lists staff members (name and role) for both home and away teams
*/
public function bench_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'side' => '' // 'home' or 'away', shows both if not specified
), $atts);
$match_id = $atts['match_id'];
if (empty($match_id)) {
return '<div class="swi-foot-error">' . __('Match ID required', 'swi_foot_matchdata') . '</div>';
}
$bench = $this->api->get_match_bench($match_id);
if (is_wp_error($bench)) {
return $this->render_error_message($bench);
}
if (empty($bench)) {
return '<div class="swi-foot-notice">' . __('No bench data available', 'swi_foot_matchdata') . '</div>';
}
// Normalize side parameter
$side = strtolower(trim($atts['side']));
$show_side_filter = !empty($side) && in_array($side, array('home', 'away'), true);
ob_start();
?>
<div class="swi-foot-bench-container" data-match-id="<?php echo esc_attr($match_id); ?>">
<?php
// Group bench players by team
$home_staff = array_filter($bench, function($person) {
return isset($person['isHomeTeam']) && $person['isHomeTeam'] === true;
});
$away_staff = array_filter($bench, function($person) {
return isset($person['isHomeTeam']) && $person['isHomeTeam'] === false;
});
// If side is specified, filter to only that side
if ($show_side_filter) {
if ($side === 'home') {
$away_staff = array();
} else {
$home_staff = array();
}
}
?>
<?php if (!empty($home_staff)): ?>
<div class="bench-team bench-home">
<?php if (!$show_side_filter): ?>
<h3><?php echo esc_html($home_staff[array_key_first($home_staff)]['teamName'] ?? __('Home Team', 'swi_foot_matchdata')); ?></h3>
<?php endif; ?>
<ul class="bench-list">
<?php foreach ($home_staff as $person): ?>
<li class="bench-person">
<?php echo esc_html($person['personName'] ?? ''); ?>
<?php if (!empty($person['roleCategoryName'])): ?>
<span class="swi-foot-role">(<?php echo esc_html($person['roleCategoryName']); ?>)</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if (!empty($away_staff)): ?>
<div class="bench-team bench-away">
<?php if (!$show_side_filter): ?>
<h3><?php echo esc_html($away_staff[array_key_first($away_staff)]['teamName'] ?? __('Away Team', 'swi_foot_matchdata')); ?></h3>
<?php endif; ?>
<ul class="bench-list">
<?php foreach ($away_staff as $person): ?>
<li class="bench-person">
<?php echo esc_html($person['personName'] ?? ''); ?>
<?php if (!empty($person['roleCategoryName'])): ?>
<span class="swi-foot-role">(<?php echo esc_html($person['roleCategoryName']); ?>)</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Render referees list for a match
* Usage: [swi_foot_referees match_id="12345"]
* Lists referees with their role and name formatted as: "Role: Firstname Name" (name in bold)
*/
public function referees_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => ''
), $atts);
$match_id = $atts['match_id'];
if (empty($match_id)) {
return '<div class="swi-foot-error">✗ ' . __('Match ID required', 'swi_foot_matchdata') . '</div>';
}
$referees = $this->api->get_match_referees($match_id);
if (is_wp_error($referees)) {
return $this->render_error_message($referees);
}
if (empty($referees)) {
return '<div class="swi-foot-data-unavailable"> ' . __('No data available', 'swi_foot_matchdata') . '</div>';
}
ob_start();
?>
<div class="swi-foot-referees-container" data-match-id="<?php echo esc_attr($match_id); ?>">
<ul class="referees-list">
<?php foreach ($referees as $referee): ?>
<li class="referee-item">
<span class="referee-role"><?php echo esc_html($referee['refereeRoleName'] ?? ''); ?>:</span>
<strong><?php echo esc_html(($referee['firstname'] ?? '') . ' ' . ($referee['name'] ?? '')); ?></strong>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php
return ob_get_clean();
}
/**
* Generic inline data point shortcode
* Extracts a specific field from match data and displays it inline
@ -512,15 +667,16 @@ class Swi_Foot_Shortcodes
return null;
}
// Parse the matchDate string - handle multiple formats
$timestamp = strtotime($match['matchDate']);
if ($timestamp === false) {
// Extract date part from ISO datetime string (e.g., "2026-03-27T19:30:00" -> "2026-03-27")
// API provides date in Swiss time, use as-is without any timezone conversion
$date_part = substr($match['matchDate'], 0, 10);
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_part)) {
return null;
}
// Use WordPress date format setting
// Convert date string to WordPress format
$date_format = get_option('date_format');
return wp_date($date_format, $timestamp);
return date_i18n($date_format, strtotime($date_part));
}
/**
@ -535,15 +691,20 @@ class Swi_Foot_Shortcodes
return null;
}
// Parse the matchDate string - handle multiple formats
$timestamp = strtotime($match['matchDate']);
if ($timestamp === false) {
// Extract time part from ISO datetime string (e.g., "2026-03-27T19:30:00" -> "19:30:00")
// API provides time in Swiss time, use as-is without any timezone conversion
if (strpos($match['matchDate'], 'T') === false) {
return null;
}
$time_part = substr($match['matchDate'], 11, 8);
if (!preg_match('/^\d{2}:\d{2}:\d{2}$/', $time_part)) {
return null;
}
// Use WordPress time format setting
// Format time string with WordPress time format setting
$time_format = get_option('time_format');
return wp_date($time_format, $timestamp);
return date_i18n($time_format, strtotime('2000-01-01 ' . $time_part));
}
private function get_match_data($atts)
@ -616,13 +777,13 @@ class Swi_Foot_Shortcodes
), $atts);
if (empty($atts['team_id'])) {
return '<span class="swi-foot-error">' . __('Team ID required', 'swi_foot_matchdata') . '</span>';
return '<div class="swi-foot-error">' . __('Team ID required', 'swi_foot_matchdata') . '</div>';
}
$standings = $this->api->get_standings($atts['team_id']);
if (is_wp_error($standings) || empty($standings)) {
return '<span class="swi-foot-error">' . __('Standings data not available', 'swi_foot_matchdata') . '</span>';
return '<div class="swi-foot-error">' . __('Standings data not available', 'swi_foot_matchdata') . '</div>';
}
ob_start();
@ -667,16 +828,18 @@ class Swi_Foot_Shortcodes
public function roster_shortcode($atts)
{
$atts = shortcode_atts(array(
'match_id' => '',
'team_id' => '',
'show_current' => 'false',
'side' => '',
'with_bench' => 'false'
'match_id' => '',
'team_id' => '',
'show_current' => 'false',
'side' => '',
'show_starting' => 'true',
'show_bench' => 'false',
'with_bench' => 'false' // Legacy support
), $atts);
$side = strtolower(trim($atts['side']));
if (!in_array($side, array('home', 'away'), true)) {
return '<span class="swi-foot-error">' . __('Parameter "side" must be "home" or "away"', 'swi_foot_matchdata') . '</span>';
return '<div class="swi-foot-error">' . __('Parameter "side" must be "home" or "away"', 'swi_foot_matchdata') . '</div>';
}
$match_id = $atts['match_id'];
@ -688,27 +851,27 @@ class Swi_Foot_Shortcodes
}
if (empty($match_id)) {
return '<span class="swi-foot-error">' . __('Match ID required', 'swi_foot_matchdata') . '</span>';
return '<div class="swi-foot-error">' . __('Match ID required', 'swi_foot_matchdata') . '</div>';
}
// Parse show_starting and show_bench attributes (handle legacy with_bench)
$show_starting = strtolower($atts['show_starting']) === 'true';
$show_bench = strtolower($atts['show_bench']) === 'true';
// Legacy support: if with_bench is true and show_starting is default, show both
if (strtolower($atts['with_bench']) === 'true' && !$show_bench) {
$show_bench = true;
}
// First check if we have saved final data
$saved = $this->api->get_finished_match_data($match_id);
if ($saved) {
$players = $saved['roster']['players'] ?? array();
$bench_players = $saved['roster']['bench'] ?? array();
$players = $saved['roster']['players'] ?? array();
} else {
// Live fetch
$players = $this->api->get_match_players($match_id);
if (is_wp_error($players)) {
return '<span class="swi-foot-error">' . __('Roster data not available', 'swi_foot_matchdata') . ': ' . esc_html($players->get_error_message()) . '</span>';
}
$bench_players = array();
if (strtolower($atts['with_bench']) === 'true') {
$bench = $this->api->get_match_bench($match_id);
if (!is_wp_error($bench)) {
$bench_players = $bench;
}
return $this->render_error_message($players);
}
// Check match ended & possibly save events + roster together
@ -722,68 +885,100 @@ class Swi_Foot_Shortcodes
if (is_array($players)) {
$this->api->save_finished_match_data(
$match_id,
array('players' => $players, 'bench' => $bench_players),
array('players' => $players),
$events
);
}
}
}
// Filter roster for side
$filtered_players = array_filter($players, function ($p) use ($side) {
// Filter players by team side
$team_players = array_filter($players, function ($p) use ($side) {
return $side === 'home' ? !empty($p['isHomeTeam']) : empty($p['isHomeTeam']);
});
$filtered_bench = array_filter($bench_players, function ($p) use ($side) {
return $side === 'home' ? !empty($p['isHomeTeam']) : empty($p['isHomeTeam']);
// Split players into starting (assignmentRoleId = 0 or 1 for captain) and bench (assignmentRoleId > 1)
$starting_players = array_filter($team_players, function ($p) {
return isset($p['assignmentRoleId']) && ((int)$p['assignmentRoleId'] === 0 || (int)$p['assignmentRoleId'] === 1);
});
$bench_players = array_filter($team_players, function ($p) {
return !isset($p['assignmentRoleId']) || ((int)$p['assignmentRoleId'] > 1);
});
// If neither option is checked, return empty div
if (!$show_starting && !$show_bench) {
return '<div class="swi-foot-error">' . __('No roster display options selected', 'swi_foot_matchdata') . '</div>';
}
ob_start();
?>
<div class="swi-foot-roster" data-match-id="<?php echo esc_attr($match_id); ?>">
<h3><?php echo esc_html(ucfirst($side) . ' ' . __('Team Roster', 'swi_foot_matchdata')); ?></h3>
<?php if (!empty($filtered_players)): ?>
<ul class="roster-list">
<?php foreach ($filtered_players as $pl): ?>
<li>
<?php
$jersey = $pl['jerseyNumber'] ?? '';
$firstname = $pl['firstname'] ?? '';
$name = $pl['name'] ?? '';
$secondName = $pl['secondName'] ?? '';
$positionName = $pl['positionName'] ?? $pl['position'] ?? '';
$displayName = trim($firstname . ' ' . $name . ' ' . $secondName);
echo esc_html($jersey . ' ' . $displayName);
if (!empty($positionName)) {
echo ' (' . esc_html($positionName) . ')';
}
?>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p><?php _e('No roster data available.', 'swi_foot_matchdata'); ?></p>
<?php if ($show_starting && !empty($starting_players)): ?>
<div class="swi-foot-roster-starting">
<h4><?php _e('Starting Squad', 'swi_foot_matchdata'); ?></h4>
<ul class="roster-list">
<?php foreach ($starting_players as $pl): ?>
<li class="roster-player<?php echo isset($pl['assignmentRoleId']) && (int)$pl['assignmentRoleId'] === 1 ? ' roster-player-captain' : ''; ?>">
<?php
$jersey = $pl['jerseyNumber'] ?? '';
$firstname = $pl['firstname'] ?? '';
$name = $pl['name'] ?? '';
$secondName = $pl['secondName'] ?? '';
$positionName = $pl['positionName'] ?? $pl['position'] ?? '';
$isCaptain = isset($pl['assignmentRoleId']) && (int)$pl['assignmentRoleId'] === 1;
$displayName = trim($firstname . ' ' . $name . ' ' . $secondName);
// Captain badge
if ($isCaptain) {
echo '<span class="swi-foot-captain-badge" title="' . esc_attr(__('Captain', 'swi_foot_matchdata')) . '">C</span> ';
}
echo esc_html($jersey . ' ' . $displayName);
if (!empty($positionName)) {
echo ' <span class="swi-foot-position">(' . esc_html($positionName) . ')</span>';
}
?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if (!empty($filtered_bench)): ?>
<h4><?php _e('Bench', 'swi_foot_matchdata'); ?></h4>
<ul class="roster-bench-list">
<?php foreach ($filtered_bench as $pl): ?>
<li>
<?php
$jersey = $pl['jerseyNumber'] ?? '';
$firstname = $pl['firstname'] ?? '';
$name = $pl['name'] ?? '';
$secondName = $pl['secondName'] ?? '';
$positionName = $pl['positionName'] ?? $pl['position'] ?? '';
$displayName = trim($firstname . ' ' . $name . ' ' . $secondName);
echo esc_html($jersey . ' ' . $displayName);
if (!empty($positionName)) {
echo ' (' . esc_html($positionName) . ')';
}
?>
</li>
<?php endforeach; ?>
</ul>
<?php if ($show_starting && !empty($starting_players) && $show_bench && !empty($bench_players)): ?>
<hr class="swi-foot-roster-divider" />
<?php endif; ?>
<?php if ($show_bench && !empty($bench_players)): ?>
<div class="swi-foot-roster-bench">
<h4><?php _e('Bench', 'swi_foot_matchdata'); ?></h4>
<ul class="roster-bench-list">
<?php foreach ($bench_players as $pl): ?>
<li class="roster-player">
<?php
$jersey = $pl['jerseyNumber'] ?? '';
$firstname = $pl['firstname'] ?? '';
$name = $pl['name'] ?? '';
$secondName = $pl['secondName'] ?? '';
$positionName = $pl['positionName'] ?? $pl['position'] ?? '';
$displayName = trim($firstname . ' ' . $name . ' ' . $secondName);
echo esc_html($jersey . ' ' . $displayName);
if (!empty($positionName)) {
echo ' <span class="swi-foot-position">(' . esc_html($positionName) . ')</span>';
}
?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if (!$show_starting && !empty($bench_players) && !$show_bench): ?>
<p><?php _e('No roster data to display.', 'swi_foot_matchdata'); ?></p>
<?php elseif ($show_starting && empty($starting_players) && empty($bench_players)): ?>
<p><?php _e('No roster data available.', 'swi_foot_matchdata'); ?></p>
<?php endif; ?>
</div>
<?php
@ -814,7 +1009,7 @@ class Swi_Foot_Shortcodes
}
if (empty($match_id)) {
return '<span class="swi-foot-error">' . __('Match ID required', 'swi_foot_matchdata') . '</span>';
return '<div class="swi-foot-error">' . __('Match ID required', 'swi_foot_matchdata') . '</div>';
}
// Try saved first
@ -824,7 +1019,7 @@ class Swi_Foot_Shortcodes
} else {
$events = $this->api->get_match_events($match_id);
if (is_wp_error($events)) {
return '<span class="swi-foot-error">' . __('Events data not available', 'swi_foot_matchdata') . '</span>';
return $this->render_error_message($events);
}
// Check if match ended and store both data sets

View File

@ -1,705 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: Swiss Football Matchdata\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-18 14:50+0100\n"
"PO-Revision-Date: 2025-02-15 12:00+0000\n"
"Language: de_CH\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: includes/class-swi-foot-admin.php:18 includes/class-swi-foot-admin.php:249
msgid "Swiss Football Matchdata Settings"
msgstr "Swiss Football Matchdata Einstellungen"
#: includes/class-swi-foot-admin.php:19
msgid "Swiss Football"
msgstr "Swiss Football"
#: includes/class-swi-foot-admin.php:37
#, fuzzy
msgid "API Configuration"
msgstr "API-Verbindung testen"
#: includes/class-swi-foot-admin.php:42
msgid "API Base URL"
msgstr "API Basis-URL"
#: includes/class-swi-foot-admin.php:43
msgid "API Username (Application Key)"
msgstr "API Benutzername (Application Key)"
#: includes/class-swi-foot-admin.php:44
msgid "API Password (Application Pass)"
msgstr "API Passwort (Application Pass)"
#: includes/class-swi-foot-admin.php:45
msgid "Verein ID (Club ID)"
msgstr "Vereins-ID (Club ID)"
#: includes/class-swi-foot-admin.php:46
msgid "Season ID"
msgstr "Saison-ID"
#: includes/class-swi-foot-admin.php:50
msgid "Cache Settings"
msgstr ""
#: includes/class-swi-foot-admin.php:55
msgid "Match Data Cache Duration (seconds)"
msgstr "Cache-Dauer für Spieldaten (Sekunden)"
#: includes/class-swi-foot-admin.php:60
msgid "Configure your Swiss Football API credentials here."
msgstr ""
#: includes/class-swi-foot-admin.php:65
msgid "Configure caching settings for match data."
msgstr ""
#: includes/class-swi-foot-admin.php:72
msgid "The base URL for the Swiss Football API"
msgstr ""
#: includes/class-swi-foot-admin.php:79
#, fuzzy
msgid "Your API application key"
msgstr "API Benutzername (Application Key)"
#: includes/class-swi-foot-admin.php:86
#, fuzzy
msgid "Your API application password"
msgstr "API Passwort (Application Pass)"
#: includes/class-swi-foot-admin.php:93
#, fuzzy
msgid "Enter your club's Verein ID (Club ID)"
msgstr "Vereins-ID (Club ID)"
#: includes/class-swi-foot-admin.php:100
msgid "Current season ID (usually the year)"
msgstr ""
#: includes/class-swi-foot-admin.php:107
msgid "How long to cache match data in seconds (10-300)"
msgstr ""
#: includes/class-swi-foot-admin.php:175
#, fuzzy
msgid "Swiss Football Shortcodes"
msgstr "Swiss Football"
#: includes/class-swi-foot-admin.php:187
msgid "Available Shortcodes:"
msgstr ""
#: includes/class-swi-foot-admin.php:190
msgid "Full Match Display:"
msgstr ""
#: includes/class-swi-foot-admin.php:196
msgid "Individual Match Elements:"
msgstr ""
#: includes/class-swi-foot-admin.php:209
msgid "Parameters:"
msgstr ""
#: includes/class-swi-foot-admin.php:219
msgid "Click any shortcode above to copy it to clipboard"
msgstr ""
#: includes/class-swi-foot-admin.php:227 includes/class-swi-foot-admin.php:237
msgid "Shortcode copied to clipboard!"
msgstr ""
#: includes/class-swi-foot-admin.php:260
msgid "Connection Test"
msgstr ""
#: includes/class-swi-foot-admin.php:263
msgid "Test API Connection"
msgstr "API-Verbindung testen"
#: includes/class-swi-foot-admin.php:270
msgid "Team Management"
msgstr ""
#: includes/class-swi-foot-admin.php:273
msgid "Refresh Teams List"
msgstr "Mannschaftsliste aktualisieren"
#: includes/class-swi-foot-admin.php:284
msgid "Cache Management"
msgstr ""
#: includes/class-swi-foot-admin.php:287
msgid "Clear Match Data Cache"
msgstr "Spieldaten-Cache leeren"
#: includes/class-swi-foot-admin.php:295
#, fuzzy
msgid "Finished Matches Data"
msgstr "Alle beendeten Spiele löschen"
#: includes/class-swi-foot-admin.php:300
msgid "Quick Shortcode Reference"
msgstr ""
#: includes/class-swi-foot-admin.php:303
msgid "Common Examples:"
msgstr ""
#: includes/class-swi-foot-admin.php:304
msgid "Display full match info:"
msgstr ""
#: includes/class-swi-foot-admin.php:308
msgid "Show next match for a team:"
msgstr ""
#: includes/class-swi-foot-admin.php:312
msgid "Individual elements in text:"
msgstr ""
#: includes/class-swi-foot-admin.php:313
msgid "The match between"
msgstr ""
#: includes/class-swi-foot-admin.php:313
msgid "and"
msgstr ""
#: includes/class-swi-foot-admin.php:313
msgid "is on"
msgstr ""
#: includes/class-swi-foot-admin.php:328
#, php-format
msgid "Error loading teams: %s"
msgstr ""
#: includes/class-swi-foot-admin.php:335
msgid "No teams found. Please check your API configuration."
msgstr ""
#: includes/class-swi-foot-admin.php:340
msgid "Available Teams:"
msgstr ""
#: includes/class-swi-foot-admin.php:360
#, php-format
msgid "Currently caching %d match records with %d second cache duration."
msgstr ""
#: includes/class-swi-foot-admin.php:375
msgid "Cache Range:"
msgstr ""
#: includes/class-swi-foot-admin.php:376
msgid "Oldest:"
msgstr ""
#: includes/class-swi-foot-admin.php:377
msgid "Newest:"
msgstr ""
#: includes/class-swi-foot-admin.php:386
msgid "No finished match data stored."
msgstr "Keine gespeicherten, beendeten Spiele vorhanden."
#: includes/class-swi-foot-admin.php:393
#, fuzzy
msgid "Match ID"
msgstr "Spielkader"
#: includes/class-swi-foot-admin.php:394
msgid "Saved At"
msgstr ""
#: includes/class-swi-foot-admin.php:395
msgid "Players"
msgstr ""
#: includes/class-swi-foot-admin.php:396
#: includes/class-swi-foot-shortcodes.php:768
msgid "Bench"
msgstr "Ersatzbank"
#: includes/class-swi-foot-admin.php:397
#, fuzzy
msgid "Events"
msgstr "Spielereignisse"
#: includes/class-swi-foot-admin.php:415
msgid "Delete"
msgstr "Löschen"
#: includes/class-swi-foot-admin.php:421
msgid "Clear All Finished Matches"
msgstr "Alle beendeten Spiele löschen"
#: includes/class-swi-foot-admin.php:428
#, fuzzy
msgid "Delete this finished match data?"
msgstr "Keine gespeicherten, beendeten Spiele vorhanden."
#: includes/class-swi-foot-admin.php:445
#, fuzzy
msgid "Clear all finished match data?"
msgstr "Alle beendeten Spiele löschen"
#: includes/class-swi-foot-blocks.php:218
msgid "Standings: No team provided in container context."
msgstr ""
#: includes/class-swi-foot-blocks.php:226
#, php-format
msgid "Error loading standings: %s"
msgstr ""
#: includes/class-swi-foot-blocks.php:231
msgid "No standings data available."
msgstr "Keine Ranglistendaten verfügbar."
#: includes/class-swi-foot-blocks.php:237
#: includes/class-swi-foot-shortcodes.php:631
msgid "Current Standings"
msgstr "Aktuelle Rangliste"
#: includes/class-swi-foot-blocks.php:241
#: includes/class-swi-foot-shortcodes.php:635
msgid "Pos"
msgstr ""
#: includes/class-swi-foot-blocks.php:242
#: includes/class-swi-foot-shortcodes.php:636
#, fuzzy
msgid "Team"
msgstr "Heimmannschaft"
#: includes/class-swi-foot-blocks.php:243
#: includes/class-swi-foot-shortcodes.php:637
msgid "P"
msgstr ""
#: includes/class-swi-foot-blocks.php:244
#: includes/class-swi-foot-shortcodes.php:638
msgid "W"
msgstr ""
#: includes/class-swi-foot-blocks.php:245
#: includes/class-swi-foot-shortcodes.php:639
msgid "D"
msgstr ""
#: includes/class-swi-foot-blocks.php:246
#: includes/class-swi-foot-shortcodes.php:640
msgid "L"
msgstr ""
#: includes/class-swi-foot-blocks.php:247
#: includes/class-swi-foot-shortcodes.php:641
msgid "GF"
msgstr ""
#: includes/class-swi-foot-blocks.php:248
#: includes/class-swi-foot-shortcodes.php:642
msgid "GA"
msgstr ""
#: includes/class-swi-foot-blocks.php:249
#: includes/class-swi-foot-shortcodes.php:643
msgid "Pts"
msgstr ""
#: includes/class-swi-foot-blocks.php:286
msgid "Schedule: No team provided in container context."
msgstr ""
#: includes/class-swi-foot-blocks.php:294
#, php-format
msgid "Error loading schedule: %s"
msgstr ""
#: includes/class-swi-foot-blocks.php:314
msgid "Upcoming Matches"
msgstr "Bevorstehende Spiele"
#: includes/class-swi-foot-blocks.php:316
msgid "No upcoming matches scheduled."
msgstr "Keine anstehenden Spiele geplant."
#: includes/class-swi-foot-blocks.php:406
msgid "Match Roster: No match provided in container context."
msgstr ""
#: includes/class-swi-foot-blocks.php:438
msgid "Match Events: No match provided in container context."
msgstr ""
#: includes/class-swi-foot-blocks.php:455
msgid "Please configure your API settings and ensure teams are available."
msgstr ""
#: includes/class-swi-foot-blocks.php:462
#, php-format
msgid "Please select a team to display %s:"
msgstr ""
#: includes/class-swi-foot-blocks.php:464
msgid "Select a team..."
msgstr ""
#: includes/class-swi-foot-shortcodes.php:201
#, fuzzy
msgid "Match data not available"
msgstr "Keine Ranglistendaten verfügbar."
#: includes/class-swi-foot-shortcodes.php:440 src/format-shortcode.js:201
msgid "Ja"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:440 src/format-shortcode.js:201
msgid "Nein"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:619
msgid "Team ID required"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:625
#, fuzzy
msgid "Standings data not available"
msgstr "Keine Ranglistendaten verfügbar."
#: includes/class-swi-foot-shortcodes.php:679
msgid "Parameter \"side\" must be \"home\" or \"away\""
msgstr ""
#: includes/class-swi-foot-shortcodes.php:691
#: includes/class-swi-foot-shortcodes.php:812
msgid "Match ID required"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:703
#, fuzzy
msgid "Roster data not available"
msgstr "Keine Ranglistendaten verfügbar."
#: includes/class-swi-foot-shortcodes.php:743
#, fuzzy
msgid "Team Roster"
msgstr "Spielkader"
#: includes/class-swi-foot-shortcodes.php:764
#, fuzzy
msgid "No roster data available."
msgstr "Keine Ranglistendaten verfügbar."
#: includes/class-swi-foot-shortcodes.php:822
#, fuzzy
msgid "Events data not available"
msgstr "Keine Ranglistendaten verfügbar."
#: includes/class-swi-foot-shortcodes.php:882
#, fuzzy
msgid "Live match events"
msgstr "Spielereignisse"
#: includes/class-swi-foot-shortcodes.php:883
msgid "Match Events"
msgstr "Spielereignisse"
#: includes/class-swi-foot-shortcodes.php:889
#, fuzzy
msgid "Match minute"
msgstr "Spielereignisse"
#: includes/class-swi-foot-shortcodes.php:950
msgid "No events recorded yet."
msgstr "Bisher keine Ereignisse erfasst."
#: src/editor-blocks.js:59
msgid "Settings"
msgstr ""
#: src/editor-blocks.js:60 src/editor-blocks.js:80
msgid "Team is inherited from the container context."
msgstr ""
#: src/editor-blocks.js:63
msgid "Standings will render on the front-end."
msgstr ""
#: src/editor-blocks.js:79
msgid "Schedule settings"
msgstr ""
#: src/editor-blocks.js:81
msgid "Limit"
msgstr ""
#: src/editor-blocks.js:84
msgid "Upcoming matches will render on the front-end."
msgstr ""
#: src/editor-blocks.js:99
#, fuzzy
msgid "Swiss Football Team Data"
msgstr "Swiss Football"
#: src/editor-blocks.js:126
msgid "Team Selection"
msgstr ""
#: src/editor-blocks.js:128
msgid "1. Select Team"
msgstr ""
#: src/editor-blocks.js:129
msgid "Choose a team..."
msgstr ""
#: src/editor-blocks.js:133
msgid "2. What to display"
msgstr ""
#: src/editor-blocks.js:133
msgid "Choose..."
msgstr ""
#: src/editor-blocks.js:133
#, fuzzy
msgid "Standings"
msgstr "Aktuelle Rangliste"
#: src/editor-blocks.js:133
#, fuzzy
msgid "Match"
msgstr "Spielkader"
#: src/editor-blocks.js:138
msgid "Team data shortcode generator"
msgstr ""
#: src/editor-blocks.js:161
msgid "Swiss Football Match Roster"
msgstr "Schweizer Fussball Spielerliste"
#: src/editor-blocks.js:168 src/editor-blocks.js:206
msgid "Display Options"
msgstr ""
#: src/editor-blocks.js:169 src/editor-blocks.js:207
msgid "Match and team are inherited from the container context."
msgstr ""
#: src/editor-blocks.js:171
#, fuzzy
msgid "Team Side"
msgstr "Heimmannschaft"
#: src/editor-blocks.js:173
#, fuzzy
msgid "Home Team"
msgstr "Heimmannschaft"
#: src/editor-blocks.js:173
msgid "Away Team"
msgstr "Gastmannschaft"
#: src/editor-blocks.js:177
msgid "Include Bench Players"
msgstr ""
#: src/editor-blocks.js:183
msgid "Match roster will render on the front-end."
msgstr ""
#: src/editor-blocks.js:199
msgid "Swiss Football Match Events"
msgstr "Schweizer Fussball Match-Ereignisse"
#: src/editor-blocks.js:209
msgid "Refresh Interval (seconds)"
msgstr ""
#: src/editor-blocks.js:217
msgid "Event Order"
msgstr "Ereignisreihenfolge"
#: src/editor-blocks.js:221
msgid "Dynamic (Newest first while live, chronological after)"
msgstr "Dynamisch (Neueste zuerst während Live-Spiel, chronologisch danach)"
#: src/editor-blocks.js:222
msgid "Newest First"
msgstr "Neueste zuerst"
#: src/editor-blocks.js:223
msgid "Oldest First"
msgstr "Älteste zuerst"
#: src/editor-blocks.js:225
msgid ""
"Dynamic: newest events at top while match is ongoing, chronological (oldest "
"first) after match ends"
msgstr ""
"Dynamisch: Neueste Ereignisse oben während Spiel läuft, chronologisch "
"(älteste zuerst) nach Spielende"
#: src/editor-blocks.js:229
msgid "Live events will be displayed on the front-end."
msgstr ""
#: src/format-shortcode.js:31
msgid "— Select data point…"
msgstr ""
#: src/format-shortcode.js:32
msgid "Match-Typ"
msgstr ""
#: src/format-shortcode.js:33
msgid "Liga"
msgstr ""
#: src/format-shortcode.js:34
msgid "Gruppe"
msgstr ""
#: src/format-shortcode.js:35
msgid "Runde"
msgstr ""
#: src/format-shortcode.js:36
msgid "Spielfeld"
msgstr ""
#: src/format-shortcode.js:37
msgid "Heimteam"
msgstr ""
#: src/format-shortcode.js:38
msgid "Gastteam"
msgstr ""
#: src/format-shortcode.js:39
msgid "Tore Heim"
msgstr ""
#: src/format-shortcode.js:40
msgid "Tore Gast"
msgstr ""
#: src/format-shortcode.js:41
msgid "Tore Halbzeit Heim"
msgstr ""
#: src/format-shortcode.js:42
msgid "Tore Halbzeit Gast"
msgstr ""
#: src/format-shortcode.js:43
msgid "Spieltag"
msgstr ""
#: src/format-shortcode.js:44
msgid "Spielzeit"
msgstr ""
#: src/format-shortcode.js:45
msgid "Spielstatus"
msgstr ""
#: src/format-shortcode.js:46
msgid "Spiel pausiert"
msgstr ""
#: src/format-shortcode.js:47
msgid "Spiel beendet"
msgstr ""
#: src/format-shortcode.js:52
msgid "— Select shortcode…"
msgstr ""
#: src/format-shortcode.js:53
msgid "Heimteam Logo"
msgstr "Heimteam-Logo"
#: src/format-shortcode.js:54
msgid "Gastteam Logo"
msgstr "Gastteam-Logo"
#: src/format-shortcode.js:55
msgid "Heimteam Logo (URL)"
msgstr "Heimteam-Logo (URL)"
#: src/format-shortcode.js:56
msgid "Gastteam Logo (URL)"
msgstr "Gastteam-Logo (URL)"
#: src/format-shortcode.js:229 src/format-shortcode.js:242
#: src/format-shortcode.js:356 src/format-shortcode.js:390
#, fuzzy
msgid "Insert Match Data"
msgstr "Alle beendeten Spiele löschen"
#: src/format-shortcode.js:234
msgid "No match data available on this page. Please add match context first."
msgstr ""
#: src/format-shortcode.js:255
#, fuzzy
msgid "Match Data"
msgstr "Spielkader"
#: src/format-shortcode.js:265
msgid "Shortcodes"
msgstr ""
#: src/format-shortcode.js:278
msgid "Data Point"
msgstr ""
#: src/format-shortcode.js:282
msgid "Select which match data to display"
msgstr ""
#: src/format-shortcode.js:289
msgid "Preview:"
msgstr ""
#: src/format-shortcode.js:297
msgid "(no value)"
msgstr ""
#: src/format-shortcode.js:307
msgid "Insert Data"
msgstr ""
#: src/format-shortcode.js:320
msgid "Shortcode"
msgstr ""
#: src/format-shortcode.js:324
msgid "Select a shortcode to insert (logos, etc.)"
msgstr ""
#: src/format-shortcode.js:331
msgid "Shortcode:"
msgstr ""
#: src/format-shortcode.js:341
msgid "Insert Shortcode"
msgstr ""
#~ msgid "Display match roster for a selected match and side."
#~ msgstr ""
#~ "Zeigt die Spielerliste für ein ausgewähltes Spiel und eine Mannschaft an."
#~ msgid "Live match events with optional auto-refresh."
#~ msgstr "Live-Match-Ereignisse mit optionaler Selbstaktualisierung."

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

182
readme.txt Normal file
View File

@ -0,0 +1,182 @@
=== Swiss Football Matchdata ===
Contributors: David Reindl
Tags: football, soccer, swiss football, sports, matches, standings, schedules, shortcodes, gutenberg
Requires at least: 5.0
Tested up to: 6.4
Stable tag: 1.0.0
Requires PHP: 7.4
License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Connect to Swiss Football Association API to display match data, standings, and schedules with flexible shortcodes and Gutenberg blocks.
== Description ==
Swiss Football Matchdata plugin allows you to integrate with the Swiss Football Association API to display:
* Current team standings/rankings
* Upcoming match schedules
* Detailed match information
* Individual match elements via shortcodes
* Easy Gutenberg block integration
The plugin features smart caching (configurable duration), automatic token management, and provides both Gutenberg blocks and shortcodes for maximum flexibility.
= Key Features =
* **Flexible Shortcodes**: Insert individual match elements anywhere in your content
* **Smart Caching**: Configurable cache duration for optimal performance (10-300 seconds)
* **Gutenberg Integration**: Easy-to-use blocks and shortcode inserter for the block editor
* **API Management**: Built-in token refresh and error handling
* **Responsive Design**: Mobile-friendly display of all data
* **Configurable Base URL**: Set your own API endpoint
* **Team Management**: Automatic team list fetching and caching
= Available Shortcodes =
**Full Match Display:**
* `[swi_foot_match match_id="12345"]` - Complete match information
* `[swi_foot_match team_id="67" show_next="true"]` - Next match for team
**Individual Match Elements:**
* `[swi_foot_match_home_team match_id="12345"]` - Home team name
* `[swi_foot_match_away_team match_id="12345"]` - Away team name
* `[swi_foot_match_date match_id="12345" format="d.m.Y"]` - Match date
* `[swi_foot_match_time match_id="12345" format="H:i"]` - Match time
* `[swi_foot_match_venue match_id="12345"]` - Match venue
* `[swi_foot_match_score match_id="12345" separator=":"]` - Current score
* `[swi_foot_match_status match_id="12345"]` - Match status
* `[swi_foot_match_league match_id="12345"]` - League name
* `[swi_foot_match_round match_id="12345"]` - Round number
= Gutenberg Blocks =
* **Swiss Football Context** - Set shared context (team, season) for child blocks
* **Swiss Football Standings** - Display team rankings and standings
* **Swiss Football Schedule** - Show upcoming matches for a team
* **Swiss Football Match Roster** - Display player rosters for a match
* **Swiss Football Match Events** - Show match events (goals, cards, substitutions) with live updates
* **Swiss Football Match Referees** - Display referee information
* **Swiss Football Shortcode** - Insert custom shortcodes with visual interface (deprecated, use paragraph toolbar instead)
= Usage Examples =
**Simple Match Display:**
`[swi_foot_match match_id="12345"]`
**Next Match for Team:**
`[swi_foot_match team_id="67" show_next="true"]`
**Individual Elements in Text:**
`The match between [swi_foot_match_home_team match_id="12345"] and [swi_foot_match_away_team match_id="12345"] will be played on [swi_foot_match_date match_id="12345" format="F j, Y"] at [swi_foot_match_time match_id="12345" format="g:i A"] in [swi_foot_match_venue match_id="12345"].
Current score: [swi_foot_match_score match_id="12345"]`
== Installation ==
1. Upload the plugin files to `/wp-content/plugins/swiss-football-matchdata/`
2. Activate the plugin through the 'Plugins' screen in WordPress
3. Go to Settings → Swiss Football to configure your API credentials
4. Enter your API base URL, username (application key), password (application pass), Verein ID, and Season ID
5. Test the connection and refresh your teams list
6. Start using the shortcodes and blocks in your content
== Configuration ==
After activation, configure these settings in Settings → Swiss Football:
**API Configuration:**
* **API Base URL**: The Swiss Football API endpoint (default: https://stg-club-api-services.football.ch)
* **API Username**: Your application key from Swiss Football
* **API Password**: Your application password from Swiss Football
* **Verein ID**: Your club's unique identifier
* **Season ID**: Current season (usually the year)
**Cache Settings:**
* **Match Data Cache Duration**: How long to cache match data (10-300 seconds, default: 30)
== Frequently Asked Questions ==
= How do I get API credentials? =
Contact the Swiss Football Association to obtain your application key, application password, and Verein ID.
= How often is data updated? =
Match data is cached according to your configured cache duration (default 30 seconds). The cache is automatically refreshed when pages with shortcodes are loaded.
= Can I customize the display styling? =
Yes, the plugin includes CSS classes you can target:
* `.swi-foot-match-container` - Full match displays
* `.swi-foot-standings` - Standings tables
* `.swi-foot-schedule` - Schedule lists
* `.swi-foot-inline` - Individual shortcode elements
* `.swi-foot-error` - Error messages
= What happens if the API is unavailable? =
The plugin will display cached data if available, or show appropriate error messages. It includes robust error handling and logging.
= How do I use shortcodes in the Gutenberg editor? =
Click the shortcode icon in any paragraph's toolbar to quickly insert shortcodes. You can also use the "Swiss Football Shortcode" block (now deprecated) for backwards compatibility.
= Can I display data for multiple teams? =
Yes, you can use different team IDs in different shortcodes and blocks throughout your site.
= How do I find my team's ID? =
After configuring your API credentials, go to Settings → Swiss Football and click "Refresh Teams List". This will display all available teams with their IDs.
= Can I use custom date/time formats? =
Yes, use the `format` parameter in date and time shortcodes with PHP date format strings (e.g., `format="d.m.Y"` for European date format).
= Does the plugin work with caching plugins? =
Yes, the plugin's internal caching system works alongside WordPress caching plugins. The 30-second cache ensures fresh data while minimizing API calls.
== Screenshots ==
1. Admin settings page with API configuration and team management
2. Shortcode helper meta box in post editor with click-to-copy functionality
3. Gutenberg shortcode inserter block with visual interface
4. Example standings table display on frontend
5. Example match schedule display with upcoming games
6. Individual shortcode elements integrated in post content
7. Cache management and connection testing interface
== Changelog ==
= 1.0.0 =
* Initial release
* Swiss Football API integration with automatic token management
* Comprehensive shortcode system for individual match elements
* Configurable caching system (10-300 seconds)
* Gutenberg blocks for standings, schedules, rosters, and events
* Context provider block for scoped data
* Admin interface for configuration and team management
* Responsive design and error handling
* Support for configurable API base URL
* Click-to-copy shortcode functionality in admin
* Smart cache updates on page load
* Paragraph toolbar shortcode insertion
== Upgrade Notice ==
= 1.0.0 =
Initial release of Swiss Football Matchdata plugin with comprehensive API integration and flexible shortcode system.
== Support ==
For support, feature requests, or bug reports, please contact the plugin author or visit the plugin's support forum.
== Privacy ==
This plugin connects to the Swiss Football Association API to retrieve match data. No personal data from your website visitors is sent to external services. The plugin only sends:
* API credentials (application key/password) for authentication
* Club/team identifiers to retrieve relevant match data
All data is cached locally on your WordPress installation according to your configured cache duration.

View File

@ -49,11 +49,24 @@ function makeAjaxCall(action, data = {}) {
});
}
function checkBlockStatus(matchId, endpoint) {
if (!matchId) return Promise.resolve(null);
const root = swiFootEditorData.rest_url.replace(/\/$/, '');
const url = `${root}/block-status/${matchId}/${endpoint}`;
const opts = { credentials: 'same-origin', headers: { 'X-WP-Nonce': swiFootEditorData.rest_nonce } };
return fetch(url, opts)
.then(r => r.json())
.catch(() => null);
}
// Standings block (editor UI only, server renders)
// Gets team from container context, no user selection needed
registerBlockType('swi-foot/standings', {
apiVersion: 3,
title: __('Swiss Football Standings', 'swi_foot_matchdata'),
category: 'embed',
category: 'swi-football',
supports: { align: true, anchor: true },
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
@ -64,7 +77,10 @@ registerBlockType('swi-foot/standings', {
<p style={{color: '#666', fontSize: '13px'}}>{__('Team is inherited from the container context.', 'swi_foot_matchdata')}</p>
</PanelBody>
</InspectorControls>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>{__('Standings will render on the front-end.', 'swi_foot_matchdata')}</div>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>
<p><strong>{__('Standings', 'swi_foot_matchdata')}</strong></p>
<p style={{fontSize: '13px', color: '#666', marginBottom: '0'}}>{__('League standings inherited from team context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}</p>
</div>
</div>
);
},
@ -74,15 +90,17 @@ registerBlockType('swi-foot/standings', {
// Schedule block (editor UI only, server renders)
// Gets team from container context, only allows limiting results
registerBlockType('swi-foot/schedule', {
apiVersion: 3,
title: __('Swiss Football Schedule', 'swi_foot_matchdata'),
category: 'embed',
category: 'swi-football',
supports: { align: true, anchor: true },
attributes: {
limit: { type: 'number', default: 10 }
limit: { type: 'number', default: 10 },
matchFilter: { type: 'string', default: 'all' }
},
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
const { limit } = attributes;
const { limit, matchFilter } = attributes;
return (
<div {...blockProps}>
@ -90,106 +108,43 @@ registerBlockType('swi-foot/schedule', {
<PanelBody title={__('Schedule settings', 'swi_foot_matchdata')}>
<p style={{color: '#666', fontSize: '13px', marginBottom: '12px'}}>{__('Team is inherited from the container context.', 'swi_foot_matchdata')}</p>
<RangeControl label={__('Limit', 'swi_foot_matchdata')} value={limit || 10} onChange={val => setAttributes({ limit: val })} min={1} max={20} />
<SelectControl
label={__('Match Filter', 'swi_foot_matchdata')}
value={matchFilter || 'all'}
options={[
{ label: __('All Matches', 'swi_foot_matchdata'), value: 'all' },
{ label: __('Home Matches Only', 'swi_foot_matchdata'), value: 'home' },
{ label: __('Away Matches Only', 'swi_foot_matchdata'), value: 'away' }
]}
onChange={val => setAttributes({ matchFilter: val })}
/>
</PanelBody>
</InspectorControls>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>{__('Upcoming matches will render on the front-end.', 'swi_foot_matchdata')}</div>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>
<p><strong>{__('Schedule', 'swi_foot_matchdata')}</strong></p>
<p style={{fontSize: '13px', color: '#666', marginBottom: '0'}}>{__('Team and match schedule inherited from context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}</p>
</div>
</div>
);
},
save: ({ attributes }) => null
});
/**
* NOTE: The shortcode-inserter block has been deprecated in favor of the
* paragraph toolbar button (SWI Inline Format) which provides a better UX.
* Users should use the toolbar button or the team-data block below.
*/
// Team Data composite block (generates shortcodes)
registerBlockType('swi-foot/team-data', {
title: __('Swiss Football Team Data', 'swi_foot_matchdata'),
category: 'embed',
supports: { align: true, anchor: true },
attributes: {
selectedTeam: { type: 'string', default: '' },
dataType: { type: 'string', default: '' },
selectedMatch: { type: 'string', default: '' },
shortcodeType: { type: 'string', default: 'standings' },
format: { type: 'string', default: '' },
separator: { type: 'string', default: ':' }
},
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
const { selectedTeam, dataType, selectedMatch, shortcodeType, format, separator } = attributes;
const [teams, setTeams] = useState([]);
const [matches, setMatches] = useState([]);
const [loadingTeams, setLoadingTeams] = useState(true);
const [loadingMatches, setLoadingMatches] = useState(false);
useEffect(() => {
makeAjaxCall('swi_foot_get_teams_for_editor').then(r => { if (r.success) setTeams(r.data); setLoadingTeams(false); }).catch(()=>setLoadingTeams(false));
}, []);
useEffect(() => {
if (selectedTeam && dataType === 'match') {
setLoadingMatches(true);
makeAjaxCall('swi_foot_get_matches_for_team', { team_id: selectedTeam })
.then(r => { if (r.success) setMatches(r.data); setLoadingMatches(false); })
.catch(()=>setLoadingMatches(false));
} else {
setMatches([]);
}
}, [selectedTeam, dataType]);
// preview/controls omitted for brevity — editor-blocks.js contains full UI
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title={__('Team Selection', 'swi_foot_matchdata')}>
{loadingTeams ? <Spinner /> : (
<SelectControl label={__('1. Select Team', 'swi_foot_matchdata')} value={selectedTeam}
options={[{ value: '', label: __('Choose a team...', 'swi_foot_matchdata') }].concat(teams.map(t=>({value:t.value,label:t.label})))}
onChange={val=>setAttributes({selectedTeam:val, dataType:'', selectedMatch:''})} />)}
{selectedTeam && (
<SelectControl label={__('2. What to display', 'swi_foot_matchdata')} value={dataType} options={[{value:'',label:__('Choose...', 'swi_foot_matchdata')},{value:'stats',label:__('Standings','swi_foot_matchdata')},{value:'match',label:__('Match','swi_foot_matchdata')}]}
onChange={val=>setAttributes({dataType:val, selectedMatch:''})} />
)}
</PanelBody>
</InspectorControls>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>{__('Team data shortcode generator', 'swi_foot_matchdata')}</div>
</div>
);
},
save: ({ attributes }) => {
const { selectedTeam, dataType, selectedMatch, shortcodeType, format, separator } = attributes;
if (selectedTeam && dataType === 'stats') return createElement(RawHTML, null, `[swi_foot_standings team_id="${selectedTeam}"]`);
if (selectedTeam && dataType === 'match' && selectedMatch) {
let s = '';
if (selectedMatch === 'current') s = `[swi_foot_${shortcodeType} team_id="${selectedTeam}" show_current="true"`;
else s = `[swi_foot_${shortcodeType} match_id="${selectedMatch}"`;
if (format && (shortcodeType==='match_date' || shortcodeType==='match_time')) s += ` format="${format}"`;
if (separator!==':' && shortcodeType==='match_score') s += ` separator="${separator}"`;
s += ']';
return createElement(RawHTML, null, s);
}
return '';
}
});
// Match Roster
// Gets match and team from container context, only allows selecting which side (home/away) and bench option
registerBlockType('swi-foot/match-roster', {
apiVersion: 3,
title: __('Swiss Football Match Roster', 'swi_foot_matchdata'),
category: 'embed',
category: 'swi-football',
supports: { align: true, anchor: true },
attributes: {
side: { type: 'string', default: 'home' },
withBench: { type: 'boolean', default: false }
showStartingSquad: { type: 'boolean', default: true },
showBench: { type: 'boolean', default: false }
},
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
const { side, withBench } = attributes;
const { side, showStartingSquad, showBench } = attributes;
return (
<div {...blockProps}>
@ -201,22 +156,35 @@ registerBlockType('swi-foot/match-roster', {
value={side}
options={[{value:'home',label:__('Home Team', 'swi_foot_matchdata')},{value:'away',label:__('Away Team', 'swi_foot_matchdata')}]}
onChange={v=>setAttributes({side:v})}
__next40pxDefaultSize={true}
__nextHasNoMarginBottom={true}
/>
<ToggleControl
label={__('Include Bench Players', 'swi_foot_matchdata')}
checked={withBench}
onChange={v=>setAttributes({withBench:v})}
label={__('Show Starting Squad', 'swi_foot_matchdata')}
checked={showStartingSquad}
onChange={v=>setAttributes({showStartingSquad:v})}
__nextHasNoMarginBottom={true}
/>
<ToggleControl
label={__('Show Bench', 'swi_foot_matchdata')}
checked={showBench}
onChange={v=>setAttributes({showBench:v})}
__nextHasNoMarginBottom={true}
/>
</PanelBody>
</InspectorControls>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>{__('Match roster will render on the front-end.', 'swi_foot_matchdata')}</div>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>
<p><strong>{__('Match Roster', 'swi_foot_matchdata')}</strong></p>
<p style={{fontSize: '13px', color: '#666', marginBottom: '0'}}>{__('Team and match are inherited from the container context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}</p>
</div>
</div>
);
},
save: ({ attributes }) => {
const { side, withBench } = attributes;
const { side, showStartingSquad, showBench } = attributes;
let shortcode = '[swi_foot_roster side="' + side + '"';
if (withBench) shortcode += ' with_bench="true"';
if (showStartingSquad) shortcode += ' starting_squad="true"';
if (showBench) shortcode += ' bench="true"';
shortcode += ']';
return createElement(RawHTML, null, shortcode);
}
@ -225,8 +193,9 @@ registerBlockType('swi-foot/match-roster', {
// Match Events
// Gets match and team from container context, only allows setting refresh interval
registerBlockType('swi-foot/match-events', {
apiVersion: 3,
title: __('Swiss Football Match Events', 'swi_foot_matchdata'),
category: 'embed',
category: 'swi-football',
supports: { align: true, anchor: true },
attributes: {
refreshInterval: { type: 'number', default: 30 },
@ -259,10 +228,15 @@ registerBlockType('swi-foot/match-events', {
{ label: __('Oldest First', 'swi_foot_matchdata'), value: 'oldest_first' }
]}
help={__('Dynamic: newest events at top while match is ongoing, chronological (oldest first) after match ends', 'swi_foot_matchdata')}
__next40pxDefaultSize={true}
__nextHasNoMarginBottom={true}
/>
</PanelBody>
</InspectorControls>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>{__('Live events will be displayed on the front-end.', 'swi_foot_matchdata')}</div>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>
<p><strong>{__('Match Events', 'swi_foot_matchdata')}</strong></p>
<p style={{fontSize: '13px', color: '#666', marginBottom: '0'}}>{__('Live match events inherited from context. ✓ Will update during match.', 'swi_foot_matchdata')}</p>
</div>
</div>
);
},
@ -277,3 +251,74 @@ registerBlockType('swi-foot/match-events', {
}
});
// Match Bench
// Gets match from container context, displays team staff and bench players
registerBlockType('swi-foot/match-bench', {
apiVersion: 3,
title: __('Swiss Football Match Bench', 'swi_foot_matchdata'),
icon: 'clipboard-user',
category: 'swi-football',
supports: { align: true, anchor: true },
attributes: {
side: { type: 'string', default: 'home' }
},
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
const { side } = attributes;
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title={__('Display Options', 'swi_foot_matchdata')}>
<p style={{color: '#666', fontSize: '13px', marginBottom: '12px'}}>{__('Match is inherited from the container context.', 'swi_foot_matchdata')}</p>
<SelectControl
label={__('Team Side', 'swi_foot_matchdata')}
value={side}
options={[{value:'home',label:__('Home Team', 'swi_foot_matchdata')},{value:'away',label:__('Away Team', 'swi_foot_matchdata')}]}
onChange={v=>setAttributes({side:v})}
__next40pxDefaultSize={true}
__nextHasNoMarginBottom={true}
/>
</PanelBody>
</InspectorControls>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>
<p><strong>{__('Match Bench', 'swi_foot_matchdata')}</strong></p>
<p style={{fontSize: '13px', color: '#666', marginBottom: '0'}}>{__('Team bench and substitutes inherited from context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}</p>
</div>
</div>
);
},
save: ({ attributes }) => {
const { side } = attributes;
let shortcode = '[swi_foot_bench side="' + side + '"]';
return createElement(RawHTML, null, shortcode);
}
});
// Match Referees
// Gets match from container context, displays match officials and referees
registerBlockType('swi-foot/match-referees', {
apiVersion: 3,
title: __('Swiss Football Match Referees', 'swi_foot_matchdata'),
icon: 'whistle',
category: 'swi-football',
supports: { align: true, anchor: true },
edit: ({ attributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title={__('Settings', 'swi_foot_matchdata')}>
<p style={{color: '#666', fontSize: '13px'}}>{__('Match is inherited from the container context.', 'swi_foot_matchdata')}</p>
</PanelBody>
</InspectorControls>
<div className="swi-foot-editor-placeholder" style={{padding: '24px', cursor: 'pointer', background: '#f5f5f5', border: '2px solid #ddd', borderRadius: '4px'}}>
<p><strong>{__('Match Referees', 'swi_foot_matchdata')}</strong></p>
<p style={{fontSize: '13px', color: '#666', marginBottom: '0'}}>{__('Match officials and referees inherited from context. ✓ Data will render on the front-end.', 'swi_foot_matchdata')}</p>
</div>
</div>
);
},
save: () => null
});

View File

@ -280,6 +280,8 @@ function InlineDataModal( { onInsert, onClose, matchId } ) {
options={ INLINE_DATA_POINTS }
onChange={ setSelectedDataPoint }
help={ __( 'Select which match data to display', 'swi_foot_matchdata' ) }
__next40pxDefaultSize={true}
__nextHasNoMarginBottom={true}
/>
</PanelRow>
@ -322,6 +324,8 @@ function InlineDataModal( { onInsert, onClose, matchId } ) {
options={ SHORTCODE_OPTIONS }
onChange={ setSelectedShortcode }
help={ __( 'Select a shortcode to insert (logos, etc.)', 'swi_foot_matchdata' ) }
__next40pxDefaultSize={true}
__nextHasNoMarginBottom={true}
/>
</PanelRow>

View File

@ -35,6 +35,15 @@ class Swiss_Football_Matchdata {
}
public function init() {
// Detect version updates and flush REST API routes cache if needed
$stored_version = get_option('swi_foot_plugin_version');
if ($stored_version !== SWI_FOOT_PLUGIN_VERSION) {
// Plugin was updated, clear REST endpoints cache
delete_transient('rest_endpoints_data');
wp_cache_delete('rest_endpoints', 'swi-foot/v1');
update_option('swi_foot_plugin_version', SWI_FOOT_PLUGIN_VERSION);
}
// Register post meta to store per-post context (season/team/match) for posts and pages
$meta_args = array(
'type' => 'object',
@ -80,9 +89,14 @@ class Swiss_Football_Matchdata {
add_option('swi_foot_verein_id', '');
add_option('swi_foot_season_id', date('Y')); // Current year as default
add_option('swi_foot_match_cache_duration', 30);
add_option('swi_foot_plugin_version', SWI_FOOT_PLUGIN_VERSION);
// Flush rewrite rules
flush_rewrite_rules();
// Clear REST API route caches
delete_transient('rest_endpoints_data');
wp_cache_delete('rest_endpoints', 'swi-foot/v1');
}
public function deactivate() {