Merge branch 'develop' into feat/auth
This commit is contained in:
63
src/components/CardContainer.tsx
Normal file
63
src/components/CardContainer.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
interface CardContainerProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
action?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
export function CardContainer({
|
||||
title,
|
||||
subtitle,
|
||||
action,
|
||||
children,
|
||||
highlighted,
|
||||
}: CardContainerProps) {
|
||||
return (
|
||||
<Box sx={{ width: '100%', bgcolor: 'background.paper' }}>
|
||||
<Box
|
||||
sx={{
|
||||
marginInline: 'auto',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
bgcolor: highlighted ? 'primary.light' : 'background.default',
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color={highlighted ? 'primary.main' : 'text.primary'}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={highlighted ? 'primary.main' : 'text.secondary'}
|
||||
variant="body2"
|
||||
>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
{action}
|
||||
</Box>
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
41
src/components/CountDownTimer.tsx
Normal file
41
src/components/CountDownTimer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
|
||||
interface CountdownTimerProps {
|
||||
initialSeconds: number;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function CountDownTimer({
|
||||
initialSeconds,
|
||||
onComplete,
|
||||
}: CountdownTimerProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
|
||||
|
||||
useEffect(() => {
|
||||
setSecondsLeft(initialSeconds);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setSecondsLeft((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer);
|
||||
onComplete?.();
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [initialSeconds, onComplete]);
|
||||
|
||||
const formatTime = (totalSeconds: number) => {
|
||||
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
||||
const seconds = String(totalSeconds % 60).padStart(2, '0');
|
||||
return toLocaleDigits(`${minutes}:${seconds}`, i18n.language);
|
||||
};
|
||||
|
||||
return <span>{formatTime(secondsLeft)}</span>;
|
||||
}
|
||||
40
src/components/CountryFlag.tsx
Normal file
40
src/components/CountryFlag.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// TODO: move countries outside of feature directory
|
||||
import { countries } from '@/features/authentication/data/Countries';
|
||||
|
||||
interface CountryFlagProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
export function CountryFlag({ code }: CountryFlagProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const countryObj = code ? countries.find((c) => c.code === code) : null;
|
||||
|
||||
if (!countryObj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayName = t(countryObj.label);
|
||||
const flagUrl = `https://flagcdn.com/w40/${countryObj.code.toLowerCase()}.png`;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box
|
||||
component="img"
|
||||
loading="lazy"
|
||||
src={flagUrl}
|
||||
alt={displayName}
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 16,
|
||||
borderRadius: 0.5,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2">{displayName}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
85
src/components/ThemToggle.tsx
Normal file
85
src/components/ThemToggle.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ToggleButtonGroup, ToggleButton, Box } from '@mui/material';
|
||||
import { Sun1, Moon } from 'iconsax-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type MouseEvent } from 'react';
|
||||
|
||||
enum ThemeMode {
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
||||
|
||||
interface ThemeToggleButtonProps {
|
||||
value: 'light' | 'dark';
|
||||
onChange: (newMode: 'light' | 'dark') => void;
|
||||
}
|
||||
|
||||
export const ThemeToggleButton = ({
|
||||
value,
|
||||
onChange,
|
||||
}: ThemeToggleButtonProps) => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const handleChange = (
|
||||
_event: MouseEvent<HTMLElement>,
|
||||
newMode: 'light' | 'dark' | null,
|
||||
) => {
|
||||
if (newMode !== null) {
|
||||
onChange(newMode);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<ToggleButtonGroup
|
||||
value={value}
|
||||
exclusive
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
borderRadius: 1.5,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
value={ThemeMode.Light}
|
||||
aria-label="light theme"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.light',
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon Component={Sun1} color="primary.main" variant="Bold" />
|
||||
{t('settings.light')}
|
||||
</ToggleButton>
|
||||
|
||||
<ToggleButton
|
||||
value={ThemeMode.Dark}
|
||||
aria-label="dark theme"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.light',
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon Component={Moon} size="medium" color="primary.light" />
|
||||
{t('settings.dark')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user