Merge commit 'refs/pull/242/head' of https://github.com/getmaxun/maxun into internationalization2
This commit is contained in:
@@ -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
50
public/locales/de.json
Normal 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
66
public/locales/en.json
Normal 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
13
public/locales/es.json
Normal 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
61
public/locales/ja.json
Normal 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
13
public/locales/zh.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Maxun",
|
||||||
|
"version": "beta"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "欢迎回来!",
|
||||||
|
"email": "电子邮件",
|
||||||
|
"password": "密码",
|
||||||
|
"button": "登录",
|
||||||
|
"register_prompt": "没有账号?注册"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/App.tsx
29
src/App.tsx
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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
22
src/i18n.ts
Normal 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;
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
) : (
|
||||||
Don’t 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;
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user