From 28771364e81c18aac12cff017b3d53119fab4e5d Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Sat, 7 Nov 2026 10:36:56 +0100 Subject: [PATCH 01/10] Added a platform display component to display platform icons. Can be reused in game details page (#77) --- .../backend/src/graphql/resolvers.ts | 21 ++++++---- T04-Project-2/backend/src/graphql/schema.ts | 1 + T04-Project-2/frontend/package-lock.json | 10 +++++ T04-Project-2/frontend/package.json | 1 + T04-Project-2/frontend/src/api/games.ts | 6 +-- .../src/components/FeaturedCarousel.css | 2 + .../src/components/FeaturedCarousel.tsx | 2 + .../src/components/PlatformDisplay.css | 5 +++ .../src/components/PlatformDisplay.tsx | 38 +++++++++++++++++++ 9 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 T04-Project-2/frontend/src/components/PlatformDisplay.css create mode 100644 T04-Project-2/frontend/src/components/PlatformDisplay.tsx diff --git a/T04-Project-2/backend/src/graphql/resolvers.ts b/T04-Project-2/backend/src/graphql/resolvers.ts index af39cc5..47acc68 100644 --- a/T04-Project-2/backend/src/graphql/resolvers.ts +++ b/T04-Project-2/backend/src/graphql/resolvers.ts @@ -30,6 +30,15 @@ const sortColumns = { playtime: games.playtime, }; +const platformsForGame = (game: { id: number }) => db.select({ + id: platforms.id, + name: platforms.name, + description: platforms.description, +}) + .from(game_platforms) + .innerJoin(platforms, eq(game_platforms.platform_id, platforms.id)) + .where(eq(game_platforms.game_id, game.id)); + export const resolvers = { Query: { games: async () => { @@ -363,13 +372,9 @@ export const resolvers = { .from(game_publishers) .innerJoin(publishers, eq(game_publishers.publisher_id, publishers.id)) .where(eq(game_publishers.game_id, game.id)), - platforms: (game: { id: number }) => db.select({ - id: platforms.id, - name: platforms.name, - description: platforms.description, - }) - .from(game_platforms) - .innerJoin(platforms, eq(game_platforms.platform_id, platforms.id)) - .where(eq(game_platforms.game_id, game.id)), + platforms: platformsForGame + }, + GameSummary: { + platforms: platformsForGame, }, }; diff --git a/T04-Project-2/backend/src/graphql/schema.ts b/T04-Project-2/backend/src/graphql/schema.ts index 0d55350..42b7783 100644 --- a/T04-Project-2/backend/src/graphql/schema.ts +++ b/T04-Project-2/backend/src/graphql/schema.ts @@ -25,6 +25,7 @@ export const typeDefs = /* GraphQL */ ` released: String background_image: String description: String + platforms: [Platform!]! } type Developer { diff --git a/T04-Project-2/frontend/package-lock.json b/T04-Project-2/frontend/package-lock.json index 389d82c..8c5a95e 100644 --- a/T04-Project-2/frontend/package-lock.json +++ b/T04-Project-2/frontend/package-lock.json @@ -15,6 +15,7 @@ "embla-carousel-wheel-gestures": "^8.1.0", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-icons": "^5.7.0", "react-router-dom": "^7.18.4" }, "devDependencies": { @@ -3645,6 +3646,15 @@ "react": "^19.3.0" } }, + "node_modules/react-icons": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/T04-Project-2/frontend/package.json b/T04-Project-2/frontend/package.json index 9cbe590..a2858a4 100644 --- a/T04-Project-2/frontend/package.json +++ b/T04-Project-2/frontend/package.json @@ -20,6 +20,7 @@ "embla-carousel-wheel-gestures": "^8.1.0", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-icons": "^5.7.0", "react-router-dom": "^7.18.4" }, "devDependencies": { diff --git a/T04-Project-2/frontend/src/api/games.ts b/T04-Project-2/frontend/src/api/games.ts index 9896b65..d760a4b 100644 --- a/T04-Project-2/frontend/src/api/games.ts +++ b/T04-Project-2/frontend/src/api/games.ts @@ -1,10 +1,10 @@ -import type { Game } from "../typeDefs"; +import type { Game, Platform } from "../typeDefs"; import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; import type { GameSearchOptions } from "./filters"; export type GameSearchResult = Pick; export type GameCardSummary = Pick; -export type FeaturedGameSummary = GameCardSummary & Pick; +export type FeaturedGameSummary = GameCardSummary & Pick & {platforms: Pick[];}; export type GameReview = { id: string; @@ -370,7 +370,7 @@ export function fetchGamesByIds(ids: readonly string[]): Promise { - return fetchGameSummariesByIds(ids, "id name released background_image description"); + return fetchGameSummariesByIds(ids, "id name released background_image description platforms { id name }"); } const GAME_QUERY_STALE_TIME = 5 * 60 * 1000; diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.css b/T04-Project-2/frontend/src/components/FeaturedCarousel.css index ebf16bf..563d7e2 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.css +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.css @@ -67,6 +67,7 @@ min-width: 0; box-sizing: border-box; background-color: var(--secondary-bg); + gap: 0.5rem; display: flex; flex-direction: column; justify-content: center; @@ -84,6 +85,7 @@ .game-release-info { color: var(--subtext); + margin: 0; } .game-info-description { diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx index 33ee5ba..78e4788 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx @@ -8,6 +8,7 @@ import { gameQueryOptions } from "../api/games" import { Link } from "react-router-dom" import { useFavorites } from "./useFavorites" import { GameLoading } from "./GameLoading" +import { PlatformDisplay } from './PlatformDisplay' type FeaturedCarouselProps = { gameIds: readonly string[]; @@ -103,6 +104,7 @@ export const FeaturedCarousel = ({gameIds}: FeaturedCarouselProps) => {

{currentGameData.name}

+

{currentGameData.released ? `Released ${currentGameData.released}` : 'Release date unknown'}

{description || 'No description is available for this game yet.'} diff --git a/T04-Project-2/frontend/src/components/PlatformDisplay.css b/T04-Project-2/frontend/src/components/PlatformDisplay.css new file mode 100644 index 0000000..1724215 --- /dev/null +++ b/T04-Project-2/frontend/src/components/PlatformDisplay.css @@ -0,0 +1,5 @@ +.platform-display { + display: flex; + gap: 0.5rem; + color: var(--text); +} \ No newline at end of file diff --git a/T04-Project-2/frontend/src/components/PlatformDisplay.tsx b/T04-Project-2/frontend/src/components/PlatformDisplay.tsx new file mode 100644 index 0000000..e065d58 --- /dev/null +++ b/T04-Project-2/frontend/src/components/PlatformDisplay.tsx @@ -0,0 +1,38 @@ +import type { IconType } from "react-icons"; +import { FaWindows, FaPlaystation, FaXbox, FaApple, FaLinux, FaAndroid, FaGamepad } from "react-icons/fa"; +import { BsNintendoSwitch, BsGlobe } from "react-icons/bs"; +import { MdPhoneIphone } from "react-icons/md"; +import type { Platform } from "../typeDefs"; +import './PlatformDisplay.css' + +const rules: [RegExp, IconType][] = [ + [/^pc$/i, FaWindows], + [/playstation|psp|ps vita/i, FaPlaystation], + [/xbox/i, FaXbox], + [/nintendo|wii|game ?boy|gamecube|snes|nes/i, BsNintendoSwitch], + [/ios/i, MdPhoneIphone], + [/macos|mac|apple/i, FaApple], + [/linux/i, FaLinux], + [/android/i, FaAndroid], + [/web/i, BsGlobe], +]; + +function getPlatformIcon(name: string): IconType { + return rules.find(([platform]) => platform.test(name))?.[1] ?? FaGamepad; +} + + +type PlatformDisplayProps = { + platforms: Pick[] +} + +export const PlatformDisplay = ({platforms}: PlatformDisplayProps) => { + const icons = [...new Map(platforms.map((p) => [getPlatformIcon(p.name), p])).entries()]; + return ( +

+ {icons.map(([Icon, p]) => ( + + ))} +
+ ) +} \ No newline at end of file From 4462e161b2fbd9830ce35c3ce5668304259e2a30 Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Sat, 7 Nov 2026 12:06:51 +0100 Subject: [PATCH 02/10] Tweaked positioning and sizing of featured games info to make it look better on all screen sizes (#77) --- .../src/components/FeaturedCarousel.css | 27 ++++++++++++++++--- .../src/components/FeaturedCarousel.tsx | 13 ++++++--- .../frontend/src/components/LoginPage.tsx | 6 ++++- .../src/components/PlatformDisplay.css | 2 +- .../src/components/PlatformDisplay.tsx | 7 ++--- T04-Project-2/frontend/src/index.css | 8 +++--- 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.css b/T04-Project-2/frontend/src/components/FeaturedCarousel.css index 563d7e2..34aa541 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.css +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.css @@ -65,19 +65,19 @@ .game-info { min-width: 0; + height: 100%; box-sizing: border-box; background-color: var(--secondary-bg); - gap: 0.5rem; + gap: 0.75rem; display: flex; flex-direction: column; - justify-content: center; padding: clamp(1.25rem, 3vw, 3rem); } .game-title { margin: 0; line-height: 1.15; - font-size: clamp(1.3rem, 3vw, 2rem); + font-size: clamp(1.3rem, 2.8vw, 3rem); overflow-wrap: anywhere; color: var(--primary-color); @@ -86,6 +86,9 @@ .game-release-info { color: var(--subtext); margin: 0; + display: flex; + align-items: center; + gap: 0.3rem; } .game-info-description { @@ -101,6 +104,12 @@ line-clamp: 7; } +.featured-platforms { + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.5rem; + font-size: clamp(1rem, 2vw, 1.5rem); +} + .games-container { position: relative; min-width: 0; @@ -115,6 +124,11 @@ height: 100%; object-fit: cover; } + +.featured-button { + background-color: var(--primary-color-dim); + margin-top: 1rem; +} .featured-image[data-role = 'hidden'] { display: none; } @@ -201,6 +215,13 @@ } } +@media (max-width: 768px) { + .game-info-description { + -webkit-line-clamp: 4; + line-clamp: 4; + } +} + @media (prefers-reduced-motion: reduce) { .featured-image[data-role = 'current'][data-direction] { animation: fade-in 0.4s ease both; diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx index 78e4788..8e2401d 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx @@ -1,5 +1,5 @@ import './FeaturedCarousel.css' -import { CaretLeftIcon, CaretRightIcon, Star } from "@phosphor-icons/react" +import { CaretLeftIcon, CaretRightIcon, Star, CalendarBlankIcon } from "@phosphor-icons/react" import { Button } from "./Button" import { useCallback, useEffect, useState } from "react" import { useQuery } from "@tanstack/react-query" @@ -9,6 +9,7 @@ import { Link } from "react-router-dom" import { useFavorites } from "./useFavorites" import { GameLoading } from "./GameLoading" import { PlatformDisplay } from './PlatformDisplay' +import { ButtonContent } from './ButtonContent' type FeaturedCarouselProps = { gameIds: readonly string[]; @@ -104,11 +105,17 @@ export const FeaturedCarousel = ({gameIds}: FeaturedCarouselProps) => {

{currentGameData.name}

- -

{currentGameData.released ? `Released ${currentGameData.released}` : 'Release date unknown'}

+ +

{currentGameData.released ? `Released ${currentGameData.released}` : 'Release date unknown'}

{description || 'No description is available for this game yet.'}

+
{userId && ( diff --git a/T04-Project-2/frontend/src/components/LoginPage.tsx b/T04-Project-2/frontend/src/components/LoginPage.tsx index 9ca053d..cb89c0b 100644 --- a/T04-Project-2/frontend/src/components/LoginPage.tsx +++ b/T04-Project-2/frontend/src/components/LoginPage.tsx @@ -71,7 +71,11 @@ function LoginPage({ onClose, onAuthenticated }: LoginPageProps) {

{isCreatingAccount ? 'Create your account' : 'Welcome back'}

{isCreatingAccount - ? 'Join the GamingGoat community.' + ? ( + <> + Join the herd of GamingGoats today! + + ) : 'Log in to continue to your gaming community.'}

diff --git a/T04-Project-2/frontend/src/components/PlatformDisplay.css b/T04-Project-2/frontend/src/components/PlatformDisplay.css index 1724215..b07c7b1 100644 --- a/T04-Project-2/frontend/src/components/PlatformDisplay.css +++ b/T04-Project-2/frontend/src/components/PlatformDisplay.css @@ -1,5 +1,5 @@ .platform-display { display: flex; - gap: 0.5rem; + gap: 0.75rem; color: var(--text); } \ No newline at end of file diff --git a/T04-Project-2/frontend/src/components/PlatformDisplay.tsx b/T04-Project-2/frontend/src/components/PlatformDisplay.tsx index e065d58..47259be 100644 --- a/T04-Project-2/frontend/src/components/PlatformDisplay.tsx +++ b/T04-Project-2/frontend/src/components/PlatformDisplay.tsx @@ -24,14 +24,15 @@ function getPlatformIcon(name: string): IconType { type PlatformDisplayProps = { platforms: Pick[] + className?: string; } -export const PlatformDisplay = ({platforms}: PlatformDisplayProps) => { +export const PlatformDisplay = ({platforms, className}: PlatformDisplayProps) => { const icons = [...new Map(platforms.map((p) => [getPlatformIcon(p.name), p])).entries()]; return ( -
+
{icons.map(([Icon, p]) => ( - + ))}
) diff --git a/T04-Project-2/frontend/src/index.css b/T04-Project-2/frontend/src/index.css index ef9ae91..2da9a4c 100644 --- a/T04-Project-2/frontend/src/index.css +++ b/T04-Project-2/frontend/src/index.css @@ -40,6 +40,10 @@ body { margin: 0; } +.text-highlight { + color: var(--primary-color); +} + @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; @@ -50,7 +54,3 @@ body { [id] { scroll-margin-top: 8rem; } - -.long { - height: 2000px; -} \ No newline at end of file From b1c9af830869e40c5704d79a153651b98322e42d Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Sat, 7 Nov 2026 13:09:55 +0100 Subject: [PATCH 03/10] Added rating to game summary to display in featured games carousel (#77) --- .../backend/src/graphql/resolvers.ts | 2 ++ T04-Project-2/backend/src/graphql/schema.ts | 1 + T04-Project-2/frontend/src/api/games.ts | 4 +-- .../src/components/FeaturedCarousel.css | 29 +++++++++++++++---- .../src/components/FeaturedCarousel.tsx | 13 ++++++++- .../frontend/src/components/GameSection.css | 4 --- .../frontend/src/components/GameSection.tsx | 5 +--- .../frontend/src/components/HomePage.tsx | 2 +- .../components/FeaturedCarousel.test.tsx | 4 +-- 9 files changed, 45 insertions(+), 19 deletions(-) diff --git a/T04-Project-2/backend/src/graphql/resolvers.ts b/T04-Project-2/backend/src/graphql/resolvers.ts index 47acc68..dc577e1 100644 --- a/T04-Project-2/backend/src/graphql/resolvers.ts +++ b/T04-Project-2/backend/src/graphql/resolvers.ts @@ -179,6 +179,7 @@ export const resolvers = { released: games.released, background_image: games.background_image, description: games.description, + rating: games.rating, }) .from(games) .where(inArray(games.id, gameIds)); @@ -208,6 +209,7 @@ export const resolvers = { released: games.released, background_image: games.background_image, description: games.description, + rating: games.rating, }) .from(games) .where(inArray(games.rawg_id, gameIds)); diff --git a/T04-Project-2/backend/src/graphql/schema.ts b/T04-Project-2/backend/src/graphql/schema.ts index 42b7783..49a35b0 100644 --- a/T04-Project-2/backend/src/graphql/schema.ts +++ b/T04-Project-2/backend/src/graphql/schema.ts @@ -25,6 +25,7 @@ export const typeDefs = /* GraphQL */ ` released: String background_image: String description: String + rating: Float platforms: [Platform!]! } diff --git a/T04-Project-2/frontend/src/api/games.ts b/T04-Project-2/frontend/src/api/games.ts index d760a4b..b78b467 100644 --- a/T04-Project-2/frontend/src/api/games.ts +++ b/T04-Project-2/frontend/src/api/games.ts @@ -4,7 +4,7 @@ import type { GameSearchOptions } from "./filters"; export type GameSearchResult = Pick; export type GameCardSummary = Pick; -export type FeaturedGameSummary = GameCardSummary & Pick & {platforms: Pick[];}; +export type FeaturedGameSummary = GameCardSummary & Pick & {platforms: Pick[];}; export type GameReview = { id: string; @@ -370,7 +370,7 @@ export function fetchGamesByIds(ids: readonly string[]): Promise { - return fetchGameSummariesByIds(ids, "id name released background_image description platforms { id name }"); + return fetchGameSummariesByIds(ids, "id name released background_image description rating platforms { id name }"); } const GAME_QUERY_STALE_TIME = 5 * 60 * 1000; diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.css b/T04-Project-2/frontend/src/components/FeaturedCarousel.css index 34aa541..d72ef5d 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.css +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.css @@ -82,6 +82,11 @@ color: var(--primary-color); } +.release-rating-container { + display: flex; + gap: 1.5rem; + color: var(--subtext); +} .game-release-info { color: var(--subtext); @@ -91,6 +96,20 @@ gap: 0.3rem; } +.featured-rating { + font-size: 1rem; + margin: 0; + font-weight: bold; + display: flex; + gap: 0.3rem; + color: var(--text); +} + +.featured-star { + color: var(--primary-color); + font-size: 1.2rem; +} + .game-info-description { display: -webkit-box; max-width: 42ch; @@ -100,13 +119,11 @@ font-size: 1rem; line-height: 1.5; -webkit-box-orient: vertical; - -webkit-line-clamp: 7; - line-clamp: 7; + -webkit-line-clamp: 8; + line-clamp: 8; } .featured-platforms { - border-bottom: 1px solid var(--border-color); - padding-bottom: 0.5rem; font-size: clamp(1rem, 2vw, 1.5rem); } @@ -126,8 +143,10 @@ } .featured-button { - background-color: var(--primary-color-dim); + background-color: var(--primary-color); margin-top: 1rem; + font-size: clamp(1rem, 1.4vw, 1.15rem); + color: var(--dark-text); } .featured-image[data-role = 'hidden'] { display: none; diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx index 8e2401d..769c3c1 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx @@ -10,6 +10,7 @@ import { useFavorites } from "./useFavorites" import { GameLoading } from "./GameLoading" import { PlatformDisplay } from './PlatformDisplay' import { ButtonContent } from './ButtonContent' +import { StarIcon } from '@phosphor-icons/react/dist/ssr' type FeaturedCarouselProps = { gameIds: readonly string[]; @@ -106,7 +107,17 @@ export const FeaturedCarousel = ({gameIds}: FeaturedCarouselProps) => {

{currentGameData.name}

-

{currentGameData.released ? `Released ${currentGameData.released}` : 'Release date unknown'}

+
+

+ + {currentGameData.released ? `Released ${currentGameData.released}` : 'Release date unknown'} +

+ | +

+ {currentGameData.rating ? Math.round(currentGameData.rating * 10) /10 : "No ratings"} + +

+

{description || 'No description is available for this game yet.'}

diff --git a/T04-Project-2/frontend/src/components/GameSection.css b/T04-Project-2/frontend/src/components/GameSection.css index e5323d4..14440de 100644 --- a/T04-Project-2/frontend/src/components/GameSection.css +++ b/T04-Project-2/frontend/src/components/GameSection.css @@ -3,10 +3,6 @@ margin-top: 2rem; } -.game-category { - margin-left: 1.5rem; -} - .game-section > .heading-container + .game-category { padding-top: 1rem; } diff --git a/T04-Project-2/frontend/src/components/GameSection.tsx b/T04-Project-2/frontend/src/components/GameSection.tsx index 0d9ff43..501bbc3 100644 --- a/T04-Project-2/frontend/src/components/GameSection.tsx +++ b/T04-Project-2/frontend/src/components/GameSection.tsx @@ -5,14 +5,11 @@ import { SectionHeading } from "./SectionHeading" type GameSectionProps = { gameCategories: readonly GameCategory[], - heading: string, - highlight: string, } -export const GameSection = ({gameCategories, heading, highlight}: GameSectionProps) => { +export const GameSection = ({gameCategories}: GameSectionProps) => { return (
- {gameCategories.map((category) => (
diff --git a/T04-Project-2/frontend/src/components/HomePage.tsx b/T04-Project-2/frontend/src/components/HomePage.tsx index 333be8a..7d8729d 100644 --- a/T04-Project-2/frontend/src/components/HomePage.tsx +++ b/T04-Project-2/frontend/src/components/HomePage.tsx @@ -8,7 +8,7 @@ function HomePage() { <> - + ) } diff --git a/T04-Project-2/frontend/tests/components/FeaturedCarousel.test.tsx b/T04-Project-2/frontend/tests/components/FeaturedCarousel.test.tsx index 3bcd032..3cb09e8 100644 --- a/T04-Project-2/frontend/tests/components/FeaturedCarousel.test.tsx +++ b/T04-Project-2/frontend/tests/components/FeaturedCarousel.test.tsx @@ -10,8 +10,8 @@ import { FavoritesProvider } from '../../src/components/FavoritesProvider' const apiUrl = 'http://localhost:4000/graphql' const games = [ - { id: '1', name: 'Goat Quest', released: '2020-01-02', background_image: null, description: '

Explore as a goat.

' }, - { id: '2', name: 'Goat Racing', released: null, background_image: null, description: null }, + { id: '1', name: 'Goat Quest', released: '2020-01-02', background_image: null, description: '

Explore as a goat.

', rating: 4.2, platforms: [{ id: '4', name: 'PC' }] }, + { id: '2', name: 'Goat Racing', released: null, background_image: null, description: null, rating: null, platforms: [] }, ] const server = setupServer( http.post(apiUrl, async () => { From 0cd417e5c41bea54eb89789ffc382f7f7e258dda Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Sun, 8 Nov 2026 12:07:06 +0100 Subject: [PATCH 04/10] Tweaked possitioning of elements and dynamic height of featured carousel (#77) --- .../src/components/FeaturedCarousel.css | 25 +++++++++++++------ .../src/components/FeaturedCarousel.tsx | 20 ++++++++------- .../src/components/PlatformDisplay.css | 2 +- T04-Project-2/frontend/src/index.css | 2 +- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.css b/T04-Project-2/frontend/src/components/FeaturedCarousel.css index d72ef5d..7563523 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.css +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.css @@ -20,8 +20,8 @@ .featured-favorite { position: absolute; z-index: 2; - top: 0.75rem; - right: 0.75rem; + top: 1.5rem; + right: 1.2rem; display: grid; width: 2.75rem; aspect-ratio: 1; @@ -47,9 +47,9 @@ .image-info-container { display: grid; - grid-template-columns: minmax(0, 1.8fr) minmax(0, 1fr); + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); width: 100%; - min-height: clamp(24rem, 36vw, 32rem); + min-height: clamp(24rem, 32vw, 32rem); min-width: 0; box-sizing: border-box; border-radius: var(--border-radius); @@ -112,15 +112,14 @@ .game-info-description { display: -webkit-box; - max-width: 42ch; overflow: hidden; margin: 0.5rem 0 0; color: var(--subtext); font-size: 1rem; - line-height: 1.5; + line-height: 1.6; -webkit-box-orient: vertical; - -webkit-line-clamp: 8; - line-clamp: 8; + -webkit-line-clamp: 5; + line-clamp: 5; } .featured-platforms { @@ -142,6 +141,13 @@ object-fit: cover; } +.info-button-container { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +} + .featured-button { background-color: var(--primary-color); margin-top: 1rem; @@ -239,6 +245,9 @@ -webkit-line-clamp: 4; line-clamp: 4; } + .release-rating-container { + gap: 0.8rem; + } } @media (prefers-reduced-motion: reduce) { diff --git a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx index 769c3c1..700eaae 100644 --- a/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx +++ b/T04-Project-2/frontend/src/components/FeaturedCarousel.tsx @@ -118,15 +118,17 @@ export const FeaturedCarousel = ({gameIds}: FeaturedCarouselProps) => {

-

- {description || 'No description is available for this game yet.'} -

-
{userId && ( diff --git a/T04-Project-2/frontend/src/components/PlatformDisplay.css b/T04-Project-2/frontend/src/components/PlatformDisplay.css index b07c7b1..d1200bf 100644 --- a/T04-Project-2/frontend/src/components/PlatformDisplay.css +++ b/T04-Project-2/frontend/src/components/PlatformDisplay.css @@ -1,5 +1,5 @@ .platform-display { display: flex; - gap: 0.75rem; + gap: 0.6rem; color: var(--text); } \ No newline at end of file diff --git a/T04-Project-2/frontend/src/index.css b/T04-Project-2/frontend/src/index.css index 2da9a4c..e0ac56b 100644 --- a/T04-Project-2/frontend/src/index.css +++ b/T04-Project-2/frontend/src/index.css @@ -19,7 +19,7 @@ /* Shared horizontal spacing so every section's content lines up. The max() keeps content from growing wider than --content-max-width on very wide screens. */ - --content-max-width: 100rem; + --content-max-width: 75rem; --page-gutter: max(clamp(1.5rem, 4vw, 5rem), (100% - var(--content-max-width)) / 2); font: 18px/145% var(--sans); From 64afef9f67db8bee96a3b4da405d4109ab6011bc Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Mon, 9 Nov 2026 01:20:04 +0100 Subject: [PATCH 05/10] Made a top 5 games component to break up carousels on the home page (#77) --- .../frontend/src/components/FeaturedGames.css | 4 +- .../src/components/FilterDropdown.css | 2 +- .../frontend/src/components/FilterSection.css | 2 - .../frontend/src/components/GameCard.css | 2 +- .../frontend/src/components/GameSection.tsx | 10 +- .../frontend/src/components/LoginPage.tsx | 10 +- .../src/components/SectionHeading.css | 12 +- .../src/components/SectionHeading.tsx | 29 +-- .../frontend/src/components/StarDisplay.css | 14 ++ .../frontend/src/components/StarDisplay.tsx | 23 +++ .../frontend/src/components/Top5Games.css | 165 ++++++++++++++++++ .../frontend/src/components/Top5Games.tsx | 74 ++++++++ T04-Project-2/frontend/src/data/games.ts | 31 ++-- .../frontend/src/data/top5MockGames.ts | 73 ++++++++ T04-Project-2/frontend/src/index.css | 17 ++ 15 files changed, 427 insertions(+), 41 deletions(-) create mode 100644 T04-Project-2/frontend/src/components/StarDisplay.css create mode 100644 T04-Project-2/frontend/src/components/StarDisplay.tsx create mode 100644 T04-Project-2/frontend/src/components/Top5Games.css create mode 100644 T04-Project-2/frontend/src/components/Top5Games.tsx create mode 100644 T04-Project-2/frontend/src/data/top5MockGames.ts diff --git a/T04-Project-2/frontend/src/components/FeaturedGames.css b/T04-Project-2/frontend/src/components/FeaturedGames.css index f9f094f..b2bc3c6 100644 --- a/T04-Project-2/frontend/src/components/FeaturedGames.css +++ b/T04-Project-2/frontend/src/components/FeaturedGames.css @@ -3,8 +3,8 @@ margin-top: -0rem; } .featured-games { - position: relative; /* lets ::before be positioned inside the section */ - isolation: isolate; /* keeps z-index: -1 inside the section instead of behind the page */ + position: relative; + isolation: isolate; } .featured-games::before { diff --git a/T04-Project-2/frontend/src/components/FilterDropdown.css b/T04-Project-2/frontend/src/components/FilterDropdown.css index b69219a..31f284a 100644 --- a/T04-Project-2/frontend/src/components/FilterDropdown.css +++ b/T04-Project-2/frontend/src/components/FilterDropdown.css @@ -55,7 +55,7 @@ font-size: 0.8rem; } .filter-dropdown-option:hover { - background-color: rgba(48, 48, 48, 0.346); + background-color: var(--bg-hover); } .filter-checkbox { diff --git a/T04-Project-2/frontend/src/components/FilterSection.css b/T04-Project-2/frontend/src/components/FilterSection.css index b07af0e..4270a69 100644 --- a/T04-Project-2/frontend/src/components/FilterSection.css +++ b/T04-Project-2/frontend/src/components/FilterSection.css @@ -1,4 +1,3 @@ -/* the gap moves into the body so a closed section doesn't leave empty space */ .filter-section-container { gap: 0; } @@ -43,7 +42,6 @@ display: flex; flex-direction: column; gap: 0.5rem; - /* room for the slider thumbs, which stick out past the track */ padding: 0.75rem 0 0.5rem; } diff --git a/T04-Project-2/frontend/src/components/GameCard.css b/T04-Project-2/frontend/src/components/GameCard.css index f74328e..810fd37 100644 --- a/T04-Project-2/frontend/src/components/GameCard.css +++ b/T04-Project-2/frontend/src/components/GameCard.css @@ -45,7 +45,7 @@ width: 100%; aspect-ratio: 3 / 2; overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.14); + border: 1px solid var(--border-color-light); border-radius: 1.25rem; background: #21171e; box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.28); diff --git a/T04-Project-2/frontend/src/components/GameSection.tsx b/T04-Project-2/frontend/src/components/GameSection.tsx index 501bbc3..50f0968 100644 --- a/T04-Project-2/frontend/src/components/GameSection.tsx +++ b/T04-Project-2/frontend/src/components/GameSection.tsx @@ -2,6 +2,7 @@ import type { GameCategory } from "../data/games" import { GameCardCarousel } from "./GameCardCarousel" import './GameSection.css' import { SectionHeading } from "./SectionHeading" +import { Top5Games } from './Top5Games' type GameSectionProps = { gameCategories: readonly GameCategory[], @@ -10,7 +11,14 @@ type GameSectionProps = { export const GameSection = ({gameCategories}: GameSectionProps) => { return (
- {gameCategories.map((category) => ( + {gameCategories.slice(0, 2).map((category) => ( +
+ + +
+ ))} + + {gameCategories.slice(2, 4).map((category) => (
diff --git a/T04-Project-2/frontend/src/components/LoginPage.tsx b/T04-Project-2/frontend/src/components/LoginPage.tsx index cb89c0b..52da3d3 100644 --- a/T04-Project-2/frontend/src/components/LoginPage.tsx +++ b/T04-Project-2/frontend/src/components/LoginPage.tsx @@ -71,12 +71,8 @@ function LoginPage({ onClose, onAuthenticated }: LoginPageProps) {

{isCreatingAccount ? 'Create your account' : 'Welcome back'}

{isCreatingAccount - ? ( - <> - Join the herd of GamingGoats today! - - ) - : 'Log in to continue to your gaming community.'} + ? 'Join the herd of GamingGoats today!' + : 'Log in to interact with the GamingGoat herd.'}

{isCreatingAccount ? ( @@ -165,7 +161,7 @@ function LoginPage({ onClose, onAuthenticated }: LoginPageProps) { setIsCreatingAccount((current) => !current); }} > - {isCreatingAccount ? 'Already have an account? Log in' : 'Create account'} + {isCreatingAccount ? 'Already part of the herd? Log in' : 'Create account'} diff --git a/T04-Project-2/frontend/src/components/SectionHeading.css b/T04-Project-2/frontend/src/components/SectionHeading.css index ab2ed0f..cb0d01f 100644 --- a/T04-Project-2/frontend/src/components/SectionHeading.css +++ b/T04-Project-2/frontend/src/components/SectionHeading.css @@ -1,11 +1,19 @@ +.main-heading-container { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 1rem 0; +} + .heading-container { display: flex; align-items: center; gap: 0.5rem; - margin: 1rem 0; width: 100%; + + position: relative; } -.heading-container[data-header-line = 'true'] { +.main-heading-container[data-header-line = 'true'] { padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-color); } diff --git a/T04-Project-2/frontend/src/components/SectionHeading.tsx b/T04-Project-2/frontend/src/components/SectionHeading.tsx index ca9aa57..322fe46 100644 --- a/T04-Project-2/frontend/src/components/SectionHeading.tsx +++ b/T04-Project-2/frontend/src/components/SectionHeading.tsx @@ -4,21 +4,30 @@ type SectionHeadingProps = { text: string highlight?: string headerLine?: boolean + subtext?: string } -export const SectionHeading = ({text, highlight, headerLine}: SectionHeadingProps) => { +export const SectionHeading = ({text, highlight, headerLine, subtext}: SectionHeadingProps) => { const textParts = text.split(new RegExp(`(${highlight})`, 'i')); return ( -
- -

- {textParts.map((part, i) => ( - part.toLowerCase() === highlight?.toLowerCase() - ? {part} - : part - ))} -

+
+
+ +

+ {textParts.map((part, i) => ( + part.toLowerCase() === highlight?.toLowerCase() + ? {part} + : part + ))} +

+
+ {subtext ? ( +

+ {subtext} +

+ ): "" + }
) } \ No newline at end of file diff --git a/T04-Project-2/frontend/src/components/StarDisplay.css b/T04-Project-2/frontend/src/components/StarDisplay.css new file mode 100644 index 0000000..e2504ff --- /dev/null +++ b/T04-Project-2/frontend/src/components/StarDisplay.css @@ -0,0 +1,14 @@ +.star-container-small { + display: flex; + align-items: center; + gap: 0.3rem; +} + +.small { + font-size: clamp(0.8rem, 1.3vw, 1rem); + cursor: default; +} +.small { + font-size: clamp(0.8rem, 1.3vw, 1rem); + cursor: default; +} \ No newline at end of file diff --git a/T04-Project-2/frontend/src/components/StarDisplay.tsx b/T04-Project-2/frontend/src/components/StarDisplay.tsx new file mode 100644 index 0000000..cc9b238 --- /dev/null +++ b/T04-Project-2/frontend/src/components/StarDisplay.tsx @@ -0,0 +1,23 @@ +import { StarIcon } from "@phosphor-icons/react" +import './StarDisplay.css' + +type StarDisplayProps = { + rating: number +} + +export const StarDisplay = ( { rating }: StarDisplayProps) => { + return ( +
+ {Array.from({ length : 5}).map((_, index) => ( +
+ + + + +
+ ))} +
+ ) +} \ No newline at end of file diff --git a/T04-Project-2/frontend/src/components/Top5Games.css b/T04-Project-2/frontend/src/components/Top5Games.css new file mode 100644 index 0000000..2d279a9 --- /dev/null +++ b/T04-Project-2/frontend/src/components/Top5Games.css @@ -0,0 +1,165 @@ +.top-5-games { + display: flex; + flex-direction: column; + margin-bottom: 5rem; +} + +.top-5-content { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 1rem; +} + +.top-5-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + list-style-type: none; + margin: 0; + padding: 0; +} + +.top-5-item { + position: relative; + background-color: var(--top-5-bg); + display: grid; + grid-template-columns: 1fr 3fr; + width: 100%; + border-radius: var(--border-radius-s); + cursor: pointer; +} +.top-5-item:hover { + background-color: rgb(38, 38, 38); +} + +.position { + position: absolute; + top: -10px; + left: -15px; + + display: flex; + align-items: center; + justify-content: center; + + color: var(--dark-text); + font-weight: bold; + font-size: clamp(0.8rem, 1.5vw, 1rem); + + width: 1.8rem; + height: 1.8rem; + border-radius: var(--border-radius-s); + background-color: var(--primary-color); +} +.first { + background-color: gold; + box-shadow: 0px 0px 10px gold; +} + +.top-5-image { + display: block; + width: 100%; + height: auto; + object-fit: cover; + object-position: center; + align-self: center; + aspect-ratio: 2 / 0.75; +} +.image-wrapper { + border-radius: var(--border-radius-s); + overflow: hidden; +} + +.top-5-description { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; +} +.top-5-heading { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + line-clamp: 2; + margin: 0; + font-size: clamp(0.8rem, 1.5vw, 1rem); + line-height: 1.15; +} + +.top-5-release { + font-size: clamp(0.6rem, 1.5vw, 0.8rem); +} + +.name-release { + max-width: 70%; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.top-5-rating { + height: 100%; + display: flex; + align-items: center; + font-size: clamp(0.8rem, 1.5vw, 1rem);; +} +.top-5-star { + font-size: clamp(1.2rem, 2vw, 1.4rem);;; + color: var(--primary-color); +} + +.review-display { + background-color: var(--top-5-bg); + border-radius: var(--border-radius-s); + + box-sizing: border-box; + padding: 1.25rem; + height: 100%; + + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.top-text { + text-transform: uppercase; + font-size: clamp(0.6rem, 1.2vw, 0.8rem); +} + +.heading-section { + margin-top: -0.75rem; + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--border-color-light); +} + + +.scroll-wrapper { + position: relative; + height: 100%; +} + +.review-container { + position: absolute; + inset: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1rem; + + /* scrollbar-color: var(--primary-color) rgba(0, 0, 0, 0); */ + scrollbar-width: none; +} + +.top-5-review { + background-color: rgb(38, 38, 38); + padding: 0.75rem; + border-radius: var(--border-radius-s); +} + +.top-5-review-text { + font-size: clamp(0.6rem, 1.5vw, 0.8rem); + margin-top: 0.3rem; +} diff --git a/T04-Project-2/frontend/src/components/Top5Games.tsx b/T04-Project-2/frontend/src/components/Top5Games.tsx new file mode 100644 index 0000000..0bbee51 --- /dev/null +++ b/T04-Project-2/frontend/src/components/Top5Games.tsx @@ -0,0 +1,74 @@ +import { top5MockGames } from '../data/top5MockGames' +import './Top5Games.css' +import { SectionHeading } from './SectionHeading' +import { StarIcon, CalendarBlankIcon } from '@phosphor-icons/react' +import logo from '../assets/GamingGoatLogo.webp' +import { StarDisplay } from './StarDisplay' + +type Top5GamesProps = { + gameIds?: readonly string[]; +} + +export const Top5Games = ({ gameIds }: Top5GamesProps) => { + return ( +
+ +
+
    + {top5MockGames.map((game, index) => ( +
  1. + {index + 1} +
    + +
    +
    +
    +

    {game.name}

    +

    + + {game.released ? `Released ${game.released}` : 'Release date unknown'} +

    +
    +

    + {game.rating ? Math.round(game.rating * 10) /10 : "No ratings"} + +

    +
    +
  2. + ))} +
+
+ {/* TODO: Implement logic for fetching a review to display here */} +

Community reviews

+
+

Goat Simulator Delux

+

+ + 4.7 +

+
+

What the herd thinks

+
+
+
+

GoatGuy4

+ +

This game changed my life! I feel like it's not worth spending time IRL any more. Amazing graphics, amazing story and most of all, AMAZING GOATS! A clear 5 out of 5 and an absolute no-brainer

+
+
+

KingGoatz

+ +

Epic game

+
+
+

monsterfighter0

+ +

Definitely the most realisitc goat simulation available right now.

+
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/T04-Project-2/frontend/src/data/games.ts b/T04-Project-2/frontend/src/data/games.ts index 6339744..ed52b82 100644 --- a/T04-Project-2/frontend/src/data/games.ts +++ b/T04-Project-2/frontend/src/data/games.ts @@ -270,19 +270,20 @@ export const gameCategories = [ { name: "Indie games", highlight: "games", gameIds: indieGameIds }, { name: "Adventure games", highlight: "games", gameIds: adventureGameIds }, { name: "RPG games", highlight: "games", gameIds: rpgGameIds }, - { name: "Strategy games", highlight: "games", gameIds: strategyGameIds }, - { name: "Shooter games", highlight: "games", gameIds: shooterGameIds }, - { name: "Casual games", highlight: "games", gameIds: casualGameIds }, - { name: "Simulation games", highlight: "games", gameIds: simulationGameIds }, - { name: "Puzzle games", highlight: "games", gameIds: puzzleGameIds }, - { name: "Arcade games", highlight: "games", gameIds: arcadeGameIds }, - { name: "Platformer games", highlight: "games", gameIds: platformerGameIds }, - { name: "Massively Multiplayer games", highlight: "games", gameIds: massivelyMultiplayerGameIds }, - { name: "Racing games", highlight: "games", gameIds: racingGameIds }, - { name: "Sports games", highlight: "games", gameIds: sportsGameIds }, - { name: "Fighting games", highlight: "games", gameIds: fightingGameIds }, - { name: "Family games", highlight: "games", gameIds: familyGameIds }, - { name: "Board Games games", highlight: "games", gameIds: boardGamesGameIds }, - { name: "Card games", highlight: "games", gameIds: cardGameIds }, - { name: "Educational games", highlight: "games", gameIds: educationalGameIds }, + // { name: "Strategy games", highlight: "games", gameIds: strategyGameIds }, + // { name: "Shooter games", highlight: "games", gameIds: shooterGameIds }, + // { name: "Casual games", highlight: "games", gameIds: casualGameIds }, + // { name: "Simulation games", highlight: "games", gameIds: simulationGameIds }, + // { name: "Puzzle games", highlight: "games", gameIds: puzzleGameIds }, + // { name: "Arcade games", highlight: "games", gameIds: arcadeGameIds }, + // { name: "Platformer games", highlight: "games", gameIds: platformerGameIds }, + // { name: "Massively Multiplayer games", highlight: "games", gameIds: massivelyMultiplayerGameIds }, + // { name: "Racing games", highlight: "games", gameIds: racingGameIds }, + // { name: "Sports games", highlight: "games", gameIds: sportsGameIds }, + // { name: "Fighting games", highlight: "games", gameIds: fightingGameIds }, + // { name: "Family games", highlight: "games", gameIds: familyGameIds }, + // { name: "Board Games games", highlight: "games", gameIds: boardGamesGameIds }, + // { name: "Card games", highlight: "games", gameIds: cardGameIds }, + // { name: "Educational games", highlight: "games", gameIds: educationalGameIds }, ] as const; + diff --git a/T04-Project-2/frontend/src/data/top5MockGames.ts b/T04-Project-2/frontend/src/data/top5MockGames.ts new file mode 100644 index 0000000..a895bef --- /dev/null +++ b/T04-Project-2/frontend/src/data/top5MockGames.ts @@ -0,0 +1,73 @@ +import type { FeaturedGameSummary } from "../api/games"; + +// Temporary mock data for designing the Top 5 section layout. +// The entries cover edge cases on purpose: a very long title, a missing image, +// a missing rating, a missing release date, and many vs. few platforms. +export const top5MockGames: FeaturedGameSummary[] = [ + { + id: "mock-1", + name: "Goat Simulator Deluxe", + released: "2023-04-12", + background_image: "https://picsum.photos/seed/goat-1/1280/720", + description: + "

Climb mountains, headbutt fences and cause chaos across a sprawling open world. A sandbox adventure where every cliff is a challenge and every barn is a target.

", + rating: 4.71, + platforms: [ + { id: "4", name: "PC" }, + { id: "187", name: "PlayStation 5" }, + { id: "186", name: "Xbox Series S/X" }, + { id: "7", name: "Nintendo Switch" }, + ], + }, + { + id: "mock-2", + name: "The Legendary Chronicles of the Mountain Herd: Rise of the Eternal Shepherd", + released: "2021-11-03", + background_image: "https://picsum.photos/seed/goat-2/1280/720", + description: + "

An epic role-playing game spanning three kingdoms, dozens of companions and over a hundred hours of story. Lead your herd through war, famine and ancient prophecy as you uncover the truth behind the Eternal Shepherd and decide the fate of the highlands.

", + rating: 4.58, + platforms: [ + { id: "4", name: "PC" }, + { id: "18", name: "PlayStation 4" }, + { id: "1", name: "Xbox One" }, + { id: "5", name: "macOS" }, + { id: "6", name: "Linux" }, + ], + }, + { + id: "mock-3", + name: "Hoof Racer", + released: "2024-08-30", + background_image: null, + description: "

Fast-paced arcade racing on cliffs.

", + rating: 4.32, + platforms: [{ id: "4", name: "PC" }], + }, + { + id: "mock-4", + name: "Pasture Tactics", + released: null, + background_image: "https://picsum.photos/seed/goat-4/1280/720", + description: null, + rating: 4.05, + platforms: [ + { id: "3", name: "iOS" }, + { id: "21", name: "Android" }, + ], + }, + { + id: "mock-5", + name: "Billy & Nanny", + released: "2019-02-14", + background_image: "https://picsum.photos/seed/goat-5/1280/720", + description: + "

A cozy co-op puzzle game about two goats finding their way home. Solve hand-drawn puzzles together on the couch or online.

", + rating: null, + platforms: [ + { id: "4", name: "PC" }, + { id: "7", name: "Nintendo Switch" }, + { id: "171", name: "Web" }, + ], + }, +]; diff --git a/T04-Project-2/frontend/src/index.css b/T04-Project-2/frontend/src/index.css index e0ac56b..64b5956 100644 --- a/T04-Project-2/frontend/src/index.css +++ b/T04-Project-2/frontend/src/index.css @@ -3,6 +3,8 @@ --dark-text: rgb(18, 16, 16); --subtext: #a09f9f; --bg: rgb(18, 16, 16); + --bg-hover: rgba(48, 48, 48, 0.346); + --top-5-bg: rgb(26, 26, 26); --secondary-bg: rgba(32, 32, 32, 0.305); --hero-bg: var(--bg); --nav-bg: rgba(20, 18, 18, 0.757); @@ -16,6 +18,7 @@ --primary-color: #19d24a; --primary-color-dim: #1e7e38; --border-color: rgb(11, 59, 11); + --border-color-light: rgba(255, 255, 255, 0.14); /* Shared horizontal spacing so every section's content lines up. The max() keeps content from growing wider than --content-max-width on very wide screens. */ @@ -40,9 +43,23 @@ body { margin: 0; } +h3 { + font-size: clamp(1rem, 1.6vw, 1.3rem); + margin: 0; +} +h4 { + font-size: clamp(0.8rem, 1.3vw, 1rem); + margin: 0; +} + .text-highlight { color: var(--primary-color); } +.subtext { + color: var(--subtext); + font-size: clamp(0.8rem, 1.3vw, 1rem); + margin: 0; +} @media (prefers-reduced-motion: no-preference) { html { From 231a07d76eb9fc535c3659158bb0fdab09c68c3d Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Mon, 9 Nov 2026 18:46:59 +0100 Subject: [PATCH 06/10] Added auto-scrolling to review display of top 5 games (#77) --- .../frontend/src/components/ReviewCard.tsx | 18 +++ .../frontend/src/components/Top5Games.css | 7 ++ .../frontend/src/components/Top5Games.tsx | 45 ++++--- .../frontend/src/components/useAutoScroll.ts | 113 ++++++++++++++++++ .../frontend/src/data/top5MockGames.ts | 4 +- 5 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 T04-Project-2/frontend/src/components/ReviewCard.tsx create mode 100644 T04-Project-2/frontend/src/components/useAutoScroll.ts diff --git a/T04-Project-2/frontend/src/components/ReviewCard.tsx b/T04-Project-2/frontend/src/components/ReviewCard.tsx new file mode 100644 index 0000000..2c186d3 --- /dev/null +++ b/T04-Project-2/frontend/src/components/ReviewCard.tsx @@ -0,0 +1,18 @@ +import { StarDisplay } from "./StarDisplay" + +type ReviewCardProps = { + username: string + rating: number + text: string +} + + +export const ReviewCard = ( {username, rating, text}: ReviewCardProps) => { + return ( +
+

{username}

+ +

{text}

+
+ ) +} \ No newline at end of file diff --git a/T04-Project-2/frontend/src/components/Top5Games.css b/T04-Project-2/frontend/src/components/Top5Games.css index 2d279a9..3ff6893 100644 --- a/T04-Project-2/frontend/src/components/Top5Games.css +++ b/T04-Project-2/frontend/src/components/Top5Games.css @@ -153,6 +153,13 @@ scrollbar-width: none; } +/* Lays out the duplicated reviews exactly like the originals, so the loop is seamless. */ +.review-loop-copy { + display: flex; + flex-direction: column; + gap: 1rem; +} + .top-5-review { background-color: rgb(38, 38, 38); padding: 0.75rem; diff --git a/T04-Project-2/frontend/src/components/Top5Games.tsx b/T04-Project-2/frontend/src/components/Top5Games.tsx index 0bbee51..0d3d806 100644 --- a/T04-Project-2/frontend/src/components/Top5Games.tsx +++ b/T04-Project-2/frontend/src/components/Top5Games.tsx @@ -3,20 +3,28 @@ import './Top5Games.css' import { SectionHeading } from './SectionHeading' import { StarIcon, CalendarBlankIcon } from '@phosphor-icons/react' import logo from '../assets/GamingGoatLogo.webp' -import { StarDisplay } from './StarDisplay' +import { ReviewCard } from './ReviewCard' +import { LOOP_COPY_ATTRIBUTE, useAutoScroll } from './useAutoScroll' type Top5GamesProps = { gameIds?: readonly string[]; } +const mockReviews = [ + { id: 'review-1', username: 'GoatGuy4', rating: 5, text: "This game changed my life! I feel like it's not worth spending time IRL any more. Amazing graphics, amazing story and most of all, AMAZING GOATS! A clear 5 out of 5 and an absolute no-brainer" }, + { id: 'review-2', username: 'KingGoatz', rating: 4.5, text: 'Epic game' }, + { id: 'review-3', username: 'monsterfighter0', rating: 4.5, text: 'Definitely the most realisitc goat simulation available right now.' }, +] + export const Top5Games = ({ gameIds }: Top5GamesProps) => { + const { ref: scrollRef, isLooping } = useAutoScroll() return (
    {top5MockGames.map((game, index) => ( -
  1. +
  2. {index + 1}
    @@ -49,22 +57,23 @@ export const Top5Games = ({ gameIds }: Top5GamesProps) => {

    What the herd thinks

    -
    -
    -

    GoatGuy4

    - -

    This game changed my life! I feel like it's not worth spending time IRL any more. Amazing graphics, amazing story and most of all, AMAZING GOATS! A clear 5 out of 5 and an absolute no-brainer

    -
    -
    -

    KingGoatz

    - -

    Epic game

    -
    -
    -

    monsterfighter0

    - -

    Definitely the most realisitc goat simulation available right now.

    -
    +
    + {mockReviews.map((r) => )} + + {isLooping && [1, 2].map((copy) => ( + + ))}
    diff --git a/T04-Project-2/frontend/src/components/useAutoScroll.ts b/T04-Project-2/frontend/src/components/useAutoScroll.ts new file mode 100644 index 0000000..5f82b07 --- /dev/null +++ b/T04-Project-2/frontend/src/components/useAutoScroll.ts @@ -0,0 +1,113 @@ +import { useEffect, useRef, useState } from "react"; + +export const LOOP_COPY_ATTRIBUTE = "data-loop-copy" + +export function useAutoScroll() { + const ref = useRef(null); + const [isLooping, setIsLooping] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; + + const speed = 15; + let position = el.scrollTop; + let lastTime: number | null = null; + + const resumeDelay = 1500; + let paused = false; + let resumeTimer: number | undefined; + + let looping = false + + const measureLoopHeight = (): number | null => { + const copy = el.querySelector(`[${LOOP_COPY_ATTRIBUTE}]`) + if(!copy) return null; + return copy.getBoundingClientRect().top - el.getBoundingClientRect().top + el.scrollTop; + }; + + const wrap = (value: number, loopHeight: number): number => { + const maxScroll = el.scrollHeight - el.clientHeight; + const start = Math.max(0, (maxScroll - loopHeight) / 2); + let wrapped = value; + while (wrapped >= start + loopHeight) wrapped -= loopHeight; + while (wrapped < start) wrapped += loopHeight; + return wrapped; + }; + + const pause = () => { + window.clearTimeout(resumeTimer); + paused = true; + } + + const resumeLater = () => { + window.clearTimeout(resumeTimer); + resumeTimer = window.setTimeout(() => {paused = false;}, resumeDelay) + } + + const pauseBriefly = () => { pause(); resumeLater(); }; + const onFocusOut = (event: FocusEvent) => { + if (!el.contains(event.relatedTarget as Node | null)) resumeLater(); + }; + + el.addEventListener("pointerenter", pause); + el.addEventListener("pointerleave", resumeLater); + el.addEventListener("focusin", pause); + el.addEventListener("focusout", onFocusOut); + el.addEventListener("wheel", pauseBriefly, {passive: true}); + el.addEventListener("touchmove", pauseBriefly, {passive: true}) + + const onScroll = () => { + if (!looping) return; + const loopHeight = measureLoopHeight(); + if (loopHeight === null || loopHeight <= 0) return; + const wrapped = wrap(el.scrollTop, loopHeight); + if (Math.abs(wrapped - el.scrollTop) > 0.5) el.scrollTop = wrapped; + }; + el.addEventListener("scroll", onScroll, { passive: true }); + + + let frame = requestAnimationFrame(function step(time) { + const loopHeight = measureLoopHeight(); + const gap = parseFloat(getComputedStyle(el).rowGap) || 0; + const originalsHeight = loopHeight === null ? el.scrollHeight : loopHeight - gap; + const shouldLoop = originalsHeight > el.clientHeight + 2; + + if (shouldLoop !== looping) { + looping = shouldLoop; + setIsLooping(shouldLoop); + } + if(paused) { + position = el.scrollTop; + lastTime = null; + } else if (!looping || loopHeight === null || loopHeight <= 0) { + lastTime = null; + } else { + if (lastTime !== null) { + position += (speed * (time - lastTime)) / 1000; + } + lastTime = time; + position = wrap(position, loopHeight); + el.scrollTop = position; + } + + frame = requestAnimationFrame(step) + }) + + return () => { + cancelAnimationFrame(frame); + + el.removeEventListener("pointerenter", pause); + el.removeEventListener("pointerleave", resumeLater); + el.removeEventListener("focusin", pause); + el.removeEventListener("focusout", onFocusOut); + el.removeEventListener("wheel", pauseBriefly); + el.removeEventListener("touchmove", pauseBriefly); + el.removeEventListener("scroll", onScroll); + window.clearTimeout(resumeTimer); + }; + },[]); + + return { ref, isLooping }; +} \ No newline at end of file diff --git a/T04-Project-2/frontend/src/data/top5MockGames.ts b/T04-Project-2/frontend/src/data/top5MockGames.ts index a895bef..e7e3532 100644 --- a/T04-Project-2/frontend/src/data/top5MockGames.ts +++ b/T04-Project-2/frontend/src/data/top5MockGames.ts @@ -8,7 +8,7 @@ export const top5MockGames: FeaturedGameSummary[] = [ id: "mock-1", name: "Goat Simulator Deluxe", released: "2023-04-12", - background_image: "https://picsum.photos/seed/goat-1/1280/720", + background_image: "https://media.rawg.io/media/games/a44/a444a7628bdb49b24d06a7672f805814.jpg", description: "

    Climb mountains, headbutt fences and cause chaos across a sprawling open world. A sandbox adventure where every cliff is a challenge and every barn is a target.

    ", rating: 4.71, @@ -23,7 +23,7 @@ export const top5MockGames: FeaturedGameSummary[] = [ id: "mock-2", name: "The Legendary Chronicles of the Mountain Herd: Rise of the Eternal Shepherd", released: "2021-11-03", - background_image: "https://picsum.photos/seed/goat-2/1280/720", + background_image: "https://media.rawg.io/media/crop/600/400/games/618/618c2031a07bbff6b4f611f10b6bcdbc.jpg", description: "

    An epic role-playing game spanning three kingdoms, dozens of companions and over a hundred hours of story. Lead your herd through war, famine and ancient prophecy as you uncover the truth behind the Eternal Shepherd and decide the fate of the highlands.

    ", rating: 4.58, From f1fb3abad5751eb9fd4477c61cfea14c94974191 Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Mon, 9 Nov 2026 18:49:20 +0100 Subject: [PATCH 07/10] Fixed a bug where the reviews couldnt be scrolled up when scrollTop was at 0 (#77) --- T04-Project-2/frontend/src/components/useAutoScroll.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/T04-Project-2/frontend/src/components/useAutoScroll.ts b/T04-Project-2/frontend/src/components/useAutoScroll.ts index 5f82b07..0818c91 100644 --- a/T04-Project-2/frontend/src/components/useAutoScroll.ts +++ b/T04-Project-2/frontend/src/components/useAutoScroll.ts @@ -78,6 +78,14 @@ export function useAutoScroll() { looping = shouldLoop; setIsLooping(shouldLoop); } + if (looping && loopHeight !== null && loopHeight > 0) { + const wrapped = wrap(el.scrollTop, loopHeight); + if (Math.abs(wrapped - el.scrollTop) > 0.5) { + el.scrollTop = wrapped; + position = wrapped; + } + } + if(paused) { position = el.scrollTop; lastTime = null; From 9a022c0f249fca5d28cc69d2b0f8573a7199648f Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Mon, 9 Nov 2026 19:43:06 +0100 Subject: [PATCH 08/10] Implemented hook to prevent review display from rendering on narrow screens (#77) --- .../frontend/src/components/ReviewPanel.tsx | 49 +++++++++++++++++++ .../frontend/src/components/Top5Games.css | 5 +- .../frontend/src/components/Top5Games.tsx | 49 +++---------------- .../frontend/src/components/useMediaQuery.ts | 12 +++++ 4 files changed, 72 insertions(+), 43 deletions(-) create mode 100644 T04-Project-2/frontend/src/components/ReviewPanel.tsx create mode 100644 T04-Project-2/frontend/src/components/useMediaQuery.ts diff --git a/T04-Project-2/frontend/src/components/ReviewPanel.tsx b/T04-Project-2/frontend/src/components/ReviewPanel.tsx new file mode 100644 index 0000000..901cbff --- /dev/null +++ b/T04-Project-2/frontend/src/components/ReviewPanel.tsx @@ -0,0 +1,49 @@ +import { StarIcon } from '@phosphor-icons/react' +import { ReviewCard } from './ReviewCard' +import { LOOP_COPY_ATTRIBUTE, useAutoScroll } from './useAutoScroll' + +const mockReviews = [ + { id: 'review-1', username: 'GoatGuy4', rating: 5, text: "This game changed my life! I feel like it's not worth spending time IRL any more. Amazing graphics, amazing story and most of all, AMAZING GOATS! A clear 5 out of 5 and an absolute no-brainer" }, + { id: 'review-2', username: 'KingGoatz', rating: 4.5, text: 'Epic game' }, + { id: 'review-3', username: 'monsterfighter0', rating: 4.5, text: 'Definitely the most realisitc goat simulation available right now.' }, + { id: 'review-4', username: 'HighlandHerder', rating: 3.5, text: "Great fun for the first few hours, but the physics get a bit wild once you unlock the rocket hooves. Still, nothing beats headbutting a mailbox off a cliff. Would love a proper co-op mode in a future update." }, + { id: 'review-5', username: 'nanny_g0at', rating: 4, text: 'Bleat-tastic.' }, +] + +export const ReviewPanel = () => { + const { ref: scrollRef, isLooping } = useAutoScroll() + + return ( +
    + {/* TODO: Implement logic for fetching a review to display here */} +

    Community reviews

    +
    +

    Goat Simulator Delux

    +

    + + 4.7 +

    +
    +

    What the herd thinks

    +
    +
    + {mockReviews.map((r) => )} + + {isLooping && [1, 2].map((copy) => ( + + ))} +
    +
    +
    + ) +} diff --git a/T04-Project-2/frontend/src/components/Top5Games.css b/T04-Project-2/frontend/src/components/Top5Games.css index 3ff6893..88774b1 100644 --- a/T04-Project-2/frontend/src/components/Top5Games.css +++ b/T04-Project-2/frontend/src/components/Top5Games.css @@ -10,6 +10,10 @@ gap: 1rem; } +.top-5-content.list-only { + grid-template-columns: 1fr; +} + .top-5-list { display: flex; flex-direction: column; @@ -153,7 +157,6 @@ scrollbar-width: none; } -/* Lays out the duplicated reviews exactly like the originals, so the loop is seamless. */ .review-loop-copy { display: flex; flex-direction: column; diff --git a/T04-Project-2/frontend/src/components/Top5Games.tsx b/T04-Project-2/frontend/src/components/Top5Games.tsx index 0d3d806..4c61efd 100644 --- a/T04-Project-2/frontend/src/components/Top5Games.tsx +++ b/T04-Project-2/frontend/src/components/Top5Games.tsx @@ -3,25 +3,21 @@ import './Top5Games.css' import { SectionHeading } from './SectionHeading' import { StarIcon, CalendarBlankIcon } from '@phosphor-icons/react' import logo from '../assets/GamingGoatLogo.webp' -import { ReviewCard } from './ReviewCard' -import { LOOP_COPY_ATTRIBUTE, useAutoScroll } from './useAutoScroll' +import { ReviewPanel } from './ReviewPanel' +import { useMediaQuery } from './useMediaQuery' type Top5GamesProps = { gameIds?: readonly string[]; } -const mockReviews = [ - { id: 'review-1', username: 'GoatGuy4', rating: 5, text: "This game changed my life! I feel like it's not worth spending time IRL any more. Amazing graphics, amazing story and most of all, AMAZING GOATS! A clear 5 out of 5 and an absolute no-brainer" }, - { id: 'review-2', username: 'KingGoatz', rating: 4.5, text: 'Epic game' }, - { id: 'review-3', username: 'monsterfighter0', rating: 4.5, text: 'Definitely the most realisitc goat simulation available right now.' }, -] - export const Top5Games = ({ gameIds }: Top5GamesProps) => { - const { ref: scrollRef, isLooping } = useAutoScroll() + // Matches the 900px breakpoint in index.css. On narrow screens the review panel + // isn't rendered at all, so its autoscroll loop doesn't run either. + const isWide = useMediaQuery('(min-width: 900px)') return (
    -
    +
      {top5MockGames.map((game, index) => (
    1. @@ -45,38 +41,7 @@ export const Top5Games = ({ gameIds }: Top5GamesProps) => {
    2. ))}
    -
    - {/* TODO: Implement logic for fetching a review to display here */} -

    Community reviews

    -
    -

    Goat Simulator Delux

    -

    - - 4.7 -

    -
    -

    What the herd thinks

    -
    -
    - {mockReviews.map((r) => )} - - {isLooping && [1, 2].map((copy) => ( - - ))} -
    -
    -
    + {isWide && }
    ) diff --git a/T04-Project-2/frontend/src/components/useMediaQuery.ts b/T04-Project-2/frontend/src/components/useMediaQuery.ts new file mode 100644 index 0000000..7d170de --- /dev/null +++ b/T04-Project-2/frontend/src/components/useMediaQuery.ts @@ -0,0 +1,12 @@ +import { useSyncExternalStore } from "react"; + +export function useMediaQuery(query: string): boolean { + return useSyncExternalStore( + (onChange) => { + const media = window.matchMedia(query); + media.addEventListener("change", onChange); + return () => media.removeEventListener("change", onChange); + }, + () => window.matchMedia(query).matches, + ); +} From 458c3ffcab59cdd277c4702d26995db68889b403 Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Mon, 9 Nov 2026 19:52:41 +0100 Subject: [PATCH 09/10] Made top 5 game cards change grid layout on narrow screens. (#77) --- T04-Project-2/frontend/src/components/Top5Games.css | 11 ++++++++++- T04-Project-2/frontend/src/components/Top5Games.tsx | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/T04-Project-2/frontend/src/components/Top5Games.css b/T04-Project-2/frontend/src/components/Top5Games.css index 88774b1..0a4e3cf 100644 --- a/T04-Project-2/frontend/src/components/Top5Games.css +++ b/T04-Project-2/frontend/src/components/Top5Games.css @@ -153,7 +153,6 @@ flex-direction: column; gap: 1rem; - /* scrollbar-color: var(--primary-color) rgba(0, 0, 0, 0); */ scrollbar-width: none; } @@ -173,3 +172,13 @@ font-size: clamp(0.6rem, 1.5vw, 0.8rem); margin-top: 0.3rem; } + +@media (max-width: 768px) { + .top-5-item { + grid-template-columns: 1fr; + gap: 1rem; + } + .top-5-list { + gap: 1rem; + } +} diff --git a/T04-Project-2/frontend/src/components/Top5Games.tsx b/T04-Project-2/frontend/src/components/Top5Games.tsx index 4c61efd..61a8c9c 100644 --- a/T04-Project-2/frontend/src/components/Top5Games.tsx +++ b/T04-Project-2/frontend/src/components/Top5Games.tsx @@ -29,7 +29,7 @@ export const Top5Games = ({ gameIds }: Top5GamesProps) => {

    {game.name}

    - + {game.released ? `Released ${game.released}` : 'Release date unknown'}

    From ba4663e5c376e4e1555c7e13e4190b71a429dcf9 Mon Sep 17 00:00:00 2001 From: Johannes Bergslid Date: Tue, 10 Nov 2026 12:11:55 +0100 Subject: [PATCH 10/10] Removed ratings from the reviews in review card (#77) --- T04-Project-2/frontend/src/components/ReviewCard.tsx | 5 +---- T04-Project-2/frontend/src/components/Top5Games.css | 8 +++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/T04-Project-2/frontend/src/components/ReviewCard.tsx b/T04-Project-2/frontend/src/components/ReviewCard.tsx index 2c186d3..48fac37 100644 --- a/T04-Project-2/frontend/src/components/ReviewCard.tsx +++ b/T04-Project-2/frontend/src/components/ReviewCard.tsx @@ -1,17 +1,14 @@ -import { StarDisplay } from "./StarDisplay" type ReviewCardProps = { username: string - rating: number text: string } -export const ReviewCard = ( {username, rating, text}: ReviewCardProps) => { +export const ReviewCard = ( {username, text}: ReviewCardProps) => { return (

    {username}

    -

    {text}

    ) diff --git a/T04-Project-2/frontend/src/components/Top5Games.css b/T04-Project-2/frontend/src/components/Top5Games.css index 0a4e3cf..6b7289e 100644 --- a/T04-Project-2/frontend/src/components/Top5Games.css +++ b/T04-Project-2/frontend/src/components/Top5Games.css @@ -176,9 +176,15 @@ @media (max-width: 768px) { .top-5-item { grid-template-columns: 1fr; - gap: 1rem; + gap: 0.5rem; + } + .name-release { + gap: 0; } .top-5-list { gap: 1rem; } + .top-5-rating { + align-items:start + } }