Merge branch 'react-19.2.8' into main

React 19 + Chakra UI v3 + router v7 migration with v2 look restoration:
deps, theme system, forms/selects, nav and dashboard chrome, details,
auth screens, dashboard hover fix, menu hover treatment, double-chevron
pagination.
This commit is contained in:
2026-09-06 12:28:07 -04:00
69 changed files with 2086 additions and 997 deletions
BIN
View File
Binary file not shown.
+31 -35
View File
@@ -3,31 +3,27 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@chakra-ui/icons": "^2.1.1", "@chakra-ui/react": "^3.37.0",
"@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.14.0",
"@emotion/react": "^11.13.3", "@hookstate/core": "^4.0.2",
"@emotion/styled": "^11.13.0",
"@hookstate/core": "^4.0.1",
"@hookstate/localstored": "^4.0.2", "@hookstate/localstored": "^4.0.2",
"axios": "1.7.5", "axios": "^1.20.0",
"bson-objectid": "^2.0.4", "bson-objectid": "^2.0.4",
"chakra-react-select": "^4.9.1", "chakra-react-select": "^6.1.3",
"chakra-ui-contextmenu": "^1.0.5", "interweave": "^13.1.1",
"framer-motion": "^11.3.30", "jodit-react": "^5.5.3",
"interweave": "^13.1.0", "js-base64": "^3.9.3",
"jodit-react": "^4.1.2", "next-themes": "^0.4.6",
"js-base64": "^3.7.7", "react": "^19.2.8",
"react": "^18.3.1", "react-dom": "^19.2.8",
"react-device-detect": "^2.2.3", "react-hook-form": "^7.87.0",
"react-dom": "^18.3.1", "react-icons": "^5.7.0",
"react-hook-form": "^7.53.0",
"react-icons": "^5.3.0",
"react-rating": "^2.0.5", "react-rating": "^2.0.5",
"react-responsive-carousel": "^3.2.23", "react-responsive-carousel": "^3.2.23",
"react-router-dom": "^6.26.1", "react-router-dom": "^7.18.3",
"react-select": "^5.8.0", "react-select": "^5.10.2",
"react-toastify": "^10.0.5", "react-toastify": "^11.1.0",
"web-vitals": "^4.2.3" "web-vitals": "^6.2.1"
}, },
"scripts": { "scripts": {
"start": "bunx --bun vite", "start": "bunx --bun vite",
@@ -42,8 +38,8 @@
] ]
}, },
"resolutions": { "resolutions": {
"@types/react": "18.2.21", "@types/react": "19.2.18",
"@types/react-dom": "18.2.7" "@types/react-dom": "19.2.7"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -58,18 +54,18 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@chakra-ui/cli": "^2.4.1", "@chakra-ui/cli": "^3.37.0",
"@hookstate/devtools": "^4.0.1", "@hookstate/devtools": "^4.0.3",
"@testing-library/jest-dom": "^6.5.0", "@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.3.3",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.6.7",
"@types/jest": "^29.5.12", "@types/jest": "^30.0.0",
"@types/node": "^22.5.1", "@types/node": "^26.4.1",
"@types/react": "^18.2.21", "@types/react": "^19.2.18",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^19.2.7",
"@types/react-table": "^7.7.20", "@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^6.1.1",
"typescript": "^5.5.4", "typescript": "^5.9.3",
"vite": "^5.4.2" "vite": "^8.2.2"
} }
} }
+13 -5
View File
@@ -1,9 +1,17 @@
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import App from './App'; import { MemoryRouter } from 'react-router-dom';
import { Provider } from './components/ui/provider';
import Home from './components/Home';
test('renders learn react link', () => { test('renders welcome message', () => {
render(<App />); render(
const linkElement = screen.getByText(/learn react/i); <Provider>
expect(linkElement).toBeInTheDocument(); <MemoryRouter>
<Home />
</MemoryRouter>
</Provider>
);
const heading = screen.getByText(/Welcome to your Games Database/i);
expect(heading).toBeInTheDocument();
}); });
+5 -2
View File
@@ -23,7 +23,8 @@ import ForgotPassword from './components/authComponents/ForgotPassword'
import ResetPassword from './components/authComponents/ResetPassword' import ResetPassword from './components/authComponents/ResetPassword'
import UserProfile from './components/userComponents/UserProfile' import UserProfile from './components/userComponents/UserProfile'
import EditUser from './components/userComponents/EditUser' import EditUser from './components/userComponents/EditUser'
import {Box, useColorMode} from "@chakra-ui/react"; import {Box} from "@chakra-ui/react";
import {useColorMode} from "./components/ui/color-mode";
import globalRouter from "./api/globalRouter"; import globalRouter from "./api/globalRouter";
function App() { function App() {
@@ -44,8 +45,10 @@ function App() {
if (!appLoad) <LoadingModal /> if (!appLoad) <LoadingModal />
useEffect(() => { useEffect(() => {
if(loadedPreferences.get().theme !== colorMode) { const preferredTheme = loadedPreferences.get().theme
if(preferredTheme && preferredTheme !== colorMode) {
localStorage.removeItem('chakra-ui-color-mode') localStorage.removeItem('chakra-ui-color-mode')
localStorage.removeItem('theme')
toggleColorMode() toggleColorMode()
} }
}, [loadedPreferences.get().theme]); }, [loadedPreferences.get().theme]);
+12 -14
View File
@@ -1,7 +1,8 @@
import React from 'react' import React from 'react'
import { useHookstate } from '@hookstate/core' import { useHookstate } from '@hookstate/core'
import { gameDashboardState } from '../stateManagement/gameState' import { gameDashboardState } from '../stateManagement/gameState'
import { Flex, Stack, HStack, Link, Spacer, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react' import { Flex, Stack, HStack, Link, Spacer } from '@chakra-ui/react'
import {Tag} from './ui/tag'
import getWithFilters from '../componentUtils/getWithFilters' import getWithFilters from '../componentUtils/getWithFilters'
import { import {
loadingState, loadingState,
@@ -53,7 +54,7 @@ const ActiveFilters = () => {
}} direction={{ }} direction={{
base: 'column', base: 'column',
md: 'row' md: 'row'
}} spacing={4}> }} gap={4}>
{searchParams.length >= 1 && ( {searchParams.length >= 1 && (
<> <>
<h2>Search: </h2> <h2>Search: </h2>
@@ -61,11 +62,8 @@ const ActiveFilters = () => {
size={'md'} size={'md'}
borderRadius="full" borderRadius="full"
variant="solid" variant="solid"
colorScheme="blue" colorPalette="blue"
> onClose={async () => {
<TagLabel><Link
onMouseDown={() => setSearchModalOpen.set(true)}>{searchParams}</Link></TagLabel>
<TagCloseButton onMouseDown={async () => {
const newSearchParams = "" const newSearchParams = ""
ChangeActiveFilterHeader(filters, newSearchParams, setShowFiltersModuleState) ChangeActiveFilterHeader(filters, newSearchParams, setShowFiltersModuleState)
searchComplete.set(false) searchComplete.set(false)
@@ -80,7 +78,9 @@ const ActiveFilters = () => {
useRatingState, useRatingState,
useSteamRatingState, useSteamRatingState,
) )
}} /> }}
><Link
onMouseDown={() => setSearchModalOpen.set(true)}>{searchParams}</Link>
</Tag> </Tag>
</> </>
)} )}
@@ -99,22 +99,20 @@ const ActiveFilters = () => {
<> <>
<HStack key={num}> <HStack key={num}>
<h3 style={{ textTransform: 'capitalize' }}>{filter}</h3> <h3 style={{ textTransform: 'capitalize' }}>{filter}</h3>
<HStack spacing={4}> <HStack gap={4}>
{filters[filter as keyof typeof filters].split(',').map((item: string, index) => ( {filters[filter as keyof typeof filters].split(',').map((item: string, index) => (
<Tag <Tag
size={'md'} size={'md'}
key={index} key={index}
borderRadius="full" borderRadius="full"
variant="solid" variant="solid"
colorScheme="blue" colorPalette="blue"
> onClose={() => removeFilters(filter, item)}
<TagLabel>{ >{
item.includes('lte') item.includes('lte')
? item.replace('lte', '<= ') ? item.replace('lte', '<= ')
: item.includes('gte') ? item.replace('gte', '>= ') : item.includes('gte') ? item.replace('gte', '>= ')
: item} : item}
</TagLabel>
<TagCloseButton onMouseDown={() => removeFilters(filter, item)} />
</Tag> </Tag>
))} ))}
</HStack> </HStack>
+26 -33
View File
@@ -3,13 +3,9 @@ import agent from '../api/agent'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Data } from '../models/game' import { Data } from '../models/game'
import { import {
AlertDialog, Button, Text
AlertDialogBody,
AlertDialogContent, AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button, Icon, Text
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot} from './ui/dialog'
type Props = { type Props = {
id: string id: string
@@ -44,34 +40,31 @@ export default function DeleteDialogue({
} }
} }
const cancelRef = useRef(null) const cancelRef = useRef<HTMLButtonElement | null>(null)
return ( return (
<AlertDialog isOpen={open} leastDestructiveRef={cancelRef} onClose={handleClose}> <DialogRoot open={open} onOpenChange={(e) => { if (!e.open) handleClose() }} role="alertdialog" initialFocusEl={() => cancelRef.current}>
<AlertDialogOverlay> <DialogContent>
<AlertDialogContent> <DialogHeader fontSize='lg' fontWeight='bold'>Delete?</DialogHeader>
<DialogBody>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>Delete?</AlertDialogHeader> Are you sure you would like to delete <strong>{game.title}</strong>?
<AlertDialogBody> </DialogBody>
Are you sure you would like to delete <strong>{game.title}</strong>? <DialogFooter>
</AlertDialogBody> <Button ref={cancelRef} onMouseDown={handleClose}>
<AlertDialogFooter> <Text mt='1'>No</Text>
<Button ref={cancelRef} onMouseDown={handleClose}> </Button>
<Icon name='remove' mr='1' /> <Text mt='1'>No</Text> <Button
</Button> colorPalette='red'
<Button onMouseDown={() => {
colorScheme='red' deleteGame(id)
onMouseDown={() => { }}
deleteGame(id) ml={3}
}} autoFocus
ml={3} >
autoFocus <Text mt='1'>Yes</Text>
> </Button>
<Icon name='checkmark' mr='1' /> <Text mt='1'>Yes</Text> </DialogFooter>
</Button> </DialogContent>
</AlertDialogFooter> </DialogRoot>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
) )
} }
+54 -60
View File
@@ -1,17 +1,12 @@
import React, {MutableRefObject, useEffect} from 'react' import React, {MutableRefObject, useEffect} from 'react'
import { import {
Center, Center,
Divider, Separator,
Drawer, Flex,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay, Flex,
Heading, Spacer, Heading, Spacer,
Text, Text,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {DrawerBody, DrawerCloseTrigger, DrawerContent, DrawerFooter, DrawerHeader, DrawerRoot} from './ui/drawer'
import AdminMode from './authComponents/AdminMode' import AdminMode from './authComponents/AdminMode'
import {StateMethods, useHookstate} from '@hookstate/core' import {StateMethods, useHookstate} from '@hookstate/core'
import {tagState} from '../stateManagement/tagState' import {tagState} from '../stateManagement/tagState'
@@ -25,7 +20,7 @@ import {
} from '../componentUtils/SelectValues' } from '../componentUtils/SelectValues'
import {retrieveTags} from '../componentUtils/retrieveTags' import {retrieveTags} from '../componentUtils/retrieveTags'
import {gameDashboardState} from '../stateManagement/gameState' import {gameDashboardState} from '../stateManagement/gameState'
import {Select} from 'chakra-react-select' import {Select} from './FixedSelect'
import getWithFilters from '../componentUtils/getWithFilters' import getWithFilters from '../componentUtils/getWithFilters'
import {loadingState, showFiltersModuleState} from '../stateManagement/loadingState' import {loadingState, showFiltersModuleState} from '../stateManagement/loadingState'
import {adminMode} from '../stateManagement/userState' import {adminMode} from '../stateManagement/userState'
@@ -99,26 +94,25 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
} }
return ( return (
<Drawer <DrawerRoot
isOpen={filterIsOpen} open={filterIsOpen}
placement="right" placement="end"
onClose={filterOnClose} onOpenChange={(e) => { if (!e.open) filterOnClose() }}
finalFocusRef={btnRef} finalFocusEl={() => btnRef.current as unknown as HTMLElement}
> >
<DrawerOverlay/>
<DrawerContent> <DrawerContent>
<DrawerCloseButton/> <DrawerCloseTrigger/>
<DrawerHeader> <DrawerHeader>
<Heading as={'h3'}>Filters</Heading> <Heading as={'h3'}>Filters</Heading>
</DrawerHeader> </DrawerHeader>
<DrawerBody> <DrawerBody>
<AdminMode/> <AdminMode/>
<Divider my={'3'}/> <Separator borderColor="gray.200" my={'3'}/>
<Center> <Center>
<ResetFilterButton/> <ResetFilterButton/>
</Center> </Center>
<Divider my={'3'}/> <Separator borderColor="gray.200" my={'3'}/>
<Text mb={'1'} fontSize={'xl'}>Series</Text> <Text mb={'1'} fontSize={'xl'}>Series</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -126,13 +120,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="series" name="series"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("series", value) changeFilters("series", value)
}} }}
options={tags.series} options={tags.series}
/> />
<Divider my={'3'}/> <Separator borderColor="gray.200" my={'3'}/>
<Flex mb={'1'}> <Flex mb={'1'}>
<Text mb={'1'} fontSize={'xl'}>Rating</Text> <Text mb={'1'} fontSize={'xl'}>Rating</Text>
<Spacer /> <Spacer />
@@ -140,13 +134,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
size={'sm'} size={'sm'}
defaultValue={ratingToSelectDefaultValue(useRatingState)} defaultValue={ratingToSelectDefaultValue(useRatingState)}
name="rating" name="rating"
onChange={(event) => { onChange={(event: any) => {
if (event === null) return if (event === null) return
setRatingState.set(event.value) setRatingState.set(event.value)
}} }}
options={ratingStateValues} options={ratingStateValues}
chakraStyles={{ chakraStyles={{
container: (provided) => ({ container: (provided: any) => ({
...provided, ...provided,
width: "75px" width: "75px"
}) })
@@ -158,13 +152,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
defaultValue={filterToSelectDefaultValue("rating")} defaultValue={filterToSelectDefaultValue("rating")}
isClearable isClearable
name="rating" name="rating"
onChange={(event) => { onChange={(event: any) => {
if(event === null) return changeFilters("rating", "") if(event === null) return changeFilters("rating", "")
changeFilters("rating", event.value) changeFilters("rating", event.value)
}} }}
options={ratingValues} options={ratingValues}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Flex mb={'1'}> <Flex mb={'1'}>
<Text mb={'1'} fontSize={'xl'}>Steam Rating</Text> <Text mb={'1'} fontSize={'xl'}>Steam Rating</Text>
<Spacer /> <Spacer />
@@ -172,13 +166,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
size={'sm'} size={'sm'}
defaultValue={ratingToSelectDefaultValue(useSteamRatingState)} defaultValue={ratingToSelectDefaultValue(useSteamRatingState)}
name="steamRating" name="steamRating"
onChange={(event) => { onChange={(event: any) => {
if (event === null) return if (event === null) return
setSteamRatingState.set(event.value) setSteamRatingState.set(event.value)
}} }}
options={ratingStateValues} options={ratingStateValues}
chakraStyles={{ chakraStyles={{
container: (provided) => ({ container: (provided: any) => ({
...provided, ...provided,
width: "75px" width: "75px"
}) })
@@ -190,13 +184,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
defaultValue={filterToSelectDefaultValue("steamRating")} defaultValue={filterToSelectDefaultValue("steamRating")}
isClearable isClearable
name="steamRating" name="steamRating"
onChange={(event) => { onChange={(event: any) => {
if(event === null) return changeFilters("steamRating", "") if(event === null) return changeFilters("steamRating", "")
changeFilters("steamRating", event.value) changeFilters("steamRating", event.value)
}} }}
options={ratingValues} options={ratingValues}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Play Status</Text> <Text mb={'1'} fontSize={'xl'}>Play Status</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -204,13 +198,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="playStatus" name="playStatus"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("playStatus", value) changeFilters("playStatus", value)
}} }}
options={playStatusValues} options={playStatusValues}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Genres</Text> <Text mb={'1'} fontSize={'xl'}>Genres</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -218,13 +212,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="genre" name="genre"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("genre", value) changeFilters("genre", value)
}} }}
options={tags.genres} options={tags.genres}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Operating System</Text> <Text mb={'1'} fontSize={'xl'}>Operating System</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -232,13 +226,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="os" name="os"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("os", value) changeFilters("os", value)
}} }}
options={osValues} options={osValues}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Store</Text> <Text mb={'1'} fontSize={'xl'}>Store</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -246,13 +240,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="store" name="store"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("store", value) changeFilters("store", value)
}} }}
options={tags.store} options={tags.store}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Controller Support</Text> <Text mb={'1'} fontSize={'xl'}>Controller Support</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -260,13 +254,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="controller" name="controller"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("controller", value) changeFilters("controller", value)
}} }}
options={controllerValues} options={controllerValues}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Developer</Text> <Text mb={'1'} fontSize={'xl'}>Developer</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -274,13 +268,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="developer" name="developer"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("developer", value) changeFilters("developer", value)
}} }}
options={tags.developer} options={tags.developer}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Publisher</Text> <Text mb={'1'} fontSize={'xl'}>Publisher</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -288,13 +282,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="publisher" name="publisher"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("publisher", value) changeFilters("publisher", value)
}} }}
options={tags.publisher} options={tags.publisher}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Do you own the Soundtrack?</Text> <Text mb={'1'} fontSize={'xl'}>Do you own the Soundtrack?</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -302,13 +296,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="soundtrack" name="soundtrack"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("soundtrack", value) changeFilters("soundtrack", value)
}} }}
options={yesNo} options={yesNo}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Does in work on Intel?</Text> <Text mb={'1'} fontSize={'xl'}>Does in work on Intel?</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -316,13 +310,13 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="intel" name="intel"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("intel", value) changeFilters("intel", value)
}} }}
options={wineIntelValues} options={wineIntelValues}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
<Text mb={'1'} fontSize={'xl'}>Does is work with Wine?</Text> <Text mb={'1'} fontSize={'xl'}>Does is work with Wine?</Text>
<Select <Select
size={'sm'} size={'sm'}
@@ -330,20 +324,20 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
isClearable isClearable
isMulti isMulti
name="wine" name="wine"
onChange={(event) => { onChange={(event: any) => {
const value = event.map(a => a.value).join(',') const value = event.map((a: any) => a.value).join(',')
changeFilters("wine", value) changeFilters("wine", value)
}} }}
options={wineIntelValues} options={wineIntelValues}
/> />
<Divider my={'1'}/> <Separator borderColor="gray.200" my={'1'}/>
</DrawerBody> </DrawerBody>
<DrawerFooter> <DrawerFooter>
</DrawerFooter> </DrawerFooter>
</DrawerContent> </DrawerContent>
</Drawer> </DrawerRoot>
) )
} }
+140
View File
@@ -0,0 +1,140 @@
import React from 'react'
import { Select as V6Select, CreatableSelect as V6Creatable } from 'chakra-react-select'
import { useColorModeValue } from './ui/color-mode'
// Shared styling that restores how these selects looked with Chakra v2 /
// chakra-react-select v4 (verified against the v4.9.1 source + old screenshots):
//
// - v4's control border resolved to the outer wrapper's gray.500 edge via
// `borderColor: inherit` (outer Box sets gray.500). v6 paints its own light
// edge (gray.100 in dark mode) on top, producing a double white+gray border.
// Fix universally: single gray.500 border on the control itself, transparent
// bg so wrapper bg shows through, no shadow, no padding so the chevron zone
// sits flush (v4 had padding:0 + overflow:hidden).
// - v4's chevron sat on a grey input-addon zone (gray.100 / whiteAlpha.300),
// flush right with rounded right corners matching the control.
// - v4's indicator separator was a subtle Divider, effectively invisible in
// the old screenshots; v6 hides it by default, so keep it hidden instead of
// rendering a prominent white line.
// - v4 multi-value pills defaulted to gray subtle tags (dark gray pill in dark
// mode), not bright blue. Defaults restored here; ActiveFilters tags pass
// blue explicitly and are unaffected.
function V6IndicatorSeparator() {
return null
}
function useSharedChakraStyles(extra: any) {
const zoneBg = useColorModeValue('gray.100', 'whiteAlpha.300')
const pillBg = useColorModeValue('gray.100', 'whiteAlpha.300')
const pillColor = useColorModeValue('gray.800', 'gray.100')
const optionHoverBg = useColorModeValue('gray.200', 'whiteAlpha.300')
return {
control: (provided: any) => ({
...provided,
borderColor: 'gray.500',
borderWidth: '1px',
borderStyle: 'solid',
background: 'transparent',
boxShadow: 'none',
padding: 0,
overflow: 'hidden',
}),
valueContainer: (provided: any, state: any) => ({
...provided,
paddingTop: '2px',
paddingBottom: '2px',
paddingLeft: state?.selectProps?.size === 'sm' ? '12px' : '16px',
paddingRight: '8px',
}),
indicatorsContainer: (provided: any) => ({
...provided,
padding: 0,
margin: 0,
}),
dropdownIndicator: (provided: any) => ({
...provided,
background: zoneBg,
alignSelf: 'stretch',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 0,
paddingLeft: '8px',
paddingRight: '8px',
borderWidth: 0,
borderRadius: 0,
borderTopRightRadius: 'md',
borderBottomRightRadius: 'md',
}),
option: (provided: any, state: any) => {
const size = state?.selectProps?.size === 'sm' ? 'sm' : state?.selectProps?.size === 'lg' ? 'lg' : 'md'
const v4 = {
sm: { fontSize: '14px', paddingLeft: '12.8px', paddingRight: '12.8px', paddingTop: '4.8px', paddingBottom: '4.8px' },
md: { fontSize: '16px', paddingLeft: '12.8px', paddingRight: '12.8px', paddingTop: '6.4px', paddingBottom: '6.4px' },
lg: { fontSize: '18px', paddingLeft: '16px', paddingRight: '16px', paddingTop: '8px', paddingBottom: '8px' },
}[size]
return {
...provided,
fontSize: v4.fontSize,
paddingLeft: v4.paddingLeft,
paddingRight: v4.paddingRight,
paddingTop: v4.paddingTop,
paddingBottom: v4.paddingBottom,
// v3 hover uses bg.emphasized/60 — near-invisible on dark surfaces.
// Brighten unselected hover in both modes; keep selected blue.
_highlighted: state?.isSelected
? provided._highlighted
: { bg: optionHoverBg },
}
},
multiValue: (provided: any) => ({
...provided,
background: pillBg,
color: pillColor,
borderRadius: 'md',
margin: '2px',
}),
multiValueLabel: (provided: any) => ({
...provided,
color: pillColor,
}),
multiValueRemove: (provided: any) => ({
...provided,
color: pillColor,
}),
...extra,
}
}
function mergeComponents(extra: any) {
return { IndicatorSeparator: V6IndicatorSeparator, ...extra }
}
export function Select(props: any) {
const { chakraStyles, components, tagColorPalette = 'gray', tagVariant = 'subtle', ...rest } = props
const mergedStyles = useSharedChakraStyles(chakraStyles)
return (
<V6Select
{...rest}
tagColorPalette={tagColorPalette}
tagVariant={tagVariant}
components={mergeComponents(components)}
chakraStyles={mergedStyles}
/>
)
}
export function CreatableSelect(props: any) {
const { chakraStyles, components, tagColorPalette = 'gray', tagVariant = 'subtle', ...rest } = props
const mergedStyles = useSharedChakraStyles(chakraStyles)
return (
<V6Creatable
{...rest}
tagColorPalette={tagColorPalette}
tagVariant={tagVariant}
components={mergeComponents(components)}
chakraStyles={mergedStyles}
/>
)
}
+1 -1
View File
@@ -6,7 +6,7 @@ export default function Footer() {
return ( return (
<Box as='footer' role='contentinfo' mx='auto' maxW='7xl' py='12' px={{ base: '4', md: '8' }}> <Box as='footer' role='contentinfo' mx='auto' maxW='7xl' py='12' px={{ base: '4', md: '8' }}>
<Stack> <Stack>
<Stack direction="row" spacing="4" align="center" justify="space-between"> <Stack direction="row" gap="4" align="center" justify="space-between">
</Stack> </Stack>
+20 -19
View File
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import agent from '../api/agent' import agent from '../api/agent'
import { useParams } from 'react-router' import { useParams } from 'react-router-dom'
import { import {
Box, Box,
Button, Button,
@@ -12,10 +12,9 @@ import {
Spacer, Spacer,
Stack, Stack,
Text, Text,
useColorModeValue,
useDisclosure,
useMediaQuery,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {useColorModeValue} from './ui/color-mode'
import {useDisclosure, useMediaQuery} from '@chakra-ui/react'
import ImageSlider from './detailsComponents/ImageSlider' import ImageSlider from './detailsComponents/ImageSlider'
import Summary from './detailsComponents/Summary' import Summary from './detailsComponents/Summary'
import Features from './detailsComponents/Features' import Features from './detailsComponents/Features'
@@ -26,7 +25,7 @@ import { loadingState } from '../stateManagement/loadingState'
import { ImmutableObject, useHookstate } from '@hookstate/core' import { ImmutableObject, useHookstate } from '@hookstate/core'
import LoadingModal from './LoadingModal' import LoadingModal from './LoadingModal'
import SteamAndDeckLogo from './helperComponents/SteamAndDeckLogo' import SteamAndDeckLogo from './helperComponents/SteamAndDeckLogo'
import { ExternalLinkIcon } from '@chakra-ui/icons' import { LuExternalLink } from 'react-icons/lu'
import { Data } from '../models/game' import { Data } from '../models/game'
import SummaryModal from './detailsComponents/SummaryModal' import SummaryModal from './detailsComponents/SummaryModal'
import SystemRequirementsModal from './detailsComponents/SystemRequirementsModal' import SystemRequirementsModal from './detailsComponents/SystemRequirementsModal'
@@ -38,14 +37,14 @@ const GameDetails = () => {
const loaded = loadedState.get() const loaded = loadedState.get()
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const bg = useColorModeValue('white', 'gray.700') const bg = useColorModeValue('white', 'gray.700')
const { isOpen: isSummaryOpen, onOpen: onSummaryOpen, onClose: onSummaryClose } = useDisclosure() const { open: isSummaryOpen, onOpen: onSummaryOpen, onClose: onSummaryClose } = useDisclosure()
const { isOpen: isSystemOpen, onOpen: onSystemOpen, onClose: onSystemClose } = useDisclosure() const { open: isSystemOpen, onOpen: onSystemOpen, onClose: onSystemClose } = useDisclosure()
const [isSmallerThan768] = useMediaQuery('(max-width: 768px)') const [isSmallerThan768] = useMediaQuery(['(max-width: 768px)'])
useEffect(() => { useEffect(() => {
const getGame = async () => { const getGame = async () => {
loadedState.set(false) loadedState.set(false)
return await agent.Games.details(id) return await agent.Games.details(id!)
} }
getGame().then((result) => { getGame().then((result) => {
gameState.set(result) gameState.set(result)
@@ -69,7 +68,7 @@ const GameDetails = () => {
<SystemRequirementsModal isOpen={isSystemOpen} onClose={onSystemClose} game={game} /> <SystemRequirementsModal isOpen={isSystemOpen} onClose={onSystemClose} game={game} />
: ''} : ''}
<Container maxW="container.xl" mt="5"> <Container maxW="1280px" mt="5">
<Flex direction={{ base: 'column', md: 'row' }}> <Flex direction={{ base: 'column', md: 'row' }}>
<Box p={2} alignSelf={{ <Box p={2} alignSelf={{
base: 'center', base: 'center',
@@ -78,25 +77,27 @@ const GameDetails = () => {
<Heading as="h1">{game.title}</Heading> <Heading as="h1">{game.title}</Heading>
</Box> </Box>
<Spacer /> <Spacer />
<Stack spacing={[1, 5]} direction={'row'} alignSelf={{ <Stack gap={[1, 5]} direction={'row'} alignSelf={{
base: 'center', base: 'center',
md: 'end' md: 'end'
}} pb="3"> }} pb="3">
{game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 && {game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 &&
(<ChakraLink href={`steam://run/${game.steamId}`}> (<ChakraLink href={`steam://run/${game.steamId}`}>
<Button variant={'outline'} colorScheme="blue"> <Button variant={'outline'} colorPalette="blue">
<Flex> <Flex>
<SteamAndDeckLogo size={'1.5em'} /> <SteamAndDeckLogo size={'1.5em'} />
<Text mt={'1'} ml={'2'}> <Text mt={'1'} ml={'2'}>
Open with Steam <ExternalLinkIcon pb={'1'} /> Open with Steam <LuExternalLink style={{display: 'inline'}} />
</Text> </Text>
</Flex> </Flex>
</Button> </Button>
</ChakraLink>)} </ChakraLink>)}
<Button as={Link} to={`/games/${game._id}/edit`} colorScheme="blue"> <Button asChild colorPalette="blue">
<Text mt={'1'}> <Link to={`/games/${game._id}/edit`}>
Edit <Text mt={'1'}>
</Text> Edit
</Text>
</Link>
</Button> </Button>
</Stack> </Stack>
</Flex> </Flex>
@@ -122,7 +123,7 @@ const GameDetails = () => {
<Box bg={bg} p="5" mt="3" rounded="md"> <Box bg={bg} p="5" mt="3" rounded="md">
{isSmallerThan768 ? {isSmallerThan768 ?
<Flex width="100%"> <Flex width="100%">
<Button colorScheme='blue' size='lg' width='100%' onMouseDown={onSummaryOpen}> <Button colorPalette='blue' size='lg' width='100%' onMouseDown={onSummaryOpen}>
About This Game About This Game
</Button> </Button>
<Spacer /> <Spacer />
@@ -138,7 +139,7 @@ const GameDetails = () => {
<Box bg={bg} mt="3" p="5" rounded="md"> <Box bg={bg} mt="3" p="5" rounded="md">
{isSmallerThan768 ? {isSmallerThan768 ?
<Flex width="100%"> <Flex width="100%">
<Button colorScheme='blue' size='lg' width='100%' onMouseDown={onSystemOpen}> <Button colorPalette='blue' size='lg' width='100%' onMouseDown={onSystemOpen}>
System Requirements System Requirements
</Button> </Button>
<Spacer /> <Spacer />
+43 -45
View File
@@ -21,17 +21,16 @@ import {
Box, Box,
Button, Button,
Container, Container,
FormControl, Flex,
FormLabel,
Heading, Heading,
InputGroup,
Spacer, Spacer,
Stack, Stack,
Switch,
Text, Text,
useColorModeValue,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {CreatableSelect,} from 'chakra-react-select' import {Field} from './ui/field'
import {Switch} from './ui/switch'
import {useColorModeValue} from './ui/color-mode'
import {CreatableSelect} from './FixedSelect'
import {adminMode} from '../stateManagement/userState' import {adminMode} from '../stateManagement/userState'
import {encode} from 'js-base64' import {encode} from 'js-base64'
import SingleFieldInput from './formFields/SingleFieldInput' import SingleFieldInput from './formFields/SingleFieldInput'
@@ -314,63 +313,64 @@ const GameForm = () => {
return ( return (
<> <>
<Container maxW="container.md" mt="10"> <Container maxW="768px" mt="10">
{id ? <Heading as="h1" size="xl">Editing {game.title}</Heading> : {id ? <Heading as="h1" size="xl">Editing {game.title}</Heading> :
<Heading as="h1" size="xl">Add a Game to the Database</Heading>} <Heading as="h1" size="xl">Add a Game to the Database</Heading>}
<form onSubmit={handleSubmit} style={{marginTop: '2rem'}}> <form onSubmit={handleSubmit} style={{marginTop: '2rem'}}>
<Stack spacing={[1, 5]} direction={{base: 'column', sm: 'row'}} justify={'flex-end'}> <Stack gap={[1, 5]} direction={{base: 'column', sm: 'row'}} justify={'flex-end'}>
<Button <Button
isLoading={submitLoading} loading={submitLoading}
colorScheme="blue" colorPalette="blue"
type="submit" type="submit"
data-name={"save"} data-name={"save"}
> >
Save Save
</Button> </Button>
<Button <Button
isLoading={moveOn} loading={moveOn}
colorScheme="green" colorPalette="green"
type="submit" type="submit"
data-name={"save-and-view"} data-name={"save-and-view"}
> >
Save and View Save and View
</Button> </Button>
</Stack> </Stack>
<InputGroup> <Flex gap={3} alignItems="center">
{!id || admin ? ( {!id || admin ? (
<Box flex="1">
<SingleFieldNumericInput label={'Steam ID'} textField={'steamId'} field={game.steamId} bg={bg} <SingleFieldNumericInput label={'Steam ID'} textField={'steamId'} field={game.steamId} bg={bg}
handleInputChange={handleInputChange} required={game.scrape}/> handleInputChange={handleInputChange} required={game.scrape}/>
</Box>
) : ''} ) : ''}
{!id && ( {!id && (
<FormControl display="flex" ml={'3'} alignItems="center"> <Flex alignItems="center" ml={'3'} flexShrink={0} gap={2}>
<FormLabel htmlFor="scrape" mb="0">Steam Scrape</FormLabel> <Text whiteSpace="nowrap">Steam Scrape</Text>
<Switch <Switch
isChecked={game.scrape} colorPalette="blue"
onChange={() => { checked={game.scrape}
onCheckedChange={() => {
handleToggle('scrape') handleToggle('scrape')
}} }}
name="scrape" name="scrape"
id="scrape" id="scrape"
/> />
</FormControl> </Flex>
)} )}
</InputGroup> </Flex>
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? ( {(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
<SingleFieldInput label={'Title'} textField={'title'} field={game.title} bg={bg} <SingleFieldInput label={'Title'} textField={'title'} field={game.title} bg={bg}
handleInputChange={handleInputChange} required={false}/> handleInputChange={handleInputChange} required={false}/>
) : ''} ) : ''}
<> <>
<FormControl mb="3"> <Field mb="3" label="Series">
<FormLabel>Series</FormLabel>
<Box <Box
bg={bg} bg={bg}
border="1px" rounded="md"
borderColor="gray.500" w="full">
rounded="md">
<CreatableSelect <CreatableSelect
name="series" name="series"
isClearable isClearable
onChange={(values) => { onChange={(values: any) => {
if (values === null) values = {value: '', label: ''} if (values === null) values = {value: '', label: ''}
if (values) { if (values) {
handleInputChange('series', values.value) handleInputChange('series', values.value)
@@ -380,20 +380,18 @@ const GameForm = () => {
value={{value: game.series, label: game.series}} value={{value: game.series, label: game.series}}
/> />
</Box> </Box>
</FormControl> </Field>
</> </>
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? ( {(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
<> <>
<SingleFieldInput label={'Front Image'} textField={'frontImage'} field={game.frontImage} <SingleFieldInput label={'Front Image'} textField={'frontImage'} field={game.frontImage}
bg={bg} bg={bg}
handleInputChange={handleInputChange} required={false}/> handleInputChange={handleInputChange} required={false}/>
<FormControl mb="3"> <Field mb="3" label="Screenshots">
<FormLabel>Screenshots</FormLabel>
<Box <Box
bg={bg} bg={bg}
border="1px" rounded="md"
borderColor="gray.500" w="full">
rounded="md">
<CreatableSelect <CreatableSelect
name="screenshots" name="screenshots"
isClearable isClearable
@@ -433,7 +431,7 @@ const GameForm = () => {
value={screenshots[0]?.value === '' ? [] : screenshots} value={screenshots[0]?.value === '' ? [] : screenshots}
/> />
</Box> </Box>
</FormControl> </Field>
</> </>
) : ''} ) : ''}
{(gameId === '' && !game.scrape) || (gameId.length >= 1) ? {(gameId === '' && !game.scrape) || (gameId.length >= 1) ?
@@ -453,14 +451,14 @@ const GameForm = () => {
</> </>
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? ( {(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
<Box mt="5" mb="5"> <Box mt="5" mb="5">
<FormLabel>Operating Systems</FormLabel> <Text fontWeight="medium" mb="2">Operating Systems</Text>
<Box <Box
bg={bg} bg={bg}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor="gray.500"
rounded="md" rounded="md"
p="2"> p="2">
<Stack spacing={[1, 5]} direction={['column', 'row']}> <Stack gap={[1, 5]} direction={['column', 'row']}>
<CheckboxInput label={'Windows'} textField={'windows'} checked={game.os.windows} <CheckboxInput label={'Windows'} textField={'windows'} checked={game.os.windows}
defaultIsChecked={true} handleOSChange={handleOSChange}/> defaultIsChecked={true} handleOSChange={handleOSChange}/>
<CheckboxInput label={'Mac OSX'} textField={'mac'} checked={game.os.mac} <CheckboxInput label={'Mac OSX'} textField={'mac'} checked={game.os.mac}
@@ -518,9 +516,9 @@ const GameForm = () => {
</> </>
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? ( {(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
<> <>
<Text align="center" fontSize="xl" mt="10">System Requirements</Text> <Text textAlign="center" fontSize="xl" mt="10">System Requirements</Text>
<Box> <Box>
<Text fontSize="lg" align="center">Windows</Text> <Text fontSize="lg" textAlign="center">Windows</Text>
<SystemRequirementsTextareaInput label={'Windows Minimum'} <SystemRequirementsTextareaInput label={'Windows Minimum'}
textField={'systemRequirements.windows.minimum'} textField={'systemRequirements.windows.minimum'}
field={game.systemRequirements.windows.minimum} bg={bg} field={game.systemRequirements.windows.minimum} bg={bg}
@@ -532,7 +530,7 @@ const GameForm = () => {
bg={bg} bg={bg}
handleSystemRequirements={handleSystemRequirements} handleSystemRequirements={handleSystemRequirements}
rows={3}/> rows={3}/>
<Text fontSize="lg" align="center">Mac OSX</Text> <Text fontSize="lg" textAlign="center">Mac OSX</Text>
<SystemRequirementsTextareaInput label={'Mac OSX Minimum'} <SystemRequirementsTextareaInput label={'Mac OSX Minimum'}
textField={'systemRequirements.mac.minimum'} textField={'systemRequirements.mac.minimum'}
field={game.systemRequirements.mac.minimum} bg={bg} field={game.systemRequirements.mac.minimum} bg={bg}
@@ -543,7 +541,7 @@ const GameForm = () => {
field={game.systemRequirements.mac.recommended} bg={bg} field={game.systemRequirements.mac.recommended} bg={bg}
handleSystemRequirements={handleSystemRequirements} handleSystemRequirements={handleSystemRequirements}
rows={3}/> rows={3}/>
<Text fontSize="lg" align="center">Linux</Text> <Text fontSize="lg" textAlign="center">Linux</Text>
<SystemRequirementsTextareaInput label={'Linux Minimum'} <SystemRequirementsTextareaInput label={'Linux Minimum'}
textField={'systemRequirements.linux.minimum'} textField={'systemRequirements.linux.minimum'}
field={game.systemRequirements.linux.minimum} bg={bg} field={game.systemRequirements.linux.minimum} bg={bg}
@@ -558,18 +556,18 @@ const GameForm = () => {
</Box> </Box>
</> </>
) : ''} ) : ''}
<Stack spacing={[1, 5]} direction={{base: 'column', sm: 'row'}} align={{base: 'start'}}> <Stack gap={[1, 5]} direction={{base: 'column', sm: 'row'}} align={{base: 'start'}}>
<Button <Button
isLoading={submitLoading} loading={submitLoading}
colorScheme="blue" colorPalette="blue"
type="submit" type="submit"
data-name={"save"} data-name={"save"}
> >
Save Save
</Button> </Button>
<Button <Button
isLoading={moveOn} loading={moveOn}
colorScheme="green" colorPalette="green"
type="submit" type="submit"
data-name={"save-and-view"} data-name={"save-and-view"}
> >
@@ -579,7 +577,7 @@ const GameForm = () => {
<> <>
<Spacer/> <Spacer/>
<Button <Button
colorScheme="red" colorPalette="red"
onMouseDown={() => { onMouseDown={() => {
openState.set(true) openState.set(true)
}} }}
+13 -11
View File
@@ -4,7 +4,7 @@ import React, { useEffect } from 'react'
import { gameDashboardState } from '../stateManagement/gameState' import { gameDashboardState } from '../stateManagement/gameState'
import { loadingState } from '../stateManagement/loadingState' import { loadingState } from '../stateManagement/loadingState'
import { useHookstate } from '@hookstate/core' import { useHookstate } from '@hookstate/core'
import { Box, Center, Container, Divider, Heading, SimpleGrid, VStack } from '@chakra-ui/react' import { Box, Center, Container, Separator, Heading, SimpleGrid, VStack } from '@chakra-ui/react'
import Pagination from './Pagination' import Pagination from './Pagination'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { adminMode } from '../stateManagement/userState' import { adminMode } from '../stateManagement/userState'
@@ -33,34 +33,36 @@ const GameDashboard = () => {
return ( return (
<> <>
<Container maxW="container.xl" mt="7"> <Container maxW="1280px" mt="7">
<Pagination /> <Pagination />
<ActiveFilters /> <ActiveFilters />
<Divider /> <Separator borderColor="gray.200" />
{!loaded ? <DashboardLoadingModal /> : games.count.totalGames === 0 ? {!loaded ? <DashboardLoadingModal /> : games.count.totalGames === 0 ?
<Center h={'80vh'}> <Center h={'80vh'}>
<VStack spacing={10}> <VStack gap={10}>
<Heading as={'h1'} size={'3xl'}>No Results Found...</Heading> <Heading as={'h1'} size={'3xl'}>No Results Found...</Heading>
<Divider /> <Separator borderColor="gray.200" />
{games.searchParams !== '' ? {games.searchParams !== '' ?
<Box mt='10'> <Box mt='10'>
<ResetFilterButton text='Clear Search and Filters' size='lg' variant='outline' colorScheme='blue' /> <ResetFilterButton text='Clear Search and Filters' size='lg' variant='outline' colorPalette='blue' />
</Box> </Box>
: :
<Box <Box
rounded={'md'} rounded={'md'}
as={NavLink} asChild
border={'1px'} borderWidth={'1px'} borderStyle={'solid'}
p={'3'} p={'3'}
fontSize={'2xl'} fontSize={'2xl'}
to="/games/create"> >
<NavLink to="/games/create">
Create a Game Create a Game
</NavLink>
</Box> </Box>
} }
</VStack> </VStack>
</Center> </Center>
: :
<SimpleGrid columns={{ base: 1, sm: 2, md: 3 }} spacing={10} style={{ marginTop: '3rem' }}> <SimpleGrid columns={{ base: 1, sm: 2, md: 3 }} gap={10} style={{ marginTop: '3rem' }}>
{games.data.map((game) => ( {games.data.map((game) => (
<GameThumbnailContextMenu <GameThumbnailContextMenu
key={game._id} key={game._id}
@@ -68,7 +70,7 @@ const GameDashboard = () => {
game={game} /> game={game} />
))} ))}
</SimpleGrid>} </SimpleGrid>}
<Divider my={'1rem'} /> <Separator borderColor="gray.200" my={'1rem'} />
<Pagination /> <Pagination />
</Container> </Container>
</> </>
+90 -92
View File
@@ -9,27 +9,23 @@ import {
Link, Link,
HStack, HStack,
Stack, Stack,
useColorModeValue,
Menu, Menu,
MenuButton, Portal,
Avatar,
MenuList,
MenuItem,
MenuDivider,
Icon, Icon,
useColorMode,
Text, Text,
Center, Center,
VStack, VStack,
Tooltip, Separator,
Divider,
Heading, Heading,
Kbd, Kbd,
Spacer, Spacer,
useDisclosure, useDisclosure,
useMediaQuery, useMediaQuery,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { AddIcon, HamburgerIcon, Search2Icon, SettingsIcon } from '@chakra-ui/icons' import {useColorMode, useColorModeValue} from './ui/color-mode'
import {Tooltip} from './ui/tooltip'
import {Avatar} from './ui/avatar'
import { LuPlus, LuMenu, LuSearch, LuSettings } from 'react-icons/lu'
import SearchModal from './SearchModal' import SearchModal from './SearchModal'
import { openAndCloseSearchDialogue } from '../stateManagement/loadingState' import { openAndCloseSearchDialogue } from '../stateManagement/loadingState'
import AdminMode from './authComponents/AdminMode' import AdminMode from './authComponents/AdminMode'
@@ -49,10 +45,10 @@ export default function NavBar() {
const bgLink = useColorModeValue('gray.200', 'gray.700') const bgLink = useColorModeValue('gray.200', 'gray.700')
const location = useLocation() const location = useLocation()
const history = useNavigate() const history = useNavigate()
const { isOpen: mobileIsOpen, onOpen: mobileOnOpen, onClose: mobileOnClose } = useDisclosure() const { open: mobileIsOpen, onOpen: mobileOnOpen, onClose: mobileOnClose } = useDisclosure()
const { isOpen: filterIsOpen, onOpen: filterOnOpen, onClose: filterOnClose } = useDisclosure() const { open: filterIsOpen, onOpen: filterOnOpen, onClose: filterOnClose } = useDisclosure()
const btnRef = React.useRef(null) const btnRef = React.useRef(null)
const [isSmallerThan768] = useMediaQuery('(max-width: 768px)') const [isSmallerThan768] = useMediaQuery(['(max-width: 768px)'])
const handleKeyPress = useCallback((event: any) => { const handleKeyPress = useCallback((event: any) => {
if (event.ctrlKey === true && event.key === ',') { if (event.ctrlKey === true && event.key === ',') {
@@ -95,13 +91,13 @@ export default function NavBar() {
position={loadedPreferences.get().stickyNav ? "fixed" : "relative"} position={loadedPreferences.get().stickyNav ? "fixed" : "relative"}
justifyContent={'space-between'} justifyContent={'space-between'}
> >
<HStack spacing={8} alignItems={'center'}> <HStack gap={8} alignItems={'center'}>
<Box as={RouteLink} to="/"> <Box asChild>
Game Database <RouteLink to="/">Game Database</RouteLink>
</Box> </Box>
<HStack <HStack
as={'nav'} as={'nav'}
spacing={4} gap={4}
display={{ base: 'none', md: 'flex' }} display={{ base: 'none', md: 'flex' }}
> >
<Link <Link
@@ -112,9 +108,8 @@ export default function NavBar() {
textDecoration: 'none', textDecoration: 'none',
bg: bgLink, bg: bgLink,
}} }}
as={RouteLink} asChild>
to="/games"> <RouteLink to="/games">Games</RouteLink>
Games
</Link> </Link>
<Link <Link
px={2} px={2}
@@ -124,9 +119,8 @@ export default function NavBar() {
textDecoration: 'none', textDecoration: 'none',
bg: bgLink, bg: bgLink,
}} }}
as={RouteLink} asChild>
to="/games/create"> <RouteLink to="/games/create">Create Game</RouteLink>
Create Game
</Link> </Link>
</HStack> </HStack>
</HStack> </HStack>
@@ -135,7 +129,7 @@ export default function NavBar() {
{isSmallerThan768 ? {isSmallerThan768 ?
<> <>
<Button ref={btnRef} onMouseDown={mobileIsOpen ? mobileOnClose : mobileOnOpen}> <Button ref={btnRef} onMouseDown={mobileIsOpen ? mobileOnClose : mobileOnOpen}>
<HamburgerIcon/> <LuMenu/>
</Button> </Button>
<MobileBar mobileIsOpen={mobileIsOpen} mobileOnClose={mobileOnClose} btnRef={btnRef} bgLink={bgLink} <MobileBar mobileIsOpen={mobileIsOpen} mobileOnClose={mobileOnClose} btnRef={btnRef} bgLink={bgLink}
user={user} colorMode={colorMode} toggleColorMode={toggleColorMode}/> user={user} colorMode={colorMode} toggleColorMode={toggleColorMode}/>
@@ -144,7 +138,7 @@ export default function NavBar() {
<Stack direction={'row'}> <Stack direction={'row'}>
{ user.role === 'admin' && <AdminMode/>} { user.role === 'admin' && <AdminMode/>}
<Tooltip hasArrow mr={'2'} label={'Filters'} aria-label={'Filters'}> <Tooltip showArrow content={'Filters'}>
<Button <Button
rounded={'full'} rounded={'full'}
bg={'transparent'} bg={'transparent'}
@@ -164,7 +158,7 @@ export default function NavBar() {
</Tooltip> </Tooltip>
<FilterBar filterIsOpen={filterIsOpen} filterOnClose={filterOnClose} btnRef={btnRef} filterLoadingState={filterLoadingState} /> <FilterBar filterIsOpen={filterIsOpen} filterOnClose={filterOnClose} btnRef={btnRef} filterLoadingState={filterLoadingState} />
<Tooltip hasArrow mr={'2'} label={'Search: ctrl+s'} aria-label={'Search'}> <Tooltip showArrow content={'Search: ctrl+s'}>
<Button <Button
rounded={'full'} rounded={'full'}
onMouseDown={() => { onMouseDown={() => {
@@ -173,77 +167,81 @@ export default function NavBar() {
bg={'transparent'} bg={'transparent'}
cursor={'pointer'} cursor={'pointer'}
minW={0}> minW={0}>
<Search2Icon/> <LuSearch/>
</Button> </Button>
</Tooltip> </Tooltip>
<Menu> <Menu.Root>
<Tooltip hasArrow mr={'2'} label={'Settings'} aria-label={'Settings'}> <Menu.Trigger asChild>
<MenuButton <Button
as={Button}
rounded={'full'} rounded={'full'}
variant={'transparent'} variant={'ghost'}
cursor={'pointer'} cursor={'pointer'}
minW={0}> minW={0}>
<SettingsIcon/> <Tooltip showArrow content={'Settings'}>
</MenuButton> <LuSettings/>
</Tooltip> </Tooltip>
<MenuList alignItems={'center'}> </Button>
<VStack> </Menu.Trigger>
{/*<Button onMouseDown={toggleColorMode} w={'70%'}>*/} <Portal>
{/* {colorMode === 'light' ? <MoonIcon/> : <SunIcon/>}*/} <Menu.Positioner>
{/* {colorMode === 'light' ?*/} <Menu.Content p={8}>
{/* <Text mt={'1'} ml={'2'}>Dark</Text> :*/} <VStack alignItems={'center'}>
{/* <Text mt={'1'} ml={'2'}>Light</Text>}*/} <Button
{/*</Button>*/} variant={'solid'}
<Button colorPalette={'blue'}
variant={'solid'} size={'md'}
colorScheme={'blue'} asChild
size={'md'} w={'70%'}
leftIcon={<AddIcon/>} >
as={RouteLink} <RouteLink to="/games/create"><LuPlus/> <Text mt="1">Create Game</Text></RouteLink>
w={'70%'} </Button>
to="/games/create"> <Separator/>
<Text mt="1">Create Game</Text> <Heading as={'h3'} size={'md'}>Keyboard Shortcuts</Heading>
</Button> <HStack><Text fontSize={'sm'}>Search:</Text><span><Kbd>ctrl</Kbd> + <Kbd>,</Kbd></span></HStack>
<Divider/> <HStack><Text fontSize={'sm'}>Games Grid:</Text><span><Kbd>ctrl</Kbd> + <Kbd>.</Kbd></span></HStack>
<Heading as={'h3'} size={'md'}>Keyboard Shortcuts</Heading> <HStack><Text fontSize={'sm'}>Create
<HStack><Text fontSize={'sm'}>Search:</Text><span><Kbd>ctrl</Kbd> + <Kbd>,</Kbd></span></HStack> Game:</Text><span><Kbd>ctrl</Kbd> + <Kbd>/</Kbd></span></HStack>
<HStack><Text fontSize={'sm'}>Games Grid:</Text><span><Kbd>ctrl</Kbd> + <Kbd>.</Kbd></span></HStack> </VStack>
<HStack><Text fontSize={'sm'}>Create </Menu.Content>
Game:</Text><span><Kbd>ctrl</Kbd> + <Kbd>/</Kbd></span></HStack> </Menu.Positioner>
</VStack> </Portal>
</MenuList> </Menu.Root>
</Menu> <Menu.Root>
<Menu> <Menu.Trigger asChild>
<Tooltip hasArrow mr={'2'} label={'User Info and Logout'} aria-label={'User Info'}> <Button
<MenuButton rounded={'full'}
as={Button} variant={'ghost'}
rounded={'full'} cursor={'pointer'}
variant={'transparent'} minW={0}>
cursor={'pointer'} <Tooltip showArrow content={'User Info and Logout'}>
minW={0}> <Avatar size={'sm'} src={user.avatar} name={user.displayName} />
<Avatar size={'sm'} src={user.avatar}/> </Tooltip>
</MenuButton> </Button>
</Tooltip> </Menu.Trigger>
<MenuList alignItems={'center'}> <Portal>
<br/> <Menu.Positioner>
<Center> <Menu.Content p={8}>
<Avatar <br/>
size={'2xl'} <Center>
src={user.avatar} <Avatar
/> size={'2xl'}
</Center> src={user.avatar}
<br/> name={user.displayName}
<Center> />
<p>{user.displayName}</p> </Center>
</Center> <br/>
<br/> <Center>
<MenuDivider/> <p>{user.displayName}</p>
<MenuItem onClick={() => history('/profile')}>Preferences</MenuItem> </Center>
<MenuItem onClick={() => logout()}><Icon as={MdOutlinePowerSettingsNew} mr={'2'}/> <Text <br/>
mt={'1'}>Logout</Text></MenuItem> <Menu.Separator/>
</MenuList> <Menu.Item value="preferences" onSelect={() => history('/profile')}>Preferences</Menu.Item>
</Menu> <Menu.Item value="logout" onSelect={() => logout()}><Icon as={MdOutlinePowerSettingsNew} mr={'2'}/> <Text
mt={'1'}>Logout</Text></Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
</Stack> </Stack>
} }
</Flex> </Flex>
+4 -4
View File
@@ -4,13 +4,13 @@ import { VStack, Image, Button, Heading, Flex, Center } from '@chakra-ui/react'
const Home = () => { const Home = () => {
return ( return (
<Flex align={'center'} bgGradient={'linear(to-br, #314755, #26a0da)'} height={'100vh'}> <Flex align={'center'} bgImage={'linear-gradient(to bottom right, #314755, #26a0da)'} height={'100vh'}>
<Center w={'100%'}> <Center w={'100%'}>
<VStack spacing={4} align={'center'} margin={'auto'}> <VStack gap={4} align={'center'} margin={'auto'}>
<Image src="/assets/Logo.svg" alt="Games Database Logo"/> <Image src="/assets/Logo.svg" alt="Games Database Logo"/>
<Heading as="h2" fontSize={'1.7em'} fontWeight={'normal'}>Welcome to your Games Database</Heading> <Heading as="h2" fontSize={'1.7em'} fontWeight={'normal'}>Welcome to your Games Database</Heading>
<Button as={Link} to="/login" size='lg' variant='outline' fontSize='1.25rem'> <Button asChild size='lg' variant='outline' fontSize='1.25rem'>
Login <Link to="/login">Login</Link>
</Button> </Button>
</VStack> </VStack>
</Center> </Center>
+7 -2
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { Center, CircularProgress } from '@chakra-ui/react' import { Center, ProgressCircle } from '@chakra-ui/react'
export default function LoadingModal() { export default function LoadingModal() {
@@ -14,7 +14,12 @@ export default function LoadingModal() {
bottom={'0'} bottom={'0'}
left={'0'} left={'0'}
> >
<CircularProgress isIndeterminate color="blue.500"/> <ProgressCircle.Root value={null} size="lg">
<ProgressCircle.Circle>
<ProgressCircle.Track />
<ProgressCircle.Range stroke="blue.500" />
</ProgressCircle.Circle>
</ProgressCircle.Root>
</Center> </Center>
</> </>
) )
+31 -43
View File
@@ -1,19 +1,18 @@
import React, { MutableRefObject } from 'react' import React, { MutableRefObject } from 'react'
import { import {
Avatar, Button, Center, ColorMode, Button, Center,
Divider, Drawer, Separator,
DrawerBody, Drawer,
DrawerCloseButton, Icon,
DrawerContent, DrawerFooter,
DrawerHeader,
DrawerOverlay, Icon,
Image, Link, Image, Link,
Stack, Text, Stack, Text,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {DrawerBody, DrawerCloseTrigger, DrawerContent, DrawerFooter, DrawerHeader, DrawerRoot} from './ui/drawer'
import {Avatar} from './ui/avatar'
import AdminMode from './authComponents/AdminMode' import AdminMode from './authComponents/AdminMode'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import Search from './Search' import Search from './Search'
import { AddIcon } from '@chakra-ui/icons' import { LuPlus } from 'react-icons/lu'
import { logout } from '../stateManagement/userState' import { logout } from '../stateManagement/userState'
import User from '../models/user' import User from '../models/user'
import { AiOutlineLogout } from 'react-icons/ai' import { AiOutlineLogout } from 'react-icons/ai'
@@ -21,12 +20,13 @@ import {useHookstate} from "@hookstate/core";
import {searchCompletedState} from "../stateManagement/loadingState"; import {searchCompletedState} from "../stateManagement/loadingState";
import {gameDashboardState} from "../stateManagement/gameState"; import {gameDashboardState} from "../stateManagement/gameState";
import ResetFilterButton from "./helperComponents/ResetFilterButton"; import ResetFilterButton from "./helperComponents/ResetFilterButton";
import type { ColorMode } from './ui/color-mode'
interface Props { interface Props {
mobileIsOpen: boolean mobileIsOpen: boolean
mobileOnClose: () => void mobileOnClose: () => void
btnRef: MutableRefObject<null> btnRef: MutableRefObject<null>
bgLink: "gray.200" | "gray.700" bgLink: string
user: User user: User
colorMode: ColorMode colorMode: ColorMode
toggleColorMode: () => void toggleColorMode: () => void
@@ -37,15 +37,14 @@ function MobileBar({ mobileIsOpen, mobileOnClose, btnRef, bgLink, user }: Props)
const gamesState = useHookstate(gameDashboardState) const gamesState = useHookstate(gameDashboardState)
const games = gamesState.get() const games = gamesState.get()
return ( return (
<Drawer <DrawerRoot
isOpen={mobileIsOpen} open={mobileIsOpen}
placement="right" placement="end"
onClose={mobileOnClose} onOpenChange={(e) => { if (!e.open) mobileOnClose() }}
finalFocusRef={btnRef} finalFocusEl={() => btnRef.current as unknown as HTMLElement}
> >
<DrawerOverlay/>
<DrawerContent> <DrawerContent>
<DrawerCloseButton/> <DrawerCloseTrigger/>
<DrawerHeader> <DrawerHeader>
<Image maxW="15rem" src="/assets/Logo.svg" alt="Games Database Logo"/> <Image maxW="15rem" src="/assets/Logo.svg" alt="Games Database Logo"/>
</DrawerHeader> </DrawerHeader>
@@ -54,7 +53,7 @@ function MobileBar({ mobileIsOpen, mobileOnClose, btnRef, bgLink, user }: Props)
<AdminMode /> <AdminMode />
<Stack <Stack
as={'nav'} as={'nav'}
spacing={4} gap={4}
> >
<Link <Link
px={2} px={2}
@@ -64,9 +63,8 @@ function MobileBar({ mobileIsOpen, mobileOnClose, btnRef, bgLink, user }: Props)
textDecoration: 'none', textDecoration: 'none',
bg: bgLink, bg: bgLink,
}} }}
as={NavLink} asChild>
to="/games"> <NavLink to="/games"><Text textAlign={'right'}>Games</Text></NavLink>
<Text align={'right'}>Games</Text>
</Link> </Link>
<Link <Link
px={2} px={2}
@@ -76,15 +74,14 @@ function MobileBar({ mobileIsOpen, mobileOnClose, btnRef, bgLink, user }: Props)
textDecoration: 'none', textDecoration: 'none',
bg: bgLink, bg: bgLink,
}} }}
as={NavLink} asChild>
to="/games/create"> <NavLink to="/games/create"><Text textAlign={'right'}>Create Game</Text></NavLink>
<Text align={'right'}>Create Game</Text>
</Link> </Link>
<Divider/> <Separator/>
<Search/> <Search/>
{searchCompleted.get() && <Text>Games Found: {games.count.totalGames}</Text>} {searchCompleted.get() && <Text>Games Found: {games.count.totalGames}</Text>}
<Divider/> <Separator/>
<Center><Avatar mr={'2'} size={'sm'} src={user.avatar}/><Text mt={1}>{user.displayName}</Text></Center> <Center><Avatar mr={'2'} size={'sm'} src={user.avatar} name={user.displayName}/><Text mt={1}>{user.displayName}</Text></Center>
<Link <Link
px={2} px={2}
py={1} py={1}
@@ -93,18 +90,10 @@ function MobileBar({ mobileIsOpen, mobileOnClose, btnRef, bgLink, user }: Props)
textDecoration: 'none', textDecoration: 'none',
bg: bgLink, bg: bgLink,
}} }}
as={NavLink} asChild>
to="/profile"> <NavLink to="/profile"><Text textAlign={'right'}>Preferences</Text></NavLink>
<Text align={'right'}>Preferences</Text>
</Link> </Link>
<Divider/> <Separator/>
{/*<Button onMouseDown={toggleColorMode} ml={'auto'}>*/}
{/* {colorMode === 'light' ? <MoonIcon/> : <SunIcon/>}*/}
{/* {colorMode === 'light' ?*/}
{/* <Text mt={'1'} ml={'2'}>Dark</Text> :*/}
{/* <Text mt={'1'} ml={'2'}>Light</Text>}*/}
{/*</Button>*/}
{/*<Divider/>*/}
<ResetFilterButton /> <ResetFilterButton />
</Stack> </Stack>
@@ -114,20 +103,19 @@ function MobileBar({ mobileIsOpen, mobileOnClose, btnRef, bgLink, user }: Props)
<DrawerFooter> <DrawerFooter>
<Button <Button
variant={'solid'} variant={'solid'}
colorScheme={'blue'} colorPalette={'blue'}
size={'md'} size={'md'}
leftIcon={<AddIcon/>} asChild
as={NavLink}
mr={3} mr={3}
to="/games/create"> >
<Text mt="1">Create Game</Text> <NavLink to="/games/create"><LuPlus/> <Text mt="1">Create Game</Text></NavLink>
</Button> </Button>
<Button onMouseDown={() => logout()}><Icon as={AiOutlineLogout} mr={'2'}/> <Text <Button onMouseDown={() => logout()}><Icon as={AiOutlineLogout} mr={'2'}/> <Text
mt={'1'}>Logout</Text></Button> mt={'1'}>Logout</Text></Button>
</DrawerFooter> </DrawerFooter>
</DrawerContent> </DrawerContent>
</Drawer> </DrawerRoot>
) )
} }
+4 -4
View File
@@ -4,12 +4,12 @@ import { Center, Flex, Heading, Button, VStack } from '@chakra-ui/react'
export default function NoGames() { export default function NoGames() {
return ( return (
<Flex align={'center'} bgGradient={'linear(to-br, #314755, #26a0da)'} height={'100vh'}> <Flex align={'center'} bgImage={'linear-gradient(to bottom right, #314755, #26a0da)'} height={'100vh'}>
<Center w={'100%'}> <Center w={'100%'}>
<VStack spacing={4} align={'center'} margin={'auto'}> <VStack gap={4} align={'center'} margin={'auto'}>
{<Heading as="h2">You have not added any games yet.</Heading>} {<Heading as="h2">You have not added any games yet.</Heading>}
<Button as={Link} to='/games/create' size='lg' variant='outline' fontSize='1.25rem'> <Button asChild size='lg' variant='outline' fontSize='1.25rem'>
Click here to add some! <Link to='/games/create'>Click here to add some!</Link>
</Button> </Button>
</VStack> </VStack>
</Center> </Center>
+58 -50
View File
@@ -4,24 +4,21 @@ import {
Grid, Grid,
GridItem, GridItem,
IconButton, IconButton,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
Text, Text,
Tooltip, Portal,
useColorModeValue,
useMediaQuery,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons' import {NumberInputRoot, NumberInputField} from './ui/number-input'
import { NativeSelect } from '@chakra-ui/react'
import { LuChevronLeft, LuChevronRight, LuChevronsLeft, LuChevronsRight } from 'react-icons/lu'
import {Tooltip} from './ui/tooltip'
import { useHookstate } from '@hookstate/core' import { useHookstate } from '@hookstate/core'
import { gameDashboardState } from '../stateManagement/gameState' import { gameDashboardState } from '../stateManagement/gameState'
import { loadingState } from '../stateManagement/loadingState' import { loadingState } from '../stateManagement/loadingState'
import { adminMode } from '../stateManagement/userState' import { adminMode } from '../stateManagement/userState'
import getWithFilters from '../componentUtils/getWithFilters' import getWithFilters from '../componentUtils/getWithFilters'
import { ratingState, steamRatingState } from "../stateManagement/ratingState"; import { ratingState, steamRatingState } from "../stateManagement/ratingState";
import {useColorModeValue} from "./ui/color-mode";
import {useMediaQuery} from "@chakra-ui/react";
export default function Pagination() { export default function Pagination() {
const gamesState = useHookstate(gameDashboardState) const gamesState = useHookstate(gameDashboardState)
@@ -34,7 +31,7 @@ export default function Pagination() {
const bg = useColorModeValue('gray.50', 'transparent') const bg = useColorModeValue('gray.50', 'transparent')
const useRatingState = useHookstate(ratingState).get() const useRatingState = useHookstate(ratingState).get()
const useSteamRatingState = useHookstate(steamRatingState).get() const useSteamRatingState = useHookstate(steamRatingState).get()
const [isSmallerThan768] = useMediaQuery('(max-width: 768px)') const [isSmallerThan768] = useMediaQuery(['(max-width: 768px)'])
const setPageSize = async (limit: number) => await getWithFilters( const setPageSize = async (limit: number) => await getWithFilters(
gamesState, gamesState,
@@ -99,22 +96,24 @@ export default function Pagination() {
}}> }}>
<GridItem area={"backArrows"}> <GridItem area={"backArrows"}>
<Flex> <Flex>
<Tooltip label="First Page"> <Tooltip content="First Page">
<IconButton <IconButton
aria-label={'First Page'} aria-label={'First Page'}
onMouseDown={() => gotoPage(0)} onMouseDown={() => gotoPage(0)}
isDisabled={pageIndex <= 1} disabled={pageIndex <= 1}
icon={<ArrowLeftIcon h={3} w={3} />}
mr={4} mr={4}
/> >
<LuChevronsLeft />
</IconButton>
</Tooltip> </Tooltip>
<Tooltip label="Previous Page"> <Tooltip content="Previous Page">
<IconButton <IconButton
aria-label={'Previous Page'} aria-label={'Previous Page'}
onMouseDown={previousPage} onMouseDown={previousPage}
isDisabled={pageIndex <= 1} disabled={pageIndex <= 1}
icon={<ChevronLeftIcon h={6} w={6} />} >
/> <LuChevronLeft />
</IconButton>
</Tooltip> </Tooltip>
</Flex> </Flex>
</GridItem> </GridItem>
@@ -133,70 +132,79 @@ export default function Pagination() {
{!isSmallerThan768 && {!isSmallerThan768 &&
<GridItem area={"goToPage"}> <GridItem area={"goToPage"}>
<Text mb={'1'}>Go to Page:</Text> <Text mb={'1'}>Go to Page:</Text>
<NumberInput <NumberInputRoot
bg={bg} bg={bg}
rounded={'md'} rounded={'md'}
w={{ base: 20, md: 28 }} w={{ base: 20, md: 28 }}
min={1} min={1}
max={pageCount} max={pageCount}
value={pageIndex} value={String(pageIndex)}
onChange={(value) => { onValueChange={(e) => {
const page = value ? Number(value) : 1 const page = e.value ? Number(e.value) : 1
gotoPage(page) gotoPage(page)
}} }}
> >
<NumberInputField /> <NumberInputField
<NumberInputStepper> borderWidth="1px"
<NumberIncrementStepper /> borderStyle="solid"
<NumberDecrementStepper /> borderColor={{ base: "gray.500", _invalid: "red.500" }}
</NumberInputStepper> />
</NumberInput> </NumberInputRoot>
</GridItem> </GridItem>
} }
<GridItem area={"countPerPage"}> <GridItem area={"countPerPage"}>
<Text mb={'1'}>Count Per Page:</Text> <Text mb={'1'}>Count Per Page:</Text>
<Select <NativeSelect.Root
ml={{ ml={{
base: '4', base: '4',
md: '0' md: '0'
}} }}
bg={bg}
rounded={'md'}
w={{ base: 20, md: 32 }} w={{ base: 20, md: 32 }}
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value))
}}
> >
{[10, 20, 30, 40, 50].map((pageSize) => ( <NativeSelect.Field
<option key={pageSize} value={pageSize}> rounded={'md'}
{pageSize} borderWidth={'1px'}
</option> borderStyle={'solid'}
))} borderColor={'gray.500'}
</Select> bg={bg}
value={pageSize}
onChange={(e: any) => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
</GridItem> </GridItem>
<GridItem area={'gamesFound'}> <GridItem area={'gamesFound'}>
<Text>Games Found: {games.count.totalGames}</Text> <Text>Games Found: {games.count.totalGames}</Text>
</GridItem> </GridItem>
<GridItem area={"forwardArrows"}> <GridItem area={"forwardArrows"}>
<Flex justifySelf={'end'}> <Flex justifySelf={'end'}>
<Tooltip label="Next Page"> <Tooltip content="Next Page">
<IconButton <IconButton
aria-label={'Next Page'} aria-label={'Next Page'}
onMouseDown={nextPage} onMouseDown={nextPage}
isDisabled={pageIndex >= pageCount} disabled={pageIndex >= pageCount}
icon={<ChevronRightIcon h={6} w={6} />} >
/> <LuChevronRight />
</IconButton>
</Tooltip> </Tooltip>
<Tooltip label="Last Page"> <Tooltip content="Last Page">
<IconButton <IconButton
aria-label={'Last Page'} aria-label={'Last Page'}
onMouseDown={() => gotoPage(pageCount)} onMouseDown={() => gotoPage(pageCount)}
isDisabled={pageIndex >= pageCount} disabled={pageIndex >= pageCount}
icon={<ArrowRightIcon h={3} w={3} />}
ml={4} ml={4}
/> >
<LuChevronsRight />
</IconButton>
</Tooltip> </Tooltip>
</Flex> </Flex>
</GridItem> </GridItem>
+17 -17
View File
@@ -1,6 +1,7 @@
import React, { FormEvent } from 'react' import React, { FormEvent } from 'react'
import { Button, FormControl, Input, InputGroup, InputRightElement, useColorModeValue } from '@chakra-ui/react' import { Button, Input, Group } from '@chakra-ui/react'
import { SearchIcon } from '@chakra-ui/icons' import { Field } from './ui/field'
import { LuSearch } from 'react-icons/lu'
import { GameList } from '../models/game' import { GameList } from '../models/game'
import { useHookstate } from '@hookstate/core' import { useHookstate } from '@hookstate/core'
import { gameDashboardState } from '../stateManagement/gameState' import { gameDashboardState } from '../stateManagement/gameState'
@@ -14,6 +15,7 @@ import { adminMode } from '../stateManagement/userState'
import getWithFilters from '../componentUtils/getWithFilters' import getWithFilters from '../componentUtils/getWithFilters'
import {ChangeActiveFilterHeader} from "../componentUtils/changeActiveFilterHeader"; import {ChangeActiveFilterHeader} from "../componentUtils/changeActiveFilterHeader";
import {ratingState, steamRatingState} from "../stateManagement/ratingState"; import {ratingState, steamRatingState} from "../stateManagement/ratingState";
import {useColorModeValue} from "./ui/color-mode";
export default function Search() { export default function Search() {
const gamesState = useHookstate(gameDashboardState) const gamesState = useHookstate(gameDashboardState)
@@ -54,16 +56,16 @@ export default function Search() {
return ( return (
<> <>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<FormControl mb="3"> <Field mb="3">
<InputGroup> <Group attached w="full">
<Input <Input
autoFocus autoFocus
id="searchParams" id="searchParams"
name="searchParams" name="searchParams"
bg={bg} bg={bg}
type="text" type="text"
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder="Search..." placeholder="Search..."
value={games.searchParams} value={games.searchParams}
@@ -71,17 +73,15 @@ export default function Search() {
handleInputChange('searchParams', e.target.value) handleInputChange('searchParams', e.target.value)
}} }}
/> />
<InputRightElement> <Button
<Button loading={submitLoading}
isLoading={submitLoading} type="submit"
type="submit" colorPalette="blue"
colorScheme="blue" >
> <LuSearch/>
<SearchIcon/> </Button>
</Button> </Group>
</InputRightElement> </Field>
</InputGroup>
</FormControl>
</form> </form>
</> </>
) )
+10 -12
View File
@@ -1,8 +1,6 @@
import React from 'react' import React from 'react'
import { import { Dialog } from '@chakra-ui/react'
Modal, ModalBody, ModalCloseButton, ModalContent, import {DialogBody, DialogCloseTrigger, DialogContent, DialogHeader, DialogRoot} from './ui/dialog'
ModalHeader,
} from '@chakra-ui/react'
import { useHookstate } from '@hookstate/core' import { useHookstate } from '@hookstate/core'
import { openAndCloseSearchDialogue } from '../stateManagement/loadingState' import { openAndCloseSearchDialogue } from '../stateManagement/loadingState'
import Search from './Search' import Search from './Search'
@@ -16,14 +14,14 @@ export default function SearchModal() {
} }
return ( return (
<Modal isOpen={isOpen} onClose={handleClose}> <DialogRoot open={isOpen} onOpenChange={(e) => { if (!e.open) handleClose() }}>
<ModalContent> <DialogContent>
<ModalHeader>Search</ModalHeader> <DialogHeader>Search</DialogHeader>
<ModalCloseButton onMouseDown={() => handleClose()}/> <DialogCloseTrigger onClick={() => handleClose()}/>
<ModalBody> <DialogBody>
<Search /> <Search />
</ModalBody> </DialogBody>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
) )
} }
+3 -2
View File
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { Checkbox, Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import {Checkbox} from '../ui/checkbox'
import { adminModeToggle, loggedInUser, adminMode } from '../../stateManagement/userState' import { adminModeToggle, loggedInUser, adminMode } from '../../stateManagement/userState'
import { useHookstate } from '@hookstate/core' import { useHookstate } from '@hookstate/core'
@@ -8,7 +9,7 @@ export default function AdminMode() {
const admin = useHookstate(adminMode).get() const admin = useHookstate(adminMode).get()
return ( return (
<Checkbox size={'lg'} onChange={() => adminModeToggle()} isChecked={admin} isDisabled={!(user.role === 'admin')}> <Checkbox colorPalette="blue" size={'lg'} onCheckedChange={() => adminModeToggle()} checked={admin} disabled={!(user.role === 'admin')}>
<Text mt={'1'}>Administrator Mode</Text> <Text mt={'1'}>Administrator Mode</Text>
</Checkbox> </Checkbox>
) )
@@ -3,11 +3,11 @@ import { useForm } from 'react-hook-form'
import { import {
Container, Container,
Button, Button,
FormControl,
FormLabel,
Input, Input,
useColorModeValue, FormErrorMessage, useToast,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {Field} from '../ui/field'
import {useColorModeValue} from '../ui/color-mode'
import {toaster} from '../ui/toaster'
import agent from '../../api/agent' import agent from '../../api/agent'
import { IForgotPassword } from '../../models/user' import { IForgotPassword } from '../../models/user'
@@ -18,7 +18,6 @@ export default function ForgotPassword() {
handleSubmit, handleSubmit,
formState: { errors, isSubmitting, isDirty }, formState: { errors, isSubmitting, isDirty },
} = useForm() } = useForm()
const toast = useToast()
interface Message { interface Message {
success: boolean success: boolean
@@ -27,27 +26,25 @@ export default function ForgotPassword() {
const onSubmit = handleSubmit(async (values) => { const onSubmit = handleSubmit(async (values) => {
const message = await agent.Account.forgot(values as IForgotPassword) as Message const message = await agent.Account.forgot(values as IForgotPassword) as Message
toast({ toaster.create({
title: 'Email Sent', title: 'Email Sent',
status: 'success', type: 'success',
description: message.data, description: message.data,
isClosable: true, closable: true,
duration: 10000, duration: 10000,
position: 'top'
}) })
}) })
return ( return (
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}> <Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FormControl isInvalid={Boolean(errors.email)} mb={'3'}> <Field label="Email" invalid={Boolean(errors.email)} errorText={errors.email?.type === 'required' ? errors.email.message?.toString() : errors.email?.type === 'pattern' ? 'Invalid Email Address' : undefined} mb={'3'}>
<FormLabel htmlFor="email">Email</FormLabel>
<Input <Input
id="email" id="email"
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder="Email" placeholder="Email"
{...register('email', { {...register('email', {
@@ -55,16 +52,13 @@ export default function ForgotPassword() {
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
})} })}
/> />
<FormErrorMessage> </Field>
{errors.email?.type === 'required' && errors.email.message?.toString()}
{errors.email?.type === 'pattern' && 'Invalid Email Address'}
</FormErrorMessage>
</FormControl>
<br/> <br/>
<Button <Button
type="submit" type="submit"
colorPalette="blue"
disabled={!isDirty} disabled={!isDirty}
isLoading={isSubmitting} loading={isSubmitting}
> >
Submit Submit
</Button> </Button>
+14 -23
View File
@@ -5,11 +5,10 @@ import { login } from '../../stateManagement/userState'
import { import {
Container, Container,
Button, Button,
FormControl, Input, HStack, Center,
FormLabel,
Input,
useColorModeValue, HStack, Center, FormErrorMessage,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {Field} from '../ui/field'
import {useColorModeValue} from '../ui/color-mode'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
export default function LoginForm() { export default function LoginForm() {
@@ -38,14 +37,13 @@ export default function LoginForm() {
return ( return (
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}> <Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FormControl isInvalid={Boolean(errors.email)} mb={'3'}> <Field label="Email" invalid={Boolean(errors.email)} errorText={errors.email?.type === 'required' ? errors.email.message?.toString() : errors.email?.type === 'pattern' ? 'Invalid Email Address' : undefined} mb={'3'}>
<FormLabel htmlFor="email">Email</FormLabel>
<Input <Input
id="email" id="email"
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder="Email" placeholder="Email"
{...register('email', { {...register('email', {
@@ -53,40 +51,33 @@ export default function LoginForm() {
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
})} })}
/> />
<FormErrorMessage> </Field>
{errors.email?.type === 'required' && errors.email.message?.toString()} <Field label="Password" invalid={Boolean(errors.password)} errorText={errors.password?.message?.toString()} mb={'3'}>
{errors.email?.type === 'pattern' && 'Invalid Email Address'}
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={Boolean(errors.password)} mb={'3'}>
<FormLabel htmlFor="password">Password</FormLabel>
<Input <Input
id="password" id="password"
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder="Password" placeholder="Password"
type="password" type="password"
{...register('password', { required: 'You must specify a password' })} {...register('password', { required: 'You must specify a password' })}
/> />
<FormErrorMessage> </Field>
{errors.password && errors.password.message?.toString()}
</FormErrorMessage>
</FormControl>
<br/> <br/>
<HStack> <HStack>
<Button <Button
type="submit" type="submit"
colorPalette="blue"
disabled={!isDirty} disabled={!isDirty}
isLoading={isSubmitting} loading={isSubmitting}
> >
Submit Submit
</Button> </Button>
</HStack> </HStack>
<Center mt={'5rem'}> <Center mt={'5rem'}>
<Button as={Link} to={'/forgotpassword'}>Forgot Password</Button> <Button asChild variant="outline"><Link to={'/forgotpassword'}>Forgot Password</Link></Button>
</Center> </Center>
</form> </form>
</Container> </Container>
+23 -32
View File
@@ -4,10 +4,11 @@ import { IResetPassword } from '../../models/user'
import { import {
Button, Button,
Container, Container,
FormControl, Input,
FormLabel,
Input, useToast, useColorModeValue, FormErrorMessage,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {Field} from '../ui/field'
import {useColorModeValue} from '../ui/color-mode'
import {toaster} from '../ui/toaster'
import agent from '../../api/agent' import agent from '../../api/agent'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from 'react-router-dom'
@@ -17,7 +18,7 @@ interface Message {
} }
interface URL { interface URL {
token: string token?: string
} }
export default function ResetPassword() { export default function ResetPassword() {
@@ -27,32 +28,29 @@ export default function ResetPassword() {
watch, watch,
formState: { errors, isSubmitting, isDirty }, formState: { errors, isSubmitting, isDirty },
} = useForm() } = useForm()
const { token } = useParams<URL>() const { token } = useParams()
const history = useNavigate() const history = useNavigate()
const toast = useToast()
const password = useRef({}) const password = useRef({})
password.current = watch('password', '') password.current = watch('password', '')
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
const message = await agent.Account.reset(token, data as IResetPassword) as Message const message = await agent.Account.reset(token!, data as IResetPassword) as Message
if (message.success) { if (message.success) {
setTimeout(() => history('/login'), 3000) setTimeout(() => history('/login'), 3000)
toast({ toaster.create({
title: 'Password Reset', title: 'Password Reset',
status: 'success', type: 'success',
description: 'Your password has been reset. You will be redirected to login automatically', description: 'Your password has been reset. You will be redirected to login automatically',
isClosable: true, closable: true,
duration: 5000, duration: 5000,
position: 'top',
}) })
} else { } else {
toast({ toaster.create({
title: 'Password Not Reset', title: 'Password Not Reset',
status: 'error', type: 'error',
description: 'Something went wrong, please contact the website administrator', description: 'Something went wrong, please contact the website administrator',
isClosable: true, closable: true,
duration: 10000, duration: 10000,
position: 'top',
}) })
} }
}) })
@@ -62,15 +60,14 @@ export default function ResetPassword() {
return ( return (
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}> <Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FormControl isInvalid={Boolean(errors.password)} mb={'3'}> <Field label="Password" invalid={Boolean(errors.password)} errorText={errors.password?.message?.toString()} mb={'3'}>
<FormLabel htmlFor="password">Password</FormLabel>
<Input <Input
id="password" id="password"
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder="Password" placeholder="Password"
type="password" type="password"
@@ -82,18 +79,14 @@ export default function ResetPassword() {
}, },
})} })}
/> />
<FormErrorMessage> </Field>
{errors.password && errors.password.message?.toString()} <Field label="Repeat Password" invalid={Boolean(errors.repeat_password)} errorText={errors.repeat_password?.message?.toString()} mb={'3'}>
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={Boolean(errors.repeat_password)} mb={'3'}>
<FormLabel htmlFor="repeat_password">Repeat Password</FormLabel>
<Input <Input
id="repeat_password" id="repeat_password"
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder="Repeat Password" placeholder="Repeat Password"
type="password" type="password"
@@ -102,15 +95,13 @@ export default function ResetPassword() {
value === password.current || 'The passwords do not match', value === password.current || 'The passwords do not match',
})} })}
/> />
<FormErrorMessage> </Field>
{errors.repeat_password && errors.repeat_password.message?.toString()}
</FormErrorMessage>
</FormControl>
<br/> <br/>
<Button <Button
type="submit" type="submit"
colorPalette="blue"
disabled={!isDirty} disabled={!isDirty}
isLoading={isSubmitting} loading={isSubmitting}
> >
Submit Submit
</Button> </Button>
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { Center, CircularProgress } from '@chakra-ui/react' import { Center, ProgressCircle } from '@chakra-ui/react'
export default function DashboardLoadingModal() { export default function DashboardLoadingModal() {
@@ -9,7 +9,12 @@ export default function DashboardLoadingModal() {
w="100%" w="100%"
mt={'1rem'} mt={'1rem'}
> >
<CircularProgress isIndeterminate color="blue.500"/> <ProgressCircle.Root value={null} size="md">
<ProgressCircle.Circle>
<ProgressCircle.Track />
<ProgressCircle.Range />
</ProgressCircle.Circle>
</ProgressCircle.Root>
</Center> </Center>
</> </>
) )
@@ -1,35 +1,33 @@
import React from 'react' import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Data } from '../../models/game' import { Data } from '../../models/game'
import { Link as ChakraLink, Flex, Heading, Image, LinkBox, LinkOverlay, Spacer, Text } from '@chakra-ui/react' import { Link as ChakraLink, Flex, Heading, Image, Spacer, Text, Box } from '@chakra-ui/react'
import GameRating from "../detailsComponents/GameRating"; import GameRating from "../detailsComponents/GameRating";
import SteamAndDeckLogo from "../helperComponents/SteamAndDeckLogo"; import SteamAndDeckLogo from "../helperComponents/SteamAndDeckLogo";
interface Props { interface Props {
game: Data game: Data
onToggle: () => void
playState: string playState: string
} }
export default function GameThumbnail({ onToggle, game, playState }: Props) { export default function GameThumbnail({ game, playState }: Props) {
const gameLink = game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 ? game.steamId : game._id const gameLink = game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 ? game.steamId : game._id
return ( return (
<> <>
<LinkBox <Box
as={Link} asChild
to={`/games/${gameLink}`}
onMouseOver={onToggle}
onMouseOut={onToggle}
> >
<LinkOverlay> <Link
to={`/games/${gameLink}`}
>
<Image borderTopRadius="md" src={game.frontImage} alt={game.title} /> <Image borderTopRadius="md" src={game.frontImage} alt={game.title} />
</LinkOverlay> <Heading m={2} size="md" my="2">
<Heading m={2} size="md" my="2"> {game.steamId !== '' ? `${game.steamId}: ${game.title}` : `${game.title}`}
{game.steamId !== '' ? `${game.steamId}: ${game.title}` : `${game.title}`} </Heading>
</Heading> <Text ml={2}>Play Status: {playState}</Text>
<Text ml={2}>Play Status: {playState}</Text> </Link>
</LinkBox> </Box>
<Flex m={2}> <Flex m={2}>
<GameRating game={game} size={'1em'} /> <GameRating game={game} size={'1em'} />
<Spacer /> <Spacer />
@@ -1,9 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Data } from "../../models/game"; import { Data } from "../../models/game";
import { ContextMenu } from 'chakra-ui-contextmenu'; import { Menu, Portal } from '@chakra-ui/react';
import { MenuItem, MenuList } from '@chakra-ui/menu';
import GameThumbnail from "./GameThumbnail"; import GameThumbnail from "./GameThumbnail";
import { Box, MenuGroup, useDisclosure } from "@chakra-ui/react"; import { Box, useDisclosure } from "@chakra-ui/react";
import GameThumbnailSlide from "./GameThumbnailSlide"; import GameThumbnailSlide from "./GameThumbnailSlide";
import { playStatusValues } from "../../componentUtils/SelectValues"; import { playStatusValues } from "../../componentUtils/SelectValues";
import agent from "../../api/agent"; import agent from "../../api/agent";
@@ -14,7 +13,7 @@ interface Props {
} }
const GameThumbnailContextMenu = ({ game }: Props) => { const GameThumbnailContextMenu = ({ game }: Props) => {
const { isOpen, onToggle } = useDisclosure() const { open: isOpen, onOpen, onClose } = useDisclosure()
const [playState, setPlayState] = useState(game.accessedBy[0].playStatus) const [playState, setPlayState] = useState(game.accessedBy[0].playStatus)
const gameLink = game.steamId.length > 1 ? game.steamId : game._id const gameLink = game.steamId.length > 1 ? game.steamId : game._id
@@ -34,20 +33,10 @@ const GameThumbnailContextMenu = ({ game }: Props) => {
let mql = window.matchMedia('(max-width: 768px)'); let mql = window.matchMedia('(max-width: 768px)');
return ( return (
<ContextMenu<HTMLDivElement> key={game._id} renderMenu={() => ( <Menu.Root key={game._id}>
<MenuList> <Menu.ContextTrigger asChild>
<MenuItem><Link to={`/games/${gameLink}`} target="_blank" rel="noopener noreferrer">Open in New Tab</Link></MenuItem>
<MenuGroup title={'Change PlayStatus'}>
{playStatusValues.map(value => <MenuItem
onClick={() => rightClick(value.label, value.value)}
>{value.label}</MenuItem>)}
</MenuGroup>
</MenuList>
)}>
{ref => <>
<Box <Box
ref={ref} borderWidth={'1px'} borderStyle={'solid'}
border={'1px'}
borderColor={'gray.500'} borderColor={'gray.500'}
maxW={'sm'} maxW={'sm'}
rounded={'md'} rounded={'md'}
@@ -55,13 +44,33 @@ const GameThumbnailContextMenu = ({ game }: Props) => {
transitionDuration={'1'} transitionDuration={'1'}
transitionTimingFunction={'ease-in-out'} transitionTimingFunction={'ease-in-out'}
_hover={{ boxShadow: '2xl', transform: 'translateY(-3px)' }} _hover={{ boxShadow: '2xl', transform: 'translateY(-3px)' }}
onMouseEnter={onOpen}
onMouseLeave={onClose}
> >
<GameThumbnail onToggle={onToggle} game={game} playState={playState} /> <GameThumbnail game={game} playState={playState} />
</Box> </Box>
{(!mql.matches) && <GameThumbnailSlide isOpen={isOpen} game={game} />} </Menu.ContextTrigger>
</> <Portal>
} <Menu.Positioner>
</ContextMenu> <Menu.Content p={4}>
<Menu.Item value="open-new-tab" asChild>
<Link to={`/games/${gameLink}`} target="_blank" rel="noopener noreferrer">Open in New Tab</Link>
</Menu.Item>
<Menu.ItemGroup>
<Menu.ItemGroupLabel>Change PlayStatus</Menu.ItemGroupLabel>
{playStatusValues.map((value, idx) => (
<Menu.Item
key={idx}
value={value.value}
onSelect={() => rightClick(value.label, value.value)}
>{value.label}</Menu.Item>
))}
</Menu.ItemGroup>
</Menu.Content>
</Menu.Positioner>
</Portal>
{(!mql.matches) && <GameThumbnailSlide isOpen={isOpen} game={game} />}
</Menu.Root>
); );
}; };
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import {Box, Container, Heading, HStack, Slide} from "@chakra-ui/react"; import {Box, Container, Heading, HStack, Presence} from "@chakra-ui/react";
import {Data} from "../../models/game"; import {Data} from "../../models/game";
import ImageSlider from "../detailsComponents/ImageSlider"; import ImageSlider from "../detailsComponents/ImageSlider";
@@ -10,17 +10,25 @@ interface Props {
const GameThumbnailSlide = ({isOpen, game}: Props) => { const GameThumbnailSlide = ({isOpen, game}: Props) => {
return ( return (
<Slide direction="bottom" in={isOpen} unmountOnExit={true} style={{zIndex: 10}}> <Presence
present={isOpen}
position="fixed"
bottom="0"
insetX="0"
animationName={{ _open: "slide-from-bottom-full", _closed: "slide-to-bottom-full" }}
animationDuration="moderate"
style={{zIndex: 10}}
>
<Box <Box
p="40px" p="40px"
color="white" color="white"
mt="4" mt="4"
bgGradient={'linear(to-br, #314755, #26a0da)'} bgImage={'linear-gradient(to bottom right, #314755, #26a0da)'}
borderTopRadius="md" borderTopRadius="md"
shadow="md" shadow="md"
> >
<Container maxW="container.xl"> <Container maxW="1280px">
<HStack spacing='2rem'> <HStack gap='2rem'>
<ImageSlider game={game} width={'250px'}/> <ImageSlider game={game} width={'250px'}/>
<Box> <Box>
<Heading>{game.title}</Heading> <Heading>{game.title}</Heading>
@@ -29,7 +37,7 @@ const GameThumbnailSlide = ({isOpen, game}: Props) => {
</HStack> </HStack>
</Container> </Container>
</Box> </Box>
</Slide> </Presence>
); );
}; };
+39 -113
View File
@@ -1,5 +1,5 @@
import React, { useState as reactUseState } from 'react' import React, { useState as reactUseState } from 'react'
import { Badge, Box, Divider, Flex, Heading, Link, Text } from '@chakra-ui/react' import { Badge, Box, Separator, Flex, Heading, Link, Text } from '@chakra-ui/react'
import { Data } from '../../models/game' import { Data } from '../../models/game'
import { gameDashboardState } from '../../stateManagement/gameState' import { gameDashboardState } from '../../stateManagement/gameState'
import { ImmutableObject, useHookstate } from '@hookstate/core' import { ImmutableObject, useHookstate } from '@hookstate/core'
@@ -36,48 +36,40 @@ export default function Features({ game }: Props) {
return history('/games') return history('/games')
} }
const badge = (label: string) => (
<Box>
<Badge colorPalette="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
{label}
</Text>
</Badge>
</Box>
)
return ( return (
<Box mb="3"> <Box mb="3">
<Heading as="h3" size="lg">Features and Information</Heading> <Heading as="h3" size="lg">Features and Information</Heading>
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Rating:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Rating:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<GameRating game={game} size={'1.5em'} /> <GameRating game={game} size={'1.5em'} />
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
{game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 && {game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 &&
(<> (<>
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Steam ID:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Steam ID:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Link pt="1" href={`steam://run/${game.steamId}`}>{game.steamId}</Link> <Link pt="1" href={`steam://run/${game.steamId}`}>{game.steamId}</Link>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
</>)} </>)}
{game.series.length > 1 && {game.series.length > 1 &&
(<> (<>
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Series:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Series:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Link onMouseDown={() => { <Link onMouseDown={() => {
gamesState.filters.series.set(game.series) gamesState.filters.series.set(game.series)
@@ -85,16 +77,10 @@ export default function Features({ game }: Props) {
}} pt="1">{game.series}</Link> }} pt="1">{game.series}</Link>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
</>)} </>)}
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Play Status:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Play Status:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Box display={'flex'}> <Box display={'flex'}>
{editMode ? {editMode ?
@@ -120,15 +106,9 @@ export default function Features({ game }: Props) {
</Box> </Box>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Genre:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Genre:
</Text>
</Badge>
</Box>
<Box mt={'2'} ml={'4'}> <Box mt={'2'} ml={'4'}>
{game.genre.map((genre, index) => [ {game.genre.map((genre, index) => [
index > 0 && ", ", index > 0 && ", ",
@@ -141,15 +121,9 @@ export default function Features({ game }: Props) {
])} ])}
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Operating Systems:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Operating Systems:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Text pt="1"> <Text pt="1">
{Object.keys(game.os).filter(os => { {Object.keys(game.os).filter(os => {
@@ -175,15 +149,9 @@ export default function Features({ game }: Props) {
</Text> </Text>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Store:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Store:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
{game.accessedBy[0].store.map((store, index) => [ {game.accessedBy[0].store.map((store, index) => [
index > 0 && ", ", index > 0 && ", ",
@@ -196,15 +164,9 @@ export default function Features({ game }: Props) {
])} ])}
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Developer:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Developer:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
{game.developer.map((developer, index) => [ {game.developer.map((developer, index) => [
index > 0 && ", ", index > 0 && ", ",
@@ -217,15 +179,9 @@ export default function Features({ game }: Props) {
])} ])}
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Publisher:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Publisher:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
{game.publisher.map((publisher, index) => [ {game.publisher.map((publisher, index) => [
index > 0 && ", ", index > 0 && ", ",
@@ -238,15 +194,9 @@ export default function Features({ game }: Props) {
])} ])}
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Controller Support:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Controller Support:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Link onMouseDown={() => { <Link onMouseDown={() => {
gamesState.filters.controller.set(game.controller) gamesState.filters.controller.set(game.controller)
@@ -254,28 +204,16 @@ export default function Features({ game }: Props) {
}} pt="1">{game.controller}</Link> }} pt="1">{game.controller}</Link>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Release Date:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Release Date:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Text pt="1">{game.releaseDate}</Text> <Text pt="1">{game.releaseDate}</Text>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Soundtrack:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Soundtrack:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Link onMouseDown={() => { <Link onMouseDown={() => {
gamesState.filters.soundtrack.set(game.accessedBy[0].soundtrack) gamesState.filters.soundtrack.set(game.accessedBy[0].soundtrack)
@@ -283,15 +221,9 @@ export default function Features({ game }: Props) {
}} pt="1">{game.accessedBy[0].soundtrack}</Link> }} pt="1">{game.accessedBy[0].soundtrack}</Link>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Does it work with Intel?:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Does it work with Intel?:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Link onMouseDown={() => { <Link onMouseDown={() => {
gamesState.filters.intel.set(game.intel) gamesState.filters.intel.set(game.intel)
@@ -299,15 +231,9 @@ export default function Features({ game }: Props) {
}} pt="1">{game.intel}</Link> }} pt="1">{game.intel}</Link>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
<Flex direction="column" m="2"> <Flex direction="column" m="2">
<Box> {badge('Does it work with WINE?:')}
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
<Text pt="1" fontSize="sm">
Does it work with WINE?:
</Text>
</Badge>
</Box>
<Box mt="1.5" ml="5" mr="2"> <Box mt="1.5" ml="5" mr="2">
<Link onMouseDown={() => { <Link onMouseDown={() => {
gamesState.filters.wine.set(game.wine) gamesState.filters.wine.set(game.wine)
@@ -315,7 +241,7 @@ export default function Features({ game }: Props) {
}} pt="1">{game.wine}</Link> }} pt="1">{game.wine}</Link>
</Box> </Box>
</Flex> </Flex>
<Divider orientation="horizontal" /> <Separator borderColor="gray.200" orientation="horizontal" />
</Box> </Box>
) )
} }
@@ -2,11 +2,12 @@ import React from 'react';
import { Data } from '../../models/game' import { Data } from '../../models/game'
import agent from "../../api/agent"; import agent from "../../api/agent";
import {playStatusValues} from "../../componentUtils/SelectValues"; import {playStatusValues} from "../../componentUtils/SelectValues";
import {Select} from "chakra-react-select"; import {Select} from "../FixedSelect";
import {ImmutableObject} from "@hookstate/core";
interface Props { interface Props {
game: Data game: Data | ImmutableObject<Data>
playState: string playState: string
setPlayState: any setPlayState: any
} }
@@ -33,14 +34,14 @@ const GamePlayStatus = ({game, playState, setPlayState}: Props) => {
value: playState, value: playState,
label: playState, label: playState,
}} }}
onChange={(e) => { onChange={(e: any) => {
if(e === null) return if(e === null) return
handleClick(e.value, e.label) handleClick(e.value, e.label)
}} }}
size={'sm'} size={'sm'}
options={playStatusValues} options={playStatusValues}
chakraStyles={{ chakraStyles={{
container: (provided) => ({ container: (provided: any) => ({
...provided, ...provided,
width: "150px" width: "150px"
}) })
@@ -5,10 +5,11 @@ import agent from "../../api/agent";
import {Flex, Link} from "@chakra-ui/react"; import {Flex, Link} from "@chakra-ui/react";
import {IoGameController, IoGameControllerOutline} from "react-icons/io5"; import {IoGameController, IoGameControllerOutline} from "react-icons/io5";
import {BiReset} from "react-icons/bi"; import {BiReset} from "react-icons/bi";
import {ImmutableObject} from "@hookstate/core";
interface Props { interface Props {
game: Data game: Data | ImmutableObject<Data>
size: string size: string
} }
+3 -3
View File
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { Data } from '../../models/game' import { Data } from '../../models/game'
import { Interweave } from 'interweave' import { Interweave } from 'interweave'
import { Divider, Box, Heading } from '@chakra-ui/react' import { Separator, Box, Heading } from '@chakra-ui/react'
import { ImmutableObject } from '@hookstate/core' import { ImmutableObject } from '@hookstate/core'
interface Props { interface Props {
@@ -15,7 +15,7 @@ export default function Summary({ game }: Props) {
{reviews.length >= 1 && ( {reviews.length >= 1 && (
<> <>
<Heading as="h1">Reviews</Heading> <Heading as="h1">Reviews</Heading>
<Divider m="3" /> <Separator borderColor="gray.200" m="3" />
<Interweave <Interweave
content={reviews} content={reviews}
allowAttributes allowAttributes
@@ -24,7 +24,7 @@ export default function Summary({ game }: Props) {
</> </>
)} )}
<Heading mt={reviews.length >= 1 ? '10' : ''} as="h1">About This Game</Heading> <Heading mt={reviews.length >= 1 ? '10' : ''} as="h1">About This Game</Heading>
<Divider m="3" /> <Separator borderColor="gray.200" m="3" />
<Interweave <Interweave
content={summary} content={summary}
allowAttributes allowAttributes
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react' import { Button, Dialog } from '@chakra-ui/react'
import {DialogBody, DialogCloseTrigger, DialogContent, DialogFooter, DialogRoot} from '../ui/dialog'
import Summary from './Summary' import Summary from './Summary'
import { ImmutableObject } from '@hookstate/core' import { ImmutableObject } from '@hookstate/core'
import { Data } from '../../models/game' import { Data } from '../../models/game'
@@ -14,20 +15,19 @@ interface Props {
const SummaryModal = ({ isOpen, onClose, game }: Props) => { const SummaryModal = ({ isOpen, onClose, game }: Props) => {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <DialogRoot open={isOpen} onOpenChange={(e) => { if (!e.open) onClose() }}>
<ModalOverlay /> <DialogContent maxW={'90%'}>
<ModalContent maxW={'90%'}> <DialogCloseTrigger />
<ModalCloseButton /> <DialogBody>
<ModalBody>
<Summary game={game} /> <Summary game={game} />
</ModalBody> </DialogBody>
<ModalFooter> <DialogFooter>
<Button colorScheme='blue' onMouseDown={onClose}> <Button colorPalette='blue' onMouseDown={onClose}>
Close Close
</Button> </Button>
</ModalFooter> </DialogFooter>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
) )
} }
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { TabList, Tab, TabPanel, TabPanels, Tabs, Heading, Box } from '@chakra-ui/react' import { Tabs, Heading, Box } from '@chakra-ui/react'
import { Data } from '../../models/game' import { Data } from '../../models/game'
import { Interweave } from 'interweave' import { Interweave } from 'interweave'
import { ImmutableObject } from '@hookstate/core' import { ImmutableObject } from '@hookstate/core'
@@ -20,51 +20,51 @@ export default function SystemRequirements({ game }: Props) {
return ( return (
<Box> <Box>
<Heading as='h3' size='lg'>System Requirements</Heading> <Heading as='h3' size='lg'>System Requirements</Heading>
<Tabs isLazy mt='3'> <Tabs.Root lazyMount mt='3' defaultValue="windows">
<TabList> <Tabs.List>
<Tab>Windows</Tab> <Tabs.Trigger value="windows">Windows</Tabs.Trigger>
<Tab isDisabled={macMin}>Mac</Tab> <Tabs.Trigger value="mac" disabled={macMin}>Mac</Tabs.Trigger>
<Tab isDisabled={linMin}>Linux</Tab> <Tabs.Trigger value="linux" disabled={linMin}>Linux</Tabs.Trigger>
</TabList> </Tabs.List>
<TabPanels> <Tabs.ContentGroup>
<TabPanel> <Tabs.Content value="windows">
<Tabs isLazy> <Tabs.Root lazyMount defaultValue="win-min">
<TabList> <Tabs.List>
<Tab>Minimum</Tab> <Tabs.Trigger value="win-min">Minimum</Tabs.Trigger>
<Tab isDisabled={winRec}>Recommended</Tab> <Tabs.Trigger value="win-rec" disabled={winRec}>Recommended</Tabs.Trigger>
</TabList> </Tabs.List>
<TabPanels> <Tabs.ContentGroup>
<TabPanel>{<Interweave content={windows.minimum} />}</TabPanel> <Tabs.Content value="win-min">{<Interweave content={windows.minimum} />}</Tabs.Content>
<TabPanel>{<Interweave content={windows.recommended} />}</TabPanel> <Tabs.Content value="win-rec">{<Interweave content={windows.recommended} />}</Tabs.Content>
</TabPanels> </Tabs.ContentGroup>
</Tabs> </Tabs.Root>
</TabPanel> </Tabs.Content>
<TabPanel> <Tabs.Content value="mac">
<Tabs isLazy> <Tabs.Root lazyMount defaultValue="mac-min">
<TabList> <Tabs.List>
<Tab isDisabled={macMin}>Minimum</Tab> <Tabs.Trigger value="mac-min" disabled={macMin}>Minimum</Tabs.Trigger>
<Tab isDisabled={macRec}>Recommended</Tab> <Tabs.Trigger value="mac-rec" disabled={macRec}>Recommended</Tabs.Trigger>
</TabList> </Tabs.List>
<TabPanels> <Tabs.ContentGroup>
<TabPanel>{<Interweave content={mac.minimum} />}</TabPanel> <Tabs.Content value="mac-min">{<Interweave content={mac.minimum} />}</Tabs.Content>
<TabPanel>{<Interweave content={mac.recommended} />}</TabPanel> <Tabs.Content value="mac-rec">{<Interweave content={mac.recommended} />}</Tabs.Content>
</TabPanels> </Tabs.ContentGroup>
</Tabs> </Tabs.Root>
</TabPanel> </Tabs.Content>
<TabPanel> <Tabs.Content value="linux">
<Tabs isLazy> <Tabs.Root lazyMount defaultValue="lin-min">
<TabList> <Tabs.List>
<Tab isDisabled={linMin}>Minimum</Tab> <Tabs.Trigger value="lin-min" disabled={linMin}>Minimum</Tabs.Trigger>
<Tab isDisabled={linRec}>Recommended</Tab> <Tabs.Trigger value="lin-rec" disabled={linRec}>Recommended</Tabs.Trigger>
</TabList> </Tabs.List>
<TabPanels> <Tabs.ContentGroup>
<TabPanel>{<Interweave content={linux.minimum} />}</TabPanel> <Tabs.Content value="lin-min">{<Interweave content={linux.minimum} />}</Tabs.Content>
<TabPanel>{<Interweave content={linux.recommended} />}</TabPanel> <Tabs.Content value="lin-rec">{<Interweave content={linux.recommended} />}</Tabs.Content>
</TabPanels> </Tabs.ContentGroup>
</Tabs> </Tabs.Root>
</TabPanel> </Tabs.Content>
</TabPanels> </Tabs.ContentGroup>
</Tabs> </Tabs.Root>
</Box> </Box>
) )
} }
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react' import { Button } from '@chakra-ui/react'
import {DialogBody, DialogCloseTrigger, DialogContent, DialogFooter, DialogRoot} from '../ui/dialog'
import { ImmutableObject } from '@hookstate/core' import { ImmutableObject } from '@hookstate/core'
import { Data } from '../../models/game' import { Data } from '../../models/game'
import SystemRequirements from './SystemRequirementsMenu' import SystemRequirements from './SystemRequirementsMenu'
@@ -14,20 +15,19 @@ interface Props {
const SummaryModal = ({ isOpen, onClose, game }: Props) => { const SummaryModal = ({ isOpen, onClose, game }: Props) => {
return ( return (
<Modal isOpen={isOpen} onClose={onClose}> <DialogRoot open={isOpen} onOpenChange={(e) => { if (!e.open) onClose() }}>
<ModalOverlay /> <DialogContent maxW={'90%'}>
<ModalContent maxW={'90%'}> <DialogCloseTrigger />
<ModalCloseButton /> <DialogBody>
<ModalBody>
<SystemRequirements game={game} /> <SystemRequirements game={game} />
</ModalBody> </DialogBody>
<ModalFooter> <DialogFooter>
<Button colorScheme='blue' onMouseDown={onClose}> <Button colorPalette='blue' onMouseDown={onClose}>
Close Close
</Button> </Button>
</ModalFooter> </DialogFooter>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
) )
} }
+6 -4
View File
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { Checkbox, Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import {Checkbox} from '../ui/checkbox'
interface Props { interface Props {
label: string label: string
@@ -12,13 +13,14 @@ interface Props {
const CheckboxInput = ({ label, textField, checked, defaultIsChecked, handleOSChange }: Props) => { const CheckboxInput = ({ label, textField, checked, defaultIsChecked, handleOSChange }: Props) => {
return ( return (
<Checkbox <Checkbox
colorPalette="blue"
defaultChecked={defaultIsChecked} defaultChecked={defaultIsChecked}
isChecked={checked} checked={checked}
onChange={() => { onCheckedChange={() => {
handleOSChange(textField) handleOSChange(textField)
}} }}
name={textField} name={textField}
spacing="1rem" gap="1rem"
><Text mt="1">{label}</Text></Checkbox> ><Text mt="1">{label}</Text></Checkbox>
) )
} }
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { Box, FormControl, FormLabel } from '@chakra-ui/react' import { Box } from '@chakra-ui/react'
import { CreatableSelect } from 'chakra-react-select' import { Field } from '../ui/field'
import { CreatableSelect } from '../FixedSelect'
interface Props { interface Props {
label: string label: string
@@ -13,13 +14,12 @@ interface Props {
const CreatableSelectInput = ({ label, textField, value, bg, handleInputChange, options}: Props) => { const CreatableSelectInput = ({ label, textField, value, bg, handleInputChange, options}: Props) => {
return ( return (
<FormControl mb="3"> <Field mb="3" label={label}>
<FormLabel>{label}</FormLabel>
<Box <Box
bg={bg} bg={bg}
border="1px" rounded="md"
borderColor="gray.500" w="full"
rounded="md"> >
<CreatableSelect <CreatableSelect
name={textField} name={textField}
isClearable isClearable
@@ -39,7 +39,7 @@ const CreatableSelectInput = ({ label, textField, value, bg, handleInputChange,
options={options} options={options}
/> />
</Box> </Box>
</FormControl> </Field>
) )
} }
+7 -9
View File
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {Box, FormControl, FormLabel} from '@chakra-ui/react' import {Box} from '@chakra-ui/react'
import {Select} from 'chakra-react-select' import { Field } from '../ui/field'
import {Select} from '../FixedSelect'
interface Props { interface Props {
label: string label: string
@@ -14,18 +15,15 @@ interface Props {
const SelectInput = ({label, textField, field, bg, handleAccessedBy, options, width = ''}: Props) => { const SelectInput = ({label, textField, field, bg, handleAccessedBy, options, width = ''}: Props) => {
return ( return (
<FormControl mb="3"> <Field mb="3" label={label}>
<FormLabel>{label}</FormLabel>
<Box <Box
bg={bg} bg={bg}
w={width} w={width || 'full'}
border="1px"
borderColor="gray.500"
rounded="md"> rounded="md">
<Select <Select
id={textField} id={textField}
name={textField} name={textField}
onChange={(values) => { onChange={(values: any) => {
if (values) handleAccessedBy(textField, values.value) if (values) handleAccessedBy(textField, values.value)
}} }}
options={options} options={options}
@@ -35,7 +33,7 @@ const SelectInput = ({label, textField, field, bg, handleAccessedBy, options, wi
}} }}
/> />
</Box> </Box>
</FormControl> </Field>
) )
} }
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import { FormControl, FormLabel, Input } from '@chakra-ui/react' import { Input } from '@chakra-ui/react'
import { Field } from '../ui/field'
import { Data } from '../../models/game' import { Data } from '../../models/game'
interface Props { interface Props {
@@ -14,15 +15,14 @@ interface Props {
const SingleFieldInput = ({ label, textField, field, bg, handleInputChange, required }: Props) => { const SingleFieldInput = ({ label, textField, field, bg, handleInputChange, required }: Props) => {
return ( return (
<FormControl mb="3"> <Field mb="3" label={label}>
<FormLabel>{label}</FormLabel>
<Input <Input
id={textField} id={textField}
name={textField} name={textField}
bg={bg} bg={bg}
type="text" type="text"
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
value={field} value={field}
required={required} required={required}
@@ -30,7 +30,7 @@ const SingleFieldInput = ({ label, textField, field, bg, handleInputChange, requ
handleInputChange(textField, e.target.value) handleInputChange(textField, e.target.value)
}} }}
/> />
</FormControl> </Field>
) )
} }
@@ -1,39 +1,63 @@
import React from 'react' import React from "react";
import { FormControl, FormLabel, Input } from '@chakra-ui/react' import { Input } from "@chakra-ui/react";
import { Data } from '../../models/game' import { Field } from "../ui/field";
import { Data } from "../../models/game";
interface Props { interface Props {
label: string label: string;
textField: string textField: string;
field: string field: string;
bg: string bg: string;
handleInputChange: <G extends keyof Data>(name: string, value: Data[G]) => void handleInputChange: <G extends keyof Data>(
required: boolean name: string,
value: Data[G],
) => void;
required: boolean;
} }
const SingleFieldNumericInput = ({ label, textField, field, bg, handleInputChange, required }: Props) => { const SingleFieldNumericInput = ({
label,
textField,
field,
bg,
handleInputChange,
required,
}: Props) => {
const isValidSteamValue = (v: string) => {
const s = v.trim();
if (!s) return false;
if (/^\d+$/.test(s)) return true;
return s.includes("store.steampowered.com");
};
const showError = field.trim() !== "" && !isValidSteamValue(field);
return ( return (
<FormControl mb="3"> <Field
<FormLabel>{label}</FormLabel> mb="3"
label={label}
errorText="Enter a Steam App Id or a Steam App URL"
>
<Input <Input
id={textField} id={textField}
name={textField} name={textField}
bg={bg} bg={bg}
type="text" type="text"
inputMode="numeric" inputMode="text"
pattern="[0-9]*" aria-invalid={showError}
border="1px" placeholder="Steam Id or Steam URL"
borderColor="gray.500" borderWidth="1px"
borderStyle="solid"
borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
value={field} value={field}
required={required} required={required}
onChange={(e) => { onChange={(e) => {
handleInputChange(textField, e.target.value) handleInputChange(textField, e.target.value);
}} }}
/> />
</FormControl> </Field>
) );
} };
export default SingleFieldNumericInput export default SingleFieldNumericInput;
@@ -1,5 +1,5 @@
import React, {useMemo, useRef} from 'react' import React, {useMemo, useRef} from 'react'
import {FormControl, FormLabel} from '@chakra-ui/react' import {Field} from '../ui/field'
import JoditEditor from "jodit-react"; import JoditEditor from "jodit-react";
interface Props { interface Props {
@@ -24,17 +24,16 @@ const SystemRequirementsTextareaInput = ({label, textField, field, handleSystemR
); );
return ( return (
<FormControl mb="3"> <Field mb="3" label={label}>
<FormLabel>{label}</FormLabel>
<JoditEditor <JoditEditor
ref={editor} ref={editor}
value={field} value={field}
config={config} config={config}
onChange={(value) => { onChange={(value: string) => {
handleSystemRequirements(arrText[1], arrText[2], value) handleSystemRequirements(arrText[1], arrText[2], value)
}} }}
/> />
</FormControl> </Field>
) )
} }
+4 -5
View File
@@ -1,5 +1,5 @@
import React, {useMemo, useRef} from 'react' import React, {useMemo, useRef} from 'react'
import { FormControl, FormLabel } from '@chakra-ui/react' import { Field } from '../ui/field'
import { Data } from '../../models/game' import { Data } from '../../models/game'
import JoditEditor from "jodit-react"; import JoditEditor from "jodit-react";
@@ -25,17 +25,16 @@ const TextareaInput = ({ label, textField, field, handleInputChange }: Props) =>
return ( return (
<FormControl mb="3"> <Field mb="3" label={label}>
<FormLabel>{label}</FormLabel>
<JoditEditor <JoditEditor
ref={editor} ref={editor}
value={field} value={field}
config={config} config={config}
onChange={(value) => { onChange={(value: string) => {
handleInputChange(textField, value) handleInputChange(textField, value)
}} }}
/> />
</FormControl> </Field>
) )
} }
@@ -9,12 +9,12 @@ import { ratingState, steamRatingState } from "../../stateManagement/ratingState
interface Props { interface Props {
text?: string text?: string
size?: string size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl"
variant?: string variant?: "solid" | "subtle" | "surface" | "outline" | "ghost" | "plain"
colorScheme?: string colorPalette?: string
} }
const ResetFilterButton = ({ text = "Reset Search and Filters", size = 'md', variant = 'solid', colorScheme = 'gray' }: Props) => { const ResetFilterButton = ({ text = "Reset Search and Filters", size = 'md', variant = 'solid', colorPalette = 'gray' }: Props) => {
const gameState = useHookstate(gameDashboardState) const gameState = useHookstate(gameDashboardState)
const games = gameState.get() const games = gameState.get()
const admin = useHookstate(adminMode).get() const admin = useHookstate(adminMode).get()
@@ -58,7 +58,7 @@ const ResetFilterButton = ({ text = "Reset Search and Filters", size = 'md', var
} }
return ( return (
<Button onMouseDown={resetFilters} size={size} variant={variant} colorScheme={colorScheme}> <Button onMouseDown={resetFilters} size={size} variant={variant} colorPalette={colorPalette}>
{text} {text}
</Button> </Button>
) )
@@ -9,17 +9,19 @@ const SteamAndDeckLogo = (props: ISteamAndDeckLogo) => {
return ( return (
<div> <div>
<Box <Box
as="svg" asChild
role="img"
width={props.size} width={props.size}
height={props.size} height={props.size}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1080 1080"
fill="#1f2127"
_hover={{ _hover={{
fill: "brand.accent", fill: "blue.500",
}} }}
> >
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1080 1080"
fill="#1f2127"
>
<title>SteamAndDeckLogo</title> <title>SteamAndDeckLogo</title>
<path <path
fill="url(#_Radial1)" fill="url(#_Radial1)"
@@ -97,6 +99,7 @@ const SteamAndDeckLogo = (props: ISteamAndDeckLogo) => {
<stop offset="1" stopColor="#00A6FF" stopOpacity="0.6"></stop> <stop offset="1" stopColor="#00A6FF" stopOpacity="0.6"></stop>
</linearGradient> </linearGradient>
</defs> </defs>
</svg>
</Box> </Box>
</div> </div>
); );
+29
View File
@@ -0,0 +1,29 @@
import { Alert as ChakraAlert } from "@chakra-ui/react"
import * as React from "react"
export interface AlertProps extends Omit<ChakraAlert.RootProps, "title"> {
startElement?: React.ReactNode
endElement?: React.ReactNode
title?: React.ReactNode
icon?: React.ReactElement
}
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
function Alert(props, ref) {
const { title, children, icon, startElement, endElement, ...rest } = props
return (
<ChakraAlert.Root ref={ref} {...rest}>
{startElement || <ChakraAlert.Indicator>{icon}</ChakraAlert.Indicator>}
{children ? (
<ChakraAlert.Content>
<ChakraAlert.Title>{title}</ChakraAlert.Title>
<ChakraAlert.Description>{children}</ChakraAlert.Description>
</ChakraAlert.Content>
) : (
<ChakraAlert.Title flex="1">{title}</ChakraAlert.Title>
)}
{endElement}
</ChakraAlert.Root>
)
},
)
+34
View File
@@ -0,0 +1,34 @@
import {
Avatar as ChakraAvatar,
AvatarGroup as ChakraAvatarGroup,
} from "@chakra-ui/react"
import * as React from "react"
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
export interface AvatarProps extends ChakraAvatar.RootProps {
name?: string
src?: string
srcSet?: string
loading?: ImageProps["loading"]
icon?: React.ReactElement
fallback?: React.ReactNode
}
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
function Avatar(props, ref) {
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
props
return (
<ChakraAvatar.Root ref={ref} {...rest}>
<ChakraAvatar.Fallback name={name}>
{icon || fallback}
</ChakraAvatar.Fallback>
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
{children}
</ChakraAvatar.Root>
)
},
)
export const AvatarGroup = ChakraAvatarGroup
+25
View File
@@ -0,0 +1,25 @@
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
import * as React from "react"
export interface CheckboxProps extends ChakraCheckbox.RootProps {
icon?: React.ReactNode
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
rootRef?: React.RefObject<HTMLLabelElement | null>
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
function Checkbox(props, ref) {
const { icon, children, inputProps, rootRef, ...rest } = props
return (
<ChakraCheckbox.Root ref={rootRef} {...rest}>
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
<ChakraCheckbox.Control>
{icon || <ChakraCheckbox.Indicator />}
</ChakraCheckbox.Control>
{children != null && (
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
)}
</ChakraCheckbox.Root>
)
},
)
+17
View File
@@ -0,0 +1,17 @@
import type { ButtonProps } from "@chakra-ui/react"
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
import * as React from "react"
import { LuX } from "react-icons/lu"
export type CloseButtonProps = ButtonProps
export const CloseButton = React.forwardRef<
HTMLButtonElement,
CloseButtonProps
>(function CloseButton(props, ref) {
return (
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
{props.children ?? <LuX />}
</ChakraIconButton>
)
})
+108
View File
@@ -0,0 +1,108 @@
"use client"
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
import { ThemeProvider, useTheme } from "next-themes"
import type { ThemeProviderProps } from "next-themes"
import * as React from "react"
import { LuMoon, LuSun } from "react-icons/lu"
export interface ColorModeProviderProps extends ThemeProviderProps {}
export function ColorModeProvider(props: ColorModeProviderProps) {
return (
<ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange {...props} />
)
}
export type ColorMode = "light" | "dark"
export interface UseColorModeReturn {
colorMode: ColorMode
setColorMode: (colorMode: ColorMode) => void
toggleColorMode: () => void
}
export function useColorMode(): UseColorModeReturn {
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
const colorMode = forcedTheme || resolvedTheme
const toggleColorMode = () => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
return {
colorMode: colorMode as ColorMode,
setColorMode: setTheme,
toggleColorMode,
}
}
export function useColorModeValue<T>(light: T, dark: T) {
const { colorMode } = useColorMode()
return colorMode === "dark" ? dark : light
}
export function ColorModeIcon() {
const { colorMode } = useColorMode()
return colorMode === "dark" ? <LuMoon /> : <LuSun />
}
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
export const ColorModeButton = React.forwardRef<
HTMLButtonElement,
ColorModeButtonProps
>(function ColorModeButton(props, ref) {
const { toggleColorMode } = useColorMode()
return (
<ClientOnly fallback={<Skeleton boxSize="9" />}>
<IconButton
onClick={toggleColorMode}
variant="ghost"
aria-label="Toggle color mode"
size="sm"
ref={ref}
{...props}
css={{
_icon: {
width: "5",
height: "5",
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
)
})
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
function LightMode(props, ref) {
return (
<Span
color="fg"
display="contents"
className="chakra-theme light"
colorPalette="gray"
colorScheme="light"
ref={ref}
{...props}
/>
)
},
)
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
function DarkMode(props, ref) {
return (
<Span
color="fg"
display="contents"
className="chakra-theme dark"
colorPalette="gray"
colorScheme="dark"
ref={ref}
{...props}
/>
)
},
)
+62
View File
@@ -0,0 +1,62 @@
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
backdrop?: boolean
}
export const DialogContent = React.forwardRef<
HTMLDivElement,
DialogContentProps
>(function DialogContent(props, ref) {
const {
children,
portalled = true,
portalRef,
backdrop = true,
...rest
} = props
return (
<Portal disabled={!portalled} container={portalRef}>
{backdrop && <ChakraDialog.Backdrop />}
<ChakraDialog.Positioner>
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDialog.Content>
</ChakraDialog.Positioner>
</Portal>
)
})
export const DialogCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDialog.CloseTriggerProps
>(function DialogCloseTrigger(props, ref) {
return (
<ChakraDialog.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref}>
{props.children}
</CloseButton>
</ChakraDialog.CloseTrigger>
)
})
export const DialogRoot = ChakraDialog.Root
export const DialogFooter = ChakraDialog.Footer
export const DialogHeader = ChakraDialog.Header
export const DialogBody = ChakraDialog.Body
export const DialogBackdrop = ChakraDialog.Backdrop
export const DialogTitle = ChakraDialog.Title
export const DialogDescription = ChakraDialog.Description
export const DialogTrigger = ChakraDialog.Trigger
export const DialogActionTrigger = ChakraDialog.ActionTrigger
+61
View File
@@ -0,0 +1,61 @@
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface DrawerContentProps extends ChakraDrawer.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
offset?: ChakraDrawer.ContentProps["padding"]
backdrop?: boolean
}
export const DrawerContent = React.forwardRef<
HTMLDivElement,
DrawerContentProps
>(function DrawerContent(props, ref) {
const {
children,
portalled = true,
portalRef,
offset,
backdrop = true,
...rest
} = props
return (
<Portal disabled={!portalled} container={portalRef}>
{backdrop && <ChakraDrawer.Backdrop />}
<ChakraDrawer.Positioner padding={offset}>
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDrawer.Content>
</ChakraDrawer.Positioner>
</Portal>
)
})
export const DrawerCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDrawer.CloseTriggerProps
>(function DrawerCloseTrigger(props, ref) {
return (
<ChakraDrawer.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref} />
</ChakraDrawer.CloseTrigger>
)
})
export const DrawerTrigger = ChakraDrawer.Trigger
export const DrawerRoot = ChakraDrawer.Root
export const DrawerFooter = ChakraDrawer.Footer
export const DrawerHeader = ChakraDrawer.Header
export const DrawerBody = ChakraDrawer.Body
export const DrawerBackdrop = ChakraDrawer.Backdrop
export const DrawerDescription = ChakraDrawer.Description
export const DrawerTitle = ChakraDrawer.Title
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
+33
View File
@@ -0,0 +1,33 @@
import { Field as ChakraField } from "@chakra-ui/react"
import * as React from "react"
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
label?: React.ReactNode
helperText?: React.ReactNode
errorText?: React.ReactNode
optionalText?: React.ReactNode
}
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
function Field(props, ref) {
const { label, children, helperText, errorText, optionalText, ...rest } =
props
return (
<ChakraField.Root ref={ref} {...rest}>
{label && (
<ChakraField.Label textStyle="md">
{label}
<ChakraField.RequiredIndicator fallback={optionalText} />
</ChakraField.Label>
)}
{children}
{helperText && (
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
)}
{errorText && (
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
)}
</ChakraField.Root>
)
},
)
+112
View File
@@ -0,0 +1,112 @@
"use client"
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react"
import * as React from "react"
import { LuCheck, LuChevronRight } from "react-icons/lu"
interface MenuContentProps extends ChakraMenu.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
}
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
function MenuContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraMenu.Positioner>
<ChakraMenu.Content ref={ref} {...rest} />
</ChakraMenu.Positioner>
</Portal>
)
},
)
export const MenuArrow = React.forwardRef<
HTMLDivElement,
ChakraMenu.ArrowProps
>(function MenuArrow(props, ref) {
return (
<ChakraMenu.Arrow ref={ref} {...props}>
<ChakraMenu.ArrowTip />
</ChakraMenu.Arrow>
)
})
export const MenuCheckboxItem = React.forwardRef<
HTMLDivElement,
ChakraMenu.CheckboxItemProps
>(function MenuCheckboxItem(props, ref) {
return (
<ChakraMenu.CheckboxItem ps="8" ref={ref} {...props}>
<AbsoluteCenter axis="horizontal" insetStart="4" asChild>
<ChakraMenu.ItemIndicator>
<LuCheck />
</ChakraMenu.ItemIndicator>
</AbsoluteCenter>
{props.children}
</ChakraMenu.CheckboxItem>
)
})
export const MenuRadioItem = React.forwardRef<
HTMLDivElement,
ChakraMenu.RadioItemProps
>(function MenuRadioItem(props, ref) {
const { children, ...rest } = props
return (
<ChakraMenu.RadioItem ps="8" ref={ref} {...rest}>
<AbsoluteCenter axis="horizontal" insetStart="4" asChild>
<ChakraMenu.ItemIndicator>
<LuCheck />
</ChakraMenu.ItemIndicator>
</AbsoluteCenter>
<ChakraMenu.ItemText>{children}</ChakraMenu.ItemText>
</ChakraMenu.RadioItem>
)
})
export const MenuItemGroup = React.forwardRef<
HTMLDivElement,
ChakraMenu.ItemGroupProps
>(function MenuItemGroup(props, ref) {
const { title, children, ...rest } = props
return (
<ChakraMenu.ItemGroup ref={ref} {...rest}>
{title && (
<ChakraMenu.ItemGroupLabel userSelect="none">
{title}
</ChakraMenu.ItemGroupLabel>
)}
{children}
</ChakraMenu.ItemGroup>
)
})
export interface MenuTriggerItemProps extends ChakraMenu.ItemProps {
startIcon?: React.ReactNode
}
export const MenuTriggerItem = React.forwardRef<
HTMLDivElement,
MenuTriggerItemProps
>(function MenuTriggerItem(props, ref) {
const { startIcon, children, ...rest } = props
return (
<ChakraMenu.TriggerItem ref={ref} {...rest}>
{startIcon}
{children}
<LuChevronRight />
</ChakraMenu.TriggerItem>
)
})
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup
export const MenuContextTrigger = ChakraMenu.ContextTrigger
export const MenuRoot = ChakraMenu.Root
export const MenuSeparator = ChakraMenu.Separator
export const MenuItem = ChakraMenu.Item
export const MenuItemText = ChakraMenu.ItemText
export const MenuItemCommand = ChakraMenu.ItemCommand
export const MenuTrigger = ChakraMenu.Trigger
+28
View File
@@ -0,0 +1,28 @@
import { NumberInput as ChakraNumberInput } from "@chakra-ui/react"
import * as React from "react"
export interface NumberInputProps extends ChakraNumberInput.RootProps {}
export const NumberInputRoot = React.forwardRef<
HTMLDivElement,
NumberInputProps
>(function NumberInput(props, ref) {
const { children, ...rest } = props
return (
<ChakraNumberInput.Root
ref={ref}
variant="outline"
{...rest}
>
{children}
<ChakraNumberInput.Control>
<ChakraNumberInput.IncrementTrigger />
<ChakraNumberInput.DecrementTrigger />
</ChakraNumberInput.Control>
</ChakraNumberInput.Root>
)
})
export const NumberInputField = ChakraNumberInput.Input
export const NumberInputScrubber = ChakraNumberInput.Scrubber
export const NumberInputLabel = ChakraNumberInput.Label
+16
View File
@@ -0,0 +1,16 @@
"use client"
import { ChakraProvider } from "@chakra-ui/react"
import {
ColorModeProvider,
type ColorModeProviderProps,
} from "./color-mode"
import system from "../../styles/theme"
export function Provider(props: ColorModeProviderProps) {
return (
<ChakraProvider value={system}>
<ColorModeProvider {...props} />
</ChakraProvider>
)
}
+39
View File
@@ -0,0 +1,39 @@
import { Switch as ChakraSwitch } from "@chakra-ui/react"
import * as React from "react"
export interface SwitchProps extends ChakraSwitch.RootProps {
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
rootRef?: React.RefObject<HTMLLabelElement | null>
trackLabel?: { on: React.ReactNode; off: React.ReactNode }
thumbLabel?: { on: React.ReactNode; off: React.ReactNode }
}
export const Switch = React.forwardRef<HTMLInputElement, SwitchProps>(
function Switch(props, ref) {
const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } =
props
return (
<ChakraSwitch.Root ref={rootRef} {...rest}>
<ChakraSwitch.HiddenInput ref={ref} {...inputProps} />
<ChakraSwitch.Control>
<ChakraSwitch.Thumb>
{thumbLabel && (
<ChakraSwitch.ThumbIndicator fallback={thumbLabel?.off}>
{thumbLabel?.on}
</ChakraSwitch.ThumbIndicator>
)}
</ChakraSwitch.Thumb>
{trackLabel && (
<ChakraSwitch.Indicator fallback={trackLabel.off}>
{trackLabel.on}
</ChakraSwitch.Indicator>
)}
</ChakraSwitch.Control>
{children != null && (
<ChakraSwitch.Label>{children}</ChakraSwitch.Label>
)}
</ChakraSwitch.Root>
)
},
)
+42
View File
@@ -0,0 +1,42 @@
import { Tag as ChakraTag } from "@chakra-ui/react"
import * as React from "react"
export interface TagProps extends ChakraTag.RootProps {
startElement?: React.ReactNode
endElement?: React.ReactNode
onClose?: VoidFunction
closable?: boolean
}
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
function Tag(props, ref) {
const {
startElement,
endElement,
onClose,
closable = !!onClose,
children,
size,
...rest
} = props
// v2 tag text per size (v3 renders one step smaller)
const labelTextStyle = size === 'lg' ? 'md' : size === 'sm' ? 'xs' : 'sm'
return (
<ChakraTag.Root ref={ref} size={size} {...rest}>
{startElement && (
<ChakraTag.StartElement>{startElement}</ChakraTag.StartElement>
)}
<ChakraTag.Label textStyle={labelTextStyle}>{children}</ChakraTag.Label>
{endElement && (
<ChakraTag.EndElement>{endElement}</ChakraTag.EndElement>
)}
{closable && (
<ChakraTag.EndElement>
<ChakraTag.CloseTrigger onClick={onClose} />
</ChakraTag.EndElement>
)}
</ChakraTag.Root>
)
},
)
+43
View File
@@ -0,0 +1,43 @@
"use client"
import {
Toaster as ChakraToaster,
Portal,
Spinner,
Stack,
Toast,
createToaster,
} from "@chakra-ui/react"
export const toaster = createToaster({
placement: "bottom-end",
pauseOnPageIdle: true,
})
export const Toaster = () => {
return (
<Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
{(toast) => (
<Toast.Root width={{ md: "sm" }}>
{toast.type === "loading" ? (
<Spinner size="sm" color="blue.solid" />
) : (
<Toast.Indicator />
)}
<Stack gap="1" flex="1" maxWidth="100%">
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
{toast.description && (
<Toast.Description>{toast.description}</Toast.Description>
)}
</Stack>
{toast.action && (
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
)}
{toast.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
)
}
+46
View File
@@ -0,0 +1,46 @@
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
import * as React from "react"
export interface TooltipProps extends ChakraTooltip.RootProps {
showArrow?: boolean
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
content: React.ReactNode
contentProps?: ChakraTooltip.ContentProps
disabled?: boolean
}
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
function Tooltip(props, ref) {
const {
showArrow,
children,
disabled,
portalled = true,
content,
contentProps,
portalRef,
...rest
} = props
if (disabled) return children
return (
<ChakraTooltip.Root {...rest}>
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
<Portal disabled={!portalled} container={portalRef}>
<ChakraTooltip.Positioner>
<ChakraTooltip.Content ref={ref} {...contentProps}>
{showArrow && (
<ChakraTooltip.Arrow>
<ChakraTooltip.ArrowTip />
</ChakraTooltip.Arrow>
)}
{content}
</ChakraTooltip.Content>
</ChakraTooltip.Positioner>
</Portal>
</ChakraTooltip.Root>
)
},
)
+30 -45
View File
@@ -3,15 +3,13 @@ import { useForm } from 'react-hook-form'
import { UserFormValues } from '../../models/user' import { UserFormValues } from '../../models/user'
import { createUser } from '../../stateManagement/userState' import { createUser } from '../../stateManagement/userState'
import { import {
Alert,
AlertDescription,
AlertIcon,
Button, Button,
Container, Container,
FormControl, FormErrorMessage, Input,
FormLabel,
Input, useColorModeValue,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {Field} from '../ui/field'
import {Alert} from '../ui/alert'
import {useColorModeValue} from '../ui/color-mode'
export default function CreateUser() { export default function CreateUser() {
const { const {
@@ -44,59 +42,52 @@ export default function CreateUser() {
return ( return (
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}> <Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FormControl isInvalid={Boolean(errors.name)} mb={'3'}> <Field label="Full Name" invalid={Boolean(errors.name)} errorText={errors.name?.message?.toString()} mb={'3'}>
<FormLabel htmlFor='name'>Full Name</FormLabel>
<Input <Input
id='name' id='name'
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder='Full Name' placeholder='Full Name'
{...register('name', { required: 'Name is required' })} {...register('name', { required: 'Name is required' })}
/> />
{errors.name && <FormErrorMessage>{ errors.name.message?.toString()}</FormErrorMessage>} </Field>
</FormControl> <Field label="Display Name" invalid={Boolean(errors.displayName)} errorText={errors.displayName?.message?.toString()} mb={'3'}>
<FormControl isInvalid={Boolean(errors.displayName)} mb={'3'}>
<FormLabel htmlFor='displayName'>Display Name</FormLabel>
<Input <Input
id='displayName' id='displayName'
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder='Display Name' placeholder='Display Name'
{...register('displayName', { required: 'Display Name is required' })} {...register('displayName', { required: 'Display Name is required' })}
/> />
{errors.displayName && <FormErrorMessage>{ errors.displayName.message?.toString()}</FormErrorMessage>} </Field>
</FormControl> <Field label="Email" invalid={Boolean(errors.email)} errorText={errors.email?.message?.toString()} mb={'3'}>
<FormControl isInvalid={Boolean(errors.email)} mb={'3'}>
<FormLabel htmlFor='email'>Email</FormLabel>
<Input <Input
id='email' id='email'
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder='Email' placeholder='Email'
{...register('email', { required: 'Email is required' })} {...register('email', { required: 'Email is required' })}
/> />
{errors.email && <FormErrorMessage>{ errors.email.message?.toString()}</FormErrorMessage>} </Field>
</FormControl> <Field label="Password" invalid={Boolean(errors.password)} errorText={errors.password?.message?.toString()} mb={'3'}>
<FormControl isInvalid={Boolean(errors.password)} mb={'3'}>
<FormLabel htmlFor='password'>Password</FormLabel>
<Input <Input
id='password' id='password'
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder='Password' placeholder='Password'
type='password' type='password'
@@ -108,26 +99,22 @@ export default function CreateUser() {
}, },
})} })}
/> />
{errors.password && <FormErrorMessage>{ errors.password.message?.toString()}</FormErrorMessage>} </Field>
</FormControl>
{errors.password && ( {errors.password && (
<> <>
<Alert status='error' style={{ marginBottom: 10 }}> <Alert status='error' title={errors.password.message?.toString()} style={{ marginBottom: 10 }}>
<AlertIcon /> {errors.password.message?.toString()}
<AlertDescription>{errors.password.message?.toString()}</AlertDescription>
{/*<CloseButton position='absolute' right='8px' top='8px' />*/}
</Alert> </Alert>
<br /> <br />
</> </>
)} )}
<FormControl isInvalid={Boolean(errors.repeat_password)} mb={'3'}> <Field label="Repeat Password" invalid={Boolean(errors.repeat_password)} errorText={errors.repeat_password?.message?.toString()} mb={'3'}>
<FormLabel htmlFor='repeat_password'>Repeat Password</FormLabel>
<Input <Input
id='repeat_password' id='repeat_password'
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder='Repeat Password' placeholder='Repeat Password'
type='password' type='password'
@@ -136,22 +123,20 @@ export default function CreateUser() {
value === password.current || 'The passwords do not match', value === password.current || 'The passwords do not match',
})} })}
/> />
{errors.repeat_password && <FormErrorMessage>{ errors.repeat_password.message?.toString()}</FormErrorMessage>} </Field>
</FormControl>
{errors.repeat_password && ( {errors.repeat_password && (
<> <>
<Alert status='error' style={{ marginBottom: 10 }}> <Alert status='error' title={errors.repeat_password.message?.toString()} style={{ marginBottom: 10 }}>
<AlertIcon /> {errors.repeat_password.message?.toString()}
<AlertDescription>{errors.repeat_password.message?.toString()}</AlertDescription>
{/*<CloseButton position='absolute' right='8px' top='8px' />*/}
</Alert> </Alert>
</> </>
)} )}
<br /> <br />
<Button <Button
type='submit' type='submit'
colorPalette="blue"
disabled={!isDirty} disabled={!isDirty}
isLoading={isSubmitting} loading={isSubmitting}
> >
Submit Submit
</Button> </Button>
+33 -44
View File
@@ -5,14 +5,14 @@ import { loadedPreferences, loggedInUser, updateUserPreferences } from '../../st
import { import {
Button, Button,
Container, Flex, Container, Flex,
FormControl,
FormLabel,
Heading, Heading,
Input, Input,
Stack, Switch, Stack,
useColorModeValue,
useToast,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {Field} from '../ui/field'
import {Switch} from '../ui/switch'
import {useColorModeValue} from '../ui/color-mode'
import {toaster} from '../ui/toaster'
import { useHookstate } from '@hookstate/core' import { useHookstate } from '@hookstate/core'
import agent from '../../api/agent' import agent from '../../api/agent'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
@@ -43,12 +43,10 @@ export default function EditUser() {
const history = useNavigate() const history = useNavigate()
useEffect(() => { useEffect(() => {
reset(user) reset(user as any)
return return
}, [user.name !== '']) }, [user.name !== ''])
const toast = useToast()
interface Message { interface Message {
success: boolean success: boolean
data: string data: string
@@ -66,13 +64,12 @@ export default function EditUser() {
const engageChangePassword = async (values: IForgotPassword) => { const engageChangePassword = async (values: IForgotPassword) => {
const message = await agent.Account.forgot(values as IForgotPassword) as Message const message = await agent.Account.forgot(values as IForgotPassword) as Message
toast({ toaster.create({
title: 'Email Sent', title: 'Email Sent',
status: 'success', type: 'success',
description: message.data, description: message.data,
isClosable: true, closable: true,
duration: 10000, duration: 10000,
position: 'top'
}) })
} }
@@ -90,78 +87,70 @@ export default function EditUser() {
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}> <Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
<Heading as="h1" marginBottom={'3rem'}>Edit {user.displayName}</Heading> <Heading as="h1" marginBottom={'3rem'}>Edit {user.displayName}</Heading>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<FormControl> <Field label="Full Name">
<FormLabel htmlFor="name">Full Name</FormLabel>
<Input <Input
id="name" id="name"
bg={bg} bg={bg}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
{...register('name', { required: 'Name is required' })} {...register('name', { required: 'Name is required' })}
/> />
</FormControl> </Field>
<FormControl mt={'3'}> <Field label="Display Name" mt={'3'}>
<FormLabel htmlFor="displayName">Display Name</FormLabel>
<Input <Input
id="displayName" id="displayName"
bg={bg} bg={bg}
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
{...register('displayName', { required: 'Display Name is required' })} {...register('displayName', { required: 'Display Name is required' })}
/> />
</FormControl> </Field>
<FormControl mt={'3'}> <Field label="Email" mt={'3'}>
<FormLabel htmlFor="email">Email</FormLabel>
<Input <Input
id="email" id="email"
bg={bg} bg={bg}
_placeholder={{ color: 'gray.500' }} _placeholder={{ color: 'gray.500' }}
isDisabled disabled
border="1px" borderWidth="1px" borderStyle="solid"
borderColor="gray.500" borderColor={{ base: "gray.500", _invalid: "red.500" }}
rounded="md" rounded="md"
placeholder="Email" placeholder="Email"
{...register('email', { required: 'Email is required' })} {...register('email', { required: 'Email is required' })}
/> />
</FormControl> </Field>
<FormControl> <Field label="Dark Mode:" orientation="horizontal" mt={'6'}>
<Flex mt={'6'}>
<FormLabel htmlFor={"theme"}>Dark Mode:</FormLabel>
<Controller <Controller
control={control} control={control}
name="theme" name="theme"
render={({ field: { onChange, value, ref } }) => ( render={({ field: { onChange, value, ref } }) => (
<Switch isChecked={value} onChange={onChange} ref={ref} /> <Switch colorPalette="blue" checked={value} onCheckedChange={({ checked }: any) => onChange(checked)} ref={ref} />
)} )}
/> />
</Flex> </Field>
</FormControl> <Field label="Sticky Navbar:" orientation="horizontal" mt={'6'}>
<FormControl>
<Flex mt={'6'}>
<FormLabel htmlFor={"stickyNav"}>Sticky Navbar:</FormLabel>
<Controller <Controller
control={control} control={control}
name="stickyNav" name="stickyNav"
render={({ field: { onChange, value, ref } }) => ( render={({ field: { onChange, value, ref } }) => (
<Switch isChecked={value} onChange={onChange} ref={ref} /> <Switch colorPalette="blue" checked={value} onCheckedChange={({ checked }: any) => onChange(checked)} ref={ref} />
)} )}
/> />
</Flex> </Field>
</FormControl>
<br /> <br />
<Stack spacing={[1, 5]} direction={'row'} align="self-end" pb="3"> <Stack gap={[1, 5]} direction={'row'} alignSelf="flex-end" pb="3">
<Button <Button
type="submit" type="submit"
colorPalette="blue"
disabled={!isDirty} disabled={!isDirty}
isLoading={isSubmitting} loading={isSubmitting}
> >
Submit Submit
</Button> </Button>
<Button <Button
isLoading={passwordLoading.get()} loading={passwordLoading.get()}
isDisabled={passwordDisabled.get()} disabled={passwordDisabled.get()}
onMouseDown={async () => { onMouseDown={async () => {
passwordLoading.set(true) passwordLoading.set(true)
passwordDisabled.set(true) passwordDisabled.set(true)
+30 -32
View File
@@ -7,14 +7,10 @@ import {
GridItem, GridItem,
Heading, Heading,
Image, Image,
Table,
TableContainer,
Tbody,
Td,
Text, Text,
Tr, Table,
useColorModeValue
} from '@chakra-ui/react' } from '@chakra-ui/react'
import {useColorModeValue} from '../ui/color-mode'
import {useHookstate} from '@hookstate/core' import {useHookstate} from '@hookstate/core'
import {loggedInUser} from '../../stateManagement/userState' import {loggedInUser} from '../../stateManagement/userState'
import {Link} from 'react-router-dom' import {Link} from 'react-router-dom'
@@ -28,10 +24,12 @@ const UserProfile = () => {
<Heading mb={'1rem'} as="h1">User Profile</Heading> <Heading mb={'1rem'} as="h1">User Profile</Heading>
<Grid templateColumns={`repeat(7, 1fr)`} templateRows={'2'} gap={6}> <Grid templateColumns={`repeat(7, 1fr)`} templateRows={'2'} gap={6}>
<GridItem rowStart={1} rowEnd={1} colStart={7} alignSelf={'end'} justifySelf={'end'}> <GridItem rowStart={1} rowEnd={1} colStart={7} alignSelf={'end'} justifySelf={'end'}>
<Button as={Link} to={`/edit-user`} colorScheme="blue" style={{textAlign: 'right'}}> <Button asChild colorPalette="blue" style={{textAlign: 'right'}}>
<Text mt={'1'}> <Link to={`/edit-user`}>
Edit <Text mt={'1'}>
</Text> Edit
</Text>
</Link>
</Button> </Button>
</GridItem> </GridItem>
<GridItem rowStart={2} rowEnd={2} colSpan={2} colStart={2}> <GridItem rowStart={2} rowEnd={2} colSpan={2} colStart={2}>
@@ -42,28 +40,28 @@ const UserProfile = () => {
</GridItem> </GridItem>
<GridItem rowStart={2} rowEnd={2} colStart={4} colEnd={8}> <GridItem rowStart={2} rowEnd={2} colStart={4} colEnd={8}>
<Box bg={bg} p="5" rounded="md"> <Box bg={bg} p="5" rounded="md">
<TableContainer> <Table.ScrollArea>
<Table variant="simple"> <Table.Root variant="outline">
<Tbody> <Table.Body>
<Tr> <Table.Row>
<Td><Text fontSize={'1.5rem'}>Display Name: </Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>Display Name: </Text></Table.Cell>
<Td><Text fontSize={'1.5rem'}>{user.displayName}</Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>{user.displayName}</Text></Table.Cell>
</Tr> </Table.Row>
<Tr> <Table.Row>
<Td><Text fontSize={'1.5rem'}>Full Name: </Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>Full Name: </Text></Table.Cell>
<Td><Text fontSize={'1.5rem'}>{user.name}</Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>{user.name}</Text></Table.Cell>
</Tr> </Table.Row>
<Tr> <Table.Row>
<Td><Text fontSize={'1.5rem'}>Email: </Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>Email: </Text></Table.Cell>
<Td><Text fontSize={'1.5rem'}>{user.email}</Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>{user.email}</Text></Table.Cell>
</Tr> </Table.Row>
<Tr> <Table.Row>
<Td><Text fontSize={'1.5rem'}>Role: </Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>Role: </Text></Table.Cell>
<Td><Text fontSize={'1.5rem'}>{user.role}</Text></Td> <Table.Cell><Text fontSize={'1.5rem'}>{user.role}</Text></Table.Cell>
</Tr> </Table.Row>
</Tbody> </Table.Body>
</Table> </Table.Root>
</TableContainer> </Table.ScrollArea>
</Box> </Box>
</GridItem> </GridItem>
</Grid> </Grid>
+2 -2
View File
@@ -29,8 +29,8 @@ export default function NotFound() {
It looks like you hit a{' '} It looks like you hit a{' '}
{<span style={styles.retroFontBody}>DEAD</span>} end! {<span style={styles.retroFontBody}>DEAD</span>} end!
</Heading> </Heading>
<Button colorScheme='blue' as={Link} to={'/games'} style={{fontSize: '1.2rem', marginLeft: '5rem'}}> <Button colorPalette='blue' asChild style={{fontSize: '1.2rem', marginLeft: '5rem'}}>
Back to Games List <Link to={'/games'}>Back to Games List</Link>
</Button> </Button>
<Suspense fallback={<LoadingModal />}> <Suspense fallback={<LoadingModal />}>
<Video/> <Video/>
+3 -1
View File
@@ -29,10 +29,12 @@ export default function SomethingWentWrong() {
{<span style={styles.retroFontBody}>WE APOLOGIZE!</span>}<br/> {<span style={styles.retroFontBody}>WE APOLOGIZE!</span>}<br/>
Please contact the website administrator! Please contact the website administrator!
</Heading> </Heading>
<Button colorScheme='blue' as={Link} to={'/games'} style={{fontSize: '1.2rem', marginLeft: '5rem'}}> <Button colorPalette='blue' asChild style={{fontSize: '1.2rem', marginLeft: '5rem'}}>
<Link to={'/games'}>
<div style={{marginTop: '.25rem'}}> <div style={{marginTop: '.25rem'}}>
Back to Games List Back to Games List
</div> </div>
</Link>
</Button> </Button>
<Suspense fallback={<LoadingModal/>}> <Suspense fallback={<LoadingModal/>}>
<Video/> <Video/>
+6 -6
View File
@@ -3,14 +3,14 @@ import React from "react";
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
import App from "./App"; import App from "./App";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import "react-toastify/dist/ReactToastify.min.css"; import "react-toastify/dist/ReactToastify.css";
import "./styles/typography.css"; import "./styles/typography.css";
import "./styles/videoBackground.css"; import "./styles/videoBackground.css";
import "./styles/overrides.css"; import "./styles/overrides.css";
import "react-responsive-carousel/lib/styles/carousel.min.css"; import "react-responsive-carousel/lib/styles/carousel.min.css";
import {createBrowserRouter, RouterProvider} from "react-router-dom"; import {createBrowserRouter, RouterProvider} from "react-router-dom";
import {ChakraProvider, ColorModeScript} from "@chakra-ui/react"; import {Provider} from "./components/ui/provider";
import theme from "./styles/theme"; import {Toaster} from "./components/ui/toaster";
import NotFound from "./errors/NotFound"; import NotFound from "./errors/NotFound";
const container = document.getElementById("root"); const container = document.getElementById("root");
@@ -26,10 +26,10 @@ const router = createBrowserRouter([
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<ChakraProvider theme={theme}> <Provider>
<ColorModeScript initialColorMode={theme.config.initialColorMode}/>
<RouterProvider router={router} /> <RouterProvider router={router} />
</ChakraProvider> <Toaster />
</Provider>
</React.StrictMode> </React.StrictMode>
); );
+8 -8
View File
@@ -1,13 +1,13 @@
import { ReportHandler } from 'web-vitals'; import type { MetricType } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: (metric: MetricType) => void) => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
getCLS(onPerfEntry); onCLS(onPerfEntry);
getFID(onPerfEntry); onINP(onPerfEntry);
getFCP(onPerfEntry); onFCP(onPerfEntry);
getLCP(onPerfEntry); onLCP(onPerfEntry);
getTTFB(onPerfEntry); onTTFB(onPerfEntry);
}); });
} }
}; };
+1 -1
View File
@@ -74,7 +74,7 @@ export const getUser = async (token: string) => {
if(userPreferencesExist === -1) { if(userPreferencesExist === -1) {
userPreferences.merge([{ userPreferences.merge([{
id: user.data._id, id: user.data._id,
theme: 'light', theme: 'dark',
stickyNav: true, stickyNav: true,
}]) }])
} }
+348 -11
View File
@@ -1,18 +1,355 @@
import { extendTheme, Theme } from '@chakra-ui/react' import {
import { mode } from '@chakra-ui/theme-tools' createSystem,
import {loadedPreferences} from "../stateManagement/userState"; defaultConfig,
defineConfig,
defineRecipe,
defineSlotRecipe,
} from '@chakra-ui/react'
import {
buttonRecipe,
headingRecipe,
inputRecipe,
linkRecipe,
menuSlotRecipe,
nativeSelectSlotRecipe,
numberInputSlotRecipe,
switchSlotRecipe,
tagSlotRecipe,
} from '@chakra-ui/react/theme'
// Chakra v2 color scales (blue-tinted). Chakra v3 replaced these with neutral
// scales and a near-black page background, which changed the app's whole look.
// Restoring the v2 scales keeps every component on the old colorscheme.
const v2Colors = {
gray: {
50: '#F7FAFC',
100: '#EDF2F7',
200: '#E2E8F0',
300: '#CBD5E0',
400: '#A0AEC0',
500: '#718096',
600: '#4A5568',
700: '#2D3748',
800: '#1A202C',
900: '#171923',
},
blue: {
50: '#EBF8FF',
100: '#BEE3F8',
200: '#90CDF4',
300: '#63B3ED',
400: '#4299E1',
500: '#3182CE',
600: '#2B6CB0',
700: '#2C5282',
800: '#2A4365',
900: '#1A365D',
},
green: {
50: '#F0FFF4',
100: '#C6F6D5',
200: '#9AE6B4',
300: '#68D391',
400: '#48BB78',
500: '#38A169',
600: '#2F855A',
700: '#276749',
800: '#22543D',
900: '#1C4532',
},
red: {
50: '#FFF5F5',
100: '#FED7D7',
200: '#FEB2B2',
300: '#FC8181',
400: '#F56565',
500: '#E53E3E',
600: '#C53030',
700: '#9B2C2C',
800: '#742A2A',
900: '#63171B',
},
} as const
const toTokens = (scale: Record<string, string>) =>
Object.fromEntries(Object.entries(scale).map(([shade, hex]) => [shade, { value: hex }]))
export default extendTheme({ // Chakra v2 solid fills were bright in dark mode (200-shade bg + dark text),
initialColorMode: loadedPreferences.theme, // while v3 solid fills stay dark with white text. Restore the v2 behavior so
useSystemColorMode: false, // buttons/tags read the same as before. This also fixes Switch/Checkbox
styles: { // checked fills, which consume the same tokens.
global: (props: Theme) => ({ const v2Solid = (palette: 'gray' | 'blue' | 'green' | 'red') => {
body: { if (palette === 'gray') {
bg: mode('gray.200', 'gray.800')(props), return {
solid: { value: { _light: '{colors.gray.100}', _dark: '{colors.whiteAlpha.200}' } },
contrast: { value: { _light: '{colors.gray.800}', _dark: 'white' } },
}
}
return {
solid: { value: { _light: `{colors.${palette}.500}`, _dark: `{colors.${palette}.200}` } },
contrast: { value: { _light: 'white', _dark: '{colors.gray.800}' } },
}
}
// Chakra v3 shrunk component text a step (e.g. md buttons/inputs render sm
// text). v2 used same-name text per size — restore that.
// NOTE: recipe bases are spread as `any` — retyping Chakra's inferred
// variant unions through defineRecipe's generics is not worth it here;
// the objects below are complete recipes by construction (spread of the
// original + targeted size overrides).
const baseButton: any = buttonRecipe
const baseHeading: any = headingRecipe
const baseInput: any = inputRecipe
const baseNumberInput: any = numberInputSlotRecipe
const baseNativeSelect: any = nativeSelectSlotRecipe
const baseSwitch: any = switchSlotRecipe
const baseTag: any = tagSlotRecipe
const baseLink: any = linkRecipe
const baseMenu: any = menuSlotRecipe
// Menu item hover uses bg.emphasized/60 — near-invisible on dark surfaces.
// Brighten to match the select-option hover (gray.200 / whiteAlpha.300).
const v2MenuRecipe = defineSlotRecipe({
...baseMenu,
variants: {
...baseMenu.variants,
variant: {
...baseMenu.variants?.variant,
subtle: {
...baseMenu.variants?.variant?.subtle,
item: {
...baseMenu.variants?.variant?.subtle?.item,
_highlighted: { bg: { base: 'gray.200', _dark: 'whiteAlpha.300' } },
},
}, },
}) },
}, },
}) })
// v3 link underlines render at 20% opacity (currentColor/20) — too dim
// against dark surfaces. Brighten universally.
const v2LinkRecipe = defineRecipe({
...baseLink,
variants: {
...baseLink.variants,
variant: {
...baseLink.variants?.variant,
underline: {
...baseLink.variants?.variant?.underline,
textDecorationColor: 'currentColor/60',
},
plain: {
...baseLink.variants?.variant?.plain,
_hover: {
...baseLink.variants?.variant?.plain?._hover,
textDecorationColor: 'currentColor/60',
},
},
},
},
})
// v2 tag text per size (v3 renders one step smaller)
const v2TagRecipe = defineSlotRecipe({
...baseTag,
base: {
...baseTag.base,
root: { ...baseTag.base?.root, pt: '1px' },
},
variants: {
...baseTag.variants,
size: {
...baseTag.variants?.size,
md: {
...baseTag.variants?.size?.md,
label: { ...baseTag.variants?.size?.md?.label, textStyle: 'sm' },
},
lg: {
...baseTag.variants?.size?.lg,
label: { ...baseTag.variants?.size?.lg?.label, textStyle: 'md' },
},
},
},
})
const v2ButtonRecipe = defineRecipe({
...baseButton,
base: {
...baseButton.base,
fontWeight: 'semibold',
},
variants: {
...baseButton.variants,
size: {
...baseButton.variants?.size,
sm: { ...baseButton.variants?.size?.sm, textStyle: 'sm', h: '8', minW: '8' },
md: { ...baseButton.variants?.size?.md, textStyle: 'md' },
lg: { ...baseButton.variants?.size?.lg, textStyle: 'lg', h: '12', minW: '12' },
},
},
})
const v2InputRecipe = defineRecipe({
...baseInput,
variants: {
...baseInput.variants,
size: {
...baseInput.variants?.size,
sm: { ...baseInput.variants?.size?.sm, textStyle: 'sm', '--input-height': 'sizes.8' },
md: { ...baseInput.variants?.size?.md, textStyle: 'md' },
lg: { ...baseInput.variants?.size?.lg, textStyle: 'lg', '--input-height': 'sizes.12' },
},
},
})
// v2 headings were a full step larger than v3 same-name sizes
// (v2 xl = 3xl/4xl ≈ 30/36px, v3 xl = 20px). Restore v2 mapping universally
// so page titles match the old screenshots.
const v2HeadingRecipe = defineRecipe({
...baseHeading,
variants: {
...baseHeading.variants,
size: {
...baseHeading.variants?.size,
xs: { textStyle: 'sm' },
sm: { textStyle: 'md' },
md: { textStyle: 'xl' },
lg: { textStyle: '3xl' },
xl: { textStyle: '4xl' },
'2xl': { textStyle: '5xl' },
'3xl': { textStyle: '6xl' },
'4xl': { textStyle: '7xl' },
},
},
})
const v2NumberInputRecipe = defineSlotRecipe({
...baseNumberInput,
variants: {
...baseNumberInput.variants,
size: {
...baseNumberInput.variants?.size,
xs: {
...baseNumberInput.variants?.size?.xs,
input: { ...v2InputRecipe.variants?.size?.xs },
},
sm: {
...baseNumberInput.variants?.size?.sm,
input: { ...v2InputRecipe.variants?.size?.sm },
},
md: {
...baseNumberInput.variants?.size?.md,
input: { ...v2InputRecipe.variants?.size?.md },
},
lg: {
...baseNumberInput.variants?.size?.lg,
input: { ...v2InputRecipe.variants?.size?.lg },
},
},
},
})
const v2NativeSelectRecipe = defineSlotRecipe({
...baseNativeSelect,
variants: {
...baseNativeSelect.variants,
size: {
...baseNativeSelect.variants?.size,
md: {
...baseNativeSelect.variants?.size?.md,
field: { ...baseNativeSelect.variants?.size?.md?.field, textStyle: 'md' },
},
lg: {
...baseNativeSelect.variants?.size?.lg,
field: { ...baseNativeSelect.variants?.size?.lg?.field, textStyle: 'lg' },
},
},
},
})
// v2 Switch: unchecked track lighter grey (gray.300 / whiteAlpha.400),
// checked track blue (500 / 200), thumb white always. v3 uses a near-black
// unchecked track in dark mode and a dark thumb when checked.
const v2SwitchRecipe = defineSlotRecipe({
...baseSwitch,
variants: {
...baseSwitch.variants,
variant: {
...baseSwitch.variants?.variant,
solid: {
...baseSwitch.variants?.variant?.solid,
control: {
...baseSwitch.variants?.variant?.solid?.control,
bg: { base: 'gray.300', _dark: 'whiteAlpha.400' },
},
thumb: {
...baseSwitch.variants?.variant?.solid?.thumb,
bg: 'white',
_checked: {
...baseSwitch.variants?.variant?.solid?.thumb?._checked,
bg: 'white',
},
},
},
},
},
})
const config = defineConfig({
theme: {
recipes: {
button: v2ButtonRecipe,
heading: v2HeadingRecipe,
input: v2InputRecipe,
link: v2LinkRecipe,
},
slotRecipes: {
numberInput: v2NumberInputRecipe,
nativeSelect: v2NativeSelectRecipe,
switch: v2SwitchRecipe,
tag: v2TagRecipe,
menu: v2MenuRecipe,
},
tokens: {
colors: {
gray: {
...toTokens(v2Colors.gray),
// v3 surfaces that default to near-black (menu/dropdown/modal
// panels) used v2's gray.700 in dark mode.
950: { value: '#2D3748' },
},
blue: toTokens(v2Colors.blue),
green: toTokens(v2Colors.green),
red: toTokens(v2Colors.red),
},
},
semanticTokens: {
colors: {
gray: v2Solid('gray'),
blue: v2Solid('blue'),
green: v2Solid('green'),
red: v2Solid('red'),
},
},
},
globalCss: {
// NOTE: top margins of page content collapse past `body`, so the strip
// above the content is painted with `html`'s background. It must match
// the body or the page gets a mismatched band.
// Explicit `.dark` class selectors are used because Chakra v3's `_dark`
// condition only matches *descendants* of `.dark`, never `html.dark`
// itself.
'html:not(.dark)': {
bg: 'gray.200',
},
'html.dark': {
bg: 'gray.800',
},
body: {
bg: { base: 'gray.200', _dark: 'gray.800' },
fontSize: '1.0625rem',
},
},
})
export default createSystem(defaultConfig, config)
+7 -4
View File
@@ -5,17 +5,20 @@
"types": ["vite/client"], "types": ["vite/client"],
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": false, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "esnext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "Bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"]
}
}, },
"include": [ "include": [
"src" "src"