initial state

This commit is contained in:
Reindl David (IT-PTR-CEN2-SL10) 2026-03-18 19:58:24 +01:00
commit fbd595aa36
35 changed files with 8813 additions and 0 deletions

78
.gitignore vendored Normal file
View File

@ -0,0 +1,78 @@
# Development Dependencies
node_modules/
package-lock.json
# Build Artifacts
# The compiled assets/build/ directory should be included in distributions
# but can be regenerated with `npm run build`
assets/build/
# IDE and Editor Configuration
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# OS-Specific Files
.DS_Store
Thumbs.db
desktop.ini
.AppleDouble
.LSOverride
# Cache Files
.npm-cache/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# PHP and Development Caches
.cache/
.PHP_VERSION
# WordPress Debug Log
debug.log
# Translation Backup Files
# Keep .po, .pot, and .mo files in repo
# Only ignore editor backup files
*.po~
*.pot~
# Distribution Artifacts
dist/
builds/
*.zip
# Environment Files
.env
.env.local
.env.*.local
# Test Coverage
coverage/
.coverage
# Temporary Files
*.tmp
*.temp
*.bak
# macOS Specific
.DS_Store
.AppleDouble
.LSOverride
._*
# Linux Temporary Files
.directory
# Editor Lock Files
*~
*.lock
# Ignore Everything in Docs Backup (if template/reference only)
# Uncomment if ~docs/ is just a reference copy and not part of distribution
~docs/

506
README-DEV.md Normal file
View File

@ -0,0 +1,506 @@
# Swiss Football Matchdata — Developer Setup Guide
This guide explains how to set up the development environment after checking out the project from Git and how to generate a distribution-ready package.
## Prerequisites
Before starting, ensure you have the following installed on your system:
- **Node.js** (LTS 18.x or newer) - [download](https://nodejs.org/)
- **npm** (comes with Node.js)
- **Git** (for cloning and version control)
- **PHP 7.4+** (for running WordPress locally, if developing)
- **WordPress** (local development installation recommended)
Verify installations:
```bash
node --version
npm --version
php --version
```
## Initial Setup After Git Checkout
### 1. Clone the Repository
```bash
git clone <repository-url> swiss-football-matchdata
cd swiss-football-matchdata
```
### 2. Install Dependencies
Install all npm dependencies required for building the Gutenberg editor blocks:
```bash
npm install
```
This reads `package.json` and installs:
- `@wordpress/scripts` — WordPress build tooling (webpack, babel, eslint)
### 3. Build the Editor Bundle
Create the compiled editor JavaScript bundle that the plugin requires:
```bash
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/`
- **Result**: `assets/build/editor-blocks.js` is now ready for use
### 4. Verify the Build
Check that the compiled bundle exists:
```bash
ls -lh assets/build/editor-blocks.js
```
You should see a JavaScript file (typically 50-150 KB depending on dependencies).
## Development Workflow
### Live Development with Hot Reload
For active development with automatic recompilation and browser refresh:
```bash
npm run start
```
This starts the WordPress development server with:
- **File watching**: Changes to `src/editor-blocks.js` trigger rebuilds
- **Hot module reload**: Changes appear in the editor without full page refresh
- **Source maps**: Easier debugging in browser DevTools
Press `Ctrl+C` to stop the server.
### Project Structure
```
swiss-football-matchdata/
├── src/
│ ├── editor-blocks.js # Main editor block components
│ ├── format-shortcode.js # Shortcode formatting toolbar
│ ├── index.js # Package entry point
│ └── [other source files]
├── blocks/ # Block definitions (metadata)
│ ├── context/
│ ├── match-events/
│ ├── match-roster/
│ ├── schedule/
│ ├── standings/
│ ├── team-data/
│ └── shortcode-inserter/
├── assets/
│ ├── build/
│ │ ├── editor-blocks.js # Compiled output (generated)
│ │ ├── editor-blocks.asset.php # Asset dependencies (generated)
│ │ └── [other generated assets]
│ ├── admin.js / admin.css # Admin-only frontend code
│ ├── blocks.js / blocks.css # Public frontend code
│ └── [other assets]
├── includes/ # PHP backend
│ ├── class-swi-foot-admin.php
│ ├── class-swi-foot-api.php
│ ├── class-swi-foot-blocks.php
│ ├── class-swi-foot-rest.php
│ └── class-swi-foot-shortcodes.php
├── languages/ # Translation files (i18n)
├── tests/ # Test files
├── webpack.config.js # Custom webpack configuration
├── package.json # npm dependencies and scripts
├── README.md # User documentation
├── DEV-README.md # Legacy build notes
└── README-DEV.md # This file
```
## Editing WordPress Blocks
### 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`
### Example Workflow
1. **Edit block in editor** (`src/editor-blocks.js`):
- Add UI elements (SelectControl, TextControl, etc.)
- Implement save logic that generates shortcodes
- 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)
3. **Verify output**:
- Insert/edit the block in WordPress
- Check browser DevTools → Network → look for `editor-blocks.js`
- Look at page source to verify shortcode is generated correctly
4. **Build for production**:
```bash
npm run build
```
## Code Quality
### Linting and Formatting
The `@wordpress/scripts` package includes eslint configuration. WordPress code standards are automatically enforced on `.js` files in the project.
To fix common issues automatically:
```bash
npx eslint src/ --fix
```
### PHP Code
PHP files follow standard WordPress coding standards. Check syntax:
```bash
php -l includes/class-swi-foot-blocks.php
```
(or any other PHP file)
### Managing Translations
Translation files include:
- **`.po` files** — Human-editable source translations (one per language)
- **`.pot` file** — Translation template (extraction from source code)
- **`.mo` files** — Compiled binary translations (generated from `.po` files)
**Workflow when adding new translatable strings:**
1. Add translation strings to PHP or JavaScript with proper functions:
```php
__('Text to translate', '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)
3. Update existing `.po` translation files from the new template (done by translators in PoEdit or similar)
4. Generate binary `.mo` files:
```bash
wp i18n make-mo languages/
```
5. Commit both `.po` and `.mo` files to git:
```bash
git add languages/*.po languages/*.mo
git commit -m "Update translations for [feature/language]"
```
**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.
## Testing in Development
### Local WordPress Installation
For testing the complete plugin:
1. **Place the plugin in WordPress**:
```bash
# Assuming WordPress is at /path/to/wordpress
cp -r . /path/to/wordpress/wp-content/plugins/swiss-football-matchdata
```
2. **Activate the plugin**:
- Go to WordPress Admin → Plugins
- Find "Swiss Football Matchdata"
- Click "Activate"
3. **Test blocks**:
- Create a new post/page
- Insert blocks using the editor
- Verify blocks are available and functional
- Hard-refresh browser to clear cache after rebuilds
### Browser DevTools Checklist
- **Console**: Watch for JavaScript errors
- **Network**: Verify `editor-blocks.js` loads correctly
- **Sources**: Use source maps to debug original `.js` files
- **Application → Storage**: Clear LocalStorage/IndexedDB if blocks misbehave
## Building for Distribution
### 1. Ensure Clean Build
```bash
# Remove old build artifacts (optional)
rm -rf assets/build
# Install fresh dependencies
npm ci # (instead of npm install - more reproducible)
# Build
npm run build
```
### 2. Verify Build Output
```bash
# All required files should exist
test -f assets/build/editor-blocks.js && echo "✓ Editor bundle present"
test -f assets/build/editor-blocks.asset.php && echo "✓ Asset manifest present"
```
### 3. Create Distribution Package
#### Method A: Manual Packaging (Recommended)
```bash
# Create a clean directory for the distribution
mkdir -p dist
PLUGIN_NAME="swiss-football-matchdata"
# Copy all plugin files (excluding development files)
rsync -av --exclude='.git' \
--exclude='node_modules' \
--exclude='.npm' \
--exclude='package-lock.json' \
--exclude='.gitignore' \
--exclude='README-DEV.md' \
--exclude='.DS_Store' \
--exclude='*.swp' \
--exclude='.idea/' \
. dist/$PLUGIN_NAME/
# Create ZIP archive
cd dist
zip -r "$PLUGIN_NAME.zip" $PLUGIN_NAME/
cd ..
# Result: dist/swiss-football-matchdata.zip
echo "Distribution package created: dist/$PLUGIN_NAME.zip"
```
#### Method B: Using Git Archive (Cleaner)
If your `.gitignore` is properly configured to exclude development files:
```bash
git archive --output=dist/swiss-football-matchdata.zip HEAD \
--prefix=swiss-football-matchdata/
```
This creates the distribution package from committed files only (respecting `.gitignore`).
### 4. Verify Distribution Package
```bash
# What's in the ZIP?
unzip -l dist/swiss-football-matchdata.zip | head -30
# Should include:
# ✓ swiss-football-matchdata.php (main plugin file)
# ✓ assets/build/editor-blocks.js (compiled bundle)
# ✓ includes/*.php (backend code)
# ✓ README.md (user documentation)
# Should NOT include:
# ✗ node_modules/ (development dependencies)
# ✗ package.json / package-lock.json
# ✗ README-DEV.md (developer guide)
# ✗ .git (git history)
# ✗ src/ (source code — users don't need it)
```
### 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
If significantly larger, check for unwanted files:
```bash
unzip -l dist/swiss-football-matchdata.zip | sort -k4 -rn | head -20
```
## Deployment / Installation
### For Site Administrators (Users)
Users install the distribution ZIP file normally:
1. Go to WordPress Admin → Plugins → Add New → Upload Plugin
2. Select the `.zip` file
3. Activate
**Important**: The plugin expects `assets/build/editor-blocks.js` to be present. Always run `npm run build` before packaging.
### For Developers on Target Site
If deploying to a site where you're also developing:
```bash
# Copy the plugin to WordPress
cp -r . /path/to/wordpress/wp-content/plugins/swiss-football-matchdata
# Navigate to plugin directory
cd /path/to/wordpress/wp-content/plugins/swiss-football-matchdata
# Ensure build is present (if needed)
npm ci && npm run build
```
## Continuous Integration (Optional)
### GitHub Actions Example
Add to `.github/workflows/build.yml`:
```yaml
name: Build and Verify
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build editor bundle
run: npm run build
- name: Verify build output
run: |
test -f assets/build/editor-blocks.js || exit 1
test -f assets/build/editor-blocks.asset.php || exit 1
- name: PHP Syntax Check
run: |
find includes -name "*.php" -exec php -l {} \;
```
This ensures:
- Every push/PR has a valid build
- Missing build artifacts fail visibly
- PHP code is syntactically correct
## Troubleshooting
### Build Fails with Module Not Found
**Problem**: `npm run build` fails with "module not found"
**Solution**:
```bash
rm package-lock.json
npm install
npm run build
```
### Changes Don't Appear in Editor
**Problem**: Edited `src/editor-blocks.js` but changes don't show in WordPress
**Solutions**:
1. Hard-refresh browser: `Cmd+Shift+R` (Mac) or `Ctrl+Shift+R` (Windows/Linux)
2. Clear WordPress cache (if caching plugin is active)
3. Verify `assets/build/editor-blocks.js` was updated: `ls -l assets/build/editor-blocks.js`
4. Check browser console for JavaScript errors
### `npm start` Doesn't Work
**Problem**: Development server won't start
**Solution**:
```bash
# Kill any existing Node process on port 8888
lsof -i :8888 | grep -v PID | awk '{print $2}' | xargs kill -9
# Restart
npm start
```
### PHP Errors
**Problem**: Plugin doesn't activate or causes errors
**Solutions**:
1. Check WordPress error log: `wp-content/debug.log`
2. Verify PHP version: `php --version` (should be 7.4+)
3. Check file permissions: `chmod -R 755 includes/`
4. Look for syntax errors: `php -l includes/class-swi-foot-blocks.php`
## Quick Reference
| Task | Command |
|------|---------|
| Install deps | `npm install` |
| Build for production | `npm run build` |
| Development with hot reload | `npm start` |
| Create distribution ZIP | `git archive --output=dist/swiss-football-matchdata.zip HEAD --prefix=swiss-football-matchdata/` |
| Check PHP syntax | `php -l includes/class-swi-foot-blocks.php` |
| Fix linting issues | `npx eslint src/ --fix` |
## Additional Resources
- [WordPress Block Editor Handbook](https://developer.wordpress.org/block-editor/)
- [WordPress Plugin Handbook](https://developer.wordpress.org/plugins/)
- [@wordpress/scripts Documentation](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/)
- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/)
## Support
For issues or questions:
1. Check existing Git issues
2. Review comments in the relevant PHP/JavaScript files
3. Consult the main [README.md](README.md) for user-facing documentation
4. See [DEV-README.md](DEV-README.md) for legacy build notes
---
Last updated: March 2026

135
README.md Normal file
View File

@ -0,0 +1,135 @@
# Swiss Football Matchdata
Connect to the Swiss Football Association API and display match data, standings, schedules and match elements using flexible shortcodes and Gutenberg blocks.
## Description
Swiss Football Matchdata provides a small, focused integration with the Swiss Football Association API so you can embed:
- Team standings and rankings
- Upcoming match schedules
- Detailed match information
- Individual match elements via shortcodes
- Gutenberg blocks for visual composition
The plugin includes caching, automatic token management, and both shortcodes and Gutenberg blocks for flexibility.
## Editor Blocks — Usage Guide
This section explains how the plugin's Gutenberg editor blocks are intended to be used in the editor. If a block is unavailable in your editor, ensure the editor script is enqueued and that no console ReferenceErrors appear.
### Insertions Methods
#### 1. Paragraph Toolbar Shortcode Button (Recommended)
- **Purpose**: Quickly insert any Swiss Football shortcode directly into paragraphs or other text blocks.
- **How to use**:
1. Click in a paragraph block where you want to insert content.
2. Look for the **shortcode icon** in the paragraph's formatting toolbar (top of the editor).
3. Click the button to open the Shortcode insertion modal.
4. Select the shortcode type (e.g., "Full Match Display", "Score", etc.).
5. Choose a team and/or match from the dropdowns.
6. Click "Insert Shortcode" — the shortcode text is inserted at your cursor.
- **Notes**:
- This is the fastest way to add shortcodes inline.
- 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)
- **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**:
1. Insert the `SWI Football Context` block at the place in the document that should scope its child blocks (usually near the top of the content where you want the team/season context to apply).
2. Open the block's Inspector Controls (right sidebar) to set context values such as `Season` or `Team` where supported.
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)
- **Swiss Football Standings**
- Purpose: Display a league standings table for a team.
- How to use: Insert the block, select a team from Inspector Controls.
- **Swiss Football Schedule**
- Purpose: Display upcoming matches for a team.
- How to use: Insert the block, select a team and set the match limit.
- **Swiss Football Match Roster**
- Purpose: Show a roster for a specific match. Works best inside a Context Provider.
- How to use: Insert the block, select a team and match. Optionally configure which side (home/away) and whether to include bench players.
- **Swiss Football Match Events**
- Purpose: Display live events (goals, cards, substitutions) for a match. Works best inside a Context Provider.
- How to use: Insert the block, select a team and match. Configure auto-refresh interval for live updates.
### Deprecated
- **Swiss Football Shortcode Inserter block** — This block is deprecated in favor of the paragraph toolbar button above. It has been retained for backwards compatibility with existing posts but is no longer recommended for new content.
## Shortcodes
Shortcodes provide additional flexibility where blocks are not desirable. Examples:
```html
[swi_foot_match match_id="12345"]
[swi_foot_match_home_team match_id="12345"]
[swi_foot_match_date match_id="12345" format="d.m.Y"]
```
Refer to the original `readme.txt` for a complete list of shortcodes and their parameters.
## Installation
1. Upload the plugin files to `/wp-content/plugins/swiss-football-matchdata/`.
2. Activate the plugin via Plugins → Installed Plugins.
3. Go to Settings → Swiss Football and configure your API credentials (base URL, application key, pass, Verein ID, Season ID).
4. Use the Shortcode helper or Gutenberg blocks in your posts/pages.
## Configuration
- **API Base URL**: Set the API endpoint for Swiss Football.
- **API Username / Password**: Application key and password provided by Swiss Football.
- **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.
## License
GPL v2 or later — see `readme.txt` for the original header and license URI.

328
assets/admin.css Normal file
View File

@ -0,0 +1,328 @@
/* Swiss Football Admin Styles */
/* Main admin sections */
.swi-foot-admin-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
}
.swi-foot-admin-section h2 {
margin-bottom: 15px;
color: #23282d;
}
/* Teams grid layout */
.swi-foot-teams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 15px;
margin-top: 15px;
}
.team-card {
padding: 15px;
border: 1px solid #ddd;
border-radius: 6px;
background: #f5f7fa;
transition: all 0.3s ease;
position: relative;
}
.team-card:hover {
background: #f0f0f0;
border-color: #007cba;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.team-card strong {
display: block;
margin-bottom: 8px;
color: #23282d;
font-size: 14px;
}
.team-card small {
color: #666;
line-height: 1.4;
}
/* Shortcodes help styling */
.swi-foot-shortcodes-help {
font-size: 12px;
}
.shortcode-group {
margin-bottom: 15px;
padding: 12px;
background: #eef3f7;
border-radius: 4px;
border-left: 3px solid #007cba;
}
.shortcode-group strong {
display: block;
margin-bottom: 8px;
color: #111;
font-size: 13px;
}
.shortcode-group code {
display: block;
margin: 4px 0;
padding: 4px 6px;
background: #fff;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 11px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
cursor: pointer;
transition: all 0.2s ease;
}
.shortcode-group code:hover {
background: #f0f8ff;
border-color: #007cba;
transform: translateX(2px);
}
.shortcode-group code:active {
background: #e6f3ff;
}
.shortcode-group ul {
margin-top: 8px;
padding-left: 20px;
}
.shortcode-group li {
margin: 3px 0;
line-height: 1.4;
}
.shortcode-group li code {
display: inline;
margin: 0;
padding: 1px 3px;
cursor: default;
}
.shortcode-group li code:hover {
transform: none;
}
/* Status indicators */
#refresh-status,
#cache-status,
#connection-status {
margin-left: 12px;
font-style: italic;
font-weight: 500;
}
#refresh-status.success,
#cache-status.success,
#connection-status.success {
color: #46b450;
}
#refresh-status.error,
#cache-status.error,
#connection-status.error {
color: #dc3232;
}
/* Quick shortcode reference */
.shortcode-quick-ref {
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
padding: 20px;
}
.shortcode-examples h4 {
margin-bottom: 15px;
color: #23282d;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.shortcode-examples p {
margin-bottom: 15px;
line-height: 1.6;
}
.shortcode-examples strong {
color: #23282d;
}
.shortcode-examples code {
background: #f1f1f1;
padding: 2px 4px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
}
/* Button enhancements */
.button {
transition: all 0.2s ease;
}
.button:hover {
transform: translateY(-1px);
}
.button:active {
transform: translateY(0);
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
/* Settings form styling */
.form-table th {
padding-top: 15px;
vertical-align: top;
}
.form-table td {
padding-top: 12px;
}
.form-table input[type="text"],
.form-table input[type="url"],
.form-table input[type="password"],
.form-table input[type="number"] {
transition: border-color 0.3s ease;
}
.form-table input[type="text"]:focus,
.form-table input[type="url"]:focus,
.form-table input[type="password"]:focus,
.form-table input[type="number"]:focus {
border-color: #007cba;
box-shadow: 0 0 0 1px #007cba;
}
.form-table .description {
color: #666;
font-style: italic;
margin-top: 5px;
}
/* Notice styling */
.notice {
margin: 15px 0;
}
.notice.notice-success {
border-left-color: #46b450;
}
.notice.notice-error {
border-left-color: #dc3232;
}
/* Meta box styling in post editor */
#swi-foot-shortcodes .inside {
padding: 15px;
}
#swi-foot-shortcodes .shortcode-group {
background: #f8f9fa;
border: 1px solid #ddd;
border-left: 3px solid #007cba;
}
/* Loading states */
.loading {
opacity: 0.6;
pointer-events: none;
}
.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007cba;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 768px) {
.swi-foot-teams-grid {
grid-template-columns: 1fr;
}
.shortcode-group code {
font-size: 10px;
padding: 3px 5px;
}
.form-table th,
.form-table td {
display: block;
width: 100%;
padding: 10px 0;
}
.form-table th {
font-weight: 600;
margin-bottom: 5px;
}
}
/* Dark mode support for WordPress admin */
@media (prefers-color-scheme: dark) {
.team-card {
background: #2c2c2c;
border-color: #555;
color: #f5f7fa;
}
.team-card:hover {
background: #383838;
}
.team-card strong {
color: #F4F4F4;
}
.shortcode-group {
background: #2c2c2c;
border-left-color: #00a0d2;
color: #eef6fb;
}
.shortcode-group code {
background: #1e1e1e;
border-color: #555;
color: #ffffff;
}
.shortcode-group code:hover {
background: #383838;
border-color: #00a0d2;
}
.shortcode-quick-ref {
background: #2c2c2c;
border-color: #555;
color: #f3f7fb;
}
}

94
assets/admin.js Normal file
View File

@ -0,0 +1,94 @@
// Swiss Football Admin JavaScript
jQuery(document).ready(function($) {
'use strict';
// Refresh teams functionality
$('#refresh-teams').on('click', function() {
var $button = $(this);
var $status = $('#refresh-status');
$button.prop('disabled', true).text('Refreshing...');
$status.text('').removeClass('success error');
fetch(swi_foot_ajax.rest_url.replace(/\/$/, '') + '/admin/refresh-teams', {
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('Teams refreshed successfully!').addClass('success');
setTimeout(function() { location.reload(); }, 1500);
} else {
$status.text('Error: ' + (response.error || 'Unknown')).addClass('error');
}
}).catch(function() {
$status.text('Network error occurred.').addClass('error');
}).finally(function() {
$button.prop('disabled', false).text('Refresh Teams List');
});
});
// Clear cache functionality
$('#clear-cache').on('click', function() {
var $button = $(this);
var $status = $('#cache-status');
if (!confirm('Are you sure you want to clear the match data cache?')) {
return;
}
$button.prop('disabled', true);
$status.text('Clearing cache...').removeClass('success error');
fetch(swi_foot_ajax.rest_url.replace(/\/$/, '') + '/admin/clear-cache', {
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('Cache cleared successfully!').addClass('success');
setTimeout(function() { location.reload(); }, 1000);
} else {
$status.text('Error clearing cache.').addClass('error');
}
}).catch(function() {
$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');
});
});

381
assets/blocks.css Normal file
View File

@ -0,0 +1,381 @@
/* Swiss Football Blocks Styles - Minimal */
/* Editor block wrapper - identifies blocks in editor */
.swi-foot-editor-block {
background-color: #f5f5f5;
border: 0.5px solid #d0d0d0;
padding: 20px;
margin: 20px 0;
border-radius: 4px;
}
/* Block container styles */
.swi-foot-standings,
.swi-foot-schedule,
.swi-foot-match-container {
margin: 20px 0;
padding: 20px;
border: 0.5px solid #d0d0d0;
border-radius: 4px;
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Table styles */
.swi-foot-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
font-size: 14px;
}
.swi-foot-table th,
.swi-foot-table td {
padding: 10px 8px;
text-align: left;
border-bottom: 1px solid #eee;
}
.swi-foot-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
font-size: 13px;
text-transform: uppercase;
}
.swi-foot-table tr.highlight {
background-color: #fff3cd;
font-weight: 600;
}
/* Event item styles */
.swi-foot-event {
padding: 16px;
margin-bottom: 12px;
border: 1px solid #e9ecef;
border-left: 4px solid #007cba;
border-radius: 6px;
background-color: #fdfdfe;
}
.event-date {
font-weight: 600;
color: #495057;
font-size: 14px;
margin-bottom: 6px;
}
.event-teams {
font-size: 16px;
font-weight: 600;
margin: 8px 0;
color: #212529;
}
/* Match container - center aligned */
.swi-foot-match-container {
text-align: center;
max-width: 450px;
margin: 20px auto;
}
.match-teams {
font-size: 20px;
font-weight: 700;
margin-bottom: 16px;
color: #212529;
}
.match-teams .vs {
margin: 0 15px;
color: #6c757d;
font-weight: 400;
}
.match-date {
font-size: 16px;
color: #495057;
font-weight: 500;
}
.match-score {
font-size: 28px;
font-weight: 800;
color: #007cba;
margin: 15px 0;
padding: 12px 16px;
border-radius: 8px;
display: inline-block;
}
.match-status {
font-size: 12px;
padding: 6px 12px;
border-radius: 15px;
background-color: #e7f3ff;
color: #007cba;
font-weight: 600;
}
/* Inline shortcodes */
.swi-foot-inline {
display: inline;
}
.swi-foot-inline.home-team,
.swi-foot-inline.away-team {
font-weight: 600;
color: #007cba;
}
.swi-foot-inline.score {
font-weight: 700;
color: #007cba;
}
.swi-foot-inline.status {
background: #007cba;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
/* Error messages */
.swi-foot-error {
color: #721c24;
padding: 12px 16px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 6px;
margin: 15px 0;
font-weight: 500;
}
/* Responsive design */
@media (max-width: 768px) {
.swi-foot-table {
font-size: 12px;
}
.swi-foot-table th,
.swi-foot-table td {
padding: 6px 4px;
}
.swi-foot-table th:nth-child(n+6),
.swi-foot-table td:nth-child(n+6) {
display: none;
}
}
@media (max-width: 480px) {
.swi-foot-table th:nth-child(n+4),
.swi-foot-table td:nth-child(n+4) {
display: none;
}
}
/* Enhanced Events Display Styles */
.swi-foot-events {
margin: 20px 0;
padding: 20px;
background-color: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
.swi-foot-events h3 {
margin-top: 0;
margin-bottom: 20px;
font-size: 18px;
font-weight: 600;
color: #333;
}
.events-timeline {
display: flex;
flex-direction: column;
gap: 12px;
}
.event-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background-color: white;
border-left: 4px solid #007cba;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s ease;
}
.event-item:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.event-minute {
display: flex;
align-items: center;
justify-content: center;
min-width: 70px;
padding: 8px 10px;
background-color: #f0f6ff;
border-radius: 4px;
font-weight: 600;
color: #007cba;
font-size: 16px;
white-space: nowrap;
}
.minute-time {
display: flex;
align-items: center;
gap: 4px;
letter-spacing: 0.5px;
}
.event-main {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.event-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
}
.event-type-info {
flex: 1;
min-width: 150px;
}
.event-type {
font-size: 15px;
color: #222;
}
.event-subtype {
font-size: 13px;
color: #666;
font-weight: normal;
}
.team-logo {
display: flex;
align-items: center;
justify-content: center;
min-width: 50px;
height: 50px;
background-color: #f5f5f5;
border-radius: 4px;
overflow: hidden;
}
.team-logo img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
padding: 2px;
}
.team-name-fallback {
display: inline-block;
padding: 4px 8px;
font-size: 11px;
font-weight: 600;
color: #666;
background-color: #e8e8e8;
border-radius: 3px;
white-space: nowrap;
}
.event-details {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
font-size: 13px;
color: #555;
}
.player-info {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background-color: #f0f0f0;
border-radius: 3px;
}
.substitute-info {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background-color: #fff3cd;
border-radius: 3px;
color: #856404;
}
/* Team Logo Shortcodes */
.swi-foot-team-logo {
max-width: 120px;
max-height: 120px;
height: auto;
object-fit: contain;
border-radius: 4px;
}
.swi-foot-team-logo.home-team {
margin-right: 10px;
}
.swi-foot-team-logo.away-team {
margin-left: 10px;
}
/* Responsive Design for Events */
@media (max-width: 768px) {
.event-item {
flex-wrap: wrap;
}
.event-header {
width: 100%;
}
.team-logo {
order: -1;
margin-bottom: 8px;
}
.minute-time {
font-size: 14px;
}
}
@media (max-width: 480px) {
.event-minute {
min-width: 60px;
font-size: 14px;
}
.event-details {
font-size: 12px;
gap: 8px;
}
.swi-foot-team-logo {
max-width: 80px;
max-height: 80px;
}
}

297
assets/blocks.js Normal file
View File

@ -0,0 +1,297 @@
// Swiss Football Blocks Frontend JavaScript
(function($) {
'use strict';
// Team selector functionality for blocks
window.swiFootSelectTeam = function(teamId, blockType) {
if (!teamId) return;
// This could be enhanced to use AJAX for dynamic loading
const url = new URL(window.location);
url.searchParams.set('swi_foot_team_' + blockType, teamId);
window.location.href = url.toString();
};
// Auto-refresh functionality for events shortcodes
$(document).ready(function() {
initEventRefresh();
setupShortcodeHelpers();
});
/**
* Initialize event refresh polling for all match event shortcodes on page
*
* Finds all `.swi-foot-events[data-match-id][data-refresh]` elements and sets up
* automatic polling based on the configured refresh interval. Respects match status
* (does not update UI until match starts, stops polling when match ends).
*
* @returns {void}
*/
function initEventRefresh() {
const $eventContainers = $('.swi-foot-events[data-match-id][data-refresh]');
if ($eventContainers.length === 0) return;
$eventContainers.each(function() {
const $container = $(this);
const matchId = $container.data('match-id');
const interval = parseInt($container.data('refresh')) || 30;
if (!matchId) return;
// Initial check: determine if match has started and if it should refresh
checkMatchStatusAndRefresh($container, matchId, interval);
});
}
/**
* Initialize polling interval for match events
*
* Sets up a setInterval that periodically fetches the latest events for a match.
* Automatically clears the interval when the match ends.
*
* @param {jQuery} $container - jQuery element for the event container
* @param {string} matchId - ID of the match to fetch events for
* @param {number} interval - Refresh interval in seconds (e.g., 30)
* @returns {void}
*/
function checkMatchStatusAndRefresh($container, matchId, interval) {
fetchMatchEvents($container, matchId);
// Set up polling - will stop automatically once match ends
const pollInterval = setInterval(function() {
const hasEnded = $container.data('match-ended');
// Stop polling if match has ended
if (hasEnded) {
clearInterval(pollInterval);
return;
}
fetchMatchEvents($container, matchId);
}, interval * 1000);
}
/**
* Fetch latest match events from REST API
*
* Makes an authenticated request to `/wp-json/swi-foot/v1/events/{matchId}` to
* retrieve the current list of match events. Updates container data with match
* status (started/ended). Only updates the DOM if the match has started.
*
* @param {jQuery} $container - jQuery element for the event container
* @param {string} matchId - Match ID to fetch events for
* @returns {void}
*/
function fetchMatchEvents($container, matchId) {
const restUrl = window.swiFootRest ? window.swiFootRest.rest_url : '/wp-json';
const nonce = window.swiFootRest ? window.swiFootRest.rest_nonce : '';
// Get event order from data attribute
const eventOrder = $container.data('event-order') || 'dynamic';
// Build URL with event_order parameter
let url = restUrl + 'swi-foot/v1/events/' + encodeURIComponent(matchId);
if (eventOrder && eventOrder !== 'dynamic') {
url += '?event_order=' + encodeURIComponent(eventOrder);
}
fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'X-WP-Nonce': nonce
}
})
.then(function(resp) {
if (!resp.ok) throw new Error('Failed to fetch events');
return resp.json();
})
.then(function(data) {
if (!data) return;
// Track match status for polling control
$container.data('match-started', data.hasMatchStarted);
$container.data('match-ended', data.hasMatchEnded);
// Only update if match has started
if (!data.hasMatchStarted) {
return; // Don't update UI, match hasn't started yet
}
// Update events list
updateEventsList($container, data.events);
})
.catch(function(err) {
console.error('Error fetching match events:', err);
});
}
/**
* Update the events timeline display with new events
*
* Renders an HTML list of match events from the API response, sorted newest first.
* Handles empty event lists with a localized message. Updates the `.events-timeline`
* element within the container.
*
* @param {jQuery} $container - jQuery element for the event container
* @param {Array<Object>} events - Array of event objects with matchMinute, eventTypeName, playerName, teamName
* @returns {void}
*/
function updateEventsList($container, events) {
const $timeline = $container.find('.events-timeline');
if ($timeline.length === 0) return;
// Build HTML for events
let html = '';
if (events && events.length > 0) {
events.forEach(function(event) {
html += '<div class="event-item">';
html += '<div class="event-time">' + (event.matchMinute || '') + '\'</div>';
html += '<div class="event-content">';
html += '<div class="event-type">' + (event.eventTypeName || '') + '</div>';
html += '<div class="event-details">';
if (event.playerName) {
html += '<span class="player">' + event.playerName + '</span>';
}
if (event.teamName) {
html += '<span class="team">(' + event.teamName + ')</span>';
}
html += '</div></div></div>';
});
} else {
html = '<p>' + (window.swiFootNoEvents || 'No events recorded yet.') + '</p>';
}
$timeline.html(html);
}
function setupShortcodeHelpers() {
// Add copy functionality to shortcode examples in admin
if ($('.shortcode-group code').length > 0) {
$('.shortcode-group code').each(function() {
const $code = $(this);
$code.css('cursor', 'pointer');
$code.attr('title', 'Click to copy shortcode');
$code.on('click', function() {
const shortcode = $(this).text();
copyToClipboard(shortcode);
});
});
}
}
function copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(function() {
showNotification('Shortcode copied to clipboard!', 'success');
}).catch(function() {
fallbackCopyTextToClipboard(text);
});
} else {
fallbackCopyTextToClipboard(text);
}
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showNotification('Shortcode copied to clipboard!', 'success');
} catch (err) {
showNotification('Failed to copy shortcode', 'error');
}
document.body.removeChild(textArea);
}
function showNotification(message, type) {
const $notification = $('<div class="swi-foot-notification swi-foot-notification-' + type + '">' + message + '</div>');
$notification.css({
position: 'fixed',
top: '20px',
right: '20px',
padding: '10px 15px',
borderRadius: '4px',
color: '#fff',
fontSize: '14px',
zIndex: '10000',
opacity: '0',
transform: 'translateY(-20px)',
transition: 'all 0.3s ease'
});
if (type === 'success') {
$notification.css('backgroundColor', '#28a745');
} else {
$notification.css('backgroundColor', '#dc3545');
}
$('body').append($notification);
// Animate in
setTimeout(function() {
$notification.css({
opacity: '1',
transform: 'translateY(0)'
});
}, 100);
// Remove after 3 seconds
setTimeout(function() {
$notification.css({
opacity: '0',
transform: 'translateY(-20px)'
});
setTimeout(function() {
$notification.remove();
}, 300);
}, 3000);
}
// Initialize tooltips for match elements
$(document).ready(function() {
$('.swi-foot-match-container [title]').each(function() {
const $element = $(this);
$element.on('mouseenter', function() {
// Simple tooltip implementation
const title = $element.attr('title');
if (title) {
const $tooltip = $('<div class="swi-foot-tooltip">' + title + '</div>');
$tooltip.css({
position: 'absolute',
background: '#333',
color: '#fff',
padding: '5px 8px',
borderRadius: '4px',
fontSize: '12px',
zIndex: '1000',
whiteSpace: 'nowrap'
});
$('body').append($tooltip);
const rect = this.getBoundingClientRect();
$tooltip.css({
left: rect.left + (rect.width / 2) - ($tooltip.outerWidth() / 2),
top: rect.top - $tooltip.outerHeight() - 5
});
}
}).on('mouseleave', function() {
$('.swi-foot-tooltip').remove();
});
});
});
})(jQuery);

156
assets/post-context.js Normal file
View File

@ -0,0 +1,156 @@
(function(wp){
const { registerPlugin } = wp.plugins;
const PluginDocumentSettingPanel = (wp && wp.editor && wp.editor.PluginDocumentSettingPanel) ? wp.editor.PluginDocumentSettingPanel : (wp && wp.editPost && wp.editPost.PluginDocumentSettingPanel) ? wp.editPost.PluginDocumentSettingPanel : null;
const { withSelect, withDispatch } = wp.data;
const { useState, useEffect } = wp.element;
const { SelectControl, PanelRow, Spinner, Button } = wp.components;
function PostContextPanel(props) {
const meta = (props.meta || {});
const ctx = meta.swi_foot_context || {};
if (!PluginDocumentSettingPanel) return null;
const [teams, setTeams] = useState([]);
const [matches, setMatches] = useState([]);
const [loadingTeams, setLoadingTeams] = useState(false);
const [loadingMatches, setLoadingMatches] = useState(false);
const restBaseRaw = (window.swi_foot_post_context && window.swi_foot_post_context.rest_url) || '/wp-json/swi-foot/v1';
const restBase = restBaseRaw.replace(/\/$/, '');
useEffect(() => {
setLoadingTeams(true);
fetch(restBase + '/teams', { credentials: 'same-origin', headers: { 'X-WP-Nonce': window.swi_foot_post_context.rest_nonce } })
.then(r => r.json()).then(data => {
if (Array.isArray(data)) setTeams(data);
}).catch(() => {}).finally(() => setLoadingTeams(false));
}, []);
useEffect(() => {
const teamId = ctx.team_id || '';
if (!teamId) {
setMatches([]);
return;
}
setLoadingMatches(true);
fetch(restBase + '/matches?team_id=' + encodeURIComponent(teamId), { credentials: 'same-origin', headers: { 'X-WP-Nonce': window.swi_foot_post_context.rest_nonce } })
.then(r => r.json()).then(data => {
if (Array.isArray(data)) {
setMatches(data);
// If no match selected yet, pick the next future match by date
try {
if (!ctx.match_id) {
const now = Date.now();
let next = null;
data.forEach(m => {
if (!m.matchDate) return;
const t = Date.parse(m.matchDate);
if (isNaN(t)) return;
if (t > now) {
if (!next || t < next.time) next = { time: t, id: m.matchId };
}
});
if (next && next.id) {
updateContext('match_id', next.id);
}
}
} catch (e) {
// ignore
}
}
}).catch(() => {}).finally(() => setLoadingMatches(false));
}, [ctx.team_id]);
function updateContext(key, val) {
const newMeta = Object.assign({}, meta, { swi_foot_context: Object.assign({}, ctx, { [key]: val }) });
props.editPost({ meta: newMeta });
}
const teamOptions = [{ value: '', label: '— use post default —' }].concat(
teams.map(t => ({ value: t.teamId, label: t.teamName + (t.teamLeagueName ? ' (' + t.teamLeagueName + ')' : '') }))
);
const matchOptions = [{ value: '', label: '— use team/season default —' }].concat(
matches.map(m => {
var datePart = m.matchDate ? (m.matchDate.substr(0,10) + ' — ') : '';
var opposing = m.matchId;
var role = '';
try {
var selectedTeam = (ctx.team_id || '').toString();
var teamAId = (m.teamAId || m.homeTeamId || '').toString();
var teamBId = (m.teamBId || m.awayTeamId || '').toString();
var teamAName = m.teamNameA || m.homeTeamName || '';
var teamBName = m.teamNameB || m.awayTeamName || '';
if (selectedTeam && selectedTeam === teamAId) {
opposing = teamBName || teamAName || m.matchId;
role = 'home';
} else if (selectedTeam && selectedTeam === teamBId) {
opposing = teamAName || teamBName || m.matchId;
role = 'away';
} else {
opposing = teamBName || teamAName || m.teamName || m.matchId;
}
} catch (e) {
opposing = m.teamNameB || m.teamNameA || m.awayTeamName || m.homeTeamName || m.teamName || m.matchId;
}
var label = datePart + opposing + (role ? (' (' + role + ')') : '');
return { value: m.matchId, label: label };
})
);
return wp.element.createElement(
PluginDocumentSettingPanel,
{ title: 'Swiss Football Context', className: 'swi-foot-post-context-panel' },
wp.element.createElement(PanelRow, null,
wp.element.createElement('div', { style: { width: '100%' } },
wp.element.createElement('label', null, 'Season'),
wp.element.createElement('input', {
type: 'number',
value: ctx.season || window.swi_foot_post_context.default_season,
onChange: function(e){ updateContext('season', e.target.value); },
style: { width: '100%' }
})
)
),
wp.element.createElement(PanelRow, null,
wp.element.createElement('div', { style: { width: '100%' } },
wp.element.createElement('label', null, 'Team'),
loadingTeams ? wp.element.createElement(Spinner, null) : wp.element.createElement(SelectControl, {
value: ctx.team_id || '',
options: teamOptions,
onChange: function(v){ updateContext('team_id', v); },
__next40pxDefaultSize: true,
__nextHasNoMarginBottom: true
})
)
),
wp.element.createElement(PanelRow, null,
wp.element.createElement('div', { style: { width: '100%' } },
wp.element.createElement('label', null, 'Match'),
loadingMatches ? wp.element.createElement(Spinner, null) : wp.element.createElement(SelectControl, {
value: ctx.match_id || '',
options: matchOptions,
onChange: function(v){ updateContext('match_id', v); },
__next40pxDefaultSize: true,
__nextHasNoMarginBottom: true
})
)
),
wp.element.createElement(PanelRow, null,
wp.element.createElement('div', { style: { width: '100%', color: '#666', fontSize: '12px', marginTop: '6px' } },
ctx.match_id ? ('Match ID: ' + ctx.match_id) : ''
)
),
wp.element.createElement(PanelRow, null,
wp.element.createElement(Button, { isSecondary: true, onClick: function(){ props.editPost({ meta: Object.assign({}, meta, { swi_foot_context: {} }) }); } }, 'Clear Context')
)
);
}
const Connected = withSelect( (select) => ({ meta: select('core/editor').getEditedPostAttribute('meta') || {} }) )( withDispatch( (dispatch) => ({ editPost: dispatch('core/editor').editPost }) )( PostContextPanel ) );
registerPlugin('swi-foot-post-context', { render: Connected });
})(window.wp);

17
blocks/context/block.json Normal file
View File

@ -0,0 +1,17 @@
{
"apiVersion": 3,
"name": "swi-foot/context",
"title": "Swiss Football Context (container)",
"category": "widgets",
"icon": "admin-site",
"description": "Provides a team/season/match context to child blocks. Children will inherit these settings unless they override them.",
"providesContext": {
"swi-foot/context": "swi_foot_context"
},
"attributes": {
"swi_foot_context": {
"type": "object",
"default": {}
}
}
}

View File

@ -0,0 +1,16 @@
{
"apiVersion": 3,
"name": "swi-foot/match-events",
"title": "Swiss Football Match Events",
"category": "widgets",
"icon": "list-view",
"description": "Live match events with optional auto-refresh.",
"attributes": {
"selectedTeam": { "type": "string", "default": "" },
"selectedMatch": { "type": "string", "default": "" },
"refreshInterval": { "type": "number", "default": 30 },
"eventOrder": { "type": "string", "default": "dynamic" }
}
,
"usesContext": ["swi-foot/context"]
}

View File

@ -0,0 +1,16 @@
{
"apiVersion": 3,
"name": "swi-foot/match-roster",
"title": "Swiss Football Match Roster",
"category": "widgets",
"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 }
}
,
"usesContext": ["swi-foot/context"]
}

View File

@ -0,0 +1,20 @@
{
"apiVersion": 3,
"name": "swi-foot/schedule",
"title": "Swiss Football Schedule",
"category": "widgets",
"icon": "schedule",
"description": "Display upcoming matches for a team.",
"attributes": {
"teamId": {
"type": "string",
"default": ""
},
"limit": {
"type": "number",
"default": 5
}
}
,
"usesContext": ["swi-foot/context"]
}

View File

@ -0,0 +1,36 @@
{
"apiVersion": 3,
"name": "swi-foot/shortcode-inserter",
"title": "SWI Football Shortcode Inserter",
"category": "widgets",
"icon": "editor-code",
"description": "Insert shortcode-based match or team displays.",
"attributes": {
"shortcodeType": {
"type": "string",
"default": "match"
},
"matchId": {
"type": "string",
"default": ""
},
"teamId": {
"type": "string",
"default": ""
},
"showNext": {
"type": "boolean",
"default": false
},
"format": {
"type": "string",
"default": ""
},
"separator": {
"type": "string",
"default": ":"
}
},
"usesContext": ["swi-foot/context"]
,"supports": { "html": false }
}

View File

@ -0,0 +1,16 @@
{
"apiVersion": 3,
"name": "swi-foot/standings",
"title": "Swiss Football Standings",
"category": "widgets",
"icon": "analytics",
"description": "Display current standings for a team.",
"attributes": {
"teamId": {
"type": "string",
"default": ""
}
}
,
"usesContext": ["swi-foot/context"]
}

View File

@ -0,0 +1,18 @@
{
"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

@ -0,0 +1,466 @@
<?php
class Swi_Foot_Admin
{
public function __construct()
{
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'settings_init'));
add_action('admin_enqueue_scripts', array($this, 'admin_scripts'));
add_action('add_meta_boxes', array($this, 'add_match_meta_boxes'));
// Finished-match deletion moved to REST endpoints (see includes/class-swi-foot-rest.php)
}
public function add_admin_menu()
{
add_options_page(
__('Swiss Football Matchdata Settings', 'swi_foot_matchdata'),
__('Swiss Football', 'swi_foot_matchdata'),
'manage_options',
'swiss-football-matchdata',
array($this, 'options_page')
);
}
public function settings_init()
{
register_setting('swi_foot_settings', 'swi_foot_api_base_url');
register_setting('swi_foot_settings', 'swi_foot_api_username');
register_setting('swi_foot_settings', 'swi_foot_api_password');
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');
add_settings_section(
'swi_foot_api_section',
__('API Configuration', 'swi_foot_matchdata'),
array($this, 'api_section_callback'),
'swi_foot_settings'
);
add_settings_field('swi_foot_api_base_url', __('API Base URL', 'swi_foot_matchdata'), array($this, 'base_url_render'), 'swi_foot_settings', 'swi_foot_api_section');
add_settings_field('swi_foot_api_username', __('API Username (Application Key)', 'swi_foot_matchdata'), array($this, 'username_render'), 'swi_foot_settings', 'swi_foot_api_section');
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_section(
'swi_foot_cache_section',
__('Cache Settings', 'swi_foot_matchdata'),
array($this, 'cache_section_callback'),
'swi_foot_settings'
);
add_settings_field('swi_foot_match_cache_duration', __('Match Data Cache Duration (seconds)', 'swi_foot_matchdata'), array($this, 'cache_duration_render'), 'swi_foot_settings', 'swi_foot_cache_section');
}
public function api_section_callback()
{
echo '<p>' . __('Configure your Swiss Football API credentials here.', 'swi_foot_matchdata') . '</p>';
}
public function cache_section_callback()
{
echo '<p>' . __('Configure caching settings for match data.', 'swi_foot_matchdata') . '</p>';
}
public function base_url_render()
{
$base_url = get_option('swi_foot_api_base_url', 'https://stg-club-api-services.football.ch');
echo '<input type="url" name="swi_foot_api_base_url" value="' . esc_attr($base_url) . '" class="regular-text" />';
echo '<p class="description">' . __('The base URL for the Swiss Football API', 'swi_foot_matchdata') . '</p>';
}
public function username_render()
{
$username = get_option('swi_foot_api_username');
echo '<input type="text" name="swi_foot_api_username" value="' . esc_attr($username) . '" class="regular-text" />';
echo '<p class="description">' . __('Your API application key', 'swi_foot_matchdata') . '</p>';
}
public function password_render()
{
$password = get_option('swi_foot_api_password');
echo '<input type="password" name="swi_foot_api_password" value="' . esc_attr($password) . '" class="regular-text" />';
echo '<p class="description">' . __('Your API application password', 'swi_foot_matchdata') . '</p>';
}
public function verein_id_render()
{
$verein_id = get_option('swi_foot_verein_id');
echo '<input type="number" name="swi_foot_verein_id" value="' . esc_attr($verein_id) . '" class="regular-text" />';
echo '<p class="description">' . __('Enter your club\'s Verein ID (Club ID)', 'swi_foot_matchdata') . '</p>';
}
public function season_id_render()
{
$season_id = get_option('swi_foot_season_id', date('Y'));
echo '<input type="number" name="swi_foot_season_id" value="' . esc_attr($season_id) . '" class="regular-text" min="2020" max="2030" />';
echo '<p class="description">' . __('Current season ID (usually the year)', 'swi_foot_matchdata') . '</p>';
}
public function cache_duration_render()
{
$duration = get_option('swi_foot_match_cache_duration', 30);
echo '<input type="number" name="swi_foot_match_cache_duration" value="' . esc_attr($duration) . '" min="10" max="300" />';
echo '<p class="description">' . __('How long to cache match data in seconds (10-300)', 'swi_foot_matchdata') . '</p>';
}
public function admin_scripts($hook)
{
if ($hook === 'settings_page_swiss-football-matchdata') {
wp_enqueue_script('swi-foot-admin', SWI_FOOT_PLUGIN_URL . 'assets/admin.js', array('jquery'), SWI_FOOT_PLUGIN_VERSION, true);
wp_localize_script('swi-foot-admin', 'swi_foot_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('swi_foot_nonce'),
'rest_url' => esc_url_raw(rest_url('swi-foot/v1')),
'rest_nonce' => wp_create_nonce('wp_rest')
));
wp_enqueue_style('swi-foot-admin', SWI_FOOT_PLUGIN_URL . 'assets/admin.css', array(), SWI_FOOT_PLUGIN_VERSION);
}
// Add shortcode help to post/page editors
global $pagenow;
if (in_array($pagenow, array('post.php', 'post-new.php', 'edit.php'))) {
// Enqueue the registered editor bundle so WordPress picks the built asset when available.
// The script handle `swi-foot-editor-blocks` is registered in `register_blocks()`.
wp_enqueue_script('swi-foot-editor-blocks');
// Add admin footer debug output to help diagnose missing script tags.
add_action('admin_footer', array($this, 'print_editor_script_debug'));
// Post context editor panel: allow editor to set per-post season/team/match
wp_enqueue_script('swi-foot-post-context', SWI_FOOT_PLUGIN_URL . 'assets/post-context.js', array('wp-plugins','wp-edit-post','wp-element','wp-data','wp-components'), SWI_FOOT_PLUGIN_VERSION, true);
wp_localize_script('swi-foot-post-context', 'swi_foot_post_context', array(
'rest_url' => esc_url_raw(rest_url('swi-foot/v1')),
'rest_nonce' => wp_create_nonce('wp_rest'),
'default_season' => get_option('swi_foot_season_id', date('Y'))
));
wp_enqueue_style('swi-foot-admin', SWI_FOOT_PLUGIN_URL . 'assets/admin.css', array(), SWI_FOOT_PLUGIN_VERSION);
}
}
public function print_editor_script_debug()
{
// Only show on post editor pages
$pagenow = isset($GLOBALS['pagenow']) ? $GLOBALS['pagenow'] : '';
if (!in_array($pagenow, array('post.php', 'post-new.php', 'edit.php'))) return;
// Check registration/enqueue status and attempt to find the resolved src
$registered = false;
$enqueued = false;
$src = '';
global $wp_scripts;
if (isset($wp_scripts) && is_object($wp_scripts)) {
$registered = wp_script_is('swi-foot-editor-blocks', 'registered');
$enqueued = wp_script_is('swi-foot-editor-blocks', 'enqueued');
$handle = $wp_scripts->query('swi-foot-editor-blocks', 'registered');
if ($handle && isset($wp_scripts->registered['swi-foot-editor-blocks']->src)) {
$src = $wp_scripts->registered['swi-foot-editor-blocks']->src;
}
}
$msg = array(
'registered' => $registered ? 'yes' : 'no',
'enqueued' => $enqueued ? 'yes' : 'no',
'src' => $src
);
echo "<script>console.debug('swi-foot: editor script debug', ". json_encode($msg) .");</script>";
}
public function add_match_meta_boxes()
{
add_meta_box(
'swi-foot-shortcodes',
__('Swiss Football Shortcodes', 'swi_foot_matchdata'),
array($this, 'shortcodes_meta_box'),
array('post', 'page'),
'side',
'default'
);
}
public function shortcodes_meta_box($post)
{
?>
<div class="swi-foot-shortcodes-help">
<h4><?php _e('Available Shortcodes:', 'swi_foot_matchdata'); ?></h4>
<div class="shortcode-group">
<strong><?php _e('Full Match Display:', 'swi_foot_matchdata'); ?></strong>
<code data-shortcode="[swi_foot_match match_id=&quot;12345&quot;]">[swi_foot_match match_id="12345"]</code>
<code data-shortcode="[swi_foot_match team_id=&quot;67&quot; show_next=&quot;true&quot;]">[swi_foot_match team_id="67" show_next="true"]</code>
</div>
<div class="shortcode-group">
<strong><?php _e('Individual Match Elements:', 'swi_foot_matchdata'); ?></strong>
<code data-shortcode="[swi_foot_match_home_team match_id=&quot;12345&quot;]">[swi_foot_match_home_team match_id="12345"]</code>
<code data-shortcode="[swi_foot_match_away_team match_id=&quot;12345&quot;]">[swi_foot_match_away_team match_id="12345"]</code>
<code data-shortcode="[swi_foot_match_date match_id=&quot;12345&quot; format=&quot;d.m.Y&quot;]">[swi_foot_match_date match_id="12345" format="d.m.Y"]</code>
<code data-shortcode="[swi_foot_match_time match_id=&quot;12345&quot; format=&quot;H:i&quot;]">[swi_foot_match_time match_id="12345" format="H:i"]</code>
<code data-shortcode="[swi_foot_match_venue match_id=&quot;12345&quot;]">[swi_foot_match_venue match_id="12345"]</code>
<code data-shortcode="[swi_foot_match_score match_id=&quot;12345&quot; separator=&quot;:&quot;]">[swi_foot_match_score match_id="12345" separator=":"]</code>
<code data-shortcode="[swi_foot_match_status match_id=&quot;12345&quot;]">[swi_foot_match_status match_id="12345"]</code>
<code data-shortcode="[swi_foot_match_league match_id=&quot;12345&quot;]">[swi_foot_match_league match_id="12345"]</code>
<code data-shortcode="[swi_foot_match_round match_id=&quot;12345&quot;]">[swi_foot_match_round match_id="12345"]</code>
</div>
<div class="shortcode-group">
<strong><?php _e('Parameters:', 'swi_foot_matchdata'); ?></strong>
<ul>
<li><code>match_id</code> - Specific match ID</li>
<li><code>team_id</code> - Team ID (for next match)</li>
<li><code>show_next</code> - Show next match (true/false)</li>
<li><code>format</code> - Date/time format</li>
<li><code>separator</code> - Score separator</li>
</ul>
</div>
<p><em><?php _e('Click any shortcode above to copy it to clipboard', 'swi_foot_matchdata'); ?></em></p>
</div>
<script>
jQuery(document).ready(function($) {
$('.swi-foot-shortcodes-help code[data-shortcode]').css('cursor', 'pointer').on('click', function() {
var shortcode = $(this).data('shortcode');
if (navigator.clipboard) {
navigator.clipboard.writeText(shortcode).then(function() {
alert('<?php _e('Shortcode copied to clipboard!', 'swi_foot_matchdata'); ?>');
});
} else {
// Use native clipboard API
var textArea = document.createElement('textarea');
textArea.value = shortcode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
alert('<?php _e('Shortcode copied to clipboard!', 'swi_foot_matchdata'); ?>');
}
});
});
</script>
<?php
}
public function options_page()
{
?>
<div class="wrap">
<h1><?php _e('Swiss Football Matchdata Settings', 'swi_foot_matchdata'); ?></h1>
<form action="options.php" method="post">
<?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'); ?>
</button>
<span id="connection-status"></span>
</p>
</div>
<div class="swi-foot-admin-section">
<h2><?php _e('Team Management', 'swi_foot_matchdata'); ?></h2>
<p>
<button type="button" id="refresh-teams" class="button">
<?php _e('Refresh Teams List', 'swi_foot_matchdata'); ?>
</button>
<span id="refresh-status"></span>
</p>
<div id="teams-list">
<?php $this->display_teams(); ?>
</div>
</div>
<div class="swi-foot-admin-section">
<h2><?php _e('Cache Management', 'swi_foot_matchdata'); ?></h2>
<p>
<button type="button" id="clear-cache" class="button">
<?php _e('Clear Match Data Cache', 'swi_foot_matchdata'); ?>
</button>
<span id="cache-status"></span>
</p>
<?php $this->display_cache_info(); ?>
</div>
<div class="swi-foot-admin-section">
<h2><?php _e('Finished Matches Data', 'swi_foot_matchdata'); ?></h2>
<?php $this->display_finished_matches(); ?>
</div>
<div class="swi-foot-admin-section">
<h2><?php _e('Quick Shortcode Reference', 'swi_foot_matchdata'); ?></h2>
<div class="shortcode-quick-ref">
<div class="shortcode-examples">
<h4><?php _e('Common Examples:', 'swi_foot_matchdata'); ?></h4>
<p><strong><?php _e('Display full match info:', 'swi_foot_matchdata'); ?></strong><br>
<code>[swi_foot_match match_id="12345"]</code>
</p>
<p><strong><?php _e('Show next match for a team:', 'swi_foot_matchdata'); ?></strong><br>
<code>[swi_foot_match team_id="67" show_next="true"]</code>
</p>
<p><strong><?php _e('Individual elements in text:', 'swi_foot_matchdata'); ?></strong><br>
<?php _e('The match between', 'swi_foot_matchdata'); ?> <code>[swi_foot_match_home_team match_id="12345"]</code> <?php _e('and', 'swi_foot_matchdata'); ?> <code>[swi_foot_match_away_team match_id="12345"]</code> <?php _e('is on', 'swi_foot_matchdata'); ?> <code>[swi_foot_match_date match_id="12345"]</code></p>
</div>
</div>
</div>
</div>
<?php
}
private function display_teams()
{
$api = new Swi_Foot_API();
$teams = $api->get_teams();
if (is_wp_error($teams)) {
echo '<p class="notice notice-error">' .
sprintf(__('Error loading teams: %s', 'swi_foot_matchdata'), $teams->get_error_message()) .
'</p>';
return;
}
if (empty($teams)) {
echo '<p class="notice notice-warning">' .
__('No teams found. Please check your API configuration.', 'swi_foot_matchdata') .
'</p>';
return;
}
echo '<h3>' . __('Available Teams:', 'swi_foot_matchdata') . '</h3>';
echo '<div class="swi-foot-teams-grid">';
foreach ($teams as $team) {
echo '<div class="team-card">';
echo '<strong>' . esc_html($team['teamName'] ?? 'Unknown Team') . '</strong><br>';
echo '<small>ID: ' . esc_html($team['teamId'] ?? 'N/A') . '</small><br>';
if (!empty($team['teamLeagueName'])) {
echo '<small>' . esc_html($team['teamLeagueName']) . '</small>';
}
echo '</div>';
}
echo '</div>';
}
private function display_cache_info()
{
$keys = get_transient('swi_foot_match_keys');
$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>';
if ($cache_count > 0) {
$timestamps = array();
foreach ($keys as $k) {
$payload = get_transient('swi_foot_match_' . $k);
if (is_array($payload) && isset($payload['cached_at'])) {
$timestamps[] = (int) $payload['cached_at'];
}
}
if (!empty($timestamps)) {
$oldest_timestamp = min($timestamps);
$newest_timestamp = max($timestamps);
echo '<p><strong>' . __('Cache Range:', 'swi_foot_matchdata') . '</strong><br>';
echo __('Oldest:', 'swi_foot_matchdata') . ' ' . date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $oldest_timestamp) . '<br>';
echo __('Newest:', 'swi_foot_matchdata') . ' ' . date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $newest_timestamp) . '</p>';
}
}
}
private function display_finished_matches()
{
$finished = get_option('swi_foot_finished_matches', array());
if (empty($finished)) {
echo '<p>' . __('No finished match data stored.', 'swi_foot_matchdata') . '</p>';
return;
}
echo '<table class="widefat fixed striped">';
echo '<thead>
<tr>
<th>' . __('Match ID', 'swi_foot_matchdata') . '</th>
<th>' . __('Saved At', 'swi_foot_matchdata') . '</th>
<th>' . __('Players', 'swi_foot_matchdata') . '</th>
<th>' . __('Bench', 'swi_foot_matchdata') . '</th>
<th>' . __('Events', 'swi_foot_matchdata') . '</th>
<th></th>
</tr>
</thead><tbody>';
foreach ($finished as $mid => $item) {
$players = count($item['roster']['players'] ?? array());
$bench = count($item['roster']['bench'] ?? array());
$events = count($item['events'] ?? array());
$saved = !empty($item['saved_at']) ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $item['saved_at']) : '-';
echo '<tr>
<td>' . esc_html($mid) . '</td>
<td>' . esc_html($saved) . '</td>
<td>' . esc_html($players) . '</td>
<td>' . esc_html($bench) . '</td>
<td>' . esc_html($events) . '</td>
<td>
<button class="button swi-foot-clear-finished" data-id="' . esc_attr($mid) . '">' . __('Delete', 'swi_foot_matchdata') . '</button>
</td>
</tr>';
}
echo '</tbody></table>';
echo '<p><button id="swi-foot-clear-all-finished" class="button button-secondary">' . __('Clear All Finished Matches', 'swi_foot_matchdata') . '</button></p>';
// Include inline JS for actions
?>
<script>
jQuery(function($) {
$('.swi-foot-clear-finished').on('click', function() {
if (!confirm('<?php _e('Delete this finished match data?', 'swi_foot_matchdata'); ?>')) return;
var btn = $(this);
var matchId = btn.data('id');
fetch(swi_foot_ajax.rest_url.replace(/\/$/, '') + '/admin/finished/' + encodeURIComponent(matchId), {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-WP-Nonce': swi_foot_ajax.rest_nonce }
}).then(function(resp) { return resp.json(); }).then(function(resp) {
if (resp && resp.success) {
btn.closest('tr').fadeOut();
} else {
alert(resp.error || 'Error');
}
}).catch(function() { alert('Network error'); });
});
$('#swi-foot-clear-all-finished').on('click', function() {
if (!confirm('<?php _e('Clear all finished match data?', 'swi_foot_matchdata'); ?>')) return;
fetch(swi_foot_ajax.rest_url.replace(/\/$/, '') + '/admin/finished', {
method: 'DELETE',
credentials: 'same-origin',
headers: { 'X-WP-Nonce': swi_foot_ajax.rest_nonce }
}).then(function(resp) { return resp.json(); }).then(function(resp) {
if (resp && resp.success) {
location.reload();
} else {
alert(resp.error || 'Error');
}
}).catch(function() { alert('Network error'); });
});
});
</script>
<?php
}
}
?>

View File

@ -0,0 +1,559 @@
<?php
class Swi_Foot_API
{
private $base_url;
private $username;
private $password;
private $verein_id;
private $season_id;
private $cache_duration;
public function __construct()
{
$this->base_url = get_option('swi_foot_api_base_url', 'https://stg-club-api-services.football.ch');
$this->username = get_option('swi_foot_api_username');
$this->password = get_option('swi_foot_api_password');
$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);
// AJAX actions were migrated to REST endpoints (see includes/class-swi-foot-rest.php)
// Hook to update match cache on page load
add_action('wp', array($this, 'maybe_update_match_cache'));
}
/**
* Build a full URL for the given endpoint, avoiding duplicate /api segments
*/
private function build_url($endpoint)
{
$base = rtrim($this->base_url, '/');
// If base already contains '/api' and endpoint starts with '/api', strip the endpoint prefix
if (strpos($base, '/api') !== false && strpos($endpoint, '/api') === 0) {
$endpoint = preg_replace('#^/api#', '', $endpoint);
}
return $base . '/' . ltrim($endpoint, '/');
}
public function maybe_update_match_cache()
{
// Only run on single posts/pages
if (!is_single() && !is_page()) {
return;
}
global $post;
if (!$post) {
return;
}
// Check if content has match shortcodes
if (
!has_shortcode($post->post_content, 'swi_foot_match') &&
!has_shortcode($post->post_content, 'swi_foot_match_home_team') &&
!has_shortcode($post->post_content, 'swi_foot_match_away_team') &&
!has_shortcode($post->post_content, 'swi_foot_match_date') &&
!has_shortcode($post->post_content, 'swi_foot_match_time') &&
!has_shortcode($post->post_content, 'swi_foot_match_venue') &&
!has_shortcode($post->post_content, 'swi_foot_match_score') &&
!has_shortcode($post->post_content, 'swi_foot_match_status') &&
!has_shortcode($post->post_content, 'swi_foot_match_league') &&
!has_shortcode($post->post_content, 'swi_foot_match_round')
) {
return;
}
// Extract match IDs from shortcodes
$match_ids = array();
$patterns = array(
'/\[swi_foot_match[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_home_team[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_away_team[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_date[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_time[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_venue[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_score[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_status[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_league[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/',
'/\[swi_foot_match_round[^\]]*match_id=["\']([^"\']+)["\'][^\]]*\]/'
);
foreach ($patterns as $pattern) {
preg_match_all($pattern, $post->post_content, $matches);
if (!empty($matches[1])) {
$match_ids = array_merge($match_ids, $matches[1]);
}
}
// Update cache for found match IDs
$match_ids = array_unique($match_ids);
foreach ($match_ids as $match_id) {
$this->get_match_details($match_id, false); // Don't force refresh unless needed
}
}
private function get_access_token()
{
$token = get_transient('swi_foot_access_token');
// Transient handles expiration; return if present
if ($token) {
return $token;
}
// Fetch a new token
return $this->refresh_access_token();
}
private function refresh_access_token()
{
if (empty($this->username) || empty($this->password)) {
return false;
}
$response = wp_remote_post($this->build_url('/api/token'), array(
'body' => json_encode(array(
'applicationKey' => $this->username,
'applicationPass' => $this->password
)),
'headers' => array(
'Content-Type' => 'application/json'
),
'timeout' => 30
));
if (is_wp_error($response)) {
error_log('Swiss Football API: Token request failed - ' . $response->get_error_message());
return false;
}
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code !== 200) {
$body_debug = wp_remote_retrieve_body($response);
error_log('Swiss Football API: Token request returned ' . $response_code . ' - body: ' . substr($body_debug, 0, 1000));
return false;
}
$body = wp_remote_retrieve_body($response);
// Try JSON decode first, fallback to trimmed string
$maybe_json = json_decode($body, true);
if (is_string($maybe_json) && $maybe_json !== '') {
$token = $maybe_json;
} elseif (is_string($body) && $body !== '') {
$token = trim($body, '"');
} else {
$token = false;
}
if ($token) {
// Store token in transient (30 minutes). WP constant MINUTE_IN_SECONDS available.
set_transient('swi_foot_access_token', $token, 30 * MINUTE_IN_SECONDS);
return $token;
}
return false;
}
private function api_request($endpoint, $params = array(), $retry_on_401 = true)
{
$token = $this->get_access_token();
if (!$token) {
return new WP_Error('auth_failed', 'Failed to authenticate with Swiss Football API');
}
$url = $this->build_url($endpoint);
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
$response = wp_remote_get($url, array(
'headers' => array(
'X-User-Token' => $token,
'Content-Type' => 'application/json'
),
'timeout' => 30
));
if (is_wp_error($response)) {
return $response;
}
$response_code = wp_remote_retrieve_response_code($response);
// Handle 401 Unauthorized - token may have expired
if ($response_code === 401 && $retry_on_401) {
error_log('Swiss Football API: Received 401 Unauthorized, attempting to refresh token');
// Clear the expired token
delete_transient('swi_foot_access_token');
// Refresh the token
$new_token = $this->refresh_access_token();
if ($new_token) {
// Retry the request with the new token (prevent infinite recursion with retry_on_401 = false)
return $this->api_request($endpoint, $params, false);
}
return new WP_Error('auth_failed', 'Authentication failed: Token refresh unsuccessful');
}
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);
}
$body = wp_remote_retrieve_body($response);
return json_decode($body, true);
}
public function get_teams()
{
// Prefer transient-based caching for teams (24 hours)
$teams = get_transient('swi_foot_teams');
if ($teams !== false) {
return $teams;
}
if (empty($this->verein_id)) {
return new WP_Error('no_verein_id', 'Verein ID not configured');
}
$teams = $this->api_request('/api/team/list', array(
'ClubId' => $this->verein_id,
'SeasonId' => $this->season_id
));
if (!is_wp_error($teams) && is_array($teams)) {
set_transient('swi_foot_teams', $teams, 24 * HOUR_IN_SECONDS);
}
return $teams;
}
public function get_standings($team_id)
{
if (empty($this->verein_id)) {
return new WP_Error('no_verein_id', 'Verein ID not configured');
}
return $this->api_request('/api/club/ranking', array(
'ClubId' => $this->verein_id,
'SeasonId' => $this->season_id,
'TeamId' => $team_id
));
}
public function get_schedule($team_id)
{
if (empty($this->verein_id)) {
return new WP_Error('no_verein_id', 'Verein ID not configured');
}
return $this->api_request('/api/club/schedule', array(
'ClubId' => $this->verein_id,
'SeasonId' => $this->season_id,
'TeamId' => $team_id
));
}
public function get_match_details($match_id, $force_refresh = false)
{
// If we've permanently saved finished match data, return it (do not call API)
$finished = $this->get_finished_match_data($match_id);
if (!$force_refresh && $finished && isset($finished['match'])) {
return $finished['match'];
}
$transient_key = 'swi_foot_match_' . $match_id;
if (!$force_refresh) {
$cached = get_transient($transient_key);
if ($cached !== false) {
return isset($cached['data']) ? $cached['data'] : $cached;
}
}
// Fetch fresh data from API
$match_data = $this->api_request('/api/match/' . $match_id);
if (!is_wp_error($match_data)) {
// Cache the data as transient for configured duration
$cache_payload = array(
'data' => $match_data,
'cached_at' => time()
);
set_transient($transient_key, $cache_payload, (int) $this->cache_duration);
// Maintain index of cached match ids for management / clearing
$keys = get_transient('swi_foot_match_keys');
if (!is_array($keys)) {
$keys = array();
}
if (!in_array($match_id, $keys, true)) {
$keys[] = $match_id;
// No expiration for keys index so we can clear caches reliably
set_transient('swi_foot_match_keys', $keys, 0);
}
// If match finished, persist it permanently (store match details inside finished data)
if (!empty($match_data['hasMatchEnded'])) {
// Ensure finished entry exists and include match details
$saved = $this->get_finished_match_data($match_id) ?: array();
$saved['match'] = $match_data;
// Keep any existing roster/events if present
if (!isset($saved['roster'])) $saved['roster'] = array();
if (!isset($saved['events'])) $saved['events'] = array();
$all = get_option('swi_foot_finished_matches', array());
$all[$match_id] = array_merge($all[$match_id] ?? array(), $saved, array('saved_at' => time()));
update_option('swi_foot_finished_matches', $all);
// Also delete transient cache to force reads from permanent store
delete_transient($transient_key);
}
}
return $match_data;
}
public function get_cached_match_data($match_id)
{
// Prefer finished permanent store
$finished = $this->get_finished_match_data($match_id);
if ($finished && isset($finished['match'])) {
return $finished['match'];
}
$transient_key = 'swi_foot_match_' . $match_id;
$cached = get_transient($transient_key);
if ($cached === false) return null;
return isset($cached['data']) ? $cached['data'] : $cached;
}
public function get_match_players($match_id)
{
return $this->api_request('/api/match/' . $match_id . '/players');
}
public function get_match_bench($match_id)
{
return $this->api_request('/api/match/' . $match_id . '/bench');
}
public function get_match_events($match_id)
{
return $this->api_request('/api/match/' . $match_id . '/events');
}
public function get_team_picture($team_id)
{
// Special handling for team picture endpoint which returns 200 with data or 204 No Content
$token = $this->get_access_token();
if (!$token) {
error_log('Swiss Football API: get_team_picture - Failed to get access token');
return null;
}
$url = $this->build_url('/api/team/picture/' . $team_id);
error_log('Swiss Football API: get_team_picture - Requesting URL: ' . $url);
$response = wp_remote_get($url, array(
'headers' => array(
'X-User-Token' => $token,
'Content-Type' => 'application/json'
),
'timeout' => 30
));
if (is_wp_error($response)) {
error_log('Swiss Football API: get_team_picture - wp_remote_get error: ' . $response->get_error_message());
return null;
}
$response_code = wp_remote_retrieve_response_code($response);
error_log('Swiss Football API: get_team_picture - Response code: ' . $response_code);
// Handle 401 Unauthorized - token may have expired
if ($response_code === 401) {
error_log('Swiss Football API: get_team_picture - Received 401, refreshing token and retrying');
delete_transient('swi_foot_access_token');
$new_token = $this->refresh_access_token();
if ($new_token) {
// Recursively retry with new token
return $this->get_team_picture($team_id);
}
error_log('Swiss Football API: get_team_picture - Token refresh failed');
return null;
}
// Handle 204 No Content - team has no picture
if ($response_code === 204) {
error_log('Swiss Football API: get_team_picture - Team ' . $team_id . ' has no picture (204)');
return null;
}
// Handle other error responses
if ($response_code !== 200) {
$body = wp_remote_retrieve_body($response);
error_log('Swiss Football API: get_team_picture - Request failed with code ' . $response_code . ' - body: ' . substr($body, 0, 500));
return null;
}
// Success - return the base64 image data
$body = wp_remote_retrieve_body($response);
error_log('Swiss Football API: get_team_picture - Successfully retrieved image, size: ' . strlen($body) . ' bytes');
return $body;
}
public function get_commons_ids()
{
return $this->api_request('/api/commons/ids');
}
/**
* Debug helper return token and resolved endpoint info for admin debugging.
* Note: only for admin use; not called on public endpoints.
*/
public function debug_get_token_info()
{
$token = $this->get_access_token();
$team_list_url = $this->build_url('/api/team/list') . '?' . http_build_query(array(
'ClubId' => $this->verein_id,
'SeasonId' => $this->season_id
));
return array(
'token_present' => ($token !== false && !empty($token)),
'token_preview' => is_string($token) ? substr($token, 0, 32) : null,
'base_url' => $this->base_url,
'team_list_url' => $team_list_url
);
}
/**
* Public helper to test whether we can obtain a valid access token
* Returns true on success, false otherwise.
*/
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;
}
// Verify we got a response (array means success)
return is_array($result);
}
public function get_current_match($team_id)
{
$schedule = $this->get_schedule($team_id);
if (is_wp_error($schedule) || empty($schedule)) {
return null;
}
$current_time = time();
$current_match = null;
// Look for upcoming match within 5 days or recent match within 2 days
foreach ($schedule as $match) {
$match_time = strtotime($match['matchDate']);
// Upcoming within 5 days
if ($match_time > $current_time && ($match_time - $current_time) <= (5 * 24 * 60 * 60)) {
$current_match = $match;
break;
}
// Recent within 2 days
if ($match_time <= $current_time && ($current_time - $match_time) <= (2 * 24 * 60 * 60)) {
$current_match = $match;
}
}
return $current_match;
}
/**
* Save finished match data permanently (roster + events in one structure)
*/
public function save_finished_match_data($match_id, $roster_data, $events_data)
{
if (empty($match_id)) {
return false;
}
$saved_matches = get_option('swi_foot_finished_matches', array());
$existing = isset($saved_matches[$match_id]) ? $saved_matches[$match_id] : array();
$existing['roster'] = $roster_data;
$existing['events'] = $events_data;
$existing['saved_at'] = time();
// If match details are cached as transient, try to move them into permanent record
$transient_key = 'swi_foot_match_' . $match_id;
$match_details = get_transient($transient_key);
if ($match_details !== false) {
$existing['match'] = $match_details;
delete_transient($transient_key);
}
$saved_matches[$match_id] = $existing;
update_option('swi_foot_finished_matches', $saved_matches);
return true;
}
/**
* Retrieve saved finished match data if available
*/
public function get_finished_match_data($match_id)
{
$saved_matches = get_option('swi_foot_finished_matches', array());
return $saved_matches[$match_id] ?? null;
}
}
/**
* Resolve effective context for a post: season, team_id, match_id.
* Priority: post meta 'swi_foot_context' -> plugin option defaults
*/
function swi_foot_resolve_context($post_id = null)
{
if (empty($post_id)) {
$post_id = get_the_ID();
}
$stored = get_post_meta($post_id, 'swi_foot_context', true);
$season = null;
$team_id = null;
$match_id = null;
if (is_array($stored)) {
$season = !empty($stored['season']) ? $stored['season'] : null;
$team_id = !empty($stored['team_id']) ? $stored['team_id'] : null;
$match_id = !empty($stored['match_id']) ? $stored['match_id'] : null;
}
if (empty($season)) {
$season = get_option('swi_foot_season_id', date('Y'));
}
return array(
'season' => $season,
'team_id' => $team_id,
'match_id' => $match_id
);
}

View File

@ -0,0 +1,479 @@
<?php
class Swi_Foot_Blocks
{
public function __construct()
{
// Register blocks immediately when this object is constructed.
// The plugin constructs this class during the plugin init sequence,
// so calling register_blocks() now ensures scripts/styles are registered
// before `enqueue_block_editor_assets` runs later in the request.
$this->register_blocks();
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
// Editor assets enqueued below; data endpoints are provided via REST (see includes/class-swi-foot-rest.php)
add_action('enqueue_block_editor_assets', array($this, 'enqueue_editor_assets'));
}
public function register_blocks()
{
// Register blocks from metadata (block.json) and provide server-side render callbacks
$base = dirname(__DIR__) . '/blocks';
// Register editor script and styles from webpack build
$editor_js_url = SWI_FOOT_PLUGIN_URL . 'assets/build/editor-blocks.js';
wp_register_script(
'swi-foot-editor-blocks',
$editor_js_url,
array('wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-data', 'wp-rich-text'),
SWI_FOOT_PLUGIN_VERSION
);
$editor_css = SWI_FOOT_PLUGIN_URL . 'assets/blocks.css';
wp_register_style('swi-foot-editor-styles', $editor_css, array(), SWI_FOOT_PLUGIN_VERSION);
if ( file_exists( $base . '/standings/block.json' ) ) {
register_block_type_from_metadata( $base . '/standings', array(
'render_callback' => array( $this, 'render_standings_block' ),
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
if ( file_exists( $base . '/context/block.json' ) ) {
register_block_type_from_metadata( $base . '/context', array(
'render_callback' => function($attributes, $content, $block) {
// Provide a transparent wrapper but push provider context to a global stack
// so inner shortcodes and blocks can read it during rendering.
$provided = is_array($attributes['swi_foot_context'] ?? null) ? $attributes['swi_foot_context'] : array();
$post_defaults = function_exists('swi_foot_resolve_context') ? swi_foot_resolve_context(get_the_ID()) : array();
$ctx = array_merge($post_defaults, $provided);
global $swi_foot_context_stack;
if (!isset($swi_foot_context_stack) || !is_array($swi_foot_context_stack)) {
$swi_foot_context_stack = array();
}
$swi_foot_context_stack[] = $ctx;
// Render inner blocks (so shortcodes executed within this scope can read the global)
$rendered = do_blocks($content);
// Pop stack
array_pop($swi_foot_context_stack);
return $rendered;
},
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
if ( file_exists( $base . '/schedule/block.json' ) ) {
register_block_type_from_metadata( $base . '/schedule', array(
'render_callback' => array( $this, 'render_schedule_block' ),
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
/**
* Deprecated shortcode-inserter block
* Kept for backwards compatibility only
*/
if ( file_exists( $base . '/shortcode-inserter/block.json' ) ) {
register_block_type_from_metadata( $base . '/shortcode-inserter', array(
'render_callback' => array( $this, 'render_shortcode_inserter_block' ),
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles',
'deprecated' => true,
) );
}
// 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' ),
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
if ( file_exists( $base . '/match-events/block.json' ) ) {
register_block_type_from_metadata( $base . '/match-events', array(
'render_callback' => array( $this, 'render_match_events_block' ),
'editor_script' => 'swi-foot-editor-blocks',
'editor_style' => 'swi-foot-editor-styles'
) );
}
}
public function enqueue_scripts()
{
wp_enqueue_script('swi-foot-blocks', SWI_FOOT_PLUGIN_URL . 'assets/blocks.js', array('jquery'), SWI_FOOT_PLUGIN_VERSION, true);
wp_enqueue_style('swi-foot-blocks', SWI_FOOT_PLUGIN_URL . 'assets/blocks.css', array(), SWI_FOOT_PLUGIN_VERSION);
// Localize REST API data for frontend event refresh polling
wp_localize_script('swi-foot-blocks', 'swiFootRest', array(
'rest_url' => rest_url('swi-foot/v1/'),
'rest_nonce' => wp_create_nonce('wp_rest')
));
}
/*
public function enqueue_editor_assets()
{
wp_enqueue_script(
'swi-foot-editor-blocks',
SWI_FOOT_PLUGIN_URL . 'assets/editor-blocks.js',
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n'),
SWI_FOOT_PLUGIN_VERSION
);
wp_localize_script('swi-foot-editor-blocks', 'swiFootData', array(
'teams' => $this->get_teams_for_editor(),
'shortcodes' => array(
'match' => __('Full Match Display', 'swi_foot_matchdata'),
'match_home_team' => __('Home Team', 'swi_foot_matchdata'),
'match_away_team' => __('Away Team', 'swi_foot_matchdata'),
'match_date' => __('Match Date', 'swi_foot_matchdata'),
'match_time' => __('Match Time', 'swi_foot_matchdata'),
'match_venue' => __('Venue', 'swi_foot_matchdata'),
'match_score' => __('Score', 'swi_foot_matchdata'),
'match_status' => __('Status', 'swi_foot_matchdata'),
'match_league' => __('League', 'swi_foot_matchdata'),
'match_round' => __('Round', 'swi_foot_matchdata')
)
));
}
*/
public function enqueue_editor_assets()
{
// Enqueue the registered script handle (register_blocks() registers it on init)
if ( ! wp_script_is( 'swi-foot-editor-blocks', 'enqueued' ) ) {
wp_enqueue_script( 'swi-foot-editor-blocks' );
}
// Enqueue editor styles globally to ensure block wrapper styling applies
wp_enqueue_style( 'swi-foot-editor-styles' );
wp_localize_script('swi-foot-editor-blocks', 'swiFootEditorData', array(
'rest_url' => esc_url_raw(rest_url('swi-foot/v1')),
'rest_nonce' => wp_create_nonce('wp_rest'),
));
}
private function get_teams_for_editor()
{
$api = new Swi_Foot_API();
$teams = $api->get_teams();
if (is_wp_error($teams) || empty($teams)) {
return array();
}
$team_options = array();
foreach ($teams as $team) {
$team_name = $team['teamName'] ?? '';
$league = $team['teamLeagueName'] ?? '';
// If league name is available, append in parentheses
if (!empty($league)) {
$team_name .= ' (' . $league . ')';
}
$team_options[] = array(
'value' => $team['teamId'] ?? '',
'label' => $team_name
);
}
return $team_options;
}
public function render_standings_block($attributes, $content = '', $block = null)
{
// Resolve context: block context (from provider) overrides post meta defaults
$provided = is_object($block) && !empty($block->context['swi-foot/context']) ? $block->context['swi-foot/context'] : null;
$ctx = array();
if (is_array($provided)) $ctx = $provided;
// Merge with post-level defaults (post meta -> plugin options)
$post_defaults = function_exists('swi_foot_resolve_context') ? swi_foot_resolve_context(get_the_ID()) : array();
$ctx = array_merge($post_defaults, $ctx);
// Team must come from context only
$team_id = $ctx['team_id'] ?? null;
if (empty($team_id)) {
return '<div class="swi-foot-error">' . __('Standings: No team provided in container context.', 'swi_foot_matchdata') . '</div>';
}
$api = new Swi_Foot_API();
$standings = $api->get_standings($team_id);
if (is_wp_error($standings)) {
return '<div class="swi-foot-error">' .
sprintf(__('Error loading standings: %s', 'swi_foot_matchdata'), $standings->get_error_message()) .
'</div>';
}
if (empty($standings)) {
return '<div class="swi-foot-notice">' . __('No standings data available.', 'swi_foot_matchdata') . '</div>';
}
ob_start();
?>
<div class="swi-foot-standings">
<h3><?php _e('Current Standings', 'swi_foot_matchdata'); ?></h3>
<table class="swi-foot-table">
<thead>
<tr>
<th><?php _e('Pos', 'swi_foot_matchdata'); ?></th>
<th><?php _e('Team', 'swi_foot_matchdata'); ?></th>
<th><?php _e('P', 'swi_foot_matchdata'); ?></th>
<th><?php _e('W', 'swi_foot_matchdata'); ?></th>
<th><?php _e('D', 'swi_foot_matchdata'); ?></th>
<th><?php _e('L', 'swi_foot_matchdata'); ?></th>
<th><?php _e('GF', 'swi_foot_matchdata'); ?></th>
<th><?php _e('GA', 'swi_foot_matchdata'); ?></th>
<th><?php _e('Pts', 'swi_foot_matchdata'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($standings as $team): ?>
<tr<?php echo ($team['teamId'] == $team_id) ? ' class="highlight"' : ''; ?>>
<td><?php echo esc_html($team['position']); ?></td>
<td><?php echo esc_html($team['teamName']); ?></td>
<td><?php echo esc_html($team['matches']); ?></td>
<td><?php echo esc_html($team['wins']); ?></td>
<td><?php echo esc_html($team['draws']); ?></td>
<td><?php echo esc_html($team['losses']); ?></td>
<td><?php echo esc_html($team['goalsFor']); ?></td>
<td><?php echo esc_html($team['goalsAgainst']); ?></td>
<td><strong><?php echo esc_html($team['points']); ?></strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
return ob_get_clean();
}
public function render_schedule_block($attributes, $content = '', $block = null)
{
$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);
// Team must come from context only
$team_id = $ctx['team_id'] ?? null;
$limit = isset($attributes['limit']) ? $attributes['limit'] : 5;
if (empty($team_id)) {
return '<div class="swi-foot-error">' . __('Schedule: No team provided in container context.', 'swi_foot_matchdata') . '</div>';
}
$api = new Swi_Foot_API();
$events = $api->get_schedule($team_id);
if (is_wp_error($events)) {
return '<div class="swi-foot-error">' .
sprintf(__('Error loading schedule: %s', 'swi_foot_matchdata'), $events->get_error_message()) .
'</div>';
}
// Filter upcoming events and limit results
$upcoming_events = array();
if (is_array($events)) {
foreach ($events as $event) {
if (strtotime($event['matchDate']) >= time()) {
$upcoming_events[] = $event;
if (count($upcoming_events) >= $limit) {
break;
}
}
}
}
ob_start();
?>
<div class="swi-foot-schedule">
<h3><?php _e('Upcoming Matches', 'swi_foot_matchdata'); ?></h3>
<?php if (empty($upcoming_events)): ?>
<p><?php _e('No upcoming matches scheduled.', 'swi_foot_matchdata'); ?></p>
<?php else: ?>
<div class="swi-foot-events">
<?php foreach ($upcoming_events as $event): ?>
<div class="swi-foot-event">
<div class="event-date">
<?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['matchDate'])); ?>
</div>
<div class="event-teams">
<?php echo esc_html($event['teamNameA'] ?? ''); ?> vs <?php echo esc_html($event['teamNameB'] ?? ''); ?>
</div>
<?php if (!empty($event['stadiumPlaygroundName'])): ?>
<div class="event-location"><?php echo esc_html($event['stadiumPlaygroundName']); ?></div>
<?php endif; ?>
<?php if (!empty($event['leagueName'])): ?>
<div class="event-league"><?php echo esc_html($event['leagueName']); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
public function render_shortcode_inserter_block($attributes, $content = '', $block = null)
{
$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);
$shortcode_type = $attributes['shortcodeType'] ?? 'match';
$match_id = $attributes['matchId'] ?? ($ctx['match_id'] ?? '');
$team_id = $attributes['teamId'] ?? ($ctx['team_id'] ?? '');
$show_next = !empty($attributes['showNext']);
$format = $attributes['format'] ?? '';
$separator = $attributes['separator'] ?? ':';
// Build shortcode based on attributes
$shortcode_parts = array();
if (!empty($match_id)) {
$shortcode_parts[] = 'match_id="' . esc_attr($match_id) . '"';
}
if (!empty($team_id)) {
$shortcode_parts[] = 'team_id="' . esc_attr($team_id) . '"';
}
if ($show_next) {
$shortcode_parts[] = 'show_next="true"';
}
if (!empty($format) && in_array($shortcode_type, array('match_date', 'match_time'))) {
$shortcode_parts[] = 'format="' . esc_attr($format) . '"';
}
if (!empty($separator) && $separator !== ':' && $shortcode_type === 'match_score') {
$shortcode_parts[] = 'separator="' . esc_attr($separator) . '"';
}
$shortcode = '[swi_foot_' . $shortcode_type . (!empty($shortcode_parts) ? ' ' . implode(' ', $shortcode_parts) : '') . ']';
// Execute the shortcode and return inline wrapper so editors can insert it inside text
$rendered = do_shortcode($shortcode);
return '<span class="swi-foot-inline shortcode-inserter">' . $rendered . '</span>';
}
/**
* Render match roster block
* Gets match from container context, uses side attribute to determine which team to show
*/
public function render_match_roster_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';
$with_bench = $attributes['withBench'] ? 'true' : 'false';
if (empty($match_id)) {
return '<div class="swi-foot-error">' . __('Match Roster: No match provided in container context.', 'swi_foot_matchdata') . '</div>';
}
// 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"';
}
$shortcode .= ']';
// Process and return the shortcode
return do_shortcode($shortcode);
}
/**
* Render match events block
* Gets match from container context, displays live events for that match
*/
public function render_match_events_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'] ?? '';
$refresh_interval = $attributes['refreshInterval'] ?? 30;
$event_order = $attributes['eventOrder'] ?? 'dynamic';
if (empty($match_id)) {
return '<div class="swi-foot-error">' . __('Match Events: No match provided in container context.', 'swi_foot_matchdata') . '</div>';
}
// Build shortcode using match from context
$shortcode = '[swi_foot_events match_id="' . esc_attr($match_id) . '" refresh_interval="' . esc_attr($refresh_interval) . '" event_order="' . esc_attr($event_order) . '"]';
// Process and return the shortcode
return do_shortcode($shortcode);
}
private function render_team_selector($block_type)
{
$api = new Swi_Foot_API();
$teams = $api->get_teams();
if (is_wp_error($teams) || empty($teams)) {
return '<div class="swi-foot-error">' .
__('Please configure your API settings and ensure teams are available.', 'swi_foot_matchdata') .
'</div>';
}
ob_start();
?>
<div class="swi-foot-team-selector">
<p><?php printf(__('Please select a team to display %s:', 'swi_foot_matchdata'), $block_type); ?></p>
<select onchange="swiFootSelectTeam(this.value, '<?php echo esc_attr($block_type); ?>')">
<option value=""><?php _e('Select a team...', 'swi_foot_matchdata'); ?></option>
<?php foreach ($teams as $team): ?>
<option value="<?php echo esc_attr($team['teamId'] ?? ''); ?>">
<?php echo esc_html($team['teamName'] ?? 'Unknown Team'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php
return ob_get_clean();
}
}
?>

View File

@ -0,0 +1,399 @@
<?php
class Swi_Foot_REST {
public function __construct() {
add_action('rest_api_init', array($this, 'register_routes'));
}
/**
* Normalize team data for consistent API responses
* @param array $team Raw team data from API
* @return array Normalized team object
*/
private function normalize_team($team) {
return array(
'teamId' => isset($team['teamId']) ? (string)$team['teamId'] : (isset($team['id']) ? (string)$team['id'] : ''),
'teamName' => isset($team['teamName']) ? $team['teamName'] : (isset($team['name']) ? $team['name'] : ''),
'teamLeagueName' => isset($team['teamLeagueName']) ? $team['teamLeagueName'] : (isset($team['league']) ? $team['league'] : null),
'icon' => isset($team['icon']) ? $team['icon'] : null,
);
}
/**
* Normalize match data for consistent API responses
* @param array $match Raw match data from API
* @return array Normalized match object
*/
private function normalize_match($match) {
// Extract team names from teams array if not present as direct fields
$teamNameA = '';
$teamNameB = '';
if (!empty($match['teams']) && is_array($match['teams'])) {
foreach ($match['teams'] as $team) {
if (!empty($team['isHomeTeam']) && !empty($team['teamName'])) {
$teamNameA = $team['teamName'];
} elseif (empty($team['isHomeTeam']) && !empty($team['teamName'])) {
$teamNameB = $team['teamName'];
}
}
}
// Use direct fields if available, fallback to extracted values, then fallback field variations
return array(
'matchId' => isset($match['matchId']) ? (string)$match['matchId'] : (isset($match['id']) ? (string)$match['id'] : ''),
'matchDate' => isset($match['matchDate']) ? $match['matchDate'] : (isset($match['date']) ? $match['date'] : null),
'teamAId' => isset($match['teamAId']) ? (string)$match['teamAId'] : (isset($match['homeTeamId']) ? (string)$match['homeTeamId'] : ''),
'teamBId' => isset($match['teamBId']) ? (string)$match['teamBId'] : (isset($match['awayTeamId']) ? (string)$match['awayTeamId'] : ''),
'teamNameA' => isset($match['teamNameA']) ? $match['teamNameA'] : ($teamNameA ?: (isset($match['homeTeamName']) ? $match['homeTeamName'] : '')),
'teamNameB' => isset($match['teamNameB']) ? $match['teamNameB'] : ($teamNameB ?: (isset($match['awayTeamName']) ? $match['awayTeamName'] : '')),
'scoreTeamA' => isset($match['scoreTeamA']) ? $match['scoreTeamA'] : (isset($match['homeScore']) ? $match['homeScore'] : null),
'scoreTeamB' => isset($match['scoreTeamB']) ? $match['scoreTeamB'] : (isset($match['awayScore']) ? $match['awayScore'] : null),
'matchStateName' => isset($match['matchStateName']) ? $match['matchStateName'] : (isset($match['status']) ? $match['status'] : null),
'stadiumFieldName' => isset($match['stadiumFieldName']) ? $match['stadiumFieldName'] : (isset($match['stadiumPlaygroundName']) ? $match['stadiumPlaygroundName'] : null),
'leagueName' => isset($match['leagueName']) ? $match['leagueName'] : null,
'divisionName' => isset($match['divisionName']) ? $match['divisionName'] : null,
'roundNbr' => isset($match['roundNbr']) ? $match['roundNbr'] : null,
'matchTypeName' => isset($match['matchTypeName']) ? $match['matchTypeName'] : null,
'hasMatchStarted' => isset($match['hasMatchStarted']) ? $match['hasMatchStarted'] : false,
'isMatchPause' => isset($match['isMatchPause']) ? $match['isMatchPause'] : false,
'hasMatchEnded' => isset($match['hasMatchEnded']) ? $match['hasMatchEnded'] : false,
'intermediateResults' => isset($match['intermediateResults']) ? $match['intermediateResults'] : null,
'teams' => isset($match['teams']) ? $match['teams'] : array(),
);
}
public function register_routes() {
register_rest_route('swi-foot/v1', '/teams', array(
'methods' => 'GET',
'callback' => array($this, 'get_teams'),
'permission_callback' => '__return_true'
));
register_rest_route('swi-foot/v1', '/matches', 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);
}
)
)
));
// 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(
'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);
}
)
)
));
// Single match details: /wp-json/swi-foot/v1/match/<match_id>
register_rest_route('swi-foot/v1', '/match/(?P<match_id>[^/]+)', array(
'methods' => 'GET',
'callback' => array($this, 'get_match_details'),
'permission_callback' => '__return_true',
'args' => array(
'match_id' => array(
'validate_callback' => function($param, $request, $key) {
return is_string($param) && strlen($param) > 0;
}
)
)
));
// Match events with status: /wp-json/swi-foot/v1/events/<match_id>
// Requires authentication to prevent abuse from non-logged-in users
register_rest_route('swi-foot/v1', '/events/(?P<match_id>[^/]+)', array(
'methods' => 'GET',
'callback' => array($this, 'get_match_events'),
'permission_callback' => function() {
return is_user_logged_in() || apply_filters('swi_foot_rest_events_public', false);
},
'args' => array(
'match_id' => array(
'validate_callback' => function($param, $request, $key) {
return is_string($param) && strlen($param) > 0;
}
)
)
));
register_rest_route('swi-foot/v1', '/commons-ids', array(
'methods' => 'GET',
'callback' => array($this, 'get_commons_ids'),
'permission_callback' => '__return_true'
));
// Admin actions
register_rest_route('swi-foot/v1', '/admin/refresh-teams', array(
'methods' => 'POST',
'callback' => array($this, 'admin_refresh_teams'),
'permission_callback' => function() { return current_user_can('manage_options'); }
));
register_rest_route('swi-foot/v1', '/admin/clear-cache', array(
'methods' => 'POST',
'callback' => array($this, 'admin_clear_cache'),
'permission_callback' => function() { return current_user_can('manage_options'); }
));
register_rest_route('swi-foot/v1', '/admin/test-connection', array(
'methods' => 'POST',
'callback' => array($this, 'admin_test_connection'),
'permission_callback' => function() { return current_user_can('manage_options'); }
));
register_rest_route('swi-foot/v1', '/admin/finished/(?P<match_id>[^/]+)', array(
'methods' => 'DELETE',
'callback' => array($this, 'admin_delete_finished_match'),
'permission_callback' => function() { return current_user_can('manage_options'); },
'args' => array(
'match_id' => array(
'validate_callback' => function($param, $request, $key) {
return is_string($param) && strlen($param) > 0;
}
)
)
));
register_rest_route('swi-foot/v1', '/admin/finished', array(
'methods' => 'DELETE',
'callback' => array($this, 'admin_delete_all_finished_matches'),
'permission_callback' => function() { return current_user_can('manage_options'); }
));
}
public function get_teams($request) {
$api = new Swi_Foot_API();
$teams = $api->get_teams();
if (is_wp_error($teams)) {
return new WP_REST_Response(array('error' => $teams->get_error_message()), 400);
}
// Normalize team data for consistent responses
$normalized = array();
if (is_array($teams)) {
foreach ($teams as $team) {
$normalized[] = $this->normalize_team($team);
}
}
return rest_ensure_response($normalized);
}
public function get_matches_for_team($request) {
$team_id = $request->get_param('team_id');
if (empty($team_id)) {
return new WP_REST_Response(array('error' => 'team_id required'), 400);
}
$api = new Swi_Foot_API();
$schedule = $api->get_schedule($team_id);
if (is_wp_error($schedule)) {
return new WP_REST_Response(array('error' => $schedule->get_error_message()), 400);
}
// Normalize matches for consistent responses
$normalized = array();
if (is_array($schedule)) {
foreach ($schedule as $match) {
$normalized[] = $this->normalize_match($match);
}
}
return rest_ensure_response($normalized);
}
public function get_commons_ids($request) {
$api = new Swi_Foot_API();
$commons = $api->get_commons_ids();
if (is_wp_error($commons)) {
return new WP_REST_Response(array('error' => $commons->get_error_message()), 400);
}
return rest_ensure_response($commons);
}
/**
* Return full match details for a given match_id
* GET /wp-json/swi-foot/v1/match/<match_id>
*/
public function get_match_details($request) {
$match_id = $request->get_param('match_id');
if (empty($match_id)) {
return new WP_REST_Response(array('error' => 'match_id required'), 400);
}
$api = new Swi_Foot_API();
$match = $api->get_match_details($match_id);
if (is_wp_error($match)) {
return new WP_REST_Response(array('error' => $match->get_error_message()), 400);
}
// Normalize match data for consistent API responses
$normalized = $this->normalize_match($match);
return rest_ensure_response($normalized);
}
/**
* Get match events with current match status (for frontend refresh polling)
*
* Endpoint: GET /wp-json/swi-foot/v1/events/<match_id>
*
* Returns the latest match events (sorted newest first) along with critical match
* status information (hasMatchStarted, hasMatchEnded). Frontend polls this endpoint
* at configurable intervals to update live event displays.
*
* Permission: Requires logged-in user (can be overridden with swi_foot_rest_events_public filter)
*
* @param WP_REST_Request $request The REST request object containing match_id parameter
* @return WP_REST_Response Array with keys:
* - matchId (string): The match ID
* - events (array): Array of event objects with matchMinute, eventTypeName, playerName, teamName, timestamp
* - hasMatchStarted (bool): Whether the match has begun
* - hasMatchEnded (bool): Whether the match has concluded
* - matchStateName (string|null): Current match status label
*/
public function get_match_events($request) {
$match_id = $request->get_param('match_id');
if (empty($match_id)) {
return new WP_REST_Response(array('error' => 'match_id required'), 400);
}
$api = new Swi_Foot_API();
// Get match details for status
$match = $api->get_match_details($match_id);
if (is_wp_error($match)) {
return new WP_REST_Response(array('error' => $match->get_error_message()), 400);
}
// Try saved finished match data first
$events = array();
$saved = $api->get_finished_match_data($match_id);
if ($saved) {
$events = $saved['events'] ?? array();
} else {
// Fetch live events
$events = $api->get_match_events($match_id);
if (is_wp_error($events)) {
$events = array();
}
}
// Get event ordering preference from query parameter
$event_order = $request->get_param('event_order') ?? 'dynamic';
// Convert exactEventTime to timestamps for sorting
if (!empty($events)) {
foreach ($events as &$event) {
// Convert ISO 8601 format (2026-03-14T17:03:50.437) to timestamp
// Strip milliseconds if present, keep only date and time part
$event['_timestamp'] = strtotime(substr($event['exactEventTime'], 0, 19));
}
unset($event);
// Sort by timestamp
if ($event_order === 'dynamic') {
// Dynamic: newest first while match is live, chronological after match ends
$match_has_ended = !empty($match['hasMatchEnded']);
usort($events, function ($a, $b) use ($match_has_ended) {
if ($match_has_ended) {
// Chronological (ascending): oldest first
return $a['_timestamp'] - $b['_timestamp'];
} else {
// Descending: newest first
return $b['_timestamp'] - $a['_timestamp'];
}
});
} elseif ($event_order === 'newest_first') {
// Always newest first (descending)
usort($events, function ($a, $b) {
return $b['_timestamp'] - $a['_timestamp'];
});
} elseif ($event_order === 'oldest_first') {
// Always oldest first (ascending)
usort($events, function ($a, $b) {
return $a['_timestamp'] - $b['_timestamp'];
});
}
}
return rest_ensure_response(array(
'matchId' => $match_id,
'events' => (array)$events,
'hasMatchStarted' => $match['hasMatchStarted'] ?? false,
'hasMatchEnded' => $match['hasMatchEnded'] ?? false,
'matchStateName' => $match['matchStateName'] ?? null,
));
}
public function admin_refresh_teams($request) {
$api = new Swi_Foot_API();
// Collect debug information to assist troubleshooting 401 responses
$debug = array();
if (method_exists($api, 'debug_get_token_info')) {
$debug = $api->debug_get_token_info();
error_log('Swiss Football Debug: refresh-teams token_present=' . ($debug['token_present'] ? '1' : '0') . ' base=' . $debug['base_url'] . ' url=' . $debug['team_list_url']);
}
delete_transient('swi_foot_teams');
$teams = $api->get_teams();
if (is_wp_error($teams)) {
// Try to include debug information in response for admin callers
$err = $teams->get_error_message();
error_log('Swiss Football API: refresh-teams failed - ' . $err);
return new WP_REST_Response(array('error' => $err, 'debug' => $debug), 400);
}
return rest_ensure_response(array('success' => true, 'data' => $teams, 'debug' => $debug));
}
public function admin_clear_cache($request) {
$keys = get_transient('swi_foot_match_keys');
if (is_array($keys)) {
foreach ($keys as $mid) {
delete_transient('swi_foot_match_' . $mid);
}
}
delete_transient('swi_foot_match_keys');
delete_transient('swi_foot_teams');
return rest_ensure_response(array('success' => true));
}
public function admin_test_connection($request) {
$api = new Swi_Foot_API();
if (method_exists($api, 'test_connection')) {
$ok = $api->test_connection();
} else {
$ok = false;
}
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) {
$match_id = $request->get_param('match_id');
if (empty($match_id)) return new WP_REST_Response(array('error' => 'match_id required'), 400);
$data = get_option('swi_foot_finished_matches', array());
if (isset($data[$match_id])) {
unset($data[$match_id]);
update_option('swi_foot_finished_matches', $data);
return rest_ensure_response(array('success' => true));
}
return new WP_REST_Response(array('error' => 'not found'), 404);
}
public function admin_delete_all_finished_matches($request) {
delete_option('swi_foot_finished_matches');
return rest_ensure_response(array('success' => true));
}
}
new Swi_Foot_REST();

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,705 @@
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.

View File

@ -0,0 +1,704 @@
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: en_US\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 Settings"
#: 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 "Test API Connection"
#: includes/class-swi-foot-admin.php:42
msgid "API Base URL"
msgstr "API Base URL"
#: includes/class-swi-foot-admin.php:43
msgid "API Username (Application Key)"
msgstr "API Username (Application Key)"
#: includes/class-swi-foot-admin.php:44
msgid "API Password (Application Pass)"
msgstr "API Password (Application Pass)"
#: includes/class-swi-foot-admin.php:45
msgid "Verein ID (Club ID)"
msgstr "Verein ID (Club ID)"
#: includes/class-swi-foot-admin.php:46
msgid "Season ID"
msgstr "Season 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 "Match Data Cache Duration (seconds)"
#: 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 Username (Application Key)"
#: includes/class-swi-foot-admin.php:86
#, fuzzy
msgid "Your API application password"
msgstr "API Password (Application Pass)"
#: includes/class-swi-foot-admin.php:93
#, fuzzy
msgid "Enter your club's Verein ID (Club ID)"
msgstr "Verein 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 "Test API Connection"
#: includes/class-swi-foot-admin.php:270
msgid "Team Management"
msgstr ""
#: includes/class-swi-foot-admin.php:273
msgid "Refresh Teams List"
msgstr "Refresh Teams List"
#: includes/class-swi-foot-admin.php:284
msgid "Cache Management"
msgstr ""
#: includes/class-swi-foot-admin.php:287
msgid "Clear Match Data Cache"
msgstr "Clear Match Data Cache"
#: includes/class-swi-foot-admin.php:295
#, fuzzy
msgid "Finished Matches Data"
msgstr "Clear All Finished Matches"
#: 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 "No finished match data stored."
#: includes/class-swi-foot-admin.php:393
#, fuzzy
msgid "Match ID"
msgstr "Match Roster"
#: 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 "Bench"
#: includes/class-swi-foot-admin.php:397
#, fuzzy
msgid "Events"
msgstr "Match Events"
#: includes/class-swi-foot-admin.php:415
msgid "Delete"
msgstr "Delete"
#: includes/class-swi-foot-admin.php:421
msgid "Clear All Finished Matches"
msgstr "Clear All Finished Matches"
#: includes/class-swi-foot-admin.php:428
#, fuzzy
msgid "Delete this finished match data?"
msgstr "No finished match data stored."
#: includes/class-swi-foot-admin.php:445
#, fuzzy
msgid "Clear all finished match data?"
msgstr "Clear All Finished Matches"
#: 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 "No standings data available."
#: includes/class-swi-foot-blocks.php:237
#: includes/class-swi-foot-shortcodes.php:631
msgid "Current Standings"
msgstr "Current Standings"
#: 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 "Home Team"
#: 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 "Upcoming Matches"
#: includes/class-swi-foot-blocks.php:316
msgid "No upcoming matches scheduled."
msgstr "No upcoming matches scheduled."
#: 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 "No standings data available."
#: 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 "No standings data available."
#: 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 "No standings data available."
#: includes/class-swi-foot-shortcodes.php:743
#, fuzzy
msgid "Team Roster"
msgstr "Match Roster"
#: includes/class-swi-foot-shortcodes.php:764
#, fuzzy
msgid "No roster data available."
msgstr "No standings data available."
#: includes/class-swi-foot-shortcodes.php:822
#, fuzzy
msgid "Events data not available"
msgstr "No standings data available."
#: includes/class-swi-foot-shortcodes.php:882
#, fuzzy
msgid "Live match events"
msgstr "Match Events"
#: includes/class-swi-foot-shortcodes.php:883
msgid "Match Events"
msgstr "Match Events"
#: includes/class-swi-foot-shortcodes.php:889
#, fuzzy
msgid "Match minute"
msgstr "Match Events"
#: includes/class-swi-foot-shortcodes.php:950
msgid "No events recorded yet."
msgstr "No events recorded yet."
#: 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 "Current Standings"
#: src/editor-blocks.js:133
#, fuzzy
msgid "Match"
msgstr "Match Roster"
#: src/editor-blocks.js:138
msgid "Team data shortcode generator"
msgstr ""
#: src/editor-blocks.js:161
msgid "Swiss Football Match Roster"
msgstr "Swiss Football Match Roster"
#: 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 "Home Team"
#: src/editor-blocks.js:173
#, fuzzy
msgid "Home Team"
msgstr "Home Team"
#: src/editor-blocks.js:173
msgid "Away Team"
msgstr "Away Team"
#: 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 "Swiss Football Match Events"
#: src/editor-blocks.js:209
msgid "Refresh Interval (seconds)"
msgstr ""
#: src/editor-blocks.js:217
msgid "Event Order"
msgstr "Event Order"
#: src/editor-blocks.js:221
msgid "Dynamic (Newest first while live, chronological after)"
msgstr "Dynamic (Newest first while live, chronological after)"
#: src/editor-blocks.js:222
msgid "Newest First"
msgstr "Newest First"
#: src/editor-blocks.js:223
msgid "Oldest First"
msgstr "Oldest First"
#: src/editor-blocks.js:225
msgid ""
"Dynamic: newest events at top while match is ongoing, chronological (oldest "
"first) after match ends"
msgstr ""
"Dynamic: newest events at top while match is ongoing, chronological (oldest "
"first) after match ends"
#: 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 "Home Team Logo"
#: src/format-shortcode.js:54
msgid "Gastteam Logo"
msgstr "Away Team Logo"
#: src/format-shortcode.js:55
msgid "Heimteam Logo (URL)"
msgstr "Home Team Logo (URL)"
#: src/format-shortcode.js:56
msgid "Gastteam Logo (URL)"
msgstr "Away Team 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 "Clear All Finished Matches"
#: 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 "Match Roster"
#: 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 "Display match roster for a selected match and side."
#~ msgid "Live match events with optional auto-refresh."
#~ msgstr "Live match events with optional auto-refresh."

Binary file not shown.

View File

@ -0,0 +1,704 @@
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: fr_FR\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 "Paramètres Swiss Football Matchdata"
#: 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 "Tester la connexion API"
#: includes/class-swi-foot-admin.php:42
msgid "API Base URL"
msgstr "URL de base API"
#: includes/class-swi-foot-admin.php:43
msgid "API Username (Application Key)"
msgstr "Nom d'utilisateur API (Clé d'application)"
#: includes/class-swi-foot-admin.php:44
msgid "API Password (Application Pass)"
msgstr "Mot de passe API (Application Pass)"
#: includes/class-swi-foot-admin.php:45
msgid "Verein ID (Club ID)"
msgstr "ID du club (Club ID)"
#: includes/class-swi-foot-admin.php:46
msgid "Season ID"
msgstr "ID de la saison"
#: 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 "Durée du cache des données de match (secondes)"
#: 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 "Nom d'utilisateur API (Clé d'application)"
#: includes/class-swi-foot-admin.php:86
#, fuzzy
msgid "Your API application password"
msgstr "Mot de passe API (Application Pass)"
#: includes/class-swi-foot-admin.php:93
#, fuzzy
msgid "Enter your club's Verein ID (Club ID)"
msgstr "ID du club (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 "Tester la connexion API"
#: includes/class-swi-foot-admin.php:270
msgid "Team Management"
msgstr ""
#: includes/class-swi-foot-admin.php:273
msgid "Refresh Teams List"
msgstr "Actualiser la liste des équipes"
#: includes/class-swi-foot-admin.php:284
msgid "Cache Management"
msgstr ""
#: includes/class-swi-foot-admin.php:287
msgid "Clear Match Data Cache"
msgstr "Vider le cache des données de matchs"
#: includes/class-swi-foot-admin.php:295
#, fuzzy
msgid "Finished Matches Data"
msgstr "Effacer tous les matchs terminés"
#: 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 "Aucune donnée de match terminé stockée."
#: includes/class-swi-foot-admin.php:393
#, fuzzy
msgid "Match ID"
msgstr "Composition du match"
#: 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 "Remplaçants"
#: includes/class-swi-foot-admin.php:397
#, fuzzy
msgid "Events"
msgstr "Événements du match"
#: includes/class-swi-foot-admin.php:415
msgid "Delete"
msgstr "Supprimer"
#: includes/class-swi-foot-admin.php:421
msgid "Clear All Finished Matches"
msgstr "Effacer tous les matchs terminés"
#: includes/class-swi-foot-admin.php:428
#, fuzzy
msgid "Delete this finished match data?"
msgstr "Aucune donnée de match terminé stockée."
#: includes/class-swi-foot-admin.php:445
#, fuzzy
msgid "Clear all finished match data?"
msgstr "Effacer tous les matchs terminés"
#: 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 "Aucun classement disponible."
#: includes/class-swi-foot-blocks.php:237
#: includes/class-swi-foot-shortcodes.php:631
msgid "Current Standings"
msgstr "Classement actuel"
#: 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 "Équipe à domicile"
#: 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 "Matchs à venir"
#: includes/class-swi-foot-blocks.php:316
msgid "No upcoming matches scheduled."
msgstr "Aucun match à venir programmé."
#: 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 "Aucun classement disponible."
#: 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 "Aucun classement disponible."
#: 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 "Aucun classement disponible."
#: includes/class-swi-foot-shortcodes.php:743
#, fuzzy
msgid "Team Roster"
msgstr "Composition du match"
#: includes/class-swi-foot-shortcodes.php:764
#, fuzzy
msgid "No roster data available."
msgstr "Aucun classement disponible."
#: includes/class-swi-foot-shortcodes.php:822
#, fuzzy
msgid "Events data not available"
msgstr "Aucun classement disponible."
#: includes/class-swi-foot-shortcodes.php:882
#, fuzzy
msgid "Live match events"
msgstr "Événements du match"
#: includes/class-swi-foot-shortcodes.php:883
msgid "Match Events"
msgstr "Événements du match"
#: includes/class-swi-foot-shortcodes.php:889
#, fuzzy
msgid "Match minute"
msgstr "Événements du match"
#: includes/class-swi-foot-shortcodes.php:950
msgid "No events recorded yet."
msgstr "Aucun événement enregistré."
#: 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 "Classement actuel"
#: src/editor-blocks.js:133
#, fuzzy
msgid "Match"
msgstr "Composition du match"
#: src/editor-blocks.js:138
msgid "Team data shortcode generator"
msgstr ""
#: src/editor-blocks.js:161
msgid "Swiss Football Match Roster"
msgstr "Feuille d'équipe du football suisse"
#: 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 "Équipe à domicile"
#: src/editor-blocks.js:173
#, fuzzy
msgid "Home Team"
msgstr "Équipe à domicile"
#: src/editor-blocks.js:173
msgid "Away Team"
msgstr "Équipe à l'extérieur"
#: 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 "Événements des matchs de football suisse"
#: src/editor-blocks.js:209
msgid "Refresh Interval (seconds)"
msgstr ""
#: src/editor-blocks.js:217
msgid "Event Order"
msgstr "Ordre des événements"
#: src/editor-blocks.js:221
msgid "Dynamic (Newest first while live, chronological after)"
msgstr "Dynamique (Les plus récents en premier pendant le match, chronologiquement après)"
#: src/editor-blocks.js:222
msgid "Newest First"
msgstr "Les plus récents en premier"
#: src/editor-blocks.js:223
msgid "Oldest First"
msgstr "Les plus anciens en premier"
#: src/editor-blocks.js:225
msgid ""
"Dynamic: newest events at top while match is ongoing, chronological (oldest "
"first) after match ends"
msgstr ""
"Dynamique : les plus récents en haut pendant le match, chronologiquement "
"(les plus anciens en premier) après la fin du match"
#: 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 "Logo de l'équipe à domicile"
#: src/format-shortcode.js:54
msgid "Gastteam Logo"
msgstr "Logo de l'équipe en déplacement"
#: src/format-shortcode.js:55
msgid "Heimteam Logo (URL)"
msgstr "Logo de l'équipe à domicile (URL)"
#: src/format-shortcode.js:56
msgid "Gastteam Logo (URL)"
msgstr "Logo de l'équipe en déplacement (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 "Effacer tous les matchs terminés"
#: 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 "Composition du match"
#: 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 "Afficher l'effectif pour un match et une équipe sélectionnés."
#~ msgid "Live match events with optional auto-refresh."
#~ msgstr "Événements en direct avec actualisation automatique optionnelle."

View File

@ -0,0 +1,678 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-18 14:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \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 ""
#: includes/class-swi-foot-admin.php:19
msgid "Swiss Football"
msgstr ""
#: includes/class-swi-foot-admin.php:37
msgid "API Configuration"
msgstr ""
#: includes/class-swi-foot-admin.php:42
msgid "API Base URL"
msgstr ""
#: includes/class-swi-foot-admin.php:43
msgid "API Username (Application Key)"
msgstr ""
#: includes/class-swi-foot-admin.php:44
msgid "API Password (Application Pass)"
msgstr ""
#: includes/class-swi-foot-admin.php:45
msgid "Verein ID (Club ID)"
msgstr ""
#: includes/class-swi-foot-admin.php:46
msgid "Season ID"
msgstr ""
#: 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 ""
#: 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
msgid "Your API application key"
msgstr ""
#: includes/class-swi-foot-admin.php:86
msgid "Your API application password"
msgstr ""
#: includes/class-swi-foot-admin.php:93
msgid "Enter your club's Verein ID (Club ID)"
msgstr ""
#: 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
msgid "Swiss Football Shortcodes"
msgstr ""
#: 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 ""
#: includes/class-swi-foot-admin.php:270
msgid "Team Management"
msgstr ""
#: includes/class-swi-foot-admin.php:273
msgid "Refresh Teams List"
msgstr ""
#: includes/class-swi-foot-admin.php:284
msgid "Cache Management"
msgstr ""
#: includes/class-swi-foot-admin.php:287
msgid "Clear Match Data Cache"
msgstr ""
#: includes/class-swi-foot-admin.php:295
msgid "Finished Matches Data"
msgstr ""
#: 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 ""
#: includes/class-swi-foot-admin.php:393
msgid "Match ID"
msgstr ""
#: 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 ""
#: includes/class-swi-foot-admin.php:397
msgid "Events"
msgstr ""
#: includes/class-swi-foot-admin.php:415
msgid "Delete"
msgstr ""
#: includes/class-swi-foot-admin.php:421
msgid "Clear All Finished Matches"
msgstr ""
#: includes/class-swi-foot-admin.php:428
msgid "Delete this finished match data?"
msgstr ""
#: includes/class-swi-foot-admin.php:445
msgid "Clear all finished match data?"
msgstr ""
#: 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 ""
#: includes/class-swi-foot-blocks.php:237
#: includes/class-swi-foot-shortcodes.php:631
msgid "Current Standings"
msgstr ""
#: 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
msgid "Team"
msgstr ""
#: 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 ""
#: includes/class-swi-foot-blocks.php:316
msgid "No upcoming matches scheduled."
msgstr ""
#: 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
msgid "Match data not available"
msgstr ""
#: 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
msgid "Standings data not available"
msgstr ""
#: 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
msgid "Roster data not available"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:743
msgid "Team Roster"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:764
msgid "No roster data available."
msgstr ""
#: includes/class-swi-foot-shortcodes.php:822
msgid "Events data not available"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:882
msgid "Live match events"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:883
msgid "Match Events"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:889
msgid "Match minute"
msgstr ""
#: includes/class-swi-foot-shortcodes.php:950
msgid "No events recorded yet."
msgstr ""
#: 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
msgid "Swiss Football Team Data"
msgstr ""
#: 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
msgid "Standings"
msgstr ""
#: src/editor-blocks.js:133
msgid "Match"
msgstr ""
#: src/editor-blocks.js:138
msgid "Team data shortcode generator"
msgstr ""
#: src/editor-blocks.js:161
msgid "Swiss Football Match Roster"
msgstr ""
#: 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
msgid "Team Side"
msgstr ""
#: src/editor-blocks.js:173
msgid "Home Team"
msgstr ""
#: src/editor-blocks.js:173
msgid "Away Team"
msgstr ""
#: 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 ""
#: src/editor-blocks.js:209
msgid "Refresh Interval (seconds)"
msgstr ""
#: src/editor-blocks.js:217
msgid "Event Order"
msgstr ""
#: src/editor-blocks.js:221
msgid "Dynamic (Newest first while live, chronological after)"
msgstr ""
#: src/editor-blocks.js:222
msgid "Newest First"
msgstr ""
#: src/editor-blocks.js:223
msgid "Oldest First"
msgstr ""
#: src/editor-blocks.js:225
msgid ""
"Dynamic: newest events at top while match is ongoing, chronological (oldest "
"first) after match ends"
msgstr ""
#: 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 ""
#: src/format-shortcode.js:54
msgid "Gastteam Logo"
msgstr ""
#: src/format-shortcode.js:55
msgid "Heimteam Logo (URL)"
msgstr ""
#: src/format-shortcode.js:56
msgid "Gastteam Logo (URL)"
msgstr ""
#: src/format-shortcode.js:229 src/format-shortcode.js:242
#: src/format-shortcode.js:356 src/format-shortcode.js:390
msgid "Insert Match Data"
msgstr ""
#: src/format-shortcode.js:234
msgid "No match data available on this page. Please add match context first."
msgstr ""
#: src/format-shortcode.js:255
msgid "Match Data"
msgstr ""
#: 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 ""

12
package.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "swi-foot-matchdata-blocks",
"version": "1.0.0",
"description": "Build tooling for Swiss Football Matchdata Gutenberg blocks",
"scripts": {
"build": "wp-scripts build --output-path=assets/build",
"start": "wp-scripts start"
},
"devDependencies": {
"@wordpress/scripts": "^31.5.0"
}
}

279
src/editor-blocks.js Normal file
View File

@ -0,0 +1,279 @@
// Editor entry (ESNext) for Swiss Football blocks.
// This registers blocks and the shortcode insertion toolbar button.
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { InspectorControls, useBlockProps, RawHTML } from '@wordpress/block-editor';
import { PanelBody, SelectControl, ToggleControl, RangeControl, Spinner } from '@wordpress/components';
import { useState, useEffect, createElement } from '@wordpress/element';
import { registerFormatType, applyFormat } from '@wordpress/rich-text';
import './format-shortcode';
function makeAjaxCall(action, data = {}) {
const root = swiFootEditorData.rest_url.replace(/\/$/, '');
let url = root;
const opts = { credentials: 'same-origin', headers: { 'X-WP-Nonce': swiFootEditorData.rest_nonce } };
if (action === 'swi_foot_get_teams_for_editor') {
url += '/teams'; opts.method = 'GET';
} else if (action === 'swi_foot_get_matches_for_team') {
url += '/matches?team_id=' + encodeURIComponent(data.team_id || ''); opts.method = 'GET';
} else if (action === 'swi_foot_get_commons_ids') {
url += '/commons-ids'; opts.method = 'GET';
} else {
return Promise.reject({ error: 'Unknown action' });
}
return fetch(url, opts)
.then(r => { if (!r.ok) return r.json().then(j => Promise.reject(j)); return r.json(); })
.then(data => {
// Transform API response into expected format for editor blocks
if (action === 'swi_foot_get_teams_for_editor') {
return {
success: true,
data: Array.isArray(data) ? data.map(team => ({
value: team.teamId,
label: team.teamName
})) : []
};
} else if (action === 'swi_foot_get_matches_for_team') {
return {
success: true,
data: Array.isArray(data) ? data.map(match => ({
value: match.matchId,
label: `${match.teamNameA} vs ${match.teamNameB} (${match.matchDate})`
})) : []
};
}
return { success: true, data: data };
});
}
// Standings block (editor UI only, server renders)
// Gets team from container context, no user selection needed
registerBlockType('swi-foot/standings', {
title: __('Swiss Football Standings', 'swi_foot_matchdata'),
category: 'embed',
supports: { align: true, anchor: true },
edit: ({ attributes, setAttributes, 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'}}>{__('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>
);
},
save: () => null
});
// Schedule block (editor UI only, server renders)
// Gets team from container context, only allows limiting results
registerBlockType('swi-foot/schedule', {
title: __('Swiss Football Schedule', 'swi_foot_matchdata'),
category: 'embed',
supports: { align: true, anchor: true },
attributes: {
limit: { type: 'number', default: 10 }
},
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
const { limit } = attributes;
return (
<div {...blockProps}>
<InspectorControls>
<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} />
</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>
);
},
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', {
title: __('Swiss Football Match Roster', 'swi_foot_matchdata'),
category: 'embed',
supports: { align: true, anchor: true },
attributes: {
side: { type: 'string', default: 'home' },
withBench: { type: 'boolean', default: false }
},
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
const { side, withBench } = attributes;
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title={__('Display Options', 'swi_foot_matchdata')}>
<p style={{color: '#666', fontSize: '13px', marginBottom: '12px'}}>{__('Match and team are 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})}
/>
<ToggleControl
label={__('Include Bench Players', 'swi_foot_matchdata')}
checked={withBench}
onChange={v=>setAttributes({withBench:v})}
/>
</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>
);
},
save: ({ attributes }) => {
const { side, withBench } = attributes;
let shortcode = '[swi_foot_roster side="' + side + '"';
if (withBench) shortcode += ' with_bench="true"';
shortcode += ']';
return createElement(RawHTML, null, shortcode);
}
});
// Match Events
// Gets match and team from container context, only allows setting refresh interval
registerBlockType('swi-foot/match-events', {
title: __('Swiss Football Match Events', 'swi_foot_matchdata'),
category: 'embed',
supports: { align: true, anchor: true },
attributes: {
refreshInterval: { type: 'number', default: 30 },
eventOrder: { type: 'string', default: 'dynamic' }
},
edit: ({ attributes, setAttributes, isSelected }) => {
const blockProps = useBlockProps({ style: isSelected ? {outline: '2px solid #0073aa'} : {} });
const { refreshInterval, eventOrder } = attributes;
return (
<div {...blockProps}>
<InspectorControls>
<PanelBody title={__('Display Options', 'swi_foot_matchdata')}>
<p style={{color: '#666', fontSize: '13px', marginBottom: '12px'}}>{__('Match and team are inherited from the container context.', 'swi_foot_matchdata')}</p>
<RangeControl
label={__('Refresh Interval (seconds)', 'swi_foot_matchdata')}
value={refreshInterval || 30}
onChange={v=>setAttributes({refreshInterval:v})}
min={10}
max={300}
step={10}
/>
<SelectControl
label={__('Event Order', 'swi_foot_matchdata')}
value={eventOrder || 'dynamic'}
onChange={v=>setAttributes({eventOrder:v})}
options={[
{ label: __('Dynamic (Newest first while live, chronological after)', 'swi_foot_matchdata'), value: 'dynamic' },
{ label: __('Newest First', 'swi_foot_matchdata'), value: 'newest_first' },
{ 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')}
/>
</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>
);
},
save: ({ attributes }) => {
const { refreshInterval, eventOrder } = attributes;
let shortcode = '[swi_foot_events refresh_interval="' + (refreshInterval || 30) + '"';
if (eventOrder && eventOrder !== 'dynamic') {
shortcode += ' event_order="' + eventOrder + '"';
}
shortcode += ']';
return createElement(RawHTML, null, shortcode);
}
});

405
src/format-shortcode.js Normal file
View File

@ -0,0 +1,405 @@
/**
* SWI Football Shortcode Insertion
*
* Adds a toolbar button to paragraph blocks for inserting shortcodes directly as text.
* Properly handles cursor position and text selection replacement.
*/
import { registerFormatType, insert } from '@wordpress/rich-text';
import { RichTextToolbarButton } from '@wordpress/block-editor';
import { Button, Modal, SelectControl, PanelRow, Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useState, useEffect, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Soccer ball SVG icon component
*/
const SoccerBallIcon = () => (
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" aria-hidden="true" focusable="false">
<g transform="scale(0.02583)">
<g>
<path d="M361.2,578.801c4,12.398,15.5,20.699,28.5,20.699h149c13,0,24.5-8.4,28.5-20.699L613.3,437.1c4-12.4-0.4-25.9-10.9-33.5l-120.5-87.7c-10.5-7.6-24.8-7.6-35.3,0L326,403.5c-10.5,7.601-14.9,21.2-10.9,33.5L361.2,578.801z"></path>
<path d="M136,792.6c42.6,42.6,92.3,76.1,147.6,99.5c57.3,24.201,118.101,36.5,180.7,36.5s123.5-12.299,180.7-36.5c55.3-23.4,104.899-56.9,147.6-99.5C835.2,750,868.7,700.301,892.1,645c24.2-57.301,36.5-118.1,36.5-180.699c0-62.701-12.3-123.501-36.5-180.701C868.7,228.3,835.2,178.7,792.6,136C750,93.4,700.3,59.9,645,36.5C587.8,12.3,527,0,464.3,0S340.8,12.3,283.6,36.5C228.3,59.9,178.6,93.4,136,136c-42.6,42.6-76.1,92.3-99.5,147.6C12.3,340.8,0,401.7,0,464.301C0,527,12.3,587.801,36.5,645C59.9,700.4,93.4,750,136,792.6z M189,189c40.6-40.6,88.6-71.1,141-90.3l-10.5,22.7c-6.9,14.7-0.7,32.3,13.8,39.5l116.5,58c8.5,4.2,18.5,4.2,26.9-0.1l116-58.9c14.6-7.4,20.6-25,13.6-39.7l-10.7-22.6c53.5,19.1,102.601,50,144,91.4c7.9,7.9,15.4,16,22.5,24.4l-24.7-2.2c-16.199-1.4-30.6,10.3-32.399,26.5L690.2,367c-1.101,9.4,2.399,18.801,9.3,25.301l94.899,89c11.9,11.1,30.4,10.799,41.9-0.801l17.4-17.6c0,0.4,0,0.9,0,1.299c0,76.102-21.7,149-62.2,211.4l-5.2-24.4c-3.4-15.898-18.8-26.199-34.8-23.199l-128,23.699C614.2,653.4,606.2,659.5,602,668l-57.3,116.9c-7.2,14.6-1.4,32.199,13,39.799l22.1,11.602c-36.9,11.398-75.8,17.299-115.5,17.299s-78.6-5.9-115.5-17.299l22.1-11.602c14.4-7.5,20.2-25.199,13-39.799L326.6,668.1c-4.2-8.5-12.1-14.6-21.5-16.299l-128-23.701c-16-3-31.4,7.301-34.8,23.201l-5.2,24.398C96.6,613.301,74.9,540.5,74.9,464.301c0-0.401,0-0.901,0-1.301l17.4,17.6c11.4,11.6,30,11.9,41.9,0.801l94.9-89c6.9,6.5,10.4-15.9,9.3-25.3l-14.8-129.3c-1.8-16.2-16.2-27.9-32.4-26.5l-24.7,2.2C173.6,205.1,181.1,196.9,189,189z"></path>
</g>
</g>
</svg>
);
// Inline data point options for displaying individual match data
const INLINE_DATA_POINTS = [
{ value: '', label: __( '— Select data point…', 'swi_foot_matchdata' ) },
{ value: 'matchTypeName', label: __( 'Match-Typ', 'swi_foot_matchdata' ) },
{ value: 'leagueName', label: __( 'Liga', 'swi_foot_matchdata' ) },
{ value: 'divisionName', label: __( 'Gruppe', 'swi_foot_matchdata' ) },
{ value: 'roundNbr', label: __( 'Runde', 'swi_foot_matchdata' ) },
{ value: 'stadiumFieldName', label: __( 'Spielfeld', 'swi_foot_matchdata' ) },
{ value: 'teamNameA', label: __( 'Heimteam', 'swi_foot_matchdata' ) },
{ value: 'teamNameB', label: __( 'Gastteam', 'swi_foot_matchdata' ) },
{ value: 'scoreTeamA', label: __( 'Tore Heim', 'swi_foot_matchdata' ) },
{ value: 'scoreTeamB', label: __( 'Tore Gast', 'swi_foot_matchdata' ) },
{ value: 'intermediateResults/scoreTeamA', label: __( 'Tore Halbzeit Heim', 'swi_foot_matchdata' ) },
{ value: 'intermediateResults/scoreTeamB', label: __( 'Tore Halbzeit Gast', 'swi_foot_matchdata' ) },
{ value: 'matchDate_date', label: __( 'Spieltag', 'swi_foot_matchdata' ) },
{ value: 'matchDate_time', label: __( 'Spielzeit', 'swi_foot_matchdata' ) },
{ value: 'hasMatchStarted', label: __( 'Spielstatus', 'swi_foot_matchdata' ) },
{ value: 'isMatchPause', label: __( 'Spiel pausiert', 'swi_foot_matchdata' ) },
{ value: 'hasMatchEnded', label: __( 'Spiel beendet', 'swi_foot_matchdata' ) },
];
// Shortcode options for inserting team logos and other shortcodes
const SHORTCODE_OPTIONS = [
{ value: '', label: __( '— Select shortcode…', 'swi_foot_matchdata' ) },
{ value: 'swi_foot_match_home_team_logo', label: __( 'Heimteam Logo', 'swi_foot_matchdata' ) },
{ value: 'swi_foot_match_away_team_logo', label: __( 'Gastteam Logo', 'swi_foot_matchdata' ) },
{ value: 'swi_foot_match_home_team_logo_url', label: __( 'Heimteam Logo (URL)', 'swi_foot_matchdata' ) },
{ value: 'swi_foot_match_away_team_logo_url', label: __( 'Gastteam Logo (URL)', 'swi_foot_matchdata' ) },
];
/**
* Fetch a specific match by ID from REST API
*
* @param {string} matchId - The numeric match ID to fetch
* @returns {Promise<Object|null>} Match data object or null if request fails
*/
async function fetchMatch( matchId ) {
try {
const url = `${ wpApiSettings.root }swi-foot/v1/match/${ encodeURIComponent( matchId ) }`;
const response = await fetch( url, {
credentials: 'same-origin',
headers: { 'X-WP-Nonce': wpApiSettings.nonce },
} );
if ( ! response.ok ) return null;
return await response.json();
} catch ( e ) {
console.warn( 'swi-foot: fetchMatch error', e );
return null;
}
}
/**
* Extract nested property values using slash-delimited path notation
*
* @param {Object} obj - The object to extract from
* @param {string} path - Path using slash notation (e.g., 'intermediateResults/scoreTeamA')
* @returns {*|null} The value at the path, or null if not found
*/
function getNestedValue( obj, path ) {
if ( ! obj || ! path ) return null;
const keys = path.split( '/' );
let current = obj;
for ( const key of keys ) {
if ( ! current || typeof current !== 'object' ) return null;
current = current[ key ];
if ( current === undefined ) return null;
}
return current;
}
/**
* Generate inline data shortcode for match data insertion
*
* Format: [swi_foot_match_data data_point="fieldName"]
* Match ID is automatically retrieved from container context, not embedded in shortcode
* Supports nested fields via slash notation (e.g., intermediateResults/scoreTeamA)
*
* @param {string} dataPoint - The field name or path to extract
* @returns {string} Shortcode string, or empty string if input invalid
*/
function generateDataShortcode( dataPoint ) {
if ( ! dataPoint ) {
return '';
}
return `[swi_foot_match_data data_point="${ dataPoint }"]`;
}
/**
* Generate shortcode string for regular shortcodes (logos, etc.)
*
* Format: [shortcode_name]
*
* @param {string} shortcodeValue - The shortcode name/identifier
* @returns {string} Shortcode string, or empty string if input invalid
*/
function generateShortcode( shortcodeValue ) {
if ( ! shortcodeValue ) {
return '';
}
return `[${ shortcodeValue }]`;
}
/**
* Modal component for selecting and previewing match data or other shortcodes to insert
*
* Allows users to:
* - Choose between inserting match data or shortcodes (logos, etc.)
* - Select a data point or shortcode from available options
* - Preview the actual value for data points
* - Generate and insert the shortcode with one click
*
* @component
* @param {Object} props
* @param {Function} props.onInsert - Callback when shortcode is inserted (receives shortcode string)
* @param {Function} props.onClose - Callback to close the modal
* @param {string|null} props.matchId - The match ID (from page context), or null if unavailable
* @returns {React.ReactElement} Modal or empty state message
*/
function InlineDataModal( { onInsert, onClose, matchId } ) {
const [ tabMode, setTabMode ] = useState( 'data-points' );
const [ selectedDataPoint, setSelectedDataPoint ] = useState( '' );
const [ selectedShortcode, setSelectedShortcode ] = useState( '' );
const [ matchData, setMatchData ] = useState( null );
const [ loading, setLoading ] = useState( true );
const [ actualValue, setActualValue ] = useState( '' );
// Fetch match data on mount
useEffect( () => {
if ( matchId ) {
fetchMatch( matchId ).then( data => {
setMatchData( data );
setLoading( false );
} );
} else {
setLoading( false );
}
}, [ matchId ] );
// Update preview value when data point selection changes
useEffect( () => {
if ( selectedDataPoint && matchData ) {
let value;
// Handle special date/time formatting
if ( selectedDataPoint === 'matchDate_date' ) {
const matchDate = matchData.matchDate;
if ( matchDate ) {
try {
const date = new Date( matchDate );
// Show date preview in browser locale
value = date.toLocaleDateString();
} catch ( e ) {
value = null;
}
}
} else if ( selectedDataPoint === 'matchDate_time' ) {
const matchDate = matchData.matchDate;
if ( matchDate ) {
try {
const date = new Date( matchDate );
// Show time preview in browser locale
value = date.toLocaleTimeString( undefined, { hour: '2-digit', minute: '2-digit' } );
} catch ( e ) {
value = null;
}
}
} else {
value = getNestedValue( matchData, selectedDataPoint );
}
// Convert boolean to readable text
if ( typeof value === 'boolean' ) {
setActualValue( value ? __( 'Ja', 'swi_foot_matchdata' ) : __( 'Nein', 'swi_foot_matchdata' ) );
} else if ( value === null || value === undefined ) {
setActualValue( '' );
} else {
setActualValue( String( value ) );
}
} else {
setActualValue( '' );
}
}, [ selectedDataPoint, matchData ] );
const handleInsertData = useCallback( () => {
const shortcode = generateDataShortcode( selectedDataPoint );
if ( shortcode ) {
onInsert( shortcode );
}
}, [ selectedDataPoint, onInsert ] );
const handleInsertShortcode = useCallback( () => {
const shortcode = generateShortcode( selectedShortcode );
if ( shortcode ) {
onInsert( shortcode );
}
}, [ selectedShortcode, onInsert ] );
if ( ! matchId ) {
return (
<Modal
title={ __( 'Insert Match Data', 'swi_foot_matchdata' ) }
onRequestClose={ onClose }
className="swi-foot-inline-data-modal"
>
<div style={ { padding: '16px', color: '#666' } }>
{ __( 'No match data available on this page. Please add match context first.', 'swi_foot_matchdata' ) }
</div>
</Modal>
);
}
return (
<Modal
title={ __( 'Insert Match Data', 'swi_foot_matchdata' ) }
onRequestClose={ onClose }
className="swi-foot-inline-data-modal"
>
<div style={ { marginBottom: '16px', display: 'flex', gap: '8px' } }>
<Button
variant={ tabMode === 'data-points' ? 'primary' : 'secondary' }
size="small"
onClick={ () => {
setTabMode( 'data-points' );
setSelectedShortcode( '' );
} }
>
{ __( 'Match Data', 'swi_foot_matchdata' ) }
</Button>
<Button
variant={ tabMode === 'shortcodes' ? 'primary' : 'secondary' }
size="small"
onClick={ () => {
setTabMode( 'shortcodes' );
setSelectedDataPoint( '' );
} }
>
{ __( 'Shortcodes', 'swi_foot_matchdata' ) }
</Button>
</div>
{ tabMode === 'data-points' ? (
<form
onSubmit={ e => {
e.preventDefault();
handleInsertData();
} }
>
<PanelRow>
<SelectControl
label={ __( 'Data Point', 'swi_foot_matchdata' ) }
value={ selectedDataPoint }
options={ INLINE_DATA_POINTS }
onChange={ setSelectedDataPoint }
help={ __( 'Select which match data to display', 'swi_foot_matchdata' ) }
/>
</PanelRow>
{ selectedDataPoint && (
<PanelRow style={ { marginTop: '16px', padding: '12px', backgroundColor: '#f5f5f5', borderRadius: '4px' } }>
<div>
<strong>{ __( 'Preview:', 'swi_foot_matchdata' ) }</strong>
{ loading ? (
<div style={ { marginTop: '8px' } }>
<Spinner />
</div>
) : (
<>
<div style={ { marginTop: '8px', padding: '8px', backgroundColor: '#fff', borderRadius: '2px', fontFamily: 'monospace', fontSize: '14px', fontWeight: 'bold' } }>
{ actualValue || __( '(no value)', 'swi_foot_matchdata' ) }
</div>
</>
) }
</div>
</PanelRow>
) }
<PanelRow style={ { marginTop: '16px' } }>
<Button variant="primary" type="submit" disabled={ ! selectedDataPoint || loading }>
{ __( 'Insert Data', 'swi_foot_matchdata' ) }
</Button>
</PanelRow>
</form>
) : (
<form
onSubmit={ e => {
e.preventDefault();
handleInsertShortcode();
} }
>
<PanelRow>
<SelectControl
label={ __( 'Shortcode', 'swi_foot_matchdata' ) }
value={ selectedShortcode }
options={ SHORTCODE_OPTIONS }
onChange={ setSelectedShortcode }
help={ __( 'Select a shortcode to insert (logos, etc.)', 'swi_foot_matchdata' ) }
/>
</PanelRow>
{ selectedShortcode && (
<PanelRow style={ { marginTop: '16px', padding: '12px', backgroundColor: '#f5f5f5', borderRadius: '4px' } }>
<div>
<strong>{ __( 'Shortcode:', 'swi_foot_matchdata' ) }</strong>
<div style={ { marginTop: '8px', padding: '8px', backgroundColor: '#fff', borderRadius: '2px', fontFamily: 'monospace', fontSize: '14px', fontWeight: 'bold' } }>
{ generateShortcode( selectedShortcode ) }
</div>
</div>
</PanelRow>
) }
<PanelRow style={ { marginTop: '16px' } }>
<Button variant="primary" type="submit" disabled={ ! selectedShortcode }>
{ __( 'Insert Shortcode', 'swi_foot_matchdata' ) }
</Button>
</PanelRow>
</form>
) }
</Modal>
);
}
/**
* Register format type with toolbar button for inline data insertion
* Shows a modal for selecting data point to insert
* Match is automatically taken from page/container context
*/
registerFormatType( 'swi-foot/shortcode-inserter', {
title: __( 'Insert Match Data', 'swi_foot_matchdata' ),
tagName: 'span',
className: 'swi-foot-shortcode-inserter',
edit: function FormatEdit( { value, onChange, isActive } ) {
const [ isModalOpen, setIsModalOpen ] = useState( false );
// Get post context for match data
const postContext = useSelect( select => {
const meta = select( 'core/editor' )?.getEditedPostAttribute?.( 'meta' ) || {};
let context = meta.swi_foot_context || {};
// Handle case where context might be stringified
if ( typeof context === 'string' ) {
try {
context = JSON.parse( context );
} catch ( e ) {
context = {};
}
}
return context;
} );
const handleInsertShortcode = useCallback( ( shortcode ) => {
// Insert the shortcode text directly at cursor position
const newValue = insert( value, shortcode );
onChange( newValue );
setIsModalOpen( false );
}, [ value, onChange ] );
return (
<>
<RichTextToolbarButton
icon={ <SoccerBallIcon /> }
title={ __( 'Insert Match Data', 'swi_foot_matchdata' ) }
onClick={ () => setIsModalOpen( true ) }
isActive={ isActive }
className="swi-foot-shortcode-btn"
/>
{ isModalOpen && (
<InlineDataModal
onInsert={ handleInsertShortcode }
onClose={ () => setIsModalOpen( false ) }
matchId={ postContext?.match_id || postContext?.matchId }
/>
) }
</>
);
},
} );

2
src/index.js Normal file
View File

@ -0,0 +1,2 @@
// Entry point for wp-scripts build
import './editor-blocks';

View File

@ -0,0 +1,107 @@
<?php
/**
* Plugin Name: Swiss Football Matchdata
* Description: WordPress plugin to consume Swiss Football Association API for team standings, schedules, and match details with shortcode support
* Version: 1.0.0
* Author: David Reindl (assisted by Claude AI)
* Text Domain: swi_foot_matchdata
* Domain Path: /languages
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Define plugin constants
define('SWI_FOOT_PLUGIN_URL', plugin_dir_url(__FILE__));
define('SWI_FOOT_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('SWI_FOOT_PLUGIN_VERSION', '1.0.0');
// Include required files
require_once SWI_FOOT_PLUGIN_PATH . 'includes/class-swi-foot-api.php';
require_once SWI_FOOT_PLUGIN_PATH . 'includes/class-swi-foot-admin.php';
require_once SWI_FOOT_PLUGIN_PATH . 'includes/class-swi-foot-blocks.php';
require_once SWI_FOOT_PLUGIN_PATH . 'includes/class-swi-foot-shortcodes.php';
require_once SWI_FOOT_PLUGIN_PATH . 'includes/class-swi-foot-rest.php';
class Swiss_Football_Matchdata {
public function __construct() {
add_action('init', array($this, 'init'));
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
add_action('plugins_loaded', array($this, 'load_textdomain'));
}
public function init() {
// Register post meta to store per-post context (season/team/match) for posts and pages
$meta_args = array(
'type' => 'object',
'single' => true,
'show_in_rest' => array(
'schema' => array(
'type' => 'object',
'properties' => array(
'season' => array('type' => array('string','integer')),
'team_id' => array('type' => array('string','integer')),
'match_id' => array('type' => array('string','integer')),
),
'additionalProperties' => true,
),
),
'auth_callback' => function( $allowed, $meta_key, $post_id, $user ) {
return current_user_can('edit_post', $post_id);
}
);
register_post_meta('post', 'swi_foot_context', $meta_args);
register_post_meta('page', 'swi_foot_context', $meta_args);
new Swi_Foot_API();
new Swi_Foot_Admin();
new Swi_Foot_Blocks();
new Swi_Foot_Shortcodes();
}
public function load_textdomain() {
load_plugin_textdomain(
'swi_foot_matchdata',
false,
dirname(plugin_basename(__FILE__)) . '/languages'
);
}
public function activate() {
// Create options with defaults
add_option('swi_foot_api_base_url', 'https://club-api-services.football.ch/api/v1');
add_option('swi_foot_api_username', '');
add_option('swi_foot_api_password', '');
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);
// Flush rewrite rules
flush_rewrite_rules();
}
public function deactivate() {
// Clean up transient cache data (do not remove finished/permanent records)
delete_transient('swi_foot_access_token');
delete_transient('swi_foot_teams');
$keys = get_transient('swi_foot_match_keys');
if (is_array($keys)) {
foreach ($keys as $mid) {
delete_transient('swi_foot_match_' . $mid);
}
}
delete_transient('swi_foot_match_keys');
// Flush rewrite rules
flush_rewrite_rules();
}
}
// Initialize the plugin
new Swiss_Football_Matchdata();
?>

85
test/register-check.js Normal file
View File

@ -0,0 +1,85 @@
// Simple Node-based headless-like test for built bundle
const path = require('path');
const fs = require('fs');
// capture registrations
const blockRegs = [];
const formatRegs = [];
const regsByName = {};
const formatsByName = {};
// Minimal fake implementations expected by the bundle
global.window = {
prompt: function (msg) {
return null;
},
wp: {
blocks: {
registerBlockType: function (name, settings) {
blockRegs.push(name);
regsByName[name] = settings || {};
},
getBlockType: function (name) { return null; }
},
element: {
createElement: function (tag, props) {
// return a serializable placeholder
const children = Array.prototype.slice.call(arguments, 2);
return { tag: tag, props: props || null, children: children };
},
useState: function () { return [null, function () {}]; },
useEffect: function () {}
},
blockEditor: {
InnerBlocks: function () {},
InspectorControls: function () {},
RichText: { applyFormat: null, toggleFormat: null },
BlockControls: function () {},
ToolbarButton: function () {}
},
components: {
PanelBody: function () {},
SelectControl: function () {},
Spinner: function () {},
TextControl: function () {},
RangeControl: function () {},
ToolbarButton: function () {}
},
richText: {
registerFormatType: function (name, settings) {
formatRegs.push(name);
formatsByName[name] = settings || {};
},
getFormatType: function (name) { return null; }
},
i18n: {
__: function (s, domain) { return s; }
},
data: {}
}
};
// require the built bundle
const bundlePath = path.resolve(__dirname, '../assets/build/index.js');
if (!fs.existsSync(bundlePath)) {
console.error('Built bundle not found at', bundlePath);
process.exit(2);
}
try {
require(bundlePath);
} catch (err) {
console.error('Error while executing bundle:', err && err.stack ? err.stack : err);
process.exit(3);
}
// report findings
const out = {
blockRegistrations: blockRegs,
formatRegistrations: formatRegs,
hasContextBlock: blockRegs.indexOf('swi-foot/context') !== -1,
hasInlineFormat: formatRegs.indexOf('swi-foot/inline-format') !== -1
};
console.log(JSON.stringify(out, null, 2));
process.exit(0);

28
uninstall.php Normal file
View File

@ -0,0 +1,28 @@
<?php
// If uninstall not called from WordPress, exit
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Remove plugin options
delete_option('swi_foot_api_base_url');
delete_option('swi_foot_api_username');
delete_option('swi_foot_api_password');
delete_option('swi_foot_verein_id');
delete_option('swi_foot_season_id');
delete_option('swi_foot_match_cache_duration');
// Remove transients and cached match data
delete_transient('swi_foot_access_token');
delete_transient('swi_foot_teams');
$keys = get_transient('swi_foot_match_keys');
if (is_array($keys)) {
foreach ($keys as $mid) {
delete_transient('swi_foot_match_' . $mid);
}
}
delete_transient('swi_foot_match_keys');
delete_transient('swi_foot_commons_ids');
// Remove permanently saved finished matches
delete_option('swi_foot_finished_matches');

10
webpack.config.js Normal file
View File

@ -0,0 +1,10 @@
const defaultConfig = require('@wordpress/scripts/config/webpack.config');
module.exports = {
...defaultConfig,
output: {
...defaultConfig.output,
// Ensure built editor bundle filename matches plugin expectation
filename: 'editor-blocks.js',
},
};