commit fbd595aa3662c13c9922f6c63fe432a7b08e6de1
Author: Reindl David (IT-PTR-CEN2-SL10)
Date: Wed Mar 18 19:58:24 2026 +0100
initial state
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8285c4f
--- /dev/null
+++ b/.gitignore
@@ -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/
diff --git a/README-DEV.md b/README-DEV.md
new file mode 100644
index 0000000..8ec8ecc
--- /dev/null
+++ b/README-DEV.md
@@ -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 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`**: ~150–300 KB
+- **Typical unzipped size**: ~800 KB–2 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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d455359
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/assets/admin.css b/assets/admin.css
new file mode 100644
index 0000000..159a4d0
--- /dev/null
+++ b/assets/admin.css
@@ -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;
+ }
+}
diff --git a/assets/admin.js b/assets/admin.js
new file mode 100644
index 0000000..867da5b
--- /dev/null
+++ b/assets/admin.js
@@ -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() {
+ $('Settings saved! The plugin will automatically refresh API tokens as needed.
')
+ .insertAfter('.wrap h1');
+ });
+
+});
diff --git a/assets/blocks.css b/assets/blocks.css
new file mode 100644
index 0000000..284e22c
--- /dev/null
+++ b/assets/blocks.css
@@ -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;
+ }
+}
diff --git a/assets/blocks.js b/assets/blocks.js
new file mode 100644
index 0000000..6fa71e7
--- /dev/null
+++ b/assets/blocks.js
@@ -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
';
+ }
+
+ public function cache_section_callback()
+ {
+ echo '' . __('Configure caching settings for match data.', 'swi_foot_matchdata') . '
';
+ }
+
+ public function base_url_render()
+ {
+ $base_url = get_option('swi_foot_api_base_url', 'https://stg-club-api-services.football.ch');
+ echo '';
+ echo '' . __('The base URL for the Swiss Football API', 'swi_foot_matchdata') . '
';
+ }
+
+ public function username_render()
+ {
+ $username = get_option('swi_foot_api_username');
+ echo '';
+ echo '' . __('Your API application key', 'swi_foot_matchdata') . '
';
+ }
+
+ public function password_render()
+ {
+ $password = get_option('swi_foot_api_password');
+ echo '';
+ echo '' . __('Your API application password', 'swi_foot_matchdata') . '
';
+ }
+
+ public function verein_id_render()
+ {
+ $verein_id = get_option('swi_foot_verein_id');
+ echo '';
+ echo '' . __('Enter your club\'s Verein ID (Club ID)', 'swi_foot_matchdata') . '
';
+ }
+
+ public function season_id_render()
+ {
+ $season_id = get_option('swi_foot_season_id', date('Y'));
+ echo '';
+ echo '' . __('Current season ID (usually the year)', 'swi_foot_matchdata') . '
';
+ }
+
+ public function cache_duration_render()
+ {
+ $duration = get_option('swi_foot_match_cache_duration', 30);
+ echo '';
+ echo '' . __('How long to cache match data in seconds (10-300)', 'swi_foot_matchdata') . '
';
+ }
+
+ 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 "";
+ }
+
+ 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)
+ {
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ get_teams();
+
+ if (is_wp_error($teams)) {
+ echo '' .
+ sprintf(__('Error loading teams: %s', 'swi_foot_matchdata'), $teams->get_error_message()) .
+ '
';
+ return;
+ }
+
+ if (empty($teams)) {
+ echo '' .
+ __('No teams found. Please check your API configuration.', 'swi_foot_matchdata') .
+ '
';
+ return;
+ }
+
+ echo '' . __('Available Teams:', 'swi_foot_matchdata') . '
';
+ echo '';
+ }
+
+ 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 '' . sprintf(__('Currently caching %d match records with %d second cache duration.', 'swi_foot_matchdata'), $cache_count, $cache_duration) . '
';
+
+ 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 '' . __('Cache Range:', 'swi_foot_matchdata') . '
';
+ echo __('Oldest:', 'swi_foot_matchdata') . ' ' . date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $oldest_timestamp) . '
';
+ echo __('Newest:', 'swi_foot_matchdata') . ' ' . date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $newest_timestamp) . '
';
+ }
+ }
+ }
+
+ private function display_finished_matches()
+ {
+ $finished = get_option('swi_foot_finished_matches', array());
+ if (empty($finished)) {
+ echo '' . __('No finished match data stored.', 'swi_foot_matchdata') . '
';
+ return;
+ }
+
+ echo '';
+ echo '
+
+ | ' . __('Match ID', 'swi_foot_matchdata') . ' |
+ ' . __('Saved At', 'swi_foot_matchdata') . ' |
+ ' . __('Players', 'swi_foot_matchdata') . ' |
+ ' . __('Bench', 'swi_foot_matchdata') . ' |
+ ' . __('Events', 'swi_foot_matchdata') . ' |
+ |
+
+ ';
+
+ 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 '
+ | ' . esc_html($mid) . ' |
+ ' . esc_html($saved) . ' |
+ ' . esc_html($players) . ' |
+ ' . esc_html($bench) . ' |
+ ' . esc_html($events) . ' |
+
+
+ |
+
';
+ }
+
+ echo '
';
+ echo '';
+
+ // Include inline JS for actions
+ ?>
+
+
\ No newline at end of file
diff --git a/includes/class-swi-foot-api.php b/includes/class-swi-foot-api.php
new file mode 100644
index 0000000..3c93248
--- /dev/null
+++ b/includes/class-swi-foot-api.php
@@ -0,0 +1,559 @@
+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
+ );
+}
+
diff --git a/includes/class-swi-foot-blocks.php b/includes/class-swi-foot-blocks.php
new file mode 100644
index 0000000..6771cc1
--- /dev/null
+++ b/includes/class-swi-foot-blocks.php
@@ -0,0 +1,479 @@
+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 '';
+ }
+
+ $api = new Swi_Foot_API();
+ $standings = $api->get_standings($team_id);
+
+ if (is_wp_error($standings)) {
+ return '';
+ }
+
+ if (empty($standings)) {
+ return '';
+ }
+
+ ob_start();
+?>
+
+
+
+
+ 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 '';
+ }
+
+ $api = new Swi_Foot_API();
+ $events = $api->get_schedule($team_id);
+
+ if (is_wp_error($events)) {
+ return '';
+ }
+
+ // 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();
+ ?>
+
+ 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 '';
+ }
+
+ /**
+ * 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 '';
+ }
+
+ // 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 '';
+ }
+
+ // 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 '';
+ }
+
+ ob_start();
+ ?>
+
+
\ No newline at end of file
diff --git a/includes/class-swi-foot-rest.php b/includes/class-swi-foot-rest.php
new file mode 100644
index 0000000..876c412
--- /dev/null
+++ b/includes/class-swi-foot-rest.php
@@ -0,0 +1,399 @@
+ 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\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/
+ register_rest_route('swi-foot/v1', '/match/(?P[^/]+)', 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/
+ // Requires authentication to prevent abuse from non-logged-in users
+ register_rest_route('swi-foot/v1', '/events/(?P[^/]+)', 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[^/]+)', 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/
+ */
+ 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/
+ *
+ * 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();
diff --git a/includes/class-swi-foot-shortcodes.php b/includes/class-swi-foot-shortcodes.php
new file mode 100644
index 0000000..220d3e0
--- /dev/null
+++ b/includes/class-swi-foot-shortcodes.php
@@ -0,0 +1,1077 @@
+api = new Swi_Foot_API();
+
+ // Register shortcodes
+ add_shortcode('swi_foot_match', array($this, 'match_shortcode'));
+ add_shortcode('swi_foot_match_home_team', array($this, 'match_home_team_shortcode'));
+ add_shortcode('swi_foot_match_away_team', array($this, 'match_away_team_shortcode'));
+ add_shortcode('swi_foot_match_date', array($this, 'match_date_shortcode'));
+ add_shortcode('swi_foot_match_time', array($this, 'match_time_shortcode'));
+ add_shortcode('swi_foot_match_venue', array($this, 'match_venue_shortcode'));
+ add_shortcode('swi_foot_match_score', array($this, 'match_score_shortcode'));
+ add_shortcode('swi_foot_match_status', array($this, 'match_status_shortcode'));
+ add_shortcode('swi_foot_match_league', array($this, 'match_league_shortcode'));
+ add_shortcode('swi_foot_match_round', array($this, 'match_round_shortcode'));
+ add_shortcode('swi_foot_standings', array($this, 'standings_shortcode'));
+ add_shortcode('swi_foot_roster', array($this, 'roster_shortcode'));
+ add_shortcode('swi_foot_events', array($this, 'events_shortcode'));
+ add_shortcode('swi_foot_match_data', array($this, 'match_data_shortcode'));
+ add_shortcode('swi_foot_match_home_team_logo', array($this, 'match_home_team_logo_shortcode'));
+ add_shortcode('swi_foot_match_away_team_logo', array($this, 'match_away_team_logo_shortcode'));
+ add_shortcode('swi_foot_match_home_team_logo_url', array($this, 'match_home_team_logo_url_shortcode'));
+ add_shortcode('swi_foot_match_away_team_logo_url', array($this, 'match_away_team_logo_url_shortcode'));
+
+ // Register query variable for team logo requests and handle image serving
+ add_filter('query_vars', array($this, 'register_query_vars'));
+ add_action('template_redirect', array($this, 'handle_team_logo_request'));
+ }
+
+ /**
+ * Register custom query variables
+ */
+ public function register_query_vars($vars)
+ {
+ $vars[] = 'swi_foot_team_logo';
+ $vars[] = 'position';
+ return $vars;
+ }
+
+ /**
+ * Handle internal requests for team logo images
+ * URL format: /?swi_foot_team_logo=team_id&position=home|away|event
+ * Uses template_redirect hook which fires after WordPress determines the query
+ * but before loading the template/theme
+ */
+ public function handle_team_logo_request()
+ {
+ $team_logo_id = get_query_var('swi_foot_team_logo');
+
+ error_log('Swiss Football: handle_team_logo_request() called, team_logo_id from get_query_var: ' . var_export($team_logo_id, true));
+
+ if (empty($team_logo_id)) {
+ error_log('Swiss Football: team_logo_id is empty, skipping image serving');
+ return; // Not a team logo request, continue normal WordPress flow
+ }
+
+ error_log('Swiss Football: Processing team logo request for team_id: ' . $team_logo_id);
+
+ // This is a team logo request, serve the image and exit
+ $this->serve_team_logo_image($team_logo_id);
+ exit; // Never reached, but for clarity
+ }
+
+ /**
+ * Serve team logo image from API base64 data
+ */
+ private function serve_team_logo_image($team_id)
+ {
+ error_log('Swiss Football: serve_team_logo_image() called with team_id: ' . $team_id . ', type: ' . gettype($team_id));
+
+ // Validate team_id is numeric
+ if (!ctype_digit((string)$team_id) || empty($team_id)) {
+ error_log('Swiss Football: Invalid team ID provided: ' . var_export($team_id, true));
+ wp_die('Invalid team ID', 'Invalid Request', array('response' => 400));
+ }
+
+ error_log('Swiss Football: team_id validation passed, team_id: ' . $team_id);
+ error_log('Swiss Football: Attempting to fetch team picture for team_id: ' . $team_id);
+
+ // Fetch image from API
+ $image_data = $this->api->get_team_picture($team_id);
+
+ error_log('Swiss Football: API response type: ' . gettype($image_data));
+ if (is_wp_error($image_data)) {
+ error_log('Swiss Football: API returned WP_Error: ' . $image_data->get_error_message());
+ } else {
+ error_log('Swiss Football: API response length: ' . strlen(var_export($image_data, true)) . ', is_array: ' . (is_array($image_data) ? 'yes' : 'no') . ', is_string: ' . (is_string($image_data) ? 'yes' : 'no'));
+ if (is_string($image_data)) {
+ error_log('Swiss Football: String response first 200 chars: ' . substr($image_data, 0, 200));
+ }
+ }
+
+ if (is_wp_error($image_data)) {
+ error_log('Swiss Football: API error fetching team picture: ' . $image_data->get_error_message());
+ wp_die('Image not found', 'Not Found', array('response' => 404));
+ }
+
+ if (empty($image_data)) {
+ error_log('Swiss Football: Team picture returned empty for team_id: ' . $team_id);
+ wp_die('Image not found', 'Not Found', array('response' => 404));
+ }
+
+ error_log('Swiss Football: Successfully fetched image data for team_id: ' . $team_id . ', length: ' . strlen($image_data));
+
+ // Handle API response that may be an array (if JSON returned a structure)
+ // or a string (if JSON returned a plain string)
+ if (is_array($image_data)) {
+ error_log('Swiss Football: Image data is array, attempting to extract base64 from common fields');
+ // Try to find the base64 data in common field names
+ $potential_fields = array('picture', 'logo', 'image', 'data', 'url', 'href');
+ $found_data = null;
+ foreach ($potential_fields as $field) {
+ if (isset($image_data[$field]) && !empty($image_data[$field])) {
+ $found_data = $image_data[$field];
+ error_log('Swiss Football: Found image data in array field: ' . $field);
+ break;
+ }
+ }
+
+ if (!$found_data) {
+ error_log('Swiss Football: Could not find image data in array, array keys: ' . implode(', ', array_keys($image_data)));
+ wp_die('Image not found', 'Not Found', array('response' => 404));
+ }
+
+ $image_data = $found_data;
+ error_log('Swiss Football: Extracted base64 from array, length: ' . strlen($image_data));
+ }
+
+ // At this point, $image_data should be a string (base64 encoded)
+
+ // The API returns base64 encoded image data
+ // Detect image format from the data or default to PNG
+ $mime_type = 'image/png';
+ if (strpos($image_data, 'data:image/') === 0) {
+ // Already has data URI format, extract mime type
+ preg_match('/data:([^;]+)/', $image_data, $matches);
+ if (isset($matches[1])) {
+ $mime_type = $matches[1];
+ // Remove data URI prefix to get just base64
+ $image_data = preg_replace('/^data:[^;]+;base64,/', '', $image_data);
+ }
+ } elseif (strpos($image_data, '/') === false && strlen($image_data) > 100) {
+ // Looks like raw base64, assume PNG
+ $mime_type = 'image/png';
+ }
+
+ // Decode base64
+ $decoded = base64_decode(trim($image_data), true);
+ if ($decoded === false) {
+ error_log('Swiss Football: Failed to decode base64 image data for team_id: ' . $team_id);
+ wp_die('Invalid image data', 'Bad Request', array('response' => 400));
+ }
+
+ error_log('Swiss Football: Successfully decoded image, size: ' . strlen($decoded) . ' bytes, mime: ' . $mime_type);
+
+ // Set headers for image serving
+ header('Content-Type: ' . $mime_type);
+ header('Content-Length: ' . strlen($decoded));
+ header('Cache-Control: public, max-age=2592000'); // 30 days
+ header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 2592000) . ' GMT');
+ header('Access-Control-Allow-Origin: *');
+
+ // Output image and exit
+ echo $decoded;
+ exit;
+ }
+
+ /**
+ * Generate internal URL for serving team logo image
+ *
+ * @param string $team_id The team ID
+ * @param string $position 'home' or 'away'
+ * @return string URL to internal team logo endpoint
+ */
+ private function get_team_logo_url($team_id, $position = 'home')
+ {
+ return add_query_arg(array(
+ 'swi_foot_team_logo' => $team_id,
+ 'position' => $position
+ ), home_url('/'));
+ }
+
+ public function match_shortcode($atts)
+ {
+ $atts = shortcode_atts(array(
+ 'match_id' => '',
+ 'team_id' => '',
+ 'show_next' => 'false'
+ ), $atts);
+
+ $match_data = $this->get_match_data($atts);
+
+ if (!$match_data) {
+ return '';
+ }
+
+ ob_start();
+?>
+
+ get_match_data($atts);
+ if (!$match_data || empty($match_data['teams'])) {
+ return '';
+ }
+
+ foreach ($match_data['teams'] as $team) {
+ if ($team['isHomeTeam']) {
+ return '';
+ }
+ }
+ return '';
+ }
+
+ public function match_away_team_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data || empty($match_data['teams'])) {
+ return '';
+ }
+
+ foreach ($match_data['teams'] as $team) {
+ if (!$team['isHomeTeam']) {
+ return '';
+ }
+ }
+ return '';
+ }
+
+ public function match_date_shortcode($atts)
+ {
+ $atts = shortcode_atts(array(
+ 'match_id' => '',
+ 'team_id' => '',
+ 'show_next' => 'false',
+ 'format' => get_option('date_format', 'd.m.Y')
+ ), $atts);
+
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data || empty($match_data['matchDate'])) {
+ return '';
+ }
+
+ return '';
+ }
+
+ public function match_time_shortcode($atts)
+ {
+ $atts = shortcode_atts(array(
+ 'match_id' => '',
+ 'team_id' => '',
+ 'show_next' => 'false',
+ 'format' => get_option('time_format', 'H:i')
+ ), $atts);
+
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data || empty($match_data['matchDate'])) {
+ return '';
+ }
+
+ return '';
+ }
+
+ public function match_venue_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data) {
+ return '';
+ }
+
+ $venue = $match_data['stadiumFieldName'] ?? '';
+ if ($venue) {
+ return '';
+ }
+ return '';
+ }
+
+ public function match_score_shortcode($atts)
+ {
+ $atts = shortcode_atts(array(
+ 'match_id' => '',
+ 'team_id' => '',
+ 'show_next' => 'false',
+ 'separator' => ':'
+ ), $atts);
+
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data) {
+ return '';
+ }
+
+ if ($match_data['hasMatchStarted'] || $match_data['hasMatchEnded']) {
+ $score = esc_html($match_data['scoreTeamA']) . ' ' . $atts['separator'] . ' ' . esc_html($match_data['scoreTeamB']);
+ return '';
+ }
+
+ return '';
+ }
+
+ public function match_status_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data) {
+ return '';
+ }
+
+ $status = $match_data['matchStateName'] ?? '';
+ if ($status) {
+ return '';
+ }
+ return '';
+ }
+
+ public function match_league_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data) {
+ return '';
+ }
+
+ $league = $match_data['leagueName'] ?? '';
+ if ($league) {
+ return '';
+ }
+ return '';
+ }
+
+ public function match_round_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data) {
+ return '';
+ }
+
+ $round = $match_data['roundNbr'] ?? '';
+ if ($round) {
+ return '';
+ }
+ return '';
+ }
+
+ /**
+ * Generic inline data point shortcode
+ * Extracts a specific field from match data and displays it inline
+ * Usage: [swi_foot_match_data data_point="teamNameA"]
+ * Match ID is automatically picked up from container context if not specified
+ * Special data points: matchDate_date and matchDate_time (formatted per WordPress locale)
+ */
+ public function match_data_shortcode($atts)
+ {
+ $atts = shortcode_atts(array(
+ 'match_id' => '',
+ 'team_id' => '',
+ 'data_point' => '',
+ ), $atts);
+
+ // If match_id not provided, get it from post context
+ if (empty($atts['match_id'])) {
+ $context = function_exists('swi_foot_resolve_context') ? swi_foot_resolve_context(get_the_ID()) : array();
+ $atts['match_id'] = $context['match_id'] ?? '';
+ }
+
+ if (empty($atts['match_id']) || empty($atts['data_point'])) {
+ return '';
+ }
+
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data) {
+ return '';
+ }
+
+ // Normalize match data to ensure consistent field names
+ $match_data = $this->normalize_shortcode_match_data($match_data);
+
+ // Handle special date/time formatting
+ if ($atts['data_point'] === 'matchDate_date') {
+ $value = $this->format_match_date($match_data);
+ } elseif ($atts['data_point'] === 'matchDate_time') {
+ $value = $this->format_match_time($match_data);
+ } else {
+ // Support nested data points like "intermediateResults/scoreTeamA"
+ $value = $this->get_nested_value($match_data, $atts['data_point']);
+ }
+
+ if ($value === null || $value === '') {
+ return '';
+ }
+
+ // Convert boolean values to readable text
+ if (is_bool($value)) {
+ $value = $value ? __('Ja', 'swi_foot_matchdata') : __('Nein', 'swi_foot_matchdata');
+ }
+
+ return '';
+ }
+
+ /**
+ * Normalize match data to ensure consistent field names
+ * Extracts team names from teams array if not present as direct fields
+ * @param array $match Raw match data
+ * @return array Normalized match data
+ */
+ private function normalize_shortcode_match_data($match)
+ {
+ // If already has teamNameA/teamNameB as direct fields, return as-is
+ if (!empty($match['teamNameA']) || !empty($match['teamNameB'])) {
+ return $match;
+ }
+
+ // Extract team names from teams array if not present as direct fields
+ if (!empty($match['teams']) && is_array($match['teams'])) {
+ foreach ($match['teams'] as $team) {
+ if (!empty($team['isHomeTeam']) && !empty($team['teamName'])) {
+ $match['teamNameA'] = $team['teamName'];
+ } elseif (empty($team['isHomeTeam']) && !empty($team['teamName'])) {
+ $match['teamNameB'] = $team['teamName'];
+ }
+ }
+ }
+
+ // Normalize stadium field name
+ if (empty($match['stadiumFieldName']) && !empty($match['stadiumPlaygroundName'])) {
+ $match['stadiumFieldName'] = $match['stadiumPlaygroundName'];
+ }
+
+ return $match;
+ }
+
+ /**
+ * Helper to get nested array values using dot/slash notation
+ * @param array $array
+ * @param string $path e.g. "intermediateResults/scoreTeamA"
+ * @return mixed
+ */
+ private function get_nested_value($array, $path)
+ {
+ if (!is_array($array) || empty($path)) {
+ return null;
+ }
+
+ $keys = explode('/', $path);
+ $current = $array;
+
+ foreach ($keys as $key) {
+ if (!is_array($current) || !isset($current[$key])) {
+ return null;
+ }
+ $current = $current[$key];
+ }
+
+ return $current;
+ }
+
+ /**
+ * Format match date according to WordPress locale settings
+ * Extracts date part from matchDate and formats using WordPress date format
+ * @param array $match Match data array
+ * @return string|null Formatted date or null if matchDate not available
+ */
+ private function format_match_date($match)
+ {
+ if (empty($match['matchDate'])) {
+ return null;
+ }
+
+ // Parse the matchDate string - handle multiple formats
+ $timestamp = strtotime($match['matchDate']);
+ if ($timestamp === false) {
+ return null;
+ }
+
+ // Use WordPress date format setting
+ $date_format = get_option('date_format');
+ return wp_date($date_format, $timestamp);
+ }
+
+ /**
+ * Format match time according to WordPress locale settings
+ * Extracts time part from matchDate and formats using WordPress time format
+ * @param array $match Match data array
+ * @return string|null Formatted time or null if matchDate not available
+ */
+ private function format_match_time($match)
+ {
+ if (empty($match['matchDate'])) {
+ return null;
+ }
+
+ // Parse the matchDate string - handle multiple formats
+ $timestamp = strtotime($match['matchDate']);
+ if ($timestamp === false) {
+ return null;
+ }
+
+ // Use WordPress time format setting
+ $time_format = get_option('time_format');
+ return wp_date($time_format, $timestamp);
+ }
+
+ private function get_match_data($atts)
+ {
+ $atts = shortcode_atts(array(
+ 'match_id' => '',
+ 'team_id' => '',
+ 'show_next' => 'false'
+ ), $atts);
+
+ $match_id = $atts['match_id'];
+
+ // If no match_id provided explicitly, try to resolve it from the post/page context
+ if (empty($match_id)) {
+ if (function_exists('swi_foot_resolve_context')) {
+ $ctx = swi_foot_resolve_context();
+ if (!empty($ctx['match_id'])) {
+ $match_id = $ctx['match_id'];
+ } elseif (empty($match_id) && !empty($ctx['team_id']) && $atts['show_next'] === 'true') {
+ // Use schedule to find next match for the team when show_next requested
+ $schedule = $this->api->get_schedule($ctx['team_id']);
+ if (!is_wp_error($schedule) && !empty($schedule)) {
+ foreach ($schedule as $match) {
+ if (strtotime($match['matchDate']) >= time()) {
+ $match_id = $match['matchId'];
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // If no match_id but show_next is true, try to find next match for team
+ if (empty($match_id) && $atts['show_next'] === 'true' && !empty($atts['team_id'])) {
+ $schedule = $this->api->get_schedule($atts['team_id']);
+ if (!is_wp_error($schedule) && !empty($schedule)) {
+ // Find next upcoming match
+ foreach ($schedule as $match) {
+ if (strtotime($match['matchDate']) >= time()) {
+ $match_id = $match['matchId'];
+ break;
+ }
+ }
+ }
+ }
+
+ if (empty($match_id)) {
+ return null;
+ }
+
+ // Try to get cached data first
+ $match_data = $this->api->get_cached_match_data($match_id);
+
+ // If no cached data, fetch from API
+ if (!$match_data) {
+ $match_data = $this->api->get_match_details($match_id);
+ if (is_wp_error($match_data)) {
+ return null;
+ }
+ }
+
+ return $match_data;
+ }
+
+ public function standings_shortcode($atts)
+ {
+ $atts = shortcode_atts(array(
+ 'team_id' => ''
+ ), $atts);
+
+ if (empty($atts['team_id'])) {
+ return '';
+ }
+
+ $standings = $this->api->get_standings($atts['team_id']);
+
+ if (is_wp_error($standings) || empty($standings)) {
+ return '';
+ }
+
+ ob_start();
+ ?>
+
+
+
+
+ '',
+ 'team_id' => '',
+ 'show_current' => 'false',
+ 'side' => '',
+ 'with_bench' => 'false'
+ ), $atts);
+
+ $side = strtolower(trim($atts['side']));
+ if (!in_array($side, array('home', 'away'), true)) {
+ return '';
+ }
+
+ $match_id = $atts['match_id'];
+ if ($atts['show_current'] === 'true' && !empty($atts['team_id'])) {
+ $current_match = $this->api->get_current_match($atts['team_id']);
+ if ($current_match) {
+ $match_id = $current_match['matchId'];
+ }
+ }
+
+ if (empty($match_id)) {
+ return '';
+ }
+
+ // First check if we have saved final data
+ $saved = $this->api->get_finished_match_data($match_id);
+ if ($saved) {
+ $players = $saved['roster']['players'] ?? array();
+ $bench_players = $saved['roster']['bench'] ?? array();
+ } else {
+ // Live fetch
+ $players = $this->api->get_match_players($match_id);
+ if (is_wp_error($players)) {
+ return '';
+ }
+ $bench_players = array();
+
+ if (strtolower($atts['with_bench']) === 'true') {
+ $bench = $this->api->get_match_bench($match_id);
+ if (!is_wp_error($bench)) {
+ $bench_players = $bench;
+ }
+ }
+
+ // Check match ended & possibly save events + roster together
+ $match_details = $this->api->get_match_details($match_id);
+ if (!is_wp_error($match_details) && !empty($match_details['hasMatchEnded'])) {
+ $events = $this->api->get_match_events($match_id);
+ if (is_wp_error($events)) {
+ $events = array();
+ }
+ // Only save if we have valid player data (not a WP_Error)
+ if (is_array($players)) {
+ $this->api->save_finished_match_data(
+ $match_id,
+ array('players' => $players, 'bench' => $bench_players),
+ $events
+ );
+ }
+ }
+ }
+
+ // Filter roster for side
+ $filtered_players = array_filter($players, function ($p) use ($side) {
+ return $side === 'home' ? !empty($p['isHomeTeam']) : empty($p['isHomeTeam']);
+ });
+ $filtered_bench = array_filter($bench_players, function ($p) use ($side) {
+ return $side === 'home' ? !empty($p['isHomeTeam']) : empty($p['isHomeTeam']);
+ });
+
+ ob_start();
+ ?>
+
+ '',
+ 'team_id' => '',
+ 'show_current' => 'false',
+ 'refresh_interval' => '30',
+ 'event_order' => 'dynamic'
+ ), $atts);
+
+ error_log('[SWI_FOOT_EVENTS_DEBUG] PARSED ATTRIBUTES: ' . json_encode($atts, JSON_UNESCAPED_SLASHES));
+ error_log('[SWI_FOOT_EVENTS_DEBUG] EVENT_ORDER VALUE: ' . var_export($atts['event_order'], true));
+
+ $match_id = $atts['match_id'];
+ if ($atts['show_current'] === 'true' && !empty($atts['team_id'])) {
+ $current_match = $this->api->get_current_match($atts['team_id']);
+ if ($current_match) {
+ $match_id = $current_match['matchId'];
+ }
+ }
+
+ if (empty($match_id)) {
+ return '';
+ }
+
+ // Try saved first
+ $saved = $this->api->get_finished_match_data($match_id);
+ if ($saved) {
+ $events = $saved['events'] ?? array();
+ } else {
+ $events = $this->api->get_match_events($match_id);
+ if (is_wp_error($events)) {
+ return '';
+ }
+
+ // Check if match ended and store both data sets
+ $match_details = $this->api->get_match_details($match_id);
+ if (!is_wp_error($match_details) && !empty($match_details['hasMatchEnded'])) {
+ $players = $this->api->get_match_players($match_id);
+ $bench = $this->api->get_match_bench($match_id);
+ if (is_wp_error($players)) $players = array();
+ if (is_wp_error($bench)) $bench = array();
+
+ $this->api->save_finished_match_data(
+ $match_id,
+ array('players' => $players, 'bench' => $bench),
+ $events
+ );
+ }
+ }
+
+ // Determine event sort order based on setting and match status
+ $event_order = $atts['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
+ $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_details = $this->api->get_match_details($match_id);
+ $match_has_ended = !is_wp_error($match_details) && !empty($match_details['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'];
+ });
+ }
+ }
+
+ ob_start();
+ ?>
+
+get_match_data($atts);
+ if (!$match_data || empty($match_data['teams']) || empty($match_data['teams'][0])) {
+ return '';
+ }
+
+ $home_team_id = $match_data['teams'][0]['teamId'] ?? '';
+ if (empty($home_team_id)) {
+ return '';
+ }
+
+ $logo_url = $this->get_team_logo_url($home_team_id, 'home');
+ $team_name = esc_attr($match_data['teams'][0]['teamName'] ?? 'Home Team');
+ return '';
+ }
+
+ public function match_away_team_logo_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (!$match_data || empty($match_data['teams']) || empty($match_data['teams'][1])) {
+ return '';
+ }
+
+ $away_team_id = $match_data['teams'][1]['teamId'] ?? '';
+ if (empty($away_team_id)) {
+ return '';
+ }
+
+ $logo_url = $this->get_team_logo_url($away_team_id, 'away');
+ $team_name = esc_attr($match_data['teams'][1]['teamName'] ?? 'Away Team');
+ return '';
+ }
+
+ /**
+ * Shortcode: Display home team logo URL only (for use in image blocks)
+ * Usage: [swi_foot_match_home_team_logo_url]
+ * Returns: Internal URL that serves the team logo image
+ */
+ public function match_home_team_logo_url_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (empty($match_data)) {
+ return '';
+ }
+
+ $home_team_id = $match_data['teams'][0]['teamId'] ?? '';
+ if (empty($home_team_id)) {
+ return '';
+ }
+
+ return $this->get_team_logo_url($home_team_id, 'home');
+ }
+
+ /**
+ * Shortcode: Display away team logo URL only (for use in image blocks)
+ * Usage: [swi_foot_match_away_team_logo_url]
+ * Returns: Internal URL that serves the team logo image
+ */
+ public function match_away_team_logo_url_shortcode($atts)
+ {
+ $match_data = $this->get_match_data($atts);
+ if (empty($match_data)) {
+ return '';
+ }
+
+ $away_team_id = $match_data['teams'][1]['teamId'] ?? '';
+ if (empty($away_team_id)) {
+ return '';
+ }
+
+ return $this->get_team_logo_url($away_team_id, 'away');
+ }
+
+
+
+ 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', 'wp-data', 'jquery'),
+ SWI_FOOT_PLUGIN_VERSION,
+ true
+ );
+
+ 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')
+ ));
+ }
+}
+
+/**
+ * Replace saved inline shortcode spans with their rendered shortcode output on the front-end.
+ * Spans are inserted by the editor format and contain a `data-shortcode` attribute.
+ */
+function swi_foot_render_inline_shortcodes($content)
+{
+ if (stripos($content, 'swi-foot-inline-shortcode') === false) {
+ return $content;
+ }
+
+ return preg_replace_callback(
+ '/]*class=["\']?[^"\'>]*swi-foot-inline-shortcode[^"\'>]*["\']?[^>]*data-shortcode=["\']([^"\']+)["\'][^>]*>.*?<\/span>/is',
+ function ($m) {
+ $sc = html_entity_decode($m[1]);
+ return do_shortcode($sc);
+ },
+ $content
+ );
+}
+
+add_filter('the_content', 'swi_foot_render_inline_shortcodes', 11);
+?>
\ No newline at end of file
diff --git a/languages/swi_foot_matchdata-de_CH.mo b/languages/swi_foot_matchdata-de_CH.mo
new file mode 100644
index 0000000..7f8b2e9
Binary files /dev/null and b/languages/swi_foot_matchdata-de_CH.mo differ
diff --git a/languages/swi_foot_matchdata-de_CH.po b/languages/swi_foot_matchdata-de_CH.po
new file mode 100644
index 0000000..d92c1b4
--- /dev/null
+++ b/languages/swi_foot_matchdata-de_CH.po
@@ -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."
diff --git a/languages/swi_foot_matchdata-en_US.mo b/languages/swi_foot_matchdata-en_US.mo
new file mode 100644
index 0000000..f4c4287
Binary files /dev/null and b/languages/swi_foot_matchdata-en_US.mo differ
diff --git a/languages/swi_foot_matchdata-en_US.po b/languages/swi_foot_matchdata-en_US.po
new file mode 100644
index 0000000..e01a5c8
--- /dev/null
+++ b/languages/swi_foot_matchdata-en_US.po
@@ -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."
diff --git a/languages/swi_foot_matchdata-fr_FR.mo b/languages/swi_foot_matchdata-fr_FR.mo
new file mode 100644
index 0000000..2a649c6
Binary files /dev/null and b/languages/swi_foot_matchdata-fr_FR.mo differ
diff --git a/languages/swi_foot_matchdata-fr_FR.po b/languages/swi_foot_matchdata-fr_FR.po
new file mode 100644
index 0000000..84148e7
--- /dev/null
+++ b/languages/swi_foot_matchdata-fr_FR.po
@@ -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."
diff --git a/languages/swi_foot_matchdata.pot b/languages/swi_foot_matchdata.pot
new file mode 100644
index 0000000..0a277d3
--- /dev/null
+++ b/languages/swi_foot_matchdata.pot
@@ -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 , 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 \n"
+"Language-Team: LANGUAGE \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 ""
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2c18f68
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/src/editor-blocks.js b/src/editor-blocks.js
new file mode 100644
index 0000000..fcb6301
--- /dev/null
+++ b/src/editor-blocks.js
@@ -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 (
+
+
+
+ {__('Team is inherited from the container context.', 'swi_foot_matchdata')}
+
+
+
{__('Standings will render on the front-end.', 'swi_foot_matchdata')}
+
+ );
+ },
+ 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 (
+
+
+
+ {__('Team is inherited from the container context.', 'swi_foot_matchdata')}
+ setAttributes({ limit: val })} min={1} max={20} />
+
+
+
{__('Upcoming matches will render on the front-end.', 'swi_foot_matchdata')}
+
+ );
+ },
+ 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 (
+
+
+
+ {loadingTeams ? : (
+ ({value:t.value,label:t.label})))}
+ onChange={val=>setAttributes({selectedTeam:val, dataType:'', selectedMatch:''})} />)}
+
+ {selectedTeam && (
+ setAttributes({dataType:val, selectedMatch:''})} />
+ )}
+
+
+
{__('Team data shortcode generator', 'swi_foot_matchdata')}
+
+ );
+ },
+ 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 (
+
+
+
+ {__('Match and team are inherited from the container context.', 'swi_foot_matchdata')}
+ setAttributes({side:v})}
+ />
+ setAttributes({withBench:v})}
+ />
+
+
+
{__('Match roster will render on the front-end.', 'swi_foot_matchdata')}
+
+ );
+ },
+ 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 (
+
+
+
+ {__('Match and team are inherited from the container context.', 'swi_foot_matchdata')}
+ setAttributes({refreshInterval:v})}
+ min={10}
+ max={300}
+ step={10}
+ />
+ 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')}
+ />
+
+
+
{__('Live events will be displayed on the front-end.', 'swi_foot_matchdata')}
+
+ );
+ },
+ 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);
+ }
+});
+
diff --git a/src/format-shortcode.js b/src/format-shortcode.js
new file mode 100644
index 0000000..839cf05
--- /dev/null
+++ b/src/format-shortcode.js
@@ -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 = () => (
+
+);
+
+// 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