diff --git a/bun.lockb b/bun.lockb index e615322..6c5f890 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index b225db5..9b01bf8 100644 --- a/package.json +++ b/package.json @@ -3,31 +3,27 @@ "version": "0.1.0", "private": true, "dependencies": { - "@chakra-ui/icons": "^2.1.1", - "@chakra-ui/react": "^2.8.2", - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", - "@hookstate/core": "^4.0.1", + "@chakra-ui/react": "^3.37.0", + "@emotion/react": "^11.14.0", + "@hookstate/core": "^4.0.2", "@hookstate/localstored": "^4.0.2", - "axios": "1.7.5", + "axios": "^1.20.0", "bson-objectid": "^2.0.4", - "chakra-react-select": "^4.9.1", - "chakra-ui-contextmenu": "^1.0.5", - "framer-motion": "^11.3.30", - "interweave": "^13.1.0", - "jodit-react": "^4.1.2", - "js-base64": "^3.7.7", - "react": "^18.3.1", - "react-device-detect": "^2.2.3", - "react-dom": "^18.3.1", - "react-hook-form": "^7.53.0", - "react-icons": "^5.3.0", + "chakra-react-select": "^6.1.3", + "interweave": "^13.1.1", + "jodit-react": "^5.5.3", + "js-base64": "^3.9.3", + "next-themes": "^0.4.6", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-hook-form": "^7.87.0", + "react-icons": "^5.7.0", "react-rating": "^2.0.5", "react-responsive-carousel": "^3.2.23", - "react-router-dom": "^6.26.1", - "react-select": "^5.8.0", - "react-toastify": "^10.0.5", - "web-vitals": "^4.2.3" + "react-router-dom": "^7.18.3", + "react-select": "^5.10.2", + "react-toastify": "^11.1.0", + "web-vitals": "^6.2.1" }, "scripts": { "start": "bunx --bun vite", @@ -42,8 +38,8 @@ ] }, "resolutions": { - "@types/react": "18.2.21", - "@types/react-dom": "18.2.7" + "@types/react": "19.2.18", + "@types/react-dom": "19.2.7" }, "browserslist": { "production": [ @@ -58,18 +54,18 @@ ] }, "devDependencies": { - "@chakra-ui/cli": "^2.4.1", - "@hookstate/devtools": "^4.0.1", - "@testing-library/jest-dom": "^6.5.0", - "@testing-library/react": "^16.0.0", - "@testing-library/user-event": "^14.5.2", - "@types/jest": "^29.5.12", - "@types/node": "^22.5.1", - "@types/react": "^18.2.21", - "@types/react-dom": "^18.2.7", + "@chakra-ui/cli": "^3.37.0", + "@hookstate/devtools": "^4.0.3", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", + "@types/jest": "^30.0.0", + "@types/node": "^26.4.1", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", "@types/react-table": "^7.7.20", - "@vitejs/plugin-react": "^4.3.1", - "typescript": "^5.5.4", - "vite": "^5.4.2" + "@vitejs/plugin-react": "^6.1.1", + "typescript": "^5.9.3", + "vite": "^8.2.2" } } diff --git a/src/App.test.tsx b/src/App.test.tsx index 2a68616..c05057a 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,9 +1,17 @@ import React from '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', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); +test('renders welcome message', () => { + render( + + + + + + ); + const heading = screen.getByText(/Welcome to your Games Database/i); + expect(heading).toBeInTheDocument(); }); diff --git a/src/App.tsx b/src/App.tsx index 45e72cb..d93755b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,8 @@ import ForgotPassword from './components/authComponents/ForgotPassword' import ResetPassword from './components/authComponents/ResetPassword' import UserProfile from './components/userComponents/UserProfile' 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"; function App() { @@ -44,8 +45,10 @@ function App() { if (!appLoad) useEffect(() => { - if(loadedPreferences.get().theme !== colorMode) { + const preferredTheme = loadedPreferences.get().theme + if(preferredTheme && preferredTheme !== colorMode) { localStorage.removeItem('chakra-ui-color-mode') + localStorage.removeItem('theme') toggleColorMode() } }, [loadedPreferences.get().theme]); diff --git a/src/components/ActiveFilters.tsx b/src/components/ActiveFilters.tsx index 0c2720f..032dc68 100644 --- a/src/components/ActiveFilters.tsx +++ b/src/components/ActiveFilters.tsx @@ -1,7 +1,8 @@ import React from 'react' import { useHookstate } from '@hookstate/core' 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 { loadingState, @@ -53,7 +54,7 @@ const ActiveFilters = () => { }} direction={{ base: 'column', md: 'row' - }} spacing={4}> + }} gap={4}> {searchParams.length >= 1 && ( <>

Search:

@@ -61,11 +62,8 @@ const ActiveFilters = () => { size={'md'} borderRadius="full" variant="solid" - colorScheme="blue" - > - setSearchModalOpen.set(true)}>{searchParams} - { + colorPalette="blue" + onClose={async () => { const newSearchParams = "" ChangeActiveFilterHeader(filters, newSearchParams, setShowFiltersModuleState) searchComplete.set(false) @@ -80,7 +78,9 @@ const ActiveFilters = () => { useRatingState, useSteamRatingState, ) - }} /> + }} + > setSearchModalOpen.set(true)}>{searchParams} )} @@ -99,22 +99,20 @@ const ActiveFilters = () => { <>

{filter}

- + {filters[filter as keyof typeof filters].split(',').map((item: string, index) => ( - { + colorPalette="blue" + onClose={() => removeFilters(filter, item)} + >{ item.includes('lte') ? item.replace('lte', '<= ') : item.includes('gte') ? item.replace('gte', '>= ') : item} - - removeFilters(filter, item)} /> ))} diff --git a/src/components/DeleteDialogue.tsx b/src/components/DeleteDialogue.tsx index 1cafb17..712e002 100644 --- a/src/components/DeleteDialogue.tsx +++ b/src/components/DeleteDialogue.tsx @@ -3,13 +3,9 @@ import agent from '../api/agent' import { useNavigate } from 'react-router-dom' import { Data } from '../models/game' import { - AlertDialog, - AlertDialogBody, - AlertDialogContent, AlertDialogFooter, - AlertDialogHeader, - AlertDialogOverlay, - Button, Icon, Text + Button, Text } from '@chakra-ui/react' +import {DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot} from './ui/dialog' type Props = { id: string @@ -44,34 +40,31 @@ export default function DeleteDialogue({ } } - const cancelRef = useRef(null) + const cancelRef = useRef(null) return ( - - - - - Delete? - - Are you sure you would like to delete {game.title}? - - - - - - - - + { if (!e.open) handleClose() }} role="alertdialog" initialFocusEl={() => cancelRef.current}> + + Delete? + + Are you sure you would like to delete {game.title}? + + + + + + + ) } diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx index ea36a8f..ccc794d 100644 --- a/src/components/FilterBar.tsx +++ b/src/components/FilterBar.tsx @@ -1,17 +1,12 @@ import React, {MutableRefObject, useEffect} from 'react' import { Center, - Divider, - Drawer, - DrawerBody, - DrawerCloseButton, - DrawerContent, - DrawerFooter, - DrawerHeader, - DrawerOverlay, Flex, + Separator, + Flex, Heading, Spacer, Text, } from '@chakra-ui/react' +import {DrawerBody, DrawerCloseTrigger, DrawerContent, DrawerFooter, DrawerHeader, DrawerRoot} from './ui/drawer' import AdminMode from './authComponents/AdminMode' import {StateMethods, useHookstate} from '@hookstate/core' import {tagState} from '../stateManagement/tagState' @@ -25,7 +20,7 @@ import { } from '../componentUtils/SelectValues' import {retrieveTags} from '../componentUtils/retrieveTags' import {gameDashboardState} from '../stateManagement/gameState' -import {Select} from 'chakra-react-select' +import {Select} from './FixedSelect' import getWithFilters from '../componentUtils/getWithFilters' import {loadingState, showFiltersModuleState} from '../stateManagement/loadingState' import {adminMode} from '../stateManagement/userState' @@ -99,26 +94,25 @@ function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) { } return ( - { if (!e.open) filterOnClose() }} + finalFocusEl={() => btnRef.current as unknown as HTMLElement} > - - + Filters - +
- + Series { - const value = event.map(a => a.value).join(',') + onChange={(event: any) => { + const value = event.map((a: any) => a.value).join(',') changeFilters("playStatus", value) }} options={playStatusValues} /> - + Genres { - const value = event.map(a => a.value).join(',') + onChange={(event: any) => { + const value = event.map((a: any) => a.value).join(',') changeFilters("os", value) }} options={osValues} /> - + Store { - const value = event.map(a => a.value).join(',') + onChange={(event: any) => { + const value = event.map((a: any) => a.value).join(',') changeFilters("controller", value) }} options={controllerValues} /> - + Developer { - const value = event.map(a => a.value).join(',') + onChange={(event: any) => { + const value = event.map((a: any) => a.value).join(',') changeFilters("publisher", value) }} options={tags.publisher} /> - + Do you own the Soundtrack? { - const value = event.map(a => a.value).join(',') + onChange={(event: any) => { + const value = event.map((a: any) => a.value).join(',') changeFilters("intel", value) }} options={wineIntelValues} /> - + Does is work with Wine? { - setPageSize(Number(e.target.value)) - }} > - {[10, 20, 30, 40, 50].map((pageSize) => ( - - ))} - + { + setPageSize(Number(e.target.value)) + }} + > + {[10, 20, 30, 40, 50].map((pageSize) => ( + + ))} + + + Games Found: {games.count.totalGames} - + = pageCount} - icon={} - /> + disabled={pageIndex >= pageCount} + > + + - + gotoPage(pageCount)} - isDisabled={pageIndex >= pageCount} - icon={} + disabled={pageIndex >= pageCount} ml={4} - /> + > + + diff --git a/src/components/Search.tsx b/src/components/Search.tsx index e6ce6dd..d051828 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,6 +1,7 @@ import React, { FormEvent } from 'react' -import { Button, FormControl, Input, InputGroup, InputRightElement, useColorModeValue } from '@chakra-ui/react' -import { SearchIcon } from '@chakra-ui/icons' +import { Button, Input, Group } from '@chakra-ui/react' +import { Field } from './ui/field' +import { LuSearch } from 'react-icons/lu' import { GameList } from '../models/game' import { useHookstate } from '@hookstate/core' import { gameDashboardState } from '../stateManagement/gameState' @@ -14,6 +15,7 @@ import { adminMode } from '../stateManagement/userState' import getWithFilters from '../componentUtils/getWithFilters' import {ChangeActiveFilterHeader} from "../componentUtils/changeActiveFilterHeader"; import {ratingState, steamRatingState} from "../stateManagement/ratingState"; +import {useColorModeValue} from "./ui/color-mode"; export default function Search() { const gamesState = useHookstate(gameDashboardState) @@ -54,16 +56,16 @@ export default function Search() { return ( <>
- - + + - - - - - + + +
) -} \ No newline at end of file +} diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx index c124d13..4d9eb11 100644 --- a/src/components/SearchModal.tsx +++ b/src/components/SearchModal.tsx @@ -1,8 +1,6 @@ import React from 'react' -import { - Modal, ModalBody, ModalCloseButton, ModalContent, - ModalHeader, -} from '@chakra-ui/react' +import { Dialog } from '@chakra-ui/react' +import {DialogBody, DialogCloseTrigger, DialogContent, DialogHeader, DialogRoot} from './ui/dialog' import { useHookstate } from '@hookstate/core' import { openAndCloseSearchDialogue } from '../stateManagement/loadingState' import Search from './Search' @@ -16,14 +14,14 @@ export default function SearchModal() { } return ( - - - Search - handleClose()}/> - + { if (!e.open) handleClose() }}> + + Search + handleClose()}/> + - - - + + + ) } diff --git a/src/components/authComponents/AdminMode.tsx b/src/components/authComponents/AdminMode.tsx index 51122a2..26bc7dc 100644 --- a/src/components/authComponents/AdminMode.tsx +++ b/src/components/authComponents/AdminMode.tsx @@ -1,5 +1,6 @@ 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 { useHookstate } from '@hookstate/core' @@ -8,8 +9,8 @@ export default function AdminMode() { const admin = useHookstate(adminMode).get() return ( - adminModeToggle()} isChecked={admin} isDisabled={!(user.role === 'admin')}> + adminModeToggle()} checked={admin} disabled={!(user.role === 'admin')}> Administrator Mode ) -} \ No newline at end of file +} diff --git a/src/components/authComponents/ForgotPassword.tsx b/src/components/authComponents/ForgotPassword.tsx index 4348f7f..adb9ad6 100644 --- a/src/components/authComponents/ForgotPassword.tsx +++ b/src/components/authComponents/ForgotPassword.tsx @@ -3,11 +3,11 @@ import { useForm } from 'react-hook-form' import { Container, Button, - FormControl, - FormLabel, Input, - useColorModeValue, FormErrorMessage, useToast, } 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 { IForgotPassword } from '../../models/user' @@ -18,7 +18,6 @@ export default function ForgotPassword() { handleSubmit, formState: { errors, isSubmitting, isDirty }, } = useForm() - const toast = useToast() interface Message { success: boolean @@ -27,27 +26,25 @@ export default function ForgotPassword() { const onSubmit = handleSubmit(async (values) => { const message = await agent.Account.forgot(values as IForgotPassword) as Message - toast({ + toaster.create({ title: 'Email Sent', - status: 'success', + type: 'success', description: message.data, - isClosable: true, + closable: true, duration: 10000, - position: 'top' }) }) return (
- - Email + - - {errors.email?.type === 'required' && errors.email.message?.toString()} - {errors.email?.type === 'pattern' && 'Invalid Email Address'} - - +
diff --git a/src/components/authComponents/LoginForm.tsx b/src/components/authComponents/LoginForm.tsx index 5e5ba8f..47ab793 100644 --- a/src/components/authComponents/LoginForm.tsx +++ b/src/components/authComponents/LoginForm.tsx @@ -5,11 +5,10 @@ import { login } from '../../stateManagement/userState' import { Container, Button, - FormControl, - FormLabel, - Input, - useColorModeValue, HStack, Center, FormErrorMessage, + Input, HStack, Center, } from '@chakra-ui/react' +import {Field} from '../ui/field' +import {useColorModeValue} from '../ui/color-mode' import { Link } from 'react-router-dom' export default function LoginForm() { @@ -38,14 +37,13 @@ export default function LoginForm() { return ( - - Email + - - {errors.email?.type === 'required' && errors.email.message?.toString()} - {errors.email?.type === 'pattern' && 'Invalid Email Address'} - - - - Password + + - - {errors.password && errors.password.message?.toString()} - - +
- +
diff --git a/src/components/authComponents/ResetPassword.tsx b/src/components/authComponents/ResetPassword.tsx index aec3cc9..3cdc3d3 100644 --- a/src/components/authComponents/ResetPassword.tsx +++ b/src/components/authComponents/ResetPassword.tsx @@ -4,10 +4,11 @@ import { IResetPassword } from '../../models/user' import { Button, Container, - FormControl, - FormLabel, - Input, useToast, useColorModeValue, FormErrorMessage, + Input, } 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 { useNavigate, useParams } from 'react-router-dom' @@ -17,7 +18,7 @@ interface Message { } interface URL { - token: string + token?: string } export default function ResetPassword() { @@ -27,32 +28,29 @@ export default function ResetPassword() { watch, formState: { errors, isSubmitting, isDirty }, } = useForm() - const { token } = useParams() + const { token } = useParams() const history = useNavigate() - const toast = useToast() const password = useRef({}) password.current = watch('password', '') 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) { setTimeout(() => history('/login'), 3000) - toast({ + toaster.create({ title: 'Password Reset', - status: 'success', + type: 'success', description: 'Your password has been reset. You will be redirected to login automatically', - isClosable: true, + closable: true, duration: 5000, - position: 'top', }) } else { - toast({ + toaster.create({ title: 'Password Not Reset', - status: 'error', + type: 'error', description: 'Something went wrong, please contact the website administrator', - isClosable: true, + closable: true, duration: 10000, - position: 'top', }) } }) @@ -62,15 +60,14 @@ export default function ResetPassword() { return (
- - Password + - - {errors.password && errors.password.message?.toString()} - - - - Repeat Password + + - - {errors.repeat_password && errors.repeat_password.message?.toString()} - - +
diff --git a/src/components/dashboardComponents/DashboardLoadingModal.tsx b/src/components/dashboardComponents/DashboardLoadingModal.tsx index e8e35a3..9f774c4 100644 --- a/src/components/dashboardComponents/DashboardLoadingModal.tsx +++ b/src/components/dashboardComponents/DashboardLoadingModal.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Center, CircularProgress } from '@chakra-ui/react' +import { Center, ProgressCircle } from '@chakra-ui/react' export default function DashboardLoadingModal() { @@ -9,7 +9,12 @@ export default function DashboardLoadingModal() { w="100%" mt={'1rem'} > - + + + + + + ) diff --git a/src/components/dashboardComponents/GameThumbnail.tsx b/src/components/dashboardComponents/GameThumbnail.tsx index 58fc6bd..fbe750c 100644 --- a/src/components/dashboardComponents/GameThumbnail.tsx +++ b/src/components/dashboardComponents/GameThumbnail.tsx @@ -1,35 +1,33 @@ import React from 'react' import { Link } from 'react-router-dom' 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 SteamAndDeckLogo from "../helperComponents/SteamAndDeckLogo"; interface Props { game: Data - onToggle: () => void 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 return ( <> - - + {game.title} - - - {game.steamId !== '' ? `${game.steamId}: ${game.title}` : `${game.title}`} - - Play Status: {playState} - + + {game.steamId !== '' ? `${game.steamId}: ${game.title}` : `${game.title}`} + + Play Status: {playState} + + diff --git a/src/components/dashboardComponents/GameThumbnailContextMenu.tsx b/src/components/dashboardComponents/GameThumbnailContextMenu.tsx index 97c5701..c61845d 100644 --- a/src/components/dashboardComponents/GameThumbnailContextMenu.tsx +++ b/src/components/dashboardComponents/GameThumbnailContextMenu.tsx @@ -1,9 +1,8 @@ import React, { useState } from 'react'; import { Data } from "../../models/game"; -import { ContextMenu } from 'chakra-ui-contextmenu'; -import { MenuItem, MenuList } from '@chakra-ui/menu'; +import { Menu, Portal } from '@chakra-ui/react'; import GameThumbnail from "./GameThumbnail"; -import { Box, MenuGroup, useDisclosure } from "@chakra-ui/react"; +import { Box, useDisclosure } from "@chakra-ui/react"; import GameThumbnailSlide from "./GameThumbnailSlide"; import { playStatusValues } from "../../componentUtils/SelectValues"; import agent from "../../api/agent"; @@ -14,7 +13,7 @@ interface Props { } const GameThumbnailContextMenu = ({ game }: Props) => { - const { isOpen, onToggle } = useDisclosure() + const { open: isOpen, onOpen, onClose } = useDisclosure() const [playState, setPlayState] = useState(game.accessedBy[0].playStatus) 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)'); return ( - key={game._id} renderMenu={() => ( - - Open in New Tab - - {playStatusValues.map(value => rightClick(value.label, value.value)} - >{value.label})} - - - )}> - {ref => <> + + { transitionDuration={'1'} transitionTimingFunction={'ease-in-out'} _hover={{ boxShadow: '2xl', transform: 'translateY(-3px)' }} + onMouseEnter={onOpen} + onMouseLeave={onClose} > - + - {(!mql.matches) && } - - } - + + + + + + Open in New Tab + + + Change PlayStatus + {playStatusValues.map((value, idx) => ( + rightClick(value.label, value.value)} + >{value.label} + ))} + + + + + {(!mql.matches) && } + ); }; diff --git a/src/components/dashboardComponents/GameThumbnailSlide.tsx b/src/components/dashboardComponents/GameThumbnailSlide.tsx index f3160ba..1c97387 100644 --- a/src/components/dashboardComponents/GameThumbnailSlide.tsx +++ b/src/components/dashboardComponents/GameThumbnailSlide.tsx @@ -1,5 +1,5 @@ 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 ImageSlider from "../detailsComponents/ImageSlider"; @@ -10,17 +10,25 @@ interface Props { const GameThumbnailSlide = ({isOpen, game}: Props) => { return ( - + - - + + {game.title} @@ -29,7 +37,7 @@ const GameThumbnailSlide = ({isOpen, game}: Props) => { - + ); }; diff --git a/src/components/detailsComponents/Features.tsx b/src/components/detailsComponents/Features.tsx index 04d218a..33a4c82 100644 --- a/src/components/detailsComponents/Features.tsx +++ b/src/components/detailsComponents/Features.tsx @@ -1,5 +1,5 @@ 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 { gameDashboardState } from '../../stateManagement/gameState' import { ImmutableObject, useHookstate } from '@hookstate/core' @@ -36,48 +36,40 @@ export default function Features({ game }: Props) { return history('/games') } + const badge = (label: string) => ( + + + + {label} + + + + ) + return ( Features and Information - - - - Rating: - - - + {badge('Rating:')} - + {game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 && (<> - - - - Steam ID: - - - + {badge('Steam ID:')} {game.steamId} - + )} {game.series.length > 1 && (<> - - - - Series: - - - + {badge('Series:')} { gamesState.filters.series.set(game.series) @@ -85,16 +77,10 @@ export default function Features({ game }: Props) { }} pt="1">{game.series} - + )} - - - - Play Status: - - - + {badge('Play Status:')} {editMode ? @@ -120,15 +106,9 @@ export default function Features({ game }: Props) { - + - - - - Genre: - - - + {badge('Genre:')} {game.genre.map((genre, index) => [ index > 0 && ", ", @@ -141,15 +121,9 @@ export default function Features({ game }: Props) { ])} - + - - - - Operating Systems: - - - + {badge('Operating Systems:')} {Object.keys(game.os).filter(os => { @@ -175,15 +149,9 @@ export default function Features({ game }: Props) { - + - - - - Store: - - - + {badge('Store:')} {game.accessedBy[0].store.map((store, index) => [ index > 0 && ", ", @@ -196,15 +164,9 @@ export default function Features({ game }: Props) { ])} - + - - - - Developer: - - - + {badge('Developer:')} {game.developer.map((developer, index) => [ index > 0 && ", ", @@ -217,15 +179,9 @@ export default function Features({ game }: Props) { ])} - + - - - - Publisher: - - - + {badge('Publisher:')} {game.publisher.map((publisher, index) => [ index > 0 && ", ", @@ -238,15 +194,9 @@ export default function Features({ game }: Props) { ])} - + - - - - Controller Support: - - - + {badge('Controller Support:')} { gamesState.filters.controller.set(game.controller) @@ -254,28 +204,16 @@ export default function Features({ game }: Props) { }} pt="1">{game.controller} - + - - - - Release Date: - - - + {badge('Release Date:')} {game.releaseDate} - + - - - - Soundtrack: - - - + {badge('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} - + - - - - Does it work with Intel?: - - - + {badge('Does it work with Intel?:')} { gamesState.filters.intel.set(game.intel) @@ -299,15 +231,9 @@ export default function Features({ game }: Props) { }} pt="1">{game.intel} - + - - - - Does it work with WINE?: - - - + {badge('Does it work with WINE?:')} { gamesState.filters.wine.set(game.wine) @@ -315,7 +241,7 @@ export default function Features({ game }: Props) { }} pt="1">{game.wine} - + ) } diff --git a/src/components/detailsComponents/GamePlayStatus.tsx b/src/components/detailsComponents/GamePlayStatus.tsx index 5b51395..6eabd95 100644 --- a/src/components/detailsComponents/GamePlayStatus.tsx +++ b/src/components/detailsComponents/GamePlayStatus.tsx @@ -2,11 +2,12 @@ import React from 'react'; import { Data } from '../../models/game' import agent from "../../api/agent"; import {playStatusValues} from "../../componentUtils/SelectValues"; -import {Select} from "chakra-react-select"; +import {Select} from "../FixedSelect"; +import {ImmutableObject} from "@hookstate/core"; interface Props { - game: Data + game: Data | ImmutableObject playState: string setPlayState: any } @@ -33,14 +34,14 @@ const GamePlayStatus = ({game, playState, setPlayState}: Props) => { value: playState, label: playState, }} - onChange={(e) => { + onChange={(e: any) => { if(e === null) return handleClick(e.value, e.label) }} size={'sm'} options={playStatusValues} chakraStyles={{ - container: (provided) => ({ + container: (provided: any) => ({ ...provided, width: "150px" }) diff --git a/src/components/detailsComponents/GameRating.tsx b/src/components/detailsComponents/GameRating.tsx index 6b94010..ad959a6 100644 --- a/src/components/detailsComponents/GameRating.tsx +++ b/src/components/detailsComponents/GameRating.tsx @@ -5,10 +5,11 @@ import agent from "../../api/agent"; import {Flex, Link} from "@chakra-ui/react"; import {IoGameController, IoGameControllerOutline} from "react-icons/io5"; import {BiReset} from "react-icons/bi"; +import {ImmutableObject} from "@hookstate/core"; interface Props { - game: Data + game: Data | ImmutableObject size: string } diff --git a/src/components/detailsComponents/Summary.tsx b/src/components/detailsComponents/Summary.tsx index 420c529..985d2be 100644 --- a/src/components/detailsComponents/Summary.tsx +++ b/src/components/detailsComponents/Summary.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Data } from '../../models/game' 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' interface Props { @@ -15,7 +15,7 @@ export default function Summary({ game }: Props) { {reviews.length >= 1 && ( <> Reviews - + )} = 1 ? '10' : ''} as="h1">About This Game - + { return ( - - - - - + { if (!e.open) onClose() }}> + + + - - - - - - + + + ) } diff --git a/src/components/detailsComponents/SystemRequirementsMenu.tsx b/src/components/detailsComponents/SystemRequirementsMenu.tsx index e750089..d05f30a 100644 --- a/src/components/detailsComponents/SystemRequirementsMenu.tsx +++ b/src/components/detailsComponents/SystemRequirementsMenu.tsx @@ -1,5 +1,5 @@ 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 { Interweave } from 'interweave' import { ImmutableObject } from '@hookstate/core' @@ -20,51 +20,51 @@ export default function SystemRequirements({ game }: Props) { return ( System Requirements - - - Windows - Mac - Linux - - - - - - Minimum - Recommended - - - {} - {} - - - - - - - Minimum - Recommended - - - {} - {} - - - - - - - Minimum - Recommended - - - {} - {} - - - - - + + + Windows + Mac + Linux + + + + + + Minimum + Recommended + + + {} + {} + + + + + + + Minimum + Recommended + + + {} + {} + + + + + + + Minimum + Recommended + + + {} + {} + + + + + ) } diff --git a/src/components/detailsComponents/SystemRequirementsModal.tsx b/src/components/detailsComponents/SystemRequirementsModal.tsx index 38de6f5..45944fd 100644 --- a/src/components/detailsComponents/SystemRequirementsModal.tsx +++ b/src/components/detailsComponents/SystemRequirementsModal.tsx @@ -1,5 +1,6 @@ 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 { Data } from '../../models/game' import SystemRequirements from './SystemRequirementsMenu' @@ -14,20 +15,19 @@ interface Props { const SummaryModal = ({ isOpen, onClose, game }: Props) => { return ( - - - - - + { if (!e.open) onClose() }}> + + + - - - - - - + + + ) } diff --git a/src/components/formFields/CheckboxInput.tsx b/src/components/formFields/CheckboxInput.tsx index 4449eb4..ca81329 100644 --- a/src/components/formFields/CheckboxInput.tsx +++ b/src/components/formFields/CheckboxInput.tsx @@ -1,5 +1,6 @@ import React from 'react' -import { Checkbox, Text } from '@chakra-ui/react' +import { Text } from '@chakra-ui/react' +import {Checkbox} from '../ui/checkbox' interface Props { label: string @@ -12,15 +13,16 @@ interface Props { const CheckboxInput = ({ label, textField, checked, defaultIsChecked, handleOSChange }: Props) => { return ( { + checked={checked} + onCheckedChange={() => { handleOSChange(textField) }} name={textField} - spacing="1rem" + gap="1rem" >{label} ) } -export default CheckboxInput \ No newline at end of file +export default CheckboxInput diff --git a/src/components/formFields/CreatableSelectInput.tsx b/src/components/formFields/CreatableSelectInput.tsx index 78f457e..d3219c6 100644 --- a/src/components/formFields/CreatableSelectInput.tsx +++ b/src/components/formFields/CreatableSelectInput.tsx @@ -1,6 +1,7 @@ import React from 'react' -import { Box, FormControl, FormLabel } from '@chakra-ui/react' -import { CreatableSelect } from 'chakra-react-select' +import { Box } from '@chakra-ui/react' +import { Field } from '../ui/field' +import { CreatableSelect } from '../FixedSelect' interface Props { label: string @@ -13,13 +14,12 @@ interface Props { const CreatableSelectInput = ({ label, textField, value, bg, handleInputChange, options}: Props) => { return ( - - {label} + + rounded="md" + w="full" + > - + ) } -export default CreatableSelectInput \ No newline at end of file +export default CreatableSelectInput diff --git a/src/components/formFields/SelectInput.tsx b/src/components/formFields/SelectInput.tsx index 1cd5507..db10fd4 100644 --- a/src/components/formFields/SelectInput.tsx +++ b/src/components/formFields/SelectInput.tsx @@ -1,6 +1,7 @@ import React from 'react' -import {Box, FormControl, FormLabel} from '@chakra-ui/react' -import {Select} from 'chakra-react-select' +import {Box} from '@chakra-ui/react' +import { Field } from '../ui/field' +import {Select} from '../FixedSelect' interface Props { label: string @@ -14,18 +15,15 @@ interface Props { const SelectInput = ({label, textField, field, bg, handleAccessedBy, options, width = ''}: Props) => { return ( - - {label} + - + ) } -export default SingleFieldInput \ No newline at end of file +export default SingleFieldInput diff --git a/src/components/formFields/SingleFieldNumericInput.tsx b/src/components/formFields/SingleFieldNumericInput.tsx index 4348e1c..03a5d8f 100644 --- a/src/components/formFields/SingleFieldNumericInput.tsx +++ b/src/components/formFields/SingleFieldNumericInput.tsx @@ -1,39 +1,63 @@ -import React from 'react' -import { FormControl, FormLabel, Input } from '@chakra-ui/react' -import { Data } from '../../models/game' +import React from "react"; +import { Input } from "@chakra-ui/react"; +import { Field } from "../ui/field"; +import { Data } from "../../models/game"; interface Props { - label: string - textField: string - field: string - bg: string - handleInputChange: (name: string, value: Data[G]) => void - required: boolean + label: string; + textField: string; + field: string; + bg: string; + handleInputChange: ( + 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 ( - - {label} + { - handleInputChange(textField, e.target.value) + handleInputChange(textField, e.target.value); }} /> - - ) -} + + ); +}; -export default SingleFieldNumericInput \ No newline at end of file +export default SingleFieldNumericInput; diff --git a/src/components/formFields/SystemRequirementsTextareaInput.tsx b/src/components/formFields/SystemRequirementsTextareaInput.tsx index 27130b8..987f339 100644 --- a/src/components/formFields/SystemRequirementsTextareaInput.tsx +++ b/src/components/formFields/SystemRequirementsTextareaInput.tsx @@ -1,5 +1,5 @@ import React, {useMemo, useRef} from 'react' -import {FormControl, FormLabel} from '@chakra-ui/react' +import {Field} from '../ui/field' import JoditEditor from "jodit-react"; interface Props { @@ -24,18 +24,17 @@ const SystemRequirementsTextareaInput = ({label, textField, field, handleSystemR ); return ( - - {label} + { + onChange={(value: string) => { handleSystemRequirements(arrText[1], arrText[2], value) }} /> - + ) } -export default SystemRequirementsTextareaInput \ No newline at end of file +export default SystemRequirementsTextareaInput diff --git a/src/components/formFields/TextareaInput.tsx b/src/components/formFields/TextareaInput.tsx index d350f6f..1dbfc51 100644 --- a/src/components/formFields/TextareaInput.tsx +++ b/src/components/formFields/TextareaInput.tsx @@ -1,5 +1,5 @@ import React, {useMemo, useRef} from 'react' -import { FormControl, FormLabel } from '@chakra-ui/react' +import { Field } from '../ui/field' import { Data } from '../../models/game' import JoditEditor from "jodit-react"; @@ -25,18 +25,17 @@ const TextareaInput = ({ label, textField, field, handleInputChange }: Props) => return ( - - {label} + { + onChange={(value: string) => { handleInputChange(textField, value) }} /> - + ) } -export default TextareaInput \ No newline at end of file +export default TextareaInput diff --git a/src/components/helperComponents/ResetFilterButton.tsx b/src/components/helperComponents/ResetFilterButton.tsx index f1bcd2c..7fd1151 100644 --- a/src/components/helperComponents/ResetFilterButton.tsx +++ b/src/components/helperComponents/ResetFilterButton.tsx @@ -9,12 +9,12 @@ import { ratingState, steamRatingState } from "../../stateManagement/ratingState interface Props { text?: string - size?: string - variant?: string - colorScheme?: string + size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" + variant?: "solid" | "subtle" | "surface" | "outline" | "ghost" | "plain" + 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 games = gameState.get() const admin = useHookstate(adminMode).get() @@ -58,7 +58,7 @@ const ResetFilterButton = ({ text = "Reset Search and Filters", size = 'md', var } return ( - ) diff --git a/src/components/helperComponents/SteamAndDeckLogo.tsx b/src/components/helperComponents/SteamAndDeckLogo.tsx index 602216d..7dc11ad 100644 --- a/src/components/helperComponents/SteamAndDeckLogo.tsx +++ b/src/components/helperComponents/SteamAndDeckLogo.tsx @@ -9,17 +9,19 @@ const SteamAndDeckLogo = (props: ISteamAndDeckLogo) => { return (
+ SteamAndDeckLogo { +
); diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..0afc8ef --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,29 @@ +import { Alert as ChakraAlert } from "@chakra-ui/react" +import * as React from "react" + +export interface AlertProps extends Omit { + startElement?: React.ReactNode + endElement?: React.ReactNode + title?: React.ReactNode + icon?: React.ReactElement +} + +export const Alert = React.forwardRef( + function Alert(props, ref) { + const { title, children, icon, startElement, endElement, ...rest } = props + return ( + + {startElement || {icon}} + {children ? ( + + {title} + {children} + + ) : ( + {title} + )} + {endElement} + + ) + }, +) diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..0f1d3a3 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,34 @@ +import { + Avatar as ChakraAvatar, + AvatarGroup as ChakraAvatarGroup, +} from "@chakra-ui/react" +import * as React from "react" + +type ImageProps = React.ImgHTMLAttributes + +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( + function Avatar(props, ref) { + const { name, src, srcSet, loading, icon, fallback, children, ...rest } = + props + return ( + + + {icon || fallback} + + + {children} + + ) + }, +) + +export const AvatarGroup = ChakraAvatarGroup diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..246e2f0 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -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 + rootRef?: React.RefObject +} + +export const Checkbox = React.forwardRef( + function Checkbox(props, ref) { + const { icon, children, inputProps, rootRef, ...rest } = props + return ( + + + + {icon || } + + {children != null && ( + {children} + )} + + ) + }, +) diff --git a/src/components/ui/close-button.tsx b/src/components/ui/close-button.tsx new file mode 100644 index 0000000..94af488 --- /dev/null +++ b/src/components/ui/close-button.tsx @@ -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 ( + + {props.children ?? } + + ) +}) diff --git a/src/components/ui/color-mode.tsx b/src/components/ui/color-mode.tsx new file mode 100644 index 0000000..3ad064a --- /dev/null +++ b/src/components/ui/color-mode.tsx @@ -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 ( + + ) +} + +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(light: T, dark: T) { + const { colorMode } = useColorMode() + return colorMode === "dark" ? dark : light +} + +export function ColorModeIcon() { + const { colorMode } = useColorMode() + return colorMode === "dark" ? : +} + +interface ColorModeButtonProps extends Omit {} + +export const ColorModeButton = React.forwardRef< + HTMLButtonElement, + ColorModeButtonProps +>(function ColorModeButton(props, ref) { + const { toggleColorMode } = useColorMode() + return ( + }> + + + + + ) +}) + +export const LightMode = React.forwardRef( + function LightMode(props, ref) { + return ( + + ) + }, +) + +export const DarkMode = React.forwardRef( + function DarkMode(props, ref) { + return ( + + ) + }, +) diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..44ff61a --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -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 + backdrop?: boolean +} + +export const DialogContent = React.forwardRef< + HTMLDivElement, + DialogContentProps +>(function DialogContent(props, ref) { + const { + children, + portalled = true, + portalRef, + backdrop = true, + ...rest + } = props + + return ( + + {backdrop && } + + + {children} + + + + ) +}) + +export const DialogCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDialog.CloseTriggerProps +>(function DialogCloseTrigger(props, ref) { + return ( + + + {props.children} + + + ) +}) + +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 diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx new file mode 100644 index 0000000..4b189b5 --- /dev/null +++ b/src/components/ui/drawer.tsx @@ -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 + 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 ( + + {backdrop && } + + + {children} + + + + ) +}) + +export const DrawerCloseTrigger = React.forwardRef< + HTMLButtonElement, + ChakraDrawer.CloseTriggerProps +>(function DrawerCloseTrigger(props, ref) { + return ( + + + + ) +}) + +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 diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx new file mode 100644 index 0000000..4866269 --- /dev/null +++ b/src/components/ui/field.tsx @@ -0,0 +1,33 @@ +import { Field as ChakraField } from "@chakra-ui/react" +import * as React from "react" + +export interface FieldProps extends Omit { + label?: React.ReactNode + helperText?: React.ReactNode + errorText?: React.ReactNode + optionalText?: React.ReactNode +} + +export const Field = React.forwardRef( + function Field(props, ref) { + const { label, children, helperText, errorText, optionalText, ...rest } = + props + return ( + + {label && ( + + {label} + + + )} + {children} + {helperText && ( + {helperText} + )} + {errorText && ( + {errorText} + )} + + ) + }, +) diff --git a/src/components/ui/menu.tsx b/src/components/ui/menu.tsx new file mode 100644 index 0000000..79ceb07 --- /dev/null +++ b/src/components/ui/menu.tsx @@ -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 +} + +export const MenuContent = React.forwardRef( + function MenuContent(props, ref) { + const { portalled = true, portalRef, ...rest } = props + return ( + + + + + + ) + }, +) + +export const MenuArrow = React.forwardRef< + HTMLDivElement, + ChakraMenu.ArrowProps +>(function MenuArrow(props, ref) { + return ( + + + + ) +}) + +export const MenuCheckboxItem = React.forwardRef< + HTMLDivElement, + ChakraMenu.CheckboxItemProps +>(function MenuCheckboxItem(props, ref) { + return ( + + + + + + + {props.children} + + ) +}) + +export const MenuRadioItem = React.forwardRef< + HTMLDivElement, + ChakraMenu.RadioItemProps +>(function MenuRadioItem(props, ref) { + const { children, ...rest } = props + return ( + + + + + + + {children} + + ) +}) + +export const MenuItemGroup = React.forwardRef< + HTMLDivElement, + ChakraMenu.ItemGroupProps +>(function MenuItemGroup(props, ref) { + const { title, children, ...rest } = props + return ( + + {title && ( + + {title} + + )} + {children} + + ) +}) + +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 ( + + {startIcon} + {children} + + + ) +}) + +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 diff --git a/src/components/ui/number-input.tsx b/src/components/ui/number-input.tsx new file mode 100644 index 0000000..0948535 --- /dev/null +++ b/src/components/ui/number-input.tsx @@ -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 ( + + {children} + + + + + + ) +}) + +export const NumberInputField = ChakraNumberInput.Input +export const NumberInputScrubber = ChakraNumberInput.Scrubber +export const NumberInputLabel = ChakraNumberInput.Label diff --git a/src/components/ui/provider.tsx b/src/components/ui/provider.tsx new file mode 100644 index 0000000..ec44b3f --- /dev/null +++ b/src/components/ui/provider.tsx @@ -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 ( + + + + ) +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..e3690c0 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -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 + rootRef?: React.RefObject + trackLabel?: { on: React.ReactNode; off: React.ReactNode } + thumbLabel?: { on: React.ReactNode; off: React.ReactNode } +} + +export const Switch = React.forwardRef( + function Switch(props, ref) { + const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } = + props + + return ( + + + + + {thumbLabel && ( + + {thumbLabel?.on} + + )} + + {trackLabel && ( + + {trackLabel.on} + + )} + + {children != null && ( + {children} + )} + + ) + }, +) diff --git a/src/components/ui/tag.tsx b/src/components/ui/tag.tsx new file mode 100644 index 0000000..df02291 --- /dev/null +++ b/src/components/ui/tag.tsx @@ -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( + 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 ( + + {startElement && ( + {startElement} + )} + {children} + {endElement && ( + {endElement} + )} + {closable && ( + + + + )} + + ) + }, +) diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 0000000..5d70a35 --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -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 ( + + + {(toast) => ( + + {toast.type === "loading" ? ( + + ) : ( + + )} + + {toast.title && {toast.title}} + {toast.description && ( + {toast.description} + )} + + {toast.action && ( + {toast.action.label} + )} + {toast.closable && } + + )} + + + ) +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..0129778 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -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 + content: React.ReactNode + contentProps?: ChakraTooltip.ContentProps + disabled?: boolean +} + +export const Tooltip = React.forwardRef( + function Tooltip(props, ref) { + const { + showArrow, + children, + disabled, + portalled = true, + content, + contentProps, + portalRef, + ...rest + } = props + + if (disabled) return children + + return ( + + {children} + + + + {showArrow && ( + + + + )} + {content} + + + + + ) + }, +) diff --git a/src/components/userComponents/CreateUser.tsx b/src/components/userComponents/CreateUser.tsx index 8e3cc58..88ea2fe 100644 --- a/src/components/userComponents/CreateUser.tsx +++ b/src/components/userComponents/CreateUser.tsx @@ -3,15 +3,13 @@ import { useForm } from 'react-hook-form' import { UserFormValues } from '../../models/user' import { createUser } from '../../stateManagement/userState' import { - Alert, - AlertDescription, - AlertIcon, Button, Container, - FormControl, FormErrorMessage, - FormLabel, - Input, useColorModeValue, + Input, } from '@chakra-ui/react' +import {Field} from '../ui/field' +import {Alert} from '../ui/alert' +import {useColorModeValue} from '../ui/color-mode' export default function CreateUser() { const { @@ -44,59 +42,52 @@ export default function CreateUser() { return ( - - Full Name + - {errors.name && { errors.name.message?.toString()}} - - - Display Name + + - {errors.displayName && { errors.displayName.message?.toString()}} - - - Email + + - {errors.email && { errors.email.message?.toString()}} - - - Password + + - {errors.password && { errors.password.message?.toString()}} - + {errors.password && ( <> - - - {errors.password.message?.toString()} - {/**/} + + {errors.password.message?.toString()}
)} - - Repeat Password + - {errors.repeat_password && { errors.repeat_password.message?.toString()}} - + {errors.repeat_password && ( <> - - - {errors.repeat_password.message?.toString()} - {/**/} + + {errors.repeat_password.message?.toString()} )}
diff --git a/src/components/userComponents/EditUser.tsx b/src/components/userComponents/EditUser.tsx index fec1405..5acfecb 100644 --- a/src/components/userComponents/EditUser.tsx +++ b/src/components/userComponents/EditUser.tsx @@ -5,14 +5,14 @@ import { loadedPreferences, loggedInUser, updateUserPreferences } from '../../st import { Button, Container, Flex, - FormControl, - FormLabel, Heading, Input, - Stack, Switch, - useColorModeValue, - useToast, + Stack, } 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 agent from '../../api/agent' import { useNavigate } from 'react-router-dom' @@ -43,12 +43,10 @@ export default function EditUser() { const history = useNavigate() useEffect(() => { - reset(user) + reset(user as any) return }, [user.name !== '']) - const toast = useToast() - interface Message { success: boolean data: string @@ -66,13 +64,12 @@ export default function EditUser() { const engageChangePassword = async (values: IForgotPassword) => { const message = await agent.Account.forgot(values as IForgotPassword) as Message - toast({ + toaster.create({ title: 'Email Sent', - status: 'success', + type: 'success', description: message.data, - isClosable: true, + closable: true, duration: 10000, - position: 'top' }) } @@ -90,78 +87,70 @@ export default function EditUser() { Edit {user.displayName} - - Full Name + - - - Display Name + + - - - Email + + - - - - Dark Mode: + + ( - + onChange(checked)} ref={ref} /> )} /> - - - - - Sticky Navbar: + + ( - + onChange(checked)} ref={ref} /> )} /> - - +
- + @@ -42,28 +40,28 @@ const UserProfile = () => { - - - - - - - - - - - - - - - - - - - - -
Display Name: {user.displayName}
Full Name: {user.name}
Email: {user.email}
Role: {user.role}
-
+ + + + + Display Name: + {user.displayName} + + + Full Name: + {user.name} + + + Email: + {user.email} + + + Role: + {user.role} + + + +
diff --git a/src/errors/NotFound.tsx b/src/errors/NotFound.tsx index eb554c0..704ef71 100644 --- a/src/errors/NotFound.tsx +++ b/src/errors/NotFound.tsx @@ -29,8 +29,8 @@ export default function NotFound() { It looks like you hit a{' '} {DEAD} end! - }>