diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b56809c..3cfcae5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@reduxjs/toolkit": "^1.9.3", "axios": "^1.5.0", "react": "^18.2.0", + "react-circle-flags": "^0.0.20", "react-dom": "^18.2.0", "react-redux": "^8.0.5", "react-scripts": "5.0.1", @@ -13886,6 +13887,17 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-circle-flags": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/react-circle-flags/-/react-circle-flags-0.0.20.tgz", + "integrity": "sha512-/Q18+veXCSA0lQWbnRo/AtR4G/NDRrukKyU6Cl7iLLXAG9KywjgFdPqHD2YJklicjfALXm2wLdbONRs0QKDITA==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -27076,6 +27088,12 @@ } } }, + "react-circle-flags": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/react-circle-flags/-/react-circle-flags-0.0.20.tgz", + "integrity": "sha512-/Q18+veXCSA0lQWbnRo/AtR4G/NDRrukKyU6Cl7iLLXAG9KywjgFdPqHD2YJklicjfALXm2wLdbONRs0QKDITA==", + "requires": {} + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 073fc53..a2533b8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "@reduxjs/toolkit": "^1.9.3", "axios": "^1.5.0", "react": "^18.2.0", + "react-circle-flags": "^0.0.20", "react-dom": "^18.2.0", "react-redux": "^8.0.5", "react-scripts": "5.0.1", diff --git a/frontend/src/components/LanguageSelect.tsx b/frontend/src/components/LanguageSelect.tsx new file mode 100644 index 0000000..bf6ac10 --- /dev/null +++ b/frontend/src/components/LanguageSelect.tsx @@ -0,0 +1,190 @@ +import * as React from 'react'; +import { FC } from 'react'; +import { Select as BaseSelect, SelectProps, SelectRootSlotProps, } from '@mui/base/Select'; +import { Option as BaseOption, optionClasses } from '@mui/base/Option'; +import { SelectOption } from '@mui/base/useOption'; +import { styled } from '@mui/system'; +import { Popper as BasePopper } from '@mui/base/Popper'; +import KeyboardArrowDownRounded from '@mui/icons-material/KeyboardArrowDownRounded'; +import { useTheme } from "@mui/material/styles"; +import { CircleFlag } from "react-circle-flags"; + +type Props = { + disabled?: boolean; + prefixId?: string; + onChange?: (value: string) => void; + value?: string; +}; + +declare type Language = { + code: string; + language: string; + tm: string; +}; + +const availableCountries: Language[] = [ + {code: 'pt', language: "Português", tm: 'BR'}, + {code: 'cn', language: '简体中文', tm: 'CNS'}, + {code: 'cn', language: '繁體中文', tm: 'CNT'}, + {code: 'cz', language: 'Česky', tm: "CZ"}, + {code: 'de', language: 'Deutsch', tm: "DE"}, + {code: 'uk', language: 'English', tm: 'EN'}, + {code: 'fr', language: 'Français', tm: 'FR'}, + {code: 'gr', language: 'Ελληνικά', tm: 'GR'}, + {code: 'hu', language: 'Magyar', tm: 'HU'}, + {code: 'it', language: 'Italiano', tm: 'IT'}, + {code: 'jp', language: '日本語', tm: 'JP'}, + {code: 'kr', language: '한국어', tm: 'KR'}, + {code: 'nl', language: 'Dutch', tm: 'NL'}, + {code: 'pl', language: 'Polski', tm: 'PL'}, + {code: 'ru', language: 'Русский', tm: 'RU'}, + {code: 'es', language: 'Español', tm: "SP"}, + {code: 'th', language: 'ไทย', tm: 'TH'}, + {code: 'ua', language: 'Українська', tm: 'UA'}, +]; + +const LanguageSelect: FC = (props) => { + + function getCountry() { + return availableCountries.find(c => c.tm === props.value); + } + + return ( + + ); +} + +export default LanguageSelect; + +const Select = React.forwardRef(function CustomSelect( + props: SelectProps, + ref: React.ForwardedRef +) { + const slots: SelectProps['slots'] = { + root: Button, + listbox: Listbox, + popper: Popper, + ...props.slots, + }; + + return ; +}); + +const Button = React.forwardRef(function Button< + TValue extends {}, + Multiple extends boolean +>( + props: SelectRootSlotProps, + ref: React.ForwardedRef +) { + const theme = useTheme(); + const {ownerState, ...other} = props; + return ( + + {other.children} + + + ); +}); + +const StyledButton = styled('button', {shouldForwardProp: () => true})( + ({theme}) => ` + box-sizing: border-box; + min-width: 80px; + padding: 8px; + text-align: left; + line-height: 1.5; + background: none; + border: none; + position: relative; + opacity: unset; + + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + -webkit-appearance: none; + + &:hover { + cursor: pointer; + } + + & img { + vertical-align: sub; + } + + & > svg { + position: absolute; + top: 0; + } + ` +); + +const Listbox = styled('ul')( + ({theme}) => ` + font-family: "Plus Jakarta Sans"; + font-size: 1rem; + box-sizing: border-box; + padding: 6px; + margin: 2px 0; + min-width: 160px; + max-height: 315px; + border-radius: 8px; + overflow: auto; + outline: 0px; + background: ${theme.palette.background.paper}; + border: 1px solid ${theme.palette.mode === 'dark' ? theme.palette.languageSwitch[800] : theme.palette.languageSwitch[100]}; + ` +); + +const Option = styled(BaseOption)( + ({theme}) => ` + list-style: none; + padding: 4px; + border-radius: 4px; + cursor: default; + + &.${optionClasses.selected}, + &.${optionClasses.highlighted}.${optionClasses.selected} { + background-color: ${theme.palette.mode === 'dark' ? theme.palette.languageSwitch[900] : theme.palette.languageSwitch[100]}; + color: ${theme.palette.mode === 'dark' ? theme.palette.languageSwitch[100] : theme.palette.languageSwitch[900]}; + } + + &:hover, + &.${optionClasses.highlighted} { + background-color: ${theme.palette.mode === 'dark' ? theme.palette.languageSwitch[800] : theme.palette.languageSwitch[100]}; + color: ${theme.palette.mode === 'dark' ? theme.palette.languageSwitch[300] : theme.palette.languageSwitch[900]}; + } + + &:hover { + cursor: pointer; + } + + & img { + margin-right: 10px; + vertical-align: middle; + } + ` +); + +const Popper = styled(BasePopper)` + z-index: 10; +`; diff --git a/frontend/src/hooks/useCriteriaCard.ts b/frontend/src/hooks/useCriteriaCard.ts index 18251c9..51a26c1 100644 --- a/frontend/src/hooks/useCriteriaCard.ts +++ b/frontend/src/hooks/useCriteriaCard.ts @@ -62,14 +62,13 @@ export const criteriaCardPool: CriteriaCard[] = [ { id: 48, criteriaSlots: 9, irrelevantCriteria: [] }, ]; -const getCardUrl = (card?: CriteriaCard) => - card - ? `https://turingmachine.info/images/criteriacards/EN/TM_GameCards_EN-${( - "0" + card.id - ).slice(-2)}.png` +const getCardUrl = (card?: CriteriaCard, language?: string) => + (card && language) + ? `https://turingmachine.info/images/criteriacards/${language}/TM_GameCards_${language}-${( "0" + card.id ).slice(-2)}.png` : ""; export const useCriteriaCard = (verifier: Verifier, index: number) => { + const language = useAppSelector((state) => state.settings.language); const comments = useAppSelector((state) => state.comments); const comment = comments.find((comment) => comment.verifier === verifier); @@ -111,7 +110,7 @@ export const useCriteriaCard = (verifier: Verifier, index: number) => { } }; - const cardImage = useMemo(() => getCardUrl(card), [card]); + const cardImage = useMemo(() => getCardUrl(card, language), [card, language]); useUpdateEffect(() => { dispatch(commentsActions.updateCard({ verifier, index, card })); diff --git a/frontend/src/hooks/usePaletteMode.ts b/frontend/src/hooks/usePaletteMode.ts index 8544e82..e6076fe 100644 --- a/frontend/src/hooks/usePaletteMode.ts +++ b/frontend/src/hooks/usePaletteMode.ts @@ -4,6 +4,16 @@ import { settingsActions } from "../store/slices/settingsSlice"; import { useAppDispatch } from "./useAppDispatch"; import { useAppSelector } from "./useAppSelector"; +declare module '@mui/material/styles' { + interface Palette { + languageSwitch: Palette['primary']; + } + + interface PaletteOptions { + languageSwitch?: PaletteOptions['primary']; + } +} + export const usePaletteMode = () => { const dispatch = useAppDispatch(); const settings = useAppSelector((state) => state.settings); @@ -31,6 +41,12 @@ export const usePaletteMode = () => { secondary: { main: "#ff1744", }, + languageSwitch: { + 100: '#E5EAF2', + 300: '#C7D0DD', + 800: '#303740', + 900: '#1C2025', + }, mode: settings.paletteMode, }, typography: { diff --git a/frontend/src/store/slices/commentsSlice.ts b/frontend/src/store/slices/commentsSlice.ts index a89d100..6aff219 100644 --- a/frontend/src/store/slices/commentsSlice.ts +++ b/frontend/src/store/slices/commentsSlice.ts @@ -56,6 +56,13 @@ export const commentsSlice = createSlice({ const { fake, ind, m } = action.payload; const nightmare = m === 2; + const addAdditionalCardAttributes = (card: number) => { + return { + ...criteriaCardPool.find((cc) => cc.id === card)!, + nightmare + } + } + for (let i = 0; i < ind.length; i++) { if (fake) { const cards = [ind[i], fake[i]]; @@ -65,8 +72,8 @@ export const commentsSlice = createSlice({ verifier: verifiers[i], nightmare, criteriaCards: [ - criteriaCardPool.find((cc) => cc.id === shuffledCards[0])!, - criteriaCardPool.find((cc) => cc.id === shuffledCards[1])!, + addAdditionalCardAttributes(shuffledCards[0]), + addAdditionalCardAttributes(shuffledCards[1]), ], letters: createLetters(ind.length, verifiers[i], nightmare), }); @@ -77,10 +84,7 @@ export const commentsSlice = createSlice({ verifier: verifiers[i], nightmare, criteriaCards: [ - { - ...criteriaCardPool.find((cc) => cc.id === card)!, - nightmare, - }, + addAdditionalCardAttributes(card), ], letters: createLetters(ind.length, verifiers[i], nightmare), }); diff --git a/frontend/src/store/slices/settingsSlice.ts b/frontend/src/store/slices/settingsSlice.ts index 27d0e9d..e2f92ec 100644 --- a/frontend/src/store/slices/settingsSlice.ts +++ b/frontend/src/store/slices/settingsSlice.ts @@ -3,11 +3,13 @@ import { createSlice } from "@reduxjs/toolkit"; export type SettingsState = { paletteMode: PaletteMode; storeVersion: number; + language: string; }; const initialState: SettingsState = { paletteMode: "light", storeVersion: parseInt(process.env.REACT_APP_STORE_VERSION), + language: "EN", }; export const settingsSlice = createSlice({ @@ -17,6 +19,9 @@ export const settingsSlice = createSlice({ togglePaletteMode: (state) => { state.paletteMode = state.paletteMode === "light" ? "dark" : "light"; }, + updateLanguage: (state, action) => { + state.language = action.payload + } }, }); diff --git a/frontend/src/views/Root.tsx b/frontend/src/views/Root.tsx index 9409cb5..43efa4e 100644 --- a/frontend/src/views/Root.tsx +++ b/frontend/src/views/Root.tsx @@ -29,6 +29,8 @@ import Registration from "./Registration"; import Rounds from "./Rounds"; import Saves from "./Saves"; import { checkDeductions } from "deductions"; +import LanguageSelect from "../components/LanguageSelect"; +import { settingsActions } from "../store/slices/settingsSlice"; const Root: FC = () => { const { theme, togglePaletteMode } = usePaletteMode(); @@ -36,6 +38,7 @@ const Root: FC = () => { const isUpLg = useMediaQuery(theme.breakpoints.up("lg")); const dispatch = useAppDispatch(); const state = useAppSelector((state) => state); + const language = useAppSelector((state) => state.settings.language); const [savesDialog, setSavesDialog] = useState(false); const [hasBadge, setHasBadge] = useState(false); @@ -140,7 +143,10 @@ const Root: FC = () => { color="primary" sx={{ position: "relative" }} onClick={() => { - dispatch(registrationActions.reset()); + // eslint-disable-next-line no-restricted-globals + if (!canBeSave() || confirm("Your game is not saved!\nDo you really want to create a new game.")) { + dispatch(registrationActions.reset()); + } }} > @@ -215,6 +221,16 @@ const Root: FC = () => { orientation="vertical" sx={{ height: "auto", margin: theme.spacing(0, 1) }} /> + dispatch(settingsActions.updateLanguage(value))} + /> +