diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json new file mode 100644 index 0000000..d38deda --- /dev/null +++ b/public/locales/fa/authentication.json @@ -0,0 +1,9 @@ +{ + "loginForm": { + "title": "ورود/ثبت‌نام", + "description": "لطفا برای شروع شماره موبایل/ایمیل خود را وارد کنید.", + "emailOrPhoneLabel": "شماره موبایل/ایمیل", + "submitButton": "ورود/ثبت‌نام", + "loginWithGoogle": "ورود با گوگل" + } +} diff --git a/src/App.tsx b/src/App.tsx index 0659ade..1fcbd6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,40 +1,16 @@ -import { Box, CssBaseline, TextField, useColorScheme } from '@mui/material'; +import { CssBaseline } from '@mui/material'; import './App.css'; -import { useTranslation } from 'react-i18next'; import { LanguageManager } from './components/LanguageManager'; +import { LoginPage } from './features/authentication/routes/LoginPage'; function App() { - const { t } = useTranslation(); - return ( <> -
-

{t('helloWorld')}

-

The main content and router will go here.

- - - - -
+ ); } export default App; - -import { Button } from '@mui/material'; - -export const ThemeToggleButton = () => { - const { mode, setMode } = useColorScheme(); - - return ( - - ); -}; diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..6f53ef6 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx new file mode 100644 index 0000000..dbba376 --- /dev/null +++ b/src/components/Logo.tsx @@ -0,0 +1,7 @@ +import LogoSvg from '@/assets/logo.svg'; + +function Logo() { + return ; +} + +export default Logo; diff --git a/src/components/components/common/Container.tsx b/src/components/components/common/Container.tsx new file mode 100644 index 0000000..c9efc78 --- /dev/null +++ b/src/components/components/common/Container.tsx @@ -0,0 +1,8 @@ +import { Box, styled } from '@mui/material'; + +export const Container = styled(Box)(() => ({ + width: '100%', + maxWidth: '100vw', + height: '100vh', + margin: '0 auto', +})); diff --git a/src/components/components/common/FlexBox.tsx b/src/components/components/common/FlexBox.tsx new file mode 100644 index 0000000..e15bb8c --- /dev/null +++ b/src/components/components/common/FlexBox.tsx @@ -0,0 +1,21 @@ +import { Box, styled, type BoxProps } from '@mui/material'; + +// Define the props our component will accept +interface FlexBoxProps extends BoxProps { + direction?: 'row' | 'column'; + justify?: string; + align?: string; +} + +export const FlexBox = styled(Box, { + // Do not forward these custom props to the DOM element + shouldForwardProp: (prop) => + prop !== 'direction' && prop !== 'justify' && prop !== 'align', +})( + ({ direction = 'row', justify = 'flex-start', align = 'stretch' }) => ({ + display: 'flex', + flexDirection: direction, + justifyContent: justify, + alignItems: align, + }), +); diff --git a/src/components/components/common/Stack.tsx b/src/components/components/common/Stack.tsx new file mode 100644 index 0000000..0e00bfd --- /dev/null +++ b/src/components/components/common/Stack.tsx @@ -0,0 +1,19 @@ +import { Box, styled, type BoxProps } from '@mui/material'; + +interface StackProps extends BoxProps { + direction?: 'row' | 'column'; + spacing?: number; // Spacing factor (multiplied by theme.spacing) + align?: string; +} + +export const Stack = styled(Box, { + shouldForwardProp: (prop) => + prop !== 'direction' && prop !== 'spacing' && prop !== 'align', +})( + ({ theme, direction = 'column', spacing = 2, align = 'stretch' }) => ({ + display: 'flex', + flexDirection: direction, + alignItems: align, + gap: theme.spacing(spacing), + }), +); diff --git a/src/features/authentication/components/CountryCodeAdornment.tsx b/src/features/authentication/components/CountryCodeAdornment.tsx new file mode 100644 index 0000000..a7593d1 --- /dev/null +++ b/src/features/authentication/components/CountryCodeAdornment.tsx @@ -0,0 +1,35 @@ +import { Box, Typography } from '@mui/material'; + +interface CountryCodeAdornmentProps { + show: boolean; +} + +/** + * An animated country code adornment that fades and slides into view. + * Its visibility is controlled by the `show` prop. + */ +export function CountryCodeAdornment({ show }: CountryCodeAdornmentProps) { + return ( + + theme.transitions.create(['width', 'opacity'], { + duration: theme.transitions.duration.short, + }), + // Prevent content from wrapping or spilling out during animation + overflow: 'hidden', + whiteSpace: 'nowrap', + }} + > + {/* This inner Box prevents the content from being squeezed during the transition */} + + + +41 + + + + ); +} diff --git a/src/features/authentication/components/LoginForm.tsx b/src/features/authentication/components/LoginForm.tsx new file mode 100644 index 0000000..dedb027 --- /dev/null +++ b/src/features/authentication/components/LoginForm.tsx @@ -0,0 +1,72 @@ +import { + Box, + Button, + InputAdornment, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CountryCodeAdornment } from './CountryCodeAdornment'; + +const isNumeric = (value: string) => /^\d*$/.test(value); + +export function LoginForm() { + const { t } = useTranslation('authentication'); + const [value, setValue] = useState(''); + const [inputType, setInputType] = useState<'phone' | 'email'>('phone'); + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + setValue(newValue); + + // If the new value contains only digits (or is empty), it's a phone number + if (isNumeric(newValue)) { + setInputType('phone'); + } else { + setInputType('email'); + } + }; + + const showAdornment = inputType === 'phone' && value.length > 0; + + return ( + + + {t('loginForm.title')} + + {t('loginForm.description')} + + + + theme.transitions.create('margin'), + }} + > + + + ), + }, + }} + sx={{ my: 4 }} + /> + + + + + + + ); +} diff --git a/src/features/authentication/index.ts b/src/features/authentication/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/features/authentication/routes/LoginPage.tsx b/src/features/authentication/routes/LoginPage.tsx new file mode 100644 index 0000000..9177456 --- /dev/null +++ b/src/features/authentication/routes/LoginPage.tsx @@ -0,0 +1,30 @@ +import { FlexBox } from '@/components/components/common/FlexBox'; +import Logo from '@/components/Logo'; +import { Paper } from '@mui/material'; +import { LoginForm } from '../components/LoginForm'; + +export function LoginPage() { + return ( + + + + + + + ); +} diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx index 29df78d..ee1201e 100644 --- a/src/providers/CustomThemeProvider.tsx +++ b/src/providers/CustomThemeProvider.tsx @@ -25,6 +25,21 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({ cssVariables: { colorSchemeSelector: 'class', }, + components: { + MuiTextField: { + defaultProps: { + variant: 'outlined', + fullWidth: true, + }, + }, + MuiButton: { + defaultProps: { + size: 'large', + fullWidth: true, + variant: 'contained', + }, + }, + }, spacing: 8, typography: typography, });