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
40 files changed
+964
-1359
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
|
||||
const quill = quillRef.current?.getEditor();
|
||||
if (quill) {
|
||||
const range = quill.getSelection(true);
|
||||
// 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();
|
||||
quill.insertEmbed(index, 'image', res.url, 'user');
|
||||
quill.setSelection(index + 1, 0);
|
||||
|
||||
// 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) {
|
||||
console.error('Crop and insert error:', e);
|
||||
@@ -500,6 +519,10 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
img.style.outline = '3px solid #3182ce';
|
||||
img.style.cursor = 'move';
|
||||
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);
|
||||
|
||||
// Set selected image state and load filters
|
||||
@@ -639,7 +662,7 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
currentAlignment = 'right';
|
||||
}
|
||||
|
||||
// Disable default drag behavior to prevent ghost image
|
||||
// Already set in selectImage, but ensure it's off
|
||||
target.setAttribute('draggable', 'false');
|
||||
|
||||
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('mousedown', handleMouseDown);
|
||||
editor.root.addEventListener('scroll', handleScroll);
|
||||
editor.root.addEventListener('dragstart', handleDragStart);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
editor.root.removeEventListener('click', handleImageClick);
|
||||
editor.root.removeEventListener('mousedown', handleMouseDown);
|
||||
editor.root.removeEventListener('scroll', handleScroll);
|
||||
editor.root.removeEventListener('dragstart', handleDragStart);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
removeResizeHandle();
|
||||
deselectImage();
|
||||
@@ -1107,6 +1142,9 @@ const CustomRichEditor: React.FC<CustomRichEditorProps> = ({
|
||||
transition: 'all 0.2s ease',
|
||||
borderRadius: '4px',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'auto',
|
||||
WebkitUserDrag: 'none',
|
||||
userDrag: 'none',
|
||||
'&:hover': {
|
||||
opacity: 0.95,
|
||||
transform: 'scale(1.01)',
|
||||
|
||||
@@ -122,14 +122,70 @@ const FilePreview: React.FC<FilePreviewProps> = ({
|
||||
}
|
||||
|
||||
if (fileInfo.type === 'pdf') {
|
||||
// Try multiple PDF viewing methods due to CSP restrictions
|
||||
return (
|
||||
<AspectRatio ratio={8.5 / 11} w="100%" minH="70vh">
|
||||
<VStack spacing={4} w="100%" minH="70vh">
|
||||
{/* Primary: Try direct iframe embed */}
|
||||
<Box w="100%" h="70vh" borderWidth="1px" borderRadius="md" overflow="hidden">
|
||||
<iframe
|
||||
src={`${fullUrl}#view=FitH`}
|
||||
src={`${fullUrl}#view=FitH&toolbar=1`}
|
||||
title={fileName}
|
||||
style={{ border: 'none', width: '100%', height: '100%' }}
|
||||
onError={(e) => {
|
||||
console.error('PDF iframe load error:', e);
|
||||
}}
|
||||
/>
|
||||
</AspectRatio>
|
||||
</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 '../../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">
|
||||
<Text w="140px" fontSize="sm" color="gray.600">{d}</Text>
|
||||
<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}>
|
||||
<Text fontWeight="bold" textAlign="center">{s || '-:-'}</Text>
|
||||
{(() => {
|
||||
if (!s || !clubName) return null;
|
||||
if (!s) return null;
|
||||
const m = s.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
|
||||
if (!m) return null;
|
||||
const hG = parseInt(m[1], 10), aG = parseInt(m[2], 10);
|
||||
|
||||
// First try ID-based matching (most reliable)
|
||||
let ourHome = false;
|
||||
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 ourHome = (() => { const A = strip(h); const B = strip(clubName); return A && B && (A===B || A.endsWith(B) || B.endsWith(A)); })();
|
||||
const ourAway = (() => { const A = strip(a); const B = strip(clubName); return A && B && (A===B || A.endsWith(B) || B.endsWith(A)); })();
|
||||
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 (hG === aG) return <Badge colorScheme="blue" variant="subtle">Remíza</Badge>;
|
||||
const our = ourHome ? hG : aG; const opp = ourHome ? aG : hG;
|
||||
@@ -135,7 +151,7 @@ const CompetitionMatches: React.FC = () => {
|
||||
<TabPanel key={c.id} px={0}>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{(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 && (
|
||||
<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 };
|
||||
score?: 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">
|
||||
<Text w="140px" fontSize="sm" color="gray.600">{date}</Text>
|
||||
<HStack flex={1} justify="flex-end">
|
||||
@@ -32,14 +33,30 @@ const MatchRow: React.FC<{
|
||||
<Text fontWeight="bold" textAlign="center">{score || '-:-'}</Text>
|
||||
{(() => {
|
||||
const sent = (() => {
|
||||
if (!score || !clubName) return null;
|
||||
if (!score) return null;
|
||||
const m = score.match(/^(\d+)\s*[:\-]\s*(\d+)$/);
|
||||
if (!m) return null;
|
||||
const h = parseInt(m[1], 10), a = parseInt(m[2], 10);
|
||||
|
||||
// First try ID-based matching (most reliable)
|
||||
let ourIsHome = false;
|
||||
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 ourIsHome = (() => { const aName = strip(home.name); const bName = strip(clubName); return aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)); })();
|
||||
const ourIsAway = (() => { const aName = strip(away.name); const bName = strip(clubName); return aName && bName && (aName===bName || aName.endsWith(bName) || bName.endsWith(aName)); })();
|
||||
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 (h === a) return { label: 'Remíza', color: 'blue' } as const;
|
||||
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 }}
|
||||
score={m.score}
|
||||
clubName={data.name}
|
||||
clubId={data.club_internal_id}
|
||||
/>
|
||||
))}
|
||||
{(c.matches || []).length === 0 && (
|
||||
|
||||
@@ -22,6 +22,8 @@ type MatchItem = {
|
||||
time: string; // HH:MM
|
||||
home: string;
|
||||
away: string;
|
||||
home_id?: string;
|
||||
away_id?: string;
|
||||
venue?: string;
|
||||
home_logo_url?: string;
|
||||
away_logo_url?: string;
|
||||
@@ -51,6 +53,7 @@ const CalendarPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const toast = useToast();
|
||||
const [clubName, setClubName] = useState<string>('');
|
||||
const [clubId, setClubId] = useState<string>('');
|
||||
const [clubType, setClubType] = useState<'football' | 'futsal'>('football');
|
||||
const [standings, setStandings] = useState<any[]>([]);
|
||||
|
||||
@@ -264,6 +267,8 @@ const CalendarPage: React.FC = () => {
|
||||
time,
|
||||
home: m.home,
|
||||
away: m.away,
|
||||
home_id: m.home_id,
|
||||
away_id: m.away_id,
|
||||
venue: m.venue,
|
||||
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
||||
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
||||
@@ -307,6 +312,8 @@ const CalendarPage: React.FC = () => {
|
||||
time,
|
||||
home: m.home,
|
||||
away: m.away,
|
||||
home_id: m.home_id,
|
||||
away_id: m.away_id,
|
||||
venue: m.venue,
|
||||
home_logo_url: getOverrideLogo(m.home, m.home_logo_url),
|
||||
away_logo_url: getOverrideLogo(m.away, m.away_logo_url),
|
||||
@@ -399,6 +406,7 @@ const CalendarPage: React.FC = () => {
|
||||
setCompLinks(compLinkMap);
|
||||
setStandings(standingsData);
|
||||
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);
|
||||
// Set active tab from query ?comp=<id>
|
||||
const compQ = searchParams.get('comp');
|
||||
@@ -521,8 +529,21 @@ const CalendarPage: React.FC = () => {
|
||||
|
||||
const s = parseScore(m.score);
|
||||
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 (s.h === s.a) return { label: 'Remíza', color: 'blue' };
|
||||
const ourGoals = ourIsHome ? s.h : s.a;
|
||||
|
||||
@@ -15,6 +15,8 @@ type MatchItem = {
|
||||
time: string;
|
||||
home: string;
|
||||
away: string;
|
||||
home_id?: string;
|
||||
away_id?: string;
|
||||
home_logo_url?: string;
|
||||
away_logo_url?: string;
|
||||
score?: string | null;
|
||||
@@ -23,7 +25,7 @@ type MatchItem = {
|
||||
venue?: string;
|
||||
competition?: string;
|
||||
competitionName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const MatchesPage: React.FC = () => {
|
||||
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 s = parseScore(m.score);
|
||||
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 (s.h === s.a) return { label: 'Remíza', colorScheme: 'blue' };
|
||||
const ourGoals = ourIsHome ? s.h : s.a;
|
||||
@@ -343,6 +358,8 @@ const MatchesPage: React.FC = () => {
|
||||
time,
|
||||
home: m.home,
|
||||
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),
|
||||
away_logo_url: getOverrideLogo(m.away, m.away_id, m.away_logo_url),
|
||||
score: actualScore,
|
||||
|
||||
@@ -36,6 +36,12 @@ const MatchLinkBadge: React.FC<{ articleId: number }> = ({ articleId }) => {
|
||||
staleTime: 60_000,
|
||||
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;
|
||||
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
|
||||
createArticle(payload),
|
||||
onSuccess: async (created: any) => {
|
||||
try {
|
||||
// If a match was selected (from temp storage), link it now that we have an article ID
|
||||
const matchRaw = tempMatchLink || matchIdInput;
|
||||
const matchToLink = typeof matchRaw === 'string' ? matchRaw : String(matchRaw || '');
|
||||
const matchId = matchToLink.trim();
|
||||
if (matchId && created?.id) {
|
||||
await putArticleMatchLink(created.id, { external_match_id: matchId, title: (editing as any)?.title || '' });
|
||||
setLinkedMatchId(matchId);
|
||||
toast({
|
||||
title: 'Článek vytvořen a propojen se zápasem',
|
||||
description: `Match ID: ${matchId}`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: 'Článek byl úspěšně vytvořen',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
}
|
||||
// Clear temporary storage after successful creation
|
||||
console.log('Article created successfully in mutation callback:', created);
|
||||
// Note: Match linking is now handled in onSubmit() to avoid race conditions
|
||||
// Clear temporary storage
|
||||
setTempMatchLink('');
|
||||
} finally {
|
||||
setMatchIdInput('');
|
||||
|
||||
// Invalidate queries to refresh the list
|
||||
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
||||
qc.invalidateQueries({ queryKey: ['article-match-link'] }); // Invalidate match links
|
||||
closeModal();
|
||||
}
|
||||
|
||||
// Don't close modal here - let onSubmit handle it after match linking
|
||||
},
|
||||
onError: (e: any) => {
|
||||
console.error('Error creating article:', e);
|
||||
@@ -702,18 +689,16 @@ const ArticlesAdminPage = () => {
|
||||
updateArticle(id, payload),
|
||||
onSuccess: (_, variables) => {
|
||||
const articleId = variables.id;
|
||||
toast({
|
||||
title: 'Článek byl úspěšně aktualizován',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true
|
||||
});
|
||||
console.log('Article updated successfully in mutation callback:', articleId);
|
||||
|
||||
// Invalidate queries to refresh the list
|
||||
qc.invalidateQueries({ queryKey: ['admin-articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['articles'] });
|
||||
qc.invalidateQueries({ queryKey: ['recentArticles'] });
|
||||
qc.invalidateQueries({ queryKey: ['article-match-link', articleId] }); // Invalidate specific match link
|
||||
qc.invalidateQueries({ queryKey: ['article', `id:${articleId}`] }); // Invalidate article detail
|
||||
closeModal();
|
||||
|
||||
// Success toast and modal closing handled in onSubmit()
|
||||
},
|
||||
onError: (e: any) => {
|
||||
console.error('Error updating article:', e);
|
||||
@@ -864,7 +849,7 @@ const ArticlesAdminPage = () => {
|
||||
} catch { return undefined; }
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = async (options: { keepOpen?: boolean } = {}) => {
|
||||
if (!editing) return;
|
||||
// Require category selection by name (kategorie je povinná)
|
||||
if (!String((editing as any)?.category_name || '').trim()) {
|
||||
@@ -932,10 +917,83 @@ const ArticlesAdminPage = () => {
|
||||
// Log the payload for debugging
|
||||
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) {
|
||||
// Update existing article
|
||||
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 {
|
||||
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) {
|
||||
@@ -1781,13 +1839,30 @@ const ArticlesAdminPage = () => {
|
||||
qc.invalidateQueries({ queryKey: ['linked-polls'] });
|
||||
}} />
|
||||
) : (
|
||||
<Alert status="info" borderRadius="md">
|
||||
<Alert status="warning" borderRadius="md">
|
||||
<AlertIcon />
|
||||
<VStack align="start" spacing={1}>
|
||||
<Text fontWeight="semibold">Nejprve uložte článek</Text>
|
||||
<VStack align="start" spacing={2}>
|
||||
<Text fontWeight="semibold">Článek ještě není uložen</Text>
|
||||
<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>
|
||||
<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>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -1798,7 +1873,7 @@ const ArticlesAdminPage = () => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<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>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
Reference in new issue
Block a user