diff --git a/README.md b/README.md index a934a3c..6596562 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,9 @@ A searchable catalog of video games where users can browse, filter and rate game 3. [Functionality](#functionality) 4. [Design](#design) 5. [Architecture](#architecture) -6. [Folder structure](#folder-structure) -7. [Accessibility](#accessibility) -8. [Sustainability](#sustainability) -9. [Development process and AI use](#development-process-and-ai-use) +6. [Accessibility](#accessibility) +7. [Sustainability](#sustainability) +8. [Development process and AI use](#development-process-and-ai-use) --- diff --git a/frontend/index.html b/frontend/index.html index 7b51410..d4263e3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@ - + it2810-t33-p2 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6299a8a..0263763 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "graphql": "^17.0.2", "react": "^19.2.8", "react-bootstrap": "^2.10.10", + "react-bootstrap-icons": "^1.11.6", "react-dom": "^19.2.8", "rxjs": "^7.8.2" }, @@ -2999,6 +3000,18 @@ } } }, + "node_modules/react-bootstrap-icons": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.6.tgz", + "integrity": "sha512-ycXiyeSyzbS1C4+MlPTYe0riB+UlZ7LV7YZQYqlERV2cxDiKtntI0huHmP/3VVvzPt4tGxqK0K+Y6g7We3U6tQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, "node_modules/react-dom": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8938b16..99447be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "graphql": "^17.0.2", "react": "^19.2.8", "react-bootstrap": "^2.10.10", + "react-bootstrap-icons": "^1.11.6", "react-dom": "^19.2.8", "rxjs": "^7.8.2" }, diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/frontend/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/frontend/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9ca8e8d..235f3d9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,122 +1,8 @@ -import { useState } from 'react' -import heroImg from './assets/hero.png' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import './App.css' +import "bootstrap/dist/css/bootstrap.min.css"; +import GamesGrid from "./pages/GamesGrid/GamesGrid"; function App() { - const [count, setCount] = useState(0) - - return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

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

-
- -
- -
- -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
- -
-
- - ) + return ; } -export default App +export default App; diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index 02251f4..0000000 Binary files a/frontend/src/assets/hero.png and /dev/null differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/components/GameListItem/GameListItem.module.css b/frontend/src/components/GameListItem/GameListItem.module.css new file mode 100644 index 0000000..10da3c9 --- /dev/null +++ b/frontend/src/components/GameListItem/GameListItem.module.css @@ -0,0 +1,34 @@ +.row { + display: flex; + align-items: center; + flex: 1 1 0; + min-height: var(--row-min-height); + gap: 1.5rem; + padding: 0 1.5rem; +} + +.odd { + background-color: var(--color-stripe-odd); +} + +.even { + background-color: var(--color-stripe-even); +} + +.image { + width: 95px; + height: 95px; + object-fit: cover; + background-color: #ddd; + flex-shrink: 0; +} + +.info p { + margin: 0; +} + +.title { + margin: 0 0 0.25rem; + font-size: 1.1rem; + font-weight: 700; +} diff --git a/frontend/src/components/GameListItem/GameListItem.tsx b/frontend/src/components/GameListItem/GameListItem.tsx new file mode 100644 index 0000000..e682427 --- /dev/null +++ b/frontend/src/components/GameListItem/GameListItem.tsx @@ -0,0 +1,31 @@ +import styles from "./GameListItem.module.css"; + +interface GameListItemProps { + title: string; + year: number; + tags: string; + rating: number; + image: string; + striped: boolean; +} + +export default function GameListItem({ + title, + year, + tags, + rating, + image, + striped, +}: GameListItemProps) { + return ( +
+ {title} +
+

{title}

+

{year}

+

{tags}

+

{rating.toFixed(1)}/10

+
+
+ ); +} diff --git a/frontend/src/components/Header/Header.module.css b/frontend/src/components/Header/Header.module.css new file mode 100644 index 0000000..7fde7a7 --- /dev/null +++ b/frontend/src/components/Header/Header.module.css @@ -0,0 +1,42 @@ +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + height: var(--header-height); + padding: 0 1.5rem; + background-color: var(--color-brand); +} + +.brand, +.link { + font-weight: 700; + color: var(--color-text); + white-space: nowrap; +} + +.link { + text-decoration: none; +} + +.search { + flex: 1; + max-width: 420px; +} + +.search :global(.input-group) { + border-radius: 999px; + overflow: hidden; +} + +.search :global(.form-control) { + border: none; +} + +.search :global(.form-control:focus) { + box-shadow: none; +} + +.search :global(.btn) { + border: none; +} diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx new file mode 100644 index 0000000..b846e7f --- /dev/null +++ b/frontend/src/components/Header/Header.tsx @@ -0,0 +1,27 @@ +import { Button, Form, InputGroup } from "react-bootstrap"; +import { Search } from "react-bootstrap-icons"; +import styles from "./Header.module.css"; + +export default function Header() { + return ( +
+ Working Title + +
+ + + + +
+ + + My Ratings + +
+ ); +} diff --git a/frontend/src/components/Layout/Layout.module.css b/frontend/src/components/Layout/Layout.module.css new file mode 100644 index 0000000..52611a0 --- /dev/null +++ b/frontend/src/components/Layout/Layout.module.css @@ -0,0 +1,5 @@ +.shell { + width: 100%; + max-width: var(--content-width); + margin: 0 auto; +} diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..ced9683 --- /dev/null +++ b/frontend/src/components/Layout/Layout.tsx @@ -0,0 +1,6 @@ +import type { ReactNode } from "react"; +import styles from "./Layout.module.css"; + +export default function Layout({ children }: { children: ReactNode }) { + return
{children}
; +} diff --git a/frontend/src/components/Toolbar/Toolbar.module.css b/frontend/src/components/Toolbar/Toolbar.module.css new file mode 100644 index 0000000..51f876a --- /dev/null +++ b/frontend/src/components/Toolbar/Toolbar.module.css @@ -0,0 +1,33 @@ +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + height: var(--toolbar-height); + padding: 0 1.5rem; + background-color: #fff; + border-bottom: 1px solid var(--color-border); + color: var(--color-muted); +} + +.action { + display: inline-flex; + align-items: center; + gap: 0.4rem; + border: none; + background: none; + color: inherit; + font: inherit; + cursor: pointer; +} + +.pagination { + display: inline-flex; + align-items: center; + gap: 0.75rem; +} + +.sort { + display: inline-flex; + align-items: center; + gap: 1rem; +} diff --git a/frontend/src/components/Toolbar/Toolbar.tsx b/frontend/src/components/Toolbar/Toolbar.tsx new file mode 100644 index 0000000..e2eadbb --- /dev/null +++ b/frontend/src/components/Toolbar/Toolbar.tsx @@ -0,0 +1,33 @@ +import { + ArrowDownUp, + ChevronDown, + ChevronLeft, + ChevronRight, + Funnel, +} from "react-bootstrap-icons"; +import styles from "./Toolbar.module.css"; + +export default function Toolbar() { + return ( +
+ + +
+ + Page 1 + +
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 5fb3313..8082bd9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,111 +1,34 @@ :root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; + /* 50% of a 1920px viewport — the grid stays centered at this width until + the viewport can't fit it anymore, then it fills the screen (see Layout). */ + --content-width: 960px; + --header-height: 72px; + --toolbar-height: 56px; + /* Rows grow to fill the list height evenly, split across however many + games are currently rendered, but never shrink below this. Once rows + hit the floor, the list clips instead of squeezing them unreadably + thin (see GamesGrid.module.css and GameListItem.module.css). */ + --row-min-height: 112px; + --color-brand: #a8504f; + --color-text: #1a1a1a; + --color-muted: #495057; + --color-stripe-odd: #f6d9d8; + --color-stripe-even: #fdf6f5; + --color-border: #e9cfce; +} + +* { box-sizing: border-box; } body { margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); + background-color: var(--color-stripe-odd); + color: var(--color-text); + font-family: + system-ui, + -apple-system, + "Segoe UI", + Roboto, + sans-serif; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..15753af 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( , -) +); diff --git a/frontend/src/pages/GameDetails/GameDetails.tsx b/frontend/src/pages/GameDetails/GameDetails.tsx new file mode 100644 index 0000000..3415488 --- /dev/null +++ b/frontend/src/pages/GameDetails/GameDetails.tsx @@ -0,0 +1,32 @@ +import { Container, Image } from "react-bootstrap"; +import Header from "../../components/Header/Header"; + +export default function GameDetails() { + return ( + <> +
+ + Release year | Age rating +

Game Title

+
+ Game Image +
+
NTNU Rating:
+

Your Rating:

+
+
+
Tags:
+
Developer(s):
+
Publisher(s):
+
Platform(s):
+

Description

+
+ + ); +} diff --git a/frontend/src/pages/GamesGrid/GamesGrid.module.css b/frontend/src/pages/GamesGrid/GamesGrid.module.css new file mode 100644 index 0000000..4dd75cc --- /dev/null +++ b/frontend/src/pages/GamesGrid/GamesGrid.module.css @@ -0,0 +1,6 @@ +.list { + display: flex; + flex-direction: column; + height: calc(100vh - var(--header-height) - var(--toolbar-height)); + overflow: hidden; +} diff --git a/frontend/src/pages/GamesGrid/GamesGrid.tsx b/frontend/src/pages/GamesGrid/GamesGrid.tsx new file mode 100644 index 0000000..93c6aff --- /dev/null +++ b/frontend/src/pages/GamesGrid/GamesGrid.tsx @@ -0,0 +1,63 @@ +import Header from "../../components/Header/Header"; +import Layout from "../../components/Layout/Layout"; +import Toolbar from "../../components/Toolbar/Toolbar"; +import GameListItem from "../../components/GameListItem/GameListItem"; +import styles from "./GamesGrid.module.css"; + +const games = [ + { + title: "Heroes of Might and Magic: Olden Era", + year: 2026, + tags: "Turn-based strategy", + rating: 9.3, + image: "", + }, + { + title: "Minecraft", + year: 2009, + tags: "Indie, Survival, Adventure, Sandbox", + rating: 9.1, + image: "", + }, + { + title: "Ori and the Blind Forest", + year: 2015, + tags: "Metroidvania", + rating: 8.8, + image: "", + }, + { + title: "Pokémon Legends: Z-A", + year: 2025, + tags: "Action RPG", + rating: 0.1, + image: "", + }, + { + title: "Hades", + year: 2020, + tags: "Roguelike", + rating: 9.4, + image: "", + }, +]; + +export default function GamesGrid() { + return ( + <> +
+ + +
+ {games.map((game, index) => ( + + ))} +
+
+ + ); +} diff --git a/frontend/src/pages/MyRatings/MyRatings.tsx b/frontend/src/pages/MyRatings/MyRatings.tsx new file mode 100644 index 0000000..aede895 --- /dev/null +++ b/frontend/src/pages/MyRatings/MyRatings.tsx @@ -0,0 +1,9 @@ +import Header from "../../components/Header/Header"; + +export default function MyRatings() { + return ( + <> +
+ + ); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5d533f7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,78 @@ +{ + "name": "T33-Project-2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "react-bootstrap-icons": "^1.11.6" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-bootstrap-icons": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.6.tgz", + "integrity": "sha512-ycXiyeSyzbS1C4+MlPTYe0riB+UlZ7LV7YZQYqlERV2cxDiKtntI0huHmP/3VVvzPt4tGxqK0K+Y6g7We3U6tQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..112c08d --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "react-bootstrap-icons": "^1.11.6" + } +}