This commit is contained in:
AmitChauhan63390
2024-12-07 22:20:17 +05:30
parent 36ebff45da
commit 5684243215
11 changed files with 508 additions and 228 deletions

View File

@@ -36,6 +36,9 @@
"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",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"joi": "^17.6.0", "joi": "^17.6.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@@ -56,6 +59,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",
@@ -110,4 +114,4 @@
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"vite": "^5.4.10" "vite": "^5.4.10"
} }
} }

0
public/locales/ar.json Normal file
View File

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

@@ -0,0 +1,13 @@
{
"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."
}
}

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"
}
}

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

@@ -0,0 +1,13 @@
{
"app": {
"name": "Maxun",
"version": "beta"
},
"login": {
"title": "おかえりなさい!",
"email": "メールアドレス",
"password": "パスワード",
"button": "ログイン",
"register_prompt": "アカウントをお持ちでない方は、新規登録"
}
}

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,134 +1,322 @@
import React, { useState, useContext } from 'react'; import React, { useState, useContext } 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, Avatar, Chip, } from "@mui/material"; import { IconButton, Menu, MenuItem, Typography, Chip } from "@mui/material";
import { AccountCircle, Logout, Clear } from "@mui/icons-material"; import { AccountCircle, Logout, Clear } 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";
import DiscordIcon from '../atoms/DiscordIcon'; import DiscordIcon from "../atoms/DiscordIcon";
import { apiUrl } from '../../apiConfig'; import { apiUrl } from "../../apiConfig";
import MaxunLogo from "../../assets/maxunlogo.png"; import MaxunLogo from "../../assets/maxunlogo.png";
import { useTranslation } from "react-i18next"; // Import useTranslation hook
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 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
}; };
return ( return (
<NavBarWrapper> <NavBarWrapper>
<div style={{ <div
display: 'flex', style={{
justifyContent: 'flex-start', display: "flex",
}}> justifyContent: "flex-start",
<img src={MaxunLogo} width={45} height={40} style={{ borderRadius: '5px', margin: '5px 0px 5px 15px' }} /> }}
<div style={{ padding: '11px' }}><ProjectName>Maxun</ProjectName></div> >
<Chip label="beta" color="primary" variant="outlined" sx={{ marginTop: '10px' }} /> <img
src={MaxunLogo}
width={45}
height={40}
style={{ borderRadius: "5px", margin: "5px 0px 5px 15px" }}
/>
<div style={{ padding: "11px" }}>
<ProjectName>Maxun</ProjectName>
</div>
<Chip
label="beta"
color="primary"
variant="outlined"
sx={{ marginTop: "10px" }}
/>
</div> </div>
{ {user ? (
user ? ( <div
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }}> style={{
{!isRecording ? ( display: "flex",
<> alignItems: "center",
<IconButton justifyContent: "flex-end",
component="a" }}
href="https://discord.gg/5GbPjBUkws" >
target="_blank" {!isRecording ? (
rel="noopener noreferrer" <>
sx={{ <IconButton
display: 'flex', component="a"
alignItems: 'center', href="https://discord.gg/5GbPjBUkws"
borderRadius: '5px', target="_blank"
padding: '8px', rel="noopener noreferrer"
marginRight: '30px', 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();
}} }}
> >
<DiscordIcon sx={{ marginRight: '5px' }} /> <Logout sx={{ marginRight: "5px" }} /> {t("logout")}
</IconButton> </MenuItem>
<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> </Menu>
<IconButton onClick={handleMenuOpen} sx={{ {/* Language dropdown */}
display: 'flex', </>
alignItems: 'center', ) : (
borderRadius: '5px', <>
padding: '8px', <IconButton
marginRight: '10px', onClick={goToMainMenu}
'&:hover': { backgroundColor: 'white', color: '#ff00c3' } sx={{
}}> borderRadius: "5px",
<AccountCircle sx={{ marginRight: '5px' }} /> padding: "8px",
<Typography variant="body1">{user.email}</Typography> background: "red",
</IconButton> color: "white",
<Menu marginRight: "10px",
anchorEl={anchorEl} "&:hover": { color: "white", backgroundColor: "red" },
open={Boolean(anchorEl)} }}
onClose={handleMenuClose} >
anchorOrigin={{ <Clear sx={{ marginRight: "5px" }} />
vertical: 'bottom', {t("discard")}
horizontal: 'right', </IconButton>
}} <SaveRecording fileName={recordingName} />
transformOrigin={{ </>
vertical: 'top', )}
horizontal: 'right', <IconButton
}} onClick={handleLangMenuOpen}
> sx={{
<MenuItem onClick={() => { handleMenuClose(); logout(); }}> display: "flex",
<Logout sx={{ marginRight: '5px' }} /> Logout alignItems: "center",
</MenuItem> borderRadius: "5px",
</Menu> padding: "8px",
</> marginRight: "10px",
) : ( }}
<> >
<IconButton onClick={goToMainMenu} sx={{ <Typography variant="body1">{t("language")}</Typography>
borderRadius: '5px', </IconButton>
padding: '8px', <Menu
background: 'red', anchorEl={langAnchorEl}
color: 'white', open={Boolean(langAnchorEl)}
marginRight: '10px', onClose={handleMenuClose}
'&:hover': { color: 'white', backgroundColor: 'red' } anchorOrigin={{
}}> vertical: "bottom",
<Clear sx={{ marginRight: '5px' }} /> horizontal: "right",
Discard }}
</IconButton> transformOrigin={{
<SaveRecording fileName={recordingName} /> vertical: "top",
</> horizontal: "right",
)} }}
</div> >
) : "" <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>
</Menu>
</div>
) : (
<><IconButton
onClick={handleLangMenuOpen}
sx={{
display: "flex",
alignItems: "center",
borderRadius: "5px",
padding: "8px",
marginRight: "10px",
}}
>
<Typography variant="body1">{t("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>
</Menu></>
)}
</NavBarWrapper> </NavBarWrapper>
); );
}; };
@@ -136,7 +324,7 @@ export const NavBar: React.FC<NavBarProps> = ({ recordingName, isRecording }) =>
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;

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', 'ar'],
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,144 @@
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'; // Add this import
const Login = () => { const Login = () => {
const [form, setForm] = useState({ const { t } = useTranslation();
email: "", console.log(i18n) // Add translation hook
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 const changeLanguage = (lng: string) => {
sx={{ i18n.changeLanguage(lng);
display: "flex", };
justifyContent: "center",
alignItems: "center", return (
maxHeight: "100vh", <Box
mt: 6, sx={{
padding: 4, display: "flex",
}} justifyContent: "center",
> alignItems: "center",
maxHeight: "100vh",
<Box mt: 6,
component="form" padding: 4,
onSubmit={submitForm} }}
sx={{ >
textAlign: "center", {/* Language Switcher Buttons */}
backgroundColor: "#ffffff",
padding: 6, <Box
borderRadius: 5, component="form"
boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)", onSubmit={submitForm}
display: "flex", sx={{
flexDirection: "column", textAlign: "center",
alignItems: "center", backgroundColor: "#ffffff",
maxWidth: 400, padding: 6,
width: "100%", borderRadius: 5,
}} boxShadow: "0px 20px 40px rgba(0, 0, 0, 0.2), 0px -5px 10px rgba(0, 0, 0, 0.15)",
> display: "flex",
<img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} /> flexDirection: "column",
<Typography variant="h4" gutterBottom> alignItems: "center",
Welcome Back! maxWidth: 400,
</Typography> width: "100%",
<TextField }}
fullWidth >
label="Email" <img src="../src/assets/maxunlogo.png" alt="logo" height={55} width={60} style={{ marginBottom: 20, borderRadius: "20%", alignItems: "center" }} />
name="email" <Typography variant="h4" gutterBottom>
value={email} {t('login.title')}
onChange={handleChange} </Typography>
margin="normal" <TextField
variant="outlined" fullWidth
required label={t('login.email')}
/> name="email"
<TextField value={email}
fullWidth onChange={handleChange}
label="Password" margin="normal"
name="password" variant="outlined"
type="password" required
value={password} />
onChange={handleChange} <TextField
margin="normal" fullWidth
variant="outlined" label={t('login.password')}
required name="password"
/> type="password"
<Button value={password}
type="submit" onChange={handleChange}
fullWidth margin="normal"
variant="contained" variant="outlined"
color="primary" required
sx={{ mt: 2, mb: 2 }} />
disabled={loading || !email || !password} <Button
> type="submit"
{loading ? ( fullWidth
<> variant="contained"
<CircularProgress size={20} sx={{ mr: 2 }} /> color="primary"
Loading sx={{ mt: 2, mb: 2 }}
</> disabled={loading || !email || !password}
) : ( >
"Login" {loading ? (
)} <>
</Button> <CircularProgress size={20} sx={{ mr: 2 }} />
<Typography variant="body2" align="center"> {t('login.loading')}
Dont have an account?{" "} </>
<Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}> ) : (
Register t('login.button')
</Link> )}
</Typography> </Button>
</Box> <Typography variant="body2" align="center">
</Box> {t('login.register_prompt')}{" "}
<Link to="/register" style={{ textDecoration: "none", color: "#ff33cc" }}>
); {t('login.register_link')}
</Link>
</Typography>
</Box>
</Box>
);
}; };
export default Login; export default Login;