mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
dev day #77
This commit is contained in:
@@ -1,24 +1,23 @@
|
||||
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { getToken } from '../utils/auth';
|
||||
|
||||
// Resolve API URL. Some code uses REACT_APP_API_URL (full api path including /api/v1),
|
||||
// others set REACT_APP_API_BASE_URL (backend origin). Normalize so baseURL always points to API root.
|
||||
const envApiUrl = process.env.REACT_APP_API_URL || process.env.REACT_APP_API_BASE_URL;
|
||||
let API_URL = envApiUrl || '/api/v1';
|
||||
function readStored(key: string): string | null {
|
||||
try { return localStorage.getItem(key); } catch { return null; }
|
||||
}
|
||||
|
||||
const storedApi = typeof window !== 'undefined' ? (readStored('fc_api_base_url') || readStored('api_base_url')) : null;
|
||||
const envApiUrl = process.env.REACT_APP_API_URL || process.env.REACT_APP_API_BASE_URL;
|
||||
let API_URL = storedApi || envApiUrl || '/api/v1';
|
||||
|
||||
// If the provided base looks like a backend origin (no /api/), append /api/v1
|
||||
try {
|
||||
const maybe = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : undefined);
|
||||
if (!/\/api\//.test(maybe.pathname)) {
|
||||
// ensure single trailing slash then append api/v1
|
||||
maybe.pathname = maybe.pathname.replace(/\/$/, '') + '/api/v1';
|
||||
API_URL = maybe.toString();
|
||||
} else {
|
||||
API_URL = maybe.toString();
|
||||
}
|
||||
} catch {
|
||||
// If URL parsing fails, keep API_URL as-is
|
||||
}
|
||||
} catch {}
|
||||
|
||||
export const api: AxiosInstance = axios.create({
|
||||
baseURL: API_URL,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Article } from './articles';
|
||||
|
||||
export interface MatchSnapshot {
|
||||
external_match_id?: string;
|
||||
competition?: string;
|
||||
date_time?: string;
|
||||
venue?: string;
|
||||
home?: string;
|
||||
away?: string;
|
||||
score?: string;
|
||||
}
|
||||
|
||||
export function stripHtml(html?: string): string {
|
||||
if (!html) return '';
|
||||
if (typeof window === 'undefined') {
|
||||
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return (div.textContent || div.innerText || '')?.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
export function composeInstagramPostFromArticle(params: {
|
||||
article: Article;
|
||||
trackingUrl: string;
|
||||
clubName?: string;
|
||||
hashtags?: string[];
|
||||
match?: MatchSnapshot | null;
|
||||
}): string {
|
||||
const { article, trackingUrl, clubName, hashtags = [], match } = params;
|
||||
const title = article.title?.trim() || '';
|
||||
const plain = stripHtml(article.content).slice(0, 280);
|
||||
const defaultTags = hashtags.length ? hashtags : [
|
||||
`#${normalizeTag(clubName || 'FKKrnov')}`,
|
||||
'#fotbal',
|
||||
'#modrazluta',
|
||||
];
|
||||
|
||||
if (match && (match.home || match.away)) {
|
||||
const home = match.home || '';
|
||||
const away = match.away || '';
|
||||
const comp = match.competition ? `${match.competition}` : '';
|
||||
const date = match.date_time ? formatDateTime(match.date_time) : '';
|
||||
const score = match.score && /\d/.test(match.score) ? match.score : '';
|
||||
|
||||
const header = `💙💛 ${clubName || 'Náš klub'}: ${title} 💛💙`;
|
||||
const lines = [
|
||||
header,
|
||||
'',
|
||||
score ? `Výsledek: ${home} ${score} ${away}` : `${home} vs ${away}`,
|
||||
comp || date ? `${comp}${comp && date ? ' • ' : ''}${date}` : '',
|
||||
match.venue ? `Místo: ${match.venue}` : '',
|
||||
'',
|
||||
plain ? `${plain}${plain.length === 280 ? '…' : ''}` : '',
|
||||
'',
|
||||
'📸 Celý článek najdeš tady 👇',
|
||||
`🔗 ${trackingUrl}`,
|
||||
'',
|
||||
defaultTags.join(' '),
|
||||
'💙💛'
|
||||
].filter(Boolean);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Informative/general article
|
||||
const header = `💙💛 ${clubName || 'Náš klub'}: ${title} 💛💙`;
|
||||
const lines = [
|
||||
header,
|
||||
'',
|
||||
plain,
|
||||
'',
|
||||
'📸 Celý článek najdeš tady 👇',
|
||||
`🔗 ${trackingUrl}`,
|
||||
'',
|
||||
defaultTags.join(' '),
|
||||
'💙💛'
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function composeInstagramPostFromActivity(params: {
|
||||
activity: any;
|
||||
trackingUrl: string;
|
||||
clubName?: string;
|
||||
hashtags?: string[];
|
||||
}): string {
|
||||
const { activity, trackingUrl, clubName, hashtags = [] } = params;
|
||||
const title = String(activity?.title || '').trim();
|
||||
const desc = stripHtml(String(activity?.description || '')).slice(0, 280);
|
||||
const date = activity?.start_time ? formatDateTime(activity.start_time) : '';
|
||||
const place = activity?.location ? String(activity.location) : '';
|
||||
|
||||
const defaultTags = hashtags.length ? hashtags : [
|
||||
`#${normalizeTag(clubName || 'FKKrnov')}`,
|
||||
'#aktivity',
|
||||
'#fotbal',
|
||||
];
|
||||
|
||||
const header = `💙💛 ${clubName || 'Náš klub'}: ${title} 💛💙`;
|
||||
const lines = [
|
||||
header,
|
||||
'',
|
||||
date || place ? `${date}${date && place ? ' • ' : ''}${place}` : '',
|
||||
desc,
|
||||
'',
|
||||
'📸 Více informací najdeš tady 👇',
|
||||
`🔗 ${trackingUrl}`,
|
||||
'',
|
||||
defaultTags.join(' '),
|
||||
'💙💛'
|
||||
].filter(Boolean);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatDateTime(dt: string): string {
|
||||
try {
|
||||
const d = new Date(dt);
|
||||
return `${d.toLocaleDateString('cs-CZ')} ${d.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
} catch {
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTag(s: string): string {
|
||||
return s
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '') // remove spaces/symbols
|
||||
.replace(/[áä]/gi, 'a')
|
||||
.replace(/[č]/gi, 'c')
|
||||
.replace(/[ď]/gi, 'd')
|
||||
.replace(/[éěë]/gi, 'e')
|
||||
.replace(/[íï]/gi, 'i')
|
||||
.replace(/[ľĺ]/gi, 'l')
|
||||
.replace(/[ň]/gi, 'n')
|
||||
.replace(/[óö]/gi, 'o')
|
||||
.replace(/[ř]/gi, 'r')
|
||||
.replace(/[š]/gi, 's')
|
||||
.replace(/[ť]/gi, 't')
|
||||
.replace(/[úůü]/gi, 'u')
|
||||
.replace(/[ý]/gi, 'y')
|
||||
.replace(/[ž]/gi, 'z');
|
||||
}
|
||||
@@ -215,6 +215,7 @@ export const ELEMENT_VARIANTS: Record<string, ElementVariant[]> = {
|
||||
{ value: 'carousel', label: 'Karusel', description: 'Horizontální karusel zápasů' },
|
||||
{ value: 'scroller', label: 'Posuvník', description: 'Plynulý horizontální posuvník' },
|
||||
{ value: 'ticker', label: 'Ticker', description: 'Úzký ticker výsledků a zápasů' },
|
||||
{ value: 'compact_split', label: 'Kompaktní rozdělený', description: 'Slider vlevo a taby jako svislé menu vpravo' },
|
||||
],
|
||||
sponsors: [
|
||||
{ value: 'grid', label: 'Mřížka', description: 'Mřížkové rozložení' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import api, { API_URL } from './api';
|
||||
import { getArticles } from './articles';
|
||||
import { getCategories } from './categories';
|
||||
import { getPlayers } from './public';
|
||||
import { getUpcomingEvents } from './eventService';
|
||||
import { getSponsors } from './sponsors';
|
||||
@@ -7,7 +8,7 @@ import facrApi from './facr/facrApi';
|
||||
import { getRelatedClubs, RelatedClub } from './relatedClubs';
|
||||
|
||||
export interface SearchResult {
|
||||
type: 'club' | 'match' | 'match_past' | 'article' | 'player' | 'event' | 'sponsor' | 'team' | 'contact' | 'gallery';
|
||||
type: 'club' | 'match' | 'match_past' | 'article' | 'player' | 'event' | 'sponsor' | 'team' | 'contact' | 'gallery' | 'category';
|
||||
id: string | number;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
@@ -32,6 +33,7 @@ export interface SearchResults {
|
||||
teams: SearchResult[];
|
||||
contacts: SearchResult[];
|
||||
gallery: SearchResult[];
|
||||
categories: SearchResult[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
@@ -128,6 +130,7 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
teams: [],
|
||||
contacts: [],
|
||||
gallery: [],
|
||||
categories: [],
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
@@ -149,6 +152,7 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
teamsRes,
|
||||
contactsRes,
|
||||
galleryRes,
|
||||
categoriesRes,
|
||||
] = await Promise.allSettled([
|
||||
relatedClubsPromise,
|
||||
facrApi.searchClubs(query).catch(() => ({ results: [] })),
|
||||
@@ -243,6 +247,7 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
return [];
|
||||
}
|
||||
})(),
|
||||
getCategories(),
|
||||
]);
|
||||
|
||||
const relatedClubs: RelatedClub[] = relatedClubsRes.status === 'fulfilled' ? relatedClubsRes.value : [];
|
||||
@@ -336,6 +341,7 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
home_logo_url: m.home_logo_url,
|
||||
away_logo_url: m.away_logo_url,
|
||||
venue: m.venue,
|
||||
external_match_id: m.match_id || m.id,
|
||||
},
|
||||
score: Math.max(
|
||||
scoreMatch(m.home || '', q),
|
||||
@@ -380,6 +386,7 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
home_logo_url: m.home_logo_url,
|
||||
away_logo_url: m.away_logo_url,
|
||||
venue: m.venue,
|
||||
external_match_id: m.match_id || m.id,
|
||||
},
|
||||
score: Math.max(
|
||||
scoreMatch(m.home || '', q),
|
||||
@@ -389,14 +396,15 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
),
|
||||
})).sort((a: SearchResult, b: SearchResult) => (b.score || 0) - (a.score || 0));
|
||||
|
||||
// Process articles
|
||||
// Process articles (base by query)
|
||||
const articlesData = articlesRes.status === 'fulfilled' ? articlesRes.value?.data || [] : [];
|
||||
const articles: SearchResult[] = articlesData
|
||||
const baseArticles: SearchResult[] = articlesData
|
||||
.filter((a: any) => {
|
||||
const titleMatch = scoreMatch(a.title || '', q);
|
||||
const excerptMatch = scoreMatch(a.excerpt || '', q);
|
||||
const contentMatch = scoreMatch(a.content || '', q);
|
||||
return titleMatch > 0 || excerptMatch > 0 || contentMatch > 0;
|
||||
const categoryMatch = scoreMatch((a?.category?.name || a?.category_name || '') as string, q);
|
||||
return titleMatch > 0 || excerptMatch > 0 || contentMatch > 0 || categoryMatch > 0;
|
||||
})
|
||||
.map((a: any) => ({
|
||||
type: 'article' as const,
|
||||
@@ -409,11 +417,56 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
score: Math.max(
|
||||
scoreMatch(a.title || '', q),
|
||||
scoreMatch(a.excerpt || '', q) * 0.7,
|
||||
scoreMatch(a.content || '', q) * 0.3
|
||||
scoreMatch(a.content || '', q) * 0.3,
|
||||
scoreMatch((a?.category?.name || a?.category_name || '') as string, q) * 0.8
|
||||
),
|
||||
}))
|
||||
.sort((a: SearchResult, b: SearchResult) => (b.score || 0) - (a.score || 0));
|
||||
|
||||
// Enrichment: articles linked to top matched matches
|
||||
const collectTopMatchIds = (arr: SearchResult[], limit: number): string[] => {
|
||||
const out: string[] = [];
|
||||
for (const item of arr) {
|
||||
if (out.length >= limit) break;
|
||||
const mid = String(item?.metadata?.external_match_id || '').trim();
|
||||
if (mid && !out.includes(mid)) out.push(mid);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const topUpcoming = collectTopMatchIds(matches, 3);
|
||||
const topPast = collectTopMatchIds(matchesPast, 3);
|
||||
const matchIds = Array.from(new Set([...topUpcoming, ...topPast])).slice(0, 5);
|
||||
let linkedArticlesData: any[] = [];
|
||||
if (matchIds.length > 0) {
|
||||
const linkedSettled = await Promise.allSettled(
|
||||
matchIds.map((id) => getArticles({ match_id: id, published: true, page: 1, page_size: 10 }))
|
||||
);
|
||||
for (const r of linkedSettled) {
|
||||
if (r.status === 'fulfilled' && r.value && Array.isArray((r.value as any).data)) {
|
||||
linkedArticlesData.push(...((r.value as any).data as any[]));
|
||||
}
|
||||
}
|
||||
}
|
||||
const mapArticle = (a: any): SearchResult => ({
|
||||
type: 'article' as const,
|
||||
id: a.id,
|
||||
title: a.title,
|
||||
description: a.excerpt,
|
||||
image_url: a.image_url,
|
||||
url: `/blog/${a.slug || a.id}`,
|
||||
date: a.published_at || a.created_at,
|
||||
score: Math.max(
|
||||
scoreMatch(a.title || '', q),
|
||||
scoreMatch(a.excerpt || '', q) * 0.7,
|
||||
scoreMatch(a.content || '', q) * 0.3,
|
||||
scoreMatch((a?.category?.name || a?.category_name || '') as string, q) * 0.8
|
||||
) + 5, // slight boost for match-linked relevance
|
||||
});
|
||||
const linkedMapped: SearchResult[] = (linkedArticlesData || []).map(mapArticle);
|
||||
const seen = new Set<number | string>(baseArticles.map((a) => a.id));
|
||||
const linkedUnique = linkedMapped.filter((a) => !seen.has(a.id));
|
||||
const articles: SearchResult[] = [...baseArticles, ...linkedUnique].sort((a, b) => (b.score || 0) - (a.score || 0));
|
||||
|
||||
// Process players
|
||||
const playersData = playersRes.status === 'fulfilled' ? playersRes.value : [];
|
||||
const players: SearchResult[] = (Array.isArray(playersData) ? playersData : [])
|
||||
@@ -551,6 +604,20 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
}))
|
||||
.sort((a: any, b: any) => (b.score || 0) - (a.score || 0));
|
||||
|
||||
// Process categories
|
||||
const categoriesList: any[] = categoriesRes && categoriesRes.status === 'fulfilled' ? (categoriesRes.value as any[]) : [];
|
||||
const categories: SearchResult[] = (Array.isArray(categoriesList) ? categoriesList : [])
|
||||
.filter((c: any) => scoreMatch(c?.name || '', q) > 0)
|
||||
.map((c: any) => ({
|
||||
type: 'category' as const,
|
||||
id: c.id,
|
||||
title: c.name,
|
||||
description: c.description,
|
||||
url: `/blog?category_id=${c.id}`,
|
||||
score: scoreMatch(c?.name || '', q),
|
||||
}))
|
||||
.sort((a: any, b: any) => (b.score || 0) - (a.score || 0));
|
||||
|
||||
const total =
|
||||
clubs.length +
|
||||
matches.length +
|
||||
@@ -561,7 +628,8 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
sponsors.length +
|
||||
teams.length +
|
||||
contacts.length +
|
||||
gallery.length;
|
||||
gallery.length +
|
||||
categories.length;
|
||||
|
||||
return {
|
||||
clubs,
|
||||
@@ -574,6 +642,7 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
teams,
|
||||
contacts,
|
||||
gallery,
|
||||
categories,
|
||||
total,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -589,6 +658,7 @@ export async function searchAll(query: string): Promise<SearchResults> {
|
||||
teams: [],
|
||||
contacts: [],
|
||||
gallery: [],
|
||||
categories: [],
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user