diff --git a/craco.config.js b/craco.config.js deleted file mode 100644 index 95f47e6..0000000 --- a/craco.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: [ - { - plugin: require('@matrix-widget-toolkit/semantic-ui/craco/buildSemanticUiThemePlugin'), - }, - ], -}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c6bea06 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,138 @@ +/* + * Copyright 2025 Nordeck IT + Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fixupPluginRules } from '@eslint/compat'; +import js from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import notice from 'eslint-plugin-notice'; +import pluginPromise from 'eslint-plugin-promise'; +import react from 'eslint-plugin-react'; +import hooksPlugin from 'eslint-plugin-react-hooks'; +import testingLibrary from 'eslint-plugin-testing-library'; +import vitest from 'eslint-plugin-vitest'; +import path from 'path'; +import ts from 'typescript-eslint'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default ts.config( + { + ignores: [ + '**/build/**', + '**/coverage/**', + '**/i18next-parser.config.js', + '**/*test.ts.snap', + 'scripts/prepack.js', + 'scripts/postpack.js', + 'scripts/publishAllPackages.js', + ], + }, + { + settings: { + react: { + version: 'detect', + }, + }, + }, + js.configs.recommended, + ...ts.configs.recommended, + pluginPromise.configs['flat/recommended'], + { + plugins: { + notice, + }, + rules: { + 'notice/notice': [ + 'error', + { + templateFile: path.resolve(__dirname, './scripts/license-header.txt'), + onNonMatchingHeader: 'replace', + templateVars: { NAME: 'Nordeck IT + Consulting GmbH' }, + varRegexps: { NAME: /.+/ }, + }, + ], + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: 'react-dom/test-utils', + importNames: ['act'], + message: 'Please import "act" from "react" instead', + }, + ], + }, + ], + // Disable for the migration to prevent a lot of errors. + // Should be revisisted + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + // Allow unused vars starting with _ + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + }, + }, + { + ...react.configs.flat.recommended, + plugins: { + ...react.configs.flat.recommended.plugins, + 'react-hooks': fixupPluginRules(hooksPlugin), + }, + rules: { + ...hooksPlugin.configs.recommended.rules, + ...react.configs.flat.recommended.rules, + 'react/display-name': 'off', + 'react/no-unescaped-entities': 'off', + // Disabled to avoid weird error messages + // https://github.com/jsx-eslint/eslint-plugin-react/issues?q=is%3Aissue+is%3Aopen+forwardRef + 'react/prop-types': 'off', + // Disabled because it would conflict with removing unused imports + 'react/react-in-jsx-scope': 'off', + }, + }, + // Test-specific configuration + { + files: ['**/*.test.*'], + plugins: { + // See https://github.com/testing-library/eslint-plugin-testing-library/issues/899#issuecomment-2121272355 and + // https://github.com/testing-library/eslint-plugin-testing-library/issues/924 + 'testing-library': fixupPluginRules({ + rules: testingLibrary.rules, + }), + vitest, + }, + rules: { + ...testingLibrary.configs['flat/react'].rules, + ...vitest.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + 'react/display-name': 'off', + }, + }, + eslintConfigPrettier, +); \ No newline at end of file diff --git a/i18next-parser.config.js b/i18next-parser.config.js index c40b8b5..9db03dd 100644 --- a/i18next-parser.config.js +++ b/i18next-parser.config.js @@ -14,9 +14,11 @@ * limitations under the License. */ -module.exports = { +const i18NextParserConfig = { locales: ['en', 'de'], output: 'public/locales/$LOCALE/$NAMESPACE.json', sort: true, resetDefaultValueLocale: 'en', }; + +export default i18NextParserConfig; diff --git a/index.html b/index.html new file mode 100644 index 0000000..da69cd9 --- /dev/null +++ b/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + Matrix BarCamp Widget + + + + + + + + + + +
+ + + diff --git a/package.json b/package.json index 8efe34e..c7201b9 100644 --- a/package.json +++ b/package.json @@ -5,32 +5,39 @@ "license": "Apache-2.0", "version": "1.2.0", "private": true, + "type": "module", "dependencies": { - "@matrix-widget-toolkit/api": "^3.2.2", - "@matrix-widget-toolkit/react": "^1.0.6", - "@matrix-widget-toolkit/semantic-ui": "^1.0.8", + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^3.1.0", + "@matrix-widget-toolkit/api": "^5.0.2", + "@matrix-widget-toolkit/mui": "^2.2.0", + "@matrix-widget-toolkit/react": "^2.1.0", "@react-hookz/web": "^14.2.2", - "@reduxjs/toolkit": "^1.9.3", + "@reduxjs/toolkit": "^2.6.0", + "@vitejs/plugin-basic-ssl": "^2.1.0", + "@vitejs/plugin-react-swc": "^4.0.1", "cross-fetch": "^4.0.0", "i18next": "^23.7.11", + "i18next-browser-languagedetector": "^8.2.0", "i18next-chained-backend": "^4.6.2", - "i18next-http-backend": "^2.4.2", + "i18next-http-backend": "^2.7.3", "immer": "^9.0.19", "joi": "^17.12.1", "lodash": "^4.17.21", "loglevel": "^1.9.1", "luxon": "^3.3.0", "matrix-widget-api": "^1.1.1", - "react": "^17.0.2", + "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", - "react-dom": "^17.0.2", + "react-dom": "^18.3.1", "react-focus-lock": "^2.9.6", "react-focus-on": "^3.9.1", - "react-redux": "^8.0.5", + "react-is": "^18.3.0", + "react-redux": "^9.2.0", "react-use": "^17.5.0", "rfc4648": "^1.5.3", - "rxjs": "^7.8.0", - "semantic-ui-react": "^2.1.5", + "rxjs": "^7.8.2", "styled-components": "^5.3.6" }, "engines": { @@ -38,14 +45,17 @@ "yarn": ">=1.22.1 <2.0.0" }, "scripts": { - "start": "cross-env HTTPS=true BROWSER=none WDS_SOCKET_PORT=0 craco start", - "dev": "cross-env BROWSER=none ESLINT_NO_DEV_ERRORS=true TSC_COMPILE_ON_ERROR=true WDS_SOCKET_PORT=0 craco start", - "build": "cross-env GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false craco build", - "lint": "eslint . --max-warnings=0", - "test": "craco test", - "test:all": "craco test --coverage --watchAll=false", + "dev": "vite", + "dev:https": "VITE_DEV_SSL=true vite", + "preview": "vite preview", + "preview:https": "VITE_DEV_SSL=true vite preview", + "build": "tsc && vite build", + "test": "vitest", + "test:all": "vitest run --coverage", + "test:ui": "vitest --ui", + "tsc": "tsc --noEmit", "deduplicate": "yarn-deduplicate", - "depcheck": "depcheck --ignores=@types/jest,@types/node,prettier-plugin-organize-imports,typescript,i18next-parser,@changesets/cli", + "depcheck": "depcheck --ignores=@types/node,@vitest/coverage-v8,@types/node,prettier-plugin-organize-imports,typescript,i18next-parser,@changesets/cli,vitest,@vitest/ui,jsdom", "translate": "i18next \"src/**/*.{ts,tsx}\"", "prettier:check": "prettier --check .", "prettier:write": "prettier --write .", @@ -86,23 +96,27 @@ "devDependencies": { "@axe-core/playwright": "^4.6.0", "@changesets/cli": "^2.27.1", - "@craco/craco": "^7.1.0", - "@matrix-widget-toolkit/testing": "^2.3.2", + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.37.0", + "@matrix-widget-toolkit/testing": "^3.0.0", "@playwright/test": "^1.30.0", - "@semantic-ui-react/craco-less": "^3.0.0", - "@testing-library/jest-dom": "^6.2.0", - "@testing-library/react": "^12.1.5", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.8.0", + "@testing-library/react": "^16.3.0", "@testing-library/react-hooks": "^8.0.1", - "@testing-library/user-event": "^14.5.2", + "@testing-library/user-event": "^14.6.1", "@types/jest": "^27.5.2", "@types/lodash": "^4.14.202", "@types/luxon": "^3.2.0", "@types/node": "^20.11.1", - "@types/react": "^17.0.52", - "@types/react-beautiful-dnd": "^13.1.2", - "@types/react-dom": "^17.0.18", + "@types/react": "^18.3.12", + "@types/react-beautiful-dnd": "^13.1.8", + "@types/react-dom": "^18.3.1", "@types/react-i18next": "^8.1.0", + "@types/react-redux": "^7.1.34", "@types/styled-components": "^5.1.26", + "@vitejs/plugin-react": "^5.0.2", + "@vitest/ui": "^3.2.4", "cross-env": "^7.0.3", "depcheck": "^1.4.7", "eslint": "^8.55.0", @@ -111,16 +125,28 @@ "eslint-plugin-notice": "^0.9.10", "eslint-plugin-playwright": "^0.12.0", "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.0", + "eslint-plugin-testing-library": "^7.13.3", + "eslint-plugin-vitest": "^0.5.4", + "happy-dom": "^19.0.2", "husky": "^9.1.6", - "i18next-parser": "8.12.0", + "i18next-parser": "^9.3.0", + "jsdom": "^26.1.0", "lint-staged": "^15.2.0", "nanoid": "^3.3.4", "prettier": "^2.8.4", "prettier-plugin-organize-imports": "^3.2.4", "react-i18next": "^12.1.4", - "react-scripts": "5.0.1", "testcontainers": "^10.5.0", - "typescript": "~4.6.4", + "typescript": "5.7.3", + "typescript-eslint": "^8.46.1", + "vite": "^7.1.4", + "vite-plugin-checker": "^0.10.3", + "vite-plugin-eslint": "^1.8.1", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4", "yarn-deduplicate": "^6.0.2" - } + }, + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" } diff --git a/public/index.html b/public/index.html deleted file mode 100644 index ab5d0a7..0000000 --- a/public/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - Matrix BarCamp Widget - - - - -
- - - diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index e4449f1..20e5d67 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -3,7 +3,10 @@ "delete": "Löschen", "errors": { "mutationFailed": "Fehler: Die Daten konnten nicht gespeichert werden.", - "queryFailed": "Fehler: Die Daten konnten nicht geladen werden." + "networkError": "Network error occurred. Please check your connection.", + "queryFailed": "Fehler: Die Daten konnten nicht geladen werden.", + "serverError": "Server error occurred. Please try again later.", + "unknownError": "An unexpected error occurred." }, "iconPicker": { "icon": "Symbol \"{{icon}}\"", @@ -14,10 +17,10 @@ "assign": "Zuweisen", "assignmentProcedure": "<0>Wenn du einen Raum zuweist, wird das Widget:<1><0>Einen Link zum Themenraum auf dem Thema hinzufügen.<1>Den Raumtitel und das Raumthema ändern.<2>Das BarCamp Widget und das Videokonferenz Widget im Raum bereitstellen.", "cancel": "Abbrechen", - "createRoomMessage": "Du musst die Räume in Element erstellen bevor du sie einem Thema zuweisen kannst. Nutze einen temporären Titel (Beispiel: „Session 1“), deaktiviere die „Ende-zu-Ende-Verschlüsselung“ und wähle als Raumsichtbarkeit „Für Space-Mitglieder sichtbar“. Beachte dass du mindestens Moderationsrechte in der Space sowie in den Themenräumen benötigst um die Zuweisung abzuschließen. Tipp: Du kannst die Räume auch bereits vor der Themenplanung anlegen um den späteren Zuweisungsprozess zu beschleunigen.", - "description": "Wähle einen Matrix Raum in dem das Thema „{{topicTitle}}“ diskutiert werden soll. Jeder Raum kann jeweils nur für ein einziges Thema benutzt werden.", + "createRoomMessage": "You must create a room in Element before you can assign it to a topic. Use a temporary name as a title (example: \"Session 1\"), disable \"end-to-end encryption\", and set the join rule to \"Visible to space members\". Tip: You can also create the rooms before the planning session starts to speed-up the assignment process.", + "description": "Select a Matrix room where the topic \"{{topicTitle}}\" should be discussed. Each Matrix room can only host a single topic.", "encryptedRoom": { - "message": "<0>Möchtest du diesen Raum wirklich zuweisen?<1>Du versuchst einen Raum zuzuweisen der Verschlüsselung verwendet. In verschlüsselten Räumen können Teilnehmer die erst später beitreteten nicht die gesamte Nachrichtenhistorie lesen.<2>Alternativ kannst du einen neuen Raum erstellen und dabei die „Ende-zu-Ende-Verschlüsselung“ deaktiveren.", + "message": "<0>Do you really want to assign this room?<1>The room you are trying to assign uses encryption. In encrypted rooms, participants that later join are unable to see the message history.<2>As an alternative you can create a new room and explicitly disabled \"end-to-end encryption\" on creation.", "title": "Verschlüsselter Raum" }, "room": "Matrix Raum", @@ -92,7 +95,7 @@ "tracks": "Tracks" }, "sessionLayout": { - "returnToLobby": "Zurück zu „{{roomName}}“", + "returnToLobby": "Return to \"{{roomName}}\"", "timeslotTimes": "{{startTime, datetime}}–{{endTime, datetime}}" }, "submittedTopics": { @@ -157,7 +160,6 @@ }, "widgets": { "jitsi": { - "lobbyConferenceTitle": "Lobby", "title": "Video Konferenz" } } diff --git a/public/locales/de/translation_old.json b/public/locales/de/translation_old.json new file mode 100644 index 0000000..ffa446d --- /dev/null +++ b/public/locales/de/translation_old.json @@ -0,0 +1,17 @@ +{ + "linkRoomDialog": { + "createRoomMessage": "Du musst die Räume in Element erstellen bevor du sie einem Thema zuweisen kannst. Nutze einen temporären Titel (Beispiel: „Session 1“), deaktiviere die „Ende-zu-Ende-Verschlüsselung“ und wähle als Raumsichtbarkeit „Für Space-Mitglieder sichtbar“. Beachte dass du mindestens Moderationsrechte in der Space sowie in den Themenräumen benötigst um die Zuweisung abzuschließen. Tipp: Du kannst die Räume auch bereits vor der Themenplanung anlegen um den späteren Zuweisungsprozess zu beschleunigen.", + "description": "Wähle einen Matrix Raum in dem das Thema „{{topicTitle}}“ diskutiert werden soll. Jeder Raum kann jeweils nur für ein einziges Thema benutzt werden.", + "encryptedRoom": { + "message": "<0>Möchtest du diesen Raum wirklich zuweisen?<1>Du versuchst einen Raum zuzuweisen der Verschlüsselung verwendet. In verschlüsselten Räumen können Teilnehmer die erst später beitreteten nicht die gesamte Nachrichtenhistorie lesen.<2>Alternativ kannst du einen neuen Raum erstellen und dabei die „Ende-zu-Ende-Verschlüsselung“ deaktiveren." + } + }, + "sessionLayout": { + "returnToLobby": "Zurück zu „{{roomName}}“" + }, + "widgets": { + "jitsi": { + "lobbyConferenceTitle": "Lobby" + } + } +} diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index cbc5db8..549aee8 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -3,7 +3,10 @@ "delete": "Delete", "errors": { "mutationFailed": "Error: Data could not be updated.", - "queryFailed": "Error: Data could not be loaded." + "networkError": "Network error occurred. Please check your connection.", + "queryFailed": "Error: Data could not be loaded.", + "serverError": "Server error occurred. Please try again later.", + "unknownError": "An unexpected error occurred." }, "iconPicker": { "icon": "Icon \"{{icon}}\"", @@ -14,10 +17,10 @@ "assign": "Assign", "assignmentProcedure": "<0>When you assign a room, the widget will:<1><0>Add a link to the session room to the sticky note.<1>Update the title and topic to match the topic.<2>Setup the BarCamp and the Video Conference widget.", "cancel": "Cancel", - "createRoomMessage": "You must create a room in Element before you can assign it to a topic. Use a temporary name as a title (example: “Session 1”), disable ”end-to-end encryption”, and set the join rule to “Visible to space members”. Tip: You can also create the rooms before the planning session starts to speed-up the assignment process.", - "description": "Select a Matrix room where the topic “{{topicTitle}}” should be discussed. Each Matrix room can only host a single topic.", + "createRoomMessage": "You must create a room in Element before you can assign it to a topic. Use a temporary name as a title (example: \"Session 1\"), disable \"end-to-end encryption\", and set the join rule to \"Visible to space members\". Tip: You can also create the rooms before the planning session starts to speed-up the assignment process.", + "description": "Select a Matrix room where the topic \"{{topicTitle}}\" should be discussed. Each Matrix room can only host a single topic.", "encryptedRoom": { - "message": "<0>Do you really want to assign this room?<1>The room you are trying to assign uses encryption. In encrypted rooms, participants that later join are unable to see the message history.<2>As an alternative you can create a new room and explicitly disabled “end-to-end encryption” on creation.", + "message": "<0>Do you really want to assign this room?<1>The room you are trying to assign uses encryption. In encrypted rooms, participants that later join are unable to see the message history.<2>As an alternative you can create a new room and explicitly disabled \"end-to-end encryption\" on creation.", "title": "Encrypted room" }, "room": "Matrix Room", @@ -92,7 +95,7 @@ "tracks": "Tracks" }, "sessionLayout": { - "returnToLobby": "Return to “{{roomName}}”", + "returnToLobby": "Return to \"{{roomName}}\"", "timeslotTimes": "{{startTime, datetime}}–{{endTime, datetime}}" }, "submittedTopics": { @@ -157,7 +160,6 @@ }, "widgets": { "jitsi": { - "lobbyConferenceTitle": "Lobby", "title": "Video Conference" } } diff --git a/public/locales/en/translation_old.json b/public/locales/en/translation_old.json new file mode 100644 index 0000000..e29596a --- /dev/null +++ b/public/locales/en/translation_old.json @@ -0,0 +1,17 @@ +{ + "linkRoomDialog": { + "createRoomMessage": "You must create a room in Element before you can assign it to a topic. Use a temporary name as a title (example: “Session 1”), disable ”end-to-end encryption”, and set the join rule to “Visible to space members”. Tip: You can also create the rooms before the planning session starts to speed-up the assignment process.", + "description": "Select a Matrix room where the topic “{{topicTitle}}” should be discussed. Each Matrix room can only host a single topic.", + "encryptedRoom": { + "message": "<0>Do you really want to assign this room?<1>The room you are trying to assign uses encryption. In encrypted rooms, participants that later join are unable to see the message history.<2>As an alternative you can create a new room and explicitly disabled “end-to-end encryption” on creation." + } + }, + "sessionLayout": { + "returnToLobby": "Return to “{{roomName}}”" + }, + "widgets": { + "jitsi": { + "lobbyConferenceTitle": "Lobby" + } + } +} diff --git a/src/App.tsx b/src/App.tsx index e0be428..370786d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,9 +16,9 @@ import { WidgetApi } from '@matrix-widget-toolkit/api'; import { - SemanticUiThemeProvider, - SemanticUiWidgetApiProvider, -} from '@matrix-widget-toolkit/semantic-ui'; + MuiThemeProvider, + MuiWidgetApiProvider, +} from '@matrix-widget-toolkit/mui'; import { Suspense } from 'react'; import { Layout } from './components/Layout'; import { @@ -38,18 +38,18 @@ export function AppWrapper({ return ( // Fallback suspense if no higher one is registered (used for i18n) }> - + - - + - + ); } diff --git a/src/components/ButtonWithIcon.tsx b/src/components/ButtonWithIcon.tsx index 314ee1c..8be870f 100644 --- a/src/components/ButtonWithIcon.tsx +++ b/src/components/ButtonWithIcon.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ +import { Button, styled } from '@mui/material'; import React from 'react'; -import { Button } from 'semantic-ui-react'; -import { styled } from './StyledComponentsThemeProvider'; export function withRefFix

>( Component: React.ComponentType

@@ -29,12 +28,10 @@ export function withRefFix

>( }; } -export const ButtonWithIcon = withRefFix( - styled(Button)({ - '&&&&&': { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }, - }) -); +const StyledButton = styled(Button)({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +export const ButtonWithIcon = withRefFix(StyledButton); diff --git a/src/components/ConfirmDialog/ConfirmDialog.test.tsx b/src/components/ConfirmDialog/ConfirmDialog.test.tsx index ba85a90..048f144 100644 --- a/src/components/ConfirmDialog/ConfirmDialog.test.tsx +++ b/src/components/ConfirmDialog/ConfirmDialog.test.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { render, screen, waitFor, within } from '@testing-library/react'; +import { render, waitFor, within } from '@testing-library/react'; +import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import { ConfirmDialog } from './ConfirmDialog'; diff --git a/src/components/ConfirmDialog/ConfirmDialog.tsx b/src/components/ConfirmDialog/ConfirmDialog.tsx index b285b4a..787302c 100644 --- a/src/components/ConfirmDialog/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog/ConfirmDialog.tsx @@ -14,16 +14,24 @@ * limitations under the License. */ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material'; import { PropsWithChildren, + ReactElement, ReactNode, + cloneElement, useCallback, useRef, useState, } from 'react'; import { AutoFocusInside, FocusOn, InFocusGuard } from 'react-focus-on'; import { useTranslation } from 'react-i18next'; -import { Button, Modal } from 'semantic-ui-react'; import { useId } from '../utils'; export type ConfirmDialogProps = PropsWithChildren<{ @@ -65,43 +73,48 @@ export function ConfirmDialog({ const headerId = useId(); const contentId = useId(); + const trigger = children as ReactElement; + const triggerWithHandler = cloneElement(trigger, { + onClick: handleOpen, + }); + return ( - - -

{title}
- - - - {message} - - - -
- - - - - - -
-
- + <> + {triggerWithHandler} + + +
{title}
+
+ + + {message} + + + +
+ + + + + + +
+
+
+ ); } diff --git a/src/components/DragAndDropProvider/DragAndDropProvider.tsx b/src/components/DragAndDropProvider/DragAndDropProvider.tsx index bdec024..87a7036 100644 --- a/src/components/DragAndDropProvider/DragAndDropProvider.tsx +++ b/src/components/DragAndDropProvider/DragAndDropProvider.tsx @@ -106,6 +106,7 @@ export function DragAndDropProvider({ }); return ( + // @ts-ignore Incorrect types in @types/react-beautiful-dnd { callbacksRef.current.onBeforeDragStartCallbacks.forEach((c) => { diff --git a/src/components/IconPicker/IconPicker.test.tsx b/src/components/IconPicker/IconPicker.test.tsx index 19e7de7..c8e6d71 100644 --- a/src/components/IconPicker/IconPicker.test.tsx +++ b/src/components/IconPicker/IconPicker.test.tsx @@ -16,17 +16,27 @@ import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { + faDog, + faStar, + faFrog, + faCoffee, + faCompass, + faFire, + faLemon, + faCheese, +} from '@fortawesome/free-solid-svg-icons'; import { IconPicker } from './IconPicker'; describe('', () => { it('should render read only icon', () => { - render( {}} />); + render( {}} />); expect(screen.getByRole('img', { name: 'Icon "dog"' })).toBeInTheDocument(); }); it('should render picker button', () => { - render( {}} />); + render( {}} />); expect( screen.getByRole('combobox', { name: 'Icon "dog"', expanded: false }) @@ -34,7 +44,7 @@ describe('', () => { }); it('should show selected icon in popup', async () => { - render( {}} />); + render( {}} />); await userEvent.click( screen.getByRole('combobox', { name: 'Icon "dog"', expanded: false }) @@ -42,7 +52,7 @@ describe('', () => { const list = screen.getByRole('listbox', { name: /available icons/i }); - expect(within(list).getAllByRole('option')).toHaveLength(25); + expect(within(list).getAllByRole('option')).toHaveLength(30); expect( within(list).getByRole('option', { name: 'Icon "dog"', selected: true }) ).toBeInTheDocument(); @@ -54,7 +64,7 @@ describe('', () => { it('should select icon by clicking', async () => { const onChange = jest.fn(); - render(); + render(); await userEvent.click( screen.getByRole('combobox', { name: 'Icon "dog"', expanded: false }) @@ -70,12 +80,12 @@ describe('', () => { expect( screen.getByRole('combobox', { expanded: false }) ).toBeInTheDocument(); - expect(onChange).toBeCalledWith('star'); + expect(onChange).toBeCalledWith(faStar); }); it('should select icon using keyboard interactions', async () => { const onChange = jest.fn(); - render(); + render(); screen .getByRole('combobox', { name: 'Icon "dog"', expanded: false }) @@ -109,12 +119,12 @@ describe('', () => { expect( within(list).getByRole('option', { - name: 'Icon "compass"', + name: 'Icon "face-surprise"', selected: true, }) ).toBeInTheDocument(); expect( - screen.getByRole('combobox', { name: 'Icon "compass"', expanded: true }) + screen.getByRole('combobox', { name: 'Icon "face-surprise"', expanded: true }) ).toBeInTheDocument(); // Go to end @@ -122,12 +132,12 @@ describe('', () => { expect( within(list).getByRole('option', { - name: 'Icon "fire"', + name: 'Icon "face-surprise"', selected: true, }) ).toBeInTheDocument(); expect( - screen.getByRole('combobox', { name: 'Icon "fire"', expanded: true }) + screen.getByRole('combobox', { name: 'Icon "face-surprise"', expanded: true }) ).toBeInTheDocument(); // Wrap over last @@ -148,11 +158,11 @@ describe('', () => { screen.getByRole('combobox', { expanded: false }) ).toBeInTheDocument(); - expect(onChange).toBeCalledWith('lemon'); + expect(onChange).toBeCalledWith(faLemon); }); it('should set focus inside picker on open and restore after closing', async () => { - render( {}} />); + render( {}} />); await userEvent.click( screen.getByRole('combobox', { name: 'Icon "dog"', expanded: false }) @@ -177,7 +187,7 @@ describe('', () => { it('should trap focus inside picker', async () => { render( <> - {}} /> + {}} /> ); @@ -210,7 +220,7 @@ describe('', () => { it('should cancel pick on click outside of the popup', async () => { const onChange = jest.fn(); const { container } = render( - {}} /> + {}} /> ); await userEvent.click( @@ -231,7 +241,7 @@ describe('', () => { it('should cancel if escape key is pressed', async () => { const onChange = jest.fn(); - render( {}} />); + render( {}} />); await userEvent.click( screen.getByRole('combobox', { name: 'Icon "dog"', expanded: false }) diff --git a/src/components/IconPicker/IconPicker.tsx b/src/components/IconPicker/IconPicker.tsx index 6f80ac1..057c80a 100644 --- a/src/components/IconPicker/IconPicker.tsx +++ b/src/components/IconPicker/IconPicker.tsx @@ -14,13 +14,21 @@ * limitations under the License. */ -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import FocusLock from 'react-focus-lock'; import { useTranslation } from 'react-i18next'; -import { Button, Icon, IconProps, List, Popup, Ref } from 'semantic-ui-react'; -import { styled } from '../StyledComponentsThemeProvider'; +import { + Popover, + List, + ListItem, + ListItemButton, + IconButton, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; import { useId } from '../utils'; import { iconSet } from './icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; const GridContainer = styled(List)({ display: 'grid', @@ -28,17 +36,7 @@ const GridContainer = styled(List)({ gap: 8, }); -function ForceFocus({ focus, ...props }: { focus: boolean }) { - const ref = useRef(null); - - useEffect(() => { - if (focus) { - ref.current?.focus(); - } - }, [ref, focus]); - - return
; -} +// Removed ForceFocus as it's not needed with MUI ListItemButton function IconGrid({ id, @@ -48,69 +46,67 @@ function IconGrid({ onSubmit, }: { id: string; - selected?: string; - icons: string[]; - onChange: (icon: string) => void; - onSubmit: (icon: string) => void; + selected?: IconDefinition; + icons: IconDefinition[]; + onChange: (icon: IconDefinition) => void; + onSubmit: (icon: IconDefinition) => void; }) { const { t } = useTranslation(); return ( {icons.map((icon, i) => ( - onChange(icon)} - onClick={() => onSubmit(icon)} - onKeyDown={(e: KeyboardEvent) => { - const isSubmit = e.code === 'Space' || e.code === 'Enter'; - const isPrevious = e.code === 'ArrowLeft' || e.code === 'ArrowUp'; - const isNext = e.code === 'ArrowRight' || e.code === 'ArrowDown'; - const isFirst = e.code === 'Home'; - const isLast = e.code === 'End'; + + onChange(icon)} + onClick={() => onSubmit(icon)} + onKeyDown={(e: React.KeyboardEvent) => { + const isSubmit = e.code === 'Space' || e.code === 'Enter'; + const isPrevious = e.code === 'ArrowLeft' || e.code === 'ArrowUp'; + const isNext = e.code === 'ArrowRight' || e.code === 'ArrowDown'; + const isFirst = e.code === 'Home'; + const isLast = e.code === 'End'; - if (isSubmit) { - onSubmit(icon); - // If we don't prevent, enter reopens the popup immediately - e.preventDefault(); - } else if (isPrevious) { - onChange(icons[i === 0 ? icons.length - 1 : i - 1]); - } else if (isNext) { - onChange(icons[(i + 1) % icons.length]); - } else if (isFirst) { - onChange(icons[0]); - } else if (isLast) { - onChange(icons[icons.length - 1]); - } - }} - > - - + if (isSubmit) { + onSubmit(icon); + // If we don't prevent, enter reopens the popup immediately + e.preventDefault(); + } else if (isPrevious) { + onChange(icons[i === 0 ? icons.length - 1 : i - 1]); + } else if (isNext) { + onChange(icons[(i + 1) % icons.length]); + } else if (isFirst) { + onChange(icons[0]); + } else if (isLast) { + onChange(icons[icons.length - 1]); + } + }} + > + + + ))} ); } -type IconSizeProp = IconProps['size']; +type IconSizeProp = 'small' | 'medium' | 'large'; export type IconPickerProps = { size?: IconSizeProp; - icon: string; + icon: IconDefinition; readOnly?: boolean; - onChange: (icon: string) => void; + onChange: (icon: IconDefinition) => void; }; export function IconPicker({ @@ -130,10 +126,10 @@ export function IconPicker({ - + ); } @@ -141,37 +137,38 @@ export function IconPicker({ const displayedIcon = open ? selectedIcon : icon; return ( - { - setOpen(false); - }} - onOpen={() => { - setSelectedIcon(icon); - setOpen(true); - }} - trigger={ -
- - - -
- } - on="click" - position="bottom left" - > + <> +
+ { + if (open) { + setOpen(false); + } else { + setSelectedIcon(icon); + setOpen(true); + } + }} + > + + +
+ setOpen(false)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + > { @@ -193,6 +190,7 @@ export function IconPicker({ }} /> -
+ + ); } diff --git a/src/components/IconPicker/icons.ts b/src/components/IconPicker/icons.ts index 214ac0d..7a6cf72 100644 --- a/src/components/IconPicker/icons.ts +++ b/src/components/IconPicker/icons.ts @@ -15,36 +15,128 @@ */ import { sample } from 'lodash'; +import { + faCoffee, + faLemon, + faCarrot, + faSeedling, + faLeaf, + faHippo, + faFish, + faCrow, + faFrog, + faDog, + faCat, + faHorse, + faSun, + faMoon, + faStar, + faUsers, + faBrain, + faNewspaper, + faCheese, + faChess, + faCookie, + faCouch, + faCar, + faCompass, + faFire, + faPizzaSlice, + faBeerMugEmpty, + faComment, + faServer, + faFaceSurprise, +} from '@fortawesome/free-solid-svg-icons'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; -export const iconSet = [ - 'coffee', - 'lemon', - 'carrot', - 'seedling', - 'leaf', - 'hippo', - 'fish', - 'crow', - 'frog', - 'dog', - 'cat', - 'horse', - 'sun', - 'moon', - 'star', - 'users', - 'brain', - 'newspaper', - 'cheese', - 'chess', - 'cookie', - 'couch', - 'car', - 'compass', - 'fire', +export const iconSet: IconDefinition[] = [ + faCoffee, + faLemon, + faCarrot, + faSeedling, + faLeaf, + faHippo, + faFish, + faCrow, + faFrog, + faDog, + faCat, + faHorse, + faSun, + faMoon, + faStar, + faUsers, + faBrain, + faNewspaper, + faCheese, + faChess, + faCookie, + faCouch, + faCar, + faCompass, + faFire, + faPizzaSlice, + faBeerMugEmpty, + faComment, + faServer, + faFaceSurprise, ]; -export function randomIcon(): string { +// Map of icon names to IconDefinition objects +const iconMap: Record = { + coffee: faCoffee, + lemon: faLemon, + carrot: faCarrot, + seedling: faSeedling, + leaf: faLeaf, + hippo: faHippo, + fish: faFish, + crow: faCrow, + frog: faFrog, + dog: faDog, + cat: faCat, + horse: faHorse, + sun: faSun, + moon: faMoon, + star: faStar, + users: faUsers, + brain: faBrain, + newspaper: faNewspaper, + cheese: faCheese, + chess: faChess, + cookie: faCookie, + couch: faCouch, + car: faCar, + compass: faCompass, + fire: faFire, + 'pizza-slice': faPizzaSlice, + 'beer-mug-empty': faBeerMugEmpty, + comment: faComment, + server: faServer, + 'face-surprise': faFaceSurprise, +}; + +/** + * Convert an icon name (string) to an IconDefinition object + */ +export function getIconByName(iconName: string): IconDefinition { + const icon = iconMap[iconName]; + if (!icon) { + // Return a default icon if the name is not found + console.warn(`Icon "${iconName}" not found, using coffee as fallback`); + return faCoffee; + } + return icon; +} + +/** + * Convert an IconDefinition object to an icon name (string) + */ +export function getIconName(icon: IconDefinition): string { + return icon.iconName; +} + +export function randomIcon(): IconDefinition { const icon = sample(iconSet); if (!icon) { diff --git a/src/components/IconPicker/index.ts b/src/components/IconPicker/index.ts index 84e5060..fa21463 100644 --- a/src/components/IconPicker/index.ts +++ b/src/components/IconPicker/index.ts @@ -15,4 +15,5 @@ */ export { IconPicker } from './IconPicker'; -export { randomIcon } from './icons'; +export { randomIcon, getIconByName, getIconName } from './icons'; + diff --git a/src/components/InlineDateTimeEdit/InlineDateTimeEdit.test.tsx b/src/components/InlineDateTimeEdit/InlineDateTimeEdit.test.tsx index 9ce70bc..270a020 100644 --- a/src/components/InlineDateTimeEdit/InlineDateTimeEdit.test.tsx +++ b/src/components/InlineDateTimeEdit/InlineDateTimeEdit.test.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { fireEvent, render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import { InlineDateTimeEdit } from './InlineDateTimeEdit'; diff --git a/src/components/InlineDateTimeEdit/InlineDateTimeEdit.tsx b/src/components/InlineDateTimeEdit/InlineDateTimeEdit.tsx index 286261f..6198036 100644 --- a/src/components/InlineDateTimeEdit/InlineDateTimeEdit.tsx +++ b/src/components/InlineDateTimeEdit/InlineDateTimeEdit.tsx @@ -17,82 +17,9 @@ import { DateTime } from 'luxon'; import { ChangeEvent, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Input } from 'semantic-ui-react'; -import { styled } from '../StyledComponentsThemeProvider'; +import { TextField, Box } from '@mui/material'; import { Tooltip } from '../Tooltip'; -const Container = styled.div({ - display: 'flex', - alignItems: 'baseline', - flexDirection: 'column', - marginLeft: -4, -}); - -const DateTimeInput = styled(Input)(({ theme }) => ({ - marginBottom: 8, - - '&&&&&': { - display: 'inline-grid', - - fontSize: 'inherit', - fontWeight: 'inherit', - fontStyle: 'inherit', - lineHeight: 'inherit', - color: 'inherit', - background: 'transparent', - }, - - '&&&&& > input, &&&&&::after, &&&&&::before': { - fontSize: 'inherit', - fontWeight: 'inherit', - fontFamily: 'inherit', - lineHeight: 'inherit', - color: 'inherit', - background: 'transparent', - padding: 4, - - gridArea: '1 / 1', - }, - - '&&&&& > input': { - colorScheme: theme.type === 'dark' ? 'dark' : undefined, - }, - - // workaround for firefox that doesn't shrink the input - // field based on the "max" attribute by default - '@-moz-document url-prefix()': { - '&&&&& > input': { - width: '12em', - }, - '&&&&&::after, &&&&&::before': { - width: 'calc(12em - 1.5em)', - }, - }, - - '&&&&&::before, &&&&&::after': { - pointerEvents: 'none', - whiteSpace: 'pre-wrap', - marginRight: '1.5em', - color: 'inherit', - - border: '1px solid transparent', - }, - - // This adds a hidden text node with the same styling and content to - // automatically sizes the text input to its contents. - '&&&&&::before': { - content: "attr(data-value) '\u00a0' attr(data-suffix)", - visibility: 'hidden', - }, - - // This adds a text node with the label that is displayed in the - // input field. - '&&&&&::after': { - content: 'attr(data-suffix)', - textAlign: 'right', - }, -})); - type InlineDateTimeEditProps = { value: string; label: string; @@ -155,14 +82,14 @@ export function InlineDateTimeEdit({ } return ( - +
{ e.preventDefault(); onSubmit(); }} > - {/* Required to make submit on enter to work in every env */} -
+ ); } diff --git a/src/components/InlineDurationEdit/InlineDurationEdit.test.tsx b/src/components/InlineDurationEdit/InlineDurationEdit.test.tsx index e357ef1..04b4c04 100644 --- a/src/components/InlineDurationEdit/InlineDurationEdit.test.tsx +++ b/src/components/InlineDurationEdit/InlineDurationEdit.test.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import { InlineDurationEdit } from './InlineDurationEdit'; diff --git a/src/components/InlineDurationEdit/InlineDurationEdit.tsx b/src/components/InlineDurationEdit/InlineDurationEdit.tsx index 2fe4097..a98b52b 100644 --- a/src/components/InlineDurationEdit/InlineDurationEdit.tsx +++ b/src/components/InlineDurationEdit/InlineDurationEdit.tsx @@ -17,73 +17,7 @@ import { clamp } from 'lodash'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Input } from 'semantic-ui-react'; -import { styled } from '../StyledComponentsThemeProvider'; - -const Container = styled.div({ - display: 'flex', - alignItems: 'baseline', - flexDirection: 'column', - marginLeft: -4, -}); - -const DurationInput = styled(Input)({ - '&&&&&': { - display: 'inline-grid', - - fontSize: 'inherit', - fontWeight: 'inherit', - fontStyle: 'inherit', - lineHeight: 'inherit', - color: 'inherit', - background: 'transparent', - }, - - '&&&&& > input, &&&&&::after, &&&&&::before': { - fontSize: 'inherit', - fontWeight: 'inherit', - fontFamily: 'inherit', - lineHeight: 'inherit', - color: 'inherit', - background: 'transparent', - padding: 4, - - gridArea: '1 / 1', - }, - - // workaround for firefox that doesn't shrink the input - // field based on the "max" attribute by default - '@-moz-document url-prefix()': { - '&&&&& > input': { - width: '6.5em', - }, - '&&&&&::after, &&&&&::before': { - width: 'calc(6.5em - 1.5em)', - }, - }, - - '&&&&&::before, &&&&&::after': { - pointerEvents: 'none', - whiteSpace: 'pre-wrap', - marginRight: '1.5em', - - border: '1px solid transparent', - }, - - // This adds a hidden text node with the same styling and content to - // automatically sizes the text input to its contents. - '&&&&&::before': { - content: "attr(data-value) '\u00a0' attr(data-suffix)", - visibility: 'hidden', - }, - - // This adds a text node with the label that is displayed in the - // input field. - '&&&&&::after': { - content: 'attr(data-suffix)', - textAlign: 'right', - }, -}); +import { TextField, Box, InputAdornment } from '@mui/material'; type InlineDurationEditProps = { minutes: number; @@ -123,7 +57,7 @@ export function InlineDurationEdit({ } return ( - +
{ ev.preventDefault(); @@ -131,28 +65,33 @@ export function InlineDurationEdit({ }} noValidate > - - setValue(event.target.valueAsNumber)} - /> - - + setValue(parseFloat(event.target.value) || 0)} + size="small" + variant="outlined" + InputProps={{ + endAdornment: ( + + {t('sessionGrid.timeSlot.durationInputSuffix', 'min.')} + + ), + }} + /> {/* Required to make submit on enter to work in every env */} -
+ ); } diff --git a/src/components/InlineTextEdit/InlineTextEdit.test.tsx b/src/components/InlineTextEdit/InlineTextEdit.test.tsx index db428b4..c9f9cee 100644 --- a/src/components/InlineTextEdit/InlineTextEdit.test.tsx +++ b/src/components/InlineTextEdit/InlineTextEdit.test.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import { InlineTextEdit } from './InlineTextEdit'; diff --git a/src/components/InlineTextEdit/InlineTextEdit.tsx b/src/components/InlineTextEdit/InlineTextEdit.tsx index f500ccf..b92df53 100644 --- a/src/components/InlineTextEdit/InlineTextEdit.tsx +++ b/src/components/InlineTextEdit/InlineTextEdit.tsx @@ -14,50 +14,43 @@ * limitations under the License. */ +import { TextField } from '@mui/material'; import { useEffect, useState } from 'react'; -import { Input } from 'semantic-ui-react'; import { styled } from '../StyledComponentsThemeProvider'; -const AutoSizeInput = styled(Input)({ - '&&&&&': { - display: 'inline-grid', - +const AutoSizeInput = styled(TextField)({ + '& .MuiInputBase-root': { fontSize: 'inherit', fontWeight: 'inherit', fontStyle: 'inherit', lineHeight: 'inherit', color: 'inherit', background: 'transparent', + border: 'none', + padding: '4px', + borderRadius: '8px', + '&:before, &:after': { + display: 'none', + }, + '&.Mui-focused': { + backgroundColor: 'rgba(0, 0, 0, 0.04)', + }, }, - - '&&&&& > input, &&&&&::after': { + '& .MuiInputBase-input': { + padding: 0, fontSize: 'inherit', fontWeight: 'inherit', fontFamily: 'inherit', lineHeight: 'inherit', color: 'inherit', - background: 'transparent', - padding: 4, - - gridArea: '1 / 1', - }, - - // This adds a hidden text node with the same styling and content to - // automatically sizes the text input to its contents. - '&&&&&::after': { - content: "attr(data-value) ' '", - visibility: 'hidden', - whiteSpace: 'pre-wrap', - - border: '1px solid transparent', }, -}); +}) as React.ComponentType>; const ReadOnly = styled.span({ padding: '4px', border: 'solid 1px transparent', borderRadius: 8, -}); +}) as React.ComponentType>; export function InlineTextEdit({ label, @@ -85,7 +78,7 @@ export function InlineTextEdit({ } if (readOnly) { - return {value}; + return {value}; } return ( @@ -95,15 +88,18 @@ export function InlineTextEdit({ e.preventDefault(); }} > - - setInput(e.target.value)} - onBlur={handleSubmit} - /> - + setInput(e.target.value)} + onBlur={handleSubmit} + InputProps={{ + disableUnderline: true, + }} + /> {/* Required to make submit on enter to work in every env */} diff --git a/src/components/Layout/LoaderLayout.tsx b/src/components/Layout/LoaderLayout.tsx index 06c9458..1eea728 100644 --- a/src/components/Layout/LoaderLayout.tsx +++ b/src/components/Layout/LoaderLayout.tsx @@ -14,20 +14,19 @@ * limitations under the License. */ -import { Loader } from 'semantic-ui-react'; -import { styled } from '../StyledComponentsThemeProvider'; - -export const Container = styled.div({ - height: '100vh', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}); +import { CircularProgress, Box } from '@mui/material'; export function LoaderLayout() { return ( - - - + + + ); } diff --git a/src/components/Layout/LobbyLayout.tsx b/src/components/Layout/LobbyLayout.tsx index 2033c10..049c600 100644 --- a/src/components/Layout/LobbyLayout.tsx +++ b/src/components/Layout/LobbyLayout.tsx @@ -33,16 +33,16 @@ const Container = styled.div({ // Make sure that shadows are not cut off paddingRight: 1, paddingBottom: 2, -}); +}) as React.ComponentType>; const LeftContainer = styled.div({ flex: 1, minWidth: 0, -}); +}) as React.ComponentType>; const RightContainer = styled.div({ width: 250, -}); +}) as React.ComponentType>; export function LobbyLayout({ timeSlots, @@ -71,15 +71,15 @@ export function LobbyLayout({ moveTopicToSession({ topicId, timeSlotId, trackId }); }} > - - + + - + diff --git a/src/components/Layout/NoSpaceMessage.tsx b/src/components/Layout/NoSpaceMessage.tsx index a9339ce..22b2c57 100644 --- a/src/components/Layout/NoSpaceMessage.tsx +++ b/src/components/Layout/NoSpaceMessage.tsx @@ -15,25 +15,28 @@ */ import { useTranslation } from 'react-i18next'; -import { Container, Message } from 'semantic-ui-react'; +import { Container, Alert, AlertTitle, Typography } from '@mui/material'; +import { Warning as WarningIcon } from '@mui/icons-material'; export function NoSpaceMessage() { const { t } = useTranslation(); return ( - - + + }> + + {t( + 'notPartOfASpace.title', + 'Your Matrix room is not part of a Matrix space.' + )} + + + {t( + 'notPartOfASpace.instructions', + 'The widget only works in rooms that belong to a space. Please create a new space, create a new room in that space, and try again.' + )} + + ); } diff --git a/src/components/Layout/SessionLayout.tsx b/src/components/Layout/SessionLayout.tsx index 362a0b5..c7093cd 100644 --- a/src/components/Layout/SessionLayout.tsx +++ b/src/components/Layout/SessionLayout.tsx @@ -16,7 +16,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Icon, Loader, Segment } from 'semantic-ui-react'; +import { + Button, + Box, + CircularProgress, + Card, + CardContent, + Typography, + Stack, +} from '@mui/material'; import { Session, TimeSlot, Track } from '../../lib/events'; import { useGetRoomNameQuery, @@ -25,36 +33,8 @@ import { useSpaceMembers, } from '../../store'; import { StickyNote } from '../StickyNote'; -import { styled } from '../StyledComponentsThemeProvider'; - -const Container = styled.div({ - display: 'flex', - flexDirection: 'column', - height: '100vh', - padding: 8, -}); - -const Center = styled.div(({ theme }) => ({ - display: 'flex', - justifyContent: 'center', -})); - -const Header = styled.div(({ theme }) => ({ - display: 'flex', - marginBottom: '1rem', - alignItems: 'center', - gap: 8, -})); - -const OverflowEllipsis = styled.div({ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', -}); - -const Secondary = styled.span(({ theme }) => ({ - color: theme.secondaryText, -})); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { getIconByName } from '../IconPicker'; export function SessionLayout({ sessionGridId, @@ -77,67 +57,81 @@ export function SessionLayout({ const { data: roomNameData } = useGetRoomNameQuery({ roomId: sessionGridId }); return ( - -
+ + -
- - - {loading ? ( -
- -
- ) : ( - <> -
- + - - {track.name} -
- - {t( - 'sessionLayout.timeslotTimes', - '{{startTime, datetime}}–{{endTime, datetime}}', - { - startTime: new Date(timeSlot.startTime), - endTime: new Date(timeSlot.endTime), - formatParams: { - startTime: { - hour: 'numeric', - minute: 'numeric', - month: 'numeric', - year: 'numeric', - day: 'numeric', + + + {loading ? ( + + + + ) : ( + <> + + + + + {track!.name} + + + {t( + 'sessionLayout.timeslotTimes', + '{{startTime, datetime}}–{{endTime, datetime}}', + { + startTime: new Date(timeSlot!.startTime), + endTime: new Date(timeSlot!.endTime), + formatParams: { + startTime: { + hour: 'numeric', + minute: 'numeric', + month: 'numeric', + year: 'numeric', + day: 'numeric', + }, + endTime: { hour: 'numeric', minute: 'numeric' }, }, - endTime: { hour: 'numeric', minute: 'numeric' }, - }, - } - )} - -
-
+ } + )} + + + - - - )} -
-
+ + + )} + + + ); } diff --git a/src/components/Layout/WelcomeLayout.tsx b/src/components/Layout/WelcomeLayout.tsx index f5bbbf2..560fc30 100644 --- a/src/components/Layout/WelcomeLayout.tsx +++ b/src/components/Layout/WelcomeLayout.tsx @@ -17,16 +17,16 @@ import { Trans, useTranslation } from 'react-i18next'; import { Button, + Card, + CardContent, Container, Grid, - Header, - Icon, - Segment, -} from 'semantic-ui-react'; + Typography, +} from '@mui/material'; +import { AccessTime as ClockIcon } from '@mui/icons-material'; import { ROOM_EVENT_BARCAMP_TOPIC_SUBMISSION } from '../../lib/events'; import { useHasRoomEncryptionQuery, - useMarkRoomAsSuggestedMutation, usePatchPowerLevelsMutation, usePatchRoomHistoryVisibilityMutation, usePowerLevels, @@ -38,7 +38,6 @@ import { ConfirmDialog } from '../ConfirmDialog'; function ModeratorWelcomeLayout() { const { t } = useTranslation(); const [setupSessionGrid] = useSetupSessionGridMutation(); - const [markRoomAsSuggested] = useMarkRoomAsSuggestedMutation(); const [patchPowerLevels] = usePatchPowerLevelsMutation(); const [patchRoomHistoryVisibility] = usePatchRoomHistoryVisibilityMutation(); const [setupLobbyRoomWidgets] = useSetupLobbyRoomWidgetsMutation(); @@ -49,8 +48,6 @@ function ModeratorWelcomeLayout() { const spaceId = event.room_id; const roomId = event.state_key; - await markRoomAsSuggested({ spaceId, roomId }).unwrap(); - await patchPowerLevels({ roomId, changes: { @@ -101,12 +98,23 @@ function ModeratorWelcomeLayout() { onConfirm={handleSetup} confirmTitle={setupTitle} > - ) : ( - )} @@ -132,31 +140,33 @@ export function WelcomeLayout() { const { t } = useTranslation(); return ( - - - - - - - -
- {t('welcome.title', 'Matrix BarCamp Widget')} -
-

- {t( - 'welcome.introduction', - "A BarCamp is an open, participatory workshop-event with no fixed agenda. The participants suggest discussion topics, place them together on the agenda, and discuss them in small sessions. The event's agenda is planned with time slots, which can either be common events in that all participants can join, or session slots where topics can be discussed in parallel. Participants can choose freely on which topics they want to participate." + + + + + + + + + + {t('welcome.title', 'Matrix BarCamp Widget')} + + + {t( + 'welcome.introduction', + "A BarCamp is an open, participatory workshop-event with no fixed agenda. The participants suggest discussion topics, place them together on the agenda, and discuss them in small sessions. The event's agenda is planned with time slots, which can either be common events in that all participants can join, or session slots where topics can be discussed in parallel. Participants can choose freely on which topics they want to participate." + )} + + + {canModerate ? ( + + ) : ( + )} -

- - {canModerate ? ( - - ) : ( - - )} -
-
-
+ + + +
); } diff --git a/src/components/LinkRoomDialog/LinkRoomDialog.tsx b/src/components/LinkRoomDialog/LinkRoomDialog.tsx index 2a29e0c..ac91cc7 100644 --- a/src/components/LinkRoomDialog/LinkRoomDialog.tsx +++ b/src/components/LinkRoomDialog/LinkRoomDialog.tsx @@ -17,7 +17,19 @@ import { PropsWithChildren, useCallback, useRef, useState } from 'react'; import { AutoFocusInside, FocusOn, InFocusGuard } from 'react-focus-on'; import { Trans, useTranslation } from 'react-i18next'; -import { Button, Form, Message, Modal } from 'semantic-ui-react'; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Alert, + Typography, + FormControl, + FormLabel, + Box, + CircularProgress +} from '@mui/material'; import { useAssignLinkedRoomMutation, useGetSessionGridQuery, @@ -135,110 +147,129 @@ export function LinkRoomDialog({ children, topicId }: LinkRoomDialogProps) { const assignTitle = t('linkRoomDialog.assign', 'Assign'); return ( - - -
- {t('linkRoomDialog.title', 'Assign a Matrix room to a topic')} -
-
- -
- -
-

- {t( - 'linkRoomDialog.description', - 'Select a Matrix room where the topic “{{topicTitle}}” should be discussed. Each Matrix room can only host a single topic.', - { topicTitle: topic?.content.title } - )} -

- - - - - - -

When you assign a room, the widget will:

-
    -
  1. Add a link to the session room to the sticky note.
  2. -
  3. Update the title and topic to match the topic.
  4. -
  5. Setup the BarCamp and the Video Conference widget.
  6. -
-
- - {t( - 'linkRoomDialog.createRoomMessage', - 'You must create a room in Element before you can assign it to a topic. Use a temporary name as a title (example: “Session 1”), disable ”end-to-end encryption”, and set the join rule to “Visible to space members”. Tip: You can also create the rooms before the planning session starts to speed-up the assignment process.' - )} - -
-
-
-
- -
- - - - {hasRoomEncryption ? ( - -

Do you really want to assign this room?

- -

- The room you are trying to assign uses encryption. In - encrypted rooms, participants that later join are unable - to see the message history. -

- -

- As an alternative you can create a new room and explicitly - disabled “end-to-end encryption” on creation. -

- - } - confirmTitle={assignTitle} - onConfirm={handleConfirm} - > - + + {hasRoomEncryption ? ( + + + Do you really want to assign this room? + + + + The room you are trying to assign uses encryption. In + encrypted rooms, participants that later join are unable + to see the message history. + + + + As an alternative you can create a new room and explicitly + disabled "end-to-end encryption" on creation. + + + } + confirmTitle={assignTitle} + onConfirm={handleConfirm} + > + + + ) : ( + -
- ) : ( - - )} -
-
-
-
+ )} + +
+ + + ); } diff --git a/src/components/LinkRoomDialog/SelectUnassignedRoomDropdown.tsx b/src/components/LinkRoomDialog/SelectUnassignedRoomDropdown.tsx index 74f735b..ea2a131 100644 --- a/src/components/LinkRoomDialog/SelectUnassignedRoomDropdown.tsx +++ b/src/components/LinkRoomDialog/SelectUnassignedRoomDropdown.tsx @@ -17,7 +17,7 @@ import { first } from 'lodash'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Dropdown, DropdownItemProps } from 'semantic-ui-react'; +import { Autocomplete, TextField } from '@mui/material'; import { useGetUnassignedRoomsQuery } from '../../store'; export function SelectUnassignedRoomDropdown({ @@ -44,23 +44,29 @@ export function SelectUnassignedRoomDropdown({ } }, [onChange, roomId, unassignedRooms]); - const options = unassignedRooms.map((r) => ({ - key: r.roomId, - value: r.roomId, - text: r.roomName, + const options = unassignedRooms.map((r) => ({ + id: r.roomId, + label: r.roomName, })); + const selectedOption = options.find(option => option.id === roomId) || null; + return ( - onChange(v.value?.toString())} - search - noResultsMessage={t( + value={selectedOption} + onChange={(_, newValue) => onChange(newValue?.id)} + getOptionLabel={(option) => option.label} + renderInput={(params) => ( + + )} + noOptionsText={t( 'linkRoomDialog.roomNoResults', 'Please create a new room in this space.' )} diff --git a/src/components/NotificationsProvider/NotificationsProvider.tsx b/src/components/NotificationsProvider/NotificationsProvider.tsx index f9e4bb9..d5ced34 100644 --- a/src/components/NotificationsProvider/NotificationsProvider.tsx +++ b/src/components/NotificationsProvider/NotificationsProvider.tsx @@ -24,7 +24,8 @@ import React, { useMemo, useState, } from 'react'; -import { Icon, Message, Transition } from 'semantic-ui-react'; +import { Alert, Fade, IconButton } from '@mui/material'; +import { Warning, Info, Close } from '@mui/icons-material'; import { styled } from '../StyledComponentsThemeProvider'; type NotificationType = 'error' | 'info'; @@ -101,7 +102,7 @@ const NotificationsContainer = styled.div({ zIndex: 2000, width: 400, maxWidth: 'calc(100% - 16px)', -}); +}) as React.ComponentType>; function NotificationDisplay({ type, @@ -125,21 +126,30 @@ function NotificationDisplay({ }, []); return ( - - - {type === 'error' ? ( - - ) : ( - - )} - - {message} - - - + + : } + action={ + setVisible(false)} + > + + + } + sx={{ + mb: 1, + boxShadow: (theme) => theme.shadows[4], + }} + role={latest ? 'alert' : undefined} + aria-live={latest ? 'assertive' : undefined} + > + {message} + + ); } @@ -151,7 +161,7 @@ export function NotificationsDisplay({ onDelete: (id: string) => void; }) { return ( - + {notifications .map((notification, idx) => ( - - - <Icon size="big" className="parking" /> - {t('parkingLot.title', 'Parking Lot')} - - + + + + + + + {t('parkingLot.title', 'Parking Lot')} + + + - deleteTopic({ topicId })} - onTopicChange={(topicId, changes) => updateTopic({ topicId, changes })} - /> + deleteTopic({ topicId })} + onTopicChange={(topicId, changes) => updateTopic({ topicId, changes })} + /> - - - + + + + + + ); } diff --git a/src/components/ParkingLot/TopicList.tsx b/src/components/ParkingLot/TopicList.tsx index d0f22c7..f6ec936 100644 --- a/src/components/ParkingLot/TopicList.tsx +++ b/src/components/ParkingLot/TopicList.tsx @@ -34,7 +34,7 @@ const List = styled.div<{ canDrop?: boolean }>(({ canDrop, theme }) => ({ background: canDrop ? `repeating-linear-gradient(45deg, ${theme.pageBackground}, ${theme.pageBackground} 10px, transparent 10px, transparent 20px)` : undefined, -})); +})) as React.ComponentType & { canDrop?: boolean }>; function ParkingLotPlaceholder() { const { t } = useTranslation(); @@ -64,12 +64,14 @@ export function TopicList({ const canDrop = dragStart?.type === 'topic'; return ( + // @ts-ignore types of react-beautiful-dnd are not compatible with React 18 {(provided) => ( )} - {provided.placeholder} + {provided.placeholder as React.ReactNode} )} diff --git a/src/components/PersonalSpace/PersonalSpace.tsx b/src/components/PersonalSpace/PersonalSpace.tsx index 50fd846..3374075 100644 --- a/src/components/PersonalSpace/PersonalSpace.tsx +++ b/src/components/PersonalSpace/PersonalSpace.tsx @@ -18,9 +18,18 @@ import { useEffect, useRef, useState } from 'react'; import { AutoFocusInside, FocusOn, InFocusGuard } from 'react-focus-on'; import { useTranslation } from 'react-i18next'; import { usePrevious } from 'react-use'; -import { Button, ButtonGroup, Icon, Modal } from 'semantic-ui-react'; +import { + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + IconButton, + Typography, + Stack +} from '@mui/material'; +import { Person, Help, Lightbulb } from '@mui/icons-material'; import { usePowerLevels } from '../../store'; -import { ButtonWithIcon } from '../ButtonWithIcon'; import { useNotifications } from '../NotificationsProvider'; import { styled } from '../StyledComponentsThemeProvider'; import { Tooltip } from '../Tooltip'; @@ -31,13 +40,9 @@ import { TopicSubmissionToggle } from './TopicSubmissionToggle'; const SideBySide = styled.div({ display: 'flex', - alignItems: 'baseline', - flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', gap: 8, - - '& > *:first-child': { - flex: '1', - }, }); export function PersonalSpace() { @@ -82,60 +87,74 @@ export function PersonalSpace() { ]); return ( - - + + + {canModerate && } + + + setOpen(false)} - onOpen={() => setOpen(true)} open={open} - role="dialog" + maxWidth="md" + fullWidth aria-labelledby={headerId} - aria-modal="true" - trigger={ - - - {t('personalSpace.submitProposal', 'Submit a topic')} - - } > - + -
- - {t('personalSpace.title', 'Personal Space')} -
+ + + + {t('personalSpace.title', 'Personal Space')} + + - - -
- - {canModerate && } -
+ + + ); } diff --git a/src/components/PersonalSpace/TopicList.tsx b/src/components/PersonalSpace/TopicList.tsx index 4c0e9e2..4ae2ddb 100644 --- a/src/components/PersonalSpace/TopicList.tsx +++ b/src/components/PersonalSpace/TopicList.tsx @@ -16,9 +16,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Icon, Message } from 'semantic-ui-react'; +import { Alert, Button } from '@mui/material'; +import { Info, Add } from '@mui/icons-material'; import { usePowerLevels } from '../../store'; -import { ButtonWithIcon } from '../ButtonWithIcon'; import { styled } from '../StyledComponentsThemeProvider'; import { usePersonalTopics } from './PersonalTopicsContextProvider'; import { TopicSubmission } from './TopicSubmission'; @@ -47,27 +47,40 @@ export function TopicList() { return ( <> {!canParticipantsSubmitTopics && ( - } + sx={{ mb: 2 }} + > + + {t( + 'personalSpace.submissionClosed.title', + 'Submission closed' + )} + +
+ {t( 'personalSpace.submissionClosed.instructions', 'Topic submission is not open yet, but you can already prepare your topics and submit them later.' )} - /> + )} + {/* @ts-ignore - styled-components JSX component type issue */} {topics.map((topic) => ( ))} - - + ); diff --git a/src/components/PersonalSpace/TopicSubmission.tsx b/src/components/PersonalSpace/TopicSubmission.tsx index 4bafd49..44857d1 100644 --- a/src/components/PersonalSpace/TopicSubmission.tsx +++ b/src/components/PersonalSpace/TopicSubmission.tsx @@ -16,13 +16,13 @@ import { useWidgetApi } from '@matrix-widget-toolkit/react'; import { useTranslation } from 'react-i18next'; -import { Icon } from 'semantic-ui-react'; +import { Button, CircularProgress } from '@mui/material'; +import { Check, Send, Delete } from '@mui/icons-material'; import { useCreateTopicSubmissionMutation, usePowerLevels, useSpaceMembers, } from '../../store'; -import { ButtonWithIcon } from '../ButtonWithIcon'; import { ConfirmDeleteDialog } from '../ConfirmDialog'; import { StickyNote, StickyNoteButton } from '../StickyNote'; import { Tooltip } from '../Tooltip'; @@ -81,10 +81,11 @@ export function TopicSubmission({ topic }: { topic: PersonalTopic }) { onDelete={() => removeTopic(topic.localId)} > + > + + @@ -95,29 +96,28 @@ export function TopicSubmission({ topic }: { topic: PersonalTopic }) { author={lookupDisplayName(widgetParameters.userId ?? '')} > {isSubmitted ? ( - : } > - {t('personalSpace.topic.alreadySubmitted', 'Already Submitted')} - + ) : ( - : } onClick={onSubmitTopic} > - {t('personalSpace.topic.submit', 'Submit')} - + )} ); diff --git a/src/components/PersonalSpace/TopicSubmissionToggle.tsx b/src/components/PersonalSpace/TopicSubmissionToggle.tsx index e7ef3cc..976bafb 100644 --- a/src/components/PersonalSpace/TopicSubmissionToggle.tsx +++ b/src/components/PersonalSpace/TopicSubmissionToggle.tsx @@ -15,7 +15,8 @@ */ import { useTranslation } from 'react-i18next'; -import { Button } from 'semantic-ui-react'; +import { Button } from '@mui/material'; +import { LockOpen, Lock } from '@mui/icons-material'; import { ROOM_EVENT_BARCAMP_TOPIC_SUBMISSION } from '../../lib/events'; import { usePatchPowerLevelsMutation, usePowerLevels } from '../../store'; import { Tooltip } from '../Tooltip'; @@ -37,10 +38,15 @@ export function TopicSubmissionToggle() { return ( ); } diff --git a/src/components/SessionGrid/AddTimeSlotButton.tsx b/src/components/SessionGrid/AddTimeSlotButton.tsx index 24b703a..6c8b548 100644 --- a/src/components/SessionGrid/AddTimeSlotButton.tsx +++ b/src/components/SessionGrid/AddTimeSlotButton.tsx @@ -16,7 +16,8 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Dropdown } from 'semantic-ui-react'; +import { IconButton, Menu, MenuItem } from '@mui/material'; +import { Add } from '@mui/icons-material'; import { TimeSlotTypes } from '../../lib/events'; import { Tooltip } from '../Tooltip'; @@ -26,50 +27,59 @@ export function AddTimeSlotButton({ onAddTimeSlot: (timeSlotType: TimeSlotTypes) => void; }) { const { t } = useTranslation(); - const [isOpen, setOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const isOpen = Boolean(anchorEl); const label = t( 'sessionGrid.timeSlot.createSessionsTimeSlot', 'Create a time slot' ); - return ( - // We have some extra logic to suppress the tooltip while the dropdown is - // open. Otherwise it behaves a bit strange - - } - options={[ - { key: 'sessions', text: label, value: 'sessions' }, - { - key: 'common-event', - text: t( - 'sessionGrid.timeSlot.createCommonEventTimeSlot', - 'Create a common event' - ), - value: 'common-event', - }, - ]} - onOpen={() => setOpen(true)} - onClose={() => setOpen(false)} - onChange={(event, data) => { - if (event.type === 'blur') { - // Ignore value if the user cancels choosing an option - return; - } + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; - if (data.value === 'common-event') { - onAddTimeSlot('common-event'); - } else if (data.value === 'sessions') { - onAddTimeSlot('sessions'); - } + const handleSelect = (timeSlotType: TimeSlotTypes) => { + onAddTimeSlot(timeSlotType); + handleClose(); + }; + + return ( + <> + + + + + + - + > + handleSelect('sessions')}> + {label} + + handleSelect('common-event')}> + {t( + 'sessionGrid.timeSlot.createCommonEventTimeSlot', + 'Create a common event' + )} + + + ); } diff --git a/src/components/SessionGrid/AddTrackButton.tsx b/src/components/SessionGrid/AddTrackButton.tsx index 9d90461..84b2e92 100644 --- a/src/components/SessionGrid/AddTrackButton.tsx +++ b/src/components/SessionGrid/AddTrackButton.tsx @@ -14,30 +14,33 @@ * limitations under the License. */ +import { Add } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { Button } from 'semantic-ui-react'; import { styled } from '../StyledComponentsThemeProvider'; import { Tooltip } from '../Tooltip'; -const ButtonWithoutMargin = styled(Button)({ +const ButtonWithoutMargin = styled(IconButton)({ '&&&&&': { margin: 0, }, -}); +}) as React.ComponentType>; export function AddTrackButton({ onAddTrack }: { onAddTrack: () => void }) { const { t } = useTranslation(); const label = t('track.create', 'Create a track'); return ( - + {/* div is needed to position the tooltip correctly */}
+ > + +
); diff --git a/src/components/SessionGrid/CommonEventSlot.tsx b/src/components/SessionGrid/CommonEventSlot.tsx index 5395177..5054068 100644 --- a/src/components/SessionGrid/CommonEventSlot.tsx +++ b/src/components/SessionGrid/CommonEventSlot.tsx @@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next'; import { CommonEventTimeSlot, Track } from '../../lib/events'; -import { IconPicker } from '../IconPicker'; +import { IconPicker, getIconByName, getIconName } from '../IconPicker'; import { InlineTextEdit } from '../InlineTextEdit'; import { styled } from '../StyledComponentsThemeProvider'; import { useEditMode } from './EditModeContext'; @@ -28,7 +28,7 @@ const CommonEventTd = styled.td(({ theme }) => ({ '&&': { verticalAlign: 'middle', }, -})); +})) as React.ComponentType & { colSpan?: number }>; const CommonEventLabel = styled.div<{ trackCount: number }>(() => ({ textAlign: 'center', @@ -36,11 +36,11 @@ const CommonEventLabel = styled.div<{ trackCount: number }>(() => ({ justifyContent: 'center', alignItems: 'center', gap: 8, -})); +})) as React.ComponentType & { trackCount: number }>; const Summary = styled.span({ fontWeight: 'bold', -}); +}) as React.ComponentType>; export type CommonEventTimeSlotChanges = Partial< Pick @@ -62,21 +62,21 @@ export function CommonEventSlot({ const { t } = useTranslation(); return ( - - + + { if (onChange) { onChange(timeSlot.id, { - icon, + icon: getIconName(icon), }); } }} /> - + + size="small" + > + + ); if (!onDelete) { diff --git a/src/components/SessionGrid/DeleteTrackButton.tsx b/src/components/SessionGrid/DeleteTrackButton.tsx index fe28382..5dcaeac 100644 --- a/src/components/SessionGrid/DeleteTrackButton.tsx +++ b/src/components/SessionGrid/DeleteTrackButton.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ +import { Delete } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { Button } from 'semantic-ui-react'; import { Track } from '../../lib/events'; import { ConfirmDeleteDialog } from '../ConfirmDialog'; import { Tooltip } from '../Tooltip'; @@ -30,13 +31,13 @@ export function DeleteTrackButton({ const { t } = useTranslation(); const deleteTrackTitle = t('track.delete.title', 'Delete track'); const trigger = ( -