Merge commit 'refs/pull/242/head' of https://github.com/getmaxun/maxun into internationalization2

This commit is contained in:
amhsirak
2024-12-19 03:34:53 +05:30
15 changed files with 806 additions and 445 deletions

View File

@@ -36,7 +36,13 @@
"fortawesome": "^0.0.1-security", "fortawesome": "^0.0.1-security",
"google-auth-library": "^9.14.1", "google-auth-library": "^9.14.1",
"googleapis": "^144.0.0", "googleapis": "^144.0.0",
"i18next": "^24.0.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^3.0.1",
"idcac-playwright": "^0.1.3", "idcac-playwright": "^0.1.3",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"joi": "^17.6.0", "joi": "^17.6.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@@ -57,6 +63,7 @@
"react": "^18.0.0", "react": "^18.0.0",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-highlight": "0.15.0", "react-highlight": "0.15.0",
"react-i18next": "^15.1.3",
"react-router-dom": "^6.26.1", "react-router-dom": "^6.26.1",
"react-simple-code-editor": "^0.11.2", "react-simple-code-editor": "^0.11.2",
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",

50
public/locales/de.json Normal file
View File

@@ -0,0 +1,50 @@
{
"login": {
"title": "Willkommen zurück!",
"email": "E-Mail",
"password": "Passwort",
"button": "Einloggen",
"loading": "Lädt",
"register_prompt": "Noch keinen Account?",
"register_link": "Registrieren",
"welcome_notification": "Willkommen bei Maxun!",
"error_notification": "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut."
},
"register": {
"title": "Konto registrieren",
"email": "E-Mail",
"password": "Passwort",
"button": "Registrieren",
"loading": "Lädt",
"register_prompt": "Bereits ein Konto?",
"login_link": "Einloggen",
"welcome_notification": "Willkommen bei Maxun!",
"error_notification": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut."
},
"recordingtable": {
"run": "Ausführen",
"name": "Name",
"schedule": "Zeitplan",
"integrate": "Integrieren",
"settings": "Einstellungen",
"options": "Optionen",
"heading": "Meine Roboter",
"new": "Roboter erstellen",
"modal": {
"title": "Geben Sie die URL ein",
"label": "URL",
"button": "Aufnahme starten"
},
"edit": "Bearbeiten",
"delete": "Löschen",
"duplicate": "Duplizieren"
},
"mainmenu": {
"recordings": "Roboter",
"runs": "Ausführungen",
"proxy": "Proxy",
"apikey": "API-Schlüssel",
"feedback": "Maxun Cloud beitreten",
"apidocs": "API-Dokumentation"
}
}

66
public/locales/en.json Normal file
View File

@@ -0,0 +1,66 @@
{
"login": {
"title": "Welcome Back!",
"email": "Email",
"password": "Password",
"button": "Login",
"loading": "Loading",
"register_prompt": "Don't have an account?",
"register_link": "Register",
"welcome_notification": "Welcome to Maxun!",
"error_notification": "Login Failed. Please try again."
},
"register": {
"title": "Register Account",
"email": "Email",
"password": "Password",
"button": "Register",
"loading": "Loading",
"register_prompt": "Already have an account?",
"login_link": "Login",
"welcome_notification": "Welcome to Maxun!",
"error_notification": "Registeration Failed. Please try again."
},
"recordingtable":{
"run": "Run",
"name": "Name",
"schedule": "Schedule",
"integrate": "Integrate",
"settings": "Settings",
"options": "Options",
"heading":"My Robots",
"new":"Create Robot",
"modal":{
"title":"Enter the URL",
"label":"URL",
"button":"Start Recording"
},
"edit":"Edit",
"delete":"Delete",
"duplicate":"Duplicate",
"search":"Search Robots..."
},
"mainmenu":{
"recordings": "Robots",
"runs": "Runs",
"proxy": "Proxy",
"apikey": "API Key",
"feedback":"Join Maxun Cloud",
"apidocs":"API Docs"
},
"runstable":{
"runs":"All Runs",
"runStatus":"Status",
"runName":"Name",
"startedAt":"Started At",
"finishedAt":"Finished At",
"delete":"Delete",
"settings":"Settings",
"search":"Search Runs..."
}
}

13
public/locales/es.json Normal file
View File

@@ -0,0 +1,13 @@
{
"app": {
"name": "Maxun",
"version": "beta"
},
"login": {
"title": "¡Bienvenido de nuevo!",
"email": "Correo electrónico",
"password": "Contraseña",
"button": "Iniciar sesión",
"register_prompt": "¿No tienes una cuenta? Regístrate"
}
}

61
public/locales/ja.json Normal file
View File

@@ -0,0 +1,61 @@
{
"login": {
"title": "お帰りなさい!",
"email": "メールアドレス",
"password": "パスワード",
"button": "ログイン",
"loading": "読み込み中",
"register_prompt": "アカウントをお持ちでないですか?",
"register_link": "登録する",
"welcome_notification": "Maxunへようこそ",
"error_notification": "ログインに失敗しました。もう一度お試しください。"
},
"register": {
"title": "アカウントを登録する",
"email": "メールアドレス",
"password": "パスワード",
"button": "登録する",
"loading": "読み込み中",
"register_prompt": "既にアカウントをお持ちですか?",
"login_link": "ログイン",
"welcome_notification": "Maxunへようこそ",
"error_notification": "登録に失敗しました。もう一度お試しください。"
},
"recordingtable": {
"run": "実行",
"name": "名前",
"schedule": "スケジュール",
"integrate": "統合",
"settings": "設定",
"options": "オプション",
"heading": "私のロボット",
"new": "ロボットを作成",
"modal": {
"title": "URLを入力してください",
"label": "URL",
"button": "録画を開始"
},
"edit": "編集",
"delete": "削除",
"duplicate": "複製",
"search": "ロボットを検索..."
},
"mainmenu": {
"recordings": "ロボット",
"runs": "実行",
"proxy": "プロキシ",
"apikey": "APIキー",
"feedback": "Maxunクラウドに参加する",
"apidocs": "APIドキュメント"
},
"runstable": {
"runs": "すべての実行",
"runStatus": "ステータス",
"runName": "名前",
"startedAt": "開始日時",
"finishedAt": "終了日時",
"delete": "削除",
"settings": "設定",
"search": "実行を検索..."
}
}

13
public/locales/zh.json Normal file
View File

@@ -0,0 +1,13 @@
{
"app": {
"name": "Maxun",
"version": "beta"
},
"login": {
"title": "欢迎回来!",
"email": "电子邮件",
"password": "密码",
"button": "登录",
"register_prompt": "没有账号?注册"
}
}

View File

@@ -1,8 +1,10 @@
import React from 'react'; import React from "react";
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from "react-router-dom";
import { ThemeProvider, createTheme } from "@mui/material/styles"; import { ThemeProvider, createTheme } from "@mui/material/styles";
import { GlobalInfoProvider } from "./context/globalInfo"; import { GlobalInfoProvider } from "./context/globalInfo";
import { PageWrapper } from "./pages/PageWrappper"; import { PageWrapper } from "./pages/PageWrappper";
import i18n from "./i18n";
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
@@ -20,14 +22,14 @@ const theme = createTheme({
}, },
containedPrimary: { containedPrimary: {
// Styles for 'contained' variant with 'primary' color // Styles for 'contained' variant with 'primary' color
'&:hover': { "&:hover": {
backgroundColor: "#ff66d9", backgroundColor: "#ff66d9",
}, },
}, },
outlined: { outlined: {
// Apply white background for all 'outlined' variant buttons // Apply white background for all 'outlined' variant buttons
backgroundColor: "#ffffff", backgroundColor: "#ffffff",
'&:hover': { "&:hover": {
backgroundColor: "#f0f0f0", // Optional lighter background on hover backgroundColor: "#f0f0f0", // Optional lighter background on hover
}, },
}, },
@@ -36,7 +38,7 @@ const theme = createTheme({
MuiLink: { MuiLink: {
styleOverrides: { styleOverrides: {
root: { root: {
'&:hover': { "&:hover": {
color: "#ff00c3", color: "#ff00c3",
}, },
}, },
@@ -63,7 +65,7 @@ const theme = createTheme({
standardInfo: { standardInfo: {
backgroundColor: "#fce1f4", backgroundColor: "#fce1f4",
color: "#ff00c3", color: "#ff00c3",
'& .MuiAlert-icon': { "& .MuiAlert-icon": {
color: "#ff00c3", color: "#ff00c3",
}, },
}, },
@@ -72,7 +74,7 @@ const theme = createTheme({
MuiAlertTitle: { MuiAlertTitle: {
styleOverrides: { styleOverrides: {
root: { root: {
'& .MuiAlert-icon': { "& .MuiAlert-icon": {
color: "#ffffff", color: "#ffffff",
}, },
}, },
@@ -81,15 +83,16 @@ const theme = createTheme({
}, },
}); });
function App() { function App() {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<GlobalInfoProvider>
<Routes> <GlobalInfoProvider>
<Route path="/*" element={<PageWrapper />} /> <Routes>
</Routes> <Route path="/*" element={<PageWrapper />} />
</GlobalInfoProvider> </Routes>
</GlobalInfoProvider>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@@ -1,10 +1,24 @@
import { useTranslation } from "react-i18next"; // Import useTranslation hook
import React, { useState, useContext, useEffect } from 'react'; import React, { useState, useContext, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import styled from "styled-components"; import styled from "styled-components";
import { stopRecording } from "../../api/recording"; import { stopRecording } from "../../api/recording";
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar } from "@mui/material"; import { IconButton, Menu, MenuItem, Typography, Chip, Button, Modal, Tabs, Tab, Box, Snackbar } from "@mui/material";
import { AccountCircle, Logout, Clear, YouTube, X, Update, Close } from "@mui/icons-material"; import { AccountCircle, Logout, Clear, YouTube, X, Update, Close,Language } from "@mui/icons-material";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../../context/auth'; import { AuthContext } from '../../context/auth';
import { SaveRecording } from '../molecules/SaveRecording'; import { SaveRecording } from '../molecules/SaveRecording';
@@ -13,18 +27,26 @@ import { apiUrl } from '../../apiConfig';
import MaxunLogo from "../../assets/maxunlogo.png"; import MaxunLogo from "../../assets/maxunlogo.png";
import packageJson from "../../../package.json" import packageJson from "../../../package.json"
interface NavBarProps { interface NavBarProps {
recordingName: string; recordingName: string;
isRecording: boolean; isRecording: boolean;
} }
export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) => { export const NavBar: React.FC<NavBarProps> = ({
const { notify, browserId, setBrowserId, recordingUrl } = useGlobalInfoStore(); recordingName,
isRecording,
}) => {
const { notify, browserId, setBrowserId } = useGlobalInfoStore();
const { state, dispatch } = useContext(AuthContext); const { state, dispatch } = useContext(AuthContext);
const { user } = state; const { user } = state;
const navigate = useNavigate(); const navigate = useNavigate();
const { t, i18n } = useTranslation(); // Get translation function and i18n methods
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null);
const currentVersion = packageJson.version; const currentVersion = packageJson.version;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -58,29 +80,40 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
setTab(newValue); setTab(newValue);
}; };
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => { const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
const handleLangMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setLangAnchorEl(event.currentTarget);
};
const handleMenuClose = () => { const handleMenuClose = () => {
setAnchorEl(null); setAnchorEl(null);
setLangAnchorEl(null);
}; };
const logout = async () => { const logout = async () => {
dispatch({ type: 'LOGOUT' }); dispatch({ type: "LOGOUT" });
window.localStorage.removeItem('user'); window.localStorage.removeItem("user");
const { data } = await axios.get(`${apiUrl}/auth/logout`); const { data } = await axios.get(`${apiUrl}/auth/logout`);
notify('success', data.message); notify("success", data.message);
navigate('/login'); navigate("/login");
}; };
const goToMainMenu = async () => { const goToMainMenu = async () => {
if (browserId) { if (browserId) {
await stopRecording(browserId); await stopRecording(browserId);
notify('warning', 'Current Recording was terminated'); notify("warning", "Current Recording was terminated");
setBrowserId(null); setBrowserId(null);
} }
navigate('/'); navigate("/");
};
const changeLanguage = (lang: string) => {
i18n.changeLanguage(lang); // Change language dynamically
localStorage.setItem("language", lang); // Persist language to localStorage
}; };
useEffect(() => { useEffect(() => {
@@ -95,228 +128,281 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
}, []); }, []);
return ( return (
<>
{isUpdateAvailable && ( <NavBarWrapper>
<Snackbar
open={isUpdateAvailable} <div
onClose={() => setIsUpdateAvailable(false)} style={{
message={ display: "flex",
`New version ${latestVersion} available! Click "Upgrade" to update.` justifyContent: "flex-start",
} }}
action={ >
<> <img
<Button src={MaxunLogo}
color="primary" width={45}
size="small" height={40}
onClick={handleUpdateOpen} style={{ borderRadius: "5px", margin: "5px 0px 5px 15px" }}
style={{ />
backgroundColor: '#ff00c3', <div style={{ padding: "11px" }}>
color: 'white', <ProjectName>Maxun</ProjectName>
fontWeight: 'bold', </div>
textTransform: 'none', <Chip
marginRight: '8px', label="beta"
borderRadius: '5px', color="primary"
}} variant="outlined"
> sx={{ marginTop: "10px" }}
Upgrade
</Button>
<IconButton
size="small"
aria-label="close"
color="inherit"
onClick={() => setIsUpdateAvailable(false)}
style={{ color: 'black' }}
>
<Close />
</IconButton>
</>
}
ContentProps={{
sx: {
background: "white",
color: "black",
}
}}
/> />
)}
<NavBarWrapper> </div>
<div style={{ {user ? (
display: 'flex', <div
justifyContent: 'flex-start', style={{
}}> display: "flex",
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} /> alignItems: "center",
<div style={{ padding: '11px' }}><ProjectName>Maxun</ProjectName></div> justifyContent: "flex-end",
<Chip }}
label={`${currentVersion}`} >
color="primary" {!isRecording ? (
variant="outlined" <>
sx={{ marginTop: '10px' }} <IconButton
/> component="a"
href="https://discord.gg/5GbPjBUkws"
target="_blank"
rel="noopener noreferrer"
sx={{
display: "flex",
alignItems: "center",
borderRadius: "5px",
padding: "8px",
marginRight: "30px",
}}
>
<DiscordIcon sx={{ marginRight: "5px" }} />
</IconButton>
<iframe
src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large"
frameBorder="0"
scrolling="0"
width="170"
height="30"
title="GitHub"
></iframe>
<IconButton
onClick={handleMenuOpen}
sx={{
display: "flex",
alignItems: "center",
borderRadius: "5px",
padding: "8px",
marginRight: "10px",
"&:hover": { backgroundColor: "white", color: "#ff00c3" },
}}
>
<AccountCircle sx={{ marginRight: "5px" }} />
<Typography variant="body1">{user.email}</Typography>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
>
<MenuItem
onClick={() => {
handleMenuClose();
logout();
}}
>
<Logout sx={{ marginRight: "5px" }} /> {t("logout")}
</MenuItem>
</Menu>
</>
) : (
<>
<IconButton
onClick={goToMainMenu}
sx={{
borderRadius: "5px",
padding: "8px",
background: "red",
color: "white",
marginRight: "10px",
"&:hover": { color: "white", backgroundColor: "red" },
}}
>
<Clear sx={{ marginRight: "5px" }} />
{t("discard")}
</IconButton>
<SaveRecording fileName={recordingName} />
</>
)}
<IconButton
onClick={handleLangMenuOpen}
sx={{
display: "flex",
alignItems: "center",
borderRadius: "5px",
padding: "8px",
marginRight: "10px",
}}
>
<Typography variant="body1">
<Language />
</Typography>
</IconButton>
<Menu
anchorEl={langAnchorEl}
open={Boolean(langAnchorEl)}
onClose={handleMenuClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
>
<MenuItem
onClick={() => {
changeLanguage("en");
handleMenuClose();
}}
>
English
</MenuItem>
<MenuItem
onClick={() => {
changeLanguage("es");
handleMenuClose();
}}
>
Español
</MenuItem>
<MenuItem
onClick={() => {
changeLanguage("ja");
handleMenuClose();
}}
>
</MenuItem>
<MenuItem
onClick={() => {
changeLanguage("ar");
handleMenuClose();
}}
>
العربية
</MenuItem>
<MenuItem
onClick={() => {
changeLanguage("zh");
handleMenuClose();
}}
>
</MenuItem>
<MenuItem
onClick={() => {
changeLanguage("de");
handleMenuClose();
}}
>
German
</MenuItem>
</Menu>
</div> </div>
{ ) : (
user ? ( <><IconButton
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}> onClick={handleLangMenuOpen}
{!isRecording ? ( sx={{
<> display: "flex",
<Button variant="outlined" onClick={handleUpdateOpen} sx={{ alignItems: "center",
marginRight: '40px', borderRadius: "5px",
color: "#00000099", padding: "8px",
border: "#00000099 1px solid", marginRight: "10px",
'&:hover': { color: '#ff00c3', border: '#ff00c3 1px solid' } }}
}}> >
<Update sx={{ marginRight: '5px' }} /> Upgrade Maxun <Typography variant="body1">{t("language")}</Typography>
</Button> </IconButton>
<Modal open={open} onClose={handleUpdateClose}> <Menu
<Box anchorEl={langAnchorEl}
sx={{ open={Boolean(langAnchorEl)}
position: "absolute", onClose={handleMenuClose}
top: "50%", anchorOrigin={{
left: "50%", vertical: "bottom",
transform: "translate(-50%, -50%)", horizontal: "right",
width: 500, }}
bgcolor: "background.paper", transformOrigin={{
boxShadow: 24, vertical: "top",
p: 4, horizontal: "right",
borderRadius: 2, }}
}} >
> <MenuItem
{latestVersion === null ? ( onClick={() => {
<Typography>Checking for updates...</Typography> changeLanguage("en");
) : currentVersion === latestVersion ? ( handleMenuClose();
<Typography variant="h6" textAlign="center"> }}
🎉 You're up to date! >
</Typography> English
) : ( </MenuItem>
<> <MenuItem
<Typography variant="body1" textAlign="left" sx={{ marginLeft: '30px' }}> onClick={() => {
A new version is available: {latestVersion}. Upgrade to the latest version for bug fixes, enhancements and new features! changeLanguage("es");
<br /> handleMenuClose();
View all the new updates }}
<a href="https://github.com/getmaxun/maxun/releases/" target="_blank" style={{ textDecoration: 'none' }}>{' '}here.</a> >
</Typography> Español
<Tabs </MenuItem>
value={tab} <MenuItem
onChange={handleUpdateTabChange} onClick={() => {
sx={{ marginTop: 2, marginBottom: 2 }} changeLanguage("ja");
centered handleMenuClose();
> }}
<Tab label="Manual Setup Upgrade" /> >
<Tab label="Docker Compose Setup Upgrade" />
</Tabs> </MenuItem>
{tab === 0 && ( <MenuItem
<Box sx={{ marginLeft: '30px', background: '#cfd0d1', padding: 1, borderRadius: 3 }}> onClick={() => {
<code style={{ color: 'black' }}> changeLanguage("ar");
<p>Run the commands below</p> handleMenuClose();
# pull latest changes }}
<br /> >
git pull origin master العربية
<br /> </MenuItem>
<br /> <MenuItem
# install dependencies onClick={() => {
<br /> changeLanguage("zh");
npm install handleMenuClose();
<br /> }}
<br /> >
# start maxun
<br /> </MenuItem>
npm run start </Menu></>
</code> )}
</Box>
)}
{tab === 1 && ( </NavBarWrapper>
<Box sx={{ marginLeft: '30px', background: '#cfd0d1', padding: 1, borderRadius: 3 }}>
<code style={{ color: 'black' }}>
<p>Run the commands below</p>
# pull latest docker images
<br />
docker-compose pull
<br />
<br />
# start maxun
<br />
docker-compose up -d
</code>
</Box>
)}
</>
)}
</Box>
</Modal>
<iframe src="https://ghbtns.com/github-btn.html?user=getmaxun&repo=maxun&type=star&count=true&size=large" frameBorder="0" scrolling="0" width="170" height="30" title="GitHub"></iframe>
<IconButton onClick={handleMenuOpen} sx={{
display: 'flex',
alignItems: 'center',
borderRadius: '5px',
padding: '8px',
marginRight: '10px',
'&:hover': { backgroundColor: 'white', color: '#ff00c3' }
}}>
<AccountCircle sx={{ marginRight: '5px' }} />
<Typography variant="body1">{user.email}</Typography>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
PaperProps={{ sx: { width: '180px' } }}
>
<MenuItem onClick={() => { handleMenuClose(); logout(); }}>
<Logout sx={{ marginRight: '5px' }} /> Logout
</MenuItem>
<MenuItem onClick={() => {
window.open('https://discord.gg/5GbPjBUkws', '_blank');
}}>
<DiscordIcon sx={{ marginRight: '5px' }} /> Discord
</MenuItem>
<MenuItem onClick={() => {
window.open('https://www.youtube.com/@MaxunOSS/videos?ref=app', '_blank');
}}>
<YouTube sx={{ marginRight: '5px' }} /> YouTube
</MenuItem>
<MenuItem onClick={() => {
window.open('https://x.com/maxun_io?ref=app', '_blank');
}}>
<X sx={{ marginRight: '5px' }} /> Twiiter (X)
</MenuItem>
</Menu>
</>
) : (
<>
<IconButton onClick={goToMainMenu} sx={{
borderRadius: '5px',
padding: '8px',
background: 'red',
color: 'white',
marginRight: '10px',
'&:hover': { color: 'white', backgroundColor: 'red' }
}}>
<Clear sx={{ marginRight: '5px' }} />
Discard
</IconButton>
<SaveRecording fileName={recordingName} />
</>
)}
</div>
) : ""
}
</NavBarWrapper>
</>
); );
}; };
const NavBarWrapper = styled.div` const NavBarWrapper = styled.div`
grid-area: navbar; grid-area: navbar;
background-color: white; background-color: white;
padding:5px; padding: 5px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;

View File

@@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table'; import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody'; import TableBody from '@mui/material/TableBody';
@@ -19,6 +20,7 @@ import { useNavigate } from 'react-router-dom';
import { stopRecording } from "../../api/recording"; import { stopRecording } from "../../api/recording";
import { GenericModal } from '../atoms/GenericModal'; import { GenericModal } from '../atoms/GenericModal';
/** TODO: /** TODO:
* 1. allow editing existing robot after persisting browser steps * 1. allow editing existing robot after persisting browser steps
*/ */
@@ -31,30 +33,9 @@ interface Column {
format?: (value: string) => string; format?: (value: string) => string;
} }
const columns: readonly Column[] = [
{ id: 'interpret', label: 'Run', minWidth: 80 },
{ id: 'name', label: 'Name', minWidth: 80 },
{
id: 'schedule',
label: 'Schedule',
minWidth: 80,
},
{
id: 'integrate',
label: 'Integrate',
minWidth: 80,
},
{
id: 'settings',
label: 'Settings',
minWidth: 80,
},
{
id: 'options',
label: 'Options',
minWidth: 80,
},
];
interface Data { interface Data {
id: string; id: string;
@@ -76,12 +57,38 @@ interface RecordingsTableProps {
} }
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => { export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
const {t} = useTranslation();
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10); const [rowsPerPage, setRowsPerPage] = React.useState(10);
const [rows, setRows] = React.useState<Data[]>([]); const [rows, setRows] = React.useState<Data[]>([]);
const [isModalOpen, setModalOpen] = React.useState(false); const [isModalOpen, setModalOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState(''); const [searchTerm, setSearchTerm] = React.useState('');
const columns: readonly Column[] = [
{ id: 'interpret', label: t('recordingtable.run'), minWidth: 80 },
{ id: 'name', label: t('recordingtable.name'), minWidth: 80 },
{
id: 'schedule',
label: t('recordingtable.schedule'),
minWidth: 80,
},
{
id: 'integrate',
label: t('recordingtable.integrate'),
minWidth: 80,
},
{
id: 'settings',
label: t('recordingtable.settings'),
minWidth: 80,
},
{
id: 'options',
label: t('recordingtable.options'),
minWidth: 80,
},
];
const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore(); const { notify, setRecordings, browserId, setBrowserId, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -151,16 +158,17 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
row.name.toLowerCase().includes(searchTerm.toLowerCase()) row.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
return ( return (
<React.Fragment> <React.Fragment>
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
My Robots {t('recordingtable.heading')}
</Typography> </Typography>
<Box display="flex" alignItems="center" gap={2}> <Box display="flex" alignItems="center" gap={2}>
<TextField <TextField
size="small" size="small"
placeholder="Search robots..." placeholder={t('recordingtable.search')}
value={searchTerm} value={searchTerm}
onChange={handleSearchChange} onChange={handleSearchChange}
InputProps={{ InputProps={{
@@ -187,7 +195,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
'&:hover': { color: 'white', backgroundColor: '#ff00c3' } '&:hover': { color: 'white', backgroundColor: '#ff00c3' }
}} }}
> >
<Add sx={{ marginRight: '5px' }} /> Create Robot <Add sx={{ marginRight: '5px' }} /> {t('recordingtable.new')}
</IconButton> </IconButton>
</Box> </Box>
</Box> </Box>
@@ -297,9 +305,9 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
/> />
<GenericModal isOpen={isModalOpen} onClose={() => setModalOpen(false)} modalStyle={modalStyle}> <GenericModal isOpen={isModalOpen} onClose={() => setModalOpen(false)} modalStyle={modalStyle}>
<div style={{ padding: '20px' }}> <div style={{ padding: '20px' }}>
<Typography variant="h6" gutterBottom>Enter URL To Extract Data</Typography> <Typography variant="h6" gutterBottom>{t('recordingtable.modal.title')}</Typography>
<TextField <TextField
label="URL" label={t('recordingtable.modal.label')}
variant="outlined" variant="outlined"
fullWidth fullWidth
value={recordingUrl} value={recordingUrl}
@@ -312,7 +320,7 @@ export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handl
onClick={startRecording} onClick={startRecording}
disabled={!recordingUrl} disabled={!recordingUrl}
> >
Start Training Robot {t('recordingtable.modal.button')}
</Button> </Button>
</div> </div>
</GenericModal> </GenericModal>
@@ -397,6 +405,8 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
setAnchorEl(null); setAnchorEl(null);
}; };
const {t} = useTranslation();
return ( return (
<> <>
<IconButton <IconButton
@@ -415,20 +425,23 @@ const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsBut
<ListItemIcon> <ListItemIcon>
<Edit fontSize="small" /> <Edit fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText>Edit</ListItemText> <ListItemText>{t('recordingtable.edit')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => { handleDuplicate(); handleClose(); }}>
<ListItemIcon>
<ContentCopy fontSize="small" />
</ListItemIcon>
<ListItemText>Duplicate</ListItemText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => { handleDelete(); handleClose(); }}> <MenuItem onClick={() => { handleDelete(); handleClose(); }}>
<ListItemIcon> <ListItemIcon>
<DeleteForever fontSize="small" /> <DeleteForever fontSize="small" />
</ListItemIcon> </ListItemIcon>
<ListItemText>Delete</ListItemText> <ListItemText>{t('recordingtable.delete')}</ListItemText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => { handleDuplicate(); handleClose(); }}>
<ListItemIcon>
<ContentCopy fontSize="small" />
</ListItemIcon>
<ListItemText>{t('recordingtable.duplicate')}</ListItemText>
</MenuItem>
</Menu> </Menu>
</> </>
); );

View File

@@ -1,4 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { useEffect, useState } from "react";
import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table'; import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody'; import TableBody from '@mui/material/TableBody';
@@ -7,14 +9,24 @@ import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead'; import TableHead from '@mui/material/TableHead';
import TablePagination from '@mui/material/TablePagination'; import TablePagination from '@mui/material/TablePagination';
import TableRow from '@mui/material/TableRow'; import TableRow from '@mui/material/TableRow';
import { useEffect, useState } from "react"; import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import SearchIcon from '@mui/icons-material/Search';
import { useGlobalInfoStore } from "../../context/globalInfo"; import { useGlobalInfoStore } from "../../context/globalInfo";
import { getStoredRuns } from "../../api/storage"; import { getStoredRuns } from "../../api/storage";
import { RunSettings } from "./RunSettings"; import { RunSettings } from "./RunSettings";
import { CollapsibleRow } from "./ColapsibleRow"; import { CollapsibleRow } from "./ColapsibleRow";
import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextField } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; // Export columns before the component
import SearchIcon from '@mui/icons-material/Search'; export const columns: readonly Column[] = [
{ id: 'runStatus', label: 'Status', minWidth: 80 },
{ id: 'name', label: 'Name', minWidth: 80 },
{ id: 'startedAt', label: 'Started At', minWidth: 80 },
{ id: 'finishedAt', label: 'Finished At', minWidth: 80 },
{ id: 'settings', label: 'Settings', minWidth: 80 },
{ id: 'delete', label: 'Delete', minWidth: 80 },
];
interface Column { interface Column {
id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings'; id: 'runStatus' | 'name' | 'startedAt' | 'finishedAt' | 'delete' | 'settings';
@@ -24,16 +36,7 @@ interface Column {
format?: (value: string) => string; format?: (value: string) => string;
} }
export const columns: readonly Column[] = [ interface Data {
{ id: 'runStatus', label: 'Status', minWidth: 80 },
{ id: 'name', label: 'Robot Name', minWidth: 80 },
{ id: 'startedAt', label: 'Started at', minWidth: 80 },
{ id: 'finishedAt', label: 'Finished at', minWidth: 80 },
{ id: 'settings', label: 'Settings', minWidth: 80 },
{ id: 'delete', label: 'Delete', minWidth: 80 },
];
export interface Data {
id: number; id: number;
status: string; status: string;
name: string; name: string;
@@ -58,15 +61,25 @@ interface RunsTableProps {
runningRecordingName: string; runningRecordingName: string;
} }
export const RunsTable = ( export const RunsTable: React.FC<RunsTableProps> = ({
{ currentInterpretationLog, abortRunHandler, runId, runningRecordingName }: RunsTableProps) => { currentInterpretationLog,
abortRunHandler,
runId,
runningRecordingName
}) => {
const { t } = useTranslation();
// Update column labels using translation if needed
const translatedColumns = columns.map(column => ({
...column,
label: t(`runstable.${column.id}`, column.label)
}));
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10); const [rowsPerPage, setRowsPerPage] = useState(10);
const [rows, setRows] = useState<Data[]>([]); const [rows, setRows] = useState<Data[]>([]);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
const handleChangePage = (event: unknown, newPage: number) => { const handleChangePage = (event: unknown, newPage: number) => {
@@ -86,16 +99,13 @@ export const RunsTable = (
const fetchRuns = async () => { const fetchRuns = async () => {
const runs = await getStoredRuns(); const runs = await getStoredRuns();
if (runs) { if (runs) {
const parsedRows: Data[] = []; const parsedRows: Data[] = runs.map((run: any, index: number) => ({
runs.map((run: any, index) => { id: index,
parsedRows.push({ ...run,
id: index, }));
...run,
});
});
setRows(parsedRows); setRows(parsedRows);
} else { } else {
notify('error', 'No runs found. Please try again.') notify('error', 'No runs found. Please try again.');
} }
}; };
@@ -104,7 +114,7 @@ export const RunsTable = (
fetchRuns(); fetchRuns();
setRerenderRuns(false); setRerenderRuns(false);
} }
}, [rerenderRuns]); }, [rerenderRuns, rows.length, setRerenderRuns]);
const handleDelete = () => { const handleDelete = () => {
setRows([]); setRows([]);
@@ -112,7 +122,6 @@ export const RunsTable = (
fetchRuns(); fetchRuns();
}; };
// Filter rows based on search term // Filter rows based on search term
const filteredRows = rows.filter((row) => const filteredRows = rows.filter((row) =>
row.name.toLowerCase().includes(searchTerm.toLowerCase()) row.name.toLowerCase().includes(searchTerm.toLowerCase())
@@ -120,7 +129,6 @@ export const RunsTable = (
// Group filtered rows by robot meta id // Group filtered rows by robot meta id
const groupedRows = filteredRows.reduce((acc, row) => { const groupedRows = filteredRows.reduce((acc, row) => {
if (!acc[row.robotMetaId]) { if (!acc[row.robotMetaId]) {
acc[row.robotMetaId] = []; acc[row.robotMetaId] = [];
} }
@@ -132,11 +140,11 @@ export const RunsTable = (
<React.Fragment> <React.Fragment>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}> <Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
All Runs {t('runstable.runs', 'Runs')}
</Typography> </Typography>
<TextField <TextField
size="small" size="small"
placeholder="Search runs..." placeholder={t('runstable.search', 'Search runs...')}
value={searchTerm} value={searchTerm}
onChange={handleSearchChange} onChange={handleSearchChange}
InputProps={{ InputProps={{
@@ -149,16 +157,14 @@ export const RunsTable = (
{Object.entries(groupedRows).map(([id, data]) => ( {Object.entries(groupedRows).map(([id, data]) => (
<Accordion key={id}> <Accordion key={id}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}> <AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="h6">{data[data.length - 1].name}</Typography> <Typography variant="h6">{data[data.length - 1].name}</Typography>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Table stickyHeader aria-label="sticky table"> <Table stickyHeader aria-label="sticky table">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell /> <TableCell />
{columns.map((column) => ( {translatedColumns.map((column) => (
<TableCell <TableCell
key={column.id} key={column.id}
align={column.align} align={column.align}
@@ -200,4 +206,4 @@ export const RunsTable = (
/> />
</React.Fragment> </React.Fragment>
); );
}; };

View File

@@ -5,6 +5,9 @@ import Box from '@mui/material/Box';
import { Paper, Button } from "@mui/material"; import { Paper, Button } from "@mui/material";
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material"; import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material";
import { apiUrl } from "../../apiConfig"; import { apiUrl } from "../../apiConfig";
import { useTranslation } from 'react-i18next';
import i18n from '../../i18n';
interface MainMenuProps { interface MainMenuProps {
value: string; value: string;
@@ -12,6 +15,7 @@ interface MainMenuProps {
} }
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => { export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
const {t} = useTranslation();
const handleChange = (event: React.SyntheticEvent, newValue: string) => { const handleChange = (event: React.SyntheticEvent, newValue: string) => {
handleChangeContent(newValue); handleChangeContent(newValue);
@@ -47,7 +51,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium', fontSize: 'medium',
}} }}
value="recordings" value="recordings"
label="Robots" label={t('mainmenu.recordings')}
icon={<AutoAwesome />} icon={<AutoAwesome />}
iconPosition="start" iconPosition="start"
/> />
@@ -58,7 +62,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium', fontSize: 'medium',
}} }}
value="runs" value="runs"
label="Runs" label={t('mainmenu.runs')}
icon={<FormatListBulleted />} icon={<FormatListBulleted />}
iconPosition="start" iconPosition="start"
/> />
@@ -69,7 +73,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium', fontSize: 'medium',
}} }}
value="proxy" value="proxy"
label="Proxy" label={t('mainmenu.proxy')}
icon={<Usb />} icon={<Usb />}
iconPosition="start" iconPosition="start"
/> />
@@ -80,7 +84,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium', fontSize: 'medium',
}} }}
value="apikey" value="apikey"
label="API Key" label={t('mainmenu.apikey')}
icon={<VpnKey />} icon={<VpnKey />}
iconPosition="start" iconPosition="start"
/> />
@@ -88,10 +92,10 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
<hr /> <hr />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1rem', textAlign: 'left' }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '1rem', textAlign: 'left' }}>
<Button href={`${apiUrl}/api-docs/`} target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Code />}> <Button href={`${apiUrl}/api-docs/`} target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Code />}>
Website To API {t('mainmenu.apidocs')}
</Button> </Button>
<Button href="https://forms.gle/hXjgqDvkEhPcaBW76" target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<CloudQueue />}> <Button href="https://forms.gle/hXjgqDvkEhPcaBW76" target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<CloudQueue />}>
Join Maxun Cloud {t('mainmenu.feedback')}
</Button> </Button>
</Box> </Box>
</Box> </Box>

22
src/i18n.ts Normal file
View File

@@ -0,0 +1,22 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: import.meta.env.DEV,
supportedLngs: ['en', 'es', 'ja', 'zh','de'],
interpolation: {
escapeValue: false, // React already escapes
},
backend: {
loadPath: '/locales/{{lng}}.json',
},
});
export default i18n;

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import App from './App'; import App from './App';
import i18n from "./i18n"
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement

View File

@@ -1,134 +1,142 @@
import axios from "axios"; import axios from "axios";
import { useState, useContext, useEffect, FormEvent } from "react"; import { useState, useContext, useEffect, FormEvent } from "react";
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link } from "react-router-dom";
import { AuthContext } from "../context/auth"; import { AuthContext } from "../context/auth";
import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material"; import { Box, Typography, TextField, Button, CircularProgress, Grid } from "@mui/material";
import { useGlobalInfoStore } from "../context/globalInfo"; import { useGlobalInfoStore } from "../context/globalInfo";
import { apiUrl } from "../apiConfig"; import { apiUrl } from "../apiConfig";
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
const Login = () => { const Login = () => {
const [form, setForm] = useState({ const { t } = useTranslation();
email: "", console.log(i18n)
password: "", console.log(t)
}); const [form, setForm] = useState({
const [loading, setLoading] = useState(false); email: "",
const { notify } = useGlobalInfoStore(); password: "",
const { email, password } = form; });
const [loading, setLoading] = useState(false);
const { notify } = useGlobalInfoStore();
const { email, password } = form;
const { state, dispatch } = useContext(AuthContext); const { state, dispatch } = useContext(AuthContext);
const { user } = state; const { user } = state;
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
if (user) { if (user) {
navigate("/"); navigate("/");
} }
}, [user, navigate]); }, [user, navigate]);
const handleChange = (e: any) => { const handleChange = (e: any) => {
const { name, value } = e.target; const { name, value } = e.target;
setForm({ ...form, [name]: value }); setForm({ ...form, [name]: value });
}; };
const submitForm = async (e: any) => { const submitForm = async (e: any) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
try { try {
const { data } = await axios.post(`${apiUrl}/auth/login`, { const { data } = await axios.post(`${apiUrl}/auth/login`, {
email, email,
password, password,
}); });
dispatch({ type: "LOGIN", payload: data }); dispatch({ type: "LOGIN", payload: data });
notify("success", "Welcome to Maxun!"); notify("success", t('login.welcome_notification')); // Translated notification
window.localStorage.setItem("user", JSON.stringify(data)); window.localStorage.setItem("user", JSON.stringify(data));
navigate("/"); navigate("/");
} catch (err) { } catch (err) {
notify("error", "Login Failed. Please try again."); notify("error", t('login.error_notification')); // Translated error
setLoading(false); setLoading(false);
} }
}; };
return ( // Language switcher function
<Box
sx={{
display: "flex", return (
justifyContent: "center", <Box
alignItems: "center", sx={{
maxHeight: "100vh", display: "flex",
mt: 6, justifyContent: "center",
padding: 4, alignItems: "center",
}} maxHeight: "100vh",
> mt: 6,
padding: 4,
<Box }}
component="form" >
onSubmit={submitForm} {/* Language Switcher Buttons */}
sx={{
textAlign: "center", <Box
backgroundColor: "#ffffff", component="form"
padding: 6, onSubmit={submitForm}
borderRadius: 5, sx={{
boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)", textAlign: "center",
display: "flex", backgroundColor: "#ffffff",
flexDirection: "column", padding: 6,
alignItems: "center", borderRadius: 5,
maxWidth: 400, boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)",
width: "100%", display: "flex",
}} flexDirection: "column",
> alignItems: "center",
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} /> maxWidth: 400,
<Typography variant="h4" gutterBottom> width: "100%",
Welcome Back! }}
</Typography> >
<TextField <img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
fullWidth <Typography variant="h4" gutterBottom>
label="Email" {t('login.title')}
name="email" </Typography>
value={email} <TextField
onChange={handleChange} fullWidth
margin="normal" label={t('login.email')}
variant="outlined" name="email"
required value={email}
/> onChange={handleChange}
<TextField margin="normal"
fullWidth variant="outlined"
label="Password" required
name="password" />
type="password" <TextField
value={password} fullWidth
onChange={handleChange} label={t('login.password')}
margin="normal" name="password"
variant="outlined" type="password"
required value={password}
/> onChange={handleChange}
<Button margin="normal"
type="submit" variant="outlined"
fullWidth required
variant="contained" />
color="primary" <Button
sx={{ mt: 2, mb: 2 }} type="submit"
disabled={loading || !email || !password} fullWidth
> variant="contained"
{loading ? ( color="primary"
<> sx={{ mt: 2, mb: 2 }}
<CircularProgress size={20} sx={{ mr: 2 }} /> disabled={loading || !email || !password}
Loading >
</> {loading ? (
) : ( <>
"Login" <CircularProgress size={20} sx={{ mr: 2 }} />
)} {t('login.loading')}
</Button> </>
<Typography variant="body2" align="center"> ) : (
Dont have an account?{" "} t('login.button')
<Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}> )}
Register </Button>
</Link> <Typography variant="body2" align="center">
</Typography> {t('login.register_prompt')}{" "}
</Box> <Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}>
</Box> {t('login.register_link')}
</Link>
); </Typography>
</Box>
</Box>
);
}; };
export default Login; export default Login;

View File

@@ -5,8 +5,13 @@ import { AuthContext } from "../context/auth";
import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material"; import { Box, Typography, TextField, Button, CircularProgress } from "@mui/material";
import { useGlobalInfoStore } from "../context/globalInfo"; import { useGlobalInfoStore } from "../context/globalInfo";
import { apiUrl } from "../apiConfig"; import { apiUrl } from "../apiConfig";
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
const Register = () => { const Register = () => {
const {t} = useTranslation();
const [form, setForm] = useState({ const [form, setForm] = useState({
email: "", email: "",
password: "", password: "",
@@ -40,11 +45,13 @@ const Register = () => {
password, password,
}); });
dispatch({ type: "LOGIN", payload: data }); dispatch({ type: "LOGIN", payload: data });
notify("success", "Registration Successful!"); notify("success", t('register.welcome_notification'));
window.localStorage.setItem("user", JSON.stringify(data)); window.localStorage.setItem("user", JSON.stringify(data));
navigate("/"); navigate("/");
} catch (error:any) { } catch (error:any) {
notify("error", `Registration Failed. Please try again. ${error.response.data}`);
notify("error", error.response.data || t('register.error_notification'));
setLoading(false); setLoading(false);
} }
}; };
@@ -78,11 +85,11 @@ const Register = () => {
> >
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} /> <img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
Create an Account {t('register.title')}
</Typography> </Typography>
<TextField <TextField
fullWidth fullWidth
label="Email" label={t('register.email')}
name="email" name="email"
value={email} value={email}
onChange={handleChange} onChange={handleChange}
@@ -92,7 +99,7 @@ const Register = () => {
/> />
<TextField <TextField
fullWidth fullWidth
label="Password" label={t('register.password')}
name="password" name="password"
type="password" type="password"
value={password} value={password}
@@ -115,13 +122,14 @@ const Register = () => {
Loading Loading
</> </>
) : ( ) : (
"Register" t('register.button')
)} )}
</Button> </Button>
<Typography variant="body2" align="center"> <Typography variant="body2" align="center">
Already have an account?{" "} {t('register.register_prompt')}{" "}
<Link to="/login" style={{ textDecoration: "none", color: "#ff33cc" }}> <Link to="/login" style={{ textDecoration: "none", color: "#ff33cc" }}>
Login
{t('register.login_link')}
</Link> </Link>
</Typography> </Typography>
</Box> </Box>