🔧 refactor(App.js): remove boilerplate code and add new components for game console

🔧 refactor(index.css): change background color to improve UI
🔧 refactor(server.ts): change port variable case and add support for environment variable
🔧 refactor(package.json): add new dependencies for the project
🔧 refactor(.env): add new environment variable for API URL
🔧 refactor(App.js): add new components and states for game console
🔧 refactor(ConsoleCard.js): add new component for displaying game console
🔧 refactor(ConsoleList.js): add new component for listing game consoles
🔧 refactor(GameCard.js): add new component for displaying game
🔧 refactor(GameList.js): add new component for listing games
🔧 refactor(GamePlayer.js): add new component for game player
🔧 refactor(Topbar.js): add new component for topbar
🔧 refactor(ConsolePage.jsx): add new page for console
🔧 refactor(GamePage.jsx): add new page for game
🔧 refactor(HomePage.jsx): add new page for home
🔧 refactor(router.js): add new router for navigation
🔧 refactor(routes.js): add new routes for navigation
🔧 refactor(placeholder.jpg): add new placeholder image for game console and game
This commit is contained in:
Djalim Simaila 2024-03-25 14:32:09 +01:00
parent be7b8bea37
commit 1dcc6f00f5
19 changed files with 1576 additions and 26 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
VIDEOGAME_API_URL=https://videogamedb.simailadjalim.fr

972
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,19 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.5",
"@mui/icons-material": "^5.14.0",
"@mui/material": "^5.14.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.4.0",
"dotenv": "^16.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},

View File

@ -1,25 +1,52 @@
import logo from './logo.svg';
import './App.css';
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import Router from './routes/router';
import ConsoleList from './page_elements/ConsoleList';
import GameList from './page_elements/GameList';
import GamePlayer from './page_elements/GamePlayer';
import Topbar from './page_elements/Topbar';
import * as React from 'react';
var API_URL = process.env.VIDEOGAME_API_URL
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
console.log(API_URL);
const [gameId, setGameId] = React.useState(0);
const [consoleState, setConsoleState] = React.useState({console_id: 0, core: "2600"});
const [current_page, setCurrentPage] = React.useState(0);
const handleConsoleChange = (console_id, core) => {
setConsoleState({console_id: console_id, core: core});
setCurrentPage(1);
}
const handleGameChange = (game_id) => {
setGameId(game_id);
setCurrentPage(2);
}
const handleBack = () => {
setCurrentPage(current_page - 1);
}
let element;
if (current_page === 0) {
element = <ConsoleList handleConsoleChange={handleConsoleChange} />;
} else if (current_page === 1) {
element = <GameList id={consoleState.console_id} handleGameChange={handleGameChange} />;
} else if (current_page === 2) {
element = <GamePlayer id={gameId} console_id={consoleState.console_id} core={consoleState.core} handleBack={handleBack} />;
}
return <>
<Topbar/>
<Router/>
</>
}
export default App;

BIN
src/images/placeholder.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

@ -5,6 +5,7 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #282c34;
}
code {

View File

@ -0,0 +1,53 @@
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import { CardActionArea } from '@mui/material';
import placeholder from '../images/placeholder.jpg';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
export default function ConsoleCard(props) {
const [game_console, setGameConsole] = React.useState(null);
const [console_cores, setConsoleCores] = React.useState(null);
const [hasLoaded, setLoaded] = React.useState(false);
const game_console_id = props.id;
const navigate = useNavigate();
let console_name = "console" + game_console_id;
let console_data = JSON.parse(localStorage.getItem(console_name));
if (!hasLoaded){
if (console_data === null){
axios.get('https://videogamedb.simailadjalim.fr/consoles/' + game_console_id).then((result) => {
localStorage.setItem(console_name,JSON.stringify(result.data));
setGameConsole(result.data["name"]);
setConsoleCores(result.data["core"]);
setLoaded(true);
})
} else {
setGameConsole(console_data["name"]);
setConsoleCores(console_data["core"]);
setLoaded(true);
}
}
return (
<Card style={{margin: "10px"}} onClick={() => navigate("/console/"+game_console_id)} sx={{ width: 250 }}>
<CardActionArea>
<CardMedia
component="img"
height="140"
image={placeholder}
alt="green iguana"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{game_console}
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}

View File

@ -0,0 +1,59 @@
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import ConsoleCard from './ConsoleCard';
import axios from 'axios';
import * as React from 'react';
import { LinearProgress } from '@mui/material';
function ConsoleList(props) {
const [game_consoles, setGameConsoles] = React.useState([]);
const [has_loaded, setHasLoaded] = React.useState(false);
const cached_game_console_ids = []
if (!has_loaded){
let consoles = JSON.parse(localStorage.getItem("consoles"));
if (consoles !== null) {
console.log("loaded cache");
for (const game_console of consoles) {
const card = <ConsoleCard id={game_console["id"]} handleConsoleChange={props.handleConsoleChange} />
cached_game_console_ids.push(card);
}
setGameConsoles(cached_game_console_ids);
setHasLoaded(true);
} else {
console.log("making requests");
axios.get('https://videogamedb.simailadjalim.fr/consoles').then((result) => {
for (const game_console of result.data) {
localStorage.setItem("consoles",JSON.stringify(result.data));
console.log(game_console["id"]);
const card = <ConsoleCard id={game_console["id"]} handleConsoleChange={props.handleConsoleChange} />
cached_game_console_ids.push(card);
}
setGameConsoles(cached_game_console_ids);
setHasLoaded(true);
})
}
}
if (!has_loaded){
return (
<div style={{display: "flex", flexWrap:"wrap", flexDirection:"row", justifyContent:"center"}}>
<LinearProgress />
</div>
);
}
else{
return (
<div style={{display: "flex", flexWrap:"wrap", flexDirection:"row", justifyContent:"center"}}>
{game_consoles}
</div>
);
}
}
export default ConsoleList;

View File

@ -0,0 +1,47 @@
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import { CardActionArea } from '@mui/material';
import placeholder from '../images/placeholder.jpg';
import { useNavigate } from 'react-router-dom';
export default function GameCard(props) {
const game_name = props.name;
const game_id = props.id;
const liste = true;
const navigate = useNavigate();
if (liste) {
return (
<Card sx={{ }} style={{margin:"20px"}} onClick={ ()=> navigate("/console/"+props.console_id+"/"+game_id)}>
<CardActionArea>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{game_name}
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
} else {
return (
<Card sx={{ maxWidth: 150 }} style={{margin:"20px"}}>
<CardActionArea>
<CardMedia
component="img"
height="140"
image={placeholder}
alt="green iguana"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{game_name}
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}
}

View File

@ -0,0 +1,45 @@
import * as React from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import { CardActionArea } from '@mui/material';
import placeholder from '../images/placeholder.jpg';
export default function GameCard(props) {
const game_name = props.name;
const game_id = props.id;
const liste = true;
if (liste) {
return (
<Card sx={{ }} style={{margin:"20px"}} onClick={ ()=> props.handleGameChange(game_id)}>
<CardActionArea>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{game_name}
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
} else {
return (
<Card sx={{ maxWidth: 150 }} style={{margin:"20px"}}>
<CardActionArea>
<CardMedia
component="img"
height="140"
image={placeholder}
alt="green iguana"
/>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{game_name}
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
}
}

View File

@ -0,0 +1,70 @@
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import * as React from 'react';
import { CircularProgress, Popover } from '@mui/material';
import axios from 'axios';
import GameCard from './GameCard';
import { useParams } from 'react-router-dom';
async function loadgamesfromlocalstorage(rom_name,console_id){
let console_rom = JSON.parse(localStorage.getItem(rom_name));
let game_components = []
for (const roms of console_rom) {
const card = <GameCard id={roms["id"]} name={roms["name"]} console_id={console_id} />
game_components.push(card);
}
return game_components;
}
function GameList(props) {
const [game_consoles, setGameConsoles] = React.useState([]);
const [has_loaded, setHasLoaded] = React.useState(false);
const game_components = []
const params = useParams()
let console_id = params.consoleId;
let rom_name = "rom" + props.id;
let console_rom = JSON.parse(localStorage.getItem(rom_name));
if (!has_loaded){
if (console_rom === null){
console.log("asking rom for console" + console_id)
axios.get('https://videogamedb.simailadjalim.fr/consoles/'+ console_id + '/roms')
.then((result) => {
localStorage.setItem(rom_name,JSON.stringify(result.data));
for (const roms of result.data) {
const card = <GameCard id={roms["id"]} name={roms["name"]} console_id={console_id} />
game_components.push(card);
}
setGameConsoles(game_components);
setHasLoaded(true);
})
}else{
console.log("loading roms from cache");
loadgamesfromlocalstorage(rom_name,props.id).then(game_components =>{
setGameConsoles(game_components);
setHasLoaded(true);
});
}
}
if (!has_loaded){
return <>
<br/>
<CircularProgress/>
</>
}
else{
return (
<div style={{display: "flex", flexDirection: "column", flexWrap: "wrap"}}>
<p>Jeux</p>
{game_consoles}
</div>
);
}
}
export default GameList;

View File

@ -0,0 +1,45 @@
import * as React from 'react';
import axios from 'axios';
import Button from '@mui/material/Button';
import { Box } from '@mui/material';
export default function GamePlayer(props) {
const [game, setGame] = React.useState(null);
var game_id = props.id;
var console_core = JSON.parse(localStorage.getItem("console"+props.consoleId)).core;
console.log(console_core);
axios.get('https://videogamedb.simailadjalim.fr/roms/' + game_id).then((result) => {
setGame(result.data["name"]);
})
function handleDownload(){
const fileUrl = 'https://videogamedb.simailadjalim.fr/roms/' + props.id + '?romfile=true';
const link = document.createElement('a');
link.href = fileUrl;
document.body.appendChild(link);
link.click();
// Clean up the temporary URL and remove the <a> element
document.body.removeChild(link);
};
return (
<div style={{display: "flex",flexDirection:"column",alignItems:"center",width:"100%"}}>
<h2>GamePlayer</h2>
<h3>{game}</h3>
<iframe
title="EmulatorJS"
src={"https://videogamedb.simailadjalim.fr/emulator?rom_id=" + game_id + "&console_core=" + console_core}
width="640"
height="480"
allowFullScreen
/>
<Box sx={{
paddingTop: "10px"
}}>
</Box>
<Button variant="contained" onClick={handleDownload}>Télécharger le jeu</Button>
</div>
);
}

View File

@ -0,0 +1,80 @@
import * as React from 'react';
import { styled, alpha } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import InputBase from '@mui/material/InputBase';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'inherit',
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
}));
export default function Topbar() {
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="open drawer"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }}
>
Gros site de retrogaming bien legal la
</Typography>
</Toolbar>
</AppBar>
</Box>
);
}

View File

@ -0,0 +1,79 @@
import * as React from 'react';
import { styled, alpha } from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import InputBase from '@mui/material/InputBase';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'inherit',
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
}));
export default function Topbar() {
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="open drawer"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography
variant="h6"
noWrap
component="div"
sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }}
>
Gros site de retrogaming bien legal la
</Toolbar>
</AppBar>
</Box>
);
}

12
src/pages/ConsolePage.jsx Normal file
View File

@ -0,0 +1,12 @@
import GameList from "../page_elements/GameList";
import {useParams} from 'react-router-dom';
export default function ConsolePage(){
const params = useParams();
return <>
<GameList id={params.consoleId} />
</>
}

11
src/pages/GamePage.jsx Normal file
View File

@ -0,0 +1,11 @@
import GamePlayer from "../page_elements/GamePlayer";
import { useParams } from "react-router-dom";
export default function GamePage() {
const params = useParams();
let gameId = params.gameId;
let consoleId = params.consoleId;
return <GamePlayer id={gameId} consoleId={consoleId} />;
}

16
src/pages/HomePage.jsx Normal file
View File

@ -0,0 +1,16 @@
import ConsoleList from "../page_elements/ConsoleList";
import Typography from '@mui/material/Typography';
export function HomePage(){
return <>
<Typography
variant="h3"
noWrap
component="div"
sx={{ flexGrow: 1, display: { xs: 'none', sm: 'block' } }}
>
Bienvenue sur le site dont tu ne dois pas partager l'existance.
</Typography>
<ConsoleList handleConsoleChange={null} />;
</>
}

20
src/routes/router.js Normal file
View File

@ -0,0 +1,20 @@
import {
createBrowserRouter,
RouterProvider,
Outlet,
} from "react-router-dom";
import { routes } from "./routes";
const router = createBrowserRouter([
{
path: "",
element: <Outlet />, // if we need a layout it's here
children: routes,
},
]);
export default function Router() {
return(
<RouterProvider router={router} />
)
}

18
src/routes/routes.js Normal file
View File

@ -0,0 +1,18 @@
import { HomePage } from "../pages/HomePage";
import ConsolePage from "../pages/ConsolePage";
import GamePage from "../pages/GamePage";
export const routes = [
{
path: "/",
element: <HomePage />,
},
{
path: "console/:consoleId",
element: <ConsolePage/>,
},
{
path: "console/:consoleId/:gameId",
element: <GamePage/>,
},
];