From fbd595aa3662c13c9922f6c63fe432a7b08e6de1 Mon Sep 17 00:00:00 2001 From: "Reindl David (IT-PTR-CEN2-SL10)" Date: Wed, 18 Mar 2026 19:58:24 +0100 Subject: [PATCH] initial state --- .gitignore | 78 ++ README-DEV.md | 506 +++++++++++ README.md | 135 +++ assets/admin.css | 328 ++++++++ assets/admin.js | 94 +++ assets/blocks.css | 381 +++++++++ assets/blocks.js | 297 +++++++ assets/post-context.js | 156 ++++ blocks/context/block.json | 17 + blocks/match-events/block.json | 16 + blocks/match-roster/block.json | 16 + blocks/schedule/block.json | 20 + blocks/shortcode-inserter/block.json | 36 + blocks/standings/block.json | 16 + blocks/team-data/block.json | 18 + includes/class-swi-foot-admin.php | 466 ++++++++++ includes/class-swi-foot-api.php | 559 ++++++++++++ includes/class-swi-foot-blocks.php | 479 +++++++++++ includes/class-swi-foot-rest.php | 399 +++++++++ includes/class-swi-foot-shortcodes.php | 1077 ++++++++++++++++++++++++ languages/swi_foot_matchdata-de_CH.mo | Bin 0 -> 2674 bytes languages/swi_foot_matchdata-de_CH.po | 705 ++++++++++++++++ languages/swi_foot_matchdata-en_US.mo | Bin 0 -> 2546 bytes languages/swi_foot_matchdata-en_US.po | 704 ++++++++++++++++ languages/swi_foot_matchdata-fr_FR.mo | Bin 0 -> 2871 bytes languages/swi_foot_matchdata-fr_FR.po | 704 ++++++++++++++++ languages/swi_foot_matchdata.pot | 678 +++++++++++++++ package.json | 12 + src/editor-blocks.js | 279 ++++++ src/format-shortcode.js | 405 +++++++++ src/index.js | 2 + swiss-football-matchdata.php | 107 +++ test/register-check.js | 85 ++ uninstall.php | 28 + webpack.config.js | 10 + 35 files changed, 8813 insertions(+) create mode 100644 .gitignore create mode 100644 README-DEV.md create mode 100644 README.md create mode 100644 assets/admin.css create mode 100644 assets/admin.js create mode 100644 assets/blocks.css create mode 100644 assets/blocks.js create mode 100644 assets/post-context.js create mode 100644 blocks/context/block.json create mode 100644 blocks/match-events/block.json create mode 100644 blocks/match-roster/block.json create mode 100644 blocks/schedule/block.json create mode 100644 blocks/shortcode-inserter/block.json create mode 100644 blocks/standings/block.json create mode 100644 blocks/team-data/block.json create mode 100644 includes/class-swi-foot-admin.php create mode 100644 includes/class-swi-foot-api.php create mode 100644 includes/class-swi-foot-blocks.php create mode 100644 includes/class-swi-foot-rest.php create mode 100644 includes/class-swi-foot-shortcodes.php create mode 100644 languages/swi_foot_matchdata-de_CH.mo create mode 100644 languages/swi_foot_matchdata-de_CH.po create mode 100644 languages/swi_foot_matchdata-en_US.mo create mode 100644 languages/swi_foot_matchdata-en_US.po create mode 100644 languages/swi_foot_matchdata-fr_FR.mo create mode 100644 languages/swi_foot_matchdata-fr_FR.po create mode 100644 languages/swi_foot_matchdata.pot create mode 100644 package.json create mode 100644 src/editor-blocks.js create mode 100644 src/format-shortcode.js create mode 100644 src/index.js create mode 100644 swiss-football-matchdata.php create mode 100644 test/register-check.js create mode 100644 uninstall.php create mode 100644 webpack.config.js 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} events - Array of event objects with matchMinute, eventTypeName, playerName, teamName + * @returns {void} + */ + function updateEventsList($container, events) { + const $timeline = $container.find('.events-timeline'); + if ($timeline.length === 0) return; + + // Build HTML for events + let html = ''; + if (events && events.length > 0) { + events.forEach(function(event) { + html += '
'; + html += '
' + (event.matchMinute || '') + '\'
'; + html += '
'; + html += '
' + (event.eventTypeName || '') + '
'; + html += '
'; + if (event.playerName) { + html += '' + event.playerName + ''; + } + if (event.teamName) { + html += '(' + event.teamName + ')'; + } + html += '
'; + }); + } else { + html = '

' + (window.swiFootNoEvents || 'No events recorded yet.') + '

'; + } + + $timeline.html(html); + } + + function setupShortcodeHelpers() { + // Add copy functionality to shortcode examples in admin + if ($('.shortcode-group code').length > 0) { + $('.shortcode-group code').each(function() { + const $code = $(this); + $code.css('cursor', 'pointer'); + $code.attr('title', 'Click to copy shortcode'); + + $code.on('click', function() { + const shortcode = $(this).text(); + copyToClipboard(shortcode); + }); + }); + } + } + + function copyToClipboard(text) { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(function() { + showNotification('Shortcode copied to clipboard!', 'success'); + }).catch(function() { + fallbackCopyTextToClipboard(text); + }); + } else { + fallbackCopyTextToClipboard(text); + } + } + + function fallbackCopyTextToClipboard(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + showNotification('Shortcode copied to clipboard!', 'success'); + } catch (err) { + showNotification('Failed to copy shortcode', 'error'); + } + + document.body.removeChild(textArea); + } + + function showNotification(message, type) { + const $notification = $('
' + message + '
'); + + $notification.css({ + position: 'fixed', + top: '20px', + right: '20px', + padding: '10px 15px', + borderRadius: '4px', + color: '#fff', + fontSize: '14px', + zIndex: '10000', + opacity: '0', + transform: 'translateY(-20px)', + transition: 'all 0.3s ease' + }); + + if (type === 'success') { + $notification.css('backgroundColor', '#28a745'); + } else { + $notification.css('backgroundColor', '#dc3545'); + } + + $('body').append($notification); + + // Animate in + setTimeout(function() { + $notification.css({ + opacity: '1', + transform: 'translateY(0)' + }); + }, 100); + + // Remove after 3 seconds + setTimeout(function() { + $notification.css({ + opacity: '0', + transform: 'translateY(-20px)' + }); + + setTimeout(function() { + $notification.remove(); + }, 300); + }, 3000); + } + + // Initialize tooltips for match elements + $(document).ready(function() { + $('.swi-foot-match-container [title]').each(function() { + const $element = $(this); + $element.on('mouseenter', function() { + // Simple tooltip implementation + const title = $element.attr('title'); + if (title) { + const $tooltip = $('
' + title + '
'); + $tooltip.css({ + position: 'absolute', + background: '#333', + color: '#fff', + padding: '5px 8px', + borderRadius: '4px', + fontSize: '12px', + zIndex: '1000', + whiteSpace: 'nowrap' + }); + + $('body').append($tooltip); + + const rect = this.getBoundingClientRect(); + $tooltip.css({ + left: rect.left + (rect.width / 2) - ($tooltip.outerWidth() / 2), + top: rect.top - $tooltip.outerHeight() - 5 + }); + } + }).on('mouseleave', function() { + $('.swi-foot-tooltip').remove(); + }); + }); + }); + +})(jQuery); diff --git a/assets/post-context.js b/assets/post-context.js new file mode 100644 index 0000000..fef7df4 --- /dev/null +++ b/assets/post-context.js @@ -0,0 +1,156 @@ +(function(wp){ + const { registerPlugin } = wp.plugins; + const PluginDocumentSettingPanel = (wp && wp.editor && wp.editor.PluginDocumentSettingPanel) ? wp.editor.PluginDocumentSettingPanel : (wp && wp.editPost && wp.editPost.PluginDocumentSettingPanel) ? wp.editPost.PluginDocumentSettingPanel : null; + const { withSelect, withDispatch } = wp.data; + const { useState, useEffect } = wp.element; + const { SelectControl, PanelRow, Spinner, Button } = wp.components; + + function PostContextPanel(props) { + const meta = (props.meta || {}); + const ctx = meta.swi_foot_context || {}; + + if (!PluginDocumentSettingPanel) return null; + + const [teams, setTeams] = useState([]); + const [matches, setMatches] = useState([]); + const [loadingTeams, setLoadingTeams] = useState(false); + const [loadingMatches, setLoadingMatches] = useState(false); + + const restBaseRaw = (window.swi_foot_post_context && window.swi_foot_post_context.rest_url) || '/wp-json/swi-foot/v1'; + const restBase = restBaseRaw.replace(/\/$/, ''); + + useEffect(() => { + setLoadingTeams(true); + fetch(restBase + '/teams', { credentials: 'same-origin', headers: { 'X-WP-Nonce': window.swi_foot_post_context.rest_nonce } }) + .then(r => r.json()).then(data => { + if (Array.isArray(data)) setTeams(data); + }).catch(() => {}).finally(() => setLoadingTeams(false)); + }, []); + + useEffect(() => { + const teamId = ctx.team_id || ''; + if (!teamId) { + setMatches([]); + return; + } + setLoadingMatches(true); + fetch(restBase + '/matches?team_id=' + encodeURIComponent(teamId), { credentials: 'same-origin', headers: { 'X-WP-Nonce': window.swi_foot_post_context.rest_nonce } }) + .then(r => r.json()).then(data => { + if (Array.isArray(data)) { + setMatches(data); + + // If no match selected yet, pick the next future match by date + try { + if (!ctx.match_id) { + const now = Date.now(); + let next = null; + data.forEach(m => { + if (!m.matchDate) return; + const t = Date.parse(m.matchDate); + if (isNaN(t)) return; + if (t > now) { + if (!next || t < next.time) next = { time: t, id: m.matchId }; + } + }); + if (next && next.id) { + updateContext('match_id', next.id); + } + } + } catch (e) { + // ignore + } + } + }).catch(() => {}).finally(() => setLoadingMatches(false)); + }, [ctx.team_id]); + + function updateContext(key, val) { + const newMeta = Object.assign({}, meta, { swi_foot_context: Object.assign({}, ctx, { [key]: val }) }); + props.editPost({ meta: newMeta }); + } + + const teamOptions = [{ value: '', label: '— use post default —' }].concat( + teams.map(t => ({ value: t.teamId, label: t.teamName + (t.teamLeagueName ? ' (' + t.teamLeagueName + ')' : '') })) + ); + + const matchOptions = [{ value: '', label: '— use team/season default —' }].concat( + matches.map(m => { + var datePart = m.matchDate ? (m.matchDate.substr(0,10) + ' — ') : ''; + var opposing = m.matchId; + var role = ''; + try { + var selectedTeam = (ctx.team_id || '').toString(); + var teamAId = (m.teamAId || m.homeTeamId || '').toString(); + var teamBId = (m.teamBId || m.awayTeamId || '').toString(); + var teamAName = m.teamNameA || m.homeTeamName || ''; + var teamBName = m.teamNameB || m.awayTeamName || ''; + if (selectedTeam && selectedTeam === teamAId) { + opposing = teamBName || teamAName || m.matchId; + role = 'home'; + } else if (selectedTeam && selectedTeam === teamBId) { + opposing = teamAName || teamBName || m.matchId; + role = 'away'; + } else { + opposing = teamBName || teamAName || m.teamName || m.matchId; + } + } catch (e) { + opposing = m.teamNameB || m.teamNameA || m.awayTeamName || m.homeTeamName || m.teamName || m.matchId; + } + var label = datePart + opposing + (role ? (' (' + role + ')') : ''); + return { value: m.matchId, label: label }; + }) + ); + + return wp.element.createElement( + PluginDocumentSettingPanel, + { title: 'Swiss Football Context', className: 'swi-foot-post-context-panel' }, + wp.element.createElement(PanelRow, null, + wp.element.createElement('div', { style: { width: '100%' } }, + wp.element.createElement('label', null, 'Season'), + wp.element.createElement('input', { + type: 'number', + value: ctx.season || window.swi_foot_post_context.default_season, + onChange: function(e){ updateContext('season', e.target.value); }, + style: { width: '100%' } + }) + ) + ), + wp.element.createElement(PanelRow, null, + wp.element.createElement('div', { style: { width: '100%' } }, + wp.element.createElement('label', null, 'Team'), + loadingTeams ? wp.element.createElement(Spinner, null) : wp.element.createElement(SelectControl, { + value: ctx.team_id || '', + options: teamOptions, + onChange: function(v){ updateContext('team_id', v); }, + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true + }) + ) + ), + wp.element.createElement(PanelRow, null, + wp.element.createElement('div', { style: { width: '100%' } }, + wp.element.createElement('label', null, 'Match'), + loadingMatches ? wp.element.createElement(Spinner, null) : wp.element.createElement(SelectControl, { + value: ctx.match_id || '', + options: matchOptions, + onChange: function(v){ updateContext('match_id', v); }, + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true + }) + ) + ), + wp.element.createElement(PanelRow, null, + wp.element.createElement('div', { style: { width: '100%', color: '#666', fontSize: '12px', marginTop: '6px' } }, + ctx.match_id ? ('Match ID: ' + ctx.match_id) : '' + ) + ), + wp.element.createElement(PanelRow, null, + wp.element.createElement(Button, { isSecondary: true, onClick: function(){ props.editPost({ meta: Object.assign({}, meta, { swi_foot_context: {} }) }); } }, 'Clear Context') + ) + ); + } + + const Connected = withSelect( (select) => ({ meta: select('core/editor').getEditedPostAttribute('meta') || {} }) )( withDispatch( (dispatch) => ({ editPost: dispatch('core/editor').editPost }) )( PostContextPanel ) ); + + registerPlugin('swi-foot-post-context', { render: Connected }); + +})(window.wp); diff --git a/blocks/context/block.json b/blocks/context/block.json new file mode 100644 index 0000000..53a7a81 --- /dev/null +++ b/blocks/context/block.json @@ -0,0 +1,17 @@ +{ + "apiVersion": 3, + "name": "swi-foot/context", + "title": "Swiss Football Context (container)", + "category": "widgets", + "icon": "admin-site", + "description": "Provides a team/season/match context to child blocks. Children will inherit these settings unless they override them.", + "providesContext": { + "swi-foot/context": "swi_foot_context" + }, + "attributes": { + "swi_foot_context": { + "type": "object", + "default": {} + } + } +} diff --git a/blocks/match-events/block.json b/blocks/match-events/block.json new file mode 100644 index 0000000..517f3ef --- /dev/null +++ b/blocks/match-events/block.json @@ -0,0 +1,16 @@ +{ + "apiVersion": 3, + "name": "swi-foot/match-events", + "title": "Swiss Football Match Events", + "category": "widgets", + "icon": "list-view", + "description": "Live match events with optional auto-refresh.", + "attributes": { + "selectedTeam": { "type": "string", "default": "" }, + "selectedMatch": { "type": "string", "default": "" }, + "refreshInterval": { "type": "number", "default": 30 }, + "eventOrder": { "type": "string", "default": "dynamic" } + } + , + "usesContext": ["swi-foot/context"] +} diff --git a/blocks/match-roster/block.json b/blocks/match-roster/block.json new file mode 100644 index 0000000..02cb7c3 --- /dev/null +++ b/blocks/match-roster/block.json @@ -0,0 +1,16 @@ +{ + "apiVersion": 3, + "name": "swi-foot/match-roster", + "title": "Swiss Football Match Roster", + "category": "widgets", + "icon": "groups", + "description": "Display match roster for a selected match and side.", + "attributes": { + "selectedTeam": { "type": "string", "default": "" }, + "selectedMatch": { "type": "string", "default": "" }, + "side": { "type": "string", "default": "home" }, + "withBench": { "type": "boolean", "default": false } + } + , + "usesContext": ["swi-foot/context"] +} diff --git a/blocks/schedule/block.json b/blocks/schedule/block.json new file mode 100644 index 0000000..faa240d --- /dev/null +++ b/blocks/schedule/block.json @@ -0,0 +1,20 @@ +{ + "apiVersion": 3, + "name": "swi-foot/schedule", + "title": "Swiss Football Schedule", + "category": "widgets", + "icon": "schedule", + "description": "Display upcoming matches for a team.", + "attributes": { + "teamId": { + "type": "string", + "default": "" + }, + "limit": { + "type": "number", + "default": 5 + } + } + , + "usesContext": ["swi-foot/context"] +} diff --git a/blocks/shortcode-inserter/block.json b/blocks/shortcode-inserter/block.json new file mode 100644 index 0000000..a01f48e --- /dev/null +++ b/blocks/shortcode-inserter/block.json @@ -0,0 +1,36 @@ +{ + "apiVersion": 3, + "name": "swi-foot/shortcode-inserter", + "title": "SWI Football Shortcode Inserter", + "category": "widgets", + "icon": "editor-code", + "description": "Insert shortcode-based match or team displays.", + "attributes": { + "shortcodeType": { + "type": "string", + "default": "match" + }, + "matchId": { + "type": "string", + "default": "" + }, + "teamId": { + "type": "string", + "default": "" + }, + "showNext": { + "type": "boolean", + "default": false + }, + "format": { + "type": "string", + "default": "" + }, + "separator": { + "type": "string", + "default": ":" + } + }, + "usesContext": ["swi-foot/context"] + ,"supports": { "html": false } +} diff --git a/blocks/standings/block.json b/blocks/standings/block.json new file mode 100644 index 0000000..1896573 --- /dev/null +++ b/blocks/standings/block.json @@ -0,0 +1,16 @@ +{ + "apiVersion": 3, + "name": "swi-foot/standings", + "title": "Swiss Football Standings", + "category": "widgets", + "icon": "analytics", + "description": "Display current standings for a team.", + "attributes": { + "teamId": { + "type": "string", + "default": "" + } + } + , + "usesContext": ["swi-foot/context"] +} diff --git a/blocks/team-data/block.json b/blocks/team-data/block.json new file mode 100644 index 0000000..2bb5c00 --- /dev/null +++ b/blocks/team-data/block.json @@ -0,0 +1,18 @@ +{ + "apiVersion": 3, + "name": "swi-foot/team-data", + "title": "Swiss Football Team Data", + "category": "widgets", + "icon": "universal-access-alt", + "description": "Guided team selector to insert shortcodes for various match/team displays.", + "attributes": { + "selectedTeam": { "type": "string", "default": "" }, + "dataType": { "type": "string", "default": "" }, + "selectedMatch": { "type": "string", "default": "" }, + "shortcodeType": { "type": "string", "default": "match" }, + "format": { "type": "string", "default": "" }, + "separator": { "type": "string", "default": ":" } + } + , + "usesContext": ["swi-foot/context"] +} diff --git a/includes/class-swi-foot-admin.php b/includes/class-swi-foot-admin.php new file mode 100644 index 0000000..570487d --- /dev/null +++ b/includes/class-swi-foot-admin.php @@ -0,0 +1,466 @@ +' . __('Configure your Swiss Football API credentials here.', 'swi_foot_matchdata') . '

'; + } + + 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) + { +?> +
+

+ +
+ + [swi_foot_match match_id="12345"] + [swi_foot_match team_id="67" show_next="true"] +
+ +
+ + [swi_foot_match_home_team match_id="12345"] + [swi_foot_match_away_team match_id="12345"] + [swi_foot_match_date match_id="12345" format="d.m.Y"] + [swi_foot_match_time match_id="12345" format="H:i"] + [swi_foot_match_venue match_id="12345"] + [swi_foot_match_score match_id="12345" separator=":"] + [swi_foot_match_status match_id="12345"] + [swi_foot_match_league match_id="12345"] + [swi_foot_match_round match_id="12345"] +
+ +
+ +
    +
  • match_id - Specific match ID
  • +
  • team_id - Team ID (for next match)
  • +
  • show_next - Show next match (true/false)
  • +
  • format - Date/time format
  • +
  • separator - Score separator
  • +
+
+ +

+
+ + +
+

+ +
+ +
+ +
+

+

+ + +

+
+ +
+

+

+ + +

+ +
+ display_teams(); ?> +
+
+ +
+

+

+ + +

+ display_cache_info(); ?> +
+ +
+

+ display_finished_matches(); ?> +
+ +
+

+
+
+

+


+ [swi_foot_match match_id="12345"] +

+ +


+ [swi_foot_match team_id="67" show_next="true"] +

+ +


+ [swi_foot_match_home_team match_id="12345"] [swi_foot_match_away_team match_id="12345"] [swi_foot_match_date match_id="12345"]

+
+
+
+
+ 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 '
'; + foreach ($teams as $team) { + echo '
'; + echo '' . esc_html($team['teamName'] ?? 'Unknown Team') . '
'; + echo 'ID: ' . esc_html($team['teamId'] ?? 'N/A') . '
'; + if (!empty($team['teamLeagueName'])) { + echo '' . esc_html($team['teamLeagueName']) . ''; + } + echo '
'; + } + 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 ' + + + + + + + + + '; + + 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 ' + + + + + + + '; + } + + echo '
' . __('Match ID', 'swi_foot_matchdata') . '' . __('Saved At', 'swi_foot_matchdata') . '' . __('Players', 'swi_foot_matchdata') . '' . __('Bench', 'swi_foot_matchdata') . '' . __('Events', 'swi_foot_matchdata') . '
' . esc_html($mid) . '' . esc_html($saved) . '' . esc_html($players) . '' . esc_html($bench) . '' . esc_html($events) . ' + +
'; + 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 '
' . __('Standings: No team provided in container context.', 'swi_foot_matchdata') . '
'; + } + + $api = new Swi_Foot_API(); + $standings = $api->get_standings($team_id); + + if (is_wp_error($standings)) { + return '
' . + sprintf(__('Error loading standings: %s', 'swi_foot_matchdata'), $standings->get_error_message()) . + '
'; + } + + if (empty($standings)) { + return '
' . __('No standings data available.', 'swi_foot_matchdata') . '
'; + } + + 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 '
' . __('Schedule: No team provided in container context.', 'swi_foot_matchdata') . '
'; + } + + $api = new Swi_Foot_API(); + $events = $api->get_schedule($team_id); + + if (is_wp_error($events)) { + return '
' . + sprintf(__('Error loading schedule: %s', 'swi_foot_matchdata'), $events->get_error_message()) . + '
'; + } + + // 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(); + ?> +
+

+ +

+ +
+ +
+
+ +
+
+ vs +
+ +
+ + +
+ +
+ +
+ +
+ 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 '' . $rendered . ''; + } + + /** + * 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 '
' . __('Match Roster: No match provided in container context.', 'swi_foot_matchdata') . '
'; + } + + // 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 '
' . __('Match Events: No match provided in container context.', 'swi_foot_matchdata') . '
'; + } + + // 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 '
' . + __('Please configure your API settings and ensure teams are available.', 'swi_foot_matchdata') . + '
'; + } + + 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 '' . __('Match data not available', 'swi_foot_matchdata') . ''; + } + + ob_start(); +?> +
+
+ + + + vs + + +
+
+
+ +
+ +
+ + +
+ : +
+ + +
+ + +
+ +
+
+ get_match_data($atts); + if (!$match_data || empty($match_data['teams'])) { + return ''; + } + + foreach ($match_data['teams'] as $team) { + if ($team['isHomeTeam']) { + return '' . esc_html($team['teamName']) . ''; + } + } + 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 '' . esc_html($team['teamName']) . ''; + } + } + 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 '' . date_i18n($atts['format'], strtotime($match_data['matchDate'])) . ''; + } + + 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 '' . date_i18n($atts['format'], strtotime($match_data['matchDate'])) . ''; + } + + public function match_venue_shortcode($atts) + { + $match_data = $this->get_match_data($atts); + if (!$match_data) { + return ''; + } + + $venue = $match_data['stadiumFieldName'] ?? ''; + if ($venue) { + return '' . esc_html($venue) . ''; + } + 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 '' . $score . ''; + } + + 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 '' . esc_html($status) . ''; + } + 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 '' . esc_html($league) . ''; + } + 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 '' . esc_html($round) . ''; + } + 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 '' . esc_html($value) . ''; + } + + /** + * 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 '' . __('Team ID required', 'swi_foot_matchdata') . ''; + } + + $standings = $this->api->get_standings($atts['team_id']); + + if (is_wp_error($standings) || empty($standings)) { + return '' . __('Standings data not available', 'swi_foot_matchdata') . ''; + } + + 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 '' . __('Parameter "side" must be "home" or "away"', 'swi_foot_matchdata') . ''; + } + + $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 '' . __('Match ID required', 'swi_foot_matchdata') . ''; + } + + // 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 '' . __('Roster data not available', 'swi_foot_matchdata') . ': ' . esc_html($players->get_error_message()) . ''; + } + $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 '' . __('Match ID required', 'swi_foot_matchdata') . ''; + } + + // 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 '' . __('Events data not available', 'swi_foot_matchdata') . ''; + } + + // 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(); + ?> +
+

+
+ + +
+
+ + ⏱ 0) { + echo '+' . esc_html($additionalMinute); + } + } + ?> + +
+
+
+
+ (' . esc_html($eventSubTypeName) . ')'; + } + ?> +
+ + + +
+
+ 👤 ' . esc_html($jerseyNumber . ' ' . $personName) . ''; + } + + // If event type is exchange (2), show substitute player + if ($eventTypeId == 2) { + $subJersey = $event['substitutePlayerJerseyNumber'] ?? ''; + $subName = $event['substitutePlayerName'] ?? ''; + if (!empty($subJersey) || !empty($subName)) { + echo ' ⇄ ' . esc_html($subJersey . ' ' . $subName) . ''; + } + } + ?> +
+
+
+ + +

+ +
+
+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 0000000000000000000000000000000000000000..7f8b2e9baeffaf9f75420fc8e56492087eec9a0e GIT binary patch literal 2674 zcmZ{lJ8T?97{><)AzTQL@O}gakfJ-0O)i##qDv66eRg7O$Cl5gB8uj2zuOzUH+!0y zy~AlxBqR!o2nh-bBqY)T9im9*0zpFu5(NmT5Tc`ij{i5iXZxIFl)az7d4BKwa`eE* z0^=oo&*3|9mk_7G^LOJ1<6rOr@IUYc@Zdc{JP#fR?+53=d%+d(8E^x90vv#kfbW9` z!B4-Ja<5dBj5$_QE;P_bt#X)mofhV$a;KL@+SBsc_#@;)FoyO06=Z+>4YKc! z+*jON2P=?Y2U(9dK-N=(tj9I*5cm<;1V06P;O`*6yNE@;{|d-*zm%_)@^$by=067? z0>1~@o}WPp{t4E>3XElY&Xv3j^8R;0*6TWmf8tC0@cC~*_Vc$O+w~X7?;M6PkAqX- zW8lS-mqDaTnB)C$fAA-ch5a0R9>?%uYsd+H2kI4Z0wq4y2`&u2VSlqPpT@_z0ha__ z*$+?QW54VjY~OgWj}f9UI1hOs#lqk`;DHq09n-4|az;6l8*58~g;nL;D6_GwP7emD z?kKM_6J~2+eZ!G8s!x;ai?oI1k=l~$r21lpOs6MiQ&Lt=r>UIRM!O!xa#{IKk6gJF zB-vD6$ytSYF`HXU#>}reOQBS8NQ$3^;veUDf z$f!8E-2?c93YpnB%73E6&|jMi=_LzJ0_%Z$9|{CW^RihR2_9PJXsUPp7630 zrAy}7CE`*hOCv2dL#bG9kw3}v-Ej~KbMEW5GKWmr)|rBl^TDJlJA4kzCRG)&RRaz2i>}Ql-r~ zWV^DC{A(NQ^U>MeeO4LQB^%9|PR8}qkZ0T4S3=>U%C(F3mARiRV=n%p%u&I)zk0_~ z=S1%-V~_?YH8E$M@>ko+YzmY+$u^-hP`5_51{xJC)9tT0V~hwgyhkPqB`;G#7>tBu`+2zH{$5i&rU;Xfo3H=QDF=&Y5q{+*_xQ zeIn3aMt=eQ!aYJ<0ypl(587YgDe!ObCGhxtLc9o`10Mjl!TZ5I@Hwy#J_Sy|$G{K4 zrftTL*gvzJl?OK+faKz;D2(VSfj5KEHyTKPSWX z8Ib)qz|&w07$a<`UtjBeb^%#S!$NM1b@d3zsd<(K3XHORSMc@;#H-i0Uu-^t*k15D` z+zk92WIetDIiK%A*5mhJpTObS{~XAAyb7`&P5iJPZIJc25%?a+dVC799$$m3#}C22 z1)@~)99aj*pRdQTcoLn@%Iyq#37s|II7p|sCs3k4g3fwyPP{+Y;j`$h1tiJO!ttL$ z=Q{a!D2F)=&Q*+rPH5OZx#UoKqNm;p>rn0Fs?M9TyUFG8uxiFPmvZ1_^ zb%k+JpIS?qmtC*2L}x=M8kCYJ(U>Eqj%E28&B%E<&=$Y5kxogb`hYITcw|jxQZq!# zsZ;|`wz61OlUcEh4zRnE%1du1VfmO-lG;g=4Gm%*-KuQT_m{H$&!d` zMutOLI3uY<&dFco@xkH}isTcP-Fagv$*0{ybD7Vm1Eo{7pHey4T~ZDu)M|+COgkr=#`t}erbo=8He@)|vTK;@7>8@U z90I9#bi6Jvd}>xMdswx&ZD^-N8c+Ivxl4m2;08p=~mu2xsqqUvh2vL;tnYt`!c zDq3krWy7f&VxF=a{cdTuwYz;7b>(8UgzPDyx``VZChkN&x|5va4_IIv$=F8^dof~{2`CC}{TwOBnzI6WN I{=YB%AK<~0A^-pY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2a649c6ab149ed5904e36b23364ede65dbbafe6b GIT binary patch literal 2871 zcma)-J!~9B6vrnJf;qkuzCsARgb&|^Tyn7#6kQS|$MyvaJGRbtDkL=Cox2;@+uh8} z+Hpfggd!>m3aCOwP6z=#NJv{CDhdR2AVf()L%~PG|IMy_{vt5a-tT7TWv7c>gxYIzB1+Iru30UxTdYXOQ)? z815eidHy+YKR6At-dDi;zz%o_%u2ok9zp+Okac_mJ^=nw_J0SzM}Gu|xgY!qWL>|3 z$G`)37J6O;+3sbK_Y1&h!D}G<<0FuDd;uz+>Pw@EP#alHY;s z=f6OnzaK$+2s{ez0r@}Y1>Eob33FZ-pOqVH$5NjYYzN0E;sg>N-jnl#W5oXEJUobp za|5pNXX1D~hKKWHb33xB!x|AH-#8DsA;o;-cyKFIy6BA0Ptyfy6)m-A1b618wX2B< zsXCsfkq)HON$lU&{P>bpCYF7*b^K+uhT&DYMvF@J#RU}yJuwj}X=pr(sHJ0Vdn%+^ z>4KiJ<&f{uq;!%dB;JdO%or6rT5vKBb=Cnj<;+@>5>+^L|jU!Jfy*7hze;0zcHi6gF3Bt28mLK5M%=tY65ww!WA&sWLxTZ>tr}i|dop zsV0pz#!lB8WA#&~>C{-W(RjXrR+*7;HK&4+zPOfR zmQz>V$uyEWK8tLZ#wvGyX|Yv5Gn{8+Y&*)-r{W;t`_rT|%i2{0QjbDK%Y4z{7BQPR z?x(ox{>9eI%q5_PjOkN2nmHY5i&JE{$#5uKl@IV<4vRxgb+GVJegW literal 0 HcmV?d00001 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} Match data object or null if request fails + */ +async function fetchMatch( matchId ) { + try { + const url = `${ wpApiSettings.root }swi-foot/v1/match/${ encodeURIComponent( matchId ) }`; + const response = await fetch( url, { + credentials: 'same-origin', + headers: { 'X-WP-Nonce': wpApiSettings.nonce }, + } ); + if ( ! response.ok ) return null; + return await response.json(); + } catch ( e ) { + console.warn( 'swi-foot: fetchMatch error', e ); + return null; + } +} + +/** + * Extract nested property values using slash-delimited path notation + * + * @param {Object} obj - The object to extract from + * @param {string} path - Path using slash notation (e.g., 'intermediateResults/scoreTeamA') + * @returns {*|null} The value at the path, or null if not found + */ +function getNestedValue( obj, path ) { + if ( ! obj || ! path ) return null; + const keys = path.split( '/' ); + let current = obj; + for ( const key of keys ) { + if ( ! current || typeof current !== 'object' ) return null; + current = current[ key ]; + if ( current === undefined ) return null; + } + return current; +} + +/** + * Generate inline data shortcode for match data insertion + * + * Format: [swi_foot_match_data data_point="fieldName"] + * Match ID is automatically retrieved from container context, not embedded in shortcode + * Supports nested fields via slash notation (e.g., intermediateResults/scoreTeamA) + * + * @param {string} dataPoint - The field name or path to extract + * @returns {string} Shortcode string, or empty string if input invalid + */ +function generateDataShortcode( dataPoint ) { + if ( ! dataPoint ) { + return ''; + } + return `[swi_foot_match_data data_point="${ dataPoint }"]`; +} + +/** + * Generate shortcode string for regular shortcodes (logos, etc.) + * + * Format: [shortcode_name] + * + * @param {string} shortcodeValue - The shortcode name/identifier + * @returns {string} Shortcode string, or empty string if input invalid + */ +function generateShortcode( shortcodeValue ) { + if ( ! shortcodeValue ) { + return ''; + } + return `[${ shortcodeValue }]`; +} + +/** + * Modal component for selecting and previewing match data or other shortcodes to insert + * + * Allows users to: + * - Choose between inserting match data or shortcodes (logos, etc.) + * - Select a data point or shortcode from available options + * - Preview the actual value for data points + * - Generate and insert the shortcode with one click + * + * @component + * @param {Object} props + * @param {Function} props.onInsert - Callback when shortcode is inserted (receives shortcode string) + * @param {Function} props.onClose - Callback to close the modal + * @param {string|null} props.matchId - The match ID (from page context), or null if unavailable + * @returns {React.ReactElement} Modal or empty state message + */ +function InlineDataModal( { onInsert, onClose, matchId } ) { + const [ tabMode, setTabMode ] = useState( 'data-points' ); + const [ selectedDataPoint, setSelectedDataPoint ] = useState( '' ); + const [ selectedShortcode, setSelectedShortcode ] = useState( '' ); + const [ matchData, setMatchData ] = useState( null ); + const [ loading, setLoading ] = useState( true ); + const [ actualValue, setActualValue ] = useState( '' ); + + // Fetch match data on mount + useEffect( () => { + if ( matchId ) { + fetchMatch( matchId ).then( data => { + setMatchData( data ); + setLoading( false ); + } ); + } else { + setLoading( false ); + } + }, [ matchId ] ); + + // Update preview value when data point selection changes + useEffect( () => { + if ( selectedDataPoint && matchData ) { + let value; + + // Handle special date/time formatting + if ( selectedDataPoint === 'matchDate_date' ) { + const matchDate = matchData.matchDate; + if ( matchDate ) { + try { + const date = new Date( matchDate ); + // Show date preview in browser locale + value = date.toLocaleDateString(); + } catch ( e ) { + value = null; + } + } + } else if ( selectedDataPoint === 'matchDate_time' ) { + const matchDate = matchData.matchDate; + if ( matchDate ) { + try { + const date = new Date( matchDate ); + // Show time preview in browser locale + value = date.toLocaleTimeString( undefined, { hour: '2-digit', minute: '2-digit' } ); + } catch ( e ) { + value = null; + } + } + } else { + value = getNestedValue( matchData, selectedDataPoint ); + } + + // Convert boolean to readable text + if ( typeof value === 'boolean' ) { + setActualValue( value ? __( 'Ja', 'swi_foot_matchdata' ) : __( 'Nein', 'swi_foot_matchdata' ) ); + } else if ( value === null || value === undefined ) { + setActualValue( '' ); + } else { + setActualValue( String( value ) ); + } + } else { + setActualValue( '' ); + } + }, [ selectedDataPoint, matchData ] ); + + const handleInsertData = useCallback( () => { + const shortcode = generateDataShortcode( selectedDataPoint ); + if ( shortcode ) { + onInsert( shortcode ); + } + }, [ selectedDataPoint, onInsert ] ); + + const handleInsertShortcode = useCallback( () => { + const shortcode = generateShortcode( selectedShortcode ); + if ( shortcode ) { + onInsert( shortcode ); + } + }, [ selectedShortcode, onInsert ] ); + + if ( ! matchId ) { + return ( + +
+ { __( 'No match data available on this page. Please add match context first.', 'swi_foot_matchdata' ) } +
+
+ ); + } + + return ( + +
+ + +
+ + { tabMode === 'data-points' ? ( +
{ + e.preventDefault(); + handleInsertData(); + } } + > + + + + + { selectedDataPoint && ( + +
+ { __( 'Preview:', 'swi_foot_matchdata' ) } + { loading ? ( +
+ +
+ ) : ( + <> +
+ { actualValue || __( '(no value)', 'swi_foot_matchdata' ) } +
+ + ) } +
+
+ ) } + + + + +
+ ) : ( +
{ + e.preventDefault(); + handleInsertShortcode(); + } } + > + + + + + { selectedShortcode && ( + +
+ { __( 'Shortcode:', 'swi_foot_matchdata' ) } +
+ { generateShortcode( selectedShortcode ) } +
+
+
+ ) } + + + + +
+ ) } +
+ ); +} + +/** + * Register format type with toolbar button for inline data insertion + * Shows a modal for selecting data point to insert + * Match is automatically taken from page/container context + */ +registerFormatType( 'swi-foot/shortcode-inserter', { + title: __( 'Insert Match Data', 'swi_foot_matchdata' ), + tagName: 'span', + className: 'swi-foot-shortcode-inserter', + edit: function FormatEdit( { value, onChange, isActive } ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + + // Get post context for match data + const postContext = useSelect( select => { + const meta = select( 'core/editor' )?.getEditedPostAttribute?.( 'meta' ) || {}; + let context = meta.swi_foot_context || {}; + + // Handle case where context might be stringified + if ( typeof context === 'string' ) { + try { + context = JSON.parse( context ); + } catch ( e ) { + context = {}; + } + } + + return context; + } ); + + const handleInsertShortcode = useCallback( ( shortcode ) => { + // Insert the shortcode text directly at cursor position + const newValue = insert( value, shortcode ); + onChange( newValue ); + setIsModalOpen( false ); + }, [ value, onChange ] ); + + return ( + <> + } + title={ __( 'Insert Match Data', 'swi_foot_matchdata' ) } + onClick={ () => setIsModalOpen( true ) } + isActive={ isActive } + className="swi-foot-shortcode-btn" + /> + { isModalOpen && ( + setIsModalOpen( false ) } + matchId={ postContext?.match_id || postContext?.matchId } + /> + ) } + + ); + }, +} ); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..19b1398 --- /dev/null +++ b/src/index.js @@ -0,0 +1,2 @@ +// Entry point for wp-scripts build +import './editor-blocks'; diff --git a/swiss-football-matchdata.php b/swiss-football-matchdata.php new file mode 100644 index 0000000..5bb4981 --- /dev/null +++ b/swiss-football-matchdata.php @@ -0,0 +1,107 @@ + 'object', + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'season' => array('type' => array('string','integer')), + 'team_id' => array('type' => array('string','integer')), + 'match_id' => array('type' => array('string','integer')), + ), + 'additionalProperties' => true, + ), + ), + 'auth_callback' => function( $allowed, $meta_key, $post_id, $user ) { + return current_user_can('edit_post', $post_id); + } + ); + + register_post_meta('post', 'swi_foot_context', $meta_args); + register_post_meta('page', 'swi_foot_context', $meta_args); + + new Swi_Foot_API(); + new Swi_Foot_Admin(); + new Swi_Foot_Blocks(); + new Swi_Foot_Shortcodes(); + } + + public function load_textdomain() { + load_plugin_textdomain( + 'swi_foot_matchdata', + false, + dirname(plugin_basename(__FILE__)) . '/languages' + ); + } + + public function activate() { + // Create options with defaults + add_option('swi_foot_api_base_url', 'https://club-api-services.football.ch/api/v1'); + add_option('swi_foot_api_username', ''); + add_option('swi_foot_api_password', ''); + add_option('swi_foot_verein_id', ''); + add_option('swi_foot_season_id', date('Y')); // Current year as default + add_option('swi_foot_match_cache_duration', 30); + + // Flush rewrite rules + flush_rewrite_rules(); + } + + public function deactivate() { + // Clean up transient cache data (do not remove finished/permanent records) + delete_transient('swi_foot_access_token'); + delete_transient('swi_foot_teams'); + $keys = get_transient('swi_foot_match_keys'); + if (is_array($keys)) { + foreach ($keys as $mid) { + delete_transient('swi_foot_match_' . $mid); + } + } + delete_transient('swi_foot_match_keys'); + + // Flush rewrite rules + flush_rewrite_rules(); + } +} + +// Initialize the plugin +new Swiss_Football_Matchdata(); +?> diff --git a/test/register-check.js b/test/register-check.js new file mode 100644 index 0000000..2fa16ae --- /dev/null +++ b/test/register-check.js @@ -0,0 +1,85 @@ +// Simple Node-based headless-like test for built bundle +const path = require('path'); +const fs = require('fs'); + +// capture registrations +const blockRegs = []; +const formatRegs = []; +const regsByName = {}; +const formatsByName = {}; + +// Minimal fake implementations expected by the bundle +global.window = { + prompt: function (msg) { + return null; + }, + wp: { + blocks: { + registerBlockType: function (name, settings) { + blockRegs.push(name); + regsByName[name] = settings || {}; + }, + getBlockType: function (name) { return null; } + }, + element: { + createElement: function (tag, props) { + // return a serializable placeholder + const children = Array.prototype.slice.call(arguments, 2); + return { tag: tag, props: props || null, children: children }; + }, + useState: function () { return [null, function () {}]; }, + useEffect: function () {} + }, + blockEditor: { + InnerBlocks: function () {}, + InspectorControls: function () {}, + RichText: { applyFormat: null, toggleFormat: null }, + BlockControls: function () {}, + ToolbarButton: function () {} + }, + components: { + PanelBody: function () {}, + SelectControl: function () {}, + Spinner: function () {}, + TextControl: function () {}, + RangeControl: function () {}, + ToolbarButton: function () {} + }, + richText: { + registerFormatType: function (name, settings) { + formatRegs.push(name); + formatsByName[name] = settings || {}; + }, + getFormatType: function (name) { return null; } + }, + i18n: { + __: function (s, domain) { return s; } + }, + data: {} + } +}; + +// require the built bundle +const bundlePath = path.resolve(__dirname, '../assets/build/index.js'); +if (!fs.existsSync(bundlePath)) { + console.error('Built bundle not found at', bundlePath); + process.exit(2); +} + +try { + require(bundlePath); +} catch (err) { + console.error('Error while executing bundle:', err && err.stack ? err.stack : err); + process.exit(3); +} + +// report findings +const out = { + blockRegistrations: blockRegs, + formatRegistrations: formatRegs, + hasContextBlock: blockRegs.indexOf('swi-foot/context') !== -1, + hasInlineFormat: formatRegs.indexOf('swi-foot/inline-format') !== -1 +}; + +console.log(JSON.stringify(out, null, 2)); +process.exit(0); diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..1d96021 --- /dev/null +++ b/uninstall.php @@ -0,0 +1,28 @@ +