diff --git a/index.html b/index.html index e4b78ea..2184835 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + Sports Agenda
diff --git a/package-lock.json b/package-lock.json index 7ef5d76..96fadb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.1", + "styled-components": "^6.1.13" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -1835,6 +1837,12 @@ "license": "MIT", "peer": true }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1965,6 +1973,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2577,6 +2591,15 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chai": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", @@ -2677,6 +2700,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -2717,6 +2749,26 @@ "node": ">= 8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -4135,7 +4187,6 @@ "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, "funding": [ { "type": "github", @@ -4381,6 +4432,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4486,6 +4543,46 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-router": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.1.tgz", + "integrity": "sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.1.1.tgz", + "integrity": "sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==", + "license": "MIT", + "dependencies": { + "react-router": "7.1.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4653,6 +4750,18 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4706,7 +4815,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4833,6 +4941,89 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/styled-components/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -4986,6 +5177,18 @@ "typescript": ">=4.2.0" } }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 7598257..681dc97 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-router-dom": "^7.1.1", + "styled-components": "^6.1.13" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..4908a6f Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/react.svg b/public/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/public/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/src/App.tsx b/src/App.tsx index 146272d..1bdb707 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,33 +1,34 @@ -import { useState } from 'react' -import "./App.css"; - -function App() { - const [count, setCount] = useState(0) +import React from "react"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import Home from "./pages/Home"; +import LeagueDetails from "./pages/LeagueDetails"; +import TeamDetails from "./pages/TeamDetails"; +import MatchDetails from "./pages/MatchDetails"; +const App: React.FC = () => { return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

+ +
+ {/* Header */} +
+ + {/* Main Content */} +
+ + } /> + } /> + } /> + } /> + +
+ + {/* Footer */} +
-

- Click on the Vite and React logos to learn more -

- - ) -} +
+ ); +}; -export default App +export default App; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..4a0d96a --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import styled from "styled-components"; + +const FooterContainer = styled.footer` + background-color: #004d40; /* Deep Green */ + color: white; + padding: 10px 20px; + text-align: center; + font-family: Arial, sans-serif; + font-size: 14px; + + a { + color: #ffcc00; /* Yellow highlight */ + text-decoration: none; + font-weight: bold; + transition: color 0.3s ease; + + &:hover { + color: white; + } + } +`; + +const Footer: React.FC = () => { + return ( + +

+ © {new Date().getFullYear()} Sports Agenda - API BR. All Rights Reserved. Built + with ❤️ by Guilherme Branco Stracini +

+
+ ); +}; + +export default Footer; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..0b3f1f9 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import styled from "styled-components"; +import { Link } from "react-router-dom"; + +const HeaderContainer = styled.header` + background-color: #004d40; /* Deep Green */ + color: white; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + font-family: Arial, sans-serif; + + h1 { + font-size: 24px; + margin: 0; + } + + nav { + a { + color: white; + text-decoration: none; + margin: 0 10px; + font-size: 16px; + font-weight: bold; + transition: color 0.3s ease; + + &:hover { + color: #ffcc00; /* Yellow highlight */ + } + } + } +`; + +const Header: React.FC = () => { + return ( + +

Sports Agenda | API BR

+ +
+ ); +}; + +export default Header; diff --git a/src/components/LeagueCarousel.tsx b/src/components/LeagueCarousel.tsx new file mode 100644 index 0000000..fd18c9d --- /dev/null +++ b/src/components/LeagueCarousel.tsx @@ -0,0 +1,66 @@ +import styled from "styled-components"; + +export const CarouselContainer = styled.div` + display: flex; + overflow-x: scroll; + padding: 10px; + gap: 15px; + + &::-webkit-scrollbar { + display: none; + } +`; + +export const LeagueCard = styled.div` + min-width: 200px; + padding: 15px; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #f9f9f9; + text-align: center; + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.05); + } + + h3 { + font-size: 18px; + margin-bottom: 5px; + } + + p { + font-size: 14px; + color: #666; + } +`; + +const LeagueCarousel = ({ leagues, onSelectLeague } : {leagues: League[], onSelectLeague: (id: number) => void}) => { + if (!leagues.length) { + return ( + +

No leagues available

+
+ ); + } + + return ( + + {leagues.map((league) => ( + onSelectLeague(league.id)} + role="button" + aria-label={`View ${league.name} league details`} + > +

{league.name}

+

{league.year}

+
+ ))} +
+ ); +}; + +export default LeagueCarousel; diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..8447013 --- /dev/null +++ b/src/components/LoadingSpinner.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import styled from "styled-components"; + +const SpinnerContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +`; + +const Spinner = styled.div` + border: 8px solid #f3f3f3; /* Light gray background */ + border-top: 8px solid #3498db; /* Blue color */ + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } +`; + +const LoadingSpinner: React.FC = () => { + return ( + + + + ); +}; + +export default LoadingSpinner; diff --git a/src/components/MatchList.tsx b/src/components/MatchList.tsx new file mode 100644 index 0000000..b9e5675 --- /dev/null +++ b/src/components/MatchList.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import styled from "styled-components"; +import { Link } from "react-router-dom"; + + +const MatchListContainer = styled.div` + display: flex; + flex-direction: column; + gap: 15px; + + a { + text-decoration: none; + color: inherit; + } + + .match-date { + font-size: 14px; + color: #ccc; + } +`; + +const MatchItem = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 18px; + padding: 10px; + background-color: #004d40; + color: white; + border-radius: 10px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + + &:hover { + background-color: #00796b; + } +`; + +const TeamNames = styled.div` + display: flex; + justify-content: space-between; + width: 50%; +`; + +const MatchList: React.FC = ({ matches }) => { + const formatDate = React.useMemo(() => + (date: string) => new Date(date).toLocaleDateString(), + [] + ); + + return ( + + {matches.map((match) => ( + + + + {match.homeTeam} + + {match.awayTeam} + +
+ {/* Display the score or a message if the match is upcoming */} + {match.score ? ( + {match.score} + ) : ( + Match not played yet + )} +
+ + {formatDate(match.date)} + +
+ + ))} +
+ ); +}; + +export default MatchList; diff --git a/src/components/TeamGrid.tsx b/src/components/TeamGrid.tsx new file mode 100644 index 0000000..bc00762 --- /dev/null +++ b/src/components/TeamGrid.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import styled from "styled-components"; +import { Link } from "react-router-dom"; + + + +const TeamGridContainer = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + + .team-card { + background-color: #f1f8e9; + border-radius: 8px; + padding: 15px; + text-align: center; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + + &:hover { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + } + + img { + width: 80px; + height: 80px; + border-radius: 50%; + margin-bottom: 10px; + } + + h3 { + font-size: 18px; + color: #004d40; + } + + a { + text-decoration: none; + color: inherit; + } + } +`; + +const TeamGrid: React.FC = ({ teams }) => { + if (!teams?.length) { + return ( + +
No teams available
+
+ ); + } + + return ( + + {teams.map((team) => ( + + {`${team.name} +

{team.name}

+ + ))} +
+ ); +}; + +export default TeamGrid; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 6119ad9..0000000 --- a/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/src/main.tsx b/src/main.tsx index bef5202..4aff025 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,5 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..e9c13bf --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,137 @@ +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import LeagueCarousel from "../components/LeagueCarousel"; +import MatchList from "../components/MatchList"; +import { useNavigate } from "react-router-dom"; +import LoadingSpinner from "../components/LoadingSpinner"; + + + +const HomeContainer = styled.div` + font-family: "Arial", sans-serif; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + + h1 { + text-align: center; + margin-bottom: 30px; + font-size: 32px; + font-weight: bold; + color: #004d40; /* Deep Green */ + } + + h2 { + margin-top: 40px; + margin-bottom: 20px; + font-size: 24px; + color: #00695c; /* Slightly lighter green */ + border-bottom: 2px solid #004d40; + display: inline-block; + padding-bottom: 5px; + } + + .section { + margin-bottom: 50px; + padding: 20px; + border-radius: 8px; + background-color: #f1f8e9; /* Light green tint */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + } + + .carousel-container { + padding: 10px; + background: #e8f5e9; /* Subtle background for the carousel */ + border-radius: 8px; + } +`; + +const Home: React.FC = () => { + const [leagues, setLeagues] = useState([]); + const [todaysMatches, setTodaysMatches] = useState([]); + const [pastMatches, setPastMatches] = useState([]); + const [upcomingMatches, setUpcomingMatches] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + // Mock API responses + const leaguesData: League[] = [ + { id: 1, name: "Premier League", year: 2023 }, + { id: 2, name: "La Liga", year: 2023 }, + { id: 3, name: "Serie A", year: 2023 }, + { id: 4, name: "Bundesliga", year: 2023 }, + { id: 5, name: "Ligue 1", year: 2023 }, + ]; + + const matchesData = { + today: [ + { id: 101, homeTeam: "Manchester United", awayTeam: "Chelsea", date: "2024-12-28T15:00:00Z" }, + { id: 102, homeTeam: "Real Madrid", awayTeam: "Barcelona", date: "2024-12-28T18:30:00Z" }, + ], + past: [ + { id: 103, homeTeam: "Arsenal", awayTeam: "Liverpool", date: "2024-12-23T15:00:00Z" }, + { id: 104, homeTeam: "Bayern Munich", awayTeam: "Borussia Dortmund", date: "2024-12-24T18:30:00Z" }, + ], + upcoming: [ + { id: 105, homeTeam: "Paris Saint-Germain", awayTeam: "Marseille", date: "2024-12-30T20:00:00Z" }, + { id: 106, homeTeam: "AC Milan", awayTeam: "Juventus", date: "2024-12-31T19:00:00Z" }, + ], + }; + + const fetchData = async () => { + // const leaguesResponse = await fetch("/api/leagues"); + // const matchesResponse = await fetch("/api/matches"); + // const leaguesData = await leaguesResponse.json(); + // const matchesData = await matchesResponse.json(); + // Simulate a delay for better development experience + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Set mocked data + setLeagues(leaguesData); + setTodaysMatches(matchesData.today); + setPastMatches(matchesData.past); + setUpcomingMatches(matchesData.upcoming); + }; + + fetchData(); + }, []); + + const handleLeagueClick = (leagueId: number) => { + navigate(`/league/${leagueId}`); + }; + + if (!leagues.length || !todaysMatches.length || !pastMatches.length || !upcomingMatches.length) { + return ( + +
+ +

Loading details...

+
+
+ ); + } + + return ( + +

Football Matches

+ + {/* League Carousel */} +

Leagues & Championships

+ + + {/* Today's Matches */} +

Today's Matches

+ + + {/* Past Matches */} +

Matches from the Last 5 Days

+ + + {/* Upcoming Matches */} +

Matches for the Next 5 Days

+ +
+ ); +}; + +export default Home; diff --git a/src/pages/LeagueDetails.tsx b/src/pages/LeagueDetails.tsx new file mode 100644 index 0000000..e2c3387 --- /dev/null +++ b/src/pages/LeagueDetails.tsx @@ -0,0 +1,236 @@ +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import { useParams } from "react-router-dom"; +import TeamGrid from "../components/TeamGrid"; +import MatchList from "../components/MatchList"; +import LoadingSpinner from "../components/LoadingSpinner"; + +const LeagueDetailsContainer = styled.div` + font-family: "Arial", sans-serif; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + + h1 { + text-align: center; + margin-bottom: 30px; + font-size: 32px; + font-weight: bold; + color: #004d40; /* Deep Green */ + } + + h2 { + margin-top: 40px; + margin-bottom: 20px; + font-size: 24px; + color: #00695c; /* Slightly lighter green */ + border-bottom: 2px solid #004d40; + display: inline-block; + padding-bottom: 5px; + } + + .teams-section, + .matches-section { + margin-bottom: 50px; + padding: 20px; + border-radius: 8px; + background-color: #f1f8e9; /* Light green tint */ + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + } + + .team-grid-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 20px; + justify-items: center; + } + + .team-card { + width: 100%; + max-width: 150px; + text-align: center; + + img { + width: 100%; + height: auto; + border-radius: 50%; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + margin-bottom: 10px; + } + + p { + font-size: 16px; + font-weight: bold; + color: #004d40; + } + } + + .match-list-container { + margin-top: 20px; + + h3 { + font-size: 20px; + color: #004d40; + margin-bottom: 10px; + } + } +`; + +const LeagueDetails: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [league, setLeague] = useState(null); + const [pastMatches, setPastMatches] = useState([]); + const [upcomingMatches, setUpcomingMatches] = useState([]); + + useEffect(() => { + // Mocked data for the league and matches + const mockedLeagues: { [key: number]: { id: number; name: string; year: number; teams: Team[]; } } = { + 1: { + id: 1, + name: "Premier League", + year: 2023, + teams: [ + { id: 101, name: "Manchester United", logo: "/images/manu.png", players: [], matches: [], leagues: [] }, + { id: 102, name: "Chelsea", logo: "/images/chelsea.png", players: [], matches: [], leagues: [] }, + { id: 103, name: "Liverpool", logo: "/images/liverpool.png", players: [], matches: [], leagues: [] }, + { id: 104, name: "Arsenal", logo: "/images/arsenal.png", players: [], matches: [], leagues: [] }, + ], + }, + 2: { + id: 2, + name: "La Liga", + year: 2023, + teams: [ + { id: 201, name: "Real Madrid", logo: "/images/realmadrid.png", players: [], matches: [], leagues: [] }, + { id: 202, name: "Barcelona", logo: "/images/barcelona.png", players: [], matches: [], leagues: [] }, + { id: 203, name: "Atletico Madrid", logo: "/images/atletico.png", players: [], matches: [], leagues: [] }, + { id: 204, name: "Sevilla", logo: "/images/sevilla.png", players: [], matches: [], leagues: [] }, + ], + }, + }; + + const mockedMatches: { [key: number]: { past: { id: number; homeTeam: string; awayTeam: string; date: string; score: string; }[]; upcoming: { id: number; homeTeam: string; awayTeam: string; date: string; }[]; } } = { + 1: { + past: [ + { + id: 301, + homeTeam: "Manchester United", + awayTeam: "Arsenal", + date: "2024-12-22T15:00:00Z", + score: "3-1", + }, + { + id: 302, + homeTeam: "Chelsea", + awayTeam: "Liverpool", + date: "2024-12-23T18:00:00Z", + score: "2-2", + }, + ], + upcoming: [ + { + id: 303, + homeTeam: "Manchester United", + awayTeam: "Liverpool", + date: "2024-12-30T17:00:00Z", + }, + { + id: 304, + homeTeam: "Arsenal", + awayTeam: "Chelsea", + date: "2024-12-31T19:00:00Z", + }, + ], + }, + 2: { + past: [ + { + id: 305, + homeTeam: "Real Madrid", + awayTeam: "Sevilla", + date: "2024-12-22T15:00:00Z", + score: "1-0", + }, + { + id: 306, + homeTeam: "Barcelona", + awayTeam: "Atletico Madrid", + date: "2024-12-23T18:00:00Z", + score: "0-2", + }, + ], + upcoming: [ + { + id: 307, + homeTeam: "Real Madrid", + awayTeam: "Barcelona", + date: "2024-12-30T20:00:00Z", + }, + { + id: 308, + homeTeam: "Sevilla", + awayTeam: "Atletico Madrid", + date: "2024-12-31T21:00:00Z", + }, + ], + }, + }; + + const fetchData = async () => { + // const leagueResponse = await fetch(`/api/leagues/${id}`); + // const matchesResponse = await fetch(`/api/matches?leagueId=${id}`); + // const leagueData = await leagueResponse.json(); + // const matchesData = await matchesResponse.json(); + + // setLeague(leagueData); + // setPastMatches(matchesData.past); + // setUpcomingMatches(matchesData.upcoming); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Mock league and matches data based on the league ID + const leagueData = id ? mockedLeagues[Number(id)] : null; + const matchesData = id ? mockedMatches[Number(id)] : { past: [], upcoming: [] }; + + setLeague(leagueData || null); + setPastMatches(matchesData?.past || []); + setUpcomingMatches(matchesData?.upcoming || []); + }; + + fetchData(); + }, [id]); + + if (!league) { + return ( + +
+ +

Loading league details...

+
+
+ ); + } + + return ( + +

+ {league.name} ({league.year}) +

+ + {/* Teams Grid */} +

Teams

+ + + {/* Matches Section */} +
+

Matches

+

Past Matches

+ + +

Upcoming Matches

+ +
+
+ ); +}; + +export default LeagueDetails; diff --git a/src/pages/MatchDetails.tsx b/src/pages/MatchDetails.tsx new file mode 100644 index 0000000..523fcce --- /dev/null +++ b/src/pages/MatchDetails.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import styled from "styled-components"; +import LoadingSpinner from "../components/LoadingSpinner"; + +const MatchDetailsContainer = styled.div` + padding: 20px; + font-family: Arial, sans-serif; + + h1 { + text-align: center; + margin-bottom: 20px; + } + + .teams { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + + img { + width: 100px; + height: 100px; + border-radius: 50%; + } + } + + .score { + margin-top: 20px; + text-align: center; + font-size: 24px; + font-weight: bold; + color: #00796b; + } + + .details { + margin-top: 20px; + text-align: center; + font-size: 18px; + } +`; + +const MatchDetails: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [match, setMatch] = useState(null); + + useEffect(() => { + // Mock API Call + const fetchMatchData = async () => { + const matchData: Match = { + id: Number(id), + homeTeam: "Team A", + awayTeam: "Team B", + homeTeamLogo: "https://via.placeholder.com/100?text=Team+A", + awayTeamLogo: "https://via.placeholder.com/100?text=Team+B", + date: "2025-01-01T15:00:00Z", + score: "2-1", // Null for upcoming matches + league: { id: 1, name: "Premier League", year: 2025 }, + }; + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + setMatch(matchData); + }; + + fetchMatchData(); + }, [id]); + + if (!match) { + return ( + +
+ +

Loading match details...

+
+
+ ); + } + + return ( + +

Match Details

+
+
+ {match.homeTeam} +

{match.homeTeam}

+
+

vs

+
+ {match.awayTeam} +

{match.awayTeam}

+
+
+ + {/* Display the score */} +
+ {match.score ? `Score: ${match.score}` : "Match not played yet"} +
+ +
+

+ Date: {new Date(match.date).toLocaleString()} +

+

+ League: {match.league?.name} +

+
+
+ ); +}; + +export default MatchDetails; diff --git a/src/pages/TeamDetails.tsx b/src/pages/TeamDetails.tsx new file mode 100644 index 0000000..f4fb734 --- /dev/null +++ b/src/pages/TeamDetails.tsx @@ -0,0 +1,215 @@ +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import { useParams } from "react-router-dom"; +import MatchList from "../components/MatchList"; +import LoadingSpinner from "../components/LoadingSpinner"; + +const TeamDetailsContainer = styled.div` + font-family: "Arial", sans-serif; + max-width: 1000px; + margin: 0 auto; + padding: 20px; + + h1 { + text-align: center; + margin-bottom: 30px; + font-size: 32px; + font-weight: bold; + color: #004d40; + } + + .team-header { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40px; + + img { + width: 100px; + height: 100px; + border-radius: 50%; + margin-right: 20px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + } + + h1 { + margin: 0; + } + } + + .section { + margin-bottom: 50px; + + h2 { + font-size: 24px; + color: #00695c; + margin-bottom: 20px; + border-bottom: 2px solid #004d40; + display: inline-block; + padding-bottom: 5px; + } + } + + .players-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + + .player-card { + background-color: #f1f8e9; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + text-align: center; + + h3 { + font-size: 18px; + color: #004d40; + margin-bottom: 5px; + } + + p { + font-size: 14px; + color: #555; + margin: 5px 0; + } + } + } + + .leagues-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; + + th, + td { + border: 1px solid #ddd; + text-align: left; + padding: 8px; + } + + th { + background-color: #004d40; + color: white; + } + + tr:nth-child(even) { + background-color: #f9f9f9; + } + + tr:hover { + background-color: #f1f1f1; + } + } +`; + +const TeamDetails: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [team, setTeam] = useState(null); + + useEffect(() => { + // const fetchTeamDetails = async () => { + // const response = await fetch(`/api/teams/${id}`); + // const data = await response.json(); + // setTeam(data); + // }; + // fetchTeamDetails(); + + // Mock Data + const mockTeamData: Team = { + id: parseInt(id || "1", 10), + name: "Mock Team", + logo: "https://via.placeholder.com/100", + players: [ + { id: 1, name: "John Doe", position: "Forward", age: 25 }, + { id: 2, name: "Jane Smith", position: "Midfielder", age: 27 }, + { id: 3, name: "Tom Johnson", position: "Defender", age: 23 }, + ], + matches: [ + { + id: 101, + homeTeam: "Mock Team", + awayTeam: "Team B", + date: "2024-12-25T15:00:00Z", + score: "2-1", + }, + { + id: 102, + homeTeam: "Team C", + awayTeam: "Mock Team", + date: "2024-12-20T15:00:00Z", + score: "1-1", + }, + ], + leagues: [ + { id: 1, name: "Premier League", year: 2023 }, + { id: 2, name: "La Liga", year: 2023 }, + ], + }; + + // Simulate API delay + setTimeout(() => setTeam(mockTeamData), 1000); + }, [id]); + + if (!team) { + return ( + +
+ +

Loading team details...

+
+
+ ); + } + + return ( + + {/* Team Header */} +
+ {team.name} +

{team.name}

+
+ + {/* Players Section */} +
+

Players

+
+ {team.players.map((player) => ( +
+

{player.name}

+

Position: {player.position}

+

Age: {player.age}

+
+ ))} +
+
+ + {/* Leagues Section */} +

Leagues

+ + + + + + + + + {team.leagues.map((league) => ( + + + + + ))} + +
LeagueYear
{league.name}{league.year}
+ + {/* Matches Section */} +
+

Recent Matches

+ +
+
+ ); +}; + +export default TeamDetails; diff --git a/src/types/types.d.ts b/src/types/types.d.ts new file mode 100644 index 0000000..7db63fa --- /dev/null +++ b/src/types/types.d.ts @@ -0,0 +1,41 @@ +interface Match { + id: number; + homeTeam: string; + awayTeam: string; + homeTeamLogo?: string; + awayTeamLogo?: string; + date: string; + score?: string; + league?: League; +} + +interface MatchListProps { + matches: Match[]; +} + +interface Team { + id: number; + name: string; + logo: string; + players: Player[]; + matches: Match[]; + leagues: League[]; +} + +interface TeamGridProps { + teams?: Team[] | null; +} + +interface League { + id: number; + name: string; + year: number; + teams?: Team[] | null; +} + +interface Player { + id: number; + name: string; + position: string; + age: number; +} diff --git a/test/App.test.tsx b/test/App.test.tsx index dd37ce5..7bea7fc 100644 --- a/test/App.test.tsx +++ b/test/App.test.tsx @@ -1,33 +1,13 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import { describe, expect } from "vitest"; +import { render } from "@testing-library/react"; +import { describe, expect, test } from "vitest"; import App from "../src/App"; describe("App component", () => { - test("renders Vite and React logos", () => { - render(); - const viteLogo = screen.getByAltText("Vite logo"); - const reactLogo = screen.getByAltText("React logo"); - expect(viteLogo).toBeInTheDocument(); - expect(reactLogo).toBeInTheDocument(); + test("renders Header component", () => { + render( + + ); + expect(true).toBeTruthy(); }); - test("renders the heading", () => { - render(); - const heading = screen.getByText(/Vite \+ React/i); - expect(heading).toBeInTheDocument(); - }); - - test("renders the button and increments count on click", () => { - render(); - const button = screen.getByRole("button", { name: /count is 0/i }); - expect(button).toBeInTheDocument(); - fireEvent.click(button); - expect(button).toHaveTextContent("count is 1"); - }); - - test("renders the edit message", () => { - render(); - const editMessage = screen.getByText(/and save to test HMR/i); - expect(editMessage).toBeInTheDocument(); - }); -}); +}); \ No newline at end of file