From 73a6daad7aa4d1d017b9cc51904d218e52934118 Mon Sep 17 00:00:00 2001 From: Aliwoto Date: Sat, 24 Aug 2024 19:52:54 +0330 Subject: [PATCH] Add support for re-rendering pages when language changes. Signed-off-by: Aliwoto --- src/AppLayout.tsx | 58 ++++++++++++++++++- .../containers/dashboardContainer.tsx | 12 ++-- src/components/labels/titleLabel.tsx | 24 ++++++-- src/components/rendering/RenderAllFields.tsx | 17 +++++- src/index.tsx | 3 +- src/pages/confirmAccountRedirectPage.tsx | 9 ++- src/pages/courseInfoPage.tsx | 10 +++- src/pages/createCoursePage.tsx | 14 ++++- src/pages/createExamPage.tsx | 10 +++- src/pages/createTopicPage.tsx | 11 +++- src/pages/createUserPage.tsx | 11 +++- src/pages/dashboardPage.tsx | 9 ++- src/pages/examInfoPage.tsx | 19 ++++-- src/pages/loginPage.tsx | 9 ++- src/pages/searchCoursePage.tsx | 9 ++- src/pages/searchTopicPage.tsx | 7 ++- src/pages/searchUserPage.tsx | 9 ++- src/pages/userInfoPage.tsx | 23 +++++--- src/translations/appTranslation.ts | 9 ++- src/translations/faTranslation.ts | 7 ++- 20 files changed, 229 insertions(+), 51 deletions(-) diff --git a/src/AppLayout.tsx b/src/AppLayout.tsx index ed1822d..4e6eec2 100644 --- a/src/AppLayout.tsx +++ b/src/AppLayout.tsx @@ -3,9 +3,52 @@ import { Box } from '@mui/material'; import AppFooter from './components/footers/AppFooter'; import { SupportedTranslations, switchAppTranslation } from './translations/translationSwitcher'; import apiClient from './apiClient'; +import { CurrentAppTranslation } from './translations/appTranslation'; +import { forceUpdateDashboardContainer } from './components/containers/dashboardContainer'; +import { forceUpdateConfirmAccountRedirectPage } from './pages/confirmAccountRedirectPage'; +import { forceUpdateCourseInfoPage } from './pages/courseInfoPage'; +import { forceUpdateCreateCoursePage } from './pages/createCoursePage'; +import { forceUpdateCreateExamPage } from './pages/createExamPage'; +import { forceUpdateCreateTopicPage } from './pages/createTopicPage'; +import { forceUpdateCreateUserPage } from './pages/createUserPage'; +import { forceUpdateDashboardPage } from './pages/dashboardPage'; +import { forceUpdateExamInfoPage } from './pages/examInfoPage'; +import { forceUpdateLoginPage } from './pages/loginPage'; +import { forceUpdateSearchCoursePage } from './pages/searchCoursePage'; +import { forceUpdateSearchTopicPage } from './pages/searchTopicPage'; +import { forceUpdateSearchUserPage } from './pages/searchUserPage'; +import { forceUpdateUserInfoPage } from './pages/userInfoPage'; + +/** + * This function forces a re-render of all the pages in the app. + * This is useful when the app language is changed. + * (I wish there was a better way to do this but I couldn't find one 😭) + */ +const forceUpdateWholePage = () => { + try { + forceUpdateDashboardContainer(); + forceUpdateConfirmAccountRedirectPage(); + forceUpdateCourseInfoPage(); + forceUpdateCreateCoursePage(); + forceUpdateCreateExamPage(); + forceUpdateCreateTopicPage(); + forceUpdateCreateUserPage(); + forceUpdateDashboardPage(); + forceUpdateExamInfoPage(); + forceUpdateLoginPage(); + forceUpdateSearchCoursePage(); + forceUpdateSearchTopicPage(); + forceUpdateSearchUserPage(); + forceUpdateUserInfoPage(); + } catch (error: any) { + console.log(`forceUpdateWholePage: failed to force update: ${error}`); + } +}; const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [currentAppLanguage, setCurrentAppLanguage] = React.useState('en'); + const [fontFamily, setFontFamily] = React.useState(`${CurrentAppTranslation.fontFamily}`); + const [, forceUpdate] = React.useReducer(x => x + 1, 0); return ( = ({ children }) => { display: 'flex', flexDirection: 'column', minHeight: '110vh', // Ensure the layout takes at least the full viewport height + fontFamily: `${fontFamily}`, }} + fontFamily={ + `${fontFamily}` + } > - + {children} { + const userLang = lang as SupportedTranslations; console.log(`Switching from ${currentAppLanguage} to ${lang}`); - // doing this, so react re-renders the app with the new language + + // doing this so react re-renders the app with the new language setCurrentAppLanguage(lang); - const userLang = lang as SupportedTranslations; switchAppTranslation(userLang); await apiClient.setAppLanguage(userLang); + setFontFamily(`${CurrentAppTranslation.fontFamily}`); + + forceUpdate(); + forceUpdateWholePage(); }} /> ); diff --git a/src/components/containers/dashboardContainer.tsx b/src/components/containers/dashboardContainer.tsx index cd73d36..cc65355 100644 --- a/src/components/containers/dashboardContainer.tsx +++ b/src/components/containers/dashboardContainer.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useReducer } from "react"; import { useState } from "react"; import SideMenu from '../menus/sideMenu'; import HeaderLabel from "../labels/headerLabel"; @@ -6,15 +6,21 @@ import TitleLabel from "../labels/titleLabel"; import MenuButton from "../menus/menuButton"; import { CurrentAppTranslation } from "../../translations/appTranslation"; + interface DashboardContainerProps { children?: React.ReactNode; style?: React.CSSProperties; disableSlideMenu?: boolean; } -const DashboardContainer: React.FC = ({ ...props }) => { +export var forceUpdateDashboardContainer: () => void = () => { }; + +export const DashboardContainer: React.FC = ({ ...props }) => { const [isSideMenuOpen, setIsSideMenuOpen] = useState(false); const toggleMenu = () => setIsSideMenuOpen(!isSideMenuOpen); + const [, setForceUpdate] = useReducer(x => x + 1, 0); + + forceUpdateDashboardContainer = () => setForceUpdate(); return (
= ({ ...props }) =>
); } - -export default DashboardContainer; \ No newline at end of file diff --git a/src/components/labels/titleLabel.tsx b/src/components/labels/titleLabel.tsx index 065efc4..bb166ee 100644 --- a/src/components/labels/titleLabel.tsx +++ b/src/components/labels/titleLabel.tsx @@ -1,8 +1,22 @@ -import styled from "styled-components"; +import React from "react"; +import { CurrentAppTranslation } from "../../translations/appTranslation"; -const TitleLabel = styled.h1` - font-size: 24px; - margin-inline: auto; -`; +interface TitleLabelProps { + children?: React.ReactNode; + style?: React.CSSProperties; +} + +const TitleLabel: React.FC = ({ ...props }) => { + return

+ {props.children} +

; +}; export default TitleLabel; \ No newline at end of file diff --git a/src/components/rendering/RenderAllFields.tsx b/src/components/rendering/RenderAllFields.tsx index aada634..0c65f6b 100644 --- a/src/components/rendering/RenderAllFields.tsx +++ b/src/components/rendering/RenderAllFields.tsx @@ -1,7 +1,7 @@ import apiClient from '../../apiClient'; import { CurrentAppTranslation } from '../../translations/appTranslation'; -import { Checkbox, FormControlLabel, Grid, TextField, Typography } from '@mui/material'; +import { Box, Checkbox, FormControlLabel, Grid, TextField, Typography } from '@mui/material'; import SelectMenu from '../../components/menus/selectMenu'; import ModernDateTimePicker from '../date/ModernDatePicker'; @@ -81,10 +81,20 @@ const RenderAllFields = (props: RenderAllFieldsProps) => { } return ( + { handleInputChange({ @@ -99,6 +109,7 @@ const RenderAllFields = (props: RenderAllFieldsProps) => { } label={CurrentAppTranslation[field as keyof (typeof CurrentAppTranslation)]} /> + ); } @@ -162,7 +173,7 @@ const RenderAllFields = (props: RenderAllFieldsProps) => { disabled={!isEditing} type={'text'} label={CurrentAppTranslation[field as keyof (typeof CurrentAppTranslation)]} - value={data[field as keyof (typeof data)] ?? ''} + value={data[field] ?? ''} onChange={(e) => { handleInputChange(e as any) }} required /> ); diff --git a/src/index.tsx b/src/index.tsx index 76f763a..813a705 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,7 +13,8 @@ root.render( - + +
diff --git a/src/pages/confirmAccountRedirectPage.tsx b/src/pages/confirmAccountRedirectPage.tsx index 847f2e5..1aa137f 100644 --- a/src/pages/confirmAccountRedirectPage.tsx +++ b/src/pages/confirmAccountRedirectPage.tsx @@ -1,8 +1,8 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useReducer, useState } from 'react'; import { CircularProgress, Container, Paper, Box, Typography, Grid, TextField, Button } from '@mui/material'; import apiClient from '../apiClient'; import { ConfirmAccountData } from '../api'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import {DashboardContainer} from '../components/containers/dashboardContainer'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import { CurrentAppTranslation } from '../translations/appTranslation'; import useAppSnackbar from '../components/snackbars/useAppSnackbars'; @@ -16,9 +16,14 @@ interface ConfirmationRequiredFields { repeat_password: string; } +export var forceUpdateConfirmAccountRedirectPage = () => {}; + const ConfirmAccountRedirectPage = () => { apiClient.clearTokens(); const urlSearch = new URLSearchParams(window.location.search); + const [, forceUpdate] = useReducer(x => x + 1, 0); + + forceUpdateConfirmAccountRedirectPage = () => forceUpdate(); const [requiredData, setRequiredData] = useState({ user_id: '', diff --git a/src/pages/courseInfoPage.tsx b/src/pages/courseInfoPage.tsx index d5df16b..d569d0a 100644 --- a/src/pages/courseInfoPage.tsx +++ b/src/pages/courseInfoPage.tsx @@ -1,12 +1,14 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useReducer } from 'react'; import { CircularProgress, Container, Paper, Box, Typography, Grid, TextField, Button } from '@mui/material'; import apiClient from '../apiClient'; import { EditCourseData } from '../api'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import { DashboardContainer } from '../components/containers/dashboardContainer'; import { CurrentAppTranslation } from '../translations/appTranslation'; import useAppSnackbar from '../components/snackbars/useAppSnackbars'; import { extractErrorDetails } from '../utils/errorUtils'; +export var forceUpdateCourseInfoPage = () => {}; + const CourseInfoPage = () => { const [courseData, setCourseData] = useState({ course_id: 0, @@ -16,8 +18,12 @@ const CourseInfoPage = () => { }); const [isEditing, setIsEditing] = useState(false); const [isCourseNotFound, setIsCourseNotFound] = useState(false); + const [, forceUpdate] = useReducer(x => x + 1, 0); const snackbar = useAppSnackbar(); + forceUpdateCourseInfoPage = () => { + forceUpdate(); + }; const fetchCourseInfo = async () => { // the user id is passed like /courseInfo?userId=123 diff --git a/src/pages/createCoursePage.tsx b/src/pages/createCoursePage.tsx index be40b62..cc17157 100644 --- a/src/pages/createCoursePage.tsx +++ b/src/pages/createCoursePage.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useReducer, useState } from 'react'; import SubmitButton from '../components/buttons/submitButton'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import { DashboardContainer } from '../components/containers/dashboardContainer'; import TitleLabel from '../components/labels/titleLabel'; import CreateUserForm from '../components/forms/createUserForm'; import CreateUserContainer from '../components/containers/createUserContainer'; @@ -12,6 +12,7 @@ import { extractErrorDetails } from '../utils/errorUtils'; import RenderAllFields from '../components/rendering/RenderAllFields'; import { autoSetWindowTitle, getFieldOf } from '../utils/commonUtils'; +export var forceUpdateCreateCoursePage = () => {}; const CreateCoursePage: React.FC = () => { const [createCourseData, setCourseData] = useState({ @@ -19,8 +20,13 @@ const CreateCoursePage: React.FC = () => { course_name: '', course_description: '', }); + const [, forceUpdate] = useReducer(x => x + 1, 0); const snackbar = useAppSnackbar(); + forceUpdateCreateCoursePage = () => { + forceUpdate(); + }; + const handleInputChange = (e: React.ChangeEvent) => { setCourseData({ ...createCourseData, @@ -50,7 +56,9 @@ const CreateCoursePage: React.FC = () => { - {CurrentAppTranslation.CreateNewCourseText} + + {CurrentAppTranslation.CreateNewCourseText} + {RenderAllFields({ data: createCourseData, handleInputChange :handleInputChange, diff --git a/src/pages/createExamPage.tsx b/src/pages/createExamPage.tsx index cd674ec..ed06110 100644 --- a/src/pages/createExamPage.tsx +++ b/src/pages/createExamPage.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useReducer, useState } from 'react'; import SubmitButton from '../components/buttons/submitButton'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import {DashboardContainer} from '../components/containers/dashboardContainer'; import TitleLabel from '../components/labels/titleLabel'; import CreateUserForm from '../components/forms/createUserForm'; import CreateUserContainer from '../components/containers/createUserContainer'; @@ -13,6 +13,7 @@ import RenderAllFields from '../components/rendering/RenderAllFields'; import { getUTCUnixTimestamp } from '../utils/timeUtils'; import { autoSetWindowTitle, getFieldOf } from '../utils/commonUtils'; +export var forceUpdateCreateExamPage = () => {}; const CreateExamPage: React.FC = () => { const [createExamData, setCreateExamData] = useState({ @@ -24,8 +25,13 @@ const CreateExamPage: React.FC = () => { exam_date: 0, is_public: false, }); + const [, forceUpdate] = useReducer(x => x + 1, 0); const snackbar = useAppSnackbar(); + forceUpdateCreateExamPage = () => { + forceUpdate(); + }; + useEffect(() => { autoSetWindowTitle(); }, []); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/src/pages/createTopicPage.tsx b/src/pages/createTopicPage.tsx index e30fb96..9708780 100644 --- a/src/pages/createTopicPage.tsx +++ b/src/pages/createTopicPage.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useReducer, useState } from 'react'; import SubmitButton from '../components/buttons/submitButton'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import { DashboardContainer } from '../components/containers/dashboardContainer'; import TitleLabel from '../components/labels/titleLabel'; import CreateUserForm from '../components/forms/createUserForm'; import CreateUserContainer from '../components/containers/createUserContainer'; @@ -12,12 +12,19 @@ import useAppSnackbar from '../components/snackbars/useAppSnackbars'; import { extractErrorDetails } from '../utils/errorUtils'; import { autoSetWindowTitle } from '../utils/commonUtils'; +export var forceUpdateCreateTopicPage = () => {}; + const CreateTopicPage: React.FC = () => { const [createTopicData, setUserInfo] = useState({ topic_name: '', }); + const [, forceUpdate] = useReducer(x => x + 1, 0); const snackbar = useAppSnackbar(); + forceUpdateCreateTopicPage = () => { + forceUpdate(); + }; + const handleInputChange = (e: React.ChangeEvent) => { setUserInfo({ ...createTopicData, [e.target.name]: e.target.value }); }; diff --git a/src/pages/createUserPage.tsx b/src/pages/createUserPage.tsx index 99e3277..fb665c7 100644 --- a/src/pages/createUserPage.tsx +++ b/src/pages/createUserPage.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; +import React, { useReducer, useState } from 'react'; import SubmitButton from '../components/buttons/submitButton'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import { DashboardContainer } from '../components/containers/dashboardContainer'; import TitleLabel from '../components/labels/titleLabel'; import CreateUserForm from '../components/forms/createUserForm'; import CreateUserContainer from '../components/containers/createUserContainer'; @@ -12,6 +12,8 @@ import { Checkbox, FormControlLabel, TextField } from '@mui/material'; import useAppSnackbar from '../components/snackbars/useAppSnackbars'; import { extractErrorDetails } from '../utils/errorUtils'; +export var forceUpdateCreateUserPage = () => {}; + const CreateUserPage: React.FC = () => { const [createUserData, setUserInfo] = useState({ user_id: '', @@ -19,8 +21,13 @@ const CreateUserPage: React.FC = () => { password: '', role: UserRole.UserRoleStudent, }); + const [, forceUpdate] = useReducer(x => x + 1, 0); const snackbar = useAppSnackbar(); + forceUpdateCreateUserPage = () => { + forceUpdate(); + }; + const handleInputChange = (e: React.ChangeEvent) => { setUserInfo({ ...createUserData, [e.target.name]: e.target.value }); }; diff --git a/src/pages/dashboardPage.tsx b/src/pages/dashboardPage.tsx index a54ad9b..ce944c4 100644 --- a/src/pages/dashboardPage.tsx +++ b/src/pages/dashboardPage.tsx @@ -1,9 +1,11 @@ import React, { useEffect } from 'react'; import styled from 'styled-components'; import apiClient from '../apiClient'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import { DashboardContainer } from '../components/containers/dashboardContainer'; import { autoSetWindowTitle } from '../utils/commonUtils'; +export var forceUpdateDashboardPage = () => {}; + const MainContent = styled.div` display: flex; gap: 20px; @@ -34,6 +36,11 @@ const ListItem = styled.li` `; const DashboardPage: React.FC = () => { + const [, forceUpdate] = React.useReducer(x => x + 1, 0); + forceUpdateDashboardPage = () => { + forceUpdate(); + } + const fetchUserInfo = async () => { try { await apiClient.getCurrentUserInfo(); diff --git a/src/pages/examInfoPage.tsx b/src/pages/examInfoPage.tsx index 1168057..a7f00aa 100644 --- a/src/pages/examInfoPage.tsx +++ b/src/pages/examInfoPage.tsx @@ -1,8 +1,8 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useReducer } from 'react'; import { CircularProgress, Container, Paper, Box, Typography, Grid, Button } from '@mui/material'; import apiClient from '../apiClient'; import { EditExamData } from '../api'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import {DashboardContainer} from '../components/containers/dashboardContainer'; import { CurrentAppTranslation } from '../translations/appTranslation'; import useAppSnackbar from '../components/snackbars/useAppSnackbars'; import { extractErrorDetails } from '../utils/errorUtils'; @@ -10,6 +10,8 @@ import { getFieldOf } from '../utils/commonUtils'; import { getUTCUnixTimestamp } from '../utils/timeUtils'; import RenderAllFields from '../components/rendering/RenderAllFields'; +export var forceUpdateExamInfoPage = () => {}; + const ExamInfoPage = () => { const [examData, setExamData] = useState({ exam_id: 0, @@ -21,10 +23,15 @@ const ExamInfoPage = () => { exam_date: 0, is_public: false, }); + const [, forceUpdate] = useReducer(x => x + 1, 0); const [isEditing, setIsEditing] = useState(false); const [isUserNotFound, setIsUserNotFound] = useState(false); const snackbar = useAppSnackbar(); + forceUpdateExamInfoPage = () => { + forceUpdate(); + }; + const fetchExamInfo = async () => { // the exam id is passed like /examInfo?examId=123 const urlSearch = new URLSearchParams(window.location.search); @@ -65,7 +72,11 @@ const ExamInfoPage = () => { window.history.pushState( `examInfo_examId_${examData.exam_id}`, "Exam Info", - `/examInfo?examId=${encodeURIComponent(examData.exam_id!)}&edit=${isEditing ? '0' : '1'}`, + `${window.location.pathname}?examId=${ + encodeURIComponent(examData.exam_id!) + }&edit=${ + isEditing ? '0' : '1' + }`, ); setIsEditing(!isEditing); } @@ -92,7 +103,7 @@ const ExamInfoPage = () => { const updatedUserData: any = { ...examData }; Object.keys(result).forEach(key => { if (key in examData) { - updatedUserData[key as keyof (typeof updatedUserData)] = result[key as keyof (typeof result)]; + updatedUserData[key] = result[key as keyof (typeof result)]; } }); diff --git a/src/pages/loginPage.tsx b/src/pages/loginPage.tsx index c64491f..a91e6b1 100644 --- a/src/pages/loginPage.tsx +++ b/src/pages/loginPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useReducer } from 'react'; import styled from 'styled-components'; import { Navigate } from 'react-router-dom'; import apiClient from '../apiClient'; @@ -17,6 +17,8 @@ import useAppSnackbar from '../components/snackbars/useAppSnackbars'; import { CurrentAppTranslation } from '../translations/appTranslation'; /********************************************/ +export var forceUpdateLoginPage = () => {}; + const CaptchaContainer = styled.div` display: flex; align-items: center; @@ -31,8 +33,13 @@ const Login = () => { const [captchaImage, setCaptchaImage] = useState(''); const [isCaptchaIncorrect, setIsCaptchaIncorrect] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false); + const [, forceUpdate] = useReducer(x => x + 1, 0); const snackbar = useAppSnackbar(); + forceUpdateLoginPage = () => { + forceUpdate(); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { diff --git a/src/pages/searchCoursePage.tsx b/src/pages/searchCoursePage.tsx index 88b2dbc..1bff2df 100644 --- a/src/pages/searchCoursePage.tsx +++ b/src/pages/searchCoursePage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useReducer, useState } from 'react'; import { TextField, List, @@ -14,11 +14,13 @@ import Pagination from '@mui/material/Pagination'; import IconButton from '@mui/material/IconButton'; import { SearchedCourseInfo } from '../api'; import apiClient from '../apiClient'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import { DashboardContainer } from '../components/containers/dashboardContainer'; import { timeAgo } from '../utils/timeUtils'; import { CurrentAppTranslation } from '../translations/appTranslation'; import { autoSetWindowTitle } from '../utils/commonUtils'; +export var forceUpdateSearchCoursePage = () => {}; + const RenderCoursesList = (courses: SearchedCourseInfo[] | undefined, forEdit: boolean = false) => { if (!courses || courses.length === 0) { return ( @@ -76,10 +78,13 @@ const SearchCoursePage = () => { const [query, setQuery] = useState(providedQuery ?? ''); const [courses, setCourses] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [, setForceUpdate] = useReducer(x => x + 1, 0); const [page, setPage] = useState(providedPage ? parseInt(providedPage) - 1 : 0); const [totalPages, setTotalPages] = useState(page + 1); const limit = 10; + forceUpdateSearchCoursePage = () => setForceUpdate(); + const handleSearch = async (newPage = 0) => { window.history.pushState( `searchCourse_query_${query}`, diff --git a/src/pages/searchTopicPage.tsx b/src/pages/searchTopicPage.tsx index 0ceb0cc..bbf87fe 100644 --- a/src/pages/searchTopicPage.tsx +++ b/src/pages/searchTopicPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, Fragment } from 'react'; +import { useEffect, useState, Fragment, useReducer } from 'react'; import { TextField, List, @@ -14,7 +14,7 @@ import SearchIcon from '@mui/icons-material/Search'; import IconButton from '@mui/material/IconButton'; import { SearchedTopicInfo } from '../api'; import apiClient from '../apiClient'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import { DashboardContainer } from '../components/containers/dashboardContainer'; import { CurrentAppTranslation } from '../translations/appTranslation'; import Dialog from '@mui/material/Dialog'; @@ -39,6 +39,7 @@ interface DeleteDialogueProps { var currentDeleteProps: DeleteDialogueProps | null = null; var confirmedDeleteTopicId: number = 0; +export var forceUpdateSearchTopicPage = () => { }; const DeleteDialogueComponent = () => { const props = currentDeleteProps; @@ -135,11 +136,13 @@ const SearchTopicPage = () => { const fullScreen = useMediaQuery(theme.breakpoints.down('md')); const [query, setQuery] = useState(providedQuery ?? ''); const [topics, setTopics] = useState([]); + const [, forceUpdate] = useReducer(x => x + 1, 0); const [isDeleteDialogueOpen, setIsDeleteDialogueOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [deleteDialogue, setDeleteDialogue] = useState(null); const snackbar = useAppSnackbar(); + forceUpdateSearchTopicPage = () => forceUpdate(); const handleSearch = async () => { window.history.pushState( diff --git a/src/pages/searchUserPage.tsx b/src/pages/searchUserPage.tsx index 8389e5e..04a124c 100644 --- a/src/pages/searchUserPage.tsx +++ b/src/pages/searchUserPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useReducer, useState } from 'react'; import { TextField, List, @@ -14,7 +14,7 @@ import Pagination from '@mui/material/Pagination'; import IconButton from '@mui/material/IconButton'; import { SearchedUserInfo } from '../api'; import apiClient from '../apiClient'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import {DashboardContainer} from '../components/containers/dashboardContainer'; import { timeAgo } from '../utils/timeUtils'; import { CurrentAppTranslation } from '../translations/appTranslation'; import { autoSetWindowTitle } from '../utils/commonUtils'; @@ -67,6 +67,8 @@ const RenderUsersList = (users: SearchedUserInfo[] | undefined, forEdit: boolean ) } +export var forceUpdateSearchUserPage = () => {}; + const SearchUserPage = () => { const urlSearch = new URLSearchParams(window.location.search); const providedQuery = urlSearch.get('query'); @@ -75,11 +77,14 @@ const SearchUserPage = () => { const [query, setQuery] = useState(providedQuery ?? ''); const [users, setUsers] = useState([]); + const [, forceUpdate] = useReducer(x => x + 1, 0); const [isLoading, setIsLoading] = useState(false); const [page, setPage] = useState(providedPage ? parseInt(providedPage) - 1 : 0); const [totalPages, setTotalPages] = useState(page + 1); const limit = 10; + forceUpdateSearchUserPage = () => forceUpdate(); + const handleSearch = async (newPage = 0) => { window.history.pushState( `searchUser_query_${query}`, diff --git a/src/pages/userInfoPage.tsx b/src/pages/userInfoPage.tsx index abd7e89..8fed3e5 100644 --- a/src/pages/userInfoPage.tsx +++ b/src/pages/userInfoPage.tsx @@ -1,22 +1,27 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useReducer } from 'react'; import { CircularProgress, Container, Paper, Box, Typography, Avatar, Grid, TextField, Button } from '@mui/material'; import apiClient from '../apiClient'; import { EditUserData } from '../api'; -import DashboardContainer from '../components/containers/dashboardContainer'; +import {DashboardContainer} from '../components/containers/dashboardContainer'; import { CurrentAppTranslation } from '../translations/appTranslation'; import useAppSnackbar from '../components/snackbars/useAppSnackbars'; import { extractErrorDetails } from '../utils/errorUtils'; +export var forceUpdateUserInfoPage = () => {}; + const UserInfoPage = () => { const [userData, setUserData] = useState({ user_id: '', full_name: '', email: '', }); + const [, forceUpdate] = useReducer(x => x + 1, 0); const [isEditing, setIsEditing] = useState(false); const [isUserNotFound, setIsUserNotFound] = useState(false); const snackbar = useAppSnackbar(); + forceUpdateUserInfoPage = () => forceUpdate(); + useEffect(() => { fetchUserInfo(); }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -54,10 +59,10 @@ const UserInfoPage = () => { const handleSave = async () => { try { const result = await apiClient.editUser(userData); - const updatedUserData = { ...userData }; + const updatedUserData: any = { ...userData }; Object.keys(result).forEach(key => { if (key in userData) { - updatedUserData[key as keyof (typeof updatedUserData)] = result[key as keyof (typeof result)]; + updatedUserData[key] = result[key as keyof (typeof result)]; } }); @@ -101,7 +106,10 @@ const UserInfoPage = () => { {Object.keys(userData).map((field) => ( - + {isEditing && apiClient.canUserFieldBeEdited(field) ? ( { ) : ( - {CurrentAppTranslation[field as keyof (typeof CurrentAppTranslation)]}: - {userData[field as keyof (typeof userData)]} + {`${CurrentAppTranslation[field as keyof (typeof CurrentAppTranslation)]}: `} + + {userData[field as keyof (typeof userData)]} )} diff --git a/src/translations/appTranslation.ts b/src/translations/appTranslation.ts index a92e0f0..c3ed626 100644 --- a/src/translations/appTranslation.ts +++ b/src/translations/appTranslation.ts @@ -1,10 +1,16 @@ import { AppCalendarType } from "../utils/AppCalendarTypes"; +export type TextDirection = "ltr" | "rtl"; + +export type TextJustifyContent = "flex-end" | "flex-start"; + export class AppTranslationBase { ShortLang: string = "en"; //#region Style Attributes - direction: "ltr" | "rtl" = "ltr"; + fontFamily: string = `"Roboto", "Helvetica", "Arial", sans-serif`; + direction: TextDirection = "ltr"; + justifyContent: TextJustifyContent = "flex-start"; textAlign: string = "left"; float: string = "left"; @@ -69,6 +75,7 @@ export class AppTranslationBase { SendEmailToUseText: string = "Send email confirmation to user"; // System messages + LanguageChangedSuccessfullyText: string = "Language changed successfully!"; AreYouSureDeleteTopicText: string = "Are you sure you want to delete this topic?"; DeleteTopicDescriptionText: string = "This action cannot be undone! All courses and exams related to this topic will be deleted as well."; TopicDeletedSuccessfullyText: string = "Topic deleted successfully!"; diff --git a/src/translations/faTranslation.ts b/src/translations/faTranslation.ts index cbfda86..08eece1 100644 --- a/src/translations/faTranslation.ts +++ b/src/translations/faTranslation.ts @@ -1,12 +1,14 @@ import { AppCalendarType } from "../utils/AppCalendarTypes"; -import { AppTranslationBase } from "./appTranslation"; +import { AppTranslationBase, TextDirection, TextJustifyContent } from "./appTranslation"; class FaTranslation extends AppTranslationBase { ShortLang: string = "fa"; //#region Style Attributes - direction: "ltr" | "rtl" = "rtl"; + fontFamily: string = `"B Kamran", "Vazir", "Shabnam", "Samim", "Iran Sans", "Yekan", "Nazanin", "Tahoma", "Arial", sans-serif`; + direction: TextDirection = "rtl"; + justifyContent: TextJustifyContent = "flex-end"; textAlign: string = "right"; float: string = "right"; @@ -71,6 +73,7 @@ class FaTranslation extends AppTranslationBase { SendEmailToUseText: string = "ارسال ایمیل تایید به کاربر"; // System messages + LanguageChangedSuccessfullyText: string = "زبان با موفقیت تغییر یافت"; AreYouSureDeleteTopicText: string = "آیا مطمئن هستید که می خواهید این موضوع را حذف کنید؟"; DeleteTopicDescriptionText: string = "این عملیات قابل بازگشت نیست! تمام دوره ها و آزمون های مرتبط با این موضوع نیز حذف خواهند شد."; TopicDeletedSuccessfullyText: string = "موضوع با موفقیت حذف شد!";