add password toggle icon for all password inputs & use share folder checkbox for remember password in login page.
This commit is contained in:
7
backend/prisma/migrations/20260710150528/migration.sql
Normal file
7
backend/prisma/migrations/20260710150528/migration.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "lab_case_tasks" ALTER COLUMN "updatedAt" DROP DEFAULT,
|
||||||
|
ALTER COLUMN "prosthesisTypeCode" DROP DEFAULT,
|
||||||
|
ALTER COLUMN "workflowStepCode" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- RenameIndex
|
||||||
|
ALTER INDEX "lab_case_tasks_labCaseId_treatmentDetailId_prosthesisTypeCode_s" RENAME TO "lab_case_tasks_labCaseId_treatmentDetailId_prosthesisTypeCo_key";
|
||||||
@@ -77,6 +77,8 @@
|
|||||||
"confirmPassword": "Confirm password",
|
"confirmPassword": "Confirm password",
|
||||||
"fullName": "Full name",
|
"fullName": "Full name",
|
||||||
"rememberMe": "Remember me",
|
"rememberMe": "Remember me",
|
||||||
|
"showPassword": "Show password",
|
||||||
|
"hidePassword": "Hide password",
|
||||||
"forgotPassword": "Forgot your password?",
|
"forgotPassword": "Forgot your password?",
|
||||||
"invalidCredentials": "Invalid email or password",
|
"invalidCredentials": "Invalid email or password",
|
||||||
"loginFailed": "Login failed",
|
"loginFailed": "Login failed",
|
||||||
|
|||||||
@@ -77,6 +77,8 @@
|
|||||||
"confirmPassword": "تأیید رمز عبور",
|
"confirmPassword": "تأیید رمز عبور",
|
||||||
"fullName": "نام و نام خانوادگی",
|
"fullName": "نام و نام خانوادگی",
|
||||||
"rememberMe": "مرا به خاطر بسپار",
|
"rememberMe": "مرا به خاطر بسپار",
|
||||||
|
"showPassword": "نمایش رمز عبور",
|
||||||
|
"hidePassword": "پنهان کردن رمز عبور",
|
||||||
"forgotPassword": "رمز عبور خود را فراموش کردهاید؟",
|
"forgotPassword": "رمز عبور خود را فراموش کردهاید؟",
|
||||||
"invalidCredentials": "ایمیل یا رمز عبور نامعتبر است",
|
"invalidCredentials": "ایمیل یا رمز عبور نامعتبر است",
|
||||||
"loginFailed": "ورود ناموفق بود",
|
"loginFailed": "ورود ناموفق بود",
|
||||||
|
|||||||
@@ -77,6 +77,8 @@
|
|||||||
"confirmPassword": "Bevestig wachtwoord",
|
"confirmPassword": "Bevestig wachtwoord",
|
||||||
"fullName": "Volledige naam",
|
"fullName": "Volledige naam",
|
||||||
"rememberMe": "Onthoud mij",
|
"rememberMe": "Onthoud mij",
|
||||||
|
"showPassword": "Wachtwoord tonen",
|
||||||
|
"hidePassword": "Wachtwoord verbergen",
|
||||||
"forgotPassword": "Wachtwoord vergeten?",
|
"forgotPassword": "Wachtwoord vergeten?",
|
||||||
"invalidCredentials": "Ongeldig e-mailadres of wachtwoord",
|
"invalidCredentials": "Ongeldig e-mailadres of wachtwoord",
|
||||||
"loginFailed": "Inloggen mislukt",
|
"loginFailed": "Inloggen mislukt",
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ const en = {
|
|||||||
confirmPassword: 'Confirm password',
|
confirmPassword: 'Confirm password',
|
||||||
fullName: 'Full name',
|
fullName: 'Full name',
|
||||||
rememberMe: 'Remember me',
|
rememberMe: 'Remember me',
|
||||||
|
showPassword: 'Show password',
|
||||||
|
hidePassword: 'Hide password',
|
||||||
forgotPassword: 'Forgot your password?',
|
forgotPassword: 'Forgot your password?',
|
||||||
invalidCredentials: 'Invalid email or password',
|
invalidCredentials: 'Invalid email or password',
|
||||||
loginFailed: 'Login failed',
|
loginFailed: 'Login failed',
|
||||||
|
|||||||
@@ -103,6 +103,11 @@ export default function AccountSettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const passwordToggleLabels = {
|
||||||
|
show: tAuth('showPassword'),
|
||||||
|
hide: tAuth('hidePassword'),
|
||||||
|
};
|
||||||
|
|
||||||
if (!isAuthReady || !user) {
|
if (!isAuthReady || !user) {
|
||||||
return (
|
return (
|
||||||
<p className="text-text-secondary text-sm">{tCommon('loadingEllipsis')}</p>
|
<p className="text-text-secondary text-sm">{tCommon('loadingEllipsis')}</p>
|
||||||
@@ -139,6 +144,7 @@ export default function AccountSettingsPage() {
|
|||||||
placeholder={tAuth('passwordPlaceholder')}
|
placeholder={tAuth('passwordPlaceholder')}
|
||||||
error={errors.currentPassword?.message}
|
error={errors.currentPassword?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -149,6 +155,7 @@ export default function AccountSettingsPage() {
|
|||||||
placeholder={tAuth('passwordPlaceholder')}
|
placeholder={tAuth('passwordPlaceholder')}
|
||||||
error={errors.newPassword?.message}
|
error={errors.newPassword?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -158,6 +165,7 @@ export default function AccountSettingsPage() {
|
|||||||
placeholder={tAuth('passwordPlaceholder')}
|
placeholder={tAuth('passwordPlaceholder')}
|
||||||
error={errors.confirmPassword?.message}
|
error={errors.confirmPassword?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ function AcceptInviteContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const passwordToggleLabels = {
|
||||||
|
show: t('showPassword'),
|
||||||
|
hide: t('hidePassword'),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen app-web-bg flex items-center justify-center p-4">
|
<div className="min-h-screen app-web-bg flex items-center justify-center p-4">
|
||||||
<div className="w-full max-w-md surface-card p-6 space-y-5">
|
<div className="w-full max-w-md surface-card p-6 space-y-5">
|
||||||
@@ -134,12 +139,14 @@ function AcceptInviteContent() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('labelConfirmPassword')}
|
label={t('labelConfirmPassword')}
|
||||||
type="password"
|
type="password"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
<Button type="button" fullWidth isLoading={submitting} onClick={() => void onAccept()}>
|
<Button type="button" fullWidth isLoading={submitting} onClick={() => void onAccept()}>
|
||||||
{t('activateAccount')}
|
{t('activateAccount')}
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ function AcceptOrganizationInviteContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const passwordToggleLabels = {
|
||||||
|
show: t('showPassword'),
|
||||||
|
hide: t('hidePassword'),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen app-web-bg flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<div className="min-h-screen app-web-bg flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
@@ -226,6 +231,7 @@ function AcceptOrganizationInviteContent() {
|
|||||||
placeholder={t('passwordPlaceholder')}
|
placeholder={t('passwordPlaceholder')}
|
||||||
error={errors.password?.message}
|
error={errors.password?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('confirmPassword')}
|
label={t('confirmPassword')}
|
||||||
@@ -234,6 +240,7 @@ function AcceptOrganizationInviteContent() {
|
|||||||
placeholder={t('passwordPlaceholder')}
|
placeholder={t('passwordPlaceholder')}
|
||||||
error={errors.confirmPassword?.message}
|
error={errors.confirmPassword?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
<Button type="button" variant="primary" onClick={() => void handleNext()} fullWidth>
|
<Button type="button" variant="primary" onClick={() => void handleNext()} fullWidth>
|
||||||
{tCommon('continue')}
|
{tCommon('continue')}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Mail, Lock } from 'lucide-react';
|
|||||||
import { useAuth } from '@/lib/hooks/useAuth';
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||||||
import { getRememberedEmail } from '@/lib/auth/rememberMe';
|
import { getRememberedEmail } from '@/lib/auth/rememberMe';
|
||||||
import { Button } from '@/components/ui/shared/Button';
|
import { Button } from '@/components/ui/shared/Button';
|
||||||
|
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||||
import { Input } from '@/components/ui/shared/Input';
|
import { Input } from '@/components/ui/shared/Input';
|
||||||
import { TopBarControls } from '@/components/ui/shared/TopBarControls';
|
import { TopBarControls } from '@/components/ui/shared/TopBarControls';
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ export default function LoginPage() {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<LoginForm>({
|
} = useForm<LoginForm>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
@@ -57,6 +60,8 @@ export default function LoginPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rememberMe = watch('rememberMe');
|
||||||
|
|
||||||
const onSubmit = async (data: LoginForm) => {
|
const onSubmit = async (data: LoginForm) => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -114,20 +119,18 @@ export default function LoginPage() {
|
|||||||
placeholder={t('passwordPlaceholder')}
|
placeholder={t('passwordPlaceholder')}
|
||||||
error={errors.password?.message}
|
error={errors.password?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={{
|
||||||
|
show: t('showPassword'),
|
||||||
|
hide: t('hidePassword'),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<Checkbox
|
||||||
<input
|
checked={rememberMe}
|
||||||
id="remember-me"
|
onChange={(checked) => setValue('rememberMe', checked)}
|
||||||
type="checkbox"
|
label={t('rememberMe')}
|
||||||
className="h-4 w-4 rounded border-border bg-background-secondary text-primary focus:ring-primary/40"
|
/>
|
||||||
{...register('rememberMe')}
|
|
||||||
/>
|
|
||||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-text-secondary">
|
|
||||||
{t('rememberMe')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<Link href="/forgot-password" className="font-medium text-primary hover:opacity-90">
|
<Link href="/forgot-password" className="font-medium text-primary hover:opacity-90">
|
||||||
{t('forgotPassword')}
|
{t('forgotPassword')}
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ export default function RegisterPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const passwordToggleLabels = {
|
||||||
|
show: t('showPassword'),
|
||||||
|
hide: t('hidePassword'),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen app-web-bg flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<div className="relative min-h-screen app-web-bg flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
<div className="absolute top-4 right-4">
|
<div className="absolute top-4 right-4">
|
||||||
@@ -189,6 +194,7 @@ export default function RegisterPage() {
|
|||||||
placeholder={t('passwordPlaceholder')}
|
placeholder={t('passwordPlaceholder')}
|
||||||
error={errors.password?.message}
|
error={errors.password?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label={t('confirmPassword')}
|
label={t('confirmPassword')}
|
||||||
@@ -197,6 +203,7 @@ export default function RegisterPage() {
|
|||||||
placeholder={t('passwordPlaceholder')}
|
placeholder={t('passwordPlaceholder')}
|
||||||
error={errors.confirmPassword?.message}
|
error={errors.confirmPassword?.message}
|
||||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||||
|
passwordToggleLabels={passwordToggleLabels}
|
||||||
/>
|
/>
|
||||||
<Button type="button" variant="primary" onClick={handleNext} fullWidth>
|
<Button type="button" variant="primary" onClick={handleNext} fullWidth>
|
||||||
{tCommon('continue')}
|
{tCommon('continue')}
|
||||||
|
|||||||
@@ -1,18 +1,63 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { forwardRef, useId } from 'react';
|
import React, { forwardRef, useId, useState } from 'react';
|
||||||
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
endIcon?: React.ReactNode;
|
||||||
|
passwordToggleLabels?: {
|
||||||
|
show: string;
|
||||||
|
hide: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ label, error, icon, className = '', id, ...props }, ref) => {
|
(
|
||||||
|
{
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
icon,
|
||||||
|
endIcon,
|
||||||
|
passwordToggleLabels,
|
||||||
|
className = '',
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const genId = useId();
|
const genId = useId();
|
||||||
const inputId = id ?? genId;
|
const inputId = id ?? genId;
|
||||||
|
|
||||||
|
const resolvedEndIcon =
|
||||||
|
endIcon ??
|
||||||
|
(passwordToggleLabels ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword((visible) => !visible)}
|
||||||
|
className="rounded p-0.5 text-text-muted transition-colors hover:text-text-secondary"
|
||||||
|
aria-label={
|
||||||
|
showPassword ? passwordToggleLabels.hide : passwordToggleLabels.show
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-5 w-5 icon-flat" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-5 w-5 icon-flat" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : undefined);
|
||||||
|
|
||||||
|
const resolvedType = passwordToggleLabels
|
||||||
|
? showPassword
|
||||||
|
? 'text'
|
||||||
|
: 'password'
|
||||||
|
: type;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{label && (
|
{label && (
|
||||||
@@ -31,15 +76,22 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{resolvedEndIcon && (
|
||||||
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center text-text-muted">
|
||||||
|
{resolvedEndIcon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={inputId}
|
id={inputId}
|
||||||
|
type={resolvedType}
|
||||||
className={`
|
className={`
|
||||||
w-full rounded-[var(--radius-md)] border
|
w-full rounded-[var(--radius-md)] border
|
||||||
${error ? 'border-red-500' : 'border-border'}
|
${error ? 'border-red-500' : 'border-border'}
|
||||||
bg-background-secondary/90 text-text-primary
|
bg-background-secondary/90 text-text-primary
|
||||||
|
|
||||||
${icon ? 'pl-10' : 'pl-4'} pr-4 py-2
|
${icon ? 'pl-10' : 'pl-4'} ${resolvedEndIcon ? 'pr-10' : 'pr-4'} py-2
|
||||||
|
|
||||||
placeholder:text-text-muted
|
placeholder:text-text-muted
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user