mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
design signup and login
This commit is contained in:
164
apps/portal/src/app/features/auth/components/AuthShell.tsx
Normal file
164
apps/portal/src/app/features/auth/components/AuthShell.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
|
rem,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { IconAnchor, IconCheck } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
const FEATURES = [
|
||||||
|
'Submit applications online, 24/7',
|
||||||
|
'Real-time status tracking & alerts',
|
||||||
|
'Available in English & አማርኛ',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EMA brand mark — mirrors the logo used in the portal header
|
||||||
|
* (gradient tile + anchor glyph). Use `variant="white"` on dark backgrounds.
|
||||||
|
*/
|
||||||
|
export function BrandMark({
|
||||||
|
size = 44,
|
||||||
|
variant = 'gradient',
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
variant?: 'gradient' | 'white';
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ThemeIcon
|
||||||
|
size={size}
|
||||||
|
radius="md"
|
||||||
|
variant={variant === 'white' ? 'white' : 'gradient'}
|
||||||
|
color={variant === 'white' ? 'emaPrimary' : undefined}
|
||||||
|
gradient={{ from: 'emaPrimary.7', to: 'emaTeal.5', deg: 135 }}
|
||||||
|
>
|
||||||
|
<IconAnchor size={Math.round(size * 0.52)} />
|
||||||
|
</ThemeIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthShellProps {
|
||||||
|
children: ReactNode;
|
||||||
|
brandTitle?: string;
|
||||||
|
brandSubtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-pane auth layout: a branded gradient panel (hidden below `md`) and the
|
||||||
|
* form pane. Standalone (rendered outside the portal chrome and i18n provider),
|
||||||
|
* so copy here is plain English to match the other auth pages.
|
||||||
|
*/
|
||||||
|
export function AuthShell({
|
||||||
|
children,
|
||||||
|
brandTitle = 'Maritime licensing, made simple.',
|
||||||
|
brandSubtitle = 'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
|
||||||
|
}: AuthShellProps) {
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const heroGradient = theme.other.heroGradient as string;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex mih="100vh">
|
||||||
|
{/* ---- Brand panel ---------------------------------------------- */}
|
||||||
|
<Box
|
||||||
|
visibleFrom="md"
|
||||||
|
w={500}
|
||||||
|
p={48}
|
||||||
|
pos="relative"
|
||||||
|
style={{ background: heroGradient, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{/* Decorative blurred orbs for depth */}
|
||||||
|
<Box
|
||||||
|
pos="absolute"
|
||||||
|
top={-120}
|
||||||
|
right={-80}
|
||||||
|
w={360}
|
||||||
|
h={360}
|
||||||
|
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.10)' }}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
pos="absolute"
|
||||||
|
bottom={-140}
|
||||||
|
left={-100}
|
||||||
|
w={320}
|
||||||
|
h={320}
|
||||||
|
style={{ borderRadius: '50%', background: 'rgba(255,255,255,0.08)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
justify="space-between"
|
||||||
|
h="100%"
|
||||||
|
pos="relative"
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<BrandMark variant="white" size={46} />
|
||||||
|
<div>
|
||||||
|
<Text c="white" fw={700} fz="lg" lh={1.1}>
|
||||||
|
EMA Portal
|
||||||
|
</Text>
|
||||||
|
<Text fz="xs" lh={1.2} style={{ color: 'rgba(255,255,255,0.75)' }}>
|
||||||
|
Ethiopian Maritime Authority
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="xl" maw={400}>
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Title order={1} c="white" fz={rem(36)} lh={1.15} fw={700}>
|
||||||
|
{brandTitle}
|
||||||
|
</Title>
|
||||||
|
<Text fz="md" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
|
||||||
|
{brandSubtitle}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap="md">
|
||||||
|
{FEATURES.map((feature) => (
|
||||||
|
<Group key={feature} gap="sm" wrap="nowrap">
|
||||||
|
<Center
|
||||||
|
w={24}
|
||||||
|
h={24}
|
||||||
|
style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'rgba(255,255,255,0.2)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCheck size={14} color="white" stroke={2.4} />
|
||||||
|
</Center>
|
||||||
|
<Text c="white" fz="sm" fw={500}>
|
||||||
|
{feature}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Text fz="xs" style={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||||
|
© 2026 Ethiopian Maritime Authority
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ---- Form panel ----------------------------------------------- */}
|
||||||
|
<Center flex={1} p="xl">
|
||||||
|
<Box w="100%" maw={420}>
|
||||||
|
<Group hiddenFrom="md" gap="sm" mb="xl">
|
||||||
|
<BrandMark size={40} />
|
||||||
|
<Text fw={700} fz="lg">
|
||||||
|
EMA Portal
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Paper,
|
|
||||||
TextInput,
|
|
||||||
PasswordInput,
|
|
||||||
Button,
|
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
Center,
|
|
||||||
Text,
|
|
||||||
Anchor,
|
Anchor,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconArrowRight,
|
||||||
|
IconDeviceMobile,
|
||||||
|
IconLock,
|
||||||
|
IconMail,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -19,6 +26,7 @@ import { loginSuccess, setUser } from '../store/auth.slice';
|
|||||||
import type { LoginPayload, AuthUser } from '../types/auth.types';
|
import type { LoginPayload, AuthUser } from '../types/auth.types';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
|
import { AuthShell } from '../components/AuthShell';
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().email({ message: 'Enter a valid email' }),
|
email: z.string().email({ message: 'Enter a valid email' }),
|
||||||
@@ -31,6 +39,7 @@ export function LoginPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [rememberMe, setRememberMe] = useState(true);
|
||||||
const [loginTrigger] = useApiMutation<LoginPayload>();
|
const [loginTrigger] = useApiMutation<LoginPayload>();
|
||||||
const [meTrigger] = useApiMutation<AuthUser>();
|
const [meTrigger] = useApiMutation<AuthUser>();
|
||||||
|
|
||||||
@@ -73,37 +82,83 @@ export function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Center h="100vh">
|
<AuthShell>
|
||||||
<Paper p="xl" shadow="md" radius="md" w={400}>
|
<Stack gap="lg">
|
||||||
<Stack gap="md">
|
<div>
|
||||||
<Title order={3}>Sign in to Portal</Title>
|
<Title order={2} fz={30}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
Welcome back
|
||||||
<Stack gap="sm">
|
</Title>
|
||||||
<TextInput
|
<Text c="dimmed" mt={6}>
|
||||||
label="Email"
|
Sign in to access your maritime licensing dashboard.
|
||||||
placeholder="you@example.com"
|
|
||||||
error={errors.email?.message}
|
|
||||||
{...register('email')}
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label="Password"
|
|
||||||
placeholder="Your password"
|
|
||||||
error={errors.password?.message}
|
|
||||||
{...register('password')}
|
|
||||||
/>
|
|
||||||
<Button type="submit" loading={isLoading} fullWidth mt="sm">
|
|
||||||
Sign in
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
<Text ta="center" size="sm" c="dimmed">
|
|
||||||
Don't have an account?{' '}
|
|
||||||
<Anchor component={Link} to="/signup">
|
|
||||||
Sign up
|
|
||||||
</Anchor>
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</div>
|
||||||
</Paper>
|
|
||||||
</Center>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label="Email or phone"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
size="md"
|
||||||
|
leftSection={<IconMail size={18} />}
|
||||||
|
error={errors.email?.message}
|
||||||
|
{...register('email')}
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password"
|
||||||
|
size="md"
|
||||||
|
leftSection={<IconLock size={18} />}
|
||||||
|
error={errors.password?.message}
|
||||||
|
{...register('password')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Checkbox
|
||||||
|
label="Remember me"
|
||||||
|
size="sm"
|
||||||
|
checked={rememberMe}
|
||||||
|
onChange={(e) => setRememberMe(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<Anchor
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
onClick={() => notify.info('Password reset is coming soon.')}
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Anchor>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={isLoading}
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
rightSection={<IconArrowRight size={18} />}
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Divider label="or" labelPosition="center" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
leftSection={<IconDeviceMobile size={18} />}
|
||||||
|
onClick={() => notify.info('Phone sign-in is coming soon.')}
|
||||||
|
>
|
||||||
|
Sign in with phone number
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<Anchor component={Link} to="/signup" fw={700}>
|
||||||
|
Create one
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</AuthShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
Paper,
|
Anchor,
|
||||||
TextInput,
|
|
||||||
Button,
|
Button,
|
||||||
|
Group,
|
||||||
Stack,
|
Stack,
|
||||||
Title,
|
|
||||||
Center,
|
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
|
ThemeIcon,
|
||||||
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { IconArrowRight, IconShieldLock } from '@tabler/icons-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
|
import { AuthShell } from '../components/AuthShell';
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
verificationCode: z.string().min(1, { message: 'Verification code is required' }),
|
verificationCode: z.string().min(1, { message: 'Verification code is required' }),
|
||||||
@@ -72,38 +75,63 @@ export function OTPVerificationPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Center h="100vh">
|
<AuthShell
|
||||||
<Paper p="xl" shadow="md" radius="md" w={400}>
|
brandTitle="One last step to secure your account."
|
||||||
<Stack gap="md">
|
brandSubtitle="We use a one-time code to confirm it's really you before granting access to the licensing portal."
|
||||||
<Title order={3}>Verify your email</Title>
|
>
|
||||||
<Text c="dimmed" size="sm">
|
<Stack gap="lg">
|
||||||
A verification code has been sent to {email || 'your email'}. Enter it
|
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
||||||
below to complete your registration.
|
<IconShieldLock size={30} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Title order={2} fz={30}>
|
||||||
|
Verify your account
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" mt={6}>
|
||||||
|
A verification code has been sent to{' '}
|
||||||
|
<Text span fw={600} c="dark">
|
||||||
|
{email || 'your email'}
|
||||||
|
</Text>
|
||||||
|
. Enter it below to continue.
|
||||||
</Text>
|
</Text>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
</div>
|
||||||
<Stack gap="sm">
|
|
||||||
<TextInput
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
label="Verification Code"
|
<Stack gap="md">
|
||||||
placeholder="Enter the code from your email"
|
<TextInput
|
||||||
error={errors.verificationCode?.message}
|
label="Verification code"
|
||||||
{...register('verificationCode')}
|
placeholder="Enter the 6-digit code"
|
||||||
/>
|
size="md"
|
||||||
<Button type="submit" loading={loading} fullWidth mt="sm">
|
error={errors.verificationCode?.message}
|
||||||
Verify
|
{...register('verificationCode')}
|
||||||
</Button>
|
/>
|
||||||
</Stack>
|
<Button
|
||||||
</form>
|
type="submit"
|
||||||
<Button
|
loading={loading}
|
||||||
variant="subtle"
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
rightSection={<IconArrowRight size={18} />}
|
||||||
|
>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Group justify="center" gap={6}>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Didn't receive a code?
|
||||||
|
</Text>
|
||||||
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
loading={resending}
|
fw={600}
|
||||||
onClick={handleResendOtp}
|
onClick={handleResendOtp}
|
||||||
fullWidth
|
style={resending ? { pointerEvents: 'none', opacity: 0.6 } : undefined}
|
||||||
>
|
>
|
||||||
Send OTP again
|
Resend code
|
||||||
</Button>
|
</Anchor>
|
||||||
</Stack>
|
</Group>
|
||||||
</Paper>
|
</Stack>
|
||||||
</Center>
|
</AuthShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Paper,
|
|
||||||
TextInput,
|
|
||||||
PasswordInput,
|
|
||||||
Button,
|
|
||||||
Stack,
|
|
||||||
Title,
|
|
||||||
Center,
|
|
||||||
Text,
|
|
||||||
Anchor,
|
Anchor,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Group,
|
||||||
|
PasswordInput,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconArrowRight,
|
||||||
|
IconAt,
|
||||||
|
IconDeviceMobile,
|
||||||
|
IconLock,
|
||||||
|
IconMail,
|
||||||
|
IconUser,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -17,6 +27,7 @@ import { useAppDispatch } from '../../../store/hooks';
|
|||||||
import { loginSuccess } from '../store/auth.slice';
|
import { loginSuccess } from '../store/auth.slice';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
|
import { AuthShell } from '../components/AuthShell';
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -52,6 +63,7 @@ interface SignupPayload {
|
|||||||
export function SignupPage() {
|
export function SignupPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const [agreed, setAgreed] = useState(false);
|
||||||
const [signupTrigger, { isLoading: loading }] = useApiMutation<{
|
const [signupTrigger, { isLoading: loading }] = useApiMutation<{
|
||||||
token: string;
|
token: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
@@ -103,67 +115,119 @@ export function SignupPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Center h="100vh">
|
<AuthShell
|
||||||
<Paper p="xl" shadow="md" radius="md" w={400}>
|
brandTitle="Join Ethiopia's maritime community."
|
||||||
<Stack gap="md">
|
brandSubtitle="Create your account to apply for licenses, manage documents, and track approvals from anywhere."
|
||||||
<Title order={3}>Create an account</Title>
|
>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<Stack gap="lg">
|
||||||
<Stack gap="sm">
|
<div>
|
||||||
|
<Title order={2} fz={30}>
|
||||||
|
Create account
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" mt={6}>
|
||||||
|
It only takes a minute to get started.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name (English)"
|
label="Name (English)"
|
||||||
placeholder="Your name in English"
|
placeholder="Abebe Bekele"
|
||||||
|
leftSection={<IconUser size={18} />}
|
||||||
error={errors.nameEn?.message}
|
error={errors.nameEn?.message}
|
||||||
{...register('nameEn')}
|
{...register('nameEn')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name (Amharic)"
|
label="Name (Amharic)"
|
||||||
placeholder="Your name in Amharic"
|
placeholder="ስም"
|
||||||
|
leftSection={<IconUser size={18} />}
|
||||||
error={errors.nameAm?.message}
|
error={errors.nameAm?.message}
|
||||||
{...register('nameAm')}
|
{...register('nameAm')}
|
||||||
/>
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label="Email address"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
|
leftSection={<IconMail size={18} />}
|
||||||
error={errors.email?.message}
|
error={errors.email?.message}
|
||||||
{...register('email')}
|
{...register('email')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Username"
|
label="Username"
|
||||||
placeholder="Choose a username"
|
placeholder="Choose a username"
|
||||||
|
leftSection={<IconAt size={18} />}
|
||||||
error={errors.username?.message}
|
error={errors.username?.message}
|
||||||
{...register('username')}
|
{...register('username')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
</SimpleGrid>
|
||||||
label="Phone Number"
|
|
||||||
placeholder="+251 911 234 567"
|
<TextInput
|
||||||
error={errors.phoneNumber?.message}
|
label="Phone number"
|
||||||
{...register('phoneNumber')}
|
placeholder="+251 911 234 567"
|
||||||
/>
|
leftSection={<IconDeviceMobile size={18} />}
|
||||||
|
error={errors.phoneNumber?.message}
|
||||||
|
{...register('phoneNumber')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label="Password"
|
||||||
placeholder="Enter a password"
|
placeholder="At least 8 characters"
|
||||||
|
leftSection={<IconLock size={18} />}
|
||||||
error={errors.password?.message}
|
error={errors.password?.message}
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Confirm Password"
|
label="Confirm password"
|
||||||
placeholder="Confirm your password"
|
placeholder="Re-enter password"
|
||||||
|
leftSection={<IconLock size={18} />}
|
||||||
error={errors.confirmPassword?.message}
|
error={errors.confirmPassword?.message}
|
||||||
{...register('confirmPassword')}
|
{...register('confirmPassword')}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" loading={loading} fullWidth mt="sm">
|
</SimpleGrid>
|
||||||
Sign up
|
|
||||||
</Button>
|
<Checkbox
|
||||||
</Stack>
|
size="sm"
|
||||||
</form>
|
checked={agreed}
|
||||||
<Text ta="center" size="sm" c="dimmed">
|
onChange={(e) => setAgreed(e.currentTarget.checked)}
|
||||||
Already have an account?{' '}
|
label={
|
||||||
<Anchor component={Link} to="/login">
|
<Text size="sm">
|
||||||
Sign in
|
I agree to the{' '}
|
||||||
</Anchor>
|
<Anchor
|
||||||
</Text>
|
size="sm"
|
||||||
</Stack>
|
fw={600}
|
||||||
</Paper>
|
onClick={(e) => e.preventDefault()}
|
||||||
</Center>
|
>
|
||||||
|
Terms & Privacy Policy
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={loading}
|
||||||
|
disabled={!agreed}
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
rightSection={<IconArrowRight size={18} />}
|
||||||
|
>
|
||||||
|
Create account
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Anchor component={Link} to="/login" fw={700}>
|
||||||
|
Sign in
|
||||||
|
</Anchor>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</AuthShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
562
ema-portal.pen
Normal file
562
ema-portal.pen
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
{
|
||||||
|
"version": "2.13",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "frame",
|
||||||
|
"id": "YKsGt",
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"name": "Welcome to Pencil",
|
||||||
|
"clip": true,
|
||||||
|
"width": 1000,
|
||||||
|
"height": 1458,
|
||||||
|
"layout": "none",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "Lmqy1",
|
||||||
|
"x": 38,
|
||||||
|
"y": 297,
|
||||||
|
"name": "Start here",
|
||||||
|
"fill": "#ff8303ff",
|
||||||
|
"textGrowth": "fixed-width-height",
|
||||||
|
"width": 923,
|
||||||
|
"height": 307,
|
||||||
|
"content": "Ask agent to \ndesign something",
|
||||||
|
"lineHeight": 1.399999976158142,
|
||||||
|
"textAlign": "center",
|
||||||
|
"fontFamily": "Noto Sans",
|
||||||
|
"fontSize": 100,
|
||||||
|
"fontWeight": "500",
|
||||||
|
"letterSpacing": -4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path",
|
||||||
|
"id": "AmD9T",
|
||||||
|
"x": 267.8010819142764,
|
||||||
|
"y": 632,
|
||||||
|
"rotation": -45,
|
||||||
|
"geometry": "M7 0l0 14m7-7l-7 7-7-7",
|
||||||
|
"width": 366,
|
||||||
|
"height": 366,
|
||||||
|
"stroke": "#ff8302ff",
|
||||||
|
"strokeWidth": 50,
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeLinecap": "round"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"themes": {
|
||||||
|
"Mode": [
|
||||||
|
"Light",
|
||||||
|
"Dark"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"--sidebar": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#E7E8E5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#18181b",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--sidebar-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#666666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#fafafa",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--sidebar-primary": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#18181b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#18181b",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--sidebar-primary-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#fafafa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#fafafa",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--sidebar-border": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#CBCCC9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#ffffff1a",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--sidebar-accent": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#CBCCC9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#2a2a30",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--sidebar-accent-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#18181b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#fafafa",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--sidebar-ring": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#71717a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#71717a",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--background": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#F2F3F0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#111111",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FFFFFF",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--card": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#FFFFFF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#1A1A1A",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--card-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FFFFFF",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--popover": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#FFFFFF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#1A1A1A",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--popover-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FFFFFF",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--primary": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#FF8400"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FF8400",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--primary-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#111111",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--secondary": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#E7E8E5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#2E2E2E",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--secondary-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FFFFFF",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--muted": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#F2F3F0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#2E2E2E",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--muted-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#666666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#B8B9B6",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--accent": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#F2F3F0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#111111",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--accent-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#111111"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#F2F3F0",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--destructive": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#D93C15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FF5C33",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--border": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#CBCCC9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#2E2E2E",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--input": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#CBCCC9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#2E2E2E",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--ring": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#666666"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#666666",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--white": {
|
||||||
|
"type": "color",
|
||||||
|
"value": "#FFFFFF"
|
||||||
|
},
|
||||||
|
"--black": {
|
||||||
|
"type": "color",
|
||||||
|
"value": "#000000"
|
||||||
|
},
|
||||||
|
"--font-primary": {
|
||||||
|
"type": "string",
|
||||||
|
"value": "JetBrains Mono"
|
||||||
|
},
|
||||||
|
"--font-secondary": {
|
||||||
|
"type": "string",
|
||||||
|
"value": "Geist"
|
||||||
|
},
|
||||||
|
"--radius-none": {
|
||||||
|
"type": "number",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"--radius-pill": {
|
||||||
|
"type": "number",
|
||||||
|
"value": 999
|
||||||
|
},
|
||||||
|
"--color-success": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#DFE6E1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#222924",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--color-success-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#004D1A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#B6FFCE",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--color-warning": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#E9E3D8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#291C0F",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--color-warning-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#804200"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FF8400",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--color-error": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#E5DCDA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#24100B",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--color-error-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#8C1C00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#FF5C33",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--color-info": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#DFDFE6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#222229",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--color-info-foreground": {
|
||||||
|
"type": "color",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"value": "#000066"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "#B2B2FF",
|
||||||
|
"theme": {
|
||||||
|
"Mode": "Dark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"--radius-m": {
|
||||||
|
"type": "number",
|
||||||
|
"value": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user