fix: styles and make it responsive

This commit is contained in:
2025-07-26 16:49:17 +03:30
committed by Koosha Lahouti
parent a782043767
commit 0e2f724f14
7 changed files with 478 additions and 197 deletions

View File

@@ -29,6 +29,7 @@
"sent": "ارسال شد!",
"country": "کشور",
"dateOfBirth": "تاریخ تولد(اختیاری)",
"invalidCountry": "کشور انتخاب شده صحیح نیست"
"invalidCountry": "کشور انتخاب شده صحیح نیست",
"rules": "قوانین و مقررات"
}
}

View File

@@ -0,0 +1,97 @@
import React, { useState, useEffect } from 'react';
import { Box, Alert, IconButton, type AlertColor } from '@mui/material';
import {
TickCircle,
CloseSquare,
Warning2,
InfoCircle,
CloseCircle,
} from 'iconsax-react';
type AlertType = AlertColor;
interface CustomAlertProps {
message: string;
onClose: () => void;
severity?: AlertType;
open: boolean;
duration?: number;
delayOnClose?: number;
rtl?: boolean;
icon?: React.ReactNode;
}
const defaultIcons: Record<AlertType, React.ReactNode> = {
success: <TickCircle size="20" color="#fff" />,
error: <CloseSquare size="20" color="#fff" />,
warning: <Warning2 size="20" color="#fff" />,
info: <InfoCircle size="20" color="#fff" />,
};
export const CustomAlert: React.FC<CustomAlertProps> = ({
message,
severity,
open,
onClose,
duration = 4000,
delayOnClose = 2000,
rtl = false,
icon,
}) => {
const [visible, setVisible] = useState(open);
useEffect(() => {
setVisible(open);
}, [open]);
useEffect(() => {
if (visible && duration > 0) {
const timer = setTimeout(() => {
setVisible(false);
onClose();
}, duration);
return () => clearTimeout(timer);
}
}, [visible, duration, onClose]);
const handleClose = () => {
setTimeout(() => {
setVisible(false);
onClose();
}, delayOnClose);
};
if (!visible) return null;
return (
<Box
sx={{
position: 'fixed',
bottom: 20,
left: 20,
zIndex: 9999,
}}
>
<Alert
severity={severity}
variant="filled"
icon={icon ?? defaultIcons[severity ?? 'success']}
action={
<IconButton onClick={handleClose} sx={{ color: 'black', p: 0.5 }}>
<CloseCircle size="20" />
</IconButton>
}
sx={{
width: '396px',
flexDirection: 'row-reverse',
justifyContent: 'space-between',
alignItems: 'center',
textAlign: rtl ? 'right' : 'left',
direction: rtl ? 'rtl' : 'ltr',
}}
>
{message}
</Alert>
</Box>
);
};

View File

@@ -24,11 +24,9 @@ export function DateOfBirth() {
textField: {
fullWidth: true,
sx: {
width: {
xs: '100%',
sm: '70%',
md: '309px',
},
flex: '1 1 260px',
maxWidth: '309px',
width: '100%',
},
},
}}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
TextField,
Box,
@@ -11,6 +11,7 @@ import {
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { TickCircle, Edit, Refresh } from 'iconsax-react';
import { CustomAlert } from '@/components/CustomAlert';
interface EmailSectionProps {
showEmail: boolean;
@@ -48,11 +49,33 @@ export function EmailSection({
handleEditEmail,
}: EmailSectionProps) {
const { t } = useTranslation('completionForm');
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [showEmailErrorAlert, setShowEmailErrorAlert] = useState(false);
const onSendCodeClick = () => {
if (!correctEmail) {
setShowEmailErrorAlert(true);
return;
}
setShowEmailErrorAlert(false);
handleSendCode();
};
const handleToggleEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
setShowEmail(e.target.checked);
};
useEffect(() => {
if (emailVerified) {
setShowSuccessAlert(true);
}
}, [emailVerified]);
const fieldSx = {
flex: '1 1 260px',
maxWidth: emailVerified ? '634px' : '462px',
width: '100%',
};
return (
<>
<FormGroup>
@@ -61,7 +84,7 @@ export function EmailSection({
display: 'flex',
alignItems: 'center',
gap: 1,
px: { xs: 2, sm: 6 },
px: { xs: 2, sm: 0 },
}}
>
<Switch checked={showEmail} onChange={handleToggleEmail} />
@@ -79,68 +102,68 @@ export function EmailSection({
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 6 },
px: { xs: 2, sm: 0 },
}}
>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
<Box sx={{ flex: 1, minWidth: '260px', maxWidth: '634px' }}>
<TextField
label={t('completion.email')}
variant="outlined"
fullWidth
value={email}
onChange={(e) => setEmail(e.target.value)}
error={email.length > 0 && !correctEmail}
sx={{ maxWidth: '462px' }}
InputProps={{
startAdornment:
!isVerifyingCode && emailVerified ? (
<InputAdornment position="end">
<TickCircle
size="24"
color="#2e7d32"
variant="Bold"
style={{
position: 'absolute',
right: 0,
marginRight: '16px',
}}
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
justifyContent: 'center',
'@media(min-width: 600px)': {
justifyContent: 'flex-start',
},
}}
>
<TextField
label={t('completion.email')}
variant="outlined"
fullWidth
value={email}
onChange={(e) => setEmail(e.target.value)}
error={email.length > 0 && !correctEmail}
sx={fieldSx}
InputProps={{
startAdornment:
!isVerifyingCode && emailVerified ? (
<InputAdornment position="end">
<TickCircle
size="24"
color="#2e7d32"
variant="Bold"
style={{
position: 'absolute',
right: 0,
marginRight: '16px',
}}
/>
</InputAdornment>
) : null,
endAdornment:
buttonState === 'counting' ? (
<InputAdornment position="start">
<IconButton onClick={handleEditEmail}>
<Edit
size="20"
color="#2979FF"
style={{ position: 'absolute', left: 0 }}
/>
</InputAdornment>
) : null,
endAdornment:
buttonState === 'counting' ? (
<InputAdornment position="start">
<IconButton onClick={handleEditEmail}>
<Edit
size="20"
color="#2979FF"
style={{ position: 'absolute', left: 0 }}
/>
</IconButton>
</InputAdornment>
) : null,
}}
inputProps={{
style: {
paddingLeft: buttonState === 'counting' ? '0px' : undefined,
},
}}
/>
{email && (
<Typography
sx={{ color: correctEmail ? 'success.main' : 'error.main' }}
variant="caption"
>
{correctEmail ? '' : t('completion.emailCorrectForm')}
</Typography>
)}
</Box>
</IconButton>
</InputAdornment>
) : null,
}}
inputProps={{
style: {
paddingLeft: buttonState === 'counting' ? '0px' : undefined,
},
}}
/>
{!isVerifyingCode && !emailVerified && (
<Button
variant="text"
onClick={handleSendCode}
onClick={onSendCodeClick}
sx={{
minWidth: '140px',
width: { xs: '100%', sm: '156px' },
@@ -150,14 +173,38 @@ export function EmailSection({
{getButtonLabel()}
</Button>
)}
{showEmailErrorAlert && (
<CustomAlert
open={showEmailErrorAlert}
onClose={() => setShowEmailErrorAlert(false)}
message={t('completion.emailCorrectForm')}
severity="error"
duration={4000}
delayOnClose={2000}
rtl
/>
)}
</Box>
{!emailVerified && codeSent && (
{email && (
<Typography
sx={{ color: correctEmail ? 'success.main' : 'error.main' }}
variant="caption"
>
{correctEmail ? '' : t('completion.emailCorrectForm')}
</Typography>
)}
{!emailVerified && codeSent && correctEmail && (
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
flexWrap: 'wrap',
gap: 2,
justifyContent: 'center',
'@media(min-width: 600px)': {
justifyContent: 'flex-start',
},
}}
>
<TextField
@@ -165,8 +212,7 @@ export function EmailSection({
variant="outlined"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
fullWidth={false}
sx={{ flex: 1 }}
sx={fieldSx}
disabled={isVerifyingCode}
/>
<Button
@@ -200,6 +246,16 @@ export function EmailSection({
)}
</Box>
)}
<CustomAlert
open={showSuccessAlert}
onClose={() => setShowSuccessAlert(false)}
message="ایمیل با موفقیت تایید شد"
severity="success"
duration={4000}
delayOnClose={2000}
rtl
/>
</>
);
}

View File

@@ -58,6 +58,12 @@ export function PasswordSection({
const handleTogglePasswordRepetitionEye = () =>
setShowPasswordRepetitionText((prev) => !prev);
const fieldSx = {
flex: '1 1 260px',
maxWidth: '309px',
width: '100%',
};
return (
<>
<FormGroup>
@@ -65,7 +71,7 @@ export function PasswordSection({
sx={{
display: 'flex',
gap: 0.5,
px: { xs: 2, sm: 6 },
px: { xs: 2, sm: -4 },
alignItems: 'center',
}}
>
@@ -89,18 +95,27 @@ export function PasswordSection({
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 6 },
px: { xs: 2, sm: 0 },
}}
>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
justifyContent: 'center',
'@media(min-width: 600px)': {
justifyContent: 'flex-start',
},
}}
>
<TextField
label={t('completion.password')}
value={password}
onChange={(e) => setPassword(e.target.value)}
variant="outlined"
type={showPasswordText ? 'text' : 'password'}
fullWidth
sx={{ maxWidth: '309px', flex: 1 }}
sx={fieldSx}
InputProps={{
endAdornment: (
<InputAdornment position="end" sx={{ height: 'unset' }}>
@@ -155,9 +170,8 @@ export function PasswordSection({
? t('completion.notCompatibility')
: ' '
}
fullWidth
sx={{ maxWidth: '309px', flex: 1 }}
type={showPasswordRepititonText ? 'text' : 'password'}
sx={fieldSx}
InputProps={{
endAdornment: (
<InputAdornment position="end" sx={{ height: 'unset' }}>
@@ -224,11 +238,20 @@ export function PasswordSection({
</Box>
{password && showValidations && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
justifyContent: 'center',
'@media(min-width: 600px)': {
justifyContent: 'flex-start',
},
}}
>
<Box
sx={{
flex: 1,
minWidth: '260px',
flex: '1 1 260px',
maxWidth: '317px',
display: 'flex',
flexDirection: 'column',
@@ -245,8 +268,7 @@ export function PasswordSection({
</Box>
<Box
sx={{
flex: 1,
minWidth: '260px',
flex: '1 1 260px',
maxWidth: '317px',
display: 'flex',
flexDirection: 'column',

View File

@@ -49,103 +49,140 @@ export function PersonalInfoFields({
setCountryError(Boolean(value.trim() && !exists));
};
const fieldSx = { width: { xs: '100%', sm: '48%', md: '48%' } };
const fieldSx = {
flex: '1 1 260px',
maxWidth: '309px',
width: '100%',
};
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 4, md: 6 },
}}
>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
<TextField
label={t('completion.name')}
placeholder={t('completion.name')}
variant="outlined"
sx={fieldSx}
/>
<TextField
label={t('completion.familyName')}
placeholder={t('completion.familyName')}
variant="outlined"
sx={fieldSx}
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
width: '100%',
px: { xs: 2, sm: 4, md: 1 },
}}
>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
justifyContent: 'center',
'@media(min-width: 600px)': {
justifyContent: 'flex-start',
},
}}
>
<TextField
label={t('completion.name')}
placeholder={t('completion.name')}
variant="outlined"
sx={fieldSx}
/>
<TextField
label={t('completion.familyName')}
placeholder={t('completion.familyName')}
variant="outlined"
sx={fieldSx}
/>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
<FormControl sx={fieldSx}>
<InputLabel>{t('completion.gender')}</InputLabel>
<Select
value={sex}
label={t('completion.gender')}
onChange={handleChangeSex}
>
<MenuItem value="female">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Woman size={20} color="#F50057" />
{t('completion.woman')}
</Box>
</MenuItem>
<MenuItem value="male">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Man size={20} color="#0091EA" />
{t('completion.man')}
</Box>
</MenuItem>
</Select>
</FormControl>
<TextField
label={t('completion.optionalNationalCode')}
placeholder={t('completion.optionalNationalCode')}
variant="outlined"
sx={fieldSx}
/>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
<Autocomplete
options={countries}
getOptionLabel={(c) => (typeof c === 'string' ? c : c.fa)}
value={countries.find((c) => c.name === country) || null}
onChange={handleChangeCountry}
onInputChange={handleInputChange}
freeSolo
noOptionsText=""
renderOption={(props, c) => (
<Box
component="li"
{...props}
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
justifyContent: 'center',
'@media(min-width: 600px)': {
justifyContent: 'flex-start',
},
}}
>
<FormControl sx={fieldSx}>
<InputLabel>{t('completion.gender')}</InputLabel>
<Select
value={sex}
label={t('completion.gender')}
onChange={handleChangeSex}
>
<img
src={`https://flagcdn.com/w40/${c.flag}.png`}
width="24"
height="24"
style={{ borderRadius: '2px', border: '1px solid #ccc' }}
alt={c.name}
/>
{c.fa}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label={t('completion.country')}
variant="outlined"
error={countryError}
helperText={countryError ? t('completion.invalidCountry') : ''}
onBlur={handleCountryBlur}
/>
)}
sx={fieldSx}
/>
<MenuItem value="female">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Woman size={20} color="#F50057" />
{t('completion.woman')}
</Box>
</MenuItem>
<MenuItem value="male">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Man size={20} color="#0091EA" />
{t('completion.man')}
</Box>
</MenuItem>
</Select>
</FormControl>
<Box sx={fieldSx}>
<DateOfBirth />
<TextField
label={t('completion.optionalNationalCode')}
placeholder={t('completion.optionalNationalCode')}
variant="outlined"
sx={fieldSx}
/>
</Box>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
justifyContent: 'center',
'@media(min-width: 600px)': {
justifyContent: 'flex-start',
},
}}
>
<Autocomplete
options={countries}
getOptionLabel={(c) => (typeof c === 'string' ? c : c.fa)}
value={countries.find((c) => c.name === country) || null}
onChange={handleChangeCountry}
onInputChange={handleInputChange}
freeSolo
noOptionsText=""
renderOption={(props, c) => (
<Box
component="li"
{...props}
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
>
<img
src={`https://flagcdn.com/w40/${c.flag}.png`}
width="24"
height="24"
style={{ borderRadius: '2px', border: '1px solid #ccc' }}
alt={c.name}
/>
{c.fa}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label={t('completion.country')}
variant="outlined"
error={countryError}
helperText={countryError ? t('completion.invalidCountry') : ''}
onBlur={handleCountryBlur}
/>
)}
sx={fieldSx}
/>
<Box sx={fieldSx}>
<DateOfBirth />
</Box>
</Box>
</Box>
</Box>

View File

@@ -1,36 +1,106 @@
import { Box, Button, Typography, Link } from '@mui/material';
import { useState } from 'react';
import {
Box,
Button,
Typography,
Link,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
export function SubmitSection() {
const { t } = useTranslation('completionForm');
const { t, i18n } = useTranslation('completionForm');
const [openDialog, setOpenDialog] = useState(false);
const handleOpenDialog = (e: React.MouseEvent) => {
e.preventDefault();
setOpenDialog(true);
};
const handleCloseDialog = () => setOpenDialog(false);
const agreementText = `۱. محرمانگی اطلاعات
 هارمونی متعهد می‌شود تحت هیچ شرایطی اطلاعات هویتی کاربران نظیر شماره تلفن، ایمیل، رمز عبور، شناسه کاربری و هرگونه داده مرتبط را در اختیار اشخاص ثالث قرار ندهد. اطلاعات کاربران صرفاً در چارچوب ارائه خدمات احراز هویت مورد استفاده قرار گرفته و حتی پس از غیرفعال‌سازی حساب یا قطع همکاری، این اطلاعات محرمانه باقی خواهد ماند. هارمونی موظف به پیاده‌سازی تدابیر امنیتی لازم برای جلوگیری از هرگونه دسترسی غیرمجاز می‌باشد.
۲. مسئولیت حفظ اطلاعات ورود
 کاربر موظف است از حساب کاربری خود محافظت کند و رمز عبوری ایمن و غیرقابل حدس انتخاب نماید. تغییر دوره‌ای رمز عبور و اقدام فوری در صورت احساس خطر دسترسی غیرمجاز الزامی است. مسئولیت هرگونه سوءاستفاده از حساب کاربری به دلیل بی‌احتیاطی کاربر، بر عهده خود وی خواهد بود.
۳. رخنه‌های امنیتی و حملات سایبری
 هارمونی در برابر رخنه‌های امنیتی ناشی از حملات سایبری که خارج از کنترل سیستم است، مسئولیتی ندارد. با این حال، هارمونی از به‌روزترین استانداردهای امنیتی و رمزنگاری برای جلوگیری از چنین حوادثی بهره می‌برد.
قصور کاربر در حفاظت از اطلاعات
 چنانچه اطلاعات حساب کاربری به دلیل سهل‌انگاری یا اشتباه کاربر افشا شود، هارمونی مسئولیتی در این خصوص ندارد. تشخیص چنین مواردی بر اساس لاگ‌های امنیتی سیستم، بر عهده مدیر فنی هارمونی خواهد بود.
۵. ثبت دقیق فعالیت‌ها در لاگ‌ها
 تمامی رویدادهای مرتبط با ثبت، ویرایش و حذف اطلاعات در سیستم، به‌صورت دقیق و غیرقابل‌تغییر در لاگ کاربران ثبت می‌گردد. هرگونه ادعا مبنی بر حذف یا تغییر داده‌ها بدون ردپای لاگ قابل پذیرش نیست مگر با ارائه مستندات از سوی کاربر.
۶. به‌روزرسانی خدمات
 خدمات هارمونی ممکن است به مرور زمان به‌روزرسانی یا تغییر پیدا کند. ادامه استفاده از سیستم پس از اعمال تغییرات به معنای پذیرش مقررات جدید است. در صورت عدم موافقت، کاربر می‌تواند درخواست حذف حساب خود را ارائه دهد.
۷. پشتیبانی کاربران
 پشتیبانی خدمات صرفاً از طریق ایمیل و تماس تلفنی صورت می‌گیرد و رایگان است. هارمونی تعهدی به ارائه پشتیبانی حضوری یا آموزش‌های فراتر از خدمات پایه ندارد.
۸. راه‌های ارتباط رسمی
 هارمونی تنها از طریق شماره تلفن و ایمیل ثبت‌شده در حساب کاربر با وی در ارتباط خواهد بود. اطلاعیه‌ها و اعلانات رسمی از این مسیرها انجام می‌گیرد.
۹. دامنه‌های رسمی ارتباط
 تمام ایمیل‌های ارسالی از سوی هارمونی صرفاً با دامنه‌ی harmony.id ارسال می‌شود. کاربران موظف به بررسی این نشانی برای جلوگیری از فیشینگ و حملات مشابه هستند.
۱۰. رعایت قوانین جمهوری اسلامی ایران
 کاربر موظف است در استفاده از سیستم، کلیه قوانین جاری کشور، از جمله «قانون تجارت الکترونیکی»، «قانون جرائم رایانه‌ای» و سایر قوانین مرتبط را رعایت نماید. مسئولیت هرگونه تخلف بر عهده کاربر خواهد بود.
۱۱. نگهداری موقت اطلاعات پس از فسخ حساب
 در صورت فسخ یا حذف حساب، اطلاعات کاربر به مدت ۳۰ روز در فضای امن نگهداری می‌شود و پس از آن، به‌طور غیرقابل‌بازگشت حذف خواهد شد.
۱۲. مالکیت اطلاعات کاربر
 تمام اطلاعات ثبت‌شده توسط کاربر متعلق به خود اوست و هارمونی هیچ‌گونه مالکیتی بر این اطلاعات ندارد. کاربر مسئول صحت، کیفیت و قانونی بودن داده‌های خود می‌باشد.
۱۳. استفاده هدفمند از اطلاعات شناسایی
 اطلاعات هویتی جمع‌آوری‌شده هنگام ثبت‌نام تنها برای احراز هویت و ارائه خدمات پایه مورد استفاده قرار می‌گیرد. این اطلاعات بدون رضایت صریح کاربر، به هیچ نهاد یا شخص ثالثی منتقل نخواهد شد.
تبصره: اطلاعات هویتی کاربران صرفاً در صورت حکم مقام قضایی یا مراجع ذی‌صلاح و در چارچوب قوانین، قابل ارائه خواهد بود.
۱۴. محرمانگی دائمی داده‌ها
 هارمونی متعهد است حتی پس از اتمام رابطه کاربری یا انحلال حساب، اطلاعات جمع‌آوری‌شده را به عنوان محرمانه حفظ نماید.
۱۵. محدودیت مسئولیت
 هارمونی مسئولیتی در قبال خسارات مستقیم یا غیرمستقیمی که به دلیل استفاده یا عدم استفاده از خدمات احراز هویت ایجاد شود، نخواهد داشت.
۱۶. اختلال در بسترهای ارتباطی
 هارمونی در برابر اختلال‌های ناشی از شبکه اینترنت، خدمات زیرساختی یا هرگونه مشکل خارج از کنترل خود، مسئولیتی ندارد.
۱۷. حوادث قهری و غیرمترقبه
 در صورت وقوع بلایای طبیعی، اعتصاب، قطعی برق، حملات سایبری یا هرگونه رخداد خارج از کنترل هارمونی که مانع ارائه خدمات شود، مسئولیتی متوجه هارمونی نخواهد بود.
۱۸. خدمات وابسته به سایر سرویس‌ها
 چنانچه بخشی از خدمات احراز هویت توسط شرکت‌های ثالث ارائه شود، قوانین استفاده از این سرویس‌ها بر عهده همان شرکت‌هاست و هارمونی نسبت به آن‌ها مسئولیتی ندارد.
۱۹. تضمین دسترسی به اطلاعات در صورت توقف فعالیت
 در صورت توقف دائمی فعالیت هارمونی، این شرکت متعهد است به مدت دو سال، سرورها را فعال نگه دارد و امکان دسترسی کاربران به اطلاعات خود را فراهم سازد.
۲۰. اطلاع‌رسانی در مورد قطع سرویس‌ها
 در صورت نیاز به توقف خدمات، هارمونی موظف است حداقل ۱۲ ساعت قبل، این موضوع را از طریق ایمیل یا پیامک به اطلاع کاربران برساند.`;
return (
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'stretch', sm: 'center' },
gap: 2,
px: { xs: 2, sm: 4, md: 6 },
mb: '17px',
}}
>
<Typography variant="body2" sx={{ flex: 1 }} color="text.primary">
{t('completion.agreementPart1')}
<Link target="_blank" rel="noopener" href="#">
{t('completion.agreementLinkText')}
</Link>{' '}
{t('completion.agreementPart2')}
</Typography>
<Button
variant="contained"
<>
<Box
sx={{
width: { xs: '100%', sm: '247px' },
alignSelf: { xs: 'stretch', sm: 'auto' },
display: 'flex',
flexWrap: 'wrap',
gap: 2,
px: { xs: 2, sm: 0 },
mb: '17px',
justifyContent: { xs: 'center', sm: 'flex-start' },
}}
>
{t('completion.registerButton')}
</Button>
</Box>
<Typography
variant="body2"
sx={{
flex: '1 1 260px',
maxWidth: 'calc(100% - 263px)',
minWidth: '260px',
}}
color="text.primary"
>
{t('completion.agreementPart1')}{' '}
<Link href="#" onClick={handleOpenDialog}>
{t('completion.agreementLinkText')}
</Link>{' '}
{t('completion.agreementPart2')}
</Typography>
<Button
variant="contained"
sx={{
width: { xs: '100%', sm: '247px' },
alignSelf: { xs: 'stretch', sm: 'center' },
}}
>
{t('completion.registerButton')}
</Button>
</Box>
<Dialog
open={openDialog}
onClose={handleCloseDialog}
fullWidth
maxWidth="md"
dir={i18n.language.startsWith('fa') ? 'rtl' : 'ltr'}
>
<DialogTitle>
{t('completion.rules') || t('completion.rules')}
</DialogTitle>
<DialogContent
dividers
sx={{ whiteSpace: 'pre-line', fontSize: '14px' }}
>
{agreementText}
</DialogContent>
</Dialog>
</>
);
}