mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-10-28 07:31:16 +00:00
dev day #65,5
This commit is contained in:
1 parent
9ccca365b3
commit
68e69e00cc
41 files changed
+981
-1376
No files matched your search
@@ -0,0 +1,94 @@
|
|||||||
|
# Blog Match Link Fix
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
When creating a new blog article with a match link in the admin panel, the match link was not being saved to the database. The article was created successfully, but the association with the FACR match was lost.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
The match linking logic was placed in the React Query mutation's `onSuccess` callback. When the page re-rendered after article creation (especially when invalidating queries), a React error #310 was occurring, which interrupted the `onSuccess` callback before the match linking API call could complete.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Moved the match linking logic from the mutation's `onSuccess` callback directly into the `onSubmit` function. This ensures the match linking happens synchronously after article creation, before the modal closes and queries are invalidated.
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
|
||||||
|
#### 1. Match Linking in `onSubmit` Function
|
||||||
|
**File**: `frontend/src/pages/admin/ArticlesAdminPage.tsx`
|
||||||
|
|
||||||
|
- For **new articles**: After `createArticle()` completes, immediately call `putArticleMatchLink()` with the new article ID
|
||||||
|
- For **existing articles**: After `updateArticle()` completes, call `putArticleMatchLink()` to update or create the link
|
||||||
|
- All match linking now happens within the same try-catch block as article creation/update
|
||||||
|
- Modal only closes after all operations complete successfully
|
||||||
|
|
||||||
|
#### 2. Simplified Mutation Callbacks
|
||||||
|
- `createMut.onSuccess`: Only handles query invalidation and state cleanup
|
||||||
|
- `updateMut.onSuccess`: Only handles query invalidation
|
||||||
|
- Removed duplicate match linking code from callbacks
|
||||||
|
- Success toasts moved to `onSubmit` after all operations complete
|
||||||
|
|
||||||
|
#### 3. Enhanced Error Handling
|
||||||
|
- Added try-catch blocks around match linking API calls
|
||||||
|
- Separate error messages for article creation vs. match linking failures
|
||||||
|
- If article creation succeeds but match linking fails, user gets a warning toast with instructions
|
||||||
|
- All errors logged to console for debugging
|
||||||
|
|
||||||
|
#### 4. Added Debug Logging
|
||||||
|
- Log match link state before submission
|
||||||
|
- Log article creation success
|
||||||
|
- Log match linking attempts and results
|
||||||
|
- Helps diagnose issues in production
|
||||||
|
|
||||||
|
#### 5. Fixed `MatchLinkBadge` Loading State
|
||||||
|
- Added loading state check to prevent React errors when refetching
|
||||||
|
- Shows "Načítání..." badge while match link data is loading
|
||||||
|
|
||||||
|
## Testing Instructions
|
||||||
|
|
||||||
|
### Test 1: Create New Article with Match Link
|
||||||
|
1. Go to Admin → Články
|
||||||
|
2. Click "Nový článek"
|
||||||
|
3. Fill in required fields (Název, Kategorie)
|
||||||
|
4. Switch to "Základní" tab
|
||||||
|
5. Select a match from the picker
|
||||||
|
6. Click "Uložit"
|
||||||
|
7. **Expected**: Toast shows "Článek vytvořen a propojen se zápasem"
|
||||||
|
8. **Verify**: Article list shows match badge with match details
|
||||||
|
|
||||||
|
### Test 2: Create Article Without Match Link
|
||||||
|
1. Create article without selecting a match
|
||||||
|
2. **Expected**: Toast shows "Článek byl úspěšně vytvořen"
|
||||||
|
3. **Verify**: Article list shows "Nepropojeno" badge
|
||||||
|
|
||||||
|
### Test 3: Update Existing Article Match Link
|
||||||
|
1. Open existing article for editing
|
||||||
|
2. Select a different match
|
||||||
|
3. Click "Uložit"
|
||||||
|
4. **Expected**: Toast shows "Článek aktualizován a propojen se zápasem"
|
||||||
|
5. **Verify**: Badge updates to show new match
|
||||||
|
|
||||||
|
### Test 4: Match Linking Failure (Backend Error)
|
||||||
|
1. Simulate backend error (e.g., stop backend)
|
||||||
|
2. Try to create article with match link
|
||||||
|
3. **Expected**: Toast shows "Článek vytvořen, ale propojení se zápasem selhalo"
|
||||||
|
4. **Verify**: Article is created but without match link
|
||||||
|
|
||||||
|
## API Endpoints Used
|
||||||
|
- `POST /api/v1/articles` - Create article
|
||||||
|
- `PUT /api/v1/articles/:id` - Update article
|
||||||
|
- `POST /api/v1/articles/:id/match-link` - Create/update match link
|
||||||
|
- `GET /api/v1/articles/:id/match-link` - Get match link (for badge display)
|
||||||
|
- `DELETE /api/v1/articles/:id/match-link` - Delete match link
|
||||||
|
|
||||||
|
## Database Tables
|
||||||
|
- `articles` - Main article data
|
||||||
|
- `article_match_links` - Junction table linking articles to FACR match IDs
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
- `tempMatchLink` - Stores selected match ID for new articles
|
||||||
|
- `matchIdInput` - Stores selected match ID (UI input)
|
||||||
|
- `linkedMatchId` - Stores confirmed linked match ID after successful save
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
- Consider adding optimistic updates to show match link immediately
|
||||||
|
- Add bulk match linking for multiple articles
|
||||||
|
- Show match preview in article form before saving
|
||||||
|
- Add match link history/audit log
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# Blog Match Link Fix - Verification Checklist
|
||||||
|
|
||||||
|
## Issue Summary
|
||||||
|
**Problem**: When creating a blog article and selecting a match from the "Propojit se zápasem" section, the match ID was not being saved to the database.
|
||||||
|
|
||||||
|
**User Evidence**: Console log showed:
|
||||||
|
```javascript
|
||||||
|
Saving article with payload: {
|
||||||
|
"title": "U17: Rýmařov...",
|
||||||
|
"category_name": "KALMAN TRADE Krajský přebor mladší dorost",
|
||||||
|
// ... other fields ...
|
||||||
|
// ❌ NO match_id field in payload
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fix Applied
|
||||||
|
|
||||||
|
### Technical Changes
|
||||||
|
1. **Moved match linking from async callback to synchronous flow**
|
||||||
|
- Previous: Match linking happened in `createMut.onSuccess` callback (after article creation)
|
||||||
|
- New: Match linking happens in `onSubmit` function (immediately after article creation completes)
|
||||||
|
- Benefit: Prevents race conditions and React error interruptions
|
||||||
|
|
||||||
|
2. **Added comprehensive error handling**
|
||||||
|
- Separate try-catch blocks for article save and match linking
|
||||||
|
- Clear user feedback for each operation
|
||||||
|
- Logging at each step for debugging
|
||||||
|
|
||||||
|
3. **Fixed React error #310**
|
||||||
|
- Added loading state to `MatchLinkBadge` component
|
||||||
|
- Prevents rendering errors during query refetch
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
### 1. Check Console Logs
|
||||||
|
After applying the fix, when creating an article with a match link, you should see:
|
||||||
|
```
|
||||||
|
Match link state before submit: {
|
||||||
|
tempMatchLink: "12345",
|
||||||
|
matchIdInput: "12345",
|
||||||
|
linkedMatchId: "",
|
||||||
|
isNewArticle: true
|
||||||
|
}
|
||||||
|
Linking new article 42 with match 12345
|
||||||
|
Match link created for new article
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Check Toast Messages
|
||||||
|
- ✅ Success: "Článek vytvořen a propojen se zápasem" (with Match ID)
|
||||||
|
- ⚠️ Partial Success: "Článek vytvořen, ale propojení se zápasem selhalo"
|
||||||
|
|
||||||
|
### 3. Check Database
|
||||||
|
Query to verify match link was saved:
|
||||||
|
```sql
|
||||||
|
SELECT * FROM article_match_links WHERE article_id = [NEW_ARTICLE_ID];
|
||||||
|
```
|
||||||
|
Should return:
|
||||||
|
- `article_id`: The ID of the newly created article
|
||||||
|
- `external_match_id`: The FACR match ID (e.g., "12345")
|
||||||
|
- `title`: The article title
|
||||||
|
|
||||||
|
### 4. Check Article List UI
|
||||||
|
- Badge should show: "Zápas: [Home Team] [Score] [Away Team]" in green (if match has score) or yellow (if no score yet)
|
||||||
|
- Should NOT show: "Nepropojeno" in gray
|
||||||
|
|
||||||
|
### 5. Check API Response
|
||||||
|
The article creation response should include match_link:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"title": "U17: Rýmařov...",
|
||||||
|
"match_link": {
|
||||||
|
"article_id": 42,
|
||||||
|
"external_match_id": "12345",
|
||||||
|
"title": "U17: Rýmařov..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### Scenario A: New Article with Match
|
||||||
|
1. Open Admin → Články → Nový článek
|
||||||
|
2. Fill in title: "Test článek"
|
||||||
|
3. Select category: "KALMAN TRADE Krajský přebor mladší dorost"
|
||||||
|
4. Click on a match in the match picker
|
||||||
|
5. Click "Uložit"
|
||||||
|
6. **Expected**: Green toast with match ID
|
||||||
|
7. **Verify**: List shows match badge with team names
|
||||||
|
|
||||||
|
### Scenario B: New Article without Match
|
||||||
|
1. Create article without selecting match
|
||||||
|
2. **Expected**: Standard success toast
|
||||||
|
3. **Verify**: List shows "Nepropojeno" badge
|
||||||
|
|
||||||
|
### Scenario C: Edit Existing Article, Add Match
|
||||||
|
1. Open existing article
|
||||||
|
2. Select a match
|
||||||
|
3. Click "Uložit"
|
||||||
|
4. **Expected**: Success toast with match info
|
||||||
|
5. **Verify**: Badge updates immediately
|
||||||
|
|
||||||
|
### Scenario D: Edit Existing Article, Change Match
|
||||||
|
1. Open article that already has a match
|
||||||
|
2. Select different match
|
||||||
|
3. Click "Uložit"
|
||||||
|
4. **Expected**: Success toast
|
||||||
|
5. **Verify**: Badge shows new match
|
||||||
|
|
||||||
|
### Scenario E: Backend Error Handling
|
||||||
|
1. Stop backend server
|
||||||
|
2. Try to create article with match
|
||||||
|
3. **Expected**: Article created locally, warning toast about match linking failure
|
||||||
|
4. Restart backend
|
||||||
|
5. **Verify**: Can manually link match by editing article
|
||||||
|
|
||||||
|
## Code Changes Summary
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
1. `/frontend/src/pages/admin/ArticlesAdminPage.tsx`
|
||||||
|
- Lines 40-43: Added loading state to MatchLinkBadge
|
||||||
|
- Lines 655-673: Simplified createMut.onSuccess
|
||||||
|
- Lines 686-702: Simplified updateMut.onSuccess
|
||||||
|
- Lines 928-987: Refactored onSubmit with inline match linking
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
1. `/DOCS/BLOG_MATCH_LINK_FIX.md` - Detailed fix documentation
|
||||||
|
2. `/DOCS/BLOG_MATCH_LINK_VERIFICATION.md` - This file
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
If issues occur, revert the ArticlesAdminPage.tsx changes:
|
||||||
|
```bash
|
||||||
|
git checkout HEAD -- frontend/src/pages/admin/ArticlesAdminPage.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
- No performance degradation
|
||||||
|
- Actually improved: One less async operation in mutation callback
|
||||||
|
- Better user experience: Immediate feedback on match linking status
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
- No new browser APIs used
|
||||||
|
- Compatible with all modern browsers (Chrome, Firefox, Safari, Edge)
|
||||||
|
- React Query handles all async state management
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
- Match linking requires article to be saved first (can't preview match before save)
|
||||||
|
- If backend is down, match link won't be saved (fails gracefully)
|
||||||
|
- Maximum 100 matches shown in picker (performance optimization)
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
- `DOCS/BLOG_CREATION_FIXED.md` - Previous blog creation fixes
|
||||||
|
- `DOCS/FACR_INTEGRATION.md` - FACR match data integration
|
||||||
|
- `DOCS/ADMIN_QUICK_REFERENCE.md` - Admin panel overview
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# PDF Preview and Poll Creation Fix
|
||||||
|
|
||||||
|
## Issues Fixed
|
||||||
|
|
||||||
|
### 1. PDF Preview Not Working (Blank Screen)
|
||||||
|
**Problem**: When trying to preview PDF files, the screen was blank due to Content Security Policy (CSP) restrictions blocking iframe embedding.
|
||||||
|
|
||||||
|
**Root Cause**: CSP header `frame-ancestors 'self'` prevented PDF files from being embedded in iframes.
|
||||||
|
|
||||||
|
**Solution**: Enhanced `FilePreview.tsx` component with multiple fallback options:
|
||||||
|
- Primary: Direct iframe embed (works if CSP allows)
|
||||||
|
- Fallback buttons:
|
||||||
|
- Open in new window
|
||||||
|
- View with Mozilla PDF.js
|
||||||
|
- View via Google Docs Viewer
|
||||||
|
- Download PDF
|
||||||
|
|
||||||
|
### 2. Poll Creation Requires Saved Article
|
||||||
|
**Problem**: Users couldn't create or link polls to articles until the article was saved first. The UI showed "Nejprve uložte článek" (Save article first).
|
||||||
|
|
||||||
|
**Root Cause**: `PollLinker` component requires an `articleId` which only exists after the article is saved.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Added "Save as draft and add polls" button
|
||||||
|
- Modified `onSubmit` function to support `keepOpen` option
|
||||||
|
- After saving, modal stays open and switches to Poll tab automatically
|
||||||
|
- Article is saved as draft (published=true by default, but can be unpublished)
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### `/frontend/src/components/common/FilePreview.tsx`
|
||||||
|
**Lines changed**: 124-189
|
||||||
|
|
||||||
|
**What changed**:
|
||||||
|
- Wrapped PDF iframe in a VStack with fallback options
|
||||||
|
- Added 4 alternative viewing methods
|
||||||
|
- Added helpful message when PDF doesn't display
|
||||||
|
- Added error handler to iframe
|
||||||
|
|
||||||
|
**Key improvements**:
|
||||||
|
```tsx
|
||||||
|
// Before: Simple iframe only
|
||||||
|
<iframe src={`${fullUrl}#view=FitH`} />
|
||||||
|
|
||||||
|
// After: Iframe + fallback buttons
|
||||||
|
<VStack>
|
||||||
|
<Box>
|
||||||
|
<iframe src={`${fullUrl}#view=FitH&toolbar=1`} />
|
||||||
|
</Box>
|
||||||
|
<HStack>
|
||||||
|
<Button href={fullUrl}>Open in new window</Button>
|
||||||
|
<Button href={pdfjs_url}>View with PDF.js</Button>
|
||||||
|
<Button href={google_viewer_url}>View via Google</Button>
|
||||||
|
<Button download>Download PDF</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/frontend/src/pages/admin/ArticlesAdminPage.tsx`
|
||||||
|
**Lines changed**:
|
||||||
|
- 852: Modified `onSubmit` function signature
|
||||||
|
- 994-997: Added conditional modal closing
|
||||||
|
- 1840-1870: Enhanced Poll tab UI
|
||||||
|
|
||||||
|
**What changed**:
|
||||||
|
1. **Modified `onSubmit` function**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const onSubmit = async () => { ... }
|
||||||
|
|
||||||
|
// After
|
||||||
|
const onSubmit = async (options: { keepOpen?: boolean } = {}) => {
|
||||||
|
// ... save logic ...
|
||||||
|
if (!options.keepOpen) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Updated Poll tab**:
|
||||||
|
- Changed from "info" alert to "warning" alert
|
||||||
|
- Added "Save as draft and add polls" button
|
||||||
|
- Button calls `onSubmit({ keepOpen: true })`
|
||||||
|
- After save, switches to Poll tab: `setActiveTabIndex(5)`
|
||||||
|
|
||||||
|
## Testing Instructions
|
||||||
|
|
||||||
|
### Test 1: PDF Preview
|
||||||
|
1. **Upload PDF to article**:
|
||||||
|
- Go to Admin → Články → Edit article
|
||||||
|
- Go to "Média" tab
|
||||||
|
- Upload a PDF file in attachments
|
||||||
|
|
||||||
|
2. **Test preview**:
|
||||||
|
- Click "Náhled" button
|
||||||
|
- **Expected**: Modal opens with PDF preview
|
||||||
|
- **If PDF doesn't show**: Fallback buttons appear
|
||||||
|
- Click "Otevřít v novém okně" - PDF opens in new tab
|
||||||
|
- Click "Zobrazit pomocí PDF.js" - PDF opens in Mozilla viewer
|
||||||
|
- Click "Zobrazit přes Google" - PDF opens in Google Docs viewer
|
||||||
|
- Click "Stáhnout PDF" - PDF downloads
|
||||||
|
|
||||||
|
### Test 2: Poll Creation for New Article
|
||||||
|
1. **Create new article**:
|
||||||
|
- Go to Admin → Články → Nový článek
|
||||||
|
- Fill in title and category
|
||||||
|
- Go to "Anketa" tab
|
||||||
|
|
||||||
|
2. **See warning message**:
|
||||||
|
- **Expected**: Orange warning box with "Článek ještě není uložen"
|
||||||
|
- Button: "Uložit jako koncept a přidat ankety"
|
||||||
|
|
||||||
|
3. **Save as draft**:
|
||||||
|
- Click the button
|
||||||
|
- **Expected**:
|
||||||
|
- Article saves successfully
|
||||||
|
- Modal stays open
|
||||||
|
- Tab switches to Poll tab (still showing)
|
||||||
|
- PollLinker component now visible
|
||||||
|
- Can create/link polls
|
||||||
|
|
||||||
|
4. **Create poll**:
|
||||||
|
- Click "Vytvořit novou" tab
|
||||||
|
- Fill in poll title and options
|
||||||
|
- Click "Vytvořit anketu"
|
||||||
|
- **Expected**: Poll created and linked to article
|
||||||
|
|
||||||
|
### Test 3: Poll Creation for Existing Article
|
||||||
|
1. **Edit existing article**:
|
||||||
|
- Go to Admin → Články → Edit article
|
||||||
|
- Go to "Anketa" tab
|
||||||
|
|
||||||
|
2. **Expected**: PollLinker shows immediately (no warning)
|
||||||
|
3. Create or link polls as normal
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### PDF Viewing Methods
|
||||||
|
1. **Direct iframe** (default):
|
||||||
|
```html
|
||||||
|
<iframe src="/uploads/file.pdf#view=FitH&toolbar=1" />
|
||||||
|
```
|
||||||
|
- Works if CSP allows
|
||||||
|
- Fastest method
|
||||||
|
- Native browser PDF viewer
|
||||||
|
|
||||||
|
2. **Mozilla PDF.js** (fallback 1):
|
||||||
|
```
|
||||||
|
https://mozilla.github.io/pdf.js/web/viewer.html?file=<encoded_url>
|
||||||
|
```
|
||||||
|
- Works even with strict CSP
|
||||||
|
- JavaScript-based PDF renderer
|
||||||
|
- Requires internet connection
|
||||||
|
|
||||||
|
3. **Google Docs Viewer** (fallback 2):
|
||||||
|
```
|
||||||
|
https://docs.google.com/viewer?url=<encoded_url>&embedded=true
|
||||||
|
```
|
||||||
|
- Requires public URL
|
||||||
|
- May have privacy concerns
|
||||||
|
- Reliable for most PDFs
|
||||||
|
|
||||||
|
4. **Direct download** (fallback 3):
|
||||||
|
- Always works
|
||||||
|
- User opens in their PDF app
|
||||||
|
|
||||||
|
### Poll Creation Flow
|
||||||
|
```
|
||||||
|
New Article (no ID yet)
|
||||||
|
↓
|
||||||
|
User clicks "Save as draft and add polls"
|
||||||
|
↓
|
||||||
|
onSubmit({ keepOpen: true })
|
||||||
|
↓
|
||||||
|
Article created in database
|
||||||
|
↓
|
||||||
|
Modal stays open (closeModal not called)
|
||||||
|
↓
|
||||||
|
setActiveTabIndex(5) - Switch to Poll tab
|
||||||
|
↓
|
||||||
|
PollLinker now has articleId
|
||||||
|
↓
|
||||||
|
User can create/link polls
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
### PDF Preview
|
||||||
|
- PDF.js fallback requires internet connection
|
||||||
|
- Google Viewer requires publicly accessible URLs
|
||||||
|
- Some browsers may block cross-origin iframes
|
||||||
|
- Very large PDFs (>10MB) may be slow
|
||||||
|
|
||||||
|
### Poll Creation
|
||||||
|
- Article must still be saved before linking polls (can't be fully offline)
|
||||||
|
- "Keep open" mode doesn't work if there's a network error
|
||||||
|
- If save fails, modal closes and poll tab doesn't appear
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
### PDF Preview
|
||||||
|
- ✅ Chrome/Edge: Native PDF viewer works
|
||||||
|
- ✅ Firefox: Native PDF viewer works
|
||||||
|
- ✅ Safari: Native PDF viewer works
|
||||||
|
- ✅ All browsers: Fallback buttons work
|
||||||
|
|
||||||
|
### Poll Creation
|
||||||
|
- ✅ All modern browsers (Chrome, Firefox, Safari, Edge)
|
||||||
|
- ✅ Mobile browsers
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
### PDF Preview
|
||||||
|
- [ ] Add PDF.js embed directly in application (no external CDN)
|
||||||
|
- [ ] Add thumbnail generation for PDFs
|
||||||
|
- [ ] Add page navigation controls
|
||||||
|
- [ ] Add zoom controls
|
||||||
|
- [ ] Add print button
|
||||||
|
|
||||||
|
### Poll Creation
|
||||||
|
- [ ] Allow poll creation before article save (store in temp state)
|
||||||
|
- [ ] Add poll preview in article form
|
||||||
|
- [ ] Bulk poll creation/linking
|
||||||
|
- [ ] Poll templates
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
- `frontend/src/components/common/FilePreview.tsx` - PDF preview component
|
||||||
|
- `frontend/src/components/admin/PollLinker.tsx` - Poll management component
|
||||||
|
- `frontend/src/pages/admin/ArticlesAdminPage.tsx` - Article editor
|
||||||
|
- `internal/controllers/base_controller.go` - Backend file upload handler
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
- `POST /api/v1/upload` - File upload (including PDFs)
|
||||||
|
- `GET /uploads/**` - Serve uploaded files
|
||||||
|
- `POST /api/v1/polls` - Create poll
|
||||||
|
- `PUT /api/v1/polls/:id` - Update poll (link to article)
|
||||||
|
- `GET /api/v1/polls?article_id=X` - Get polls for article
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Rich Text Editor Image Insertion Fix
|
||||||
|
|
||||||
|
**Date**: January 2025
|
||||||
|
**Issue**: Quill.js image insertion errors and image duplication on drag
|
||||||
|
|
||||||
|
## Problems Fixed
|
||||||
|
|
||||||
|
### 1. Quill.js Emitter Error
|
||||||
|
**Error Message**: `Uncaught TypeError: can't access property "emit", this.emitter is undefined`
|
||||||
|
|
||||||
|
**Root Cause**:
|
||||||
|
- The Quill editor's internal state wasn't fully initialized when `insertEmbed` was called
|
||||||
|
- Calling `insertEmbed` immediately after upload without ensuring the editor is ready caused the emitter to be undefined
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// Insert into editor
|
||||||
|
const quill = quillRef.current?.getEditor();
|
||||||
|
if (quill) {
|
||||||
|
// Ensure editor is focused and ready
|
||||||
|
quill.focus();
|
||||||
|
|
||||||
|
// Use setTimeout to ensure Quill's internal state is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const range = quill.getSelection();
|
||||||
|
const index = range ? range.index : quill.getLength();
|
||||||
|
|
||||||
|
// Insert the image with 'api' source to prevent event loops
|
||||||
|
quill.insertEmbed(index, 'image', res.url, 'api');
|
||||||
|
|
||||||
|
// Move cursor after the image
|
||||||
|
quill.setSelection(index + 1, 0, 'api');
|
||||||
|
|
||||||
|
// Force content change to trigger re-render
|
||||||
|
onChangeRef.current(quill.root.innerHTML);
|
||||||
|
|
||||||
|
toast({ title: 'Obrázek vložen', status: 'success', duration: 2000 });
|
||||||
|
} catch (embedError) {
|
||||||
|
console.error('Error inserting image:', embedError);
|
||||||
|
toast({ title: 'Chyba při vkládání obrázku', description: String(embedError), status: 'error' });
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Changes**:
|
||||||
|
- Added `quill.focus()` before insertion to ensure the editor has focus
|
||||||
|
- Wrapped insertion in `setTimeout(50ms)` to allow Quill's internal state to stabilize
|
||||||
|
- Changed source parameter from `'user'` to `'api'` to prevent triggering user-initiated event handlers
|
||||||
|
- Added try-catch block for better error handling
|
||||||
|
- Force content update with `onChangeRef.current(quill.root.innerHTML)` to trigger re-render
|
||||||
|
|
||||||
|
### 2. Image Not Showing After Insertion
|
||||||
|
**Problem**: After uploading and inserting an image, it didn't appear in the editor
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
- Added explicit `onChangeRef.current(quill.root.innerHTML)` call after insertion
|
||||||
|
- This forces React to recognize the DOM change and re-render the component
|
||||||
|
- The `'api'` source parameter prevents infinite loops while still triggering the necessary updates
|
||||||
|
|
||||||
|
### 3. Image Duplication on Drag
|
||||||
|
**Problem**: Dragging images created duplicates due to default browser drag-and-drop behavior
|
||||||
|
|
||||||
|
**Solution (Multiple Layers)**:
|
||||||
|
|
||||||
|
#### A. Set draggable attribute on image selection
|
||||||
|
```typescript
|
||||||
|
const selectImage = (img: HTMLImageElement) => {
|
||||||
|
// ... other code ...
|
||||||
|
|
||||||
|
// Prevent default drag behavior to avoid duplication
|
||||||
|
img.setAttribute('draggable', 'false');
|
||||||
|
|
||||||
|
createResizeHandle(img);
|
||||||
|
// ... rest of code ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Added dragstart event listener
|
||||||
|
```typescript
|
||||||
|
// Prevent default drag behavior on images
|
||||||
|
const handleDragStart = (e: DragEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.tagName === 'IMG') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.root.addEventListener('dragstart', handleDragStart);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### C. Added CSS to disable drag
|
||||||
|
```typescript
|
||||||
|
img: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
maxWidth: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
display: 'block',
|
||||||
|
margin: '12px 0',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
borderRadius: '4px',
|
||||||
|
userSelect: 'none',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
WebkitUserDrag: 'none', // Disable WebKit drag
|
||||||
|
userDrag: 'none', // Disable standard drag
|
||||||
|
// ... hover styles ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
### `/frontend/src/components/common/CustomRichEditor.tsx`
|
||||||
|
|
||||||
|
**Lines 274-301**: Image insertion with proper Quill initialization
|
||||||
|
**Lines 523-524**: Set draggable="false" on image selection
|
||||||
|
**Lines 735-743**: Added dragstart event handler
|
||||||
|
**Lines 755**: Added dragstart cleanup in useEffect return
|
||||||
|
**Lines 1146-1147**: Added CSS properties to disable drag
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
1. **Test Image Insertion**:
|
||||||
|
- Open any admin page with rich text editor (Articles, Activities, About)
|
||||||
|
- Click "Vložit obrázek" button
|
||||||
|
- Upload an image and crop it
|
||||||
|
- Verify image appears immediately in the editor
|
||||||
|
- Check browser console for no errors
|
||||||
|
|
||||||
|
2. **Test Image Drag Behavior**:
|
||||||
|
- Insert an image in the editor
|
||||||
|
- Click to select the image (blue outline appears)
|
||||||
|
- Try to drag the image left/right
|
||||||
|
- Verify alignment changes but no duplicate is created
|
||||||
|
- Verify the image doesn't create a "ghost" drag preview
|
||||||
|
|
||||||
|
3. **Test Image Resize**:
|
||||||
|
- Select an image
|
||||||
|
- Grab the blue corner/edge handles
|
||||||
|
- Resize the image
|
||||||
|
- Verify smooth resizing without duplication
|
||||||
|
|
||||||
|
4. **Test Multiple Images**:
|
||||||
|
- Insert several images
|
||||||
|
- Interact with each one
|
||||||
|
- Verify no interference between images
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
The fixes use standard web APIs and should work in:
|
||||||
|
- ✅ Chrome/Edge (Chromium-based)
|
||||||
|
- ✅ Firefox
|
||||||
|
- ✅ Safari (WebKit)
|
||||||
|
|
||||||
|
The `-webkit-user-drag` CSS property specifically targets WebKit browsers.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- The 50ms delay in image insertion is a workaround for Quill's initialization timing
|
||||||
|
- If issues persist, the delay can be increased to 100ms, but this may cause noticeable lag
|
||||||
|
- The drag prevention is comprehensive but may interfere with future drag-and-drop features if needed
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- `/frontend/src/components/common/RichTextEditor.tsx` - Wrapper component
|
||||||
|
- `/frontend/src/services/imageProcessing.ts` - Image upload/crop backend services
|
||||||
|
- `/frontend/src/styles/custom-editor.css` - Additional editor styles
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
1. Consider migrating to Quill 2.0 when stable (better event handling)
|
||||||
|
2. Add loading state during image upload to prevent multiple insertions
|
||||||
|
3. Add image compression options in the crop modal
|
||||||
|
4. Consider lazy loading for large images
|
||||||
Vendored
-39
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"ID": 1,
|
|
||||||
"CreatedAt": "2025-10-19T11:57:20.992362Z",
|
|
||||||
"UpdatedAt": "2025-10-19T11:57:20.992362Z",
|
|
||||||
"DeletedAt": null,
|
|
||||||
"title": "U17: Rýmařov potrestal naše chyby, z Polanky chceme tři body",
|
|
||||||
"content": "\u003ch2\u003eU17: Rýmařov potrestal naše chyby, z Polanky chceme tři body\u003c/h2\u003e\u003cp\u003eV sobotu jsme odehráli mistrovské utkání na půdě Rýmařova, které jsme bohužel prohráli 2:5. Hra měla pro nás smíšený průběh, ale především nám chyběla soustředěnost a kvalitní obrana.\u003c/p\u003e\u003ch3\u003ePrvní poločas: Špatný start a rychlé góly soupeře\u003c/h3\u003e\u003cp\u003ePrvní poločas nám vůbec nevyšel – byli jsme málo aktivní a dopouštěli se zbytečných chyb v obraně, zejména při nákopech soupeře za naši defenzivu. Rýmařov vsadil na jednoduchý, ale účinný styl hry: získat míč a okamžitě ho poslat dopředu. Na tento způsob hry jsme v úvodu nenašli odpověď. Soupeř využil naše chyby a rychle se dostal do vedení.\u003c/p\u003e\u003ch3\u003eDruhý poločas: Zlepšení, ale pozdě\u003c/h3\u003e\u003cp\u003eO poločase jsme si jasně řekli, co je potřeba změnit. Do druhého dějství jsme vstoupili mnohem lépe a hned na jeho začátku jsme měli velkou šanci, kdy jsme šli sami na brankáře – bohužel bez gólového efektu. Krátce poté soupeř přidal čtvrtou branku, ale náš tým to nezlomilo a během dvou minut jsme odpověděli snížením.\u003c/p\u003e\u003cp\u003eDruhý poločas byl z naší strany výrazně lepší – více pohybu, nasazení i snahy o kombinaci. Přesto se nám skóre nepodařilo otočit a soupeř v závěru přidal ještě pátý gól.\u003c/p\u003e\u003ch3\u003eAnalýza zápasu\u003c/h3\u003e\u003cp\u003eCelkově jsme udělali příliš mnoho chyb, byli málo důrazní a prohrávali osobní souboje. Kdybychom proměnili šanci hned po přestávce a snížili na 2:3, mohl zápas vypadat úplně jinak. Soupeř byl efektivnější a lépe využíval naše chybné momenty.\u003c/p\u003e\u003ch3\u003ePohled do budoucna\u003c/h3\u003e\u003cp\u003eNevěšíme hlavu — zapracujeme na nedostatcích, připravíme se poctivě a doma proti Polance uděláme maximum pro zisk tří bodů! V další úterý nás čeká důležitý domácí zápas, který bude mít klíčový význam pro naše další postupy v soutěži.\u003c/p\u003e\u003cp\u003eMichal Vala – U17\u003c/p\u003e\u003cp\u003e\u003cimg src=\"https://eu.zonerama.com/photos/565775563_1500x1000.jpg\" alt=\"Gallery photo\"\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e",
|
|
||||||
"author_id": 1,
|
|
||||||
"category_id": 1,
|
|
||||||
"image_url": "https://eu.zonerama.com/photos/565775554_1500x1000.jpg",
|
|
||||||
"published": true,
|
|
||||||
"published_at": "2025-10-19T11:57:20.992143Z",
|
|
||||||
"slug": "u17-rymarov-chyby-polanka",
|
|
||||||
"excerpt": "",
|
|
||||||
"featured": true,
|
|
||||||
"seo_title": "U17: Rýmařov potrestal naše chyby, z Polanky chceme tři body | Fotbalový klub",
|
|
||||||
"seo_description": "Přečtěte si více o u17: rýmařov potrestal naše chyby, z polanky chceme tři body. Aktuální informace, novinky a zajímavosti z našeho fotbalového klubu.",
|
|
||||||
"og_image_url": "https://eu.zonerama.com/photos/565775554_1500x1000.jpg",
|
|
||||||
"external_link": "",
|
|
||||||
"view_count": 0,
|
|
||||||
"read_time": 2,
|
|
||||||
"unique_views": 0,
|
|
||||||
"category_name": "",
|
|
||||||
"attachments": "",
|
|
||||||
"gallery_album_id": "13903610",
|
|
||||||
"gallery_album_url": "https://eu.zonerama.com/FKKofolaKrnov/Album/13903610",
|
|
||||||
"gallery_photo_ids": "565775563,565775549",
|
|
||||||
"youtube_video_id": "_OsRmfYOXJ4",
|
|
||||||
"youtube_video_title": "Bizoni UH-Atraps Brno 6:5/3:4/-4.kolo 2.futs.liga Východ-UH 10.10.25",
|
|
||||||
"youtube_video_url": "https://www.youtube.com/watch?v=_OsRmfYOXJ4",
|
|
||||||
"youtube_video_thumbnail": "https://img.youtube.com/vi/_OsRmfYOXJ4/maxresdefault.jpg"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"page": 1,
|
|
||||||
"page_size": 1,
|
|
||||||
"total": 1
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"data":"eyJuYW1lIjoiRm90YmFsb3bDvSBrbHViIEtybm92IiwiY2x1Yl9pZCI6IjdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OCIsImNsdWJfdHlwZSI6ImZvb3RiYWxsIiwiY2x1Yl9pbnRlcm5hbF9pZCI6IjgwMTAyMTEiLCJ1cmwiOiJodHRwczovL3d3dy5mb3RiYWwuY3ovc291dGV6ZS9jbHViL2NsdWIvN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4IiwibG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4LzdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OF9jcm9wLmpwZyIsImNhdGVnb3J5IjoiRm90YmFsIiwiY29tcGV0aXRpb25zIjpbeyJpZCI6ImUzMTI3ODY1LWExMDktNDVjZC05MDQ4LTNlNjQyOWUyZWIxMSIsImNvZGUiOiJBMUEiLCJuYW1lIjoiU0FUVU0gNS4gbGlnYSBtdcW+xa8iLCJ0ZWFtX2NvdW50IjoiMTYiLCJtYXRjaGVzX2xpbmsiOiJodHRwczovL3d3dy5mb3RiYWwuY3ovc291dGV6ZS90dXJuYWplL3RhYmxlL2UzMTI3ODY1LWExMDktNDVjZC05MDQ4LTNlNjQyOWUyZWIxMSIsIm1hdGNoZXMiOlt7ImRhdGVfdGltZSI6IjEwLjA4LjIwMjUgMTc6MDAiLCJob21lIjoiS3JhdmHFmWUiLCJob21lX2xvZ29fdXJsIjoiaHR0cHM6Ly9pczEuZm90YmFsLmN6L21lZGlhL2tsdWJ5LzM3N2JmMGFhLTMzZTktNDk4Ny1hYjQyLTM5NzRiYTU4OGQ2Zi8zNzdiZjBhYS0zM2U5LTQ5ODctYWI0Mi0zOTc0YmE1ODhkNmZfY3JvcC5qcGciLCJhd2F5IjoiRksgS29mb2xhIEtybm92IiwiYXdheV9pZCI6IjdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OCIsImF3YXlfbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4LzdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OF9jcm9wLmpwZyIsInNjb3JlIjoiNDowIiwidmVudWUiOiJLcmF2YcWZZSAtIHRyw6F2YSIsIm1hdGNoX2lkIjoiOTE3NmQ4ZWQtZjFmMS00MDkzLWE2MTUtNzEyN2FlNWNjYTgzIiwicmVwb3J0X3VybCI6Imh0dHBzOi8vaXMuZm90YmFsLmN6L3B1YmxpYy96YXBhc3kvemFwaXMtby11dGthbmktcmVwb3J0LmFzcHglM0Z6YXBhcz05MTc2ZDhlZC1mMWYxLTQwOTMtYTYxNS03MTI3YWU1Y2NhODNcdTAwMjZ6YXBpcz0xXHUwMDI2bm9wcmludD0xXHUwMDI2YnRucHJpbnQ9MVx1MDAyNi5odG0iLCJmYWNyX2xpbmsiOiJodHRwczovL3d3dy5mb3RiYWwuY3ovc291dGV6ZS96YXBhc3kvemFwYXMvOTE3NmQ4ZWQtZjFmMS00MDkzLWE2MTUtNzEyN2FlNWNjYTgzIiwiZGVsZWdhdGlvbl91cmwiOiJodHRwczovL2lzLmZvdGJhbC5jei9wdWJsaWMvemFwYXN5L3phcGFzLWRlbGVnYWNlLXJlcG9ydC5hc3B4JTNGemFwYXM9OTE3NmQ4ZWQtZjFmMS00MDkzLWE2MTUtNzEyN2FlNWNjYTgzXHUwMDI2emFwaXM9MVx1MDAyNmhpZGVtZW51PTFcdTAwMjYuaHRtIn0seyJkYXRlX3RpbWUiOiIxNy4wOC4yMDI1IDE1OjAwIiwiaG9tZSI6IkZLIEtvZm9sYSBLcm5vdiIsImhvbWVfaWQiOiI3ZWFjZDlmMC1iZmEwLTQ5MjgtYTliNi05MzYxNDAxNjhmNTgiLCJob21lX2xvZ29fdXJsIjoiaHR0cHM6Ly9pczEuZm90YmFsLmN6L21lZGlhL2tsdWJ5LzdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OC83ZWFjZDlmMC1iZmEwLTQ5MjgtYTliNi05MzYxNDAxNjhmNThfY3JvcC5qcGciLCJhd2F5IjoiQnJ1xaFwZXJrIiwiYXdheV9sb2dvX3VybCI6Ii9kaXN0L2ltZy9sb2dvLWNsdWItZW1wdHkuc3ZnIiwic2NvcmUiOiIxOjMiLCJ2ZW51ZSI6Iktybm92LXRyw6F2YSIsIm1hdGNoX2lkIjoiODE1ZmZkNzAtZjAzYS00OWQwLWI3YjQtYjVjZmE4OWNlMTJmIiwicmVwb3J0X3VybCI6Imh0dHBzOi8vaXMuZm90YmFsLmN6L3B1YmxpYy96YXBhc3kvemFwaXMtby11dGthbmktcmVwb3J0LmFzcHglM0Z6YXBhcz04MTVmZmQ3MC1mMDNhLTQ5ZDAtYjdiNC1iNWNmYTg5Y2UxMmZcdTAwMjZ6YXBpcz0xXHUwMDI2bm9wcmludD0xXHUwMDI2YnRucHJpbnQ9MVx1MDAyNi5odG0iLCJmYWNyX2xpbmsiOiJodHRwczovL3d3dy5mb3RiYWwuY3ovc291dGV6ZS96YXBhc3kvemFwYXMvODE1ZmZkNzAtZjAzYS00OWQwLWI3YjQtYjVjZmE4OWNlMTJmIiwiZGVsZWdhdGlvbl91cmwiOiJodHRwczovL2lzLmZvdGJhbC5jei9wdWJsaWMvemFwYXN5L3phcGFzLWRlbGVnYWNlLXJlcG9ydC5hc3B4JTNGemFwYXM9ODE1ZmZkNzAtZjAzYS00OWQwLWI3YjQtYjVjZmE4OWNlMTJmXHUwMDI2emFwaXM9MVx1MDAyNmhpZGVtZW51PTFcdTAwMjYuaHRtIn0seyJkYXRlX3RpbWUiOiIyNC4wOC4yMDI1IDE2OjMwIiwiaG9tZSI6IkZDIERvbG7DrSBCZW5lxaFvdiIsImhvbWVfbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvMDgwZTNhZTEtMmJjNC00ZDkzLWJlYTktZGIyNmRhNzY4ZmE1LzA4MGUzYWUxLTJiYzQtNGQ5My1iZWE5LWRiMjZkYTc2OGZhNV9jcm9wLmpwZyIsImF3YXkiOiJGSyBLb2ZvbGEgS3Jub3YiLCJhd2F5X2lkIjoiN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4IiwiYXdheV9sb2dvX3VybCI6Imh0dHBzOi8vaXMxLmZvdGJhbC5jei9tZWRpYS9rbHVieS83ZWFjZDlmMC1iZmEwLTQ5MjgtYTliNi05MzYxNDAxNjhmNTgvN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4X2Nyb3AuanBnIiwic2NvcmUiOiIyOjEiLCJ2ZW51ZSI6IkQuIEJlbmXFoW92IC0gdHLDoXZhIiwibWF0Y2hfaWQiOiJmZTUxNjE3Ny1kNDg0LTQ5MDUtOGMxYy0yMjE5NDZjNTM5MDIiLCJyZXBvcnRfdXJsIjoiaHR0cHM6Ly9pcy5mb3RiYWwuY3ovcHVibGljL3phcGFzeS96YXBpcy1vLXV0a2FuaS1yZXBvcnQuYXNweCUzRnphcGFzPWZlNTE2MTc3LWQ0ODQtNDkwNS04YzFjLTIyMTk0NmM1MzkwMlx1MDAyNnphcGlzPTFcdTAwMjZub3ByaW50PTFcdTAwMjZidG5wcmludD0xXHUwMDI2Lmh0bSIsImZhY3JfbGluayI6Imh0dHBzOi8vd3d3LmZvdGJhbC5jei9zb3V0ZXplL3phcGFzeS96YXBhcy9mZTUxNjE3Ny1kNDg0LTQ5MDUtOGMxYy0yMjE5NDZjNTM5MDIiLCJkZWxlZ2F0aW9uX3VybCI6Imh0dHBzOi8vaXMuZm90YmFsLmN6L3B1YmxpYy96YXBhc3kvemFwYXMtZGVsZWdhY2UtcmVwb3J0LmFzcHglM0Z6YXBhcz1mZTUxNjE3Ny1kNDg0LTQ5MDUtOGMxYy0yMjE5NDZjNTM5MDJcdTAwMjZ6YXBpcz0xXHUwMDI2aGlkZW1lbnU9MVx1MDAyNi5odG0ifSx7ImRhdGVfdGltZSI6IjI3LjA4LjIwMjUgMTY6MzAiLCJob21lIjoiRksgS29mb2xhIEtybm92IiwiaG9tZV9pZCI6IjdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OCIsImhvbWVfbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4LzdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OF9jcm9wLmpwZyIsImF3YXkiOiJLb2JlxZlpY2UiLCJhd2F5X2xvZ29fdXJsIjoiaHR0cHM6Ly9pczEuZm90YmFsLmN6L21lZGlhL2tsdWJ5LzU1Zjk2MzA3LWM5MTYtNDgwMS05NDhiLWJjODRmNDZmMjFiZC81NWY5NjMwNy1jOTE2LTQ4MDEtOTQ4Yi1iYzg0ZjQ2ZjIxYmRfY3JvcC5qcGciLCJzY29yZSI6IjM6MSIsInZlbnVlIjoiS3Jub3YtdHLDoXZhIiwibWF0Y2hfaWQiOiI5YWZhZTQzMS1lMDLine truncated
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"data":"eyJuYW1lIjoiRm90YmFsb3bDvSBrbHViIEtybm92IiwiY2x1Yl9pZCI6IjdlYWNkOWYwLWJmYTAtNDkyOC1hOWI2LTkzNjE0MDE2OGY1OCIsImNsdWJfdHlwZSI6ImZvb3RiYWxsIiwiY2x1Yl9pbnRlcm5hbF9pZCI6IjgwMTAyMTEiLCJsb2dvX3VybCI6Imh0dHBzOi8vaXMxLmZvdGJhbC5jei9tZWRpYS9rbHVieS83ZWFjZDlmMC1iZmEwLTQ5MjgtYTliNi05MzYxNDAxNjhmNTgvN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4X2Nyb3AuanBnIiwiYWRkcmVzcyI6IjgwMTAyMTEiLCJjYXRlZ29yeSI6IklEIGtsdWJ1IiwiY29tcGV0aXRpb25zIjpbeyJpZCI6ImUzMTI3ODY1LWExMDktNDVjZC05MDQ4LTNlNjQyOWUyZWIxMSIsImNvZGUiOiJBMUEiLCJuYW1lIjoiU0FUVU0gNS4gbGlnYSBtdcW+xa8iLCJ0ZWFtX2NvdW50IjoiMTYiLCJtYXRjaGVzX2xpbmsiOiJodHRwczovL3d3dy5mb3RiYWwuY3ovc291dGV6ZS90dXJuYWplL3RhYmxlL2UzMTI3ODY1LWExMDktNDVjZC05MDQ4LTNlNjQyOWUyZWIxMSIsInRhYmxlIjp7Im92ZXJhbGwiOlt7InJhbmsiOiIxIiwidGVhbSI6IktyYXZhxZllIiwidGVhbV9sb2dvX3VybCI6Imh0dHBzOi8vaXMxLmZvdGJhbC5jei9tZWRpYS9rbHVieS8zNzdiZjBhYS0zM2U5LTQ5ODctYWI0Mi0zOTc0YmE1ODhkNmYvMzc3YmYwYWEtMzNlOS00OTg3LWFiNDItMzk3NGJhNTg4ZDZmX2Nyb3AuanBnIiwicGxheWVkIjoiMTIiLCJ3aW5zIjoiOSIsImRyYXdzIjoiMSIsImxvc3NlcyI6IjIiLCJzY29yZSI6IjM3OjEyIiwicG9pbnRzIjoiMjgifSx7InJhbmsiOiIyIiwidGVhbSI6IkjDoWogdmUgU2xlenNrdSIsInRlYW1fbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvMjU3OGI5ZmYtOTM4ZS00NjFiLTkwOTAtZDk2OTdlYjkzNzFmLzI1NzhiOWZmLTkzOGUtNDYxYi05MDkwLWQ5Njk3ZWI5MzcxZl9jcm9wLmpwZyIsInBsYXllZCI6IjEyIiwid2lucyI6IjciLCJkcmF3cyI6IjMiLCJsb3NzZXMiOiIyIiwic2NvcmUiOiIyNzoxMyIsInBvaW50cyI6IjI0In0seyJyYW5rIjoiMyIsInRlYW0iOiJKYWt1YsSNb3ZpY2UiLCJ0ZWFtX2xvZ29fdXJsIjoiaHR0cHM6Ly9pczEuZm90YmFsLmN6L21lZGlhL2tsdWJ5LzRlMGI1ZjJmLTRhMjctNDQ0Yy1iZjc3LWUzNzI1Yjg5ODA4Ni80ZTBiNWYyZi00YTI3LTQ0NGMtYmY3Ny1lMzcyNWI4OTgwODZfY3JvcC5qcGciLCJwbGF5ZWQiOiIxMiIsIndpbnMiOiI3IiwiZHJhd3MiOiIwIiwibG9zc2VzIjoiNSIsInNjb3JlIjoiMTg6MjIiLCJwb2ludHMiOiIyMSJ9LHsicmFuayI6IjQiLCJ0ZWFtIjoiRksgSFx1MDAyNlAgU3RhcsOpIE3Em3N0byIsInRlYW1fbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvZWMzYjhmN2YtNTc2NC00YTRlLWIzN2YtNTZkZWE3MDY5NmNiL2VjM2I4ZjdmLTU3NjQtNGE0ZS1iMzdmLTU2ZGVhNzA2OTZjYl9jcm9wLmpwZyIsInBsYXllZCI6IjEyIiwid2lucyI6IjYiLCJkcmF3cyI6IjIiLCJsb3NzZXMiOiI0Iiwic2NvcmUiOiIyMjoyMCIsInBvaW50cyI6IjIwIn0seyJyYW5rIjoiNSIsInRlYW0iOiJCcnXFoXBlcmsiLCJ0ZWFtX2xvZ29fdXJsIjoiL2Rpc3QvaW1nL2xvZ28tY2x1Yi1lbXB0eS5zdmciLCJwbGF5ZWQiOiIxMiIsIndpbnMiOiI2IiwiZHJhd3MiOiIxIiwibG9zc2VzIjoiNSIsInNjb3JlIjoiMjk6MjciLCJwb2ludHMiOiIxOSJ9LHsicmFuayI6IjYiLCJ0ZWFtIjoiQmFuw61rIEFsYnJlY2h0aWNlIiwidGVhbV9sb2dvX3VybCI6Imh0dHBzOi8vaXMxLmZvdGJhbC5jei9tZWRpYS9rbHVieS84ODdhNjdkNi1jNjA3LTRlODAtOTFiZS1kMWFmZjk0MDY2OTgvODg3YTY3ZDYtYzYwNy00ZTgwLTkxYmUtZDFhZmY5NDA2Njk4X2Nyb3AuanBnIiwicGxheWVkIjoiMTEiLCJ3aW5zIjoiNiIsImRyYXdzIjoiMSIsImxvc3NlcyI6IjQiLCJzY29yZSI6IjI0OjIzIiwicG9pbnRzIjoiMTkifSx7InJhbmsiOiI3IiwidGVhbSI6IkZLIE3Em3N0byBBbGJyZWNodGljZSIsInRlYW1fbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvNzUwYjhkODEtNTQyYi00ODVjLThhMTgtZmMwYzQ5NGZmNDExLzc1MGI4ZDgxLTU0MmItNDg1Yy04YTE4LWZjMGM0OTRmZjQxMV9jcm9wLmpwZyIsInBsYXllZCI6IjEyIiwid2lucyI6IjUiLCJkcmF3cyI6IjQiLCJsb3NzZXMiOiIzIiwic2NvcmUiOiIxOToxOCIsInBvaW50cyI6IjE5In0seyJyYW5rIjoiOCIsInRlYW0iOiJNRksgVsOtdGtvdmljZSBCIiwidGVhbV9sb2dvX3VybCI6Imh0dHBzOi8vaXMxLmZvdGJhbC5jei9tZWRpYS9rbHVieS9hZjg4MGQwNi02ZmZjLTQ5M2EtOTRiYi05MGUyYmRhYjcxMTkvYWY4ODBkMDYtNmZmYy00OTNhLTk0YmItOTBlMmJkYWI3MTE5X2Nyb3AuanBnIiwicGxheWVkIjoiMTEiLCJ3aW5zIjoiNSIsImRyYXdzIjoiMyIsImxvc3NlcyI6IjMiLCJzY29yZSI6IjI2OjIwIiwicG9pbnRzIjoiMTgifSx7InJhbmsiOiI5IiwidGVhbSI6IlNsYXZpYSBPcmxvdsOhIiwidGVhbV9sb2dvX3VybCI6Imh0dHBzOi8vaXMxLmZvdGJhbC5jei9tZWRpYS9rbHVieS82ZWEwNmQzYS1hN2JmLTRlZTItOTlmZi0xYmExZWRjNjJjOGYvNmVhMDZkM2EtYTdiZi00ZWUyLTk5ZmYtMWJhMWVkYzYyYzhmX2Nyb3AuanBnIiwicGxheWVkIjoiMTEiLCJ3aW5zIjoiNSIsImRyYXdzIjoiMiIsImxvc3NlcyI6IjQiLCJzY29yZSI6IjI0OjIzIiwicG9pbnRzIjoiMTcifSx7InJhbmsiOiIxMCIsInRlYW0iOiJGQyBWxZllc2luYSIsInRlYW1fbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWVkaWEva2x1YnkvZGMwNWY5YzUtYTQzNi00ZmNlLWI5Y2ItMDZjN2ZmODVkMDE5L2RjMDVmOWM1LWE0MzYtNGZjZS1iOWNiLTA2YzdmZjg1ZDAxOV9jcm9wLmpwZyIsInBsYXllZCI6IjEyIiwid2lucyI6IjUiLCJkcmF3cyI6IjEiLCJsb3NzZXMiOiI2Iiwic2NvcmUiOiIyNjoyNSIsInBvaW50cyI6IjE2In0seyJyYW5rIjoiMTEiLCJ0ZWFtIjoiRksgS29mb2xhIEtybm92IiwidGVhbV9sb2dvX3VybCI6Imh0dHBzOi8vaXMxLmZvdGJhbC5jei9tZWRpYS9rbHVieS83ZWFjZDlmMC1iZmEwLTQ5MjgtYTliNi05MzYxNDAxNjhmNTgvN2VhY2Q5ZjAtYmZhMC00OTI4LWE5YjYtOTM2MTQwMTY4ZjU4X2Nyb3AuanBnIiwicGxheWVkIjoiMTEiLCJ3aW5zIjoiNSIsImRyYXdzIjoiMSIsImxvc3NlcyI6IjUiLCJzY29yZSI6IjE3OjIxIiwicG9pbnRzIjoiMTYifSx7InJhbmsiOiIxMiIsInRlYW0iOiJLb2JlxZlpY2UiLCJ0ZWFtX2xvZ29fdXJsIjoiaHR0cHM6Ly9pczEuZm90YmFsLmN6L21lZGlhL2tsdWJ5LzU1Zjk2MzA3LWM5MTYtNDgwMS05NDhiLWJjODRmNDZmMjFiZC81NWY5NjMwNy1jOTE2LTQ4MDEtOTQ4Yi1iYzg0ZjQ2ZjIxYmRfY3JvcC5qcGciLCJwbGF5ZWQiOiIxMiIsIndpbnMiOiI1IiwiZHJhd3MiOiIwIiwibG9zc2VzIjoiNyIsInNjb3JlIjoiMjM6MTkiLCJwb2ludHMiOiIxNSJ9LHsicmFuayI6IjEzIiwidGVhbSI6IlNLIEJFU0tZRCBGcmVuxaF0w6F0IHAuIFIuIiwidGVhbV9sb2dvX3VybCI6Ii9kaXN0L2ltZy9sb2dvLWNsdWItZW1wdHkuc3ZnIiwicGxheWVkIjoiMTIiLCJ3aW5zIjoiMyIsImRyYXdzIjoiMiIsImxvc3NlcyI6IjciLCJzY29yZSI6IjE2OjI2IiwicG9pbnRzIjoiMTEifSx7InJhbmsiOiIxNCIsInRlYW0iOiJEYXJrb3ZpxI1reSIsInRlYW1fbG9nb191cmwiOiJodHRwczovL2lzMS5mb3RiYWwuY3ovbWLine truncated
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"items":[{"ID":1,"CreatedAt":"2025-10-19T11:57:20.992362Z","UpdatedAt":"2025-10-19T11:57:20.992362Z","DeletedAt":null,"title":"U17: Rýmařov potrestal naše chyby, z Polanky chceme tři body","content":"\u003ch2\u003eU17: Rýmařov potrestal naše chyby, z Polanky chceme tři body\u003c/h2\u003e\u003cp\u003eV sobotu jsme odehráli mistrovské utkání na půdě Rýmařova, které jsme bohužel prohráli 2:5. Hra měla pro nás smíšený průběh, ale především nám chyběla soustředěnost a kvalitní obrana.\u003c/p\u003e\u003ch3\u003ePrvní poločas: Špatný start a rychlé góly soupeře\u003c/h3\u003e\u003cp\u003ePrvní poločas nám vůbec nevyšel – byli jsme málo aktivní a dopouštěli se zbytečných chyb v obraně, zejména při nákopech soupeře za naši defenzivu. Rýmařov vsadil na jednoduchý, ale účinný styl hry: získat míč a okamžitě ho poslat dopředu. Na tento způsob hry jsme v úvodu nenašli odpověď. Soupeř využil naše chyby a rychle se dostal do vedení.\u003c/p\u003e\u003ch3\u003eDruhý poločas: Zlepšení, ale pozdě\u003c/h3\u003e\u003cp\u003eO poločase jsme si jasně řekli, co je potřeba změnit. Do druhého dějství jsme vstoupili mnohem lépe a hned na jeho začátku jsme měli velkou šanci, kdy jsme šli sami na brankáře – bohužel bez gólového efektu. Krátce poté soupeř přidal čtvrtou branku, ale náš tým to nezlomilo a během dvou minut jsme odpověděli snížením.\u003c/p\u003e\u003cp\u003eDruhý poločas byl z naší strany výrazně lepší – více pohybu, nasazení i snahy o kombinaci. Přesto se nám skóre nepodařilo otočit a soupeř v závěru přidal ještě pátý gól.\u003c/p\u003e\u003ch3\u003eAnalýza zápasu\u003c/h3\u003e\u003cp\u003eCelkově jsme udělali příliš mnoho chyb, byli málo důrazní a prohrávali osobní souboje. Kdybychom proměnili šanci hned po přestávce a snížili na 2:3, mohl zápas vypadat úplně jinak. Soupeř byl efektivnější a lépe využíval naše chybné momenty.\u003c/p\u003e\u003ch3\u003ePohled do budoucna\u003c/h3\u003e\u003cp\u003eNevěšíme hlavu — zapracujeme na nedostatcích, připravíme se poctivě a doma proti Polance uděláme maximum pro zisk tří bodů! V další úterý nás čeká důležitý domácí zápas, který bude mít klíčový význam pro naše další postupy v soutěži.\u003c/p\u003e\u003cp\u003eMichal Vala – U17\u003c/p\u003e\u003cp\u003e\u003cimg src=\"https://eu.zonerama.com/photos/565775563_1500x1000.jpg\" alt=\"Gallery photo\"\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e\u003cp\u003e\u003cbr\u003e\u003c/p\u003e","author_id":1,"author":{"ID":1,"CreatedAt":"2025-10-19T11:45:13.565636Z","UpdatedAt":"2025-10-19T11:45:19.049972Z","DeletedAt":null,"email":"[email protected]","first_name":"Tomas","last_name":"Dvorak","role":"admin","IsActive":true,"last_login":"2025-10-19T11:45:19.04969Z"},"category_id":1,"category":{"ID":1,"CreatedAt":"2025-10-19T11:57:20.990034Z","UpdatedAt":"2025-10-19T11:57:20.990034Z","DeletedAt":null,"name":"KALMAN TRADE Krajský přebor mladší dorost","description":"","slug":""},"image_url":"https://eu.zonerama.com/photos/565775554_1500x1000.jpg","published":true,"published_at":"2025-10-19T11:57:20.992143Z","slug":"u17-rymarov-chyby-polanka","excerpt":"","featured":true,"seo_title":"U17: Rýmařov potrestal naše chyby, z Polanky chceme tři body | Fotbalový klub","seo_description":"Přečtěte si více o u17: rýmařov potrestal naše chyby, z polanky chceme tři body. Aktuální informace, novinky a zajímavosti z našeho fotbalového klubu.","og_image_url":"https://eu.zonerama.com/photos/565775554_1500x1000.jpg","external_link":"","view_count":0,"Line truncated
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:34:56Z","last_modified":""}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
[{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"A1A","alias":"SATUM 5. liga mužů","original_name":"SATUM 5. liga mužů","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"C1A","alias":"KALMAN TRADE Krajský přebor starší dorost","original_name":"KALMAN TRADE Krajský přebor starší dorost","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"D1A","alias":"KALMAN TRADE Krajský přebor mladší dorost","original_name":"KALMAN TRADE Krajský přebor mladší dorost","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"E1S","alias":"2.MSŽL-U 15 sk. E","original_name":"2.MSŽL-U 15 sk. E","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"E2S","alias":"2.MSŽL-U 14 sk. E","original_name":"2.MSŽL-U 14 sk. E","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"F1S","alias":"1. liga SpSM-U 13 SEVER","original_name":"1. liga SpSM-U 13 SEVER","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"F2S","alias":"1. liga SpSM-U 12 SEVER","original_name":"1. liga SpSM-U 12 SEVER","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"G1D","alias":"Starší přípravka 1+5 sk.D","original_name":"Starší přípravka 1+5 sk.D","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"H1A","alias":"Okresní přebor mladší přípravky (4+1)","original_name":"Okresní přebor mladší přípravky (4+1)","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"H1C","alias":"Mladší přípravka 1+4 sk.C","original_name":"Mladší přípravka 1+4 sk.C","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"U1E","alias":"PC U1E U-10 Šumperk","original_name":"PC U1E U-10 Šumperk","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"V1C","alias":"PC V1C U-8 Nový Jičín","original_name":"PC V1C U-8 Nový Jičín","display_order":0},{"ID":0,"CreatedAt":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DeletedAt":null,"code":"V5B","alias":"PC V5B U-9 Hlučín","original_name":"PC V5B U-9 Hlučín","display_order":0}]
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:34:56Z","last_modified":""}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:34:56Z","last_modified":""}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"name":"Fotbalový klub Krnov","club_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","club_type":"football","club_internal_id":"8010211","url":"https://www.fotbal.cz/souteze/club/club/7eacd9f0-bfa0-4928-a9b6-936140168f58","logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","category":"Fotbal","competitions":[{"id":"e3127865-a109-45cd-9048-3e6429e2eb11","code":"A1A","name":"SATUM 5. liga mužů","team_count":"16","matches_link":"https://www.fotbal.cz/souteze/turnaje/table/e3127865-a109-45cd-9048-3e6429e2eb11","matches":[{"date_time":"10.08.2025 17:00","home":"Kravaře","home_logo_url":"https://is1.fotbal.cz/media/kluby/377bf0aa-33e9-4987-ab42-3974ba588d6f/377bf0aa-33e9-4987-ab42-3974ba588d6f_crop.jpg","away":"FK Kofola Krnov","away_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","away_logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","score":"4:0","venue":"Kravaře - tráva","match_id":"9176d8ed-f1f1-4093-a615-7127ae5cca83","report_url":"https://is.fotbal.cz/public/zapasy/zapis-o-utkani-report.aspx%3Fzapas=9176d8ed-f1f1-4093-a615-7127ae5cca83\u0026zapis=1\u0026noprint=1\u0026btnprint=1\u0026.htm","facr_link":"https://www.fotbal.cz/souteze/zapasy/zapas/9176d8ed-f1f1-4093-a615-7127ae5cca83","delegation_url":"https://is.fotbal.cz/public/zapasy/zapas-delegace-report.aspx%3Fzapas=9176d8ed-f1f1-4093-a615-7127ae5cca83\u0026zapis=1\u0026hidemenu=1\u0026.htm"},{"date_time":"17.08.2025 15:00","home":"FK Kofola Krnov","home_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","home_logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","away":"Brušperk","away_logo_url":"/dist/img/logo-club-empty.svg","score":"1:3","venue":"Krnov-tráva","match_id":"815ffd70-f03a-49d0-b7b4-b5cfa89ce12f","report_url":"https://is.fotbal.cz/public/zapasy/zapis-o-utkani-report.aspx%3Fzapas=815ffd70-f03a-49d0-b7b4-b5cfa89ce12f\u0026zapis=1\u0026noprint=1\u0026btnprint=1\u0026.htm","facr_link":"https://www.fotbal.cz/souteze/zapasy/zapas/815ffd70-f03a-49d0-b7b4-b5cfa89ce12f","delegation_url":"https://is.fotbal.cz/public/zapasy/zapas-delegace-report.aspx%3Fzapas=815ffd70-f03a-49d0-b7b4-b5cfa89ce12f\u0026zapis=1\u0026hidemenu=1\u0026.htm"},{"date_time":"24.08.2025 16:30","home":"FC Dolní Benešov","home_logo_url":"https://is1.fotbal.cz/media/kluby/080e3ae1-2bc4-4d93-bea9-db26da768fa5/080e3ae1-2bc4-4d93-bea9-db26da768fa5_crop.jpg","away":"FK Kofola Krnov","away_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","away_logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","score":"2:1","venue":"D. Benešov - tráva","match_id":"fe516177-d484-4905-8c1c-221946c53902","report_url":"https://is.fotbal.cz/public/zapasy/zapis-o-utkani-report.aspx%3Fzapas=fe516177-d484-4905-8c1c-221946c53902\u0026zapis=1\u0026noprint=1\u0026btnprint=1\u0026.htm","facr_link":"https://www.fotbal.cz/souteze/zapasy/zapas/fe516177-d484-4905-8c1c-221946c53902","delegation_url":"https://is.fotbal.cz/public/zapasy/zapas-delegace-report.aspx%3Fzapas=fe516177-d484-4905-8c1c-221946c53902\u0026zapis=1\u0026hidemenu=1\u0026.htm"},{"date_time":"27.08.2025 16:30","home":"FK Kofola Krnov","home_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","home_logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","away":"Kobeřice","away_logo_url":"https://is1.fotbal.cz/media/kluby/55f96307-c916-4801-948b-bc84f46f21bd/55f96307-c916-4801-948b-bc84f46f21bd_crop.jpg","score":"3:1","venue":"Krnov-tráva","match_id":"9afae431-e091-48b8-a023-4cc633cc6f86","report_url":"https://is.fotbal.cz/public/zapasy/zapis-o-utkani-report.aspx%3Fzapas=9afae431-e091-48b8-a023-4cc633cc6f86\u0026zapis=1\u0026noprint=1\u0026btnprint=1\u0026.htm","facr_link":"https://www.fotbal.cz/souteze/zapasy/zapas/9afae431-e091-48b8-a023-4cc633cc6f86","delegation_url":"https://is.fotbal.cz/public/zapasy/zapas-delegace-report.aspx%3Fzapas=9afae431-e091-48b8-a023-4cc633cc6f86\u0026zapis=1\u0026hidemenu=1\u0026.htm"},{"date_time":"31.08.2025 15:00","home":"FK Kofola Krnov","home_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","home_logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","away":"FK H\u0026P Staré Město","away_logo_url":"https://is1.fotbal.cz/media/kluby/ec3b8f7f-5764-4a4e-b37f-56dea70696cb/ec3b8f7f-5764-4a4e-b37f-56dea70696cb_crop.jpg","score":"2:0","venue":"Krnov-tráva","match_id":"b4d518a5-e2e7-42d1-8455-817ccd78a225","report_url":"https://is.fotbal.cz/public/zapasy/zapis-o-utkani-report.aspx%3Fzapas=b4d518a5-e2e7-42d1-8455-817ccd78a225\u0026zapis=1\u0026noprint=1\u0026btnprint=1\u0026.htm","facr_link":"https://www.fotbal.cz/souteze/zapasy/zapas/b4d518a5-e2e7-42d1-8455-817ccd78a225","delegation_url":"https://Line truncated
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:35:00Z","last_modified":""}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"name":"Fotbalový klub Krnov","club_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","club_type":"football","club_internal_id":"8010211","logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","address":"8010211","category":"ID klubu","competitions":[{"id":"e3127865-a109-45cd-9048-3e6429e2eb11","code":"A1A","name":"SATUM 5. liga mužů","team_count":"16","matches_link":"https://www.fotbal.cz/souteze/turnaje/table/e3127865-a109-45cd-9048-3e6429e2eb11","table":{"overall":[{"rank":"1","team":"Kravaře","team_logo_url":"https://is1.fotbal.cz/media/kluby/377bf0aa-33e9-4987-ab42-3974ba588d6f/377bf0aa-33e9-4987-ab42-3974ba588d6f_crop.jpg","played":"12","wins":"9","draws":"1","losses":"2","score":"37:12","points":"28"},{"rank":"2","team":"Háj ve Slezsku","team_logo_url":"https://is1.fotbal.cz/media/kluby/2578b9ff-938e-461b-9090-d9697eb9371f/2578b9ff-938e-461b-9090-d9697eb9371f_crop.jpg","played":"12","wins":"7","draws":"3","losses":"2","score":"27:13","points":"24"},{"rank":"3","team":"Jakubčovice","team_logo_url":"https://is1.fotbal.cz/media/kluby/4e0b5f2f-4a27-444c-bf77-e3725b898086/4e0b5f2f-4a27-444c-bf77-e3725b898086_crop.jpg","played":"12","wins":"7","draws":"0","losses":"5","score":"18:22","points":"21"},{"rank":"4","team":"FK H\u0026P Staré Město","team_logo_url":"https://is1.fotbal.cz/media/kluby/ec3b8f7f-5764-4a4e-b37f-56dea70696cb/ec3b8f7f-5764-4a4e-b37f-56dea70696cb_crop.jpg","played":"12","wins":"6","draws":"2","losses":"4","score":"22:20","points":"20"},{"rank":"5","team":"Brušperk","team_logo_url":"/dist/img/logo-club-empty.svg","played":"12","wins":"6","draws":"1","losses":"5","score":"29:27","points":"19"},{"rank":"6","team":"Baník Albrechtice","team_logo_url":"https://is1.fotbal.cz/media/kluby/887a67d6-c607-4e80-91be-d1aff9406698/887a67d6-c607-4e80-91be-d1aff9406698_crop.jpg","played":"11","wins":"6","draws":"1","losses":"4","score":"24:23","points":"19"},{"rank":"7","team":"FK Město Albrechtice","team_logo_url":"https://is1.fotbal.cz/media/kluby/750b8d81-542b-485c-8a18-fc0c494ff411/750b8d81-542b-485c-8a18-fc0c494ff411_crop.jpg","played":"12","wins":"5","draws":"4","losses":"3","score":"19:18","points":"19"},{"rank":"8","team":"MFK Vítkovice B","team_logo_url":"https://is1.fotbal.cz/media/kluby/af880d06-6ffc-493a-94bb-90e2bdab7119/af880d06-6ffc-493a-94bb-90e2bdab7119_crop.jpg","played":"11","wins":"5","draws":"3","losses":"3","score":"26:20","points":"18"},{"rank":"9","team":"Slavia Orlová","team_logo_url":"https://is1.fotbal.cz/media/kluby/6ea06d3a-a7bf-4ee2-99ff-1ba1edc62c8f/6ea06d3a-a7bf-4ee2-99ff-1ba1edc62c8f_crop.jpg","played":"11","wins":"5","draws":"2","losses":"4","score":"24:23","points":"17"},{"rank":"10","team":"FC Vřesina","team_logo_url":"https://is1.fotbal.cz/media/kluby/dc05f9c5-a436-4fce-b9cb-06c7ff85d019/dc05f9c5-a436-4fce-b9cb-06c7ff85d019_crop.jpg","played":"12","wins":"5","draws":"1","losses":"6","score":"26:25","points":"16"},{"rank":"11","team":"FK Kofola Krnov","team_logo_url":"https://is1.fotbal.cz/media/kluby/7eacd9f0-bfa0-4928-a9b6-936140168f58/7eacd9f0-bfa0-4928-a9b6-936140168f58_crop.jpg","played":"11","wins":"5","draws":"1","losses":"5","score":"17:21","points":"16"},{"rank":"12","team":"Kobeřice","team_logo_url":"https://is1.fotbal.cz/media/kluby/55f96307-c916-4801-948b-bc84f46f21bd/55f96307-c916-4801-948b-bc84f46f21bd_crop.jpg","played":"12","wins":"5","draws":"0","losses":"7","score":"23:19","points":"15"},{"rank":"13","team":"SK BESKYD Frenštát p. R.","team_logo_url":"/dist/img/logo-club-empty.svg","played":"12","wins":"3","draws":"2","losses":"7","score":"16:26","points":"11"},{"rank":"14","team":"Darkovičky","team_logo_url":"https://is1.fotbal.cz/media/kluby/8e207b30-7b68-44bb-ad08-bc25495dd094/8e207b30-7b68-44bb-ad08-bc25495dd094_crop.jpg","played":"12","wins":"2","draws":"3","losses":"7","score":"16:23","points":"9"},{"rank":"15","team":"Heřmanice","team_logo_url":"https://is1.fotbal.cz/media/kluby/c32dc305-6b78-41c7-8053-d8644eef96f1/c32dc305-6b78-41c7-8053-d8644eef96f1_crop.jpg","played":"12","wins":"1","draws":"5","losses":"6","score":"17:30","points":"8"},{"rank":"16","team":"FC Dolní Benešov","team_logo_url":"https://is1.fotbal.cz/media/kluby/080e3ae1-2bc4-4d93-bea9-db26da768fa5/080e3ae1-2bc4-4d93-bea9-db26da768fa5_crop.jpg","played":"12","wins":"2","draws":"1","losses":"9","score":"16:35","points":"7"}]}},{"id":"7ae7e3d0-ab3c-4afe-af6d-4a26d74ea554","code":"C1A","name":"KALMAN TRADE Krajský přebor starší dorost","team_count":"16","matches_link":"https://www.fotbal.cz/souteze/turnaje/table/7ae7e3d0-ab3c-4afe-af6d-4a26d74ea554","table":{"overall":[{"rank":"1","team":"Hlubina","team_logo_url":"https://is1.fotbal.cz/media/kluby/da62c705-ca73-4561-9ea4-ab93e6afce88/da62c705-ca73-4561-9ea4-ab93e6afce88_crop.jpg","played":"12","wins":"11","draws":"0","losses":"1","score":"59:10","points":"33"},{"rank":"2","team":"Polanka nad Odrou","team_logo_url":"https://is1.fotbal.cz/meLine truncated
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:35:00Z","last_modified":""}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
null
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"lastUpdated":"2025-10-19T15:35:00Z"}
|
|
||||||
Vendored
-47
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"baseURL": "http://127.0.0.1:8080/api/v1",
|
|
||||||
"duration_ms": 3857,
|
|
||||||
"endpoints": [
|
|
||||||
{
|
|
||||||
"path": "/sponsors",
|
|
||||||
"file": "sponsors.json",
|
|
||||||
"ok": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/events/upcoming",
|
|
||||||
"file": "events_upcoming.json",
|
|
||||||
"ok": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/public/team-logo-overrides",
|
|
||||||
"file": "team_logo_overrides.json",
|
|
||||||
"ok": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/competition-aliases",
|
|
||||||
"file": "competition_aliases.json",
|
|
||||||
"ok": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/settings",
|
|
||||||
"file": "settings.json",
|
|
||||||
"ok": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/articles?page=1\u0026page_size=10\u0026published=true",
|
|
||||||
"file": "articles.json",
|
|
||||||
"ok": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/facr/club/football/7eacd9f0-bfa0-4928-a9b6-936140168f58/table",
|
|
||||||
"file": "facr_tables.json",
|
|
||||||
"ok": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/facr/club/football/7eacd9f0-bfa0-4928-a9b6-936140168f58",
|
|
||||||
"file": "facr_club_info.json",
|
|
||||||
"ok": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lastUpdated": "2025-10-19T15:35:00Z"
|
|
||||||
}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"about_html":"","accent_color":"#ffb500","background_color":"#ffffff","club_id":"7eacd9f0-bfa0-4928-a9b6-936140168f58","club_logo_url":"http://logoapi.sportcreative.eu/logos/7eacd9f0-bfa0-4928-a9b6-936140168f58?format=svg","club_name":"Fotbalový klub Krnov","club_type":"football","club_url":"https://www.fotbal.cz/souteze/club/club/7eacd9f0-bfa0-4928-a9b6-936140168f58","contact_address":"Úvoz","contact_city":"Krnov","contact_country":"Česko","contact_email":"[email protected]","contact_phone":"+420 778 701 838","contact_zip":"794 01","custom_nav":null,"facebook_url":"https://www.facebook.com/p/FK-Kofola-Krnov-61561103731912/","font_body":"Archivo","font_heading":"Archivo","gallery_label":"Fotogalerie","gallery_url":"https://eu.zonerama.com/FKKofolaKrnov","instagram_url":"https://www.instagram.com/fkkofolakrnov/","location_latitude":50.0860754,"location_longitude":17.6699647,"map_style":"dark","map_zoom_level":15,"merch_items":null,"merch_limit":0,"merch_module_enabled":false,"merch_source":"","merch_style":"","primary_color":"#ffd900","secondary_color":"#0002ff","show_about_in_nav":true,"show_map_on_homepage":false,"sponsors_layout":"","sponsors_theme":"","text_color":"#111111","videos":null,"videos_items":[{"length":"","thumbnail_url":"https://img.youtube.com/vi/WKXh4Z6SYMs/maxresdefault.jpg","title":"Bizoni UH vs. FC ATRAPS z.s. - 2. Futsal liga - východ (celý zápas)","uploaded_at":"2025-10-12","url":"https://www.youtube.com/watch?v=WKXh4Z6SYMs"},{"length":"","thumbnail_url":"https://img.youtube.com/vi/_OsRmfYOXJ4/maxresdefault.jpg","title":"Bizoni UH-Atraps Brno 6:5/3:4/-4.kolo 2.futs.liga Východ-UH 10.10.25","uploaded_at":"2025-10-11","url":"https://www.youtube.com/watch?v=_OsRmfYOXJ4"},{"length":"","thumbnail_url":"https://img.youtube.com/vi/h_-TS6oVvKA/maxresdefault.jpg","title":"Bizoni UH-RT F.Místek 5:5/1:3/-2.kolo 2.liga UH 26.9.25","uploaded_at":"2025-09-28","url":"https://www.youtube.com/watch?v=h_-TS6oVvKA"},{"length":"","thumbnail_url":"https://img.youtube.com/vi/ozH8xE7V458/maxresdefault.jpg","title":"Bizoni UH-Tango Hodonín 7:4/2:3/-regionální finále poháru SFČR-16.9.25-UH","uploaded_at":"2025-09-19","url":"https://www.youtube.com/watch?v=ozH8xE7V458"},{"length":"","thumbnail_url":"https://img.youtube.com/vi/nrj6_1IoYoo/maxresdefault.jpg","title":"Bizoni UH-Fr.Místek 7:2/4:1/-Superpohár-12.9.25 v Uh.Hradišti","uploaded_at":"2025-09-19","url":"https://www.youtube.com/watch?v=nrj6_1IoYoo"}],"videos_limit":5,"videos_module_enabled":true,"videos_source":"auto","videos_style":"slider","youtube_url":"https://www.youtube.com/@FCBizoniUH"}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:34:56Z","last_modified":""}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:34:56Z","last_modified":""}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"by_name":{}}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"etag":"","fetched_at":"2025-10-19T15:34:56Z","last_modified":""}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"channel":"@FCBizoniUH","channel_url":"https://www.youtube.com/@FCBizoniUH/videos","subscribers_text":"65 subscribers","subscribers":65,"videos":[{"video_id":"WKXh4Z6SYMs","title":"Bizoni UH vs. FC ATRAPS z.s. - 2. Futsal liga - východ (celý zápas)","thumbnail_url":"https://img.youtube.com/vi/WKXh4Z6SYMs/maxresdefault.jpg","views_text":"71 views","views":71,"published_text":"7 days ago","published_date":"2025-10-12"},{"video_id":"_OsRmfYOXJ4","title":"Bizoni UH-Atraps Brno 6:5/3:4/-4.kolo 2.futs.liga Východ-UH 10.10.25","thumbnail_url":"https://img.youtube.com/vi/_OsRmfYOXJ4/maxresdefault.jpg","views_text":"228 views","views":228,"published_text":"8 days ago","published_date":"2025-10-11"},{"video_id":"h_-TS6oVvKA","title":"Bizoni UH-RT F.Místek 5:5/1:3/-2.kolo 2.liga UH 26.9.25","thumbnail_url":"https://img.youtube.com/vi/h_-TS6oVvKA/maxresdefault.jpg","views_text":"211 views","views":211,"published_text":"3 weeks ago","published_date":"2025-09-28"},{"video_id":"ozH8xE7V458","title":"Bizoni UH-Tango Hodonín 7:4/2:3/-regionální finále poháru SFČR-16.9.25-UH","thumbnail_url":"https://img.youtube.com/vi/ozH8xE7V458/maxresdefault.jpg","views_text":"299 views","views":299,"published_text":"1 month ago","published_date":"2025-09-19"},{"video_id":"nrj6_1IoYoo","title":"Bizoni UH-Fr.Místek 7:2/4:1/-Superpohár-12.9.25 v Uh.Hradišti","thumbnail_url":"https://img.youtube.com/vi/nrj6_1IoYoo/maxresdefault.jpg","views_text":"232 views","views":232,"published_text":"1 month ago","published_date":"2025-09-19"},{"video_id":"pSGwSJvdd14","title":"Bizoni UH vs Žabinští Vlci Brno 6:4 | Semifinále poháru SFČR UH","thumbnail_url":"https://img.youtube.com/vi/pSGwSJvdd14/maxresdefault.jpg","views_text":"104 views","views":104,"published_text":"4 months ago","published_date":"2025-06-19"},{"video_id":"AbGKYfOmtlw","title":"FC Bizoni UH 14-1 Hombres Brno | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/AbGKYfOmtlw/maxresdefault.jpg","views_text":"232 views","views":232,"published_text":"7 months ago","published_date":"2025-03-19"},{"video_id":"6TE21O06TZA","title":"FC Bizoni UH 11-3 Amor Kloboučky Vyškov B | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/6TE21O06TZA/maxresdefault.jpg","views_text":"123 views","views":123,"published_text":"7 months ago","published_date":"2025-03-19"},{"video_id":"LTEZcrev6xI","title":"FC Bizoni UH 10-0 FFT Vinohrady Brno | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/LTEZcrev6xI/maxresdefault.jpg","views_text":"139 views","views":139,"published_text":"8 months ago","published_date":"2025-02-19"},{"video_id":"V91k9Dirvkg","title":"FC Bizoni UH 19-2 FC Kozlany-Bohdalice | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/V91k9Dirvkg/maxresdefault.jpg","views_text":"178 views","views":178,"published_text":"9 months ago","published_date":"2025-01-19"},{"video_id":"FWLttVdzfsQ","title":"FC Bizoni UH 13-3 TJ Agrotec Hustopeče | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/FWLttVdzfsQ/maxresdefault.jpg","views_text":"175 views","views":175,"published_text":"10 months ago","published_date":"2024-12-19"},{"video_id":"zx747-IPN4s","title":"FC Bizoni UH 11-8 Remos Oslavany | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/zx747-IPN4s/maxresdefault.jpg","views_text":"196 views","views":196,"published_text":"10 months ago","published_date":"2024-12-19"},{"video_id":"ZzsBsqJTJdo","title":"FC Bizoni UH 18-2 FC KALÁBEK SPORT Brno | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/ZzsBsqJTJdo/maxresdefault.jpg","views_text":"308 views","views":308,"published_text":"11 months ago","published_date":"2024-11-19"},{"video_id":"m0R2d3AZy3A","title":"FC Bizoni UH 10-1 Žabinští Vlci Brno B | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/m0R2d3AZy3A/maxresdefault.jpg","views_text":"254 views","views":254,"published_text":"11 months ago","published_date":"2024-11-19"},{"video_id":"FY9aYVNpyqY","title":"FC Bizoni UH - Žabinští Vlci Brno B (celý zápas)","thumbnail_url":"https://img.youtube.com/vi/FY9aYVNpyqY/maxresdefault.jpg","views_text":"95 views","views":95,"published_text":"11 months ago","published_date":"2024-11-19"},{"video_id":"cYazpGpL7PQ","title":"FC Bizoni UH 14-4 AC Napoleon Prace | HIGHLIGHTS","thumbnail_url":"https://img.youtube.com/vi/cYazpGpL7PQ/maxresdefault.jpg","views_text":"234 views","views":234,"published_text":"1 year ago","published_date":"2024-10-19"},{"video_id":"M2kb2QvBKPw","title":"SK UP Olomouc 2-9 FK Chrudim | HIGHLIGHTS | 1. Futsal liga 2024/25","thumbnail_url":"https://img.youtube.com/vi/M2kb2QvBKPw/maxresdefault.jpg","views_text":"62 views","views":62,"published_text":"1 year ago","published_date":"2024-10-19"},{"video_id":"CUtNBwJg9Uc","title":"SKUP Olomouc - FK Chrudim (celý zápas)","thumbnail_url":"https://img.youtube.com/vi/CUtNBwJg9Uc/maxresdefault.jpg","views_text":"34 views","views":34,"published_text":"1 year ago","published_date":"2024-10-19"},{"video_id":"b54-jmuwFko","title":"Line truncated
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"fetched_at":"2025-10-19T12:25:01Z","source":"https://youtube.tdvorak.dev/channel_videos?channel=https%3A%2F%2Fwww.youtube.com%2F%40FCBizoniUH"}
|
|
||||||
Vendored
-11
@@ -1,11 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "565775554",
|
|
||||||
"album_id": "",
|
|
||||||
"album_url": "https://eu.zonerama.com/FKKofolaKrnov/Album/13903610",
|
|
||||||
"page_url": "https://eu.zonerama.com/FKKofolaKrnov/Photo/13903610/565775554",
|
|
||||||
"image_url": "https://eu.zonerama.com/photos/565775554_1500x1000.jpg",
|
|
||||||
"title": "",
|
|
||||||
"picked_at": "2025-10-19T11:57:11Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Vendored
-102
@@ -1,102 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "",
|
|
||||||
"title": "",
|
|
||||||
"url": "",
|
|
||||||
"date": "",
|
|
||||||
"photos_count": 0,
|
|
||||||
"views_count": 0,
|
|
||||||
"photos": null,
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
null
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"fetched_at": "2025-10-19T12:25:21Z",
|
|
||||||
"link": ""
|
|
||||||
}
|
|
||||||
Vendored
-1071
File diff suppressed because it is too large.
Load diff
@@ -274,11 +274,30 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
|||||||
// Insert into editor
|
// Insert into editor
|
||||||
const quill = quillRef.current?.getEditor();
|
const quill = quillRef.current?.getEditor();
|
||||||
if (quill) {
|
if (quill) {
|
||||||
const range = quill.getSelection(true);
|
// Ensure editor is focused and ready
|
||||||
const index = range ? range.index : quill.getLength();
|
quill.focus();
|
||||||
quill.insertEmbed(index, 'image', res.url, 'user');
|
|
||||||
quill.setSelection(index + 1, 0);
|
// Use setTimeout to ensure Quill's internal state is ready
|
||||||
toast({ title: 'Obrázek vložen', status: 'success', duration: 2000 });
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const range = quill.getSelection();
|
||||||
|
const index = range ? range.index : quill.getLength();
|
||||||
|
|
||||||
|
// Insert the image
|
||||||
|
quill.insertEmbed(index, 'image', res.url, 'api');
|
||||||
|
|
||||||
|
// Move cursor after the image
|
||||||
|
quill.setSelection(index + 1, 0, 'api');
|
||||||
|
|
||||||
|
// Force content change to trigger re-render
|
||||||
|
onChangeRef.current(quill.root.innerHTML);
|
||||||
|
|
||||||
|
toast({ title: 'Obrázek vložen', status: 'success', duration: 2000 });
|
||||||
|
} catch (embedError) {
|
||||||
|
console.error('Error inserting image:', embedError);
|
||||||
|
toast({ title: 'Chyba při vkládání obrázku', description: String(embedError), status: 'error' });
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Crop and insert error:', e);
|
console.error('Crop and insert error:', e);
|
||||||
@@ -500,6 +519,10 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
|||||||
img.style.outline = '3px solid #3182ce';
|
img.style.outline = '3px solid #3182ce';
|
||||||
img.style.cursor = 'move';
|
img.style.cursor = 'move';
|
||||||
img.style.boxShadow = '0 4px 12px rgba(49, 130, 206, 0.3)';
|
img.style.boxShadow = '0 4px 12px rgba(49, 130, 206, 0.3)';
|
||||||
|
|
||||||
|
// Prevent default drag behavior to avoid duplication
|
||||||
|
img.setAttribute('draggable', 'false');
|
||||||
|
|
||||||
createResizeHandle(img);
|
createResizeHandle(img);
|
||||||
|
|
||||||
// Set selected image state and load filters
|
// Set selected image state and load filters
|
||||||
@@ -639,7 +662,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
|||||||
currentAlignment = 'right';
|
currentAlignment = 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable default drag behavior to prevent ghost image
|
// Already set in selectImage, but ensure it's off
|
||||||
target.setAttribute('draggable', 'false');
|
target.setAttribute('draggable', 'false');
|
||||||
|
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
@@ -709,15 +732,27 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prevent default drag behavior on images
|
||||||
|
const handleDragStart = (e: DragEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.tagName === 'IMG') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
editor.root.addEventListener('click', handleImageClick);
|
editor.root.addEventListener('click', handleImageClick);
|
||||||
editor.root.addEventListener('mousedown', handleMouseDown);
|
editor.root.addEventListener('mousedown', handleMouseDown);
|
||||||
editor.root.addEventListener('scroll', handleScroll);
|
editor.root.addEventListener('scroll', handleScroll);
|
||||||
|
editor.root.addEventListener('dragstart', handleDragStart);
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
editor.root.removeEventListener('click', handleImageClick);
|
editor.root.removeEventListener('click', handleImageClick);
|
||||||
editor.root.removeEventListener('mousedown', handleMouseDown);
|
editor.root.removeEventListener('mousedown', handleMouseDown);
|
||||||
editor.root.removeEventListener('scroll', handleScroll);
|
editor.root.removeEventListener('scroll', handleScroll);
|
||||||
|
editor.root.removeEventListener('dragstart', handleDragStart);
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
removeResizeHandle();
|
removeResizeHandle();
|
||||||
deselectImage();
|
deselectImage();
|
||||||
@@ -1107,6 +1142,9 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
|||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
WebkitUserDrag: 'none',
|
||||||
|
userDrag: 'none',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
opacity: 0.95,
|
opacity: 0.95,
|
||||||
transform: 'scale(1.01)',
|
transform: 'scale(1.01)',
|
||||||
|
|||||||
@@ -122,14 +122,70 @@ const FilePreview: React.FC<FilePreviewProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fileInfo.type === 'pdf') {
|
if (fileInfo.type === 'pdf') {
|
||||||
|
// Try multiple PDF viewing methods due to CSP restrictions
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={8.5 / 11} w="100%" minH="70vh">
|
<VStack spacing={4} w="100%" minH="70vh">
|
||||||
<iframe
|
{/* Primary: Try direct iframe embed */}
|
||||||
src={`${fullUrl}#view=FitH`}
|
<Box w="100%" h="70vh" borderWidth="1px" borderRadius="md" overflow="hidden">
|
||||||
title={fileName}
|
<iframe
|
||||||
style={{ border: 'none', width: '100%', height: '100%' }}
|
src={`${fullUrl}#view=FitH&toolbar=1`}
|
||||||
/>
|
title={fileName}
|
||||||
</AspectRatio>
|
style={{ border: 'none', width: '100%', height: '100%' }}
|
||||||
|
onError={(e) => {
|
||||||
|
console.error('PDF iframe load error:', e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Fallback options */}
|
||||||
|
<VStack spacing={2} w="100%">
|
||||||
|
<Text fontSize="sm" color={mutedText} textAlign="center">
|
||||||
|
Pokud se PDF nezobrazuje, použijte jedno z tlačítek níže:
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={3} flexWrap="wrap" justify="center">
|
||||||
|
<Button
|
||||||
|
as={ChakraLink}
|
||||||
|
href={fullUrl}
|
||||||
|
isExternal
|
||||||
|
leftIcon={<FiEye />}
|
||||||
|
colorScheme="blue"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Otevřít v novém okně
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
as={ChakraLink}
|
||||||
|
href={`https://mozilla.github.io/pdf.js/web/viewer.html?file=${encodeURIComponent(fullUrl)}`}
|
||||||
|
isExternal
|
||||||
|
leftIcon={<FiEye />}
|
||||||
|
colorScheme="purple"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Zobrazit pomocí PDF.js
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
as={ChakraLink}
|
||||||
|
href={`https://docs.google.com/viewer?url=${encodeURIComponent(fullUrl)}&embedded=true`}
|
||||||
|
isExternal
|
||||||
|
leftIcon={<FiEye />}
|
||||||
|
colorScheme="green"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Zobrazit přes Google
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
as={ChakraLink}
|
||||||
|
href={fullUrl}
|
||||||
|
download
|
||||||
|
leftIcon={<FiDownload />}
|
||||||
|
colorScheme="gray"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Stáhnout PDF
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { sortCategoriesWithOrder } from '../../utils/categorySort';
|
|||||||
import { sanitizeClubName } from '../../utils/url';
|
import { sanitizeClubName } from '../../utils/url';
|
||||||
import '../../styles/logos.css';
|
import '../../styles/logos.css';
|
||||||
|
|
||||||
const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string; aid?: string; al?: string; s?: string; clubName?: string }> = ({ d, h, hid, hl, a, aid, al, s, clubName }) => (
|
const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string; aid?: string; al?: string; s?: string; clubName?: string; clubId?: string }> = ({ d, h, hid, hl, a, aid, al, s, clubName, clubId }) => (
|
||||||
<HStack justify="space-between" borderRadius="lg" p={3} bg="white" boxShadow="sm">
|
<HStack justify="space-between" borderRadius="lg" p={3} bg="white" boxShadow="sm">
|
||||||
<Text w="140px" fontSize="sm" color="gray.600">{d}</Text>
|
<Text w="140px" fontSize="sm" color="gray.600">{d}</Text>
|
||||||
<HStack flex={1} justify="flex-end" spacing={4}>
|
<HStack flex={1} justify="flex-end" spacing={4}>
|
||||||
@@ -28,14 +28,30 @@ const Row: React.FC<{ d: string; h: string; hid?: string; hl?: string; a: string
|
|||||||
<HStack minW="60px" justify="center" spacing={2}>
|
<HStack minW="60px" justify="center" spacing={2}>
|
||||||
<Text fontWeight="bold" textAlign="center">{s || '-:-'}</Text>
|
<Text fontWeight="bold" textAlign="center">{s || '-:-'}</Text>
|
||||||
{(() => {
|
{(() => {
|
||||||
if (!s || !clubName) return null;
|
if (!s) return null;
|
||||||
const m = s.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
|
const m = s.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
const hG = parseInt(m[1], 10), aG = parseInt(m[2], 10);
|
const hG = parseInt(m[1], 10), aG = parseInt(m[2], 10);
|
||||||
const norm = (x: string) => String(x||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
|
|
||||||
const strip = (x: string) => norm(x).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
|
// First try ID-based matching (most reliable)
|
||||||
const ourHome = (() => { const A = strip(h); const B = strip(clubName); return A && B && (A===B || A.endsWith(B) || B.endsWith(A)); })();
|
let ourHome = false;
|
||||||
const ourAway = (() => { const A = strip(a); const B = strip(clubName); return A && B && (A===B || A.endsWith(B) || B.endsWith(A)); })();
|
let ourAway = false;
|
||||||
|
if (clubId && hid && aid) {
|
||||||
|
ourHome = hid === clubId;
|
||||||
|
ourAway = aid === clubId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to name matching if IDs not available or no match
|
||||||
|
if (!ourHome && !ourAway && clubName) {
|
||||||
|
const norm = (x: string) => String(x||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,' ').trim().toLowerCase();
|
||||||
|
const strip = (x: string) => norm(x).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
|
||||||
|
const A = strip(h);
|
||||||
|
const B = strip(clubName);
|
||||||
|
ourHome = Boolean(A && B && (A===B || A.endsWith(B) || B.endsWith(A)));
|
||||||
|
const C = strip(a);
|
||||||
|
ourAway = Boolean(C && B && (C===B || C.endsWith(B) || B.endsWith(C)));
|
||||||
|
}
|
||||||
|
|
||||||
if (!ourHome && !ourAway) return null;
|
if (!ourHome && !ourAway) return null;
|
||||||
if (hG === aG) return <Badge colorScheme="blue" variant="subtle">Remíza</Badge>;
|
if (hG === aG) return <Badge colorScheme="blue" variant="subtle">Remíza</Badge>;
|
||||||
const our = ourHome ? hG : aG; const opp = ourHome ? aG : hG;
|
const our = ourHome ? hG : aG; const opp = ourHome ? aG : hG;
|
||||||
@@ -135,7 +151,7 @@ const CompetitionMatches: React.FC = () => {
|
|||||||
<TabPanel key={c.id} px={0}>
|
<TabPanel key={c.id} px={0}>
|
||||||
<VStack align="stretch" spacing={3}>
|
<VStack align="stretch" spacing={3}>
|
||||||
{(c.matches || []).slice(0, 6).map((m, idx) => (
|
{(c.matches || []).slice(0, 6).map((m, idx) => (
|
||||||
<Row key={m.match_id || idx} d={m.date_time} h={m.home} hid={m.home_id} hl={m.home_logo_url} a={m.away} aid={m.away_id} al={m.away_logo_url} s={m.score} clubName={data.name} />
|
<Row key={m.match_id || idx} d={m.date_time} h={m.home} hid={m.home_id} hl={m.home_logo_url} a={m.away} aid={m.away_id} al={m.away_logo_url} s={m.score} clubName={data.name} clubId={data.club_internal_id} />
|
||||||
))}
|
))}
|
||||||
{(c.matches || []).length === 0 && (
|
{(c.matches || []).length === 0 && (
|
||||||
<Text color="gray.500">Žádné zápasy k dispozici.</Text>
|
<Text color="gray.500">Žádné zápasy k dispozici.</Text>
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const MatchRow: React.FC<{
|
|||||||
away: { name: string; logo?: string; id?: string };
|
away: { name: string; logo?: string; id?: string };
|
||||||
score?: string;
|
score?: string;
|
||||||
clubName?: string;
|
clubName?: string;
|
||||||
}> = ({ date, home, away, score, clubName }) => (
|
clubId?: string;
|
||||||
|
}> = ({ date, home, away, score, clubName, clubId }) => (
|
||||||
<HStack justify="space-between" borderWidth="1px" borderRadius="md" p={3} bg="white">
|
<HStack justify="space-between" borderWidth="1px" borderRadius="md" p={3} bg="white">
|
||||||
<Text w="140px" fontSize="sm" color="gray.600">{date}</Text>
|
<Text w="140px" fontSize="sm" color="gray.600">{date}</Text>
|
||||||
<HStack flex={1} justify="flex-end">
|
<HStack flex={1} justify="flex-end">
|
||||||
@@ -32,14 +33,30 @@ const MatchRow: React.FC<{
|
|||||||
<Text fontWeight="bold" textAlign="center">{score || '-:-'}</Text>
|
<Text fontWeight="bold" textAlign="center">{score || '-:-'}</Text>
|
||||||
{(() => {
|
{(() => {
|
||||||
const sent = (() => {
|
const sent = (() => {
|
||||||
if (!score || !clubName) return null;
|
if (!score) return null;
|
||||||
const m = score.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
|
const m = score.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
const h = parseInt(m[1], 10), a = parseInt(m[2], 10);
|
const h = parseInt(m[1], 10), a = parseInt(m[2], 10);
|
||||||
const norm = (s: string) => String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g,' ').trim().toLowerCase();
|
|
||||||
const strip = (s: string) => norm(s).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
|
// First try ID-based matching (most reliable)
|
||||||
const ourIsHome = (() => { const aName = strip(home.name); const bName = strip(clubName); return aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)); })();
|
let ourIsHome = false;
|
||||||
const ourIsAway = (() => { const aName = strip(away.name); const bName = strip(clubName); return aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)); })();
|
let ourIsAway = false;
|
||||||
|
if (clubId && home.id && away.id) {
|
||||||
|
ourIsHome = home.id === clubId;
|
||||||
|
ourIsAway = away.id === clubId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to name matching if IDs not available or no match
|
||||||
|
if (!ourIsHome && !ourIsAway && clubName) {
|
||||||
|
const norm = (s: string) => String(s||'').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/\s+/g,' ').trim().toLowerCase();
|
||||||
|
const strip = (s: string) => norm(s).replace(/\b(mestsky|m\.?f\.?k\.?|mfk|tj|sk|sokol|fotbalovy|fotbalový|fotbalovy\s+klub|fotbalovy\s+klub)\b/g,'').replace(/\s+/g,' ').trim();
|
||||||
|
const aName = strip(home.name);
|
||||||
|
const bName = strip(clubName);
|
||||||
|
ourIsHome = Boolean(aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)));
|
||||||
|
const cName = strip(away.name);
|
||||||
|
ourIsAway = Boolean(cName && bName && (cName===bName || cName.endsWith(bName) || bName.endsWith(cName)));
|
||||||
|
}
|
||||||
|
|
||||||
if (!ourIsHome && !ourIsAway) return null;
|
if (!ourIsHome && !ourIsAway) return null;
|
||||||
if (h === a) return { label: 'Remíza', color: 'blue' } as const;
|
if (h === a) return { label: 'Remíza', color: 'blue' } as const;
|
||||||
const our = ourIsHome ? h : a; const opp = ourIsHome ? a : h;
|
const our = ourIsHome ? h : a; const opp = ourIsHome ? a : h;
|
||||||
@@ -100,6 +117,7 @@ const MatchesSection: React.FC = () => {
|
|||||||
away={{ name: m.away, logo: m.away_logo_url, id: m.away_id }}
|
away={{ name: m.away, logo: m.away_logo_url, id: m.away_id }}
|
||||||
score={m.score}
|
score={m.score}
|
||||||
clubName={data.name}
|
clubName={data.name}
|
||||||
|
clubId={data.club_internal_id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{(c.matches || []).length === 0 && (
|
{(c.matches || []).length === 0 && (
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type MatchItem = {
|
|||||||
time: string; // HH:MM
|
time: string; // HH:MM
|
||||||
home: string;
|
home: string;
|
||||||
away: string;
|
away: string;
|
||||||
|
home_id?: string;
|
||||||
|
away_id?: string;
|
||||||
venue?: string;
|
venue?: string;
|
||||||
home_logo_url?: string;
|
home_logo_url?: string;
|
||||||
away_logo_url?: string;
|
away_logo_url?: string;
|
||||||
@@ -51,6 +53,7 @@ const CalendarPage: React.FC = () => {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [clubName, setClubName] = useState<string>('');
|
const [clubName, setClubName] = useState<string>('');
|
||||||
|
const [clubId, setClubId] = useState<string>('');
|
||||||
const [clubType, setClubType] = useState<'football' | 'futsal'>('football');
|
const [clubType, setClubType] = useState<'football' | 'futsal'>('football');
|
||||||
const [standings, setStandings] = useState<any[]>([]);
|
const [standings, setStandings] = useState<any[]>([]);
|
||||||
|
|
||||||
@@ -264,6 +267,8 @@ const CalendarPage: React.FC = () => {
|
|||||||
time,
|
time,
|
||||||
home: m.home,
|
home: m.home,
|
||||||
away: m.away,
|
away: m.away,
|
||||||
|
home_id: m.home_id,
|
||||||
|
away_id: m.away_id,
|
||||||
venue: m.venue,
|
venue: m.venue,
|
||||||
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
||||||
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
||||||
@@ -307,6 +312,8 @@ const CalendarPage: React.FC = () => {
|
|||||||
time,
|
time,
|
||||||
home: m.home,
|
home: m.home,
|
||||||
away: m.away,
|
away: m.away,
|
||||||
|
home_id: m.home_id,
|
||||||
|
away_id: m.away_id,
|
||||||
venue: m.venue,
|
venue: m.venue,
|
||||||
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
||||||
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
||||||
@@ -399,6 +406,7 @@ const CalendarPage: React.FC = () => {
|
|||||||
setCompLinks(compLinkMap);
|
setCompLinks(compLinkMap);
|
||||||
setStandings(standingsData);
|
setStandings(standingsData);
|
||||||
if (json?.name) setClubName(String(json.name));
|
if (json?.name) setClubName(String(json.name));
|
||||||
|
if (json?.club_internal_id) setClubId(String(json.club_internal_id));
|
||||||
if (json?.club_type) setClubType(json.club_type);
|
if (json?.club_type) setClubType(json.club_type);
|
||||||
// Set active tab from query ?comp=<id>
|
// Set active tab from query ?comp=<id>
|
||||||
const compQ = searchParams.get('comp');
|
const compQ = searchParams.get('comp');
|
||||||
@@ -521,8 +529,21 @@ const CalendarPage: React.FC = () => {
|
|||||||
|
|
||||||
const s = parseScore(m.score);
|
const s = parseScore(m.score);
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
const ourIsHome = isClubTeam(m.home);
|
|
||||||
const ourIsAway = isClubTeam(m.away);
|
// First try ID-based matching (most reliable)
|
||||||
|
let ourIsHome = false;
|
||||||
|
let ourIsAway = false;
|
||||||
|
if (clubId && m.home_id && m.away_id) {
|
||||||
|
ourIsHome = m.home_id === clubId;
|
||||||
|
ourIsAway = m.away_id === clubId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to name matching if IDs not available or no match
|
||||||
|
if (!ourIsHome && !ourIsAway) {
|
||||||
|
ourIsHome = isClubTeam(m.home);
|
||||||
|
ourIsAway = isClubTeam(m.away);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ourIsHome && !ourIsAway) return null; // unknown perspective
|
if (!ourIsHome && !ourIsAway) return null; // unknown perspective
|
||||||
if (s.h === s.a) return { label: 'Remíza', color: 'blue' };
|
if (s.h === s.a) return { label: 'Remíza', color: 'blue' };
|
||||||
const ourGoals = ourIsHome ? s.h : s.a;
|
const ourGoals = ourIsHome ? s.h : s.a;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ type MatchItem = {
|
|||||||
time: string;
|
time: string;
|
||||||
home: string;
|
home: string;
|
||||||
away: string;
|
away: string;
|
||||||
|
home_id?: string;
|
||||||
|
away_id?: string;
|
||||||
home_logo_url?: string;
|
home_logo_url?: string;
|
||||||
away_logo_url?: string;
|
away_logo_url?: string;
|
||||||
score?: string | null;
|
score?: string | null;
|
||||||
@@ -23,7 +25,7 @@ type MatchItem = {
|
|||||||
venue?: string;
|
venue?: string;
|
||||||
competition?: string;
|
competition?: string;
|
||||||
competitionName?: string;
|
competitionName?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
const MatchesPage: React.FC = () => {
|
const MatchesPage: React.FC = () => {
|
||||||
const [clubName, setClubName] = useState<string>('');
|
const [clubName, setClubName] = useState<string>('');
|
||||||
@@ -115,8 +117,21 @@ const MatchesPage: React.FC = () => {
|
|||||||
const getSentiment = (m: MatchItem): { label: 'Výhra'|'Remíza'|'Prohra'; colorScheme: 'green'|'blue'|'red' } | null => {
|
const getSentiment = (m: MatchItem): { label: 'Výhra'|'Remíza'|'Prohra'; colorScheme: 'green'|'blue'|'red' } | null => {
|
||||||
const s = parseScore(m.score);
|
const s = parseScore(m.score);
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
const ourIsHome = isClubTeam(m.home);
|
|
||||||
const ourIsAway = isClubTeam(m.away);
|
// First try ID-based matching (most reliable)
|
||||||
|
let ourIsHome = false;
|
||||||
|
let ourIsAway = false;
|
||||||
|
if (clubId && m.home_id && m.away_id) {
|
||||||
|
ourIsHome = m.home_id === clubId;
|
||||||
|
ourIsAway = m.away_id === clubId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to name matching if IDs not available or no match
|
||||||
|
if (!ourIsHome && !ourIsAway) {
|
||||||
|
ourIsHome = isClubTeam(m.home);
|
||||||
|
ourIsAway = isClubTeam(m.away);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ourIsHome && !ourIsAway) return null;
|
if (!ourIsHome && !ourIsAway) return null;
|
||||||
if (s.h === s.a) return { label: 'Remíza', colorScheme: 'blue' };
|
if (s.h === s.a) return { label: 'Remíza', colorScheme: 'blue' };
|
||||||
const ourGoals = ourIsHome ? s.h : s.a;
|
const ourGoals = ourIsHome ? s.h : s.a;
|
||||||
@@ -343,6 +358,8 @@ const MatchesPage: React.FC = () => {
|
|||||||
time,
|
time,
|
||||||
home: m.home,
|
home: m.home,
|
||||||
away: m.away,
|
away: m.away,
|
||||||
|
home_id: m.home_id,
|
||||||
|
away_id: m.away_id,
|
||||||
home_logo_url: getOverrideLogo(m.home, m.home_id, m.home_logo_url),
|
home_logo_url: getOverrideLogo(m.home, m.home_id, m.home_logo_url),
|
||||||
away_logo_url: getOverrideLogo(m.away, m.away_id, m.away_logo_url),
|
away_logo_url: getOverrideLogo(m.away, m.away_id, m.away_logo_url),
|
||||||
score: actualScore,
|
score: actualScore,
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ const MatchLinkBadge: React.FC<{ articleId: number }> = ({ articleId }) => {
|
|||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Show loading state while fetching
|
||||||
|
if (linkQ.isLoading) {
|
||||||
|
return <Badge colorScheme="gray">Načítání...</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
const mid = (linkQ.data as any)?.external_match_id;
|
const mid = (linkQ.data as any)?.external_match_id;
|
||||||
if (!mid) return <Badge colorScheme="gray">Nepropojeno</Badge>;
|
if (!mid) return <Badge colorScheme="gray">Nepropojeno</Badge>;
|
||||||
|
|
||||||
@@ -651,38 +657,19 @@ const ArticlesAdminPage = () => {
|
|||||||
// Forward the payload as-is so new fields (youtube, gallery) are persisted
|
// Forward the payload as-is so new fields (youtube, gallery) are persisted
|
||||||
createArticle(payload),
|
createArticle(payload),
|
||||||
onSuccess: async (created: any) => {
|
onSuccess: async (created: any) => {
|
||||||
try {
|
console.log('Article created successfully in mutation callback:', created);
|
||||||
// If a match was selected (from temp storage), link it now that we have an article ID
|
// Note: Match linking is now handled in onSubmit() to avoid race conditions
|
||||||
const matchRaw = tempMatchLink || matchIdInput;
|
// Clear temporary storage
|
||||||
const matchToLink = typeof matchRaw === 'string' ? matchRaw : String(matchRaw || '');
|
setTempMatchLink('');
|
||||||
const matchId = matchToLink.trim();
|
setMatchIdInput('');
|
||||||
if (matchId && created?.id) {
|
|
||||||
await putArticleMatchLink(created.id, { external_match_id: matchId, title: (editing as any)?.title || '' });
|
// Invalidate queries to refresh the list
|
||||||
setLinkedMatchId(matchId);
|
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
||||||
toast({
|
qc.invalidateQueries({ queryKey: ['articles'] });
|
||||||
title: 'Článek vytvořen a propojen se zápasem',
|
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
||||||
description: `Match ID: ${matchId}`,
|
qc.invalidateQueries({ queryKey: ['article-match-link'] }); // Invalidate match links
|
||||||
status: 'success',
|
|
||||||
duration: 3000,
|
// Don't close modal here - let onSubmit handle it after match linking
|
||||||
isClosable: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: 'Článek byl úspěšně vytvořen',
|
|
||||||
status: 'success',
|
|
||||||
duration: 3000,
|
|
||||||
isClosable: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Clear temporary storage after successful creation
|
|
||||||
setTempMatchLink('');
|
|
||||||
} finally {
|
|
||||||
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
|
||||||
qc.invalidateQueries({ queryKey: ['articles'] });
|
|
||||||
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
|
||||||
qc.invalidateQueries({ queryKey: ['article-match-link'] }); // Invalidate match links
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (e: any) => {
|
onError: (e: any) => {
|
||||||
console.error('Error creating article:', e);
|
console.error('Error creating article:', e);
|
||||||
@@ -702,18 +689,16 @@ const ArticlesAdminPage = () => {
|
|||||||
updateArticle(id, payload),
|
updateArticle(id, payload),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const articleId = variables.id;
|
const articleId = variables.id;
|
||||||
toast({
|
console.log('Article updated successfully in mutation callback:', articleId);
|
||||||
title: 'Článek byl úspěšně aktualizován',
|
|
||||||
status: 'success',
|
// Invalidate queries to refresh the list
|
||||||
duration: 3000,
|
|
||||||
isClosable: true
|
|
||||||
});
|
|
||||||
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
||||||
qc.invalidateQueries({ queryKey: ['articles'] });
|
qc.invalidateQueries({ queryKey: ['articles'] });
|
||||||
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
||||||
qc.invalidateQueries({ queryKey: ['article-match-link', articleId] }); // Invalidate specific match link
|
qc.invalidateQueries({ queryKey: ['article-match-link', articleId] }); // Invalidate specific match link
|
||||||
qc.invalidateQueries({ queryKey: ['article', `id:${articleId}`] }); // Invalidate article detail
|
qc.invalidateQueries({ queryKey: ['article', `id:${articleId}`] }); // Invalidate article detail
|
||||||
closeModal();
|
|
||||||
|
// Success toast and modal closing handled in onSubmit()
|
||||||
},
|
},
|
||||||
onError: (e: any) => {
|
onError: (e: any) => {
|
||||||
console.error('Error updating article:', e);
|
console.error('Error updating article:', e);
|
||||||
@@ -864,7 +849,7 @@ const ArticlesAdminPage = () => {
|
|||||||
} catch { return undefined; }
|
} catch { return undefined; }
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async (options: { keepOpen?: boolean } = {}) => {
|
||||||
if (!editing) return;
|
if (!editing) return;
|
||||||
// Require category selection by name (kategorie je povinná)
|
// Require category selection by name (kategorie je povinná)
|
||||||
if (!String((editing as any)?.category_name || '').trim()) {
|
if (!String((editing as any)?.category_name || '').trim()) {
|
||||||
@@ -932,10 +917,83 @@ const ArticlesAdminPage = () => {
|
|||||||
// Log the payload for debugging
|
// Log the payload for debugging
|
||||||
console.log('Saving article with payload:', JSON.stringify(payload, null, 2));
|
console.log('Saving article with payload:', JSON.stringify(payload, null, 2));
|
||||||
|
|
||||||
|
// Debug: Log match link state before submission
|
||||||
|
console.log('Match link state before submit:', {
|
||||||
|
tempMatchLink,
|
||||||
|
matchIdInput,
|
||||||
|
linkedMatchId,
|
||||||
|
isNewArticle: !(editing as any)?.id
|
||||||
|
});
|
||||||
|
|
||||||
if ((editing as any)?.id) {
|
if ((editing as any)?.id) {
|
||||||
|
// Update existing article
|
||||||
await updateMut.mutateAsync({ id: (editing as any).id, payload });
|
await updateMut.mutateAsync({ id: (editing as any).id, payload });
|
||||||
|
|
||||||
|
// Handle match linking for existing articles (update or delete)
|
||||||
|
const matchRaw = matchIdInput || linkedMatchId;
|
||||||
|
const matchId = String(matchRaw || '').trim();
|
||||||
|
let matchLinked = false;
|
||||||
|
if (matchId) {
|
||||||
|
try {
|
||||||
|
await putArticleMatchLink((editing as any).id, { external_match_id: matchId, title: editing.title || '' });
|
||||||
|
console.log('Match link updated for existing article');
|
||||||
|
matchLinked = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Failed to update match link:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
toast({
|
||||||
|
title: matchLinked ? 'Článek aktualizován a propojen se zápasem' : 'Článek byl úspěšně aktualizován',
|
||||||
|
status: 'success',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await createMut.mutateAsync(payload);
|
// Create new article
|
||||||
|
const created = await createMut.mutateAsync(payload);
|
||||||
|
|
||||||
|
// Handle match linking for new articles
|
||||||
|
const matchRaw = tempMatchLink || matchIdInput;
|
||||||
|
const matchId = String(matchRaw || '').trim();
|
||||||
|
if (matchId && created?.id) {
|
||||||
|
console.log('Linking new article', created.id, 'with match', matchId);
|
||||||
|
try {
|
||||||
|
await putArticleMatchLink(created.id, { external_match_id: matchId, title: editing.title || '' });
|
||||||
|
console.log('Match link created for new article');
|
||||||
|
setLinkedMatchId(matchId);
|
||||||
|
toast({
|
||||||
|
title: 'Článek vytvořen a propojen se zápasem',
|
||||||
|
description: `Match ID: ${matchId}`,
|
||||||
|
status: 'success',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Failed to link match:', err);
|
||||||
|
toast({
|
||||||
|
title: 'Článek vytvořen, ale propojení se zápasem selhalo',
|
||||||
|
description: err?.response?.data?.error || err?.message || 'Zkuste propojit zápas ručně',
|
||||||
|
status: 'warning',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (created?.id) {
|
||||||
|
// No match to link, just show success
|
||||||
|
toast({
|
||||||
|
title: 'Článek byl úspěšně vytvořen',
|
||||||
|
status: 'success',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal after successful save (unless keepOpen is true)
|
||||||
|
if (!options.keepOpen) {
|
||||||
|
closeModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -1781,13 +1839,30 @@ const ArticlesAdminPage = () => {
|
|||||||
qc.invalidateQueries({ queryKey: ['linked-polls'] });
|
qc.invalidateQueries({ queryKey: ['linked-polls'] });
|
||||||
}} />
|
}} />
|
||||||
) : (
|
) : (
|
||||||
<Alert status="info" borderRadius="md">
|
<Alert status="warning" borderRadius="md">
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<VStack align="start" spacing={1}>
|
<VStack align="start" spacing={2}>
|
||||||
<Text fontWeight="semibold">Nejprve uložte článek</Text>
|
<Text fontWeight="semibold">Článek ještě není uložen</Text>
|
||||||
<Text fontSize="sm">
|
<Text fontSize="sm">
|
||||||
Pro vytvoření nebo propojení ankety nejprve uložte článek tlačítkem "Uložit" níže. Poté se vrátíte do úprav a budete moci přidat ankety.
|
Pro propojení anket s článkem musíte nejprve článek uložit. Klikněte na "Uložit" níže - článek se uloží jako koncept a poté budete moci přidat ankety.
|
||||||
</Text>
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={async () => {
|
||||||
|
// Save article as draft first, keep modal open
|
||||||
|
try {
|
||||||
|
await onSubmit({ keepOpen: true });
|
||||||
|
// Switch to poll tab after save
|
||||||
|
setActiveTabIndex(5); // Poll tab is index 5
|
||||||
|
} catch (error) {
|
||||||
|
// Error is handled by onSubmit
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
isLoading={createMut.isLoading}
|
||||||
|
>
|
||||||
|
Uložit jako koncept a přidat ankety
|
||||||
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -1798,7 +1873,7 @@ const ArticlesAdminPage = () => {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button variant="ghost" mr={3} onClick={closeModal}>Zrušit</Button>
|
<Button variant="ghost" mr={3} onClick={closeModal}>Zrušit</Button>
|
||||||
<Button colorScheme="blue" onClick={onSubmit} isLoading={createMut.isLoading || updateMut.isLoading}>Uložit</Button>
|
<Button colorScheme="blue" onClick={() => onSubmit()} isLoading={createMut.isLoading || updateMut.isLoading}>Uložit</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -404,13 +404,13 @@ func (pcr *PollControllerRefactored) GetPollResults(c *gin.Context) {
|
|||||||
for _, option := range poll.Options {
|
for _, option := range poll.Options {
|
||||||
var count int64
|
var count int64
|
||||||
pcr.DB.Model(&models.PollVote{}).Where("option_id = ?", option.ID).Count(&count)
|
pcr.DB.Model(&models.PollVote{}).Where("option_id = ?", option.ID).Count(&count)
|
||||||
|
|
||||||
results = append(results, OptionResult{
|
results = append(results, OptionResult{
|
||||||
OptionID: option.ID,
|
OptionID: option.ID,
|
||||||
OptionText: option.Text,
|
OptionText: option.Text,
|
||||||
VoteCount: count,
|
VoteCount: count,
|
||||||
})
|
})
|
||||||
|
|
||||||
totalVotes += count
|
totalVotes += count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user