mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
forgot password implementation
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
"Bash(awk '/profile: \\\\{/,/^ \\\\},/' src/app/i18n/locales/am.ts)",
|
||||
"Bash(echo \"=== total errors: $\\(node_modules/.bin/tsc --noEmit -p apps/portal/tsconfig.json 2>&1)",
|
||||
"Bash(node -e \"const p=require\\('./package.json'\\); console.log\\(JSON.stringify\\(p.scripts,null,2\\)\\)\")",
|
||||
"Bash(node -e \"let s='';process.stdin.on\\('data',d=>s+=d\\).on\\('end',\\(\\)=>{try{console.log\\(Object.keys\\(JSON.parse\\(s\\).targets||{}\\)\\)}catch\\(e\\){console.log\\('no project.json'\\)}}\\)\")"
|
||||
"Bash(node -e \"let s='';process.stdin.on\\('data',d=>s+=d\\).on\\('end',\\(\\)=>{try{console.log\\(Object.keys\\(JSON.parse\\(s\\).targets||{}\\)\\)}catch\\(e\\){console.log\\('no project.json'\\)}}\\)\")",
|
||||
"Bash(echo \"EXIT: $?\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
188
apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx
Normal file
188
apps/portal/src/app/features/auth/pages/ForgotPasswordPage.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconMail,
|
||||
IconMailForward,
|
||||
IconSend,
|
||||
} from '@tabler/icons-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email({ message: 'Enter a valid email' }),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
export function ForgotPasswordPage() {
|
||||
const [forgotTrigger, { isLoading }] = useApiMutation();
|
||||
const [sentTo, setSentTo] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
const sendResetLink = async (email: string) => {
|
||||
await forgotTrigger({
|
||||
url: '/auth/forgot-password',
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
}).unwrap();
|
||||
setSentTo(email);
|
||||
};
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
try {
|
||||
await sendResetLink(values.email);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Something went wrong';
|
||||
notify.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResend = async () => {
|
||||
if (!sentTo || isLoading) return;
|
||||
try {
|
||||
await sendResetLink(sentTo);
|
||||
notify.success('Reset link sent again');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Something went wrong';
|
||||
notify.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const backToSignIn = (
|
||||
<Center>
|
||||
<Anchor
|
||||
component={Link}
|
||||
to="/login"
|
||||
size="sm"
|
||||
fw={600}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
<IconArrowLeft size={16} />
|
||||
Back to sign in
|
||||
</Anchor>
|
||||
</Center>
|
||||
);
|
||||
|
||||
if (sentTo) {
|
||||
return (
|
||||
<AuthShell
|
||||
brandTitle="Reset your password securely."
|
||||
brandSubtitle="We'll email you a secure link to set a new password and get you back into your licensing portal."
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
||||
<IconMailForward size={30} />
|
||||
</ThemeIcon>
|
||||
|
||||
<div>
|
||||
<Title order={2} fz={30}>
|
||||
Check your email
|
||||
</Title>
|
||||
<Text c="dimmed" mt={6}>
|
||||
We've sent a password reset link to{' '}
|
||||
<Text span fw={600} c="dark">
|
||||
{sentTo}
|
||||
</Text>
|
||||
. Follow the link in that email to choose a new password.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
component="a"
|
||||
href="https://mail.google.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
fullWidth
|
||||
size="md"
|
||||
leftSection={<IconMail size={18} />}
|
||||
>
|
||||
Open email app
|
||||
</Button>
|
||||
|
||||
<Group justify="center" gap={6}>
|
||||
<Text size="sm" c="dimmed">
|
||||
Didn't get the email?
|
||||
</Text>
|
||||
<Anchor
|
||||
size="sm"
|
||||
fw={600}
|
||||
onClick={handleResend}
|
||||
style={isLoading ? { pointerEvents: 'none', opacity: 0.6 } : undefined}
|
||||
>
|
||||
Resend link
|
||||
</Anchor>
|
||||
</Group>
|
||||
|
||||
{backToSignIn}
|
||||
</Stack>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthShell
|
||||
brandTitle="Reset your password securely."
|
||||
brandSubtitle="We'll email you a secure link to set a new password and get you back into your licensing portal."
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={2} fz={30}>
|
||||
Forgot your password?
|
||||
</Title>
|
||||
<Text c="dimmed" mt={6}>
|
||||
Enter the email linked to your account and we'll send you a link
|
||||
to reset your password.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="md">
|
||||
<TextInput
|
||||
label="Email address"
|
||||
placeholder="you@example.com"
|
||||
size="md"
|
||||
leftSection={<IconMail size={18} />}
|
||||
error={errors.email?.message}
|
||||
{...register('email')}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
fullWidth
|
||||
size="md"
|
||||
rightSection={<IconSend size={18} />}
|
||||
>
|
||||
Send reset link
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
{backToSignIn}
|
||||
</Stack>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
@@ -120,9 +120,10 @@ export function LoginPage() {
|
||||
onChange={(e) => setRememberMe(e.currentTarget.checked)}
|
||||
/>
|
||||
<Anchor
|
||||
component={Link}
|
||||
to="/forgot-password"
|
||||
size="sm"
|
||||
fw={600}
|
||||
onClick={() => notify.info('Password reset is coming soon.')}
|
||||
>
|
||||
Forgot password?
|
||||
</Anchor>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PortalLayout } from './layouts/PortalLayout';
|
||||
import { LoginPage } from './features/auth/pages/LoginPage';
|
||||
import { SignupPage } from './features/auth/pages/SignupPage';
|
||||
import { OTPVerificationPage } from './features/auth/pages/OTPVerificationPage';
|
||||
import { ForgotPasswordPage } from './features/auth/pages/ForgotPasswordPage';
|
||||
|
||||
// Portal feature pages
|
||||
import { DashboardPage } from './features/dashboard/pages/DashboardPage';
|
||||
@@ -43,6 +44,7 @@ export const router = createBrowserRouter([
|
||||
{ path: '/login', element: <LoginPage /> },
|
||||
{ path: '/signup', element: <SignupPage /> },
|
||||
{ path: '/otp-verify', element: <OTPVerificationPage /> },
|
||||
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
|
||||
|
||||
// Portal (open access for this UI template).
|
||||
// Wrapped in the portal's own i18n instance so it is isolated from the
|
||||
|
||||
Reference in New Issue
Block a user