Added scoreboard integration
Some checks failed
Deploy Volleyball CMS / deploy (push) Failing after 13s
Some checks failed
Deploy Volleyball CMS / deploy (push) Failing after 13s
This commit is contained in:
parent
3aff4b89f9
commit
8b516d6f68
@ -1 +1 @@
|
|||||||
VITE_API_URL=http://localhost:5000
|
VITE_API_URL=http://192.168.50.65:3000
|
||||||
|
|||||||
30
src/App.tsx
30
src/App.tsx
@ -14,20 +14,22 @@ const queryClient = new QueryClient();
|
|||||||
|
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<QueryClientProvider client={queryClient}>
|
<div className="min-h-screen px-4 py-8 max-w-7xl mx-auto bg-[hsl(var(--background))] text-[hsl(var(--foreground))] transition-colors duration-300">
|
||||||
<TooltipProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster />
|
<TooltipProvider>
|
||||||
<Sonner />
|
<Toaster />
|
||||||
<BrowserRouter>
|
<Sonner />
|
||||||
<ScrollToTop />
|
<BrowserRouter>
|
||||||
<Routes>
|
<ScrollToTop />
|
||||||
<Route path="/" element={<Index />} />
|
<Routes>
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@ -38,6 +38,9 @@ const TeamDetail = () => {
|
|||||||
const [players, setPlayers] = useState<Player[]>([]);
|
const [players, setPlayers] = useState<Player[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const [spielelink, setSpielelink] = useState("");
|
||||||
|
const [scraper_Identifier, setScraperIdentifier] = useState("");
|
||||||
|
|
||||||
const fetchTeam = async () => {
|
const fetchTeam = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${apiBase}/api/teams/${id}`);
|
const res = await fetch(`${apiBase}/api/teams/${id}`);
|
||||||
@ -57,6 +60,10 @@ const TeamDetail = () => {
|
|||||||
setBeschreibung(data.beschreibung ?? "");
|
setBeschreibung(data.beschreibung ?? "");
|
||||||
setTabellenlink(data.tabellenlink ?? "");
|
setTabellenlink(data.tabellenlink ?? "");
|
||||||
setPlayers(data.players ?? []);
|
setPlayers(data.players ?? []);
|
||||||
|
|
||||||
|
setSpielelink(data.spielelink ?? "");
|
||||||
|
setScraperIdentifier(data.scraper_identifier ?? "");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Fehler beim Laden des Teams:", err);
|
console.error("Fehler beim Laden des Teams:", err);
|
||||||
} finally {
|
} finally {
|
||||||
@ -87,6 +94,8 @@ const TeamDetail = () => {
|
|||||||
teamfarben,
|
teamfarben,
|
||||||
beschreibung,
|
beschreibung,
|
||||||
tabellenlink,
|
tabellenlink,
|
||||||
|
spielelink,
|
||||||
|
scraper_Identifier
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,6 +175,17 @@ const TeamDetail = () => {
|
|||||||
<Textarea value={beschreibung} onChange={(e) => setBeschreibung(e.target.value)} placeholder="Beschreibung" />
|
<Textarea value={beschreibung} onChange={(e) => setBeschreibung(e.target.value)} placeholder="Beschreibung" />
|
||||||
<Input value={tabellenlink} onChange={(e) => setTabellenlink(e.target.value)} placeholder="Link zur Tabelle" />
|
<Input value={tabellenlink} onChange={(e) => setTabellenlink(e.target.value)} placeholder="Link zur Tabelle" />
|
||||||
<Input value={socialMedia} onChange={(e) => setSocialMedia(e.target.value)} placeholder="Social Media Link" />
|
<Input value={socialMedia} onChange={(e) => setSocialMedia(e.target.value)} placeholder="Social Media Link" />
|
||||||
|
<Input
|
||||||
|
value={spielelink}
|
||||||
|
onChange={(e) => setSpielelink(e.target.value)}
|
||||||
|
placeholder="Link zu den Spielen"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={scraper_Identifier}
|
||||||
|
onChange={(e) => setScraperIdentifier(e.target.value)}
|
||||||
|
placeholder="Scraper-Identifier"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -100,7 +100,7 @@ const TeamSection = () => {
|
|||||||
<div ref={sliderRef} className="keen-slider">
|
<div ref={sliderRef} className="keen-slider">
|
||||||
{teams.map((team) => (
|
{teams.map((team) => (
|
||||||
<div key={team.id} className="keen-slider__slide">
|
<div key={team.id} className="keen-slider__slide">
|
||||||
<Card className="h-full hover:shadow-lg transition-shadow bg-white dark:bg-neutral-800 text-gray-800 dark:text-gray-100 border border-gray-200 dark:border-neutral-700 border-t-4 border-t-frog-500 dark:border-t-frog-400">
|
<Card className="flex flex-col h-full hover:shadow-lg transition-shadow bg-white dark:bg-neutral-800 text-gray-800 dark:text-gray-100 border border-gray-200 dark:border-neutral-700 border-t-4 border-t-frog-500 dark:border-t-frog-400">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center">
|
<CardTitle className="flex items-center">
|
||||||
<Users className="h-5 w-5 mr-2 text-frog-500 dark:text-frog-400" />
|
<Users className="h-5 w-5 mr-2 text-frog-500 dark:text-frog-400" />
|
||||||
@ -110,7 +110,7 @@ const TeamSection = () => {
|
|||||||
{team.liga}
|
{team.liga}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="flex-grow">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{team.beschreibung ?? "Keine Beschreibung vorhanden."}
|
{team.beschreibung ?? "Keine Beschreibung vorhanden."}
|
||||||
</p>
|
</p>
|
||||||
@ -119,7 +119,8 @@ const TeamSection = () => {
|
|||||||
{team.trainingszeiten ?? "Nicht angegeben"}
|
{team.trainingszeiten ?? "Nicht angegeben"}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
|
||||||
|
<CardFooter className="mt-auto">
|
||||||
<Link to={`/teams/${team.id}`} className="w-full">
|
<Link to={`/teams/${team.id}`} className="w-full">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -130,6 +131,7 @@ const TeamSection = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -32,6 +32,12 @@ const TeamDetailPage = () => {
|
|||||||
const [team, setTeam] = useState<Team | null>(null);
|
const [team, setTeam] = useState<Team | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const [liveData, setLiveData] = useState<{
|
||||||
|
scoreboard: any[];
|
||||||
|
spiele: any[];
|
||||||
|
last_updated: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTeam = async () => {
|
const fetchTeam = async () => {
|
||||||
try {
|
try {
|
||||||
@ -43,8 +49,13 @@ const TeamDetailPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTeam(data);
|
setTeam(data);
|
||||||
|
|
||||||
|
// Live-Daten abrufen
|
||||||
|
const liveRes = await fetch(`${apiBase}/api/team-live/${id}`);
|
||||||
|
const liveJson = await liveRes.json();
|
||||||
|
setLiveData(liveJson);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Fehler beim Laden des Teams:", err);
|
console.error("Fehler beim Laden:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -55,7 +66,7 @@ const TeamDetailPage = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto py-8 px-4">
|
<div className="max-w-7xl mx-auto py-8 px-4 bg-white dark:bg-neutral-900 text-gray-900 dark:text-gray-100 transition-colors duration-300">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<Skeleton height={400} borderRadius={12} />
|
<Skeleton height={400} borderRadius={12} />
|
||||||
</div>
|
</div>
|
||||||
@ -64,10 +75,10 @@ const TeamDetailPage = () => {
|
|||||||
<h1 className="text-4xl font-bold text-frog-600">
|
<h1 className="text-4xl font-bold text-frog-600">
|
||||||
<Skeleton width={220} />
|
<Skeleton width={220} />
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg text-gray-700 mt-2">
|
<p className="text-lg text-gray-700 dark:text-gray-300 mt-2">
|
||||||
<Skeleton width={160} />
|
<Skeleton width={160} />
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-4 text-gray-600">
|
<p className="mt-4 text-gray-600 dark:text-gray-400">
|
||||||
<Skeleton count={2} />
|
<Skeleton count={2} />
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-frog-700 font-medium mt-2">
|
<p className="text-sm text-frog-700 font-medium mt-2">
|
||||||
@ -77,7 +88,7 @@ const TeamDetailPage = () => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
{[...Array(3)].map((_, i) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
<div key={i} className="bg-white rounded-lg shadow-md p-4">
|
<div key={i} className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4">
|
||||||
<Skeleton height={200} className="mb-4" />
|
<Skeleton height={200} className="mb-4" />
|
||||||
<Skeleton width={`60%`} height={24} />
|
<Skeleton width={`60%`} height={24} />
|
||||||
<Skeleton width={`40%`} height={18} />
|
<Skeleton width={`40%`} height={18} />
|
||||||
@ -89,7 +100,7 @@ const TeamDetailPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!team) return <p className="text-center py-12">Team nicht gefunden 🐸</p>;
|
if (!team) return <p className="text-center py-12 dark:text-white">Team nicht gefunden 🐸</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto py-8 px-4">
|
<div className="max-w-7xl mx-auto py-8 px-4">
|
||||||
@ -119,14 +130,14 @@ const TeamDetailPage = () => {
|
|||||||
|
|
||||||
{/* Team Info */}
|
{/* Team Info */}
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h1 className="text-4xl font-bold text-frog-600">{team.name}</h1>
|
<h1 className="text-4xl font-bold text-frog-600 dark:text-frog-400">{team.name}</h1>
|
||||||
<p className="text-lg text-gray-700 mt-2">{team.liga}</p>
|
<p className="text-lg text-gray-700 dark:text-gray-300 mt-2">{team.liga}</p>
|
||||||
<p className="text-gray-600 mt-4">{team.beschreibung}</p>
|
<p className="text-gray-600 dark:text-gray-400 mt-4">{team.beschreibung}</p>
|
||||||
<p className="text-sm text-frog-700 font-medium mt-2">
|
<p className="text-sm text-frog-700 dark:text-frog-500 font-medium mt-2">
|
||||||
Training: {team.trainingszeiten || "Nicht angegeben"}
|
Training: {team.trainingszeiten || "Nicht angegeben"}
|
||||||
</p>
|
</p>
|
||||||
{team.trainingsort && (
|
{team.trainingsort && (
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
Ort: {team.trainingsort}
|
Ort: {team.trainingsort}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -141,12 +152,85 @@ const TeamDetailPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{liveData && (
|
||||||
|
<div className="mt-12">
|
||||||
|
<h2 className="text-2xl font-bold text-frog-600 dark:text-frog-400 mb-4">🏆 Aktuelle Tabelle</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full bg-white dark:bg-neutral-800 rounded shadow">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-frog-100 dark:bg-frog-700 text-left text-sm font-semibold text-gray-700 dark:text-white">
|
||||||
|
<th className="p-2">Platz</th>
|
||||||
|
<th className="p-2">Team</th>
|
||||||
|
<th className="p-2">Spiele</th>
|
||||||
|
<th className="p-2">Siege</th>
|
||||||
|
<th className="p-2">Sätze</th>
|
||||||
|
<th className="p-2">Punkte</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{liveData.scoreboard.map((row, i) => {
|
||||||
|
const isTop = row.platz === 1;
|
||||||
|
const isBottom = row.platz === liveData.scoreboard.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={i} className="border-t dark:border-neutral-700">
|
||||||
|
<td className="p-2 font-medium flex items-center gap-1">
|
||||||
|
{row.platz}
|
||||||
|
{isTop && <span className="text-green-600">▲</span>}
|
||||||
|
{isBottom && <span className="text-red-600">▼</span>}
|
||||||
|
</td>
|
||||||
|
<td className="p-2">{row.team}</td>
|
||||||
|
<td className="p-2">{row.spiele}</td>
|
||||||
|
<td className="p-2">{row.siege}</td>
|
||||||
|
<td className="p-2">{row.saetze}</td>
|
||||||
|
<td className="p-2">{row.punkte}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold text-frog-600 dark:text-frog-400 mt-10 mb-4">📅 Spiele</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full bg-white dark:bg-neutral-800 rounded shadow">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-frog-100 dark:bg-frog-700 text-left text-sm font-semibold text-gray-700 dark:text-white">
|
||||||
|
<th className="p-2">Datum</th>
|
||||||
|
<th className="p-2">Team 1</th>
|
||||||
|
<th className="p-2">Team 2</th>
|
||||||
|
<th className="p-2">Ergebnis</th>
|
||||||
|
<th className="p-2">Satzverlauf</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{liveData.spiele.map((spiel, i) => (
|
||||||
|
<tr key={i} className="border-t dark:border-neutral-700">
|
||||||
|
<td className="p-2">{spiel.datum}</td>
|
||||||
|
<td className="p-2">{spiel.team1}</td>
|
||||||
|
<td className="p-2">{spiel.team2}</td>
|
||||||
|
<td className="p-2">{spiel.ergebnis}</td>
|
||||||
|
<td className="p-2">{spiel.satzverlauf}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-4">
|
||||||
|
Zuletzt aktualisiert: {new Date(liveData.last_updated).toLocaleString("de-DE")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Spielerübersicht */}
|
{/* Spielerübersicht */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
{team.players.map((player) => (
|
{team.players.map((player) => (
|
||||||
<div
|
<div
|
||||||
key={player.id}
|
key={player.id}
|
||||||
className="bg-white rounded-lg shadow-md p-4 flex flex-col items-center text-center transform transition duration-300 hover:-translate-y-1 hover:shadow-xl"
|
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 flex flex-col items-center text-center transform transition duration-300 hover:-translate-y-1 hover:shadow-xl"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
@ -157,12 +241,12 @@ const TeamDetailPage = () => {
|
|||||||
alt={player.name}
|
alt={player.name}
|
||||||
className="w-full h-108 object-cover rounded-lg mb-4"
|
className="w-full h-108 object-cover rounded-lg mb-4"
|
||||||
/>
|
/>
|
||||||
<h3 className="text-xl font-semibold text-frog-700">
|
<h3 className="text-xl font-semibold text-frog-700 dark:text-frog-400">
|
||||||
{player.name}
|
{player.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-500">{player.position}</p>
|
<p className="text-gray-500 dark:text-gray-300">{player.position}</p>
|
||||||
{player.nickname && (
|
{player.nickname && (
|
||||||
<p className="text-sm text-frog-500 mt-1">„{player.nickname}“</p>
|
<p className="text-sm text-frog-500 dark:text-frog-300 mt-1">„{player.nickname}“</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user