-
-
-
- t10-project-2
-
-
-
-
-
+
+
+
+
+ t10-project-2
+
+
+
+
+
diff --git a/package.json b/package.json
index 8481ee4..4527c35 100644
--- a/package.json
+++ b/package.json
@@ -1,56 +1,56 @@
{
- "name": "t10-project-2",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "lint": "eslint .",
- "preview": "vite preview",
- "format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"",
- "format:check": "prettier --check \"src/**/*.{ts,tsx,css,md}\"",
- "test": "vitest",
- "test:ui": "vitest --ui",
- "test:coverage": "vitest run --coverage",
- "server": "node src/backend/server.ts"
- },
- "dependencies": {
- "@apollo/server": "^5.5.1",
- "@emotion/react": "^11.14.0",
- "@emotion/styled": "^11.14.1",
- "@material/card": "^14.0.0",
- "@mui/material": "^9.4.0",
- "dotenv": "^18.0.7",
- "graphql": "^16.14.2",
- "lucide-react": "^1.48.0",
- "pg": "^8.23.1",
- "react": "^19.2.8",
- "react-dom": "^19.2.8",
- "react-router-dom": "^7.18.4"
- },
- "devDependencies": {
- "@eslint/js": "^10.0.1",
- "@testing-library/jest-dom": "^7.0.1",
- "@testing-library/react": "^16.3.3",
- "@testing-library/user-event": "^14.6.7",
- "@types/node": "^24.13.3",
- "@types/pg": "^8.23.1",
- "@types/react": "^19.2.18",
- "@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",
- "eslint-plugin-react-refresh": "^0.5.6",
- "globals": "^17.12.0",
- "jsdom": "^29.1.1",
- "prettier": "^3.9.8",
- "typescript": "~6.0.2",
- "typescript-eslint": "^8.69.0",
- "vite": "^8.3.0",
- "vitest": "^5.0.2"
- }
+ "name": "t10-project-2",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "format": "prettier --write \"src/**/*.{ts,tsx,css,md}\"",
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,css,md}\"",
+ "test": "vitest",
+ "test:ui": "vitest --ui",
+ "test:coverage": "vitest run --coverage",
+ "server": "node src/backend/Server.ts"
+ },
+ "dependencies": {
+ "@apollo/Server": "^5.5.1",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@material/card": "^14.0.0",
+ "@mui/material": "^9.4.0",
+ "dotenv": "^18.0.7",
+ "graphql": "^16.14.2",
+ "lucide-react": "^1.48.0",
+ "pg": "^8.23.1",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "react-router-dom": "^7.18.4"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@testing-library/jest-dom": "^7.0.1",
+ "@testing-library/react": "^16.3.3",
+ "@testing-library/user-event": "^14.6.7",
+ "@types/node": "^24.13.3",
+ "@types/pg": "^8.23.1",
+ "@types/react": "^19.2.18",
+ "@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",
+ "eslint-plugin-react-refresh": "^0.5.6",
+ "globals": "^17.12.0",
+ "jsdom": "^29.1.1",
+ "prettier": "^3.9.8",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.69.0",
+ "vite": "^8.3.0",
+ "vitest": "^5.0.2"
+ }
}
diff --git a/src/App.tsx b/src/App.tsx
index cbcbc14..b5f9821 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,24 +1,24 @@
import { HashRouter, Routes, Route } from 'react-router-dom'
-import MainPage from './pages/mainpage'
-import GamePage from './pages/gamepage'
-import { SearchPage } from './pages/searchpage'
-import SignUpPage from './pages/signup'
-import SignInPage from './pages/signin'
-import WishlistPage from './pages/wishlistpage'
-import ProfilePage from './pages/profilepage'
+import MainPage from './pages/MainPage'
+import GamePage from './pages/GamePage'
+import { SearchPage } from './pages/SearchPage'
+import SignUpPage from './pages/SignUp'
+import SignInPage from './pages/SignIn'
+import WishlistPage from './pages/WishlistPage'
+import ProfilePage from './pages/ProfilePage'
function App() {
return (
} />
- } />
+ } />
} />
- } />
- } />
- } />
- } />
- } />
+ } />
+ } />
+ } />
+ } />
+ } />
)
diff --git a/src/main.tsx b/src/Main.tsx
similarity index 77%
rename from src/main.tsx
rename to src/Main.tsx
index 4d0d7f8..d03c3ef 100644
--- a/src/main.tsx
+++ b/src/Main.tsx
@@ -1,8 +1,8 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
-import './css/global.css'
+import './css/Global.css'
import App from './App.tsx'
-import { AuthProvider } from './context/authcontext.tsx'
+import { AuthProvider } from './context/AuthContext.tsx'
createRoot(document.getElementById('root')!).render(
diff --git a/src/api/fill_db.ts b/src/api/FillDb.ts
similarity index 68%
rename from src/api/fill_db.ts
rename to src/api/FillDb.ts
index a1db2d3..381b56a 100644
--- a/src/api/fill_db.ts
+++ b/src/api/FillDb.ts
@@ -1,16 +1,16 @@
-import "dotenv/config"
-import { pool } from "../backend/db_connection"
-import { fetchGames } from "../backend/steam"
+import 'dotenv/config'
+import { pool } from '../backend/DbConnection'
+import { FetchGames } from '../backend/Steam'
const BATCH = 200
const TARGET = 2000
const START = 200 //changed based on wether it crashes or not
-async function fill_db() {
+async function FillDb() {
try {
for (let offset = START; offset < TARGET; offset += BATCH) {
- const games = await fetchGames(BATCH, offset)
- for(const g of games) {
+ const games = await FetchGames(BATCH, offset)
+ for (const g of games) {
await pool.query(
`INSERT INTO games (game_id, game_name, current_price, initial_price, discount_percent, image_url, genre, description)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
@@ -22,7 +22,16 @@ async function fill_db() {
image_url = EXCLUDED.image_url,
genre = EXCLUDED.genre,
description = EXCLUDED.description`,
- [g.appid, g.name, g.price, g.initialprice, g.discount, g.img, g.genre, g.description]
+ [
+ g.appid,
+ g.name,
+ g.price,
+ g.initialprice,
+ g.discount,
+ g.img,
+ g.genre,
+ g.description,
+ ],
)
}
console.log(`Inserted/updated ${games.length} games`)
@@ -35,4 +44,4 @@ async function fill_db() {
}
}
-fill_db()
\ No newline at end of file
+FillDb()
diff --git a/src/api/gameClient.ts b/src/api/GameClient.ts
similarity index 80%
rename from src/api/gameClient.ts
rename to src/api/GameClient.ts
index fec47f9..796c4b7 100644
--- a/src/api/gameClient.ts
+++ b/src/api/GameClient.ts
@@ -1,4 +1,4 @@
-import type { Game } from '../backend/steam'
+import type { Game } from '../backend/Steam'
interface GraphQLResponse {
data?: T
@@ -13,7 +13,7 @@ interface CachedGames {
games: Game[]
}
-function isGame(value: unknown): value is Game {
+function IsGame(value: unknown): value is Game {
if (typeof value !== 'object' || value === null) return false
const game = value as Record
@@ -29,33 +29,33 @@ function isGame(value: unknown): value is Game {
)
}
-function isCachedGames(value: unknown): value is CachedGames {
+function IsCachedGames(value: unknown): value is CachedGames {
if (typeof value !== 'object' || value === null) return false
const cache = value as Record
return (
typeof cache.fetchedAt === 'number' &&
Array.isArray(cache.games) &&
- cache.games.every(isGame)
+ cache.games.every(IsGame)
)
}
-function gamesCacheKey(limit: number, offset: number): string {
+function GamesCacheKey(limit: number, offset: number): string {
return `games:v1:${limit}:${offset}`
}
-function gameByIdCacheKey(appid: number): string {
+function GameByIdCacheKey(appid: number): string {
return `game:v1:${appid}`
}
-function readCachedGames(key: string): Game[] | null {
+function ReadCachedGames(key: string): Game[] | null {
try {
const rawCache = globalThis.localStorage?.getItem(key)
if (!rawCache) return null
const cache: unknown = JSON.parse(rawCache)
- const age = Date.now() - (isCachedGames(cache) ? cache.fetchedAt : 0)
- if (isCachedGames(cache) && age >= 0 && age < CACHE_TTL_MS) {
+ const age = Date.now() - (IsCachedGames(cache) ? cache.fetchedAt : 0)
+ if (IsCachedGames(cache) && age >= 0 && age < CACHE_TTL_MS) {
return cache.games
}
@@ -67,7 +67,7 @@ function readCachedGames(key: string): Game[] | null {
return null
}
-function writeCachedGames(key: string, games: Game[]): void {
+function WriteCachedGames(key: string, games: Game[]): void {
try {
globalThis.localStorage?.setItem(
key,
@@ -78,8 +78,8 @@ function writeCachedGames(key: string, games: Game[]): void {
}
}
-function findCachedGameById(appid: number): Game | null {
- const cachedGame = readCachedGames(gameByIdCacheKey(appid))?.find(
+function FindCachedGameById(appid: number): Game | null {
+ const cachedGame = ReadCachedGames(GameByIdCacheKey(appid))?.find(
(game) => game.appid === appid,
)
if (cachedGame) return cachedGame
@@ -94,7 +94,7 @@ function findCachedGameById(appid: number): Game | null {
).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)
+ const game = ReadCachedGames(key)?.find((candidate) => candidate.appid === appid)
if (game) return game
}
} catch (error) {
@@ -104,8 +104,8 @@ function findCachedGameById(appid: number): Game | null {
return null
}
-export async function getGameById(appid: number): Promise {
- const cachedGame = findCachedGameById(appid)
+export async function GetGameById(appid: number): Promise {
+ const cachedGame = FindCachedGameById(appid)
if (cachedGame) return cachedGame
const response = await fetch('http://it2810-10.idi.ntnu.no:3001/', {
@@ -141,13 +141,13 @@ export async function getGameById(appid: number): Promise {
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])
+ if (data.game) WriteCachedGames(GameByIdCacheKey(appid), [data.game])
return data.game
}
-async function fetchGamesFromDB(limit: number, offset: number): Promise {
- const cacheKey = gamesCacheKey(limit, offset)
- const cachedGames = readCachedGames(cacheKey)
+async function FetchGamesFromDB(limit: number, offset: number): Promise {
+ const cacheKey = GamesCacheKey(limit, offset)
+ const cachedGames = ReadCachedGames(cacheKey)
if (cachedGames) return cachedGames
const response = await fetch('http://it2810-10.idi.ntnu.no:3001/', {
@@ -183,31 +183,31 @@ async function fetchGamesFromDB(limit: number, offset: number): Promise
if (errors?.length) throw new Error(errors[0].message)
if (!data) throw new Error('No data returned')
- writeCachedGames(cacheKey, data.games)
+ WriteCachedGames(cacheKey, data.games)
return data.games
}
-export function getGames(): Promise {
- return fetchAllGames()
+export function GetGames(): Promise {
+ return FetchAllGames()
}
-async function fetchAllGames(): Promise {
+async function FetchAllGames(): Promise {
const games: Game[] = []
for (let offset = 0; ; offset += GAME_PAGE_SIZE) {
- const page = await fetchGamesFromDB(GAME_PAGE_SIZE, offset)
+ const page = await FetchGamesFromDB(GAME_PAGE_SIZE, offset)
games.push(...page)
if (page.length < GAME_PAGE_SIZE) return games
}
}
-export default fetchGamesFromDB
+export default FetchGamesFromDB
//This is an example of how to fetch information
// import { useEffect, useState } from 'react'
-// import fetchGamesFromDB from '../api/gameClient'
-// import type { Game } from '../backend/steam'
+// import fetchGamesFromDB from '../api/GameClient'
+// import type { Game } from '../backend/Steam'
//
// Before return:
// const [games, setGames] = useState([])
diff --git a/src/api/LogicConnection.ts b/src/api/LogicConnection.ts
new file mode 100644
index 0000000..c01e7bc
--- /dev/null
+++ b/src/api/LogicConnection.ts
@@ -0,0 +1,13 @@
+const BACKEND_URL = 'http://it2810-10.idi.ntnu.no:3001/'
+
+export async function Gql(query: string, variables: Record): Promise {
+ const res = await fetch(BACKEND_URL, {
+ 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
+}
diff --git a/src/api/mainpageLogicFrontend.ts b/src/api/MainPageLogicFrontend.ts
similarity index 72%
rename from src/api/mainpageLogicFrontend.ts
rename to src/api/MainPageLogicFrontend.ts
index 4d7fa33..62813a6 100644
--- a/src/api/mainpageLogicFrontend.ts
+++ b/src/api/MainPageLogicFrontend.ts
@@ -7,47 +7,47 @@ const mainPageGenreStorageKeys: Record = {
const discountedOnlyStorageKey = 'GameStart:discountedOnlySearchpage'
-function reportStorageError(action: string, error: unknown) {
+function ReportStorageError(action: string, error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(`MainPage: Could not ${action} in session storage: ${message}`)
}
-export function getSavedMainPageGenre(section: MainPageGenreSection): string {
+export function GetSavedMainPageGenre(section: MainPageGenreSection): string {
try {
const parsed = JSON.parse(sessionStorage.getItem(mainPageGenreStorageKeys[section]) ?? '""')
return typeof parsed === 'string' ? parsed : ''
} catch (error: unknown) {
- reportStorageError('read the saved genre', error)
+ ReportStorageError('read the saved genre', error)
return ''
}
}
-export function saveMainPageGenre(section: MainPageGenreSection, genre: string) {
+export function SaveMainPageGenre(section: MainPageGenreSection, genre: string) {
try {
sessionStorage.setItem(mainPageGenreStorageKeys[section], JSON.stringify(genre))
} catch (error: unknown) {
- reportStorageError('save the genre', error)
+ ReportStorageError('save the genre', error)
}
}
-export function getDiscountedOnlySearchpage(): boolean {
+export function GetDiscountedOnlySearchpage(): boolean {
try {
return JSON.parse(sessionStorage.getItem(discountedOnlyStorageKey) ?? 'false') === true
} catch (error: unknown) {
- reportStorageError('read the discounted-only filter', error)
+ ReportStorageError('read the discounted-only filter', error)
return false
}
}
-export function saveDiscountedOnlySearchpage(discountedOnly: boolean) {
+export function SaveDiscountedOnlySearchpage(discountedOnly: boolean) {
try {
sessionStorage.setItem(discountedOnlyStorageKey, JSON.stringify(discountedOnly))
} catch (error: unknown) {
- reportStorageError('save the discounted-only filter', error)
+ ReportStorageError('save the discounted-only filter', error)
}
}
-export function prepareDiscountGenreSearch(genre: string) {
+export function PrepareDiscountGenreSearch(genre: string) {
const searchPageValues: Record = {
'GameStart:filtersSearchpage': JSON.stringify(genre ? [genre] : []),
[discountedOnlyStorageKey]: JSON.stringify(true),
@@ -61,6 +61,6 @@ export function prepareDiscountGenreSearch(genre: string) {
sessionStorage.setItem(key, value)
}
} catch (error: unknown) {
- reportStorageError('prepare the discounted genre search', error)
+ ReportStorageError('prepare the discounted genre search', error)
}
}
diff --git a/src/api/SearchPageLogicFrontend.ts b/src/api/SearchPageLogicFrontend.ts
new file mode 100644
index 0000000..fa5ec56
--- /dev/null
+++ b/src/api/SearchPageLogicFrontend.ts
@@ -0,0 +1,90 @@
+const backendport = 'http://it2810-10.idi.ntnu.no:3001/'
+
+async function Gql(query: string, variables: Record): Promise {
+ 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 {
+ 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: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ console.log(`Searchpage: Could not save filters to session storage: ${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: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ console.log(`Searchpage: Could not save search query to session storage: ${message}`)
+ }
+}
+
+export function SaveSortOptionSearchpage(query: string) {
+ try {
+ sessionStorage.setItem('GameStart:sortOptionSearchpage', JSON.stringify(query))
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ console.log(`Searchpage: Could not save sorting option to session storage: ${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: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ console.log(`Searchpage: Could not save page to session storage: ${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
+ }
+}
diff --git a/src/api/userlogic_frontend.ts b/src/api/UserLogicFrontend.ts
similarity index 62%
rename from src/api/userlogic_frontend.ts
rename to src/api/UserLogicFrontend.ts
index 734fcf8..8e4f999 100644
--- a/src/api/userlogic_frontend.ts
+++ b/src/api/UserLogicFrontend.ts
@@ -1,59 +1,61 @@
-import { gql } from "./logicconnection"
+import { Gql } from './LogicConnection'
export interface User {
- id: number,
- email: string,
+ id: number
+ email: string
username: string
}
const USER_FIELDS = 'id email username'
// Calls upon the backend function to sign in a user.
-export async function signinUser(email: string, password: string): Promise {
- const data = await gql<{ signin: User }>(
+export async function SigninUser(email: string, password: string): Promise {
+ const data = await Gql<{ signin: User }>(
`mutation($email: String!, $password: String!) {
signin(email: $email, password: $password) { ${USER_FIELDS} }
}`,
- { email, password }
+ { email, password },
)
return data.signin
}
// Calls upon the backend function to sign up a user.
-export async function signupUser(username: string, email: string, password: string): Promise {
- const data = await gql<{ signup: User }>(
+export async function SignupUser(username: string, email: string, password: string): Promise {
+ const data = await Gql<{ signup: User }>(
`mutation($username: String!, $email: String!, $password: String!) {
signup(username: $username, email: $email password: $password) { ${USER_FIELDS} }
}`,
- { username, email, password }
+ { username, email, password },
)
return data.signup
}
//If the user wants to be remembered, their user will be saved to the localstorage.
//If the user does not want to be remembered, they are saved to sessionstorage.
-export function saveUser(user: User, remember = true) {
+export function SaveUser(user: User, remember = true) {
try {
- clearUser()
+ ClearUser()
const storage = remember ? localStorage : sessionStorage
storage.setItem('GameStart:user', JSON.stringify(user))
- } catch (error: any) {
- console.log(`Could not save user information to storage: ${error.message}`)
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ console.log(`Could not save user information to storage: ${message}`)
}
}
// Returns the saved user or null if no user is found.
-export function getSavedUser(): User | null {
+export function GetSavedUser(): User | null {
try {
- const raw = localStorage.getItem('GameStart:user') ?? sessionStorage.getItem('GameStart:user')
+ const raw =
+ localStorage.getItem('GameStart:user') ?? sessionStorage.getItem('GameStart:user')
return raw ? (JSON.parse(raw) as User) : null
- } catch (error) {
+ } catch {
return null
}
}
//Clears the current user
-export function clearUser() {
+export function ClearUser() {
localStorage.removeItem('GameStart:user')
sessionStorage.removeItem('GameStart:user')
-}
\ No newline at end of file
+}
diff --git a/src/api/WishlistLogicFrontend.ts b/src/api/WishlistLogicFrontend.ts
new file mode 100644
index 0000000..46b70d7
--- /dev/null
+++ b/src/api/WishlistLogicFrontend.ts
@@ -0,0 +1,73 @@
+import { type Game } from '../backend/Steam'
+
+const backendport = 'http://it2810-10.idi.ntnu.no:3001/'
+
+async function Gql(query: string, variables: Record): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ return isWishlisted
+ ? RemoveFromWishlistFrontend(userId, gameId)
+ : AddToWishlistFrontend(userId, gameId)
+}
+
+export function SaveSortOptionWishlist(query: string) {
+ try {
+ sessionStorage.setItem('GameStart:sortOptionWishlist', JSON.stringify(query))
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : String(error)
+ console.log(`Searchpage: Could not save sorting option to session storage: ${message}`)
+ }
+}
+
+export function GetSortOptionWishlist(): string {
+ try {
+ const parsed = JSON.parse(sessionStorage.getItem('GameStart:sortOptionWishlist') ?? '""')
+ return typeof parsed === 'string' ? parsed : ''
+ } catch {
+ return 'discount-desc'
+ }
+}
diff --git a/src/api/games.ts b/src/api/games.ts
deleted file mode 100644
index 0f87474..0000000
--- a/src/api/games.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import barotrauma from '../assets/barotrauma.png'
-import bigWalk from '../assets/bigwalk.png'
-import silksong from '../assets/silksong.png'
-import slayTheSpire from '../assets/slaythespire.png'
-import mockGames from './mockdata.json'
-import type { GameData } from '../types/game'
-
-const imagesById: Record = {
- 1: bigWalk,
- 2: barotrauma,
- 3: silksong,
- 4: slayTheSpire,
-}
-
-export const games: GameData[] = mockGames.map((game) => ({
- ...game,
- image: imagesById[game.id],
-}))
diff --git a/src/api/logicconnection.ts b/src/api/logicconnection.ts
deleted file mode 100644
index 154894a..0000000
--- a/src/api/logicconnection.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-
-const BACKEND_URL = 'http://it2810-10.idi.ntnu.no:3001/'
-
-export async function gql(query: string, variables: Record): Promise {
- const res = await fetch(BACKEND_URL, {
- 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;
-}
\ No newline at end of file
diff --git a/src/api/mockdata.json b/src/api/mockdata.json
deleted file mode 100644
index 641d7c9..0000000
--- a/src/api/mockdata.json
+++ /dev/null
@@ -1,61 +0,0 @@
-[
- {
- "id": 1,
- "name": "Big Walk",
- "price": 19.90,
- "genre": [
- "Co-op",
- "Exploration",
- "Adventure"
- ],
- "developer": "House House",
- "description": "Big Walk is a cooperative multiplayer adventure about teamwork and talking. Set out with your friends through a wide-open world full of challenges, puzzles, and discoveries. You’ll need to work together to find your way around, stay in contact using an assortment of tools and toys, and figure out new ways to communicate when you suddenly find yourself speechless.Adventuring aside, there’s plenty of time to just hang out. Sit and watch the sunset, or steal your friend’s binoculars and kick them into the ocean. Spending time with friends is the best part of a big walk.",
- "image": "../assets/bigwalk.png",
- "salePercent": 10
- },
- {
- "id": 2,
- "name": "Barotrauma",
- "price": 39.20,
- "genre": [
- "Co-op",
- "Multiplayer",
- "Action",
- "Simulation",
- "Strategy"
- ],
- "developer": "FakeFish, Undertow Games",
- "description": "Barotrauma is a 2D co-op survival horror submarine simulator, inspired by games like FTL: Faster Than Light, Rimworld, Dwarf Fortress and Space Station 13. It’s a Sci-Fi game that combines ragdoll physics and alien sea monsters with teamwork and existential fear.",
- "image": "../assets/barotrauma.png",
- "salePercent": 5
- },
- {
- "id": 3,
- "name": "Hollow Knight: Silksong",
- "price": 22.75,
- "genre": [
- "Indie",
- "Action",
- "Adventure"
- ],
- "developer": "Team Cherry",
- "description": "As the lethal hunter Hornet, adventure through a kingdom ruled by silk and song! Captured and taken to this unfamiliar world, prepare to battle mighty foes and solve ancient mysteries as you ascend on a deadly pilgrimage to the kingdom’s peak. Hollow Knight: Silksong is the epic sequel to Hollow Knight, the award winning action-adventure. Journey to all-new lands, discover new powers, battle vast hordes of bugs and beasts and uncover secrets tied to your nature and your past.",
- "image": "../assets/silksong.png",
- "salePercent": 0
- },
- {
- "id": 4,
- "name": "Slay the Spire",
- "price": 24.31,
- "genre": [
- "Card Game",
- "Indie",
- "Strategy",
- "Rougelike"
- ],
- "developer": "Mega Crit",
- "description": "“The devs have taken the best of the deckbuilding and roguelike genres to craft an immensely deep, frustrating, and gratifying experience. Slay the Spire is a perfect example of “easy to learn, hard to master”.” 10/10 – Goomba Stomp",
- "image": "../assets/slaythespire.png",
- "salePercent": 0
- }
-]
\ No newline at end of file
diff --git a/src/api/searchpageLogicFrontend.ts b/src/api/searchpageLogicFrontend.ts
deleted file mode 100644
index 4cd0c51..0000000
--- a/src/api/searchpageLogicFrontend.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-const backendport = 'http://it2810-10.idi.ntnu.no:3001/'
-
-async function gql(query: string, variables: Record): Promise {
- 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 {
- 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;
- }
-}
\ No newline at end of file
diff --git a/src/api/wishlistLogicFrontend.ts b/src/api/wishlistLogicFrontend.ts
deleted file mode 100644
index 6d39080..0000000
--- a/src/api/wishlistLogicFrontend.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { type Game } from '../backend/steam'
-
-const backendport = 'http://it2810-10.idi.ntnu.no:3001/'
-
-async function gql(query: string, variables: Record): Promise {
- 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 {
- 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 {
- 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 {
- 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 {
- 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";
- }
-}
\ No newline at end of file
diff --git a/src/assets/logo_gamestart_transparent.png b/src/assets/LogoGameStartTransparent.png
similarity index 100%
rename from src/assets/logo_gamestart_transparent.png
rename to src/assets/LogoGameStartTransparent.png
diff --git a/src/assets/barotrauma.png b/src/assets/barotrauma.png
deleted file mode 100644
index a483105..0000000
Binary files a/src/assets/barotrauma.png and /dev/null differ
diff --git a/src/assets/bigwalk.png b/src/assets/bigwalk.png
deleted file mode 100644
index 785d43f..0000000
Binary files a/src/assets/bigwalk.png and /dev/null differ
diff --git a/src/assets/silksong.png b/src/assets/silksong.png
deleted file mode 100644
index d901d9c..0000000
Binary files a/src/assets/silksong.png and /dev/null differ
diff --git a/src/assets/slaythespire.png b/src/assets/slaythespire.png
deleted file mode 100644
index 1d8e076..0000000
Binary files a/src/assets/slaythespire.png and /dev/null differ
diff --git a/src/backend/DbConnection.ts b/src/backend/DbConnection.ts
new file mode 100644
index 0000000..8124db6
--- /dev/null
+++ b/src/backend/DbConnection.ts
@@ -0,0 +1,10 @@
+import 'dotenv/config'
+import { Pool } from 'pg'
+
+export const pool = new Pool({
+ host: process.env.DB_HOST,
+ port: Number(process.env.DB_PORT),
+ database: process.env.DB_NAME,
+ user: process.env.DB_USER,
+ password: process.env.DB_PASSWORD,
+})
diff --git a/src/backend/genres.ts b/src/backend/Genres.ts
similarity index 65%
rename from src/backend/genres.ts
rename to src/backend/Genres.ts
index e1b1ab3..cdf352e 100644
--- a/src/backend/genres.ts
+++ b/src/backend/Genres.ts
@@ -1,11 +1,11 @@
-import { pool } from './db_connection.ts'
+import { pool } from './DbConnection.ts'
-export async function fetchGenres(): Promise {
+export async function FetchGenres(): Promise {
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`
+ ORDER BY genre`,
)
return rows.map((r) => r.genre)
-}
\ No newline at end of file
+}
diff --git a/src/backend/server.ts b/src/backend/Server.ts
similarity index 78%
rename from src/backend/server.ts
rename to src/backend/Server.ts
index 2ecb143..d98270e 100644
--- a/src/backend/server.ts
+++ b/src/backend/Server.ts
@@ -1,10 +1,10 @@
-import { ApolloServer } from '@apollo/server'
+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'
-import { addToWishlist, removeFromWishlist } from './wishlistLogic.ts'
+import { type Game } from './Steam.ts'
+import { pool } from './DbConnection.ts'
+import { FetchGenres } from './Genres.ts'
+import { Signin, Signup } from './UserLogicBackend.ts'
+import { AddToWishlist, RemoveFromWishlist } from './WishlistLogic.ts'
// Documentation on apollo: https://www.apollographql.com/docs/deploy-preview/5dc6222f824f9e089e2a5735/apollo-server/api/apollo-server
@@ -78,7 +78,7 @@ const resolvers = {
JOIN games g ON g.game_id = w.game_id
WHERE w.user_id = $1
ORDER BY g.game_name`,
- [userId]
+ [userId],
)
return rows
},
@@ -98,21 +98,29 @@ const resolvers = {
)
return rows[0] ?? null
},
- genres: () => fetchGenres(),
+ genres: () => FetchGenres(),
},
Mutation: {
- addToWishlist: async (_: unknown, { userId, gameId }: { userId: number; gameId: number }) => {
- await addToWishlist(userId, gameId)
+ 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)
+ 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),
+ 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 }) =>
- signin(email, password),
+ Signin(email, password),
},
}
diff --git a/src/backend/steam.ts b/src/backend/Steam.ts
similarity index 80%
rename from src/backend/steam.ts
rename to src/backend/Steam.ts
index 67acfff..bd3cc3e 100644
--- a/src/backend/steam.ts
+++ b/src/backend/Steam.ts
@@ -37,13 +37,13 @@ const PAGE_SIZE = 1000
const pageCache = new Map()
//Because the bulk fetch happens page wise, we have to know which page to continue fetching from. This function will return the correct page.
-async function fetchPage(page: number): Promise {
+async function FetchPage(page: number): Promise {
const cached_page = pageCache.get(page)
- if(cached_page) return cached_page
+ if (cached_page) return cached_page
const response = await fetch(`https://steamspy.com/api.php?request=all&page=${page}`)
- if(!response.ok) {
+ if (!response.ok) {
throw new Error(`SteamSpy request failed: ${response.status}`)
}
@@ -53,26 +53,24 @@ async function fetchPage(page: number): Promise {
}
// Run this to start the Api fetch. Will run all the other functions.
-export async function fetchGames(limit: number, offset = 0, delayMs = 300): Promise {
- const bulkGames = await fetchBulkGames(limit, offset)
- return fillAll(bulkGames, delayMs)
+export async function FetchGames(limit: number, offset = 0, delayMs = 300): Promise {
+ const bulkGames = await FetchBulkGames(limit, offset)
+ return FillAll(bulkGames, delayMs)
}
//This contains all the api calls we can do: https://www.steamspy.com/api.php
// This fetches "limit" amount of games as a bulk from point "offset" and removes all apps that are hidden (app id = 99999)
-export async function fetchBulkGames(limit: number, offset: number): Promise {
+export async function FetchBulkGames(limit: number, offset: number): Promise {
const firstPage = Math.floor(offset / PAGE_SIZE)
const lastPage = Math.floor((offset + limit - 1) / PAGE_SIZE)
const all = []
- for(let page = firstPage; page <= lastPage; page++) {
- all.push(...Object.values(await fetchPage(page)))
+ for (let page = firstPage; page <= lastPage; page++) {
+ all.push(...Object.values(await FetchPage(page)))
}
const start = offset - firstPage * PAGE_SIZE
- const games = all
- .slice(start, start + limit)
- .filter((g) => g.appid !== HIDDEN_APP_ID)
+ const games = all.slice(start, start + limit).filter((g) => g.appid !== HIDDEN_APP_ID)
return games.map((g) => ({
appid: g.appid,
@@ -88,10 +86,10 @@ export async function fetchBulkGames(limit: number, offset: number): Promise {
+export async function FillAll(games: Game[], delayMs = 300): Promise {
const filled_games: Game[] = []
for (const game of games) {
- filled_games.push(await fillGame(game))
+ filled_games.push(await FillGame(game))
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs))
}
@@ -100,7 +98,7 @@ export async function fillAll(games: Game[], delayMs = 300): Promise {
}
// The "enrichment chamber"
-export async function fillGame(game: Game): Promise {
+export async function FillGame(game: Game): Promise {
const response = await fetch(
`https://store.steampowered.com/api/appdetails?appids=${game.appid}`,
)
@@ -123,6 +121,6 @@ export async function fillGame(game: Game): Promise {
}
// This is for test purposes only
-export function clearPageCache() {
+export function ClearPageCache() {
pageCache.clear()
-}
\ No newline at end of file
+}
diff --git a/src/backend/UserLogicBackend.ts b/src/backend/UserLogicBackend.ts
new file mode 100644
index 0000000..880ba41
--- /dev/null
+++ b/src/backend/UserLogicBackend.ts
@@ -0,0 +1,56 @@
+import { pool } from './DbConnection.ts'
+
+export interface User {
+ id: number
+ email: string
+ username: string
+}
+
+function NormalizeEmail(email: string) {
+ return email.trim().toLowerCase()
+}
+
+export async function Signup(username: string, email: string, password: string): Promise {
+ if (!username || !password || !email) {
+ throw new Error('Email, username and password are required.')
+ }
+ if (!/\S+@\S+\.\S+/.test(NormalizeEmail(email))) {
+ throw new Error('Please enter a valid email address.')
+ }
+ try {
+ const { rows } = await pool.query(
+ `INSERT INTO users (user_name, password_hash, email)
+ VALUES ($1, $2, $3)
+ RETURNING user_id, user_name, email`,
+ [username, password, NormalizeEmail(email)],
+ )
+ return { id: rows[0].user_id, email: rows[0].email, username: rows[0].user_name }
+ } catch (error: unknown) {
+ if (
+ typeof error === 'object' &&
+ error !== null &&
+ 'code' in error &&
+ error.code === '23505'
+ ) {
+ throw new Error('An account with this email already exists.', { cause: error })
+ } else {
+ const message = error instanceof Error ? error.message : String(error)
+ throw new Error(`Could not register user: ${message}`, { cause: error })
+ }
+ }
+}
+
+export async function Signin(email: string, password: string): Promise {
+ const { rows } = await pool.query(
+ `SELECT user_id, user_name, email
+ FROM users
+ WHERE email = $1 AND password_hash = $2
+ ORDER BY user_id
+ LIMIT 1`,
+ [NormalizeEmail(email), password],
+ )
+ if (rows.length == 0) {
+ throw new Error('User not found. Invalid email or password.')
+ }
+ return { id: rows[0].user_id, email: rows[0].email, username: rows[0].user_name }
+}
diff --git a/src/backend/WishlistLogic.ts b/src/backend/WishlistLogic.ts
new file mode 100644
index 0000000..ac87728
--- /dev/null
+++ b/src/backend/WishlistLogic.ts
@@ -0,0 +1,36 @@
+import { pool } from './DbConnection.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: unknown) {
+ // 23503 = foreign key violation (user or game doesn't exist)
+ if (typeof err === 'object' && err !== null && 'code' in err && err.code === '23503') {
+ throw new Error('addToWishlist: user or game does not exist', { cause: err })
+ }
+ 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 }
+}
diff --git a/src/backend/db_connection.ts b/src/backend/db_connection.ts
deleted file mode 100644
index 1e992d5..0000000
--- a/src/backend/db_connection.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import "dotenv/config";
-import { Pool } from "pg";
-
-export const pool = new Pool({
- host: process.env.DB_HOST,
- port: Number(process.env.DB_PORT),
- database: process.env.DB_NAME,
- user: process.env.DB_USER,
- password: process.env.DB_PASSWORD,
-});
\ No newline at end of file
diff --git a/src/backend/userlogic_backend.ts b/src/backend/userlogic_backend.ts
deleted file mode 100644
index 9254cb5..0000000
--- a/src/backend/userlogic_backend.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { pool } from './db_connection.ts'
-
-export interface User{
- id: number,
- email: string,
- username: string
-}
-
-function normalizeEmail(email: string) {
- return email.trim().toLowerCase()
-}
-
-export async function signup(username:string, email: string, password:string): Promise {
- if(!username || !password || !email) {
- throw new Error("Email, username and password are required.")
- }
- if (!/\S+@\S+\.\S+/.test(normalizeEmail(email))) {
- throw new Error('Please enter a valid email address.')
- }
- try {
- const {rows} = await pool.query(
- `INSERT INTO users (user_name, password_hash, email)
- VALUES ($1, $2, $3)
- RETURNING user_id, user_name, email`,
- [username, password, normalizeEmail(email)]
- )
- return {id: rows[0].user_id, email: rows[0].email, username: rows[0].user_name}
- } catch (error: any) {
- if(error.code === '23505'){
- throw new Error(`An account with this email already exists.`)
- }
- else {
- throw new Error(`Could not register user: ${error.message}`)
- }
- }
-}
-
-export async function signin(email: string, password: string): Promise {
- const {rows} = await pool.query(
- `SELECT user_id, user_name, email
- FROM users
- WHERE email = $1 AND password_hash = $2
- ORDER BY user_id
- LIMIT 1`,
- [normalizeEmail(email), password]
- )
- if(rows.length == 0) {
- throw new Error("User not found. Invalid email or password.")
- }
- return {id: rows[0].user_id, email: rows[0].email, username: rows[0].user_name}
-}
\ No newline at end of file
diff --git a/src/backend/wishlistLogic.ts b/src/backend/wishlistLogic.ts
deleted file mode 100644
index 3542760..0000000
--- a/src/backend/wishlistLogic.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-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 }
-}
\ No newline at end of file
diff --git a/src/components/activegamecard.tsx b/src/components/ActiveGameCard.tsx
similarity index 84%
rename from src/components/activegamecard.tsx
rename to src/components/ActiveGameCard.tsx
index ea02a6b..d04504d 100644
--- a/src/components/activegamecard.tsx
+++ b/src/components/ActiveGameCard.tsx
@@ -1,12 +1,13 @@
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 '../css/ActiveGameCard.css'
+import type { GameData } from '../types/Game'
+import { GetSavedUser } from '../api/UserLogicFrontend'
import { useState, useEffect } from 'react'
-import { removeFromWishlistFrontend,
- addToWishlistFrontend,
- fetchWishlistFrontend }
- from '../api/wishlistLogicFrontend'
+import {
+ RemoveFromWishlistFrontend,
+ AddToWishlistFrontend,
+ FetchWishlistFrontend,
+} from '../api/WishlistLogicFrontend'
type ActiveGameCardProps = {
game: GameData
@@ -14,28 +15,30 @@ type ActiveGameCardProps = {
onAddToWishlist?: (gameId: number) => void
}
-export default function ActiveGameCard({ game, onAddToWishlist, onRemoveFromWishlist }: ActiveGameCardProps) {
+export default function ActiveGameCard({
+ game,
+ onAddToWishlist,
+ onRemoveFromWishlist,
+}: ActiveGameCardProps) {
const [isWishlisted, setIsWishlisted] = useState(false)
- const [isChecking, setIsChecking] = useState(true)
+ const [isChecking, setIsChecking] = useState(() => GetSavedUser() !== null)
const [isBusy, setIsBusy] = useState(false)
const [error, setError] = useState(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)
+ const user = GetSavedUser()
+
+ if (!user) return
+
+ FetchWishlistFrontend(user.id)
.then((wishlist) => {
- if (!cancelled) setIsWishlisted(wishlist.some((wishlisted) => wishlisted.appid === game.id))
+ if (!cancelled)
+ setIsWishlisted(wishlist.some((wishlisted) => wishlisted.appid === game.id))
})
.catch(() => {
if (!cancelled) setError('Could not check your wishlist')
@@ -43,29 +46,29 @@ export default function ActiveGameCard({ game, onAddToWishlist, onRemoveFromWish
.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()
+ 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)
+ await RemoveFromWishlistFrontend(user.id, game.id)
setIsWishlisted(false)
onRemoveFromWishlist?.(game.id)
} else {
- await addToWishlistFrontend(user.id, game.id)
+ await AddToWishlistFrontend(user.id, game.id)
setIsWishlisted(true)
onAddToWishlist?.(game.id)
}
@@ -89,7 +92,7 @@ export default function ActiveGameCard({ game, onAddToWishlist, onRemoveFromWish
className="activeGameWishlistButton"
variant="contained"
disabled={isChecking || isBusy}
- onClick={handleToggleWishlist}
+ onClick={HandleToggleWishlist}
>
{isWishlisted ? 'Remove from wishlist' : 'Add to wishlist'}
@@ -161,9 +164,9 @@ export default function ActiveGameCard({ game, onAddToWishlist, onRemoveFromWish
/* How to use this:
/* The below may be inserted under the imports section of a page.
-import ActiveGameCard from '../components/activegamecard'
-import games from '../api/mockdata.json'
-import bigWalk from '../assets/bigwalk.png'
+import ActiveGameCard from '../components/ActiveGameCard'
+import games from '../api/MockData.json'
+import bigWalk from '../assets/BigWalk.png'
*/
/* The following may be put underneath the export function part of a page, example is written with ''' around it, the code itself is the const.
diff --git a/src/components/AuthTheme.ts b/src/components/AuthTheme.ts
new file mode 100644
index 0000000..886325c
--- /dev/null
+++ b/src/components/AuthTheme.ts
@@ -0,0 +1,14 @@
+import { createTheme } from '@mui/material/styles'
+
+const AuthTheme = createTheme({
+ palette: {
+ primary: {
+ main: '#27b722',
+ light: '#63ce5c',
+ dark: '#1b8317',
+ contrastText: '#000000',
+ },
+ },
+})
+
+export default AuthTheme
diff --git a/src/components/footer.tsx b/src/components/Footer.tsx
similarity index 73%
rename from src/components/footer.tsx
rename to src/components/Footer.tsx
index 57bac3f..59df83e 100644
--- a/src/components/footer.tsx
+++ b/src/components/Footer.tsx
@@ -1,11 +1,11 @@
-import '../css/footer.css'
-import gameStartLogo from '../assets/logo_gamestart_transparent.png'
+import '../css/Footer.css'
+import gameStartLogo from '../assets/LogoGameStartTransparent.png'
import { Divider } from '@mui/material'
export default function Footer() {
return (
-
+
)
-}
\ No newline at end of file
+}
diff --git a/src/components/passivegamecard.tsx b/src/components/PassiveGameCard.tsx
similarity index 87%
rename from src/components/passivegamecard.tsx
rename to src/components/PassiveGameCard.tsx
index 978bff9..d879e4c 100644
--- a/src/components/passivegamecard.tsx
+++ b/src/components/PassiveGameCard.tsx
@@ -1,4 +1,4 @@
-import '../css/passivegamecard.css'
+import '../css/PassiveGameCard.css'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import CardMedia from '@mui/material/CardMedia'
@@ -6,7 +6,7 @@ import Typography from '@mui/material/Typography'
import CardActionArea from '@mui/material/CardActionArea'
import { Link } from 'react-router-dom'
import { useLayoutEffect, useRef, useState } from 'react'
-import type { PassiveGameData } from '../types/game'
+import type { PassiveGameData } from '../types/Game'
type Props = {
game: PassiveGameData
@@ -24,13 +24,13 @@ export default function PassiveGameCard({ game, rank }: Props) {
const title = titleContainer?.firstElementChild
if (!(title instanceof HTMLElement) || !titleContainer) return
- const updateOverflow = () => {
+ const UpdateOverflow = () => {
setIsTitleOverflowing(title.scrollHeight > titleContainer.clientHeight + 1)
}
- updateOverflow()
+ UpdateOverflow()
if (typeof ResizeObserver === 'undefined') return
- const resizeObserver = new ResizeObserver(updateOverflow)
+ const resizeObserver = new ResizeObserver(UpdateOverflow)
resizeObserver.observe(titleContainer)
resizeObserver.observe(title)
@@ -83,13 +83,13 @@ The section that has "slice(0,1)" only exists for the time being to allow develo
and compare multiples of the same object to each other, if needed. Change the "1" to alter amount of PassiveGameCards
that are shown.*/
/* The below may be inserted under the imports part of a page. This is a temporary hard-coded method for the first delivery of the project.
-import games from '../api/mockdata.json'
-import bigWalk from '../assets/bigwalk.png'
-import barotrauma from '../assets/barotrauma.png'
-import silksong from '../assets/silksong.png'
-import slayTheSpire from '../assets/slaythespire.png'
-import PassiveGameCard from '../components/passivegamecard'
-import WishlistGameCard from '../components/wishlistgamecard'
+import games from '../api/MockData.json'
+import bigWalk from '../assets/BigWalk.png'
+import barotrauma from '../assets/Barotrauma.png'
+import silksong from '../assets/Silksong.png'
+import slayTheSpire from '../assets/SlayTheSpire.png'
+import PassiveGameCard from '../components/PassiveGameCard'
+import WishlistGameCard from '../components/WishlistGameCard'
const imagesById: Record = {
1: bigWalk,
diff --git a/src/components/SignIn&UpCard.tsx b/src/components/SignIn&UpCard.tsx
deleted file mode 100644
index 843970b..0000000
--- a/src/components/SignIn&UpCard.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { styled } from '@mui/material/styles'
-import { createTheme } from '@mui/material/styles'
-import MuiCard from '@mui/material/Card'
-
-export const authTheme = createTheme({
- palette: {
- primary: {
- main: '#27b722',
- light: '#63ce5c',
- dark: '#1b8317',
- contrastText: '#000000',
- },
- },
-})
-
-export const SignInAndUpCard = styled(MuiCard)(({ theme }) => ({
- display: 'flex',
- flexDirection: 'column',
- alignSelf: 'center',
- width: 'min(calc(100% - 3rem), 450px)',
- padding: theme.spacing(4),
- gap: theme.spacing(2),
- margin: 'auto',
- borderColor: 'var(--secondary-component-color)',
- '& a': {
- color: theme.palette.primary.main,
- '&:hover': {
- color: theme.palette.primary.dark,
- },
- },
- boxShadow:
- '0px 5px 15px 0px var(--box-shadow-color), 0px 15px 35px -5px var(--box-shadow-color)',
- [theme.breakpoints.up('sm')]: {
- width: '450px',
- },
- ...theme.applyStyles('dark', {
- boxShadow:
- '0px 5px 15px 0px var(--box-shadow-color), 0px 15px 35px -5px var(--box-shadow-color)',
- }),
-}))
diff --git a/src/components/SignInAndUpCard.tsx b/src/components/SignInAndUpCard.tsx
new file mode 100644
index 0000000..ebe9a13
--- /dev/null
+++ b/src/components/SignInAndUpCard.tsx
@@ -0,0 +1,7 @@
+import { Card as MuiCard } from '@mui/material'
+import type { CardProps } from '@mui/material'
+import '../css/SignIn&UpCard.css'
+
+export const SignInAndUpCard = (props: CardProps) => (
+
+)
diff --git a/src/components/wishlistgamecard.tsx b/src/components/WishlistGameCard.tsx
similarity index 74%
rename from src/components/wishlistgamecard.tsx
rename to src/components/WishlistGameCard.tsx
index 7369420..a62c4e4 100644
--- a/src/components/wishlistgamecard.tsx
+++ b/src/components/WishlistGameCard.tsx
@@ -1,8 +1,17 @@
-import { Card, CardActionArea, CardActions, CardContent, CardMedia, Chip, Typography, Button } from '@mui/material'
-import '../css/wishlistgamecard.css'
-import type { PassiveGameData } from '../types/game'
-import { removeFromWishlistFrontend } from '../api/wishlistLogicFrontend'
-import { getSavedUser } from '../api/userlogic_frontend'
+import {
+ Card,
+ CardActionArea,
+ CardActions,
+ CardContent,
+ CardMedia,
+ Chip,
+ Typography,
+ Button,
+} from '@mui/material'
+import '../css/WishlistGameCard.css'
+import type { PassiveGameData } from '../types/Game'
+import { RemoveFromWishlistFrontend } from '../api/WishlistLogicFrontend'
+import { GetSavedUser } from '../api/UserLogicFrontend'
import { useState } from 'react'
import { Link } from 'react-router-dom'
@@ -14,21 +23,21 @@ type wishlistGamecardProps = {
export default function WishlistGameCard({ game, onRemoveFromWishlist }: wishlistGamecardProps) {
const [isRemoving, setIsRemoving] = useState(false)
const [error, setError] = useState(null)
-
+
const isOnSale =
game.salePercent !== undefined && game.salePercent > 0 && game.salePercent <= 100
- async function handleRemove() {
- const user = getSavedUser()
+ async function HandleRemove() {
+ const user = GetSavedUser()
if (!user) {
setError('You need to sign in to change your wishlist')
return
}
-
+
setIsRemoving(true)
setError(null)
try {
- await removeFromWishlistFrontend(user.id, game.id)
+ await RemoveFromWishlistFrontend(user.id, game.id)
onRemoveFromWishlist?.(game.id)
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Could not remove game')
@@ -36,11 +45,14 @@ export default function WishlistGameCard({ game, onRemoveFromWishlist }: wishlis
setIsRemoving(false)
}
}
-
+
return (
-
+
{game.name}
-
-
+
{isOnSale && (
Remove
- {error &&
{error}
}
+ {error &&
{error}
}
)
@@ -88,13 +99,13 @@ The section that has "slice(0,1)" only exists for the time being to allow develo
and compare multiples of the same object to each other, if needed. Change the "1" to alter amount of WishlistGameCards
that are shown.*/
/* The below may be inserted under the imports part of a page. This is a temporary hard-coded method for the first delivery of the project.
-import games from '../api/mockdata.json'
-import bigWalk from '../assets/bigwalk.png'
-import barotrauma from '../assets/barotrauma.png'
-import silksong from '../assets/silksong.png'
-import slayTheSpire from '../assets/slaythespire.png'
-import PassiveGameCard from '../components/passivegamecard'
-import WishlistGameCard from '../components/wishlistgamecard'
+import games from '../api/MockData.json'
+import bigWalk from '../assets/BigWalk.png'
+import barotrauma from '../assets/Barotrauma.png'
+import silksong from '../assets/Silksong.png'
+import slayTheSpire from '../assets/SlayTheSpire.png'
+import PassiveGameCard from '../components/PassiveGameCard'
+import WishlistGameCard from '../components/WishlistGameCard'
const imagesById: Record = {
1: bigWalk,
diff --git a/src/context/authcontext.tsx b/src/context/AuthContext.tsx
similarity index 53%
rename from src/context/authcontext.tsx
rename to src/context/AuthContext.tsx
index 6d44c44..73512ce 100644
--- a/src/context/authcontext.tsx
+++ b/src/context/AuthContext.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { type User, saveUser, clearUser, getSavedUser } from '../api/userlogic_frontend'
+import { type User, SaveUser, ClearUser, GetSavedUser } from '../api/UserLogicFrontend'
interface AuthContextValue {
user: User | null
@@ -9,22 +9,26 @@ interface AuthContextValue {
const AuthContext = React.createContext(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
- const [user, setUser] = React.useState(() => getSavedUser())
+ const [user, setUser] = React.useState(() => GetSavedUser())
- const login = (user: User, remember = true) => {
- saveUser(user, remember)
+ const Login = (user: User, remember = true) => {
+ SaveUser(user, remember)
setUser(user)
}
- const logout = () => {
- clearUser()
+ const Logout = () => {
+ ClearUser()
setUser(null)
}
- return {children}
+ return (
+
+ {children}
+
+ )
}
-export function useAuth() {
+export function UseAuth() {
const context = React.useContext(AuthContext)
if (!context) throw new Error('useAuth must be used inside AuthProvider')
return context
diff --git a/src/css/activegamecard.css b/src/css/ActiveGameCard.css
similarity index 100%
rename from src/css/activegamecard.css
rename to src/css/ActiveGameCard.css
diff --git a/src/css/createaccountpage.css b/src/css/CreateAccountPage.css
similarity index 100%
rename from src/css/createaccountpage.css
rename to src/css/CreateAccountPage.css
diff --git a/src/css/footer.css b/src/css/Footer.css
similarity index 100%
rename from src/css/footer.css
rename to src/css/Footer.css
diff --git a/src/css/gamepage.css b/src/css/GamePage.css
similarity index 100%
rename from src/css/gamepage.css
rename to src/css/GamePage.css
diff --git a/src/css/global.css b/src/css/Global.css
similarity index 84%
rename from src/css/global.css
rename to src/css/Global.css
index 621db39..1682754 100644
--- a/src/css/global.css
+++ b/src/css/Global.css
@@ -2,11 +2,11 @@
/* Colors */
--primary-background-color: #ffffff;
- --primary-component-color: #7BCF73;
- --secondary-component-color: #DAF1D7;
+ --primary-component-color: #7bcf73;
+ --secondary-component-color: #daf1d7;
--primary-accent-color: #27b722;
- --box-shadow-color: #DAF1D7;
+ --box-shadow-color: #daf1d7;
/* Text */
--primary-text-color: #000000;
diff --git a/src/css/header.css b/src/css/Header.css
similarity index 100%
rename from src/css/header.css
rename to src/css/Header.css
diff --git a/src/css/loginpage.css b/src/css/LoginPage.css
similarity index 100%
rename from src/css/loginpage.css
rename to src/css/LoginPage.css
diff --git a/src/css/mainpage.css b/src/css/MainPage.css
similarity index 100%
rename from src/css/mainpage.css
rename to src/css/MainPage.css
diff --git a/src/css/pageheading.css b/src/css/PageHeading.css
similarity index 100%
rename from src/css/pageheading.css
rename to src/css/PageHeading.css
diff --git a/src/css/passivegamecard.css b/src/css/PassiveGameCard.css
similarity index 100%
rename from src/css/passivegamecard.css
rename to src/css/PassiveGameCard.css
diff --git a/src/css/profilepage.css b/src/css/ProfilePage.css
similarity index 100%
rename from src/css/profilepage.css
rename to src/css/ProfilePage.css
diff --git a/src/css/searchpage.css b/src/css/SearchPage.css
similarity index 99%
rename from src/css/searchpage.css
rename to src/css/SearchPage.css
index 43b5aed..87397d9 100644
--- a/src/css/searchpage.css
+++ b/src/css/SearchPage.css
@@ -117,4 +117,4 @@
.pageinationSearchTop .MuiPagination-ul {
flex-wrap: wrap;
justify-content: center;
-}
\ No newline at end of file
+}
diff --git a/src/css/SignIn&UpCard.css b/src/css/SignIn&UpCard.css
new file mode 100644
index 0000000..61991be
--- /dev/null
+++ b/src/css/SignIn&UpCard.css
@@ -0,0 +1,107 @@
+.sign-in-and-up-card {
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ width: min(calc(100% - 3rem), 450px);
+ padding: 32px;
+ gap: 16px;
+ margin: 2rem;
+ border-color: var(--secondary-component-color);
+ box-shadow:
+ 0px 5px 15px 0px var(--box-shadow-color),
+ 0px 15px 35px -5px var(--box-shadow-color);
+}
+
+.pageShell {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ min-height: 100dvh;
+}
+
+.sign-in-and-up-card a {
+ color: #27b722;
+}
+
+.sign-in-and-up-card a:hover {
+ color: #1b8317;
+}
+
+.signup-container {
+ flex: 1;
+ min-height: 0;
+ position: relative;
+ justify-content: center;
+}
+
+.signup-container {
+ flex: 1;
+ min-height: 0;
+ width: 100%;
+ position: relative;
+ justify-content: center;
+ align-items: center;
+}
+
+.signin-container {
+ flex: 1;
+ width: 100%;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+}
+
+.pageShell .footerWrapper {
+ margin-top: auto;
+}
+
+.signup-container::before {
+ content: '';
+ display: block;
+ position: absolute;
+ z-index: -1;
+ inset: 0;
+ background-image: radial-gradient(ellipse at 50% 50%, hsl(120, 45%, 97%), hsl(0, 0%, 100%));
+ background-repeat: no-repeat;
+}
+
+.signup-form {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: 16px;
+}
+
+.signup-form-footer {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.signup-footer-text {
+ text-align: center;
+}
+
+.signin-form {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: 16px;
+}
+
+.signin-form .MuiFormControl-root {
+ width: 100%;
+}
+
+.signin-form .MuiButton-root {
+ width: 100%;
+}
+
+@media (min-width: 600px) {
+ .sign-in-and-up-card {
+ width: 450px;
+ }
+}
diff --git a/src/css/wishlistgamecard.css b/src/css/WishlistGameCard.css
similarity index 100%
rename from src/css/wishlistgamecard.css
rename to src/css/WishlistGameCard.css
diff --git a/src/css/wishlistpage.css b/src/css/WishlistPage.css
similarity index 99%
rename from src/css/wishlistpage.css
rename to src/css/WishlistPage.css
index 695ebd3..8ac1552 100644
--- a/src/css/wishlistpage.css
+++ b/src/css/WishlistPage.css
@@ -50,4 +50,4 @@
.wishlistSortControl.MuiFormControl-root {
min-width: 200px;
}
-}
\ No newline at end of file
+}
diff --git a/src/pages/gamepage.tsx b/src/pages/GamePage.tsx
similarity index 89%
rename from src/pages/gamepage.tsx
rename to src/pages/GamePage.tsx
index dd0bd4b..37bad2b 100644
--- a/src/pages/gamepage.tsx
+++ b/src/pages/GamePage.tsx
@@ -1,12 +1,12 @@
-import '../css/gamepage.css'
-import Header from '../components/header'
-import Footer from '../components/footer'
-import ActiveGameCard from '../components/activegamecard'
+import '../css/GamePage.css'
+import Header from '../components/Header'
+import Footer from '../components/Footer'
+import ActiveGameCard from '../components/ActiveGameCard'
import { useParams } from 'react-router-dom'
-import PageHeading from '../components/pageheading'
+import PageHeading from '../components/PageHeading'
import { useEffect, useState } from 'react'
-import { getGameById } from '../api/gameClient'
-import type { Game } from '../backend/steam'
+import { GetGameById } from '../api/GameClient'
+import type { Game } from '../backend/Steam'
export default function GamePage() {
const { gameId } = useParams()
@@ -22,7 +22,7 @@ export default function GamePage() {
if (!Number.isSafeInteger(appid)) return
- getGameById(appid)
+ GetGameById(appid)
.then((loadedGame) => {
if (!cancelled) setLoadedGame({ appid, game: loadedGame, error: null })
})
diff --git a/src/pages/mainpage.tsx b/src/pages/MainPage.tsx
similarity index 93%
rename from src/pages/mainpage.tsx
rename to src/pages/MainPage.tsx
index 76513ff..141de0d 100644
--- a/src/pages/mainpage.tsx
+++ b/src/pages/MainPage.tsx
@@ -1,16 +1,16 @@
-import '../css/mainpage.css'
-import Header from '../components/header'
-import Footer from '../components/footer'
-import PassiveGameCard from '../components/passivegamecard'
+import '../css/MainPage.css'
+import Header from '../components/Header'
+import Footer from '../components/Footer'
+import PassiveGameCard from '../components/PassiveGameCard'
import { WishlistCard } from '../components/WishlistCard'
-import { getGames } from '../api/gameClient'
+import { GetGames } from '../api/GameClient'
import {
- getSavedMainPageGenre,
- saveMainPageGenre,
- prepareDiscountGenreSearch,
+ GetSavedMainPageGenre,
+ SaveMainPageGenre,
+ PrepareDiscountGenreSearch,
type MainPageGenreSection,
-} from '../api/mainpageLogicFrontend'
-import type { Game } from '../backend/steam'
+} from '../api/MainPageLogicFrontend'
+import type { Game } from '../backend/Steam'
import { Link } from 'react-router-dom'
import type { CSSProperties } from 'react'
import { useEffect, useMemo, useState } from 'react'
@@ -28,7 +28,7 @@ type PageGame = {
salePercent: number
}
-function toPageGame(game: Game): PageGame {
+function ToPageGame(game: Game): PageGame {
const price = Number(game.price)
const salePercent = Number(game.discount)
const genres =
@@ -70,7 +70,7 @@ function GenreGamesSection({
showSeeMore = false,
storageKey,
}: GenreGamesSectionProps) {
- const [selectedGenre, setSelectedGenre] = useState(() => getSavedMainPageGenre(storageKey))
+ const [selectedGenre, setSelectedGenre] = useState(() => GetSavedMainPageGenre(storageKey))
const [firstGameIndex, setFirstGameIndex] = useState(0)
const isPhone = useMediaQuery('(max-width: 43.75rem)')
const isTwoColumnLayout = useMediaQuery('(max-width: 89.99rem)')
@@ -89,16 +89,16 @@ function GenreGamesSection({
const discountLoading = isLoadingDiscounts ?? isLoading
useEffect(() => {
- if (activeGenre) saveMainPageGenre(storageKey, activeGenre)
+ if (activeGenre) SaveMainPageGenre(storageKey, activeGenre)
}, [activeGenre, storageKey])
- const selectGenre = (genre: string) => {
+ const SelectGenre = (genre: string) => {
setSelectedGenre(genre)
setFirstGameIndex(0)
}
- const openDiscountsInSearch = () => {
- prepareDiscountGenreSearch(activeGenre)
+ const OpenDiscountsInSearch = () => {
+ PrepareDiscountGenreSearch(activeGenre)
}
return (
@@ -119,7 +119,7 @@ function GenreGamesSection({
value={activeGenre || null}
openOnFocus
onChange={(_, genre) => {
- selectGenre(genre ?? '')
+ SelectGenre(genre ?? '')
}}
disabled={genres.length === 0}
renderInput={(params) => (
@@ -131,9 +131,9 @@ function GenreGamesSection({
@@ -206,7 +206,7 @@ export default function MainPage() {
useEffect(() => {
let cancelled = false
- getGames()
+ GetGames()
.then((loadedGames) => {
if (!cancelled) setDatabaseGames(loadedGames)
})
@@ -226,7 +226,7 @@ export default function MainPage() {
}
}, [])
- const pageGames = useMemo(() => databaseGames.map(toPageGame), [databaseGames])
+ const pageGames = useMemo(() => databaseGames.map(ToPageGame), [databaseGames])
const featuredGames = pageGames.slice(0, 3)
const genresToFeature = useMemo(
() => [...new Set(pageGames.flatMap((game) => game.genre))].sort(),
diff --git a/src/pages/profilepage.tsx b/src/pages/ProfilePage.tsx
similarity index 80%
rename from src/pages/profilepage.tsx
rename to src/pages/ProfilePage.tsx
index b7ae41e..201fa18 100644
--- a/src/pages/profilepage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -1,12 +1,12 @@
-import Footer from '../components/footer'
-import Header from '../components/header'
-import '../css/profilepage.css'
+import Footer from '../components/Footer'
+import Header from '../components/Header'
+import '../css/ProfilePage.css'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Avatar from '@mui/material/Avatar'
import Divider from '@mui/material/Divider'
-import PageHeading from '../components/pageheading'
+import PageHeading from '../components/PageHeading'
export default function ProfilePage() {
return (
@@ -27,9 +27,9 @@ export default function ProfilePage() {
- I am a student at NTNU. I love playing videogames! I like adventuregames,
- multiplayer and cute designs. I like to play games with my friends, its a
- lot more fun when you dont play alone!
+ I am a student at NTNU. I love playing videogames! I like
+ adventuregames, multiplayer and cute designs. I like to play games with
+ my friends, its a lot more fun when you dont play alone!
diff --git a/src/pages/searchpage.tsx b/src/pages/SearchPage.tsx
similarity index 65%
rename from src/pages/searchpage.tsx
rename to src/pages/SearchPage.tsx
index 6f17fb4..2a8422b 100644
--- a/src/pages/searchpage.tsx
+++ b/src/pages/SearchPage.tsx
@@ -1,32 +1,41 @@
-import '../css/searchpage.css'
-import Header from '../components/header'
-import Footer from '../components/footer'
-import { InputLabel, FormControl, Select, MenuItem, Paper, InputBase, IconButton, Pagination } from '@mui/material'
+import '../css/SearchPage.css'
+import Header from '../components/Header'
+import Footer from '../components/Footer'
+import {
+ InputLabel,
+ FormControl,
+ Select,
+ MenuItem,
+ Paper,
+ InputBase,
+ IconButton,
+ Pagination,
+} from '@mui/material'
import { Funnel, SearchIcon } from 'lucide-react'
import { useMemo, useState, useEffect } from 'react'
-import PageHeading from '../components/pageheading'
-import PassiveGameCard from '../components/passivegamecard'
-import { getGames } from '../api/gameClient'
+import PageHeading from '../components/PageHeading'
+import PassiveGameCard from '../components/PassiveGameCard'
+import { GetGames } from '../api/GameClient'
import {
- fetchGenres,
- getSavedFiltersSearchpage,
- saveFiltersSearchpage,
- getQuerySearchpage,
- saveQuerySearchpage,
- getSortOptionSearchpage,
- saveSortOptionSearchpage,
- getPageSearchpage,
- savePageSearchpage
-} from '../api/searchpageLogicFrontend'
-import type { Game } from '../backend/steam'
+ FetchGenres,
+ GetSavedFiltersSearchpage,
+ SaveFiltersSearchpage,
+ GetQuerySearchpage,
+ SaveQuerySearchpage,
+ GetSortOptionSearchpage,
+ SaveSortOptionSearchpage,
+ GetPageSearchpage,
+ SavePageSearchpage,
+} from '../api/SearchPageLogicFrontend'
+import type { Game } from '../backend/Steam'
import type { SelectChangeEvent } from '@mui/material'
type SortOption = '' | 'discount-desc' | 'price-asc' | 'price-desc' | 'title-asc'
const SORT_OPTIONS: SortOption[] = ['', 'discount-desc', 'price-asc', 'price-desc', 'title-asc']
-function getInitialSort(): SortOption {
- const saved = getSortOptionSearchpage()
+function GetInitialSort(): SortOption {
+ const saved = GetSortOptionSearchpage()
return SORT_OPTIONS.includes(saved as SortOption) ? (saved as SortOption) : ''
}
@@ -34,17 +43,19 @@ export function SearchPage() {
const [games, setGames] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
- const [page, setPage] = useState(getPageSearchpage)
+ const [page, setPage] = useState(GetPageSearchpage)
const [genres, setGenres] = useState([])
- const [activeFilters, setActiveFilters] = useState(getSavedFiltersSearchpage)
- const [activeSorting, setActiveSorting] = useState(getInitialSort)
- const [searchVal, setSearchVal] = useState(getQuerySearchpage)
- const [searchQuery, setSearchQuery] = useState(getQuerySearchpage)
- const [isFilterActive, setIsFilterActive] = useState(() => activeFilters.length > 0 || activeSorting !== '')
+ const [activeFilters, setActiveFilters] = useState(GetSavedFiltersSearchpage)
+ const [activeSorting, setActiveSorting] = useState(GetInitialSort)
+ const [searchVal, setSearchVal] = useState(GetQuerySearchpage)
+ const [searchQuery, setSearchQuery] = useState(GetQuerySearchpage)
+ const [isFilterActive, setIsFilterActive] = useState(
+ () => activeFilters.length > 0 || activeSorting !== '',
+ )
// fetches the genres from the DB
useEffect(() => {
- fetchGenres().then(setGenres).catch(console.error)
+ FetchGenres().then(setGenres).catch(console.error)
}, [])
// retures a list of games based on the search query, if there is no query, it returns the original list
@@ -67,47 +78,60 @@ export function SearchPage() {
const sortedGames = useMemo(() => {
const list = [...filteredGames]
switch (activeSorting) {
- case 'discount-desc': return list.sort((a, b) => Number(b.discount) - Number(a.discount))
- case 'price-asc': return list.sort((a, b) => Number(a.price) - Number(b.price))
- case 'price-desc': return list.sort((a, b) => Number(b.price) - Number(a.price))
- case 'title-asc': return list.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
- default: return list
+ case 'discount-desc':
+ return list.sort((a, b) => Number(b.discount) - Number(a.discount))
+ case 'price-asc':
+ return list.sort((a, b) => Number(a.price) - Number(b.price))
+ case 'price-desc':
+ return list.sort((a, b) => Number(b.price) - Number(a.price))
+ case 'title-asc':
+ return list.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''))
+ default:
+ return list
}
}, [filteredGames, activeSorting])
// Opens the filter menu
- const toggleActive = () => setIsFilterActive((isActive) => !isActive)
+ const ToggleActive = () => setIsFilterActive((isActive) => !isActive)
- const handleSearchSubmit = (e: React.FormEvent) => {
+ const HandleSearchSubmit = (e: React.FormEvent) => {
e.preventDefault()
const q = searchVal.trim()
setSearchQuery(q)
setPage(1)
}
- const handleFilterChange = (e: SelectChangeEvent) => {
+ const HandleFilterChange = (e: SelectChangeEvent) => {
const { value } = e.target
setActiveFilters(typeof value === 'string' ? value.split(',') : value)
setPage(1)
}
- const handleSortChange = (e: SelectChangeEvent) => {
+ const HandleSortChange = (e: SelectChangeEvent) => {
setActiveSorting(e.target.value as SortOption)
setPage(1)
}
// These useEffects save the current filters, query and sorting option in session storage
// so that when the user goes back to the searchpage they are kept.
- useEffect(() => { saveFiltersSearchpage(activeFilters) }, [activeFilters])
- useEffect(() => { saveSortOptionSearchpage(activeSorting) }, [activeSorting])
- useEffect(() => { saveQuerySearchpage(searchQuery) }, [searchQuery])
- useEffect(() => { savePageSearchpage(page) }, [page])
+ useEffect(() => {
+ SaveFiltersSearchpage(activeFilters)
+ }, [activeFilters])
+ useEffect(() => {
+ SaveSortOptionSearchpage(activeSorting)
+ }, [activeSorting])
+ useEffect(() => {
+ SaveQuerySearchpage(searchQuery)
+ }, [searchQuery])
+ useEffect(() => {
+ SavePageSearchpage(page)
+ }, [page])
// Loades the games
useEffect(() => {
let cancelled = false
- getGames()
+ GetGames()
.then((loadedGames) => {
if (!cancelled) setGames(loadedGames)
})
@@ -138,29 +162,26 @@ export function SearchPage() {