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:
+31
-35
@@ -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
@@ -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
@@ -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]);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,21 +324,21 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilterBar
|
export default FilterBar
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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
@@ -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,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,21 +103,20 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MobileBar
|
export default MobileBar
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+18
-18
@@ -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,18 +73,16 @@ 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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +9,8 @@ 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,15 +13,16 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckboxInput
|
export default CheckboxInput
|
||||||
|
|||||||
@@ -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,8 +39,8 @@ const CreatableSelectInput = ({ label, textField, value, bg, handleInputChange,
|
|||||||
options={options}
|
options={options}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</FormControl>
|
</Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreatableSelectInput
|
export default CreatableSelectInput
|
||||||
|
|||||||
@@ -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,8 +33,8 @@ const SelectInput = ({label, textField, field, bg, handleAccessedBy, options, wi
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</FormControl>
|
</Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectInput
|
export default SelectInput
|
||||||
|
|||||||
@@ -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,8 +30,8 @@ const SingleFieldInput = ({ label, textField, field, bg, handleInputChange, requ
|
|||||||
handleInputChange(textField, e.target.value)
|
handleInputChange(textField, e.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</Field>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SingleFieldInput
|
export default SingleFieldInput
|
||||||
|
|||||||
@@ -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,18 +24,17 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemRequirementsTextareaInput
|
export default SystemRequirementsTextareaInput
|
||||||
|
|||||||
@@ -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,18 +25,17 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TextareaInput
|
export default TextareaInput
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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/>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|||||||
+349
-12
@@ -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
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user