Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f395b20
feat: sorting and filter by genre
carmennt Oct 9, 2026
2dfdc76
fix: bug in selecting sorting
carmennt Oct 9, 2026
132ac84
style: fixing background colour of filteroptions
carmennt Oct 9, 2026
03ecf23
fix: Sorting hover color
carmennt Oct 9, 2026
8138e41
styling: navbar icon color change
carmennt Oct 9, 2026
415577a
styling: Styling of searchbar
carmennt Oct 9, 2026
35d8068
feat: sorting and filter by genre
carmennt Oct 9, 2026
5535e86
fix: bug in selecting sorting
carmennt Oct 9, 2026
492ad38
style: fixing background colour of filteroptions
carmennt Oct 9, 2026
7d56852
fix: Sorting hover color
carmennt Oct 9, 2026
7dea36f
styling: navbar icon color change
carmennt Oct 9, 2026
674735b
styling: Styling of searchbar
carmennt Oct 9, 2026
6d5848e
Merge branch '40-update-search-functionality' of https://git.ntnu.no/…
carmennt Oct 13, 2026
940139b
fix: rea-dding pagination
carmennt Oct 13, 2026
e6c4a26
feat: Sorting works!
carmennt Oct 13, 2026
22271f0
feat: Searching functionality
carmennt Oct 14, 2026
5c5a53e
feat: gamepage fetches from database
elinejaa Oct 14, 2026
c46f84d
feat: gamepage loads individual game fra database
elinejaa Oct 14, 2026
713968d
test: make test for gameclient update and gamepage
elinejaa Oct 14, 2026
93b6870
fix: handle failing tests
elinejaa Oct 14, 2026
b45c259
feat: filter functionality on the way
carmennt Oct 14, 2026
f0d70bb
Merge branch '40-update-search-functionality' into 49-update-gamepage
carmennt Oct 14, 2026
40d849e
Merge pull request #50 from IT2810-H26/49-update-gamepage
carmennt Oct 14, 2026
c873880
fix: line spacing
carmennt Oct 14, 2026
8b90d3f
fix: missing } in server.ts
carmennt Oct 14, 2026
ffc3609
fix: fix port
carmennt Oct 14, 2026
8c8ab4e
feat: fetchGenres()
carmennt Oct 14, 2026
2a5748e
fix: session storage of searching functionality
carmennt Oct 14, 2026
0a1bd03
fix: Fixing the code
carmennt Oct 14, 2026
f3fae46
fix: silent errors
carmennt Oct 15, 2026
f3b2b8f
test: tests for searchpage
carmennt Oct 15, 2026
58314e3
test: backend test for genre function
carmennt Oct 15, 2026
d0653c9
Merge branch 'main' into 40-update-search-functionality
carmennt Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,057 changes: 1,013 additions & 44 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"@vitest/coverage-v8": "^5.0.2",
"@vitest/ui": "^5.0.3",
"eslint": "^10.10.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
Expand Down
71 changes: 71 additions & 0 deletions src/api/gameClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ function gamesCacheKey(limit: number, offset: number): string {
return `games:v1:${limit}:${offset}`
}

function gameByIdCacheKey(appid: number): string {
return `game:v1:${appid}`
}

function readCachedGames(key: string): Game[] | null {
try {
const rawCache = globalThis.localStorage?.getItem(key)
Expand Down Expand Up @@ -74,6 +78,73 @@ function writeCachedGames(key: string, games: Game[]): void {
}
}

function findCachedGameById(appid: number): Game | null {
const cachedGame = readCachedGames(gameByIdCacheKey(appid))?.find(
(game) => game.appid === appid,
)
if (cachedGame) return cachedGame

try {
const storage = globalThis.localStorage
if (!storage) return null

// Collect keys first because readCachedGames may remove expired entries.
const cacheKeys = Array.from({ length: storage.length }, (_, index) =>
storage.key(index),
).filter((key): key is string => key !== null && key.startsWith('games:v1:'))

for (const key of cacheKeys) {
const game = readCachedGames(key)?.find((candidate) => candidate.appid === appid)
if (game) return game
}
} catch (error) {
console.warn('Unable to search localStorage; fetching game from the database.', error)
}

return null
}

export async function getGameById(appid: number): Promise<Game | null> {
const cachedGame = findCachedGameById(appid)
if (cachedGame) return cachedGame

const response = await fetch('http://it2810-10.idi.ntnu.no:3001/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query Game($appid: Int!) {
game(appid: $appid) {
appid
name
price
initialprice
discount
img
genre
description
}
}
`,
variables: { appid },
}),
})

if (!response.ok) {
const body = await response.text()
throw new Error(`Failed to fetch game: ${response.status} ${body}`)
}

const { data, errors } = (await response.json()) as GraphQLResponse<{
game: Game | null
}>
if (errors?.length) throw new Error(errors[0].message)
if (!data) throw new Error('No data returned')

if (data.game) writeCachedGames(gameByIdCacheKey(appid), [data.game])
return data.game
}

async function fetchGamesFromDB(limit: number, offset: number): Promise<Game[]> {
const cacheKey = gamesCacheKey(limit, offset)
const cachedGames = readCachedGames(cacheKey)
Expand Down
87 changes: 87 additions & 0 deletions src/api/searchpageLogicFrontend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const backendport = 'http://it2810-10.idi.ntnu.no:3001/'

async function gql<T>(query: string, variables: Record<string, unknown>): Promise<T> {
const res = await fetch(backendport, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const json = await res.json();
if (json.errors) throw new Error(json.errors[0].message);
return json.data;
}

export async function fetchGenres(): Promise<string[]> {
const data = await gql<{ genres: string[] }>(`query { genres }`, {});
return data.genres;
}

export function getSavedFiltersSearchpage(): string[] {
try {
const parsed = JSON.parse(sessionStorage.getItem("GameStart:filtersSearchpage") ?? "[]");
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}

export function saveFiltersSearchpage(filters: string[]) {
try {
sessionStorage.setItem("GameStart:filtersSearchpage", JSON.stringify(filters));
} catch (error: any) {
console.log(`Searchpage: Could not save filters to session storage: ${error.message}`)
}
}


export function getQuerySearchpage(): string {
try {
const parsed = JSON.parse(sessionStorage.getItem("GameStart:querySearchpage") ?? '""');
return typeof parsed === "string" ? parsed : "";
} catch {
return "";
}
}

export function saveQuerySearchpage(query: string) {
try {
sessionStorage.setItem("GameStart:querySearchpage", JSON.stringify(query));
} catch (error: any) {
console.log(`Searchpage: Could not save search query to session storage: ${error.message}`)
}
}

export function saveSortOptionSearchpage(query: string) {
try {
sessionStorage.setItem("GameStart:sortOptionSearchpage", JSON.stringify(query));
} catch (error: any) {
console.log(`Searchpage: Could not save sorting option to session storage: ${error.message}`)
}
}

export function getSortOptionSearchpage(): string {
try {
const parsed = JSON.parse(sessionStorage.getItem("GameStart:sortOptionSearchpage") ?? '""');
return typeof parsed === "string" ? parsed : "";
} catch {
return "";
}
}

export function savePageSearchpage(pageNumber: number) {
try {
sessionStorage.setItem("GameStart:pageSearchpage", JSON.stringify(pageNumber));
} catch (error: any) {
console.log(`Searchpage: Could not save page to session storage: ${error.message}`)
}
}

export function getPageSearchpage(): number {
try {
const parsed = JSON.parse(sessionStorage.getItem("GameStart:pageSearchpage") ?? "1");
return Number.isInteger(parsed) && parsed >= 1 ? parsed : 1;
} catch {
return 1;
}
}
11 changes: 11 additions & 0 deletions src/backend/genres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { pool } from './db_connection.ts'

export async function fetchGenres(): Promise<string[]> {
const { rows } = await pool.query(
`SELECT DISTINCT TRIM(g) AS genre
FROM games, unnest(string_to_array(genre, ',')) AS g
WHERE genre IS NOT NULL AND TRIM(g) <> ''
ORDER BY genre`
)
return rows.map((r) => r.genre)
}
27 changes: 25 additions & 2 deletions src/backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { type Game } from './steam.ts'
import { pool } from './db_connection.ts'
import { fetchGenres } from './genres.ts'
import { signin, signup } from './userlogic_backend.ts'

// Documentation on apollo: https://www.apollographql.com/docs/deploy-preview/5dc6222f824f9e089e2a5735/apollo-server/api/apollo-server
Expand All @@ -27,6 +28,8 @@ const typeDefs = `#graphql
type Query {
games(limit: Int = 500, offset: Int = 0): [Game!]!
game(appid: Int!): Game
genres: [String!]!
}
type Mutation {
Expand All @@ -37,7 +40,10 @@ const typeDefs = `#graphql

const resolvers = {
Query: {
games: async (_parent: unknown, { limit, offset }: { limit: number; offset: number }): Promise<Game[]> => {
games: async (
_parent: unknown,
{ limit, offset }: { limit: number; offset: number },
): Promise<Game[]> => {
// This prevents the limit from being too high, and the offset from being negative.
const safeLimit = Math.min(Math.max(limit, 0), 500)
const safeOffset = Math.max(offset, 0)
Expand All @@ -53,10 +59,27 @@ const resolvers = {
FROM games
ORDER BY game_id
LIMIT $1 OFFSET $2`,
[safeLimit, safeOffset]
[safeLimit, safeOffset],
)
return rows
},
game: async (_parent: unknown, { appid }: { appid: number }): Promise<Game | null> => {
const { rows } = await pool.query(
`SELECT game_id AS appid,
game_name AS name,
current_price::text AS price,
initial_price::text AS initialprice,
discount_percent::text AS discount,
image_url AS img,
genre,
description
FROM games
WHERE game_id = $1`,
[appid],
)
return rows[0] ?? null
},
genres: () => fetchGenres(),
},
Mutation: {
signup: (_parent: unknown, { username, email, password }: { username: string; email:string; password: string }) =>
Expand Down
2 changes: 1 addition & 1 deletion src/css/header.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ header {
}

#navigationMenue a.is-active {
color: var(--primary-component-color);
color: var(--primary-accent-color);
}

header > button {
Expand Down
93 changes: 52 additions & 41 deletions src/css/searchpage.css
Original file line number Diff line number Diff line change
@@ -1,78 +1,84 @@
.searchinput.MuiTextField-root {
background-color: var(--secondary-component-color);
border-color: var(--primary-accent-color);
.searchbar.MuiPaper-root {
display: flex;
align-items: center;
align-self: center;
width: min(30rem, calc(100% - 5rem));
margin: 0;
padding: 0;
background-color: var(--secondary-component-color);
border: 1px solid var(--primary-component-color);
font-family: var(--primary-font);
}

.searchinput .MuiOutlinedInput-root {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.searchinput.MuiInputBase-root {
flex: 1;
margin-left: 0.5rem;
}

.searchinput .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline,
.searchinput .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline,
.searchinput .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {
border-color: var(--primary-accent-color);
.searchConfirm.MuiIconButton-root,
.filterbutton.MuiIconButton-root {
color: var(--primary-text-color);
}

.searchbar {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
.searchConfirm.MuiIconButton-root {
padding: 10px;
}

.filterbutton {
background-color: var(--primary-component-color);
border: 1px solid var(--primary-accent-color);
border-left: 0;
border-radius: 0 4px 4px 0;
.filterbutton.MuiIconButton-root {
height: 56px;
padding: 0 1rem;
border-left: 1px solid var(--primary-component-color);
border-radius: 0 4px 4px 0;
}

.filterbutton.MuiIconButton-root:hover,
.searchConfirm.MuiIconButton-root:hover {
background-color: transparent;
}

.filterbutton.MuiIconButton-root.is-active,
.filterbutton.MuiIconButton-root.is-active:hover,
.filterbutton.MuiIconButton-root:active {
background-color: var(--primary-component-color);
border-radius: 0;
}

.filterbutton.is-active,
.filterbutton:active {
.filterbutton.MuiIconButton-root .MuiTouchRipple-child,
.searchConfirm.MuiIconButton-root .MuiTouchRipple-child {
background-color: var(--primary-accent-color);
}

.filtersection {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
border: 1px;
align-self: center;
justify-content: center;
gap: 0.5rem;
border-top: 0;
border-color: var(--primary-accent-color);
border-style: solid;
width: min(30rem, calc(100% - 5rem));
padding: 1rem;
}

.filtersection .MuiButtonGroup-root {
gap: 0.5rem;
}

.filtersection .MuiButtonGroup-root .MuiButtonGroup-grouped {
border: 1px solid var(--primary-accent-color);
border-radius: 4px;
border-top: 0;
}

.filtersection .MuiButtonGroup-root .MuiButtonGroup-grouped:not(:last-of-type) {
border-right: 1px solid var(--primary-accent-color);
.filtersection .MuiFormControl-root {
min-width: 140px;
}

.filtersection .MuiButton-root {
color: var(--primary-text-color);
@media (min-width: 600px) {
.filtersection .MuiFormControl-root {
min-width: 200px;
}
}

.filtersection .MuiButton-root.is-active {
.filterOption.MuiMenuItem-root.Mui-selected,
.sortingOption.MuiMenuItem-root.Mui-selected,
.sortingOption.MuiMenuItem-root.Mui-selected:hover {
background-color: var(--secondary-component-color);
color: var(--primary-accent-color);
}

.filterOption.MuiMenuItem-root.Mui-selected:hover,
.filterOption.MuiMenuItem-root.Mui-selected.Mui-focusVisible {
background-color: var(--primary-component-color);
}

.games-grid {
Expand All @@ -93,3 +99,8 @@
margin-top: 1.5rem;
align-self: center;
}

.filterOption.MuiMenuItem-root.Mui-selected:hover,
.filterOption.MuiMenuItem-root.Mui-selected.Mui-focusVisible {
background-color: var(--primary-component-color);
}
Loading