mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Add FA/EN locale, home activity charts, and theme-aware polish.
Ship shared LocaleProvider, business/customer i18n, branding defaultLocale, curated home tiles with dual 30-day charts, and chart/page aura tokens; refresh PROJECT_CONTEXT. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f5b2193ba1
commit
66004a0fba
@@ -16,6 +16,8 @@ import {
|
||||
sendOtp,
|
||||
verifyOtp,
|
||||
} from '../services/authService'
|
||||
import { LanguageSelect } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import meshkeeLogo from '../assets/meshkee-logo.png'
|
||||
import styles from './LoginPage.module.css'
|
||||
|
||||
@@ -36,6 +38,7 @@ export function LoginPage() {
|
||||
const { login } = useAuth()
|
||||
const { businessName, logoUrl } = useTenantBranding()
|
||||
const tenantDomain = getTenantDomain()
|
||||
const t = useT()
|
||||
|
||||
const [view, setView] = useState<AuthView>('login')
|
||||
const [smsStep, setSmsStep] = useState<SmsStep>('phone')
|
||||
@@ -114,7 +117,7 @@ export function LoginPage() {
|
||||
setSmsStep('code')
|
||||
startCountdown()
|
||||
} catch (err) {
|
||||
handleApiError(err, 'Unable to send verification code.')
|
||||
handleApiError(err, t('login.error.sendCode'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -130,7 +133,7 @@ export function LoginPage() {
|
||||
await login(cellNumber, password)
|
||||
navigate(redirectTo)
|
||||
} catch (err) {
|
||||
handleApiError(err, 'Unable to sign in. Check your connection and try again.')
|
||||
handleApiError(err, t('login.error.signIn'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -141,12 +144,12 @@ export function LoginPage() {
|
||||
clearMessages()
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError('Passwords do not match.')
|
||||
setError(t('signup.error.match'))
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError('Password must be at least 8 characters.')
|
||||
setError(t('signup.error.length'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,7 +183,7 @@ export function LoginPage() {
|
||||
await login(cellNumber, password)
|
||||
navigate(redirectTo)
|
||||
} catch (err) {
|
||||
handleApiError(err, 'Unable to create account.')
|
||||
handleApiError(err, t('signup.error.create'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -191,7 +194,7 @@ export function LoginPage() {
|
||||
clearMessages()
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
setError('Password must be at least 8 characters.')
|
||||
setError(t('forgot.error.length'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,12 +203,10 @@ export function LoginPage() {
|
||||
try {
|
||||
const cellNumber = toE164CellNumber(phone)
|
||||
await verifyOtp(cellNumber, smsCode)
|
||||
setInfo(
|
||||
'Phone number verified. Full password reset via SMS is not available yet — please contact support or sign in if you remember your password.',
|
||||
)
|
||||
setInfo(t('forgot.info.partial'))
|
||||
setTimeout(() => switchView('login'), 2500)
|
||||
} catch (err) {
|
||||
handleApiError(err, 'Unable to verify code.')
|
||||
handleApiError(err, t('forgot.error.verify'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -221,14 +222,14 @@ export function LoginPage() {
|
||||
await verifyOtp(cellNumber, smsCode)
|
||||
|
||||
if (!password) {
|
||||
setError('Enter your account password to complete sign-in after SMS verification.')
|
||||
setError(t('otp.error.password'))
|
||||
return
|
||||
}
|
||||
|
||||
await login(cellNumber, password)
|
||||
navigate(redirectTo)
|
||||
} catch (err) {
|
||||
handleApiError(err, 'Unable to sign in with SMS verification.')
|
||||
handleApiError(err, t('otp.error.signIn'))
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -241,14 +242,17 @@ export function LoginPage() {
|
||||
<img src={logoUrl || meshkeeLogo} alt="" className={styles.logo} />
|
||||
<div className={styles.brandText}>
|
||||
<span className={styles.domain}>{businessName || tenantDomain}</span>
|
||||
<span className={styles.appName}>powered by Meshkee.app</span>
|
||||
<span className={styles.appName}>{t('app.poweredBy')}</span>
|
||||
</div>
|
||||
<div className={styles.langSelect}>
|
||||
<LanguageSelect />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === 'login' && (
|
||||
<>
|
||||
<h1 className={styles.title}>Welcome back</h1>
|
||||
<p className={styles.subtitle}>Sign in with your mobile number</p>
|
||||
<h1 className={styles.title}>{t('login.welcome')}</h1>
|
||||
<p className={styles.subtitle}>{t('login.subtitle')}</p>
|
||||
|
||||
<form className={styles.form} onSubmit={handleLogin}>
|
||||
{error && (
|
||||
@@ -259,7 +263,7 @@ export function LoginPage() {
|
||||
{info && <div className={styles.info}>{info}</div>}
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="login-phone">Mobile number</label>
|
||||
<label htmlFor="login-phone">{t('login.mobile')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Smartphone size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
@@ -276,13 +280,13 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="login-password">Password</label>
|
||||
<label htmlFor="login-password">{t('login.password')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Lock size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="login-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
@@ -293,7 +297,7 @@ export function LoginPage() {
|
||||
type="button"
|
||||
className={styles.togglePassword}
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
aria-label={showPassword ? t('login.hidePassword') : t('login.showPassword')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -308,17 +312,17 @@ export function LoginPage() {
|
||||
onClick={() => switchView('forgot')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Forgot password?
|
||||
{t('login.forgot')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign in'}
|
||||
{isSubmitting ? t('login.signingIn') : t('login.signIn')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className={styles.divider}>
|
||||
<span>or</span>
|
||||
<span>{t('login.or')}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -328,18 +332,18 @@ export function LoginPage() {
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<KeyRound size={18} />
|
||||
One-time login with SMS
|
||||
{t('login.otp')}
|
||||
</button>
|
||||
|
||||
<p className={styles.footerText}>
|
||||
Don't have an account?{' '}
|
||||
{t('login.noAccount')}{' '}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.linkBtn}
|
||||
onClick={() => switchView('signup')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Sign up
|
||||
{t('login.signUp')}
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
@@ -347,8 +351,8 @@ export function LoginPage() {
|
||||
|
||||
{view === 'signup' && (
|
||||
<>
|
||||
<h1 className={styles.title}>Create account</h1>
|
||||
<p className={styles.subtitle}>Register as a customer of {tenantDomain}</p>
|
||||
<h1 className={styles.title}>{t('signup.title')}</h1>
|
||||
<p className={styles.subtitle}>{t('signup.subtitle', { domain: tenantDomain })}</p>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSignup}>
|
||||
{error && (
|
||||
@@ -360,13 +364,13 @@ export function LoginPage() {
|
||||
|
||||
<div className={styles.fieldRow}>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-first">First name</label>
|
||||
<label htmlFor="signup-first">{t('signup.firstName')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<User size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-first"
|
||||
type="text"
|
||||
placeholder="First name"
|
||||
placeholder={t('signup.firstName')}
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
required
|
||||
@@ -376,13 +380,13 @@ export function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-last">Last name</label>
|
||||
<label htmlFor="signup-last">{t('signup.lastName')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<User size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-last"
|
||||
type="text"
|
||||
placeholder="Last name"
|
||||
placeholder={t('signup.lastName')}
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
required
|
||||
@@ -394,7 +398,7 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-phone">Mobile number</label>
|
||||
<label htmlFor="signup-phone">{t('login.mobile')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Smartphone size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
@@ -411,13 +415,13 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-password">Password</label>
|
||||
<label htmlFor="signup-password">{t('login.password')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Lock size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Choose a password"
|
||||
placeholder={t('signup.passwordPlaceholder')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
@@ -428,7 +432,7 @@ export function LoginPage() {
|
||||
type="button"
|
||||
className={styles.togglePassword}
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
aria-label={showPassword ? t('login.hidePassword') : t('login.showPassword')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||
@@ -437,13 +441,13 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="signup-confirm">Confirm password</label>
|
||||
<label htmlFor="signup-confirm">{t('signup.confirm')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Lock size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="signup-confirm"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Repeat your password"
|
||||
placeholder={t('signup.confirmPlaceholder')}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
@@ -454,19 +458,19 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Creating account...' : 'Create account'}
|
||||
{isSubmitting ? t('signup.creating') : t('signup.create')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className={styles.footerText}>
|
||||
Already have an account?{' '}
|
||||
{t('signup.hasAccount')}{' '}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.linkBtn}
|
||||
onClick={() => switchView('login')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Sign in
|
||||
{t('signup.signIn')}
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
@@ -481,14 +485,12 @@ export function LoginPage() {
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<ArrowLeft size={18} />
|
||||
Back to sign in
|
||||
{t('forgot.back')}
|
||||
</button>
|
||||
|
||||
<h1 className={styles.title}>Forgot password</h1>
|
||||
<h1 className={styles.title}>{t('forgot.title')}</h1>
|
||||
<p className={styles.subtitle}>
|
||||
{smsStep === 'phone'
|
||||
? 'We will send a verification code via SMS'
|
||||
: 'Enter the code and your new password'}
|
||||
{smsStep === 'phone' ? t('forgot.subtitlePhone') : t('forgot.subtitleCode')}
|
||||
</p>
|
||||
|
||||
<form className={styles.form} onSubmit={handleResetPassword}>
|
||||
@@ -502,7 +504,7 @@ export function LoginPage() {
|
||||
{smsStep === 'phone' ? (
|
||||
<>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="forgot-phone">Mobile number</label>
|
||||
<label htmlFor="forgot-phone">{t('login.mobile')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Smartphone size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
@@ -524,19 +526,19 @@ export function LoginPage() {
|
||||
onClick={() => void handleSendCode()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Send SMS code'}
|
||||
{isSubmitting ? t('forgot.sending') : t('forgot.sendCode')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{codeSent && (
|
||||
<p className={styles.codeHint}>
|
||||
Verification code sent to <strong>{phone}</strong>
|
||||
{t('common.codeSent', { phone })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="forgot-code">SMS verification code</label>
|
||||
<label htmlFor="forgot-code">{t('forgot.code')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<KeyRound size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
@@ -554,13 +556,13 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="forgot-new-password">New password</label>
|
||||
<label htmlFor="forgot-new-password">{t('forgot.newPassword')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Lock size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="forgot-new-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter new password"
|
||||
placeholder={t('forgot.newPasswordPlaceholder')}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
@@ -572,7 +574,7 @@ export function LoginPage() {
|
||||
|
||||
<div className={styles.resendRow}>
|
||||
{countdown > 0 ? (
|
||||
<span className={styles.countdown}>Resend code in {countdown}s</span>
|
||||
<span className={styles.countdown}>{t('common.resendIn', { seconds: countdown })}</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
@@ -580,13 +582,13 @@ export function LoginPage() {
|
||||
onClick={() => void handleSendCode()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Resend SMS code
|
||||
{t('common.resend')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Verifying...' : 'Reset password'}
|
||||
{isSubmitting ? t('forgot.verifying') : t('forgot.reset')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -603,14 +605,12 @@ export function LoginPage() {
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<ArrowLeft size={18} />
|
||||
Back to sign in
|
||||
{t('otp.back')}
|
||||
</button>
|
||||
|
||||
<h1 className={styles.title}>One-time login</h1>
|
||||
<h1 className={styles.title}>{t('otp.title')}</h1>
|
||||
<p className={styles.subtitle}>
|
||||
{smsStep === 'phone'
|
||||
? 'Verify your mobile number with a one-time SMS code'
|
||||
: 'Enter the SMS code and your password'}
|
||||
{smsStep === 'phone' ? t('otp.subtitlePhone') : t('otp.subtitleCode')}
|
||||
</p>
|
||||
|
||||
<form className={styles.form} onSubmit={handleOtpLogin}>
|
||||
@@ -623,7 +623,7 @@ export function LoginPage() {
|
||||
{smsStep === 'phone' ? (
|
||||
<>
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="otp-phone">Mobile number</label>
|
||||
<label htmlFor="otp-phone">{t('login.mobile')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Smartphone size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
@@ -645,19 +645,19 @@ export function LoginPage() {
|
||||
onClick={() => void handleSendCode()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Send SMS code'}
|
||||
{isSubmitting ? t('forgot.sending') : t('forgot.sendCode')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{codeSent && (
|
||||
<p className={styles.codeHint}>
|
||||
Verification code sent to <strong>{phone}</strong>
|
||||
{t('common.codeSent', { phone })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="otp-code">SMS verification code</label>
|
||||
<label htmlFor="otp-code">{t('otp.code')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<KeyRound size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
@@ -675,13 +675,13 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className={styles.field}>
|
||||
<label htmlFor="otp-password">Password</label>
|
||||
<label htmlFor="otp-password">{t('otp.password')}</label>
|
||||
<div className={styles.inputWrap}>
|
||||
<Lock size={18} className={styles.inputIcon} />
|
||||
<input
|
||||
id="otp-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Your account password"
|
||||
placeholder={t('otp.passwordPlaceholder')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
@@ -693,7 +693,7 @@ export function LoginPage() {
|
||||
|
||||
<div className={styles.resendRow}>
|
||||
{countdown > 0 ? (
|
||||
<span className={styles.countdown}>Resend code in {countdown}s</span>
|
||||
<span className={styles.countdown}>{t('common.resendIn', { seconds: countdown })}</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
@@ -701,13 +701,13 @@ export function LoginPage() {
|
||||
onClick={() => void handleSendCode()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Resend SMS code
|
||||
{t('common.resend')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="submit" className={styles.submitBtn} disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign in'}
|
||||
{isSubmitting ? t('login.signingIn') : t('login.signIn')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user