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,
});