diff --git a/package.json b/package.json index d784cec2..6e5d3527 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "graasp-app-math-input", - "version": "1.2.3", + "version": "0.0.0", "license": "AGPL-3.0-only", "author": "Graasp", "contributors": [ - "Basile Spaenlehauer", - "Jérémy La Scala" + { + "name": "Jérémy La Scala", + "email": "jeremy.lascala@epfl.ch", + "url": "https://github.com/swouf" + } ], "homepage": ".", "type": "module", @@ -25,6 +28,7 @@ "@types/react": "18.2.79", "@types/react-dom": "18.2.25", "i18next": "^23.9.0", + "mathlive": "^0.98.6", "react": "18.2.0", "react-dom": "18.2.0", "react-i18next": "14.1.1", @@ -34,6 +38,7 @@ "scripts": { "dev": "yarn vite", "start": "yarn dev", + "dev:mock": "VITE_ENABLE_MOCK_API=true && yarn vite", "start:test": "yarn vite --mode test", "build": "yarn vite build", "build:test": "yarn vite build --mode test", diff --git a/src/config/i18n.ts b/src/config/i18n.ts index 015e49a3..6021bb2a 100644 --- a/src/config/i18n.ts +++ b/src/config/i18n.ts @@ -27,7 +27,7 @@ i18n.use(initReactI18next).init({ debug: import.meta.env.DEV, ns: [defaultNS], defaultNS, - keySeparator: false, + keySeparator: '.', interpolation: { escapeValue: false, formatSeparator: ',', diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 3d48de78..d8ee9643 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -2,5 +2,11 @@ export const PLAYER_VIEW_CY = 'player-view'; export const BUILDER_VIEW_CY = 'builder-view'; export const ANALYTICS_VIEW_CY = 'analytics-view'; +export const MATH_INPUT_VIEW_CY = 'math-input-view'; +export const SETTINGS_VIEW_CY = 'settings_view'; + +export const MATH_INPUT_TAB_CY = 'math-input-tab'; +export const SETTINGS_TAB_CY = 'settings-tab'; + export const buildDataCy = (selector: string): string => `[data-cy=${selector}]`; diff --git a/src/langs/en.json b/src/langs/en.json index f8f4256a..8fd86c4b 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -1,6 +1,8 @@ { "translations": { "Welcome to the Graasp App Starter Kit": "Welcome to the Graasp App Starter Kit", + "MATH_INPUT_TAB": "Math input", + "SETTINGS_TAB": "Settings", "ERROR_BOUNDARY": { "FALLBACK": { "MESSAGE_TITLE": "Sorry, something went wrong with this application", diff --git a/src/math-field.d.ts b/src/math-field.d.ts new file mode 100644 index 00000000..c436c014 --- /dev/null +++ b/src/math-field.d.ts @@ -0,0 +1,14 @@ +import { MathfieldElement } from 'mathlive'; + +declare global { + namespace React.JSX { + interface IntrinsicElements { + 'math-field': React.DetailedHTMLProps< + React.HTMLAttributes, + MathfieldElement + >; + } + } +} + +export {}; diff --git a/src/mocks/db.ts b/src/mocks/db.ts index b87c6aed..b9161326 100644 --- a/src/mocks/db.ts +++ b/src/mocks/db.ts @@ -35,7 +35,7 @@ export const mockItem: DiscriminatedItem = AppItemFactory({ export const defaultMockContext: LocalContext = { apiHost: API_HOST, permission: PermissionLevel.Admin, - context: 'builder', + context: 'player', itemId: mockItem.id, memberId: mockMembers[0].id, }; diff --git a/src/modules/common/MathField.tsx b/src/modules/common/MathField.tsx new file mode 100644 index 00000000..539558e0 --- /dev/null +++ b/src/modules/common/MathField.tsx @@ -0,0 +1,50 @@ +import React, { ChangeEvent, useEffect, useRef, useState } from 'react'; + +// eslint-disable-next-line import/no-duplicates +import 'mathlive'; +// eslint-disable-next-line import/no-duplicates +import { MathfieldElement } from 'mathlive'; + +interface MathFieldProps { + value?: string; + onChange?: (newValue: string) => void; +} + +const MathField = ({ value, onChange }: MathFieldProps): JSX.Element => { + const [internalValue, setInternalValue] = useState(''); + const mf = useRef(); + + useEffect(() => { + if (mf.current) { + mf.current.mathVirtualKeyboardPolicy = 'manual'; + } + }); + + useEffect(() => { + if (value) { + setInternalValue(value); + } + }, [value]); + + const onInput = (evt: ChangeEvent): void => { + const newValue = evt.target.value; + if (onChange) { + onChange(newValue); + } + setInternalValue(newValue); + }; + + return ( + + {typeof value !== 'undefined' ? value : internalValue} + + ); +}; + +export default MathField; diff --git a/src/modules/common/TabPanel.tsx b/src/modules/common/TabPanel.tsx new file mode 100644 index 00000000..4976dddf --- /dev/null +++ b/src/modules/common/TabPanel.tsx @@ -0,0 +1,30 @@ +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +const TabPanel = (props: TabPanelProps): JSX.Element => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + +export default TabPanel; diff --git a/src/modules/main/BuilderView.tsx b/src/modules/main/BuilderView.tsx index 8514e63d..9e436238 100644 --- a/src/modules/main/BuilderView.tsx +++ b/src/modules/main/BuilderView.tsx @@ -1,104 +1,86 @@ -import { Box, Button, Stack, Typography } from '@mui/material'; +import { SyntheticEvent, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; import { useLocalContext } from '@graasp/apps-query-client'; +import { PermissionLevel } from '@graasp/sdk'; -import { BUILDER_VIEW_CY } from '@/config/selectors'; +import { + BUILDER_VIEW_CY, + MATH_INPUT_TAB_CY, + SETTINGS_TAB_CY, +} from '@/config/selectors'; -import { hooks, mutations } from '../../config/queryClient'; +import TabPanel from '../common/TabPanel'; +import MathInputView from '../math-input-view/MathInputView'; +import SettingsView from '../settings/SettingsView'; -const AppSettingsDisplay = (): JSX.Element => { - const { data: appSettings } = hooks.useAppSettings(); - return ( - - App Setting - {appSettings ? ( -
{JSON.stringify(appSettings, null, 2)}
- ) : ( - Loading - )} -
+interface TabType { + tabLabel: string; + tabChild: JSX.Element; + tabSelector: string; +} + +const BuilderView = (): JSX.Element => { + const [selectedTab, setSelectedTab] = useState(0); + const { t } = useTranslation(); + const handleChange = (event: SyntheticEvent, value: number): void => { + setSelectedTab(value); + }; + const { permission } = useLocalContext(); + + const isAdmin = useMemo( + () => permission === PermissionLevel.Admin, + [permission], ); -}; -const AppActionsDisplay = (): JSX.Element => { - const { data: appActions } = hooks.useAppActions(); - return ( - - App Actions - {appActions ? ( -
{JSON.stringify(appActions, null, 2)}
- ) : ( - Loading - )} -
+ const mathInputTab = useMemo( + () => ({ + tabLabel: t('MATH_INPUT_TAB'), + tabChild: , + tabSelector: MATH_INPUT_TAB_CY, + }), + [t], ); -}; -const BuilderView = (): JSX.Element => { - const { permission } = useLocalContext(); - const { data: appDatas } = hooks.useAppData(); - const { mutate: postAppData } = mutations.usePostAppData(); - const { mutate: postAppAction } = mutations.usePostAppAction(); - const { mutate: patchAppData } = mutations.usePatchAppData(); - const { mutate: deleteAppData } = mutations.useDeleteAppData(); - const { mutate: postAppSetting } = mutations.usePostAppSetting(); + const settingsTab = useMemo( + () => ({ + tabLabel: t('SETTINGS_TAB'), + tabChild: , + tabSelector: SETTINGS_TAB_CY, + }), + [t], + ); + + const tabs: TabType[] = useMemo( + () => (isAdmin ? [mathInputTab, settingsTab] : [mathInputTab]), + [isAdmin, mathInputTab, settingsTab], + ); return ( -
- Builder as {permission} - - - - - - - - - - App Data -
{JSON.stringify(appDatas, null, 2)}
-
- - -
-
+ + + + {tabs.map((tab, index) => ( + + ))} + + + {tabs.map((tab, index) => ( + + {tab.tabChild} + + ))} + ); }; + export default BuilderView; diff --git a/src/modules/main/PlayerView.tsx b/src/modules/main/PlayerView.tsx index 7cf9f253..c6bfdaa7 100644 --- a/src/modules/main/PlayerView.tsx +++ b/src/modules/main/PlayerView.tsx @@ -1,23 +1,12 @@ -import { Box, Typography } from '@mui/material'; +import Container from '@mui/material/Container'; -import { useLocalContext } from '@graasp/apps-query-client'; - -import { hooks } from '@/config/queryClient'; import { PLAYER_VIEW_CY } from '@/config/selectors'; -const PlayerView = (): JSX.Element => { - const { permission } = useLocalContext(); - const { data: appContext } = hooks.useAppContext(); - const members = appContext?.members; +import MathInputView from '../math-input-view/MathInputView'; - return ( -
- Player as {permission} - - Members -
{JSON.stringify(members, null, 2)}
-
-
- ); -}; +const PlayerView = (): JSX.Element => ( + + + +); export default PlayerView; diff --git a/src/modules/math-input-view/MathInputView.tsx b/src/modules/math-input-view/MathInputView.tsx new file mode 100644 index 00000000..37443b29 --- /dev/null +++ b/src/modules/math-input-view/MathInputView.tsx @@ -0,0 +1,11 @@ +import Box from '@mui/material/Box'; + +import MathField from '../common/MathField'; + +const MathInputView = (): JSX.Element => ( + + + +); + +export default MathInputView; diff --git a/src/modules/settings/SettingsView.tsx b/src/modules/settings/SettingsView.tsx new file mode 100644 index 00000000..f0570c5d --- /dev/null +++ b/src/modules/settings/SettingsView.tsx @@ -0,0 +1,7 @@ +import Typography from '@mui/material/Typography'; + +const SettingsView = (): JSX.Element => ( + Settings +); + +export default SettingsView; diff --git a/src/styles/MathField.css b/src/styles/MathField.css new file mode 100644 index 00000000..049f81ad --- /dev/null +++ b/src/styles/MathField.css @@ -0,0 +1,7 @@ +math-field::part(virtual-keyboard-toggle) { + display: none; + } + + math-field::part(menu-toggle) { + display: none; + } diff --git a/yarn.lock b/yarn.lock index 4f406a9b..901d97fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1913,6 +1913,16 @@ __metadata: languageName: node linkType: hard +"@cortex-js/compute-engine@npm:0.22.0": + version: 0.22.0 + resolution: "@cortex-js/compute-engine@npm:0.22.0" + dependencies: + complex.js: "npm:^2.1.1" + decimal.js: "npm:^10.4.3" + checksum: 10/38e5b5ccea61ce2a73e76d60e51ad9800e9129148fb7813f13aab878e2119aeb99b7c93f702505ec561852bcf6f0bb430b6b9d76cebd1be30359eb07f7f82f2b + languageName: node + linkType: hard + "@cypress/code-coverage@npm:3.12.36": version: 3.12.36 resolution: "@cypress/code-coverage@npm:3.12.36" @@ -5575,6 +5585,13 @@ __metadata: languageName: node linkType: hard +"complex.js@npm:^2.1.1": + version: 2.1.1 + resolution: "complex.js@npm:2.1.1" + checksum: 10/1905d5204dd8a4d6f591182aca2045986f1ff3c5373e455ccd10c6ee2905bf1d3811a313d38c68f8a8507523202f91e25177387e3adc386c1b5b5ec2f13a6dbb + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -5939,6 +5956,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 10/de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0 + languageName: node + linkType: hard + "deep-eql@npm:^4.1.3": version: 4.1.3 resolution: "deep-eql@npm:4.1.3" @@ -7898,6 +7922,7 @@ __metadata: eslint-plugin-react-hooks: "npm:4.6.0" husky: "npm:9.0.11" i18next: "npm:^23.9.0" + mathlive: "npm:^0.98.6" miragejs: "npm:^0.1.48" nock: "npm:^13.5.3" nyc: "npm:15.1.0" @@ -9437,6 +9462,15 @@ __metadata: languageName: node linkType: hard +"mathlive@npm:^0.98.6": + version: 0.98.6 + resolution: "mathlive@npm:0.98.6" + dependencies: + "@cortex-js/compute-engine": "npm:0.22.0" + checksum: 10/c67963f413ba5a76faa05cea1c8373dd98b92d5f74a4c97b34ba5d08272e861ffcf6c32b8a8791ff24f99fa802628ba11ca328d0bb9ce13e509d38e52c0646b1 + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0"