diff --git a/cache/prefetch/articles.json.hdr b/cache/prefetch/articles.json.hdr index 48f058c..80f2695 100644 --- a/cache/prefetch/articles.json.hdr +++ b/cache/prefetch/articles.json.hdr @@ -1 +1 @@ -{"etag":"","fetched_at":"2025-10-24T14:27:54Z","last_modified":""} \ No newline at end of file +{"etag":"","fetched_at":"2025-10-24T15:58:00Z","last_modified":""} \ No newline at end of file diff --git a/cache/prefetch/competition_aliases.json.hdr b/cache/prefetch/competition_aliases.json.hdr index 48f058c..80f2695 100644 --- a/cache/prefetch/competition_aliases.json.hdr +++ b/cache/prefetch/competition_aliases.json.hdr @@ -1 +1 @@ -{"etag":"","fetched_at":"2025-10-24T14:27:54Z","last_modified":""} \ No newline at end of file +{"etag":"","fetched_at":"2025-10-24T15:58:00Z","last_modified":""} \ No newline at end of file diff --git a/cache/prefetch/events_upcoming.json.hdr b/cache/prefetch/events_upcoming.json.hdr index 48f058c..80f2695 100644 --- a/cache/prefetch/events_upcoming.json.hdr +++ b/cache/prefetch/events_upcoming.json.hdr @@ -1 +1 @@ -{"etag":"","fetched_at":"2025-10-24T14:27:54Z","last_modified":""} \ No newline at end of file +{"etag":"","fetched_at":"2025-10-24T15:58:00Z","last_modified":""} \ No newline at end of file diff --git a/cache/prefetch/meta.json b/cache/prefetch/meta.json index 7bae4f7..397de74 100644 --- a/cache/prefetch/meta.json +++ b/cache/prefetch/meta.json @@ -1 +1 @@ -{"lastUpdated":"2025-10-24T14:27:54Z"} \ No newline at end of file +{"lastUpdated":"2025-10-24T15:58:00Z"} \ No newline at end of file diff --git a/cache/prefetch/prefetch_status.json b/cache/prefetch/prefetch_status.json index 5493da9..f4bc963 100644 --- a/cache/prefetch/prefetch_status.json +++ b/cache/prefetch/prefetch_status.json @@ -1,6 +1,6 @@ { "baseURL": "http://127.0.0.1:8080/api/v1", - "duration_ms": 15, + "duration_ms": 34, "endpoints": [ { "path": "/settings", @@ -33,5 +33,5 @@ "ok": true } ], - "lastUpdated": "2025-10-24T14:27:54Z" + "lastUpdated": "2025-10-24T15:58:00Z" } \ No newline at end of file diff --git a/cache/prefetch/settings.json.hdr b/cache/prefetch/settings.json.hdr index 48f058c..80f2695 100644 --- a/cache/prefetch/settings.json.hdr +++ b/cache/prefetch/settings.json.hdr @@ -1 +1 @@ -{"etag":"","fetched_at":"2025-10-24T14:27:54Z","last_modified":""} \ No newline at end of file +{"etag":"","fetched_at":"2025-10-24T15:58:00Z","last_modified":""} \ No newline at end of file diff --git a/cache/prefetch/sponsors.json.hdr b/cache/prefetch/sponsors.json.hdr index 48f058c..80f2695 100644 --- a/cache/prefetch/sponsors.json.hdr +++ b/cache/prefetch/sponsors.json.hdr @@ -1 +1 @@ -{"etag":"","fetched_at":"2025-10-24T14:27:54Z","last_modified":""} \ No newline at end of file +{"etag":"","fetched_at":"2025-10-24T15:58:00Z","last_modified":""} \ No newline at end of file diff --git a/cache/prefetch/team_logo_overrides.json.hdr b/cache/prefetch/team_logo_overrides.json.hdr index 48f058c..80f2695 100644 --- a/cache/prefetch/team_logo_overrides.json.hdr +++ b/cache/prefetch/team_logo_overrides.json.hdr @@ -1 +1 @@ -{"etag":"","fetched_at":"2025-10-24T14:27:54Z","last_modified":""} \ No newline at end of file +{"etag":"","fetched_at":"2025-10-24T15:58:00Z","last_modified":""} \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 21005e1..9d0c2e3 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -51,6 +51,7 @@ import { getArticles } from '../services/articles'; import { getCachedYouTube } from '../services/youtube'; import { getZoneramaManifestWithFallbacks } from '../services/zonerama'; import { getMyNewsletterToken } from '../services/public/newsletter'; +import { API_URL } from '../services/api'; type NavLink = { label: string; to?: string; items?: { label: string; to: string }[]; external?: boolean }; @@ -327,8 +328,7 @@ const Navbar: React.FC<{ fullWidth?: boolean }> = ({ fullWidth = false }) => { if (!url) return; // Normalize relative upload paths to API origin so favicon resolves on all pages try { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const apiOrigin = new URL(apiUrl).origin; + const apiOrigin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; if (/^\/.+/.test(url) && !/^https?:\/\//i.test(url)) { // If starts with /uploads or any absolute path, prefix API origin url = apiOrigin + url; @@ -420,8 +420,7 @@ const Navbar: React.FC<{ fullWidth?: boolean }> = ({ fullWidth = false }) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(base).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; return new URL(path, origin).toString(); } return path; diff --git a/frontend/src/components/common/SponsorsSection.tsx b/frontend/src/components/common/SponsorsSection.tsx index 14bafe3..cb86b43 100644 --- a/frontend/src/components/common/SponsorsSection.tsx +++ b/frontend/src/components/common/SponsorsSection.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { assetUrl } from '../../utils/url'; +import { API_URL } from '../../services/api'; interface Sponsor { id: number | string; @@ -18,9 +19,8 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const b = new URL(base); - const abs = new URL(path, `${b.protocol}//${b.host}`); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const abs = new URL(path, origin); return abs.toString(); } return path; @@ -42,7 +42,7 @@ const SponsorsSection: React.FC = ({ const fetchSponsors = async () => { try { // Try API first - const apiRes = await fetch(`${process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'}/public/sponsors`); + const apiRes = await fetch(`${API_URL}/public/sponsors`); if (apiRes.ok) { const data = await apiRes.json(); if (!cancelled && Array.isArray(data)) { diff --git a/frontend/src/components/home/GallerySection.tsx b/frontend/src/components/home/GallerySection.tsx index 0cb3949..bed37fb 100644 --- a/frontend/src/components/home/GallerySection.tsx +++ b/frontend/src/components/home/GallerySection.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { API_URL } from '../../services/api'; import { Link as RouterLink } from 'react-router-dom'; import { Box, @@ -33,9 +34,8 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const b = new URL(base); - const abs = new URL(path, `${b.protocol}//${b.host}`); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const abs = new URL(path, origin); return abs.toString(); } return path; diff --git a/frontend/src/components/home/PhotosSection.tsx b/frontend/src/components/home/PhotosSection.tsx index 9f2d219..368cd49 100644 --- a/frontend/src/components/home/PhotosSection.tsx +++ b/frontend/src/components/home/PhotosSection.tsx @@ -1,5 +1,6 @@ import { Box, Grid, GridItem, Heading, Image, Button, HStack, Text, VStack, Badge } from '@chakra-ui/react'; import React, { useEffect, useState } from 'react'; +import { API_URL } from '../../services/api'; import { Link as RouterLink } from 'react-router-dom'; import { Calendar, Image as ImageIcon } from 'lucide-react'; @@ -22,9 +23,8 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = (process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'); - const b = new URL(base); - const abs = new URL(path, `${b.protocol}//${b.host}`); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const abs = new URL(path, origin); return abs.toString(); } return path; @@ -39,8 +39,7 @@ const PhotosSection: React.FC<{ zoneramaUrl?: string | null }> = ({ zoneramaUrl let active = true; (async () => { try { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const response = await fetch(`${apiUrl}/gallery/albums`); + const response = await fetch(`${API_URL}/gallery/albums`); if (response.ok) { const data = await response.json(); diff --git a/frontend/src/components/layout/Footer.tsx b/frontend/src/components/layout/Footer.tsx index 9ff1099..a7bd3a2 100644 --- a/frontend/src/components/layout/Footer.tsx +++ b/frontend/src/components/layout/Footer.tsx @@ -6,15 +6,14 @@ import { trackNavigation } from '../../utils/umami'; import { useClubTheme } from '../../contexts/ClubThemeContext'; import { usePublicSettings } from '../../hooks/usePublicSettings'; import { assetUrl } from '../../utils/url'; +import { API_URL } from '../../services/api'; const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const u = new URL(base); - u.pathname = path; - return u.toString(); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + return new URL(path, origin).toString(); } return path; } catch { @@ -59,8 +58,7 @@ const Footer: React.FC = () => { } catch {} // Fetch sponsors try { - const apiUrl = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const sponsorsRes = await fetch(`${apiUrl}/public/sponsors`); + const sponsorsRes = await fetch(`${API_URL}/public/sponsors`); if (sponsorsRes.ok) { const data = await sponsorsRes.json(); if (!cancelled && Array.isArray(data)) { diff --git a/frontend/src/components/widgets/MatchesWidget.tsx b/frontend/src/components/widgets/MatchesWidget.tsx index 0236eed..079ddf5 100644 --- a/frontend/src/components/widgets/MatchesWidget.tsx +++ b/frontend/src/components/widgets/MatchesWidget.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; import { FaCalendarAlt, FaFutbol, FaExclamationTriangle, FaMapMarkerAlt } from 'react-icons/fa'; import { useQuery } from '@tanstack/react-query'; -import { api } from '../../services/api'; +import { api, API_URL } from '../../services/api'; import { useSettings } from '@/hooks/useSettings'; import { Widget } from './Widget'; import { format, parse, isToday, isTomorrow, isAfter } from 'date-fns'; @@ -46,8 +46,7 @@ export const MatchesWidget = () => { const resolveUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; return origin + path; } catch { return path; @@ -109,8 +108,7 @@ export const MatchesWidget = () => { } const chosen = candidate || orig; if (typeof chosen === 'string' && chosen.startsWith('/')) { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; return origin + chosen; } return chosen || (assetUrl('/dist/img/logo-club-empty.svg') as string); @@ -122,8 +120,7 @@ export const MatchesWidget = () => { queryKey: ['upcomingMatchesCache'], queryFn: async () => { // Build absolute origin from API URL env (which may include /api/v1) - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } }); diff --git a/frontend/src/pages/AlbumDetailPage.tsx b/frontend/src/pages/AlbumDetailPage.tsx index 5c965a0..66d3bac 100644 --- a/frontend/src/pages/AlbumDetailPage.tsx +++ b/frontend/src/pages/AlbumDetailPage.tsx @@ -20,6 +20,7 @@ import { } from '@chakra-ui/react'; import { ChevronRight, ExternalLink, Calendar, Image as ImageIcon } from 'lucide-react'; import MainLayout from '../components/layout/MainLayout'; +import { API_URL } from '../services/api'; import PhotoModal from '../components/gallery/PhotoModal'; interface Photo { @@ -43,9 +44,8 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = (process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'); - const b = new URL(base); - const abs = new URL(path, `${b.protocol}//${b.host}`); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const abs = new URL(path, origin); return abs.toString(); } return path; diff --git a/frontend/src/pages/ArticleDetailPage.tsx b/frontend/src/pages/ArticleDetailPage.tsx index f6d980d..47fc91b 100644 --- a/frontend/src/pages/ArticleDetailPage.tsx +++ b/frontend/src/pages/ArticleDetailPage.tsx @@ -12,6 +12,7 @@ import { ExternalLink, ArrowRight, Eye, Clock } from 'lucide-react'; import React from 'react'; import { trackEvent as umamiTrackEvent, trackMatchView as umamiTrackMatchView, trackVideoPlay as umamiTrackVideoPlay, trackArticleView as umamiTrackArticleView } from '../utils/umami'; import { assetUrl } from '../utils/url'; +import { API_URL } from '../services/api'; const toText = (html?: string) => { if (!html) return ''; @@ -60,8 +61,7 @@ const ArticleDetailPage: React.FC = () => { queryKey: ['facr-cached-match', (matchLinkQuery.data as any)?.external_match_id], enabled: Boolean((matchLinkQuery.data as any)?.external_match_id), queryFn: async () => { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { cache: 'no-cache' }); if (!res.ok) return null as any; @@ -167,8 +167,7 @@ const ArticleDetailPage: React.FC = () => { const toAbsoluteUploads = React.useCallback((html?: string) => { if (!html) return ''; try { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(base).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; // Replace src="/uploads... and href="/uploads... return html .replace(/src=("|')\s*(\/uploads\/[^"']+)("|')/g, (_m, q1, p2, q3) => `src=${q1}${origin}${p2}${q3}`) diff --git a/frontend/src/pages/BlogPage.tsx b/frontend/src/pages/BlogPage.tsx index 55bc520..7a1d9ba 100644 --- a/frontend/src/pages/BlogPage.tsx +++ b/frontend/src/pages/BlogPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Box, Container, Heading, VStack, Image, Text, Skeleton, LinkBox, HStack, Select, Badge, useColorModeValue } from '@chakra-ui/react'; import { useInfiniteQuery } from '@tanstack/react-query'; -import { getArticles, Article } from '../services/articles'; +import { getArticles, Article, Paginated } from '../services/articles'; import { Link as RouterLink } from 'react-router-dom'; import { assetUrl } from '../utils/url'; import MainLayout from '../components/layout/MainLayout'; @@ -110,9 +110,15 @@ const BlogPage: React.FC = () => { isFetchingNextPage, hasNextPage, fetchNextPage, - } = useInfiniteQuery( + } = useInfiniteQuery>( ['articles-public', { page_size: pageSize, published: true, category_id: categoryId || undefined }], - ({ pageParam = 1 }) => getArticles({ page: pageParam, page_size: pageSize, published: true, ...(categoryId ? { category_id: Number(categoryId) } : {}) }), + ({ pageParam = 1 }) => + getArticles({ + page: pageParam, + page_size: pageSize, + published: true, + ...(categoryId ? { category_id: Number(categoryId) } : {}), + }), { getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce((sum, p) => sum + (p?.data?.length || 0), 0); diff --git a/frontend/src/pages/CalendarPage.tsx b/frontend/src/pages/CalendarPage.tsx index f7d782e..e1029b7 100644 --- a/frontend/src/pages/CalendarPage.tsx +++ b/frontend/src/pages/CalendarPage.tsx @@ -12,6 +12,7 @@ import NewsletterCTA from '../components/common/NewsletterCTA'; import { sortCategoriesWithOrder } from '../utils/categorySort'; import ClubModal from '../components/home/ClubModal'; import { assetUrl } from '../utils/url'; +import { API_URL } from '../services/api'; // Weekday headers (Czech, starting Monday) const WEEKDAYS_SHORT: string[] = ['Po', 'Út', 'St', 'Čt', 'Pá', 'So', 'Ne']; @@ -91,10 +92,8 @@ const CalendarPage: React.FC = () => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const u = new URL(base); - u.pathname = path; // use backend origin root - return u.toString(); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + return new URL(path, origin).toString(); } return path; } catch { diff --git a/frontend/src/pages/GalleryPage.tsx b/frontend/src/pages/GalleryPage.tsx index a9a7a99..03e22de 100644 --- a/frontend/src/pages/GalleryPage.tsx +++ b/frontend/src/pages/GalleryPage.tsx @@ -16,6 +16,7 @@ import { } from '@chakra-ui/react'; import { Calendar, Image as ImageIcon, ExternalLink } from 'lucide-react'; import MainLayout from '../components/layout/MainLayout'; +import { API_URL } from '../services/api'; import SponsorsSection from '../components/common/SponsorsSection'; import NewsletterCTA from '../components/common/NewsletterCTA'; @@ -38,9 +39,8 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = (process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'); - const b = new URL(base); - const abs = new URL(path, `${b.protocol}//${b.host}`); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const abs = new URL(path, origin); return abs.toString(); } return path; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 2c83783..9271707 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -23,6 +23,7 @@ import MyUIbrixErrorBoundary from '../components/editor/MyUIbrixErrorBoundary'; import ClubModal from '../components/home/ClubModal'; import MatchModal from '../components/home/MatchModal'; import { useAllPageElementConfigs } from '../hooks/usePageElementConfig'; +import { API_URL } from '../services/api'; // Types for real API-driven data type NewsItem = { @@ -132,11 +133,8 @@ const HomePage: React.FC = () => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const u = new URL(base); - // We want backend origin root, not /api/v1 - u.pathname = path; - return u.toString(); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + return new URL(path, origin).toString(); } return path; } catch { diff --git a/frontend/src/pages/MatchDetailPage.tsx b/frontend/src/pages/MatchDetailPage.tsx index 7bab7aa..4fbe256 100644 --- a/frontend/src/pages/MatchDetailPage.tsx +++ b/frontend/src/pages/MatchDetailPage.tsx @@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query'; import { getCompetitionAliasesPublic } from '../services/competitionAliases'; import SponsorsSection from '../components/common/SponsorsSection'; import NewsletterCTA from '../components/common/NewsletterCTA'; +import { API_URL } from '../services/api'; interface MatchItem { id: string | number; @@ -26,10 +27,8 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const u = new URL(base); - u.pathname = path; // use backend origin root - return u.toString(); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + return new URL(path, origin).toString(); } return path; } catch { diff --git a/frontend/src/pages/admin/ArticlesAdminPage.tsx b/frontend/src/pages/admin/ArticlesAdminPage.tsx index 7882f7b..b3a067d 100644 --- a/frontend/src/pages/admin/ArticlesAdminPage.tsx +++ b/frontend/src/pages/admin/ArticlesAdminPage.tsx @@ -20,6 +20,7 @@ import { assetUrl } from '../../utils/url'; import { getPublicSettings } from '../../services/settings'; import { getZoneramaManifestWithFallbacks, getZoneramaAlbum, putZoneramaPick, saveAlbumToCache } from '../../services/zonerama'; import { facrApi } from '../../services/facr/facrApi'; +import { API_URL } from '../../services/api'; import AlbumPhotoPicker from '../../components/admin/AlbumPhotoPicker'; import PollLinker from '../../components/admin/PollLinker'; import ThumbnailPreview from '../../components/common/ThumbnailPreview'; @@ -49,8 +50,7 @@ const MatchLinkBadge: React.FC<{ articleId: number }> = ({ articleId }) => { retry: false, queryFn: async () => { try { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { cache: 'no-cache' }); if (!res.ok) return null; @@ -188,8 +188,7 @@ const ArticlesAdminPage = () => { React.useEffect(() => { (async () => { try { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } }); if (!res.ok) return; @@ -495,13 +494,12 @@ const ArticlesAdminPage = () => { try { setZLoading(true); // Use correct API endpoint format based on album-api.md - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; const params = new URLSearchParams({ link: link, photo_limit: '48', rendered: 'true' }); - const res = await fetch(`${apiUrl}/zonerama-album?${params.toString()}`); + const res = await fetch(`${API_URL}/zonerama-album?${params.toString()}`); if (!res.ok) throw new Error('Failed to fetch album'); const data = await res.json(); diff --git a/frontend/src/pages/admin/CompetitionAliasesAdminPage.tsx b/frontend/src/pages/admin/CompetitionAliasesAdminPage.tsx index f522b5d..b0e91b2 100644 --- a/frontend/src/pages/admin/CompetitionAliasesAdminPage.tsx +++ b/frontend/src/pages/admin/CompetitionAliasesAdminPage.tsx @@ -38,6 +38,7 @@ import { } from '../../services/competitionAliases'; import AdminLayout from '../../layouts/AdminLayout'; import { PageHeader } from '../../components/admin/PageHeader'; +import { API_URL } from '../../services/api'; const CompetitionAliasesAdminPage: React.FC = () => { const cardBg = useColorModeValue('white', 'gray.800'); @@ -79,10 +80,8 @@ const CompetitionAliasesAdminPage: React.FC = () => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads')) { - const base = (process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'); - const u = new URL(base); - u.pathname = path; - return u.toString(); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + return new URL(path, origin).toString(); } return path; } catch { @@ -124,10 +123,8 @@ const CompetitionAliasesAdminPage: React.FC = () => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads')) { - const base = (process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'); - const u = new URL(base); - u.pathname = path; - return u.toString(); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + return new URL(path, origin).toString(); } return path; } catch { diff --git a/frontend/src/pages/admin/FilesAdminPage.tsx b/frontend/src/pages/admin/FilesAdminPage.tsx index 3234cfe..88596c9 100644 --- a/frontend/src/pages/admin/FilesAdminPage.tsx +++ b/frontend/src/pages/admin/FilesAdminPage.tsx @@ -63,6 +63,7 @@ import { formatFileSize, getFileIcon, } from '../../services/files'; +import { API_URL } from '../../services/api'; const FilesAdminPage: React.FC = () => { const toast = useToast(); @@ -187,8 +188,7 @@ const FilesAdminPage: React.FC = () => { const getImageUrl = (url: string) => { if (url.startsWith('http')) return url; - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; return `${origin}${url}`; }; diff --git a/frontend/src/pages/admin/MatchesAdminPage.tsx b/frontend/src/pages/admin/MatchesAdminPage.tsx index 34418ec..33ba04a 100644 --- a/frontend/src/pages/admin/MatchesAdminPage.tsx +++ b/frontend/src/pages/admin/MatchesAdminPage.tsx @@ -46,6 +46,7 @@ import { useSearchParams } from 'react-router-dom'; import { parse } from 'date-fns'; import { assetUrl } from '../../utils/url'; import { batchFetchLogosFromSportLogosAPI } from '../../utils/sportLogosAPI'; +import { API_URL } from '../../services/api'; const MatchesAdminPage = () => { const queryClient = useQueryClient(); @@ -155,8 +156,7 @@ const MatchesAdminPage = () => { queryKey: ['admin-matches-list-cache'], queryFn: async () => { // Read cached FACR club info - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } }); if (!res.ok) throw new Error(`Failed to load cache: ${res.status}`); @@ -225,8 +225,7 @@ const MatchesAdminPage = () => { const { data: facrClubInfo } = useQuery({ queryKey: ['facr-club-info-name'], queryFn: async () => { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } }); if (!res.ok) return null; diff --git a/frontend/src/pages/admin/PlayersAdminPage.tsx b/frontend/src/pages/admin/PlayersAdminPage.tsx index 49fe791..8d98343 100644 --- a/frontend/src/pages/admin/PlayersAdminPage.tsx +++ b/frontend/src/pages/admin/PlayersAdminPage.tsx @@ -42,6 +42,7 @@ import { Player, getPlayers, createPlayer, updatePlayer, deletePlayer } from '.. import { uploadFile } from '../../services/articles'; import { translateNationality } from '../../utils/nationality'; import ThumbnailPreview from '../../components/common/ThumbnailPreview'; +import { API_URL } from '../../services/api'; type Editing = Partial & { id?: number }; @@ -55,8 +56,7 @@ const PlayersAdminPage: React.FC = () => { // If it's already absolute, return as-is if (/^https?:\/\//i.test(url)) return url; // If it's an uploads path, prefix with API origin - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; if (url.startsWith('/uploads/')) return `${origin}${url}`; // Fallback: treat as relative to origin return `${origin}${url.startsWith('/') ? '' : '/'}${url}`; diff --git a/frontend/src/pages/admin/SponsorsAdminPage.tsx b/frontend/src/pages/admin/SponsorsAdminPage.tsx index 258e001..21f0d95 100644 --- a/frontend/src/pages/admin/SponsorsAdminPage.tsx +++ b/frontend/src/pages/admin/SponsorsAdminPage.tsx @@ -39,6 +39,7 @@ import { FiEdit2, FiPlus, FiTrash2, FiUpload, FiExternalLink } from 'react-icons import AdminLayout from '../../layouts/AdminLayout'; import { Sponsor, getSponsors, createSponsor, updateSponsor, deleteSponsor } from '../../services/sponsors'; import { uploadFile } from '../../services/articles'; +import { API_URL } from '../../services/api'; const SponsorsAdminPage: React.FC = () => { const cardBg = useColorModeValue('white', 'gray.800'); @@ -47,8 +48,7 @@ const SponsorsAdminPage: React.FC = () => { const normalizeImageUrl = (url?: string) => { if (!url || url === '') return '/logo192.png'; if (/^https?:\/\//i.test(url)) return url; - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; if (url.startsWith('/uploads/')) return `${origin}${url}`; return `${origin}${url.startsWith('/') ? '' : '/'}${url}`; }; diff --git a/frontend/src/pages/admin/StandingsAdminPage.tsx b/frontend/src/pages/admin/StandingsAdminPage.tsx index f2f414c..ec13a51 100644 --- a/frontend/src/pages/admin/StandingsAdminPage.tsx +++ b/frontend/src/pages/admin/StandingsAdminPage.tsx @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'; import AdminLayout from '../../layouts/AdminLayout'; import { assetUrl } from '../../utils/url'; import { TeamLogo } from '../../components/common/TeamLogo'; +import { API_URL } from '../../services/api'; type TableRow = { rank?: string; @@ -22,8 +23,7 @@ const StandingsAdminPage: React.FC = () => { const { data, isLoading, error } = useQuery({ queryKey: ['facr-tables-cache'], queryFn: async () => { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; const url = `${origin}/cache/prefetch/facr_tables.json`; const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } }); if (!res.ok) throw new Error(`Failed to load cache: ${res.status}`); diff --git a/frontend/src/pages/admin/TeamsAdminPage.tsx b/frontend/src/pages/admin/TeamsAdminPage.tsx index 254f9ad..9a3c403 100644 --- a/frontend/src/pages/admin/TeamsAdminPage.tsx +++ b/frontend/src/pages/admin/TeamsAdminPage.tsx @@ -51,6 +51,7 @@ import { searchClubs, uploadImage, putTeamLogoOverride, fetchTeamLogoOverrides, import { getFacrTablesCache } from '../../services/facr/cache'; import { assetUrl } from '../../utils/url'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { API_URL } from '../../services/api'; type TableRow = { @@ -77,8 +78,7 @@ const TeamsAdminPage = () => { const competitions: any[] = Array.isArray(data?.competitions) ? data!.competitions : []; // Backend origin (used to resolve relative URLs like /uploads/...) - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const backendOrigin = new URL(apiUrl).origin; + const backendOrigin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; // Load public/admin overrides map to apply on cache-fed view const { data: overrides = {} } = useQuery({ diff --git a/frontend/src/services/clothing.ts b/frontend/src/services/clothing.ts index 0d02cb7..3f0b234 100644 --- a/frontend/src/services/clothing.ts +++ b/frontend/src/services/clothing.ts @@ -1,6 +1,5 @@ import axios from 'axios'; - -const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; +import { API_URL as API_BASE_URL } from './api'; export interface ClothingItem { id: number; diff --git a/frontend/src/services/facr/cache.ts b/frontend/src/services/facr/cache.ts index 685eed6..9e7fbc7 100644 --- a/frontend/src/services/facr/cache.ts +++ b/frontend/src/services/facr/cache.ts @@ -4,10 +4,8 @@ import { API_URL } from '../api'; function resolveBackend(path: string): string { try { if (/^https?:\/\//i.test(path)) return path; - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const u = new URL(base); - u.pathname = path.startsWith('/') ? path : `/${path}`; - return u.toString(); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + return new URL(path.startsWith('/') ? path : `/${path}`, origin).toString(); } catch { return path; } diff --git a/frontend/src/services/facr/facrApi.ts b/frontend/src/services/facr/facrApi.ts index 497ef5b..fb197f9 100644 --- a/frontend/src/services/facr/facrApi.ts +++ b/frontend/src/services/facr/facrApi.ts @@ -17,7 +17,7 @@ const cache = new Map>(); // Create axios instance with base URL from environment variables const apiClient: AxiosInstance = axios.create({ - baseURL: process.env.REACT_APP_FACR_API_BASE_URL || 'http://localhost:8080/api/v1/facr', + baseURL: process.env.REACT_APP_FACR_API_BASE_URL || '/api/v1/facr', timeout: parseInt(process.env.REACT_APP_FACR_API_TIMEOUT || '20000', 10), headers: { 'Content-Type': 'application/json', @@ -30,10 +30,12 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const baseOrigin = new URL(base).origin; + const explicit = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || ''; + const origin = explicit + ? new URL(explicit, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin + : (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'); // Use URL constructor so query strings in `path` (e.g. /api/v1/x?t=123) are handled correctly - return new URL(path, baseOrigin).toString(); + return new URL(path, origin).toString(); } return path; } catch { diff --git a/frontend/src/services/files.ts b/frontend/src/services/files.ts index 1b9809c..310fb2b 100644 --- a/frontend/src/services/files.ts +++ b/frontend/src/services/files.ts @@ -1,6 +1,7 @@ import axios from 'axios'; +import { API_URL } from './api'; -const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; +// Use shared API_URL which already resolves to '/api/v1' under current origin export interface FileInfo { id: number; diff --git a/frontend/src/services/navigation.ts b/frontend/src/services/navigation.ts index 3ca1cc9..8d6b4b5 100644 --- a/frontend/src/services/navigation.ts +++ b/frontend/src/services/navigation.ts @@ -1,6 +1,5 @@ import axios from 'axios'; - -const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; +import { API_URL as API_BASE_URL } from './api'; export interface NavigationItem { id?: number; diff --git a/frontend/src/services/pageElements.ts b/frontend/src/services/pageElements.ts index d354577..568877e 100644 --- a/frontend/src/services/pageElements.ts +++ b/frontend/src/services/pageElements.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { API_URL as API_BASE_URL } from './api'; import { IconType } from 'react-icons'; import { FaRegClipboard, @@ -35,7 +36,7 @@ import { FaCube } from 'react-icons/fa'; -const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; +// Use shared API base URL export interface PageElementConfig { id?: number; diff --git a/frontend/src/services/relatedClubs.ts b/frontend/src/services/relatedClubs.ts index a9a1455..af1d2cf 100644 --- a/frontend/src/services/relatedClubs.ts +++ b/frontend/src/services/relatedClubs.ts @@ -1,4 +1,5 @@ import { assetUrl } from '../utils/url'; +import { API_URL } from './api'; export interface RelatedClub { id: string; @@ -21,8 +22,7 @@ const resolveBackendUrl = (path: string): string => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(base).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; return new URL(path, origin).toString(); } return path; diff --git a/frontend/src/services/search.ts b/frontend/src/services/search.ts index ad9c792..a555aaa 100644 --- a/frontend/src/services/search.ts +++ b/frontend/src/services/search.ts @@ -1,4 +1,4 @@ -import api from './api'; +import api, { API_URL } from './api'; import { getArticles } from './articles'; import { getPlayers } from './public'; import { getUpcomingEvents } from './eventService'; @@ -96,8 +96,7 @@ const resolveBackendUrl = (path: string): string => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(base).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; return new URL(path, origin).toString(); } return path; diff --git a/frontend/src/services/youtube.ts b/frontend/src/services/youtube.ts index 21cb2ce..751e13f 100644 --- a/frontend/src/services/youtube.ts +++ b/frontend/src/services/youtube.ts @@ -1,4 +1,4 @@ -import api from './api'; +import api, { API_URL } from './api'; export type YouTubeVideo = { video_id: string; @@ -38,12 +38,9 @@ export const getCachedYouTube = async (): Promise // Helper: fetch static cached JSON from /cache/prefetch const fetchStaticYouTubeCache = async (): Promise => { try { - // Determine backend origin from env config similar to HomePage resolve logic - const base = (process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'); - const u = new URL(base); - // Force path to backend-exposed cache file - u.pathname = '/cache/prefetch/youtube_channel.json'; - const resp = await fetch(u.toString(), { cache: 'no-cache' }); + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const url = `${origin}/cache/prefetch/youtube_channel.json`; + const resp = await fetch(url, { cache: 'no-cache' }); if (!resp.ok) return null; const data = (await resp.json()) as YouTubeChannelPayload; return sortByPublishedDate(data); diff --git a/frontend/src/services/zonerama.ts b/frontend/src/services/zonerama.ts index 18b2b22..21e3b42 100644 --- a/frontend/src/services/zonerama.ts +++ b/frontend/src/services/zonerama.ts @@ -1,4 +1,4 @@ -import api from './api'; +import api, { API_URL } from './api'; export interface ZoneramaPhoto { id: string; @@ -60,8 +60,7 @@ export async function saveAlbumToCache(albumLink: string, photoLimit: number = 5 // Helper to read the flat manifest produced by the prefetcher for fast grid rendering export async function getZoneramaManifest(): Promise> { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; // New unified path for prefetched Zonerama items const url = `${origin}/cache/prefetch/zonerama_flat.json`; const res = await fetch(url, { cache: 'no-cache' }); @@ -81,8 +80,7 @@ export async function getZoneramaManifestWithFallbacks(): Promise 0) return primary; - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; // 1b) Backward-compat removed - old path no longer used to avoid 404 errors diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts index ace7656..a0c5eea 100644 --- a/frontend/src/utils/auth.ts +++ b/frontend/src/utils/auth.ts @@ -43,8 +43,9 @@ export const setHasAdmin = (value: boolean): void => { export const checkAdminExists = async (): Promise => { try { - const base = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const response = await fetch(`${base}/auth/admin/exists`, { + // Use shared API base URL which is normalized to '/api/v1' + const { API_URL } = await import('../services/api'); + const response = await fetch(`${API_URL}/auth/admin/exists`, { headers: { 'Accept': 'application/json' }, }); if (response.ok) { diff --git a/package-lock.json b/package-lock.json index 6a6b1c8..ac30d51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "react-markdown": "^10.1.0" + "react-markdown": "^10.1.0", + "typescript": "^5.9.3" }, "devDependencies": { "@types/geojson": "^7946.0.16" @@ -1123,6 +1124,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/package.json b/package.json index 01a91d1..67d1788 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@types/geojson": "^7946.0.16" }, "dependencies": { - "react-markdown": "^10.1.0" + "react-markdown": "^10.1.0", + "typescript": "^5.9.3" } }