Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
8c9ff6c
feat: remove from wishlist at wishlistpage
carmennt Oct 6, 2026
0682b1e
fix: back navigate and css
carmennt Oct 6, 2026
bc24783
fix: removing back button
carmennt Oct 6, 2026
eeb4118
fix: removing unused imports
carmennt Oct 6, 2026
69fe2a1
chore: removing unused imports
carmennt Oct 6, 2026
f7f0aa0
feat: place for wishlist backend functions
carmennt Oct 6, 2026
a6a7c31
feat: remove from wishlist at wishlistpage
carmennt Oct 6, 2026
7e83b60
fix: back navigate and css
carmennt Oct 6, 2026
adea1df
fix: removing unused imports
carmennt Oct 6, 2026
32e0898
chore: removing unused imports
carmennt Oct 6, 2026
1037a5e
feat: place for wishlist backend functions
carmennt Oct 6, 2026
fcc83dd
Merge branch '33-implement-wishlist-functionality' of https://git.ntn…
carmennt Oct 9, 2026
0441942
fix: removed unused import
carmennt Oct 9, 2026
ae8c1ac
feat: remove from wishlist at wishlistpage
carmennt Oct 6, 2026
94872f9
fix: back navigate and css
carmennt Oct 6, 2026
632cf66
fix: removing unused imports
carmennt Oct 6, 2026
20212ee
chore: removing unused imports
carmennt Oct 6, 2026
5af1da2
feat: place for wishlist backend functions
carmennt Oct 6, 2026
748cc59
fix: back navigate and css
carmennt Oct 6, 2026
b2e3de3
fix: removing unused imports
carmennt Oct 6, 2026
dd18791
chore: removing unused imports
carmennt Oct 6, 2026
7fd2326
fix: removed unused import
carmennt Oct 9, 2026
872c283
Merge branch '33-implement-wishlist-functionality' of https://git.ntn…
carmennt Oct 12, 2026
5a505a6
feat: working on wishlist functionality
carmennt Oct 12, 2026
0928ae0
fix: comments in wishlistpage.tsx
carmennt Oct 13, 2026
0287346
feat: backend logic for wishlist functionality
carmennt Oct 13, 2026
11b6cec
chore: revert gitignore
Oct 14, 2026
4c85a63
feat: add optional rank parameter to passivegamecard
Oct 15, 2026
d6620f5
feat: add sections and carousels to mainpage
Oct 15, 2026
b03b69a
feat: fetching games for wishlist
carmennt Oct 15, 2026
3516b13
test: Tests for backend and fronted for wishlist
carmennt Oct 15, 2026
735f273
feat: remove from wishlist at wishlistpage
carmennt Oct 6, 2026
c5f223c
fix: back navigate and css
carmennt Oct 6, 2026
ed06d02
fix: removing unused imports
carmennt Oct 6, 2026
858bd9b
chore: removing unused imports
carmennt Oct 6, 2026
502fde4
feat: place for wishlist backend functions
carmennt Oct 6, 2026
74bb50b
fix: back navigate and css
carmennt Oct 6, 2026
c16910b
fix: removing unused imports
carmennt Oct 6, 2026
6a4f27d
chore: removing unused imports
carmennt Oct 6, 2026
0f5e496
fix: removed unused import
carmennt Oct 9, 2026
202e32b
fix: back navigate and css
carmennt Oct 6, 2026
6fd7523
fix: removing unused imports
carmennt Oct 6, 2026
567b90e
chore: removing unused imports
carmennt Oct 6, 2026
b7ebd91
fix: back navigate and css
carmennt Oct 6, 2026
c37c8ab
fix: removing unused imports
carmennt Oct 6, 2026
3bf50fd
chore: removing unused imports
carmennt Oct 6, 2026
d1a5f87
feat: working on wishlist functionality
carmennt Oct 12, 2026
9feb938
fix: comments in wishlistpage.tsx
carmennt Oct 13, 2026
88ade27
feat: backend logic for wishlist functionality
carmennt Oct 13, 2026
a5577ba
feat: fetching games for wishlist
carmennt Oct 15, 2026
c88ad5a
test: Tests for backend and fronted for wishlist
carmennt Oct 15, 2026
97e1e30
Merge branch '33-implement-wishlist-functionality' of https://git.ntn…
carmennt Oct 15, 2026
7c9f8ff
feat: wishlist button on active game card
carmennt Oct 15, 2026
5e81791
test: add tests for mainpage
Oct 16, 2026
ac03351
Merge branch 'main' of https://git.ntnu.no/IT2810-H26/T10-Project-2 i…
Oct 16, 2026
37150bd
fix: wishlist diplay works
carmennt Oct 16, 2026
8dc982d
feat: remove from wishlist at wishlistpage
carmennt Oct 6, 2026
9dcdafd
fix: back navigate and css
carmennt Oct 6, 2026
6b3ea93
fix: removing unused imports
carmennt Oct 6, 2026
3d0850e
chore: removing unused imports
carmennt Oct 6, 2026
feff402
feat: place for wishlist backend functions
carmennt Oct 6, 2026
e8ab87e
fix: back navigate and css
carmennt Oct 6, 2026
13884fb
fix: removing unused imports
carmennt Oct 6, 2026
0b2072e
chore: removing unused imports
carmennt Oct 6, 2026
431018e
fix: removed unused import
carmennt Oct 9, 2026
2294f8e
fix: back navigate and css
carmennt Oct 6, 2026
b700f38
fix: removing unused imports
carmennt Oct 6, 2026
4a48fd9
chore: removing unused imports
carmennt Oct 6, 2026
25b22a7
fix: back navigate and css
carmennt Oct 6, 2026
86c1c1b
fix: removing unused imports
carmennt Oct 6, 2026
3fadf20
chore: removing unused imports
carmennt Oct 6, 2026
e7f4f12
feat: working on wishlist functionality
carmennt Oct 12, 2026
26730c4
fix: comments in wishlistpage.tsx
carmennt Oct 13, 2026
2fc1368
feat: backend logic for wishlist functionality
carmennt Oct 13, 2026
a50555d
feat: fetching games for wishlist
carmennt Oct 15, 2026
cf4d6ef
test: Tests for backend and fronted for wishlist
carmennt Oct 15, 2026
b9d9d81
feat: wishlist button on active game card
carmennt Oct 15, 2026
96164d6
fix: wishlist diplay works
carmennt Oct 16, 2026
fe003ea
Merge branch '33-implement-wishlist-functionality' of https://git.ntn…
carmennt Oct 16, 2026
dcc3e51
fix: final wishlist functionality
carmennt Oct 16, 2026
a40796d
fix: wishlistGameCard now links to activeCard
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
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.env

# Coverage
Coverage/

#Env file
.env
.env
72 changes: 72 additions & 0 deletions src/api/wishlistLogicFrontend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { type Game } from '../backend/steam'

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;
}

// Frontend function for calling backend to fetch wishlisted games of a user
export async function fetchWishlistFrontend(userId: number | null): Promise<Game[]> {
const data = await gql<{ wishlist: Game[] }>(
`query($userId: Int!) {
wishlist(userId: $userId) {
appid name price initialprice discount img genre description
}
}`,
{ userId }
);
return data.wishlist;
}

// Frontend function for calling backend to add wishlisted game to a user
export async function addToWishlistFrontend(userId: number, gameId: number): Promise<void> {
await gql(
`mutation($userId: Int!, $gameId: Int!) { addToWishlist(userId: $userId, gameId: $gameId) }`,
{ userId, gameId }
);
}

// Frontend function for calling backend to remove wishlisted game from a user
export async function removeFromWishlistFrontend(userId: number, gameId: number): Promise<void> {
await gql(
`mutation($userId: Int!, $gameId: Int!) { removeFromWishlist(userId: $userId, gameId: $gameId) }`,
{ userId, gameId }
);
}

// Frontend function for toggeling a game to and from the wishlist
export function toggleGameWishlist(
userId: number,
gameId: number,
isWishlisted: boolean
): Promise<void> {
return isWishlisted
? removeFromWishlistFrontend(userId, gameId)
: addToWishlistFrontend(userId, gameId)
}

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

export function getSortOptionWishlist(): string {
try {
const parsed = JSON.parse(sessionStorage.getItem("GameStart:sortOptionWishlist") ?? '""');
return typeof parsed === "string" ? parsed : "";
} catch {
return "discount-desc";
}
}
49 changes: 38 additions & 11 deletions src/backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@ import { type Game } from './steam.ts'
import { pool } from './db_connection.ts'
import { fetchGenres } from './genres.ts'
import { signin, signup } from './userlogic_backend.ts'
import { addToWishlist, removeFromWishlist } from './wishlistLogic.ts'

// Documentation on apollo: https://www.apollographql.com/docs/deploy-preview/5dc6222f824f9e089e2a5735/apollo-server/api/apollo-server

// Shared column list so `games` and `wishlist` return the same Game shape
const GAME_COLUMNS = `
g.game_id AS appid,
g.game_name AS name,
g.current_price::text AS price,
g.initial_price::text AS initialprice,
g.discount_percent::text AS discount,
g.image_url AS img,
g.genre,
g.description`

//This is a schema for the server. It tells the shape of the data and which queries exist.
const typeDefs = `#graphql
type Game {
Expand All @@ -28,11 +40,14 @@ const typeDefs = `#graphql
type Query {
games(limit: Int = 500, offset: Int = 0): [Game!]!
wishlist(userId: Int!): [Game!]!
game(appid: Int!): Game
genres: [String!]!
}
type Mutation {
addToWishlist(userId: Int!, gameId: Int!): Boolean!
removeFromWishlist(userId: Int!, gameId: Int!): Boolean!
signup(username: String!, email: String!, password: String!): User!
signin(email: String!, password: String!): User!
}
Expand All @@ -48,21 +63,25 @@ const resolvers = {
const safeLimit = Math.min(Math.max(limit, 0), 500)
const safeOffset = Math.max(offset, 0)
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
ORDER BY game_id
`SELECT ${GAME_COLUMNS}
FROM games g
ORDER BY g.game_id
LIMIT $1 OFFSET $2`,
[safeLimit, safeOffset],
)
return rows
},
wishlist: async (_: unknown, { userId }: { userId: number }): Promise<Game[]> => {
const { rows } = await pool.query(
`SELECT ${GAME_COLUMNS}
FROM wishlist w
JOIN games g ON g.game_id = w.game_id
WHERE w.user_id = $1
ORDER BY g.game_name`,
[userId]
)
return rows
},
game: async (_parent: unknown, { appid }: { appid: number }): Promise<Game | null> => {
const { rows } = await pool.query(
`SELECT game_id AS appid,
Expand All @@ -82,6 +101,14 @@ const resolvers = {
genres: () => fetchGenres(),
},
Mutation: {
addToWishlist: async (_: unknown, { userId, gameId }: { userId: number; gameId: number }) => {
await addToWishlist(userId, gameId)
return true
},
removeFromWishlist: async (_: unknown, { userId, gameId }: { userId: number; gameId: number }) => {
await removeFromWishlist(userId, gameId)
return true
},
signup: (_parent: unknown, { username, email, password }: { username: string; email:string; password: string }) =>
signup(username, email, password),
signin: (_p: unknown, { email, password }: { email: string; password: string }) =>
Expand Down
31 changes: 31 additions & 0 deletions src/backend/wishlistLogic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { pool } from './db_connection.ts'

export async function addToWishlist(userId: number, gameId: number): Promise<{ ok: true; gameId: number }> {
if (!userId) throw new Error("addToWishlist: userId can not be empty")
if (!gameId) throw new Error("addToWishlist: gameId can not be empty")

try {
await pool.query(
`INSERT INTO wishlist (user_id, game_id)
VALUES ($1, $2)
ON CONFLICT (user_id, game_id) DO NOTHING`,
[userId, gameId]
)
} catch (err: any) {
// 23503 = foreign key violation (user or game doesn't exist)
if (err.code === '23503') throw new Error("addToWishlist: user or game does not exist")
throw err
}
return { ok: true, gameId }
}

export async function removeFromWishlist(userId: number, gameId: number): Promise<{ ok: true; gameId: number }> {
if (!userId) throw new Error("removeFromWishlist: userId can not be empty")
if (!gameId) throw new Error("removeFromWishlist: gameId can not be empty")

await pool.query(
'DELETE FROM wishlist WHERE user_id = $1 AND game_id = $2',
[userId, gameId]
)
return { ok: true, gameId }
}
5 changes: 0 additions & 5 deletions src/components/WishlistCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@ export const WishlistCard = styled(MuiCard)(({ theme }) => ({
boxSizing: 'border-box',
padding: theme.spacing(2),
gap: theme.spacing(2),
// margin: 'auto',
boxShadow:
'0px 5px 15px 0px var(--box-shadow-color), 0px 15px 35px -5px var(--box-shadow-color)',
[theme.breakpoints.up('sm')]: {
padding: theme.spacing(4),
},
...theme.applyStyles('dark', {
boxShadow:
'0px 5px 15px 0px var(--box-shadow-color), 0px 15px 35px -5px var(--box-shadow-color)',
}),
}))
82 changes: 71 additions & 11 deletions src/components/activegamecard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,80 @@
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import CardMedia from '@mui/material/CardMedia'
import Chip from '@mui/material/Chip'
import Divider from '@mui/material/Divider'
import Typography from '@mui/material/Typography'
import { Box, Button, Card, CardContent, CardMedia, Chip, Divider, Typography } from '@mui/material'
import '../css/activegamecard.css'
import type { GameData } from '../types/game'
import { getSavedUser } from '../api/userlogic_frontend'
import { useState, useEffect } from 'react'
import { removeFromWishlistFrontend,
addToWishlistFrontend,
fetchWishlistFrontend }
from '../api/wishlistLogicFrontend'

type ActiveGameCardProps = {
game: GameData
onRemoveFromWishlist?: (gameId: number) => void
onAddToWishlist?: (gameId: number) => void
}

export default function ActiveGameCard({ game, onAddToWishlist }: ActiveGameCardProps) {
export default function ActiveGameCard({ game, onAddToWishlist, onRemoveFromWishlist }: ActiveGameCardProps) {
const [isWishlisted, setIsWishlisted] = useState(false)
const [isChecking, setIsChecking] = useState(true)
const [isBusy, setIsBusy] = useState(false)
const [error, setError] = useState<string | null>(null)

const isOnSale =
game.salePercent !== undefined && game.salePercent > 0 && game.salePercent <= 100

// Finds out if the signed in user already has this game on their wishlist
useEffect(() => {
let cancelled = false
const user = getSavedUser()

if (!user) {
setIsChecking(false)
return
}

fetchWishlistFrontend(user.id)
.then((wishlist) => {
if (!cancelled) setIsWishlisted(wishlist.some((wishlisted) => wishlisted.appid === game.id))
})
.catch(() => {
if (!cancelled) setError('Could not check your wishlist')
})
.finally(() => {
if (!cancelled) setIsChecking(false)
})

return () => {
cancelled = true
}
}, [game.id])

// Adds the game to the wishlist, or removes it if it is already there
async function handleToggleWishlist() {
const user = getSavedUser()
if (!user) {
setError('You need to sign in to change your wishlist')
return
}

setIsBusy(true)
setError(null)
try {
if (isWishlisted) {
await removeFromWishlistFrontend(user.id, game.id)
setIsWishlisted(false)
onRemoveFromWishlist?.(game.id)
} else {
await addToWishlistFrontend(user.id, game.id)
setIsWishlisted(true)
onAddToWishlist?.(game.id)
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Could not update wishlist')
} finally {
setIsBusy(false)
}
}

return (
<Box component="main" className="activeGameLayout">
Expand All @@ -30,10 +88,12 @@ export default function ActiveGameCard({ game, onAddToWishlist }: ActiveGameCard
<Button
className="activeGameWishlistButton"
variant="contained"
onClick={() => onAddToWishlist?.(game.id)}
disabled={isChecking || isBusy}
onClick={handleToggleWishlist}
>
Add to wishlist
{isWishlisted ? 'Remove from wishlist' : 'Add to wishlist'}
</Button>
{error && <p role="alert">{error}</p>}
</Card>
<Box className="activeGameDetails">
<Card className="activeGameInfoCard">
Expand Down
12 changes: 9 additions & 3 deletions src/components/passivegamecard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ import type { PassiveGameData } from '../types/game'

type Props = {
game: PassiveGameData
rank?: number
}

export default function PassiveGameCard({ game }: Props) {
export default function PassiveGameCard({ game, rank }: Props) {
const isOnSale =
game.salePercent !== undefined && game.salePercent > 0 && game.salePercent <= 100

return (
<Card className="passiveGameCard">
<Card className={`passiveGameCard${rank === undefined ? '' : ' rankedPassiveGameCard'}`}>
{rank !== undefined && (
<span className="passiveGameCardRank" aria-label={`Rank ${rank}`}>
#{rank}
</span>
)}
<CardActionArea
className="passiveGameCardAction"
component={Link}
Expand All @@ -37,7 +43,7 @@ export default function PassiveGameCard({ game }: Props) {
<div className="GamePrice">
<div className="GameSale">{isOnSale ? `-${game.salePercent}%` : ''}</div>
<Typography className="passiveGameCardPrice" component="p">
{game.price.toFixed(2)}
{game.price.toFixed(2)} $
</Typography>
</div>
</CardContent>
Expand Down
Loading