Skip to content

Commit

Permalink
Load games from Steam
Browse files Browse the repository at this point in the history
  • Loading branch information
grubersjoe committed Jun 5, 2023
1 parent ea62c45 commit 626ccbb
Show file tree
Hide file tree
Showing 26 changed files with 1,307 additions and 1,065 deletions.
1 change: 1 addition & 0 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ VITE_MEASUREMENT_ID=
VITE_MESSAGING_SENDER_ID
VITE_PROJECT_ID=
VITE_PUBLIC_URL=
VITE_STEAM_AUTH_API=
VITE_UNKNOWN_GAME_FIRESTORE_ID=
VITE_USE_EMULATED_FIRESTORE=
VITE_VAPID_KEY-=
65 changes: 33 additions & 32 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "daddel",
"version": "0.14.1",
"author": "Jonathan Gruber <gruberjonathan@gmail.com>",
"version": "0.15.0",
"private": true,
"scripts": {
"build": "tsc && vite build",
Expand All @@ -13,34 +14,34 @@
"test": "jest"
},
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.126",
"@mui/material": "^5.12.0",
"@mui/system": "^5.12.0",
"@mui/x-date-pickers": "^5.0.8",
"@sindresorhus/slugify": "^2.2.0",
"@tanstack/react-query": "^4.29.3",
"@tanstack/react-query-devtools": "^4.29.3",
"axios": "^1.3.5",
"date-fns": "^2.29.3",
"firebase": "^9.19.1",
"firebase-tools": "^11.27.0",
"@mui/lab": "^5.0.0-alpha.132",
"@mui/material": "^5.13.3",
"@mui/system": "^5.13.2",
"@mui/x-date-pickers": "^6.6.0",
"@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-query-devtools": "^4.29.12",
"axios": "^1.4.0",
"date-fns": "^2.30.0",
"firebase": "^9.22.1",
"firebase-tools": "^12.3.0",
"generate-emoji-list": "^1.1.0",
"oidc-react": "^3.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-firebase-hooks": "^4.0.2",
"react-helmet-async": "^1.3.0",
"react-intersection-observer": "^9.4.3",
"react-router-dom": "^6.10.0",
"react-intersection-observer": "^9.4.4",
"react-router-dom": "^6.11.2",
"react-timeago": "^7.1.0",
"typeface-roboto": "^1.1.13"
},
"resolutions": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@types/react": "^18.0.20"
"react-dom": "^18.2.0"
},
"browserslist": {
"production": [
Expand All @@ -55,28 +56,28 @@
]
},
"devDependencies": {
"@babel/preset-env": "^7.21.4",
"@babel/preset-typescript": "^7.21.4",
"@babel/preset-env": "^7.22.4",
"@babel/preset-typescript": "^7.21.5",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.11",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/jest": "^29.5.2",
"@types/node": "^20.2.5",
"@types/react": "^18.2.8",
"@types/react-dom": "^18.2.4",
"@types/react-helmet": "^6.1.6",
"@types/react-router-dom": "^5.3.3",
"@types/react-timeago": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@vitejs/plugin-react": "^3.1.0",
"eslint": "^8.38.0",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.42.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"jest": "^29.5.0",
"jest-localstorage-mock": "^2.4.26",
"prettier": "^2.8.7",
"sass": "^1.62.0",
"prettier": "^2.8.8",
"sass": "^1.62.1",
"serve": "^14.2.0",
"typescript": "^5.0.4",
"vite": "^4.2.1"
"typescript": "^5.1.3",
"vite": "^4.3.9"
}
}
8 changes: 2 additions & 6 deletions src/assets/images/games/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import slugify from '@sindresorhus/slugify';

import { Game } from '../../../types';

export async function getGameBanner(game: Game) {
const fileName = slugify(game.name, { separator: '-' }).concat('.jpg');
export async function getGameBanner(steamAppId: number) {
const fileName = String(steamAppId).concat('.jpg');
const url = new URL(`./out/${fileName}`, import.meta.url);

return url.pathname === '/undefined' ? null : url;
Expand Down
11 changes: 7 additions & 4 deletions src/components/AppBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import FilterIcon from '@mui/icons-material/FilterListRounded';
import {
Badge,
Box,
Container,
IconButton,
AppBar as MuiAppBar,
Toolbar,
Expand All @@ -27,11 +28,13 @@ interface Props {
const AppBar: FunctionComponent<Props> = ({ children, filter, title }) => (
<MuiAppBar position="fixed">
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Toolbar variant="dense">
<Toolbar>
{title && (
<Typography component="h1" variant="h6">
{title}
</Typography>
<Container>
<Typography component="h1" variant="h6">
{title}
</Typography>
</Container>
)}
{children && <Box flexGrow={1}>{children}</Box>}
<div>
Expand Down
39 changes: 39 additions & 0 deletions src/components/Auth/SteamAuthentication.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import FaceIcon from '@mui/icons-material/Face';
import { Box, Button, Chip, CircularProgress } from '@mui/material';
import React from 'react';

import { useSteamUser } from '../../hooks/useSteamUser';
import { signInSteam, signOutFromSteam } from '../../services/auth';

const SteamAuthentication = () => {
const { data: steamUser, isLoading, refetch } = useSteamUser();

return (
<>
{steamUser && (
<Box mb={3}>
<Chip
icon={<FaceIcon />}
label={`Angemeldet als ${steamUser.displayName}`}
variant="filled"
/>
</Box>
)}
<Button
onClick={() =>
steamUser ? signOutFromSteam().then(() => refetch()) : signInSteam()
}
disabled={isLoading}
startIcon={
isLoading ? (
<CircularProgress color="inherit" size={18} thickness={3} />
) : null
}
>
{steamUser ? 'Abmelden' : 'Bei Steam anmelden'}
</Button>
</>
);
};

export default SteamAuthentication;
7 changes: 3 additions & 4 deletions src/components/DateTimePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { TextField } from '@mui/material';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { DateTimePicker as MuiDateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
Expand All @@ -17,9 +16,9 @@ const DateTimePicker: FunctionComponent<Props> = ({ date, onChange }) => (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={deLocale}>
<MuiDateTimePicker
label="Datum und Uhrzeit"
renderInput={props => (
<TextField {...props} variant="outlined" fullWidth required />
)}
slotProps={{
textField: { variant: 'outlined', fullWidth: true, required: true },
}}
value={date}
onChange={onChange}
minDate={new Date()}
Expand Down
47 changes: 20 additions & 27 deletions src/components/Match/FallbackBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
import { Box } from '@mui/material';
import React, { FunctionComponent } from 'react';

import { UNKNOWN_GAME_ID } from '../../constants';
import { Game } from '../../types';

const FallbackBanner: FunctionComponent<{ game: Game }> = ({ game }) => {
const isUnknownGame = game.id === UNKNOWN_GAME_ID;

return (
const FallbackBanner: FunctionComponent<{ game: Game }> = ({ game }) => (
<Box
display="flex"
alignItems="center"
justifyContent="center"
width="100%"
height="100%"
position="absolute"
>
<Box
display="flex"
alignItems="center"
justifyContent="center"
width="100%"
height="100%"
position="absolute"
sx={{
margin: '-28px 1em 0 1em',
textAlign: 'center',
fontWeight: 'bold',
fontSize: 'clamp(1.25rem, 2vw, 1.5rem)',
lineHeight: 1.4,
userSelect: 'none',
}}
>
<Box
sx={{
margin: '-28px 1em 0 1em',
textAlign: 'center',
fontWeight: 'bold',
fontSize: isUnknownGame
? 'clamp(1rem, 20vw, 120px)'
: 'clamp(1.25rem, 2vw, 1.5rem)',
lineHeight: 1.4,
userSelect: 'none',
}}
>
{isUnknownGame ? <span title="Unbekanntes Spiel">?</span> : game.name}
</Box>
{game.name}
</Box>
);
};
</Box>
);

export default FallbackBanner;
2 changes: 1 addition & 1 deletion src/components/Match/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const Filter: FunctionComponent<Props> = ({ filter, setFilter }) => {
disabled={gamesLoading}
filterSelectedOptions
getOptionLabel={option => option.name}
isOptionEqualToValue={(a, b) => a.id === b.id}
isOptionEqualToValue={(a, b) => a.name === b.name}
multiple
loading={gamesLoading}
onChange={(_event, games) => {
Expand Down
52 changes: 52 additions & 0 deletions src/components/Match/GameSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Autocomplete as MuiAutocomplete, TextField } from '@mui/material';
import React, { useState } from 'react';

import { SteamGame, useSteamGames } from '../../hooks/useSteamGames';

interface Props {
defaultValue?: GameOption;
onChange: (game: GameOption | null) => void;
}

export type GameOption = SteamGame | { name: string };

const GameSelect = (props: Props) => {
const { data: games, isLoading: gamesLoading } = useSteamGames();
const [value, setValue] = useState<GameOption | null>(
props.defaultValue ?? null,
);

return (
<MuiAutocomplete<GameOption, false, false, true>
freeSolo
options={games ?? []}
value={value}
onChange={(event, option) => {
const value = typeof option === 'string' ? { name: option } : option;
setValue(value);
props.onChange(value);
}}
onInputChange={(event, newValue) => {
setValue({ name: newValue });
props.onChange({ name: newValue });
}}
getOptionLabel={option => {
if (typeof option === 'string') {
return option;
}

return option.name;
}}
selectOnFocus
handleHomeEndKeys
renderInput={props => (
<TextField {...props} label="Spiel" disabled={gamesLoading} required />
)}
renderOption={(props, option) => <li {...props}>{option.name}</li>}
disabled={gamesLoading}
loading={gamesLoading}
/>
);
};

export default GameSelect;
29 changes: 9 additions & 20 deletions src/components/Match/MatchCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import { useTheme } from '@mui/material/styles';
import { Theme } from '@mui/system';
import endOfDay from 'date-fns/endOfDay';
import isFuture from 'date-fns/isFuture';
import { getDoc } from 'firebase/firestore';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';

import { getGameBanner } from '../../assets/images/games';
import { toggleMatchReaction } from '../../services/reactions';
import { Game, Match, UserMap } from '../../types';
import { Match, UserMap } from '../../types';
import { formatDate, formatTime } from '../../utils/date';
import JoinMatchDialog from '../Dialogs/JoinMatchDialog';
import EmojiPicker from '../EmojiPicker';
Expand Down Expand Up @@ -103,33 +102,23 @@ const MatchCard: FunctionComponent<Props> = ({
setPageMetadata,
}) => {
const sx = styles(useTheme());
const { game } = match;
const [gameBanner, setGameBanner] = useState<URL | null>(null);

const [game, setGame] = useState<Game | null>();
const [gameBanner, setGameBanner] = useState<URL | null>();

// Retrieve the game via reference
useEffect(() => {
getDoc<Game>(match.game).then(game => {
const data = game.data();
setGame(data ? ({ ...data, id: game.id } as Game) : null);
});
}, [match]);

// Then retrieve game banner
useEffect(() => {
if (game) {
getGameBanner(game).then(setGameBanner);
if (game.steamAppId) {
getGameBanner(game.steamAppId).then(setGameBanner);
}
}, [game]);
}, [game.steamAppId]);

const hasBanner = gameBanner !== null;
const hasBanner = Boolean(gameBanner);

// It should be able to join a match until the end of its date
const isJoinable = isFuture(endOfDay(match.date.toDate()));

const handleEmojiClick = (emoji: string) => toggleMatchReaction(match, emoji);

if (!game || gameBanner === undefined) {
if (!game) {
return <MatchCardSkeleton />;
}

Expand All @@ -139,7 +128,7 @@ const MatchCard: FunctionComponent<Props> = ({

<Card sx={sx.card} raised>
<CardMedia
image={hasBanner ? gameBanner.href : undefined}
image={gameBanner?.href}
sx={{
...sx.media,
...(!hasBanner && {
Expand Down
Loading

0 comments on commit 626ccbb

Please sign in to comment.