Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • s86116/project_ss23
1 result
Show changes
import Game from "./Game";
type DetailView = {
id: number;
name: string;
cover: { url: string };
summary: string;
}
type DetailViewArray = DetailView[];
export default async function GamesList() {
const res = await fetch("https://api.igdb.com/v4/games", {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Client-ID': `${process.env.IMDB_CLIENT_ID}`,
'Authorization': `${process.env.IMDB_AUTH}`,
},
body: `fields name,cover.*; limit 40; where cover != null;`,
});
const data: DetailViewArray = await res.json()
return (
<div>
Games List Page
{data.map((game: any) => (
<Game key={game.id} id={game.id} name={game.name} cover={game.cover} />
))}
</div>
)
}
import { Inter } from 'next/font/google'
"use client"
const inter = Inter({ subsets: ['latin'] })
import { Container, CssBaseline, ThemeProvider } from "@mui/material"
import { createContext, useState } from "react"
import { QueryClient, QueryClientProvider } from "react-query"
import Header from "../components/Header"
import { Theme } from "./theme"
// metadata for the website
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'GameUnity',
description: 'Soon',
}
// for dark mode global context
export const ColorModeContext = createContext({ toggleColorMode: () => { } });
// this is the root layout for all pages ({children})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const [queryClient] = useState(() => new QueryClient());
const [theme, colorMode] = Theme();
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<QueryClientProvider client={queryClient}>
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={theme}>
<CssBaseline />
<body>
<Container maxWidth={false}>
<Header />
{children}
</Container>
</body>
</ThemeProvider>
</ColorModeContext.Provider>
</QueryClientProvider>
</html>
)
}
// custom super small breakpoint for responsive design
declare module '@mui/material/styles' {
interface BreakpointOverrides {
ss: true;
}
}
\ No newline at end of file
import Link from "next/link";
// renders home page
export default function Home() {
return (
<main>
<div>
Hello World!
<h1>Welcome to GameUnity!</h1>
<p>This will be our Home Page and is still WIP</p>
<Link href="/games">Games List Progress</Link>
</div>
</main>
)
}
}
\ No newline at end of file
import { Theme, createTheme } from "@mui/material";
import { useMemo, useState } from "react";
// this is the main theme for the website
export function Theme(): [Theme, { toggleColorMode: () => void }] {
const [mode, setMode] = useState<'light' | 'dark'>('dark');
const colorMode = useMemo(
() => ({
toggleColorMode: () => {
setMode((prevMode) => (prevMode === 'dark' ? 'light' : 'dark'));
},
}),
[],
);
return [useMemo(() =>
createTheme({
palette: {
mode: mode,
},
breakpoints: {
values: {
xs: 0,
ss: 300,
sm: 600,
md: 900,
lg: 1200,
xl: 1536,
},
},
}),
[mode],
),
colorMode];
}
\ No newline at end of file
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import ExploreIcon from '@mui/icons-material/Explore';
import GroupIcon from '@mui/icons-material/Group';
import HelpIcon from '@mui/icons-material/Help';
import NotificationsIcon from '@mui/icons-material/Notifications';
import PeopleIcon from '@mui/icons-material/People';
import SettingsIcon from '@mui/icons-material/Settings';
import SportsEsportsIcon from '@mui/icons-material/SportsEsports';
import { Box, Button, Hidden, Stack, Typography } from "@mui/material";
import Link from "next/link";
export default function Dashboard() {
const loggedIn = false;
const username = "coolguy123";
return (
<Box sx={{ position: 'sticky', top: 0 }}>
{loggedIn ?
<Stack spacing={2}>
<Link href={`/${username}`}>
<Button variant="text" size="large" startIcon={<AccountCircleIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
My Profile
</Hidden>
</Button>
</Link>
<Link href="/notifications">
<Button variant="text" size="large" startIcon={<NotificationsIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
Notifications
</Hidden>
</Button>
</Link>
<Link href="/friends">
<Button variant="text" size="large" startIcon={<PeopleIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
Friends
</Hidden>
</Button>
</Link>
<Link href="/games">
<Button variant="text" size="large" startIcon={<SportsEsportsIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
Games
</Hidden>
</Button>
</Link>
<Link href="/communities">
<Button variant="text" size="large" startIcon={<GroupIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
Communities
</Hidden>
</Button>
</Link>
<Link href="/blogs">
<Button variant="text" size="large" startIcon={<ExploreIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
Explore
</Hidden>
</Button>
</Link>
<Box height={30} />
<Link href="/settings">
<Button variant="text" size="large" startIcon={<SettingsIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
Settings
</Hidden>
</Button>
</Link>
<Link href="/blogs">
<Button variant="text" size="large" startIcon={<HelpIcon />} sx={{ borderRadius: "999px" }}>
<Hidden lgDown>
Help
</Hidden>
</Button>
</Link>
</Stack>
:
<Stack spacing={2} sx={{ justifyContent: "center", textAlign: "center" }}>
<Link href="/login">
<Button variant="contained" size="large" sx={{ borderRadius: "999px" }}>
Log In
</Button>
</Link>
<Link href="/signup">
<Button variant="outlined" size="large" sx={{ borderRadius: "999px" }}>
Sign Up
</Button>
</Link>
<Typography variant="subtitle1">
Unlock endless possibilities - register or log in to unleash the full potential of our website.
</Typography>
</Stack>
}
</Box>
)
}
\ No newline at end of file
import { Card, CardContent, Typography } from "@mui/material";
import Image from "next/image";
import Link from "next/link";
// this is a single game helper-component, only for design purposes
export default function Game({ id, name, cover }: { id: number, name: string, cover: { url: string } }) {
return (
<Card sx={{ maxWidth: 264 }} variant="outlined" >
<Link href={`/games/${id}`}>
<Image src={cover.url} alt={name} width={264} height={374} priority={true} style={{ width: '100%', height: '100%' }} />
</Link>
<CardContent>
<Typography noWrap={true}>{name}</Typography>
</CardContent>
</Card>
)
}
\ No newline at end of file
import { ColorModeContext } from "@/app/layout";
import Brightness4Icon from '@mui/icons-material/Brightness4';
import Brightness7Icon from '@mui/icons-material/Brightness7';
import { Button, Container, Grid, IconButton, useTheme } from "@mui/material";
import Image from "next/image";
import Link from "next/link";
import { useContext } from "react";
import logoSvg from "../public/logo.svg";
export default function Header() {
const theme = useTheme();
const colorMode = useContext(ColorModeContext);
return (
<Container>
<Grid container spacing={2} height={100} sx={{ alignItems: "center" }}>
<Grid item xs={2}>
<Link href="/">
<Image src={logoSvg} alt="GameUnity" width={50} height={50} priority />
</Link>
</Grid>
<Grid item xs={8} sx={{ justifyContent: "center", textAlign: "center" }}>
<Link href="/games">
<Button variant="text" size="large" sx={{ borderRadius: "999px" }}>
Games
</Button>
</Link>
<Link href="/threads">
<Button variant="text" size="large" sx={{ borderRadius: "999px" }}>
Threads
</Button>
</Link>
<Link href="/communities">
<Button variant="text" size="large" sx={{ borderRadius: "999px" }}>
Communities
</Button>
</Link>
<Link href="/blogs">
<Button variant="text" size="large" sx={{ borderRadius: "999px" }}>
Blog
</Button>
</Link>
</Grid>
<Grid item xs={2} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<IconButton sx={{ ml: 1 }} onClick={colorMode.toggleColorMode} color="inherit">
{theme.palette.mode === 'dark' ? <Brightness7Icon /> : <Brightness4Icon />}
</IconButton>
</Grid>
</Grid>
</Container>
)
}
\ No newline at end of file
import ArrowCircleRightRoundedIcon from '@mui/icons-material/ArrowCircleRightRounded';
import SearchIcon from '@mui/icons-material/Search';
import { Container, IconButton, InputAdornment, TextField } from '@mui/material';
export default function SearchInput() {
const handleSearch = (event: { target: { value: any; }; }) => {
const searchText = event.target.value;
console.log('Search:', searchText);
};
return (
<Container maxWidth="sm" sx={{ justifyContent: "center", textAlign: "center" }}>
<TextField
placeholder="Search"
variant="outlined"
size="small"
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
<IconButton edge="end" aria-label="start search">
<ArrowCircleRightRoundedIcon />
</IconButton>
</InputAdornment>
),
style: {
borderRadius: '999px',
},
}}
/>
</Container>
);
};
\ No newline at end of file
import { Box, Card, CardContent, FormControl, FormHelperText, MenuItem, Select, SelectChangeEvent, Typography } from "@mui/material";
import { useState } from "react";
// this is a single sorting helper-component, only for design purposes
export default function Sort() {
const [select, setSelct] = useState('');
const handleChange = (event: SelectChangeEvent) => {
setSelct(event.target.value);
};
return (
<Box sx={{ position: 'sticky', top: 0 }}>
<Card variant="outlined" >
<CardContent>
<Typography>Filter</Typography>
<FormControl fullWidth>
<FormHelperText>Sorty By</FormHelperText>
<Select
value={select}
onChange={handleChange}
displayEmpty
inputProps={{ 'aria-label': 'Without label' }}
>
<MenuItem value="">
<em>Any</em>
</MenuItem>
<MenuItem value={1}>Rating</MenuItem>
<MenuItem value={2}>Release Date</MenuItem>
</Select>
</FormControl>
</CardContent>
</Card>
</Box>
)
}
\ No newline at end of file
import { IAuth, IGame } from "@/types/types"
import { calculateOffset, getImageURL } from "./utils"
const TWITCH_AUTH_BASE_URL = process.env.TWITCH_AUTH_BASE_URL ?? ''
const IGDB_BASE_URL = process.env.IGDB_BASE_URL ?? ''
const CLIENT_ID = process.env.TWITCH_CLIENT_ID ?? ''
const CLIENT_SECRET = process.env.TWITCH_CLIENT_SECRET ?? ''
const limit = 200
let _auth: IAuth
let _lastUpdate = 0
// fetches a new token if the current one is expired
async function getToken(): Promise<IAuth> {
if (!_auth || Date.now() - _lastUpdate > _auth.expires_in) {
const url = new URL(`${TWITCH_AUTH_BASE_URL}/token`)
url.searchParams.set('client_id', CLIENT_ID)
url.searchParams.set('client_secret', CLIENT_SECRET)
url.searchParams.set('grant_type', 'client_credentials')
const response = await fetch(url, { method: 'POST' })
_auth = await response.json() as IAuth
_lastUpdate = Date.now()
}
return _auth
}
// fetches the top 200 games with a rating of 96 or higher
export async function getGames(page = 1): Promise<IGame[]> {
const auth = await getToken()
const url = new URL(`${IGDB_BASE_URL}/games`)
let offset = calculateOffset(page, limit)
const response = await fetch(url, {
method: 'POST',
headers: {
'Client-ID': CLIENT_ID,
'Authorization': `Bearer ${auth.access_token}`
},
body: `fields name, cover.*; limit ${limit}; offset ${offset};
sort total_rating desc; where total_rating_count > 2
& cover != null & total_rating != null & rating != null;`
})
const games = await response.json() as IGame[]
games.forEach(game => {
game.cover.url = getImageURL(game.cover.image_id, 'cover_big')
})
return games
}
// fetches a single game by id
export async function getGame(id: number): Promise<IGame[]> {
const auth = await getToken()
const url = new URL(`${IGDB_BASE_URL}/games`)
const response = await fetch(url, {
method: 'POST',
headers: {
'Client-ID': CLIENT_ID,
'Authorization': `Bearer ${auth.access_token}`
},
body: `fields name, cover.*, summary; where cover != null; where id = ${id};`
})
const games = await response.json() as IGame[]
games.forEach(game => {
game.cover.url = getImageURL(game.cover.image_id, 'cover_big')
})
return games
}
\ No newline at end of file
const IGDB_IMG_BASE_URL = process.env.IGDB_IMG_BASE_URL ?? ''
// changes the default size of the image to be fetched
export function getImageURL(hashId: string, size: string): string {
return `${IGDB_IMG_BASE_URL}/t_${size}_2x/${hashId}.jpg`
}
// returns the base url for the current environment, even considering current port
export function getBaseURL(): string {
return process.env.NODE_ENV === 'production'
? process.env.PROD_URL ?? ''
: (typeof window !== 'undefined'
? `http://${window.location.hostname}:${window.location.port}`
: 'http://localhost:3000')
}
// calculates the offset for the query
export function calculateOffset(page: number, limit: number): number {
return (page - 1) * limit
}
\ No newline at end of file
This diff is collapsed.
......@@ -9,21 +9,24 @@
"lint": "next lint"
},
"dependencies": {
"@clerk/nextjs": "^4.17.1",
"@clerk/nextjs": "^4.18.2",
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.12.3",
"@types/node": "20.1.0",
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.13.1",
"@types/node": "20.2.1",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"eslint": "8.40.0",
"eslint-config-next": "13.4.1",
"next": "13.4.1",
"eslint": "8.41.0",
"eslint-config-next": "13.4.3",
"next": "13.4.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-infinite-scroll-component": "^6.1.0",
"react-query": "^3.39.3",
"typescript": "5.0.4"
},
"devDependencies": {
"prisma": "^4.13.0"
"prisma": "^4.14.1"
}
}
}
\ No newline at end of file
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.2756 22.8622H9.13782V30C9.13782 31.1046 10.0332 32 11.1378 32H16.2756C17.3802 32 18.2756 31.1046 18.2756 30V22.8622Z" fill="black"/>
<path d="M18.2934 22.8622L13.7067 18.2934L9.13782 22.8622H18.2934Z" fill="black"/>
<path d="M20.8444 0H15.7067C14.6021 0 13.7067 0.895431 13.7067 2V9.13785H22.8444V2C22.8444 0.89543 21.949 0 20.8444 0Z" fill="black"/>
<path d="M13.7067 9.15555L18.2933 13.7245L22.8622 9.15555H13.7067Z" fill="black"/>
<path d="M9.13776 9.15555H2C0.89543 9.15555 0 10.051 0 11.1555V16.2934C0 17.398 0.895432 18.2934 2 18.2934H9.13776V9.15555Z" fill="#8E4CC2"/>
<path d="M9.13782 18.2934L13.7067 13.7245L9.13782 9.15555V18.2934Z" fill="#8E4CC2"/>
<path d="M29.9999 13.7245H22.8622V22.8622H29.9999C31.1045 22.8622 31.9999 21.9667 31.9999 20.8622V15.7245C31.9999 14.6199 31.1045 13.7245 29.9999 13.7245Z" fill="black"/>
<path d="M22.8623 13.7245L18.2933 18.2934L22.8623 22.8622V13.7245Z" fill="black"/>
</svg>
export enum EAgeRatingCategory {
'ESRB' = 0,
'PEGI' = 2,
'CERO' = 3,
'USK' = 4,
'GRAC' = 5,
'CLASS_IND' = 6,
'ACB' = 7,
}
export enum EAgeRatingRating {
'Three' = 1,
'Seven' = 2,
'Twelve' = 3,
'Sixteen' = 4,
'Eighteen' = 5,
'RP' = 6,
'EC' = 7,
'E' = 8,
'E10' = 9,
'T' = 10,
'M' = 11,
'AO' = 12,
'CERO_A' = 13,
'CERO_B' = 14,
'CERO_C' = 15,
'CERO_D' = 16,
'CERO_Z' = 17,
'USK_0' = 18,
'USK_6' = 19,
'USK_12' = 20,
'USK_16' = 21,
'USK_18' = 22,
'GRAC_ALL' = 23,
'GRAC_Twelve' = 24,
'GRAC_Fifteen' = 25,
'GRAC_Eighteen' = 26,
'GRAC_TESTING' = 27,
'CLASS_IND_L' = 28,
'CLASS_IND_Ten' = 29,
'CLASS_IND_Twelve' = 30,
'CLASS_IND_Fourteen' = 31,
'CLASS_IND_Sixteen' = 32,
'CLASS_IND_Eighteen' = 33,
'ACB_G' = 34,
'ACB_PG' = 35,
'ACB_M' = 36,
'ACB_MA15' = 37,
'ACB_R18' = 38,
'ACB_RC' = 39,
}
export enum EGameCategory {
'main_game' = 0,
'dlc_addon' = 1,
'expansion' = 2,
'bundle' = 3,
'standalone_expansion' = 4,
'mod' = 5,
'episode' = 6,
'season' = 7,
'remake' = 8,
'remaster' = 9,
'expanded_game' = 10,
'port' = 11,
'fork' = 12,
'pack' = 13,
'update' = 14,
}
export enum EGameStatus {
'released' = 0,
'alpha' = 2,
'beta' = 3,
'early_access' = 4,
'offline' = 5,
'cancelled' = 6,
'rumored' = 7,
'delisted' = 8,
}
export interface IAuth {
access_token: string;
expires_in: number;
token_type: 'bearer';
}
export interface IGame {
id: number;
age_ratings: EAgeRatingCategory[];
aggregrated_rating: number;
aggregrated_rating_count: number;
alternative_names: number[];
artworks: number[];
bundles: number[];
category: EGameCategory;
checksum: string;
collection: number;
cover: ICover;
created_at: number;
dlcs: number[];
expanded_games: number[];
expansions: number[];
external_games: number[];
first_release_date: number;
follows: number;
forks: number[];
franchise: number;
franchises: number[];
game_engines: number[];
game_localizations: number[];
game_modes: number[];
genres: number[];
hypes: number;
involved_companies: number[];
keywords: number[];
language_supports: number[];
multiplayer_modes: number[];
name: string;
parent_game: string;
platforms: number[];
player_perspectives: number[];
ports: number[];
rating: number;
rating_count: number;
release_dates: number[];
remakes: number[];
remasters: number[];
screenshots: number[];
similar_games: number[];
slug: string;
standalone_expansions: number[];
status: EGameStatus;
storyline: string;
summary: string;
tags: number[];
themes: number[];
total_rating: number;
total_rating_count: number;
updated_at: number;
url: string;
version_parent: number;
version_title: string;
videos: number[];
websites: number[];
}
export interface ICover {
id: number;
alpha_channel: boolean;
animated: boolean;
checksum: string;
game: number;
game_localization: number;
height: number;
image_id: string;
url: string;
width: number;
}
\ No newline at end of file