Added scoreboard integration
Some checks failed
Deploy Volleyball CMS / deploy (push) Failing after 13s

This commit is contained in:
Marc Wieland 2025-06-05 16:03:17 +02:00
parent 3aff4b89f9
commit 8b516d6f68
5 changed files with 142 additions and 34 deletions

View File

@ -1 +1 @@
VITE_API_URL=http://localhost:5000
VITE_API_URL=http://192.168.50.65:3000

View File

@ -14,6 +14,7 @@ const queryClient = new QueryClient();
const App = () => (
<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">
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<Toaster />
@ -22,12 +23,13 @@ const App = () => (
<ScrollToTop />
<Routes>
<Route path="/" element={<Index />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
</div>
);
export default App;

View File

@ -38,6 +38,9 @@ const TeamDetail = () => {
const [players, setPlayers] = useState<Player[]>([]);
const [loading, setLoading] = useState(true);
const [spielelink, setSpielelink] = useState("");
const [scraper_Identifier, setScraperIdentifier] = useState("");
const fetchTeam = async () => {
try {
const res = await fetch(`${apiBase}/api/teams/${id}`);
@ -57,6 +60,10 @@ const TeamDetail = () => {
setBeschreibung(data.beschreibung ?? "");
setTabellenlink(data.tabellenlink ?? "");
setPlayers(data.players ?? []);
setSpielelink(data.spielelink ?? "");
setScraperIdentifier(data.scraper_identifier ?? "");
} catch (err) {
console.error("Fehler beim Laden des Teams:", err);
} finally {
@ -87,6 +94,8 @@ const TeamDetail = () => {
teamfarben,
beschreibung,
tabellenlink,
spielelink,
scraper_Identifier
}),
});
@ -166,6 +175,17 @@ const TeamDetail = () => {
<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={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">
<input

View File

@ -100,7 +100,7 @@ const TeamSection = () => {
<div ref={sliderRef} className="keen-slider">
{teams.map((team) => (
<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>
<CardTitle className="flex items-center">
<Users className="h-5 w-5 mr-2 text-frog-500 dark:text-frog-400" />
@ -110,7 +110,7 @@ const TeamSection = () => {
{team.liga}
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="flex-grow">
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
{team.beschreibung ?? "Keine Beschreibung vorhanden."}
</p>
@ -119,7 +119,8 @@ const TeamSection = () => {
{team.trainingszeiten ?? "Nicht angegeben"}
</div>
</CardContent>
<CardFooter>
<CardFooter className="mt-auto">
<Link to={`/teams/${team.id}`} className="w-full">
<Button
variant="outline"
@ -130,6 +131,7 @@ const TeamSection = () => {
</Button>
</Link>
</CardFooter>
</Card>
</div>
))}

View File

@ -32,6 +32,12 @@ const TeamDetailPage = () => {
const [team, setTeam] = useState<Team | null>(null);
const [loading, setLoading] = useState(true);
const [liveData, setLiveData] = useState<{
scoreboard: any[];
spiele: any[];
last_updated: string;
} | null>(null);
useEffect(() => {
const fetchTeam = async () => {
try {
@ -43,8 +49,13 @@ const TeamDetailPage = () => {
}
setTeam(data);
// Live-Daten abrufen
const liveRes = await fetch(`${apiBase}/api/team-live/${id}`);
const liveJson = await liveRes.json();
setLiveData(liveJson);
} catch (err) {
console.error("Fehler beim Laden des Teams:", err);
console.error("Fehler beim Laden:", err);
} finally {
setLoading(false);
}
@ -55,7 +66,7 @@ const TeamDetailPage = () => {
if (loading) {
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">
<Skeleton height={400} borderRadius={12} />
</div>
@ -64,10 +75,10 @@ const TeamDetailPage = () => {
<h1 className="text-4xl font-bold text-frog-600">
<Skeleton width={220} />
</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} />
</p>
<p className="mt-4 text-gray-600">
<p className="mt-4 text-gray-600 dark:text-gray-400">
<Skeleton count={2} />
</p>
<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">
{[...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 width={`60%`} height={24} />
<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 (
<div className="max-w-7xl mx-auto py-8 px-4">
@ -119,14 +130,14 @@ const TeamDetailPage = () => {
{/* Team Info */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-frog-600">{team.name}</h1>
<p className="text-lg text-gray-700 mt-2">{team.liga}</p>
<p className="text-gray-600 mt-4">{team.beschreibung}</p>
<p className="text-sm text-frog-700 font-medium mt-2">
<h1 className="text-4xl font-bold text-frog-600 dark:text-frog-400">{team.name}</h1>
<p className="text-lg text-gray-700 dark:text-gray-300 mt-2">{team.liga}</p>
<p className="text-gray-600 dark:text-gray-400 mt-4">{team.beschreibung}</p>
<p className="text-sm text-frog-700 dark:text-frog-500 font-medium mt-2">
Training: {team.trainingszeiten || "Nicht angegeben"}
</p>
{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}
</p>
)}
@ -141,12 +152,85 @@ const TeamDetailPage = () => {
</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 */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{team.players.map((player) => (
<div
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
src={
@ -157,12 +241,12 @@ const TeamDetailPage = () => {
alt={player.name}
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}
</h3>
<p className="text-gray-500">{player.position}</p>
<p className="text-gray-500 dark:text-gray-300">{player.position}</p>
{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>
))}