initialisation on the doormile express console with astryx
This commit is contained in:
@@ -1,45 +1,90 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { enqueueSnackbar, closeSnackbar } from 'notistack';
|
||||
import AnimateButton from 'components/@extended/AnimateButton';
|
||||
import OtpInput from 'react18-input-otp';
|
||||
|
||||
import { Box, Card, CardContent, Stack, TextField, Button, Typography, Link, FormLabel, IconButton, InputAdornment } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Loader from 'components/Loader';
|
||||
import logo from 'assets/images/logo-nearle1.png';
|
||||
import expressImage from 'assets/images/express.png';
|
||||
import logo from 'assets/images/doormile-logo.png';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { OpenToast } from 'components/third-party/OpenToast';
|
||||
import { closeGlobalToast, GlobalToast } from 'components/nearle_components/GlobalToast';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import { setLoginUser } from 'store/reducers/loginUserSlice';
|
||||
import { markSessionStart } from 'utils/session';
|
||||
import { DT } from 'themes/dt/tokens';
|
||||
|
||||
// Astryx design system — see themes/astryx.js for the Doormile brand theme
|
||||
// and CLAUDE.md's <!-- ASTRYX:START --> block for the CLI workflow.
|
||||
// NOTE: custom CSS (xstyle/stylex.create()) isn't wired up yet — see the
|
||||
// comment in config-overrides.js. Everything below uses Astryx component
|
||||
// props only; the brand gradient panel and the two logo images are plain
|
||||
// native elements with inline `style` for that reason.
|
||||
import { AppShell } from '@astryxdesign/core/AppShell';
|
||||
import { Theme } from '@astryxdesign/core/theme';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { Center } from '@astryxdesign/core/Center';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { Heading } from '@astryxdesign/core/Heading';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { TextInput } from '@astryxdesign/core/TextInput';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { Link } from '@astryxdesign/core/Link';
|
||||
import { doormileTheme } from 'themes/astryx';
|
||||
|
||||
const brandPanelStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
width: '46%',
|
||||
height: '100vh',
|
||||
color: '#fff',
|
||||
padding: 48,
|
||||
background: `linear-gradient(150deg, ${DT.brand} 0%, #D25463 100%)`
|
||||
};
|
||||
|
||||
const logoLockupStyle = { position: 'absolute', top: 48, left: 48, maxHeight: 60 };
|
||||
|
||||
const bulletDotStyle = {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.18)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
flexShrink: 0
|
||||
};
|
||||
|
||||
// doormile-logo.png is a white asset; recolour to brand red for this
|
||||
// white-background card (the brand-panel logo stays white as-is).
|
||||
const formLogoStyle = {
|
||||
maxHeight: 48,
|
||||
filter: 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)'
|
||||
};
|
||||
|
||||
const BULLETS = ['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'];
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const fcmtoken = useSelector((state) => state.fcm);
|
||||
const permission = useSelector((state) => state.fcm.permission);
|
||||
const theme = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
let navigate = useNavigate();
|
||||
const [otp, setOtp] = useState('');
|
||||
const [currentotp, setCurrentotp] = useState('');
|
||||
const [userinfo, setUserinfo] = useState({});
|
||||
const [username, setUsername] = useState('');
|
||||
const [passwordStatus, setPasswordStatus] = useState(0);
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isPassword, setIspassword] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [userid, setUserid] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('firstname')) {
|
||||
navigate('/nearle/dispatch');
|
||||
navigate('/doormile/dispatch');
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -100,7 +145,7 @@ const Login = () => {
|
||||
localStorage.setItem('userfcmtoken', userinfo.userfcmtoken);
|
||||
markSessionStart();
|
||||
fetchAppLocations(userinfo.userid);
|
||||
navigate('/nearle/dispatch');
|
||||
navigate('/doormile/dispatch');
|
||||
} else {
|
||||
OpenToast(res.data.message, 'error', 3000);
|
||||
}
|
||||
@@ -123,7 +168,7 @@ const Login = () => {
|
||||
markSessionStart();
|
||||
closeGlobalToast(); // to close the pin snackbar
|
||||
|
||||
navigate('/nearle/dispatch');
|
||||
navigate('/doormile/dispatch');
|
||||
};
|
||||
|
||||
const opentoast = (message) => {
|
||||
@@ -152,320 +197,157 @@ const Login = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (passwordStatus == 0) {
|
||||
loginsend();
|
||||
} else if (passwordStatus == 1) {
|
||||
if (!password || !confirmPassword || password != confirmPassword) {
|
||||
OpenToast('Check Password', 'warning', 3000);
|
||||
} else {
|
||||
updateUser();
|
||||
}
|
||||
} else if (passwordStatus == 2) {
|
||||
if (!password) {
|
||||
OpenToast('Invalid Password', 'warning', 3000);
|
||||
}
|
||||
loginsend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', display: 'flex', bgcolor: '#f8fafc' }}>
|
||||
{loading && <Loader />}
|
||||
<Theme theme={doormileTheme} mode="light">
|
||||
<AppShell contentPadding={0}>
|
||||
{loading && <Loader />}
|
||||
<HStack gap={0} height="100vh" wrap="nowrap">
|
||||
{/* ---- Left brand panel (plain element: see the note at the top of
|
||||
this file about custom styling not going through Astryx yet) ---- */}
|
||||
<div style={brandPanelStyle}>
|
||||
<img src={logo} alt="Doormile" style={logoLockupStyle} />
|
||||
|
||||
{/* ---- Left brand panel (hidden on small screens) ---- */}
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
flexBasis: '46%',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
p: 6,
|
||||
background: 'linear-gradient(150deg, #4D1C61 0%, #662582 52%, #9255AB 100%)'
|
||||
}}
|
||||
>
|
||||
{/* Logo at the top-left corner */}
|
||||
<img
|
||||
src={expressImage}
|
||||
alt="Operate your dispatch"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 48,
|
||||
left: 48,
|
||||
maxHeight: 60
|
||||
}}
|
||||
/>
|
||||
<VStack gap={2} maxWidth={430}>
|
||||
<Heading level={1} color="inherit">
|
||||
Operate your dispatch, end to end.
|
||||
</Heading>
|
||||
<Text type="large" color="inherit">
|
||||
Orders, AI route optimisation, live rider tracking and billing — all in the Doormile Express operator console.
|
||||
</Text>
|
||||
|
||||
{/* decorative light glows */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -120,
|
||||
right: -80,
|
||||
width: 360,
|
||||
height: 360,
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 70%)'
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -150,
|
||||
left: -110,
|
||||
width: 440,
|
||||
height: 440,
|
||||
borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0) 70%)'
|
||||
}}
|
||||
/>
|
||||
<VStack gap={1.5} padding={0}>
|
||||
{BULLETS.map((t) => (
|
||||
<HStack key={t} gap={1.25} vAlign="center">
|
||||
<span style={bulletDotStyle}>✓</span>
|
||||
<Text color="inherit">{t}</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</div>
|
||||
|
||||
<Box sx={{ position: 'relative', maxWidth: 430 }}>
|
||||
<Typography sx={{ fontSize: 40, fontWeight: 700, lineHeight: 1.18, letterSpacing: '-0.02em', mb: 2 }}>
|
||||
Operate your dispatch,
|
||||
<br />
|
||||
end to end.
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: 17.5, color: 'rgba(255,255,255,0.85)', mb: 4, lineHeight: 1.6 }}>
|
||||
Orders, AI route optimisation, live rider tracking and billing — all in the NearlExpress operator console.
|
||||
</Typography>
|
||||
{/* ---- Right form panel ---- */}
|
||||
<Center axis="both" width="54%" height="100vh">
|
||||
<VStack width="100%" maxWidth={420} gap={3} padding={3}>
|
||||
<Card padding={4} elevation="low">
|
||||
<VStack gap={0.5} hAlign="center" padding={0}>
|
||||
<img src={logo} alt="Doormile" style={formLogoStyle} />
|
||||
<Heading level={2}>Welcome back</Heading>
|
||||
<Text type="supporting">Sign in to the Doormile Express console</Text>
|
||||
</VStack>
|
||||
|
||||
<Stack spacing={1.5} sx={{ display: 'inline-flex', textAlign: 'left' }}>
|
||||
{['Real-time fleet visibility', 'AI-optimised dispatch routes', 'Tenant, pricing & invoice control'].map((t) => (
|
||||
<Stack key={t} direction="row" spacing={1.25} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'rgba(255,255,255,0.18)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 13,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: 16, color: 'rgba(255,255,255,0.9)' }}>{t}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ---- Right form panel ---- */}
|
||||
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: { xs: 2.5, sm: 4 } }}>
|
||||
<Box sx={{ width: '100%', maxWidth: 420 }}>
|
||||
<Card
|
||||
sx={{
|
||||
width: '100%',
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||
p: { xs: 2.5, sm: 4 }
|
||||
}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<Stack alignItems="center" mb={2.5}>
|
||||
<img src={logo} alt="loginpagelogo" style={{ maxHeight: 48 }} />
|
||||
</Stack>
|
||||
|
||||
{/* Title */}
|
||||
<Typography variant="h3" textAlign="center" sx={{ fontWeight: 700, mb: 0.5 }}>
|
||||
Welcome back
|
||||
</Typography>
|
||||
<Typography variant="body2" textAlign="center" sx={{ color: '#64748b', mb: 3 }}>
|
||||
Sign in to the NearlExpress console
|
||||
</Typography>
|
||||
|
||||
<CardContent sx={{ p: 0 }}>
|
||||
<form
|
||||
noValidate
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (passwordStatus == 0) {
|
||||
loginsend();
|
||||
} else if (passwordStatus == 1) {
|
||||
if (!password || !confirmPassword || password != confirmPassword) {
|
||||
OpenToast('Check Password', 'warning', 3000);
|
||||
} else {
|
||||
updateUser();
|
||||
}
|
||||
} else if (passwordStatus == 2) {
|
||||
if (!password) {
|
||||
OpenToast('Invalid Password', 'warning', 3000);
|
||||
}
|
||||
loginsend();
|
||||
}
|
||||
// if (currentotp) {
|
||||
// if (currentotp == otp) {
|
||||
// loginsuccessful();
|
||||
// fetchAppLocations();
|
||||
// } else {
|
||||
// opentoast('Invalid pin');
|
||||
// }
|
||||
// }
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
{/* Email */}
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="E-mail Address"
|
||||
variant="outlined"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value.toLocaleLowerCase())}
|
||||
InputProps={{ readOnly: passwordStatus }}
|
||||
/>
|
||||
{/* Setup Password */}
|
||||
{passwordStatus == 1 && (
|
||||
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
|
||||
<Typography variant="h4" textAlign="start" mb={3}>
|
||||
Setup Password
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Enter New Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<VStack gap={3} padding={0}>
|
||||
<TextInput
|
||||
hasAutoFocus
|
||||
label="E-mail Address"
|
||||
type="email"
|
||||
isRequired
|
||||
value={username}
|
||||
onChange={(value) => setUsername(value.toLocaleLowerCase())}
|
||||
isDisabled={!!passwordStatus}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
error={confirmPassword !== '' && password !== confirmPassword}
|
||||
fullWidth
|
||||
label="Re-Enter Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowConfirmPassword((prev) => !prev)} edge="end">
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{/* Enter Password */}
|
||||
{passwordStatus == 2 && (
|
||||
<Stack display={'flex'} flexDirection={'column'} spacing={3}>
|
||||
<Typography variant="h4" textAlign="start" mb={3}>
|
||||
Enter Password
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Enter Password"
|
||||
variant="outlined"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword((prev) => !prev)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{/* Setup Password */}
|
||||
{passwordStatus == 1 && (
|
||||
<VStack gap={3} padding={0}>
|
||||
<Heading level={4}>Setup Password</Heading>
|
||||
<TextInput
|
||||
hasAutoFocus
|
||||
label="Enter New Password"
|
||||
type="password"
|
||||
isRequired
|
||||
value={password}
|
||||
onChange={(value) => setPassword(value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Re-Enter Password"
|
||||
type="password"
|
||||
isRequired
|
||||
value={confirmPassword}
|
||||
onChange={(value) => setConfirmPassword(value)}
|
||||
status={
|
||||
confirmPassword !== '' && password !== confirmPassword
|
||||
? { type: 'error', message: 'Passwords do not match' }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* OTP */}
|
||||
{isPassword && (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<FormLabel>Enter Password</FormLabel>
|
||||
<Link
|
||||
variant="body2"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setOtp('');
|
||||
loginsend();
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Link>
|
||||
</Stack>
|
||||
{/* Enter Password */}
|
||||
{passwordStatus == 2 && (
|
||||
<VStack gap={3} padding={0}>
|
||||
<Heading level={4}>Enter Password</Heading>
|
||||
<TextInput
|
||||
hasAutoFocus
|
||||
label="Enter Password"
|
||||
type="password"
|
||||
isRequired
|
||||
value={password}
|
||||
onChange={(value) => setPassword(value)}
|
||||
/>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* <OtpInput
|
||||
shouldAutoFocus
|
||||
value={otp}
|
||||
onChange={(otp) => setOtp(otp)}
|
||||
numInputs={4}
|
||||
containerStyle={{ justifyContent: 'space-between' }}
|
||||
inputStyle={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${borderColor}`,
|
||||
fontSize: 18
|
||||
}}
|
||||
focusStyle={{
|
||||
outline: 'none',
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
boxShadow: theme.customShadows.primary
|
||||
}}
|
||||
/> */}
|
||||
<TextField type="passowrd" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</Stack>
|
||||
)}
|
||||
{/* Submit */}
|
||||
<AnimateButton>
|
||||
<Button fullWidth size="large" type="submit" variant="contained" color="primary">
|
||||
Continue
|
||||
</Button>
|
||||
</AnimateButton>
|
||||
</Stack>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* OTP / retry */}
|
||||
{isPassword && (
|
||||
<VStack gap={1.5} padding={0}>
|
||||
<HStack justify="between">
|
||||
<Text type="label">Enter Password</Text>
|
||||
<Link
|
||||
onClick={() => {
|
||||
setOtp('');
|
||||
loginsend();
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Link>
|
||||
</HStack>
|
||||
<TextInput label="Password" type="password" value={password} onChange={(value) => setPassword(value)} isLabelHidden />
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* footer */}
|
||||
<Stack direction="row" justifyContent="center" alignItems="center" flexWrap="wrap" useFlexGap spacing={2} sx={{ mt: 3 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component={Link}
|
||||
href="https://nearle.in"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
>
|
||||
© All rights reserved
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component={Link}
|
||||
href="https://nearle.in/terms"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
>
|
||||
Terms and Conditions
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component={Link}
|
||||
href="https://nearle.in/privacy"
|
||||
target="_blank"
|
||||
sx={{ color: '#94a3b8', textDecoration: 'none', fontWeight: 600, '&:hover': { color: '#662582' } }}
|
||||
>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button label="Continue" type="submit" variant="primary" size="lg" width="100%" />
|
||||
</VStack>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{/* footer */}
|
||||
<HStack justify="center" wrap="wrap" gap={2}>
|
||||
<Link href="https://nearle.in" target="_blank" isExternalLink>
|
||||
© All rights reserved
|
||||
</Link>
|
||||
<Link href="https://nearle.in/terms" target="_blank" isExternalLink>
|
||||
Terms and Conditions
|
||||
</Link>
|
||||
<Link href="https://nearle.in/privacy" target="_blank" isExternalLink>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Center>
|
||||
</HStack>
|
||||
</AppShell>
|
||||
</Theme>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user