dark mode v1

This commit is contained in:
hamid zarghami
2025-08-18 15:23:31 +03:30
parent 561bf9d5d5
commit 916b88731c
49 changed files with 655 additions and 371 deletions
+20 -17
View File
@@ -19,6 +19,7 @@ import Login from './pages/auth/Login';
import { EmailWebSocketProvider } from './contexts/EmailWebSocketContext';
import SocketManagment from './lib/SocketManagment';
import NajvaToken from './components/NajvaToken';
import { ThemeProvider } from './contexts/ThemeContext';
declare global {
interface Window {
@@ -121,23 +122,25 @@ const App: FC = () => {
}, []);
return (
<BrowserRouter>
<I18nextProvider i18n={i18next}>
<QueryClientProvider client={queryClient}>
{isLogin === 'isLogin' && userToken ? (
<EmailWebSocketProvider token={userToken}>
<Main />
<SocketManagment />
<NajvaToken />
</EmailWebSocketProvider>
) : isLogin === 'isNotLogin' ? (
<Login />
) : null}
<NewMessage />
<ToastContainer />
</QueryClientProvider>
</I18nextProvider>
</BrowserRouter>
<ThemeProvider>
<BrowserRouter>
<I18nextProvider i18n={i18next}>
<QueryClientProvider client={queryClient}>
{isLogin === 'isLogin' && userToken ? (
<EmailWebSocketProvider token={userToken}>
<Main />
<SocketManagment />
<NajvaToken />
</EmailWebSocketProvider>
) : isLogin === 'isNotLogin' ? (
<Login />
) : null}
<NewMessage />
<ToastContainer />
</QueryClientProvider>
</I18nextProvider>
</BrowserRouter>
</ThemeProvider>
)
}
+19 -18
View File
@@ -1,39 +1,40 @@
import { FC, ReactNode } from 'react'
import { FC, ReactNode, ButtonHTMLAttributes } from 'react'
import { clx } from '../helpers/utils'
import MoonLoader from "react-spinners/ClipLoader"
interface ButtonProps {
label?: string;
children?: ReactNode;
className?: string;
loading?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
type ButtonBaseProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'>
interface ButtonProps extends ButtonBaseProps {
label?: string
children?: ReactNode
className?: string
loading?: boolean
variant?: 'primary' | 'secondary'
}
const Button: FC<ButtonProps> = (props) => {
const { label, children, className, loading, variant, type, disabled, ...rest } = props
const buttonClass = clx(
'w-full bg-primary text-white cursor-pointer rounded-xl font-normal text-xs md:text-sm h-10 px-3 md:px-4 transition-all duration-200 hover:bg-opacity-90 disabled:opacity-50 disabled:cursor-not-allowed',
props.variant === 'secondary' && 'bg-[#ECEEF5] text-black',
props.className
'w-full bg-primary text-primary-foreground cursor-pointer rounded-xl font-normal text-xs md:text-sm h-10 px-3 md:px-4 transition-all duration-200 hover:bg-opacity-90 disabled:opacity-50 disabled:cursor-not-allowed',
variant === 'secondary' && 'bg-[#ECEEF5] text-black',
className
)
return (
<button
onClick={props.onClick}
type={props.type || 'button'}
disabled={props.disabled || props.loading}
type={type || 'button'}
disabled={disabled || loading}
className={buttonClass}
{...rest}
>
{props.loading ? (
<MoonLoader className='mt-1.5' size={15} color={props.variant === 'secondary' ? '#000' : '#fff'} />
{loading ? (
<MoonLoader className='mt-1.5' size={15} color={'currentColor'} />
) : (
props.children || props.label
children || label
)}
</button>
)
+6 -3
View File
@@ -1,5 +1,5 @@
import { FC, Fragment } from 'react'
import Td from './Td'
import Td from '@/components/Td'
import Skeleton from 'react-loading-skeleton'
type Props = {
@@ -8,15 +8,18 @@ type Props = {
}
const DefaultTableSkeleton: FC<Props> = ({ tdCount, trCount = 5 }) => {
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark')
const baseColor = isDark ? '#2d2d30' : '#e5e7eb'
const highlightColor = isDark ? '#3c3c3c' : '#f3f4f6'
return (
<Fragment>
{
Array.from({ length: trCount }).map((_, rowIndex) => (
<tr className="w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5]" key={rowIndex}>
<tr className="w-full h-[64px] md:h-[74px] bg-surface border-t border-surface" key={rowIndex}>
{
Array.from({ length: tdCount }).map((_, colIndex) => (
<Td text={''} key={colIndex}>
<Skeleton />
<Skeleton baseColor={baseColor} highlightColor={highlightColor} />
</Td>
))
}
+12 -11
View File
@@ -3,6 +3,7 @@ import { useEmailWebSocket } from '@/hooks/useEmailWebSocket';
import { useEmailEvents, EmailPayload, UnreadCountPayload, EmailStatusPayload } from '@/hooks/useEmailEvents';
import { useNotificationSound } from '@/hooks/useNotificationSound';
import { Notification as NotificationIcon, VolumeHigh } from 'iconsax-react';
import { getIconColor } from '@/utils/colorUtils';
interface EmailNotificationsProps {
userToken: string;
@@ -94,17 +95,17 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
onClick={() => {
playSound();
}}
className="p-2 rounded-full hover:bg-gray-100 transition-colors"
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="تست صدا"
>
<VolumeHigh size={20} color="black" />
<VolumeHigh size={20} color={getIconColor('primary')} />
</button>
<div
className="relative cursor-pointer p-2 rounded-full hover:bg-gray-100 transition-colors"
className="relative cursor-pointer p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
onClick={() => setIsVisible(!isVisible)}
>
<NotificationIcon size={24} color="black" />
<NotificationIcon size={24} color={getIconColor('primary')} />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full min-w-[18px] h-[18px] flex items-center justify-center font-bold">
{unreadCount > 99 ? '99+' : unreadCount}
@@ -113,20 +114,20 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
</div>
{isVisible && (
<div className="absolute top-full left-0 w-80 max-h-96 bg-white border border-gray-200 rounded-lg shadow-lg z-50 overflow-hidden">
<div className="p-3 bg-gray-50 border-b border-gray-200 flex justify-between items-center">
<div className="absolute top-full left-0 w-80 max-h-96 bg-white dark:bg-[#252526] border border-gray-200 dark:border-[#3e3e42] rounded-lg shadow-lg z-50 overflow-hidden">
<div className="p-3 bg-gray-50 dark:bg-[#2d2d30] border-b border-gray-200 dark:border-[#3e3e42] flex justify-between items-center">
<h3 className="font-semibold">📧 نوتیفیکیشنهای ایمیل</h3>
<span className="text-sm text-gray-600">{unreadCount} خوانده نشده</span>
<span className="text-sm text-gray-600 dark:text-gray-400">{unreadCount} خوانده نشده</span>
</div>
<div className="max-h-72 overflow-y-auto">
{notifications.length === 0 ? (
<div className="p-4 text-center text-gray-500">ایمیل جدیدی وجود ندارد</div>
<div className="p-4 text-center text-gray-500 dark:text-gray-400">ایمیل جدیدی وجود ندارد</div>
) : (
notifications.map((email) => (
<div
key={email.messageId}
className="p-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 transition-colors"
className="p-3 border-b border-gray-100 dark:border-[#2d2d30] cursor-pointer hover:bg-gray-50 dark:hover:bg-[#2d2d30] transition-colors"
onClick={() => onEmailClick?.(email)}
>
<div className="flex items-center justify-between mb-1">
@@ -136,8 +137,8 @@ export const EmailNotifications: React.FC<EmailNotificationsProps> = ({
{email.hasAttachments && <span>📎</span>}
</div>
<div className="font-medium text-sm mb-1 truncate">{email.subject}</div>
<div className="text-xs text-gray-600 mb-1 line-clamp-2">{email.preview}</div>
<div className="text-xs text-gray-400">
<div className="text-xs text-gray-600 dark:text-gray-400 mb-1 line-clamp-2">{email.preview}</div>
<div className="text-xs text-gray-400 dark:text-gray-500">
{new Date(email.timestamp).toLocaleString('fa-IR')}
</div>
</div>
+6 -5
View File
@@ -1,6 +1,7 @@
import { FC, InputHTMLAttributes, useEffect, useState } from 'react'
import { clx } from '../helpers/utils';
import { Eye, SearchNormal } from 'iconsax-react';
import { getIconColor } from '@/utils/colorUtils';
import Error from './Error';
type Variant = "floating_outlined" | "primary" | "search";
@@ -34,8 +35,8 @@ const Input: FC<Props> = (props: Props) => {
const [search, setSearch] = useState<string>('')
const inputClass = clx(
'w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border',
props.readOnly && 'bg-gray-100 border-0 text-description',
'w-full input-surface h-10 block px-4 text-xs rounded-xl transition-colors',
props.readOnly && 'bg-muted border-0 text-muted-foreground',
props.variant === 'search' && 'ps-10',
props.className
);
@@ -79,7 +80,7 @@ const Input: FC<Props> = (props: Props) => {
return (
<div className='w-full'>
<label className='text-sm'>
<label className='text-sm text-primary-content'>
{props.label}
</label>
@@ -94,12 +95,12 @@ const Input: FC<Props> = (props: Props) => {
{
props.type === 'password' &&
<Eye onClick={() => setShowPassword((oldValue) => !oldValue)} size={20} color='#8C90A3' className='absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
<Eye onClick={() => setShowPassword((oldValue) => !oldValue)} size={20} color={getIconColor('muted')} className='absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
}
{
props.variant === 'search' &&
<SearchNormal size={20} color='#8C90A3' className='absolute pointer-events-none top-0 w-5 bottom-0 my-auto right-3' />
<SearchNormal size={20} color={getIconColor('muted')} className='absolute pointer-events-none top-0 w-5 bottom-0 my-auto right-3' />
}
{
+41 -41
View File
@@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { More } from 'iconsax-react';
import React, { useState, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { More } from 'iconsax-react'
import { getIconColor } from '@/utils/colorUtils'
export interface RowActionItem {
label: string;
@@ -15,66 +16,66 @@ interface RowActionsDropdownProps {
}
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, className = '' }) => {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false)
const [position, setPosition] = useState({ top: 0, left: 0 })
const dropdownRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
setIsOpen(false)
}
};
}
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
e.stopPropagation()
if (!isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
const isMobile = window.innerWidth < 768;
const dropdownWidth = isMobile ? 140 : 150;
const dropdownHeight = actions.length * (isMobile ? 36 : 40);
const rect = buttonRef.current.getBoundingClientRect()
const isMobile = window.innerWidth < 768
const dropdownWidth = isMobile ? 140 : 150
const dropdownHeight = actions.length * (isMobile ? 36 : 40)
let left = rect.left + window.scrollX - dropdownWidth + rect.width;
let top = rect.bottom + window.scrollY;
let left = rect.left + window.scrollX - dropdownWidth + rect.width
let top = rect.bottom + window.scrollY
if (left + dropdownWidth > window.innerWidth) {
left = rect.left + window.scrollX - dropdownWidth;
left = rect.left + window.scrollX - dropdownWidth
}
if (left < 0) {
left = isMobile ? 20 : 40;
left = isMobile ? 20 : 40
}
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - dropdownHeight;
top = rect.top + window.scrollY - dropdownHeight
}
setPosition({ top, left });
setPosition({ top, left })
}
setIsOpen(!isOpen);
};
setIsOpen(!isOpen)
}
const handleActionClick = (action: RowActionItem) => {
action.onClick();
setIsOpen(false);
};
action.onClick()
setIsOpen(false)
}
const renderDropdown = () => {
if (!isOpen) return null;
if (!isOpen) return null
return createPortal(
<div
ref={dropdownRef}
className="fixed py-3 md:py-3 bg-white rounded-xl md:rounded-2xl shadow-lg z-[5] min-w-[140px] md:min-w-[150px]"
className="fixed py-3 md:py-3 bg-white dark:bg-[#252526] rounded-xl md:rounded-2xl shadow-lg z-[5] min-w-[140px] md:min-w-[150px] border border-gray-200 dark:border-[#3e3e42]"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
@@ -84,31 +85,30 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, classN
<button
key={index}
onClick={() => handleActionClick(action)}
className={`w-full mt-1 text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 ${index === 0 ? 'rounded-t-lg' : ''
} ${action.className || ''}`}
className={`w-full mt-1 text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-[#2d2d30] ${index === 0 ? 'rounded-t-lg' : ''} ${action.className || ''}`}
>
{action.icon && <span className="ml-2">{action.icon}</span>}
<span className={action.label === 'حذف' ? 'text-red-500' : ''}>{action.label}</span>
<span className={`${action.label === 'حذف' ? 'text-red-500' : ''} dark:text-gray-200`}>{action.label}</span>
</button>
))}
</div>,
document.body
);
};
)
}
return (
<div className={className}>
<button
ref={buttonRef}
onClick={handleToggle}
className="mt-2 hover:bg-gray-100 rounded-full transition-colors"
className="mt-2 hover:bg-gray-100 dark:hover:bg-[#2d2d30] rounded-full transition-colors"
>
<More size={16} color="black" className="rotate-90" />
<More size={16} color={getIconColor('primary')} className="rotate-90" />
</button>
{renderDropdown()}
</div>
);
};
)
}
export default RowActionsDropdown;
export default RowActionsDropdown
+5 -4
View File
@@ -1,6 +1,7 @@
import { FC, SelectHTMLAttributes } from 'react'
import { clx } from '../helpers/utils'
import { ArrowDown2 } from 'iconsax-react'
import { getIconColor } from '@/utils/colorUtils'
export type ItemsSelectType = {
value: string,
@@ -20,14 +21,14 @@ const Select: FC<Props> = (props: Props) => {
<div className='w-full'>
{
props.label &&
<label className='text-sm'>
<label className='text-sm text-primary-content'>
{props.label}
</label>
}
<div className='relative'>
<select {...props} className={clx(
'w-full text-black relative block border appearance-none border-border px-2.5 h-10 text-sm rounded-2.5 bg-white',
props.readOnly && 'bg-gray-100 border-0 text-description',
'w-full input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-2.5 transition-colors',
props.readOnly && 'bg-muted border-0 text-muted-foreground',
props.className,
props.label && 'mt-1'
)}>
@@ -46,7 +47,7 @@ const Select: FC<Props> = (props: Props) => {
}
</select>
<ArrowDown2 size={16} color='black' className={clx(
<ArrowDown2 size={16} color={getIconColor('muted')} className={clx(
'absolute z-0 top-3 left-2 pointer-events-none',
)} />
</div>
+21 -21
View File
@@ -15,7 +15,7 @@ const Table = <T extends RowDataType>({
className = '',
rowClassName = '',
noDataMessage = 'هیچ داده‌ای یافت نشد.',
headerClassName = 'bg-gray-50',
headerClassName = 'bg-muted',
emptyRowsCount = 5,
showHeader = true,
actions = null,
@@ -150,9 +150,9 @@ const Table = <T extends RowDataType>({
if (isLoading) {
return (
<div className={`bg-white ${roundedClass} overflow-hidden`}>
<div className={`bg-surface ${roundedClass} overflow-hidden`}>
{Array.from({ length: emptyRowsCount }).map((_, index) => (
<div key={index} className={`h-[48px] bg-gray-100 animate-pulse ${index !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}></div>
<div key={index} className={`h-[48px] bg-muted animate-pulse ${index !== 0 ? 'border-t border-surface' : ''}`}></div>
))}
</div>
);
@@ -160,25 +160,25 @@ const Table = <T extends RowDataType>({
if (data.length === 0) {
return (
<div className={`bg-white ${roundedClass} text-center text-gray-500`}>
<img src={NoData} alt='no data' className='w-[100px] mx-auto pt-[70px]' />
<div className={`bg-surface ${roundedClass} text-center text-muted-foreground`}>
<img src={NoData} alt='no data' className='w-[100px] mx-auto pt-[70px] filterWhite' />
<div className='-mt-5 pb-[70px]'>{noDataMessage}</div>
</div>
);
}
return (
<div className={`bg-white ${roundedClass} overflow-hidden`}>
<div className={`bg-surface ${roundedClass} overflow-hidden`}>
{data.map((item, rowIndex) => {
const isUnread = 'seen' in item && !item.seen;
const bgColor = isUnread ? 'bg-white' : 'bg-gray-50';
const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100';
const bgColor = isUnread ? 'bg-surface' : 'bg-muted';
const hoverColor = isUnread ? 'hover:bg-muted' : 'hover:bg-accent';
const fontWeight = isUnread ? 'font-semibold' : 'font-normal';
return (
<div
key={item.id}
className={`w-full min-h-[85px] ${bgColor} ${hoverColor} ${mobileSelectionMode || !onRowClick ? '' : 'cursor-pointer'} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''} ${longPressActiveId === item.id ? 'bg-blue-50' : ''}`}
className={`w-full min-h-[85px] ${bgColor} ${hoverColor} ${mobileSelectionMode || !onRowClick ? '' : 'cursor-pointer'} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-surface' : ''} ${longPressActiveId === item.id ? 'bg-accent' : ''}`}
onClick={() => handleMobileRowClick(item)}
onTouchStart={() => handleLongPressStart(item)}
onTouchEnd={handleLongPressEnd}
@@ -205,7 +205,7 @@ const Table = <T extends RowDataType>({
</div>
{columns.length > 1 && (
<div className={`flex items-center gap-3 text-xs text-gray-500 ${fontWeight}`}>
<div className={`flex items-center gap-3 text-xs text-muted-foreground ${fontWeight}`}>
{columns.slice(1).map((col) => (
<div key={col.key} className="truncate">
{col.render ? col.render(item) : (item[col.key] as React.ReactNode)}
@@ -242,8 +242,8 @@ const Table = <T extends RowDataType>({
if (data.length === 0) {
return (
<tr>
<td colSpan={columns.length + (selectable ? 1 : 0) + (inlineActions ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 pb-6 md:pb-14 border-t border-border text-center text-gray-500">
<img src={NoData} alt='no data' className='w-[100px] mx-auto pt-14' />
<td colSpan={columns.length + (selectable ? 1 : 0) + (inlineActions ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 pb-6 md:pb-14 border-t border-border text-center text-gray-500 dark:text-gray-400">
<img src={NoData} alt='no data' className='w-[100px] mx-auto pt-14 filterWhite' />
<div className='-mt-5'>{noDataMessage}</div>
</td>
</tr>
@@ -251,14 +251,14 @@ const Table = <T extends RowDataType>({
}
return data.map((item, rowIndex) => {
const isUnread = 'seen' in item && !item.seen;
const bgColor = isUnread ? 'bg-white' : 'bg-gray-50';
const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100';
const bgColor = isUnread ? 'bg-surface' : 'bg-muted';
const hoverColor = isUnread ? 'hover:bg-muted' : 'hover:bg-accent';
const fontWeight = isUnread ? 'font-semibold' : 'font-normal';
return (
<tr
key={item.id}
className={`w-full md:h-[64px] !h-[74px] ${bgColor} border-t border-[#EAEDF5] ${hoverColor} ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
className={`w-full md:h-[64px] !h-[74px] ${bgColor} border-t border-surface ${hoverColor} ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
@@ -318,7 +318,7 @@ const Table = <T extends RowDataType>({
if (!showHeader || actionsPosition === 'header-replace') return null;
return (
<thead className={`h-[60px] md:h-[69px] bg-white text-sm text-[#8C90A3] ${headerClassName}`}>
<thead className={`h-[60px] md:h-[69px] bg-surface text-sm text-muted-foreground ${headerClassName}`}>
<tr>
{selectable && (
<th
@@ -349,7 +349,7 @@ const Table = <T extends RowDataType>({
if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null;
return (
<div className="h-[64px] rounded-t-2xl md:rounded-t-3xl md:h-[74px] bg-white flex items-center px-3 md:px-6">
<div className="h-[64px] rounded-t-2xl md:rounded-t-3xl md:h-[74px] bg-surface flex items-center px-3 md:px-6">
<div className='flex items-center gap-4 md:gap-4'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
@@ -363,7 +363,7 @@ const Table = <T extends RowDataType>({
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
<div className={`md:hidden`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
<div className="h-[64px] bg-white flex items-center justify-between px-4 rounded-t-2xl border-b border-[#EAEDF5]">
<div className="h-[64px] bg-surface flex items-center justify-between px-4 rounded-t-2xl border-b border-surface">
<div className='flex items-center gap-4 md:gap-2'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
@@ -395,14 +395,14 @@ const Table = <T extends RowDataType>({
<div className={`hidden md:block`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
<div className="h-[74px] bg-white flex items-center px-6 rounded-t-3xl">
<div className="h-[74px] bg-surface flex items-center px-6 rounded-t-3xl">
<div className='flex items-center gap-4'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
</div>
</div>
)}
<div className={`overflow-x-auto overflow-hidden ${(actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions)) ? 'rounded-b-3xl' : 'rounded-3xl'} bg-white`}>
<div className={`overflow-x-auto overflow-hidden ${(actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions)) ? 'rounded-b-3xl' : 'rounded-3xl'} bg-surface`}>
<table className="w-full text-sm border-collapse min-w-[600px]">
<tbody>
{renderTableBody()}
@@ -428,7 +428,7 @@ const Table = <T extends RowDataType>({
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
<div className={`overflow-x-auto overflow-hidden ${showHeader && actionsPosition !== 'header-replace' ? 'rounded-2xl md:rounded-3xl' : 'rounded-b-2xl md:rounded-b-3xl'} bg-white`}>
<div className={`overflow-x-auto overflow-hidden ${showHeader && actionsPosition !== 'header-replace' ? 'rounded-2xl md:rounded-3xl' : 'rounded-b-2xl md:rounded-b-3xl'} bg-surface`}>
<table className="w-full text-sm border-collapse min-w-[600px]">
{renderHeader()}
{actionsPosition === 'header-replace' && (
+2 -2
View File
@@ -25,7 +25,7 @@ const Tabs: FC<Props> = (props: Props) => {
<Swiper
slidesPerView='auto'
spaceBetween={20}
className='px-4 md:px-8 xl:px-10 max-w-full w-fit items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-4 md:gap-6 xl:gap-10 flex h-[60px] md:h-[70px] xl:h-[80px] rounded-[20px] md:rounded-[28px] xl:rounded-[32px] bg-white bg-opacity-45'>
className='px-4 md:px-8 xl:px-10 max-w-full w-fit items-center text-description dark:text-gray-300 mx-auto backdrop-blur-md border-2 border-white dark:border-white/10 gap-4 md:gap-6 xl:gap-10 flex h-[60px] md:h-[70px] xl:h-[80px] rounded-[20px] md:rounded-[28px] xl:rounded-[32px] bg-white/45 dark:bg-white/10'>
{
props.items.map((item: Item, index: number) => {
return (
@@ -33,7 +33,7 @@ const Tabs: FC<Props> = (props: Props) => {
'flex flex-col max-w-fit mt-[10px] md:mt-[12px] xl:mt-[15px] items-center gap-1 md:gap-2 cursor-pointer',
index === 0 && 'pr-[15px] md:pr-[20px] xl:pr-[30px]',
index === props.items.length - 1 && props.items.length > 3 && 'pl-[5px]',
props.active === item.value && 'text-black'
props.active === item.value && 'text-black dark:text-white'
)}>
{item.icon}
<div className='text-[10px] md:text-xs'>
+45
View File
@@ -0,0 +1,45 @@
import { Moon, Sun1 } from 'iconsax-react'
import { useTheme } from '@/contexts/ThemeContext'
import Button from '@/components/Button'
import { getIconColor } from '@/utils/colorUtils'
const ThemeToggle = () => {
const { theme, toggleTheme } = useTheme()
const isDark = theme === 'dark'
const label = isDark ? 'تغییر به حالت روشن' : 'تغییر به حالت تاریک'
return (
<Button
onClick={toggleTheme}
variant="secondary"
title={label}
className={`group relative w-16 h-8 p-0 rounded-full transition-all duration-300
bg-secondary/90 dark:bg-secondary ring-1 ring-inset ring-border/60
hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`}
>
{/* Thumb */}
<span
className={`pointer-events-none absolute top-1 left-1 w-6 h-6 rounded-full border border-border z-0
bg-card shadow-[0_2px_6px_rgba(0,0,0,0.08)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.35)]
transition-transform duration-300 ease-out will-change-transform ${isDark ? 'translate-x-8' : 'translate-x-0'}`}
/>
{/* Icons */}
<span className="pointer-events-none absolute inset-0 z-10 grid grid-cols-2 place-items-center">
<Sun1
size={18}
color={isDark ? getIconColor('muted') : getIconColor('primary')}
className={`transition-opacity duration-300 ${isDark ? 'opacity-60' : 'opacity-100'}`}
/>
<Moon
size={18}
color={isDark ? getIconColor('primary') : getIconColor('muted')}
className={`transition-opacity duration-300 ${isDark ? 'opacity-100' : 'opacity-60'}`}
/>
</span>
</Button>
)
}
export default ThemeToggle
+5 -5
View File
@@ -53,7 +53,7 @@ const UploadBox: FC<Props> = (props: Props) => {
<div>
<div className='text-sm'>{props.label}</div>
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary dark:bg-[#2d2d30] dark:text-gray-200'>
<input {...getInputProps()} />
{t('select_file')}
@@ -62,9 +62,9 @@ const UploadBox: FC<Props> = (props: Props) => {
<div className='lg:flex gap-2 hidden'>
{
files?.map((item, index) => (
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
<div key={index} className='flex bg-gray-200 dark:bg-[#2d2d30] py-1.5 px-2 rounded-lg items-center gap-1'>
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
<div className='text-xs'>{item.name}</div>
<div className='text-xs dark:text-gray-300'>{item.name}</div>
</div>
))
}
@@ -73,9 +73,9 @@ const UploadBox: FC<Props> = (props: Props) => {
<div className='lg:hidden gap-2 flex flex-wrap mt-4'>
{
files?.map((item, index) => (
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
<div key={index} className='flex bg-gray-200 dark:bg-[#2d2d30] py-1.5 px-2 rounded-lg items-center gap-1'>
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
<div className='text-xs'>{item.name}</div>
<div className='text-xs dark:text-gray-300'>{item.name}</div>
</div>
))
}
+11 -10
View File
@@ -3,6 +3,7 @@ import { FC, useCallback, useEffect, useState } from 'react'
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { clx } from '../helpers/utils';
import { getIconColor } from '@/utils/colorUtils';
type Props = {
label: string;
@@ -77,22 +78,22 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
return (
<div>
<div {...getRootProps()} className='w-full py-8 border border-dashed border-description flex flex-col justify-center items-center gap-4 rounded-2xl'>
<div {...getRootProps()} className='w-full py-8 border border-dashed border-description dark:border-[#3e3e42] flex flex-col justify-center items-center gap-4 rounded-2xl'>
<input {...getInputProps()} />
<Gallery
className='size-8'
color='#8C90A3'
color={getIconColor('muted')}
/>
<div className='text-description text-xs'>
<div className='text-description dark:text-gray-400 text-xs'>
{props.label}
</div>
<div className='flex items-center gap-2'>
<DocumentUpload
className='size-4'
color='black'
color={getIconColor('primary')}
/>
<div className='text-xs'>{t('upload')}</div>
<div className='text-xs dark:text-gray-300'>{t('upload')}</div>
</div>
</div>
@@ -114,7 +115,7 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
'size-10 rounded-full object-cover',
cover === item ? 'border-2 border-red-400' : ''
)} />
<div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
<div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white dark:bg-[#252526] size-5 rounded-full flex justify-center items-center'>
<CloseCircle className='size-4 ' color='red' />
</div>
</div>
@@ -126,10 +127,10 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
files.map((file, index) => {
if (props.isFile) {
return (
<div key={index} className='flex border p-2 rounded-lg items-center gap-2'>
<div key={index} className='flex border dark:border-[#3e3e42] p-2 rounded-lg items-center gap-2'>
<div className='flex relative items-center gap-2'>
<div className='text-xs'>{file.name}</div>
<div className='absolute -left-4 -top-4 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
<div className='text-xs dark:text-gray-300'>{file.name}</div>
<div className='absolute -left-4 -top-4 shadow-md bg-white dark:bg-[#252526] size-5 rounded-full flex justify-center items-center'>
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
</div>
</div>
@@ -141,7 +142,7 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
<div key={index} className='flex items-center gap-2'>
<div key={index} className='flex relative items-center gap-2'>
<img src={URL.createObjectURL(file)} alt={file.name} className='size-10 rounded-full object-cover' />
<div className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
<div className='absolute -left-2 -top-2 shadow-md bg-white dark:bg-[#252526] size-5 rounded-full flex justify-center items-center'>
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
</div>
</div>
+4 -3
View File
@@ -2,6 +2,7 @@ import { CloseCircle, Paperclip2 } from 'iconsax-react'
import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDropzone } from 'react-dropzone'
import { getIconColor } from '@/utils/colorUtils'
type Props = {
@@ -36,15 +37,15 @@ const UploadButton: FC<Props> = (props) => {
return (
<div className='flex flex-col gap-4 sm:flex-row'>
<div {...getRootProps()} className='h-10 bg-[#ECEEF5] rounded-full flex gap-2 items-center px-3 cursor-pointer'>
<div {...getRootProps()} className='h-10 bg-[#ECEEF5] dark:bg-[#2d2d30] text-black dark:text-white rounded-full flex gap-2 items-center px-3 cursor-pointer'>
<input {...getInputProps()} />
<Paperclip2 size={20} color='#000' />
<Paperclip2 size={20} color={getIconColor('primary')} />
<div className='text-xs'>{title ? title : t('attachment_select')}</div>
</div>
{files.map((file, index) => (
<div key={file.name} className='bg-[#EBEDF5] text-xs flex gap-2 h-10 rounded-full items-center px-3'>
<div key={file.name} className='bg-[#EBEDF5] dark:bg-[#2d2d30] text-xs dark:text-gray-300 flex gap-2 h-10 rounded-full items-center px-3'>
<span>{file.name}</span>
<CloseCircle size={19} color='#D52903' className='cursor-pointer' onClick={() => handleRemove(index)} />
</div>
+2 -1
View File
@@ -1,5 +1,6 @@
import { FC } from 'react'
import { EmojiHappy } from 'iconsax-react'
import { getIconColor } from '@/utils/colorUtils'
import Reply from './Reply'
import Forward from './Forward'
import { MessageDetail } from '@/pages/received/types/Types'
@@ -21,7 +22,7 @@ const EmailActions: FC<EmailActionsProps> = ({
{showReactions && (
<EmojiHappy
size={24}
color='black'
color={getIconColor('primary')}
className='cursor-pointer hover:opacity-70 transition-opacity'
/>
)}
+1 -1
View File
@@ -91,7 +91,7 @@ const EmailEditor: FC<EmailEditorProps> = ({
placeholder={placeholder}
modules={modules}
formats={formats}
className="bg-white rounded-lg"
className="bg-white dark:bg-[#252526] rounded-lg"
/>
</div>
)
@@ -49,7 +49,7 @@ const EmailFormActions: FC<EmailFormActionsProps> = ({
{/* Save Draft Button */}
{onSaveDraft && (
<Button
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
className='!w-full sm:!w-fit px-6 md:px-10 bg-white dark:bg-transparent text-black dark:text-white border border-primary order-2 sm:order-1'
label={t('new_message.draft')}
onClick={onSaveDraft}
loading={isSavingDraft}
@@ -59,7 +59,7 @@ const EmailFormActions: FC<EmailFormActionsProps> = ({
{/* Close Button */}
{onClose && !onSaveDraft && (
<Button
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
className='!w-full sm:!w-fit px-6 md:px-10 bg-white dark:bg-transparent text-black dark:text-white border border-primary order-2 sm:order-1'
label={t('common.close')}
onClick={onClose}
/>
+5 -5
View File
@@ -169,11 +169,11 @@ const EmailInput: FC<EmailInputProps> = ({
return (
<div className='w-full relative'>
<div className="w-full bg-white min-h-10 text-black flex flex-wrap items-center gap-1 px-4 py-2 text-xs rounded-xl border border-border">
<div className="w-full bg-white dark:bg-[#252526] min-h-10 text-black dark:text-white flex flex-wrap items-center gap-1 px-4 py-2 text-xs rounded-xl border border-border">
{toEmails.map((email, index) => (
<div
key={index}
className="flex items-center gap-1 bg-[#EBEDF5] text-xs h-7 rounded-full px-3"
className="flex items-center gap-1 bg-[#EBEDF5] dark:bg-[#2d2d30] text-xs h-7 rounded-full px-3"
>
<span>{email}</span>
<CloseCircle
@@ -198,11 +198,11 @@ const EmailInput: FC<EmailInputProps> = ({
</div>
{showSuggestions && filteredSuggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg z-[10000] mt-1">
<div className="absolute top-full left-0 right-0 bg-white dark:bg-[#252526] border border-gray-200 dark:border-[#3e3e42] rounded-lg shadow-lg z-[10000] mt-1">
{filteredSuggestions.map((suggestion, index) => (
<div
key={suggestion.address}
className={`px-4 py-2 cursor-pointer text-sm hover:bg-gray-100 border-b border-gray-100 last:border-b-0 ${selectedSuggestionIndex === index ? 'bg-blue-50 text-blue-600' : 'text-gray-700'
className={`px-4 py-2 cursor-pointer text-sm hover:bg-gray-100 dark:hover:bg-[#2d2d30] border-b border-gray-100 dark:border-[#2d2d30] last:border-b-0 ${selectedSuggestionIndex === index ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300'
}`}
onMouseDown={() => selectSuggestion(suggestion)}
onMouseEnter={() => setSelectedSuggestionIndex(index)}
@@ -210,7 +210,7 @@ const EmailInput: FC<EmailInputProps> = ({
<div className="flex flex-col">
<span className="font-medium">{suggestion.name || suggestion.address}</span>
{suggestion.name && (
<span className="text-xs text-gray-500">{suggestion.address}</span>
<span className="text-xs text-gray-500 dark:text-gray-400">{suggestion.address}</span>
)}
</div>
</div>
+3 -2
View File
@@ -11,6 +11,7 @@ import { useNewMessage } from './hooks/useNewMessage'
import Intelligense from '@/assets/images/intelligense.svg'
import { useGenerateEmail } from '@/pages/received/hooks/useEmailData'
import { clx } from '@/helpers/utils'
import { getIconColor } from '@/utils/colorUtils'
const NewMessage: FC = () => {
const { t } = useTranslation('global')
@@ -116,7 +117,7 @@ const NewMessage: FC = () => {
}`}
onClick={handleClose}
/>
<div className={`fixed bottom-2 inset-x-2 md:bottom-4 md:inset-x-4 xl:left-4 xl:right-auto xl:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
<div className={`fixed bottom-2 inset-x-2 md:bottom-4 md:inset-x-4 xl:left-4 xl:right-auto xl:bottom-4 bg-white dark:bg-[#252526] rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
}`}>
{/* Header */}
<div className='flex justify-between items-center mb-6 md:mb-8'>
@@ -124,7 +125,7 @@ const NewMessage: FC = () => {
{t('new_message.title')}
</div>
<div onClick={handleClose} className='cursor-pointer p-1'>
<CloseCircle size={20} className='md:w-6 md:h-6' color='#292D32' />
<CloseCircle size={20} className='md:w-6 md:h-6' color={getIconColor('primary')} />
</div>
</div>
+70
View File
@@ -0,0 +1,70 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export const useTheme = () => {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
interface ThemeProviderProps {
children: ReactNode
}
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
const [theme, setTheme] = useState<Theme>(() => {
// بررسی تم ذخیره شده در localStorage
const savedTheme = localStorage.getItem('theme') as Theme
if (savedTheme) {
return savedTheme
}
// بررسی تنظیمات سیستم
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark'
}
return 'light'
})
useEffect(() => {
const root = document.documentElement
// حذف کلاس‌های قبلی
root.classList.remove('light', 'dark')
// اضافه کردن کلاس مناسب
root.classList.add(theme)
// ذخیره در localStorage
localStorage.setItem('theme', theme)
// بروزرسانی رنگ background در body برای CSS قدیمی
if (theme === 'dark') {
document.body.style.background = '#1e1e1e'
} else {
document.body.style.background = '#eceef6'
}
}, [theme])
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
+168 -61
View File
@@ -24,6 +24,7 @@ body {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
transition: background-color 0.3s ease;
}
/* ریسپانسیو شدن برای صفحات کوچک */
@@ -61,6 +62,11 @@ body,
border-radius: 5px;
height: 6px;
outline: none;
transition: background-color 0.3s ease;
}
.dark .slider {
background: #2d2d30;
}
.slider::-webkit-slider-thumb {
@@ -72,6 +78,11 @@ body,
background: #374151;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease;
}
.dark .slider::-webkit-slider-thumb {
background: #007acc;
}
.slider::-moz-range-thumb {
@@ -82,12 +93,23 @@ body,
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease;
}
.dark .slider::-moz-range-thumb {
background: #007acc;
}
}
input::placeholder,
textarea::placeholder {
color: #888888;
transition: color 0.3s ease;
}
.dark input::placeholder,
.dark textarea::placeholder {
color: #969696;
}
@theme {
@@ -107,11 +129,23 @@ textarea::placeholder {
width: 100% !important;
margin: 0px;
padding-right: 16px !important;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.dark .rmdp-input {
background-color: #3c3c3c !important;
border: 1px solid #3e3e42 !important;
color: #d4d4d4 !important;
}
.readOny .rmdp-input {
background-color: #f5f5f5 !important;
border: none !important;
}
.dark .readOny .rmdp-input {
background-color: #252526 !important;
}
.rmdp-container {
width: 100%;
}
@@ -156,71 +190,108 @@ textarea::placeholder {
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--background: #ffffff;
--foreground: #000000;
--card: #ffffff;
--card-foreground: #000000;
--popover: #ffffff;
--popover-foreground: #000000;
--primary: #000000;
--primary-foreground: #ffffff;
--secondary: #f5f5f5;
--secondary-foreground: #000000;
--muted: #f5f5f5;
--muted-foreground: #888888;
--accent: #f5f5f5;
--accent-foreground: #000000;
--destructive: #ef4444;
--border: #e5e5e5;
--input: #ffffff;
--ring: #888888;
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--sidebar: #ffffff;
--sidebar-foreground: #000000;
--sidebar-primary: #000000;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f5f5f5;
--sidebar-accent-foreground: #000000;
--sidebar-border: #e5e5e5;
--sidebar-ring: #888888;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--background: #1e1e1e;
--foreground: #d4d4d4;
--card: #252526;
--card-foreground: #d4d4d4;
--popover: #252526;
--popover-foreground: #d4d4d4;
--primary: #ffffff;
--primary-foreground: #1e1e1e;
--secondary: #2d2d30;
--secondary-foreground: #cccccc;
--muted: #2d2d30;
--muted-foreground: #969696;
--accent: #37373d;
--accent-foreground: #d4d4d4;
--destructive: #f14c4c;
--border: #3e3e42;
--input: #3c3c3c;
--ring: #ffffff;
--chart-1: #ffffff;
--chart-2: #f472b6;
--chart-3: #fb7185;
--chart-4: #f43f5e;
--chart-5: #fb923c;
--sidebar: #252526;
--sidebar-foreground: #cccccc;
--sidebar-primary: #ffffff;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #2d2d30;
--sidebar-accent-foreground: #cccccc;
--sidebar-border: #3e3e42;
--sidebar-ring: #ffffff;
}
/* کلاس‌های یکپارچه برای رنگ‌بندی */
.icon-color {
@apply text-foreground;
}
.icon-muted {
@apply text-muted-foreground;
}
.icon-primary {
@apply text-primary;
}
.bg-surface {
@apply bg-card;
}
.bg-surface-elevated {
@apply bg-background;
}
.text-primary-content {
@apply text-foreground;
}
.text-secondary-content {
@apply text-muted-foreground;
}
.border-surface {
@apply border-border;
}
.input-surface {
@apply bg-input border border-border text-foreground;
}
@layer base {
@@ -237,14 +308,34 @@ textarea::placeholder {
font-family: irancell !important;
direction: rtl;
min-height: 100px !important;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.dark .ql-container {
background-color: #3c3c3c !important;
border-color: #3e3e42 !important;
}
.ql-toolbar {
@apply rounded-t-xl;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.dark .ql-toolbar {
background-color: #252526 !important;
border-color: #3e3e42 !important;
}
.ql-editor {
text-align: start !important;
unicode-bidi: plaintext !important;
transition: color 0.3s ease;
}
.dark .ql-editor {
color: #d4d4d4 !important;
}
.ql-editor p {
unicode-bidi: plaintext !important;
text-align: start !important;
@@ -355,22 +446,33 @@ textarea::placeholder {
.modalGlass {
background: rgba(0, 0, 0, 0.436);
background-blend-mode: multiply;
backdrop-filter: blur(7px);
transition: background-color 0.3s ease;
}
.dark .modalGlass {
background: rgba(30, 30, 30, 0.85);
}
.modalGlass2 {
/* background: rgba(255, 255, 255, 0.502); */
background-color: white;
background-blend-mode: multiply;
transition: background-color 0.3s ease;
}
/* backdrop-filter: blur(5px); */
.dark .modalGlass2 {
background-color: #252526;
}
.modalGlass3 {
background: rgba(62, 61, 61, 0.2);
background-blend-mode: multiply;
backdrop-filter: blur(44px);
transition: background-color 0.3s ease;
}
.dark .modalGlass3 {
background: rgba(37, 37, 38, 0.7);
}
strong {
font-weight: bold;
@@ -412,3 +514,8 @@ strong {
line-height: 1.4;
}
}
.dark .filterWhite {
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(7416%)
hue-rotate(296deg) brightness(109%) contrast(97%);
}
+5 -4
View File
@@ -13,6 +13,7 @@ import { ErrorType } from '@/helpers/types';
import { toast } from '@/components/Toast';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
const navigate = useNavigate();
@@ -137,14 +138,14 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<RowActionsDropdown actions={[
{
label: 'حذف همه',
icon: <Trash size={18} color='black' />,
icon: <Trash size={18} color={getIconColor('primary')} />,
onClick: () => setShowModal(true)
}
]} />
@@ -153,7 +154,7 @@ const List: FC = () => {
const selectedActions = (
<>
<RecoveryConvert size={18} color='black' onClick={() => handleRestoreSelected()} className="cursor-pointer" />
<RecoveryConvert size={18} color={getIconColor('primary')} onClick={() => handleRestoreSelected()} className="cursor-pointer" />
<Trash size={18} color='red' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
@@ -199,7 +200,7 @@ const List: FC = () => {
const getRowActions = (message: TrashMessage): RowActionItem[] => [
{
label: 'بازگردانی',
icon: <RecoveryConvert size={16} color="black" />,
icon: <RecoveryConvert size={16} color={getIconColor('primary')} />,
onClick: () => emailActions.restore({ messageId: message.id, mailbox: message.mailbox }),
},
{
+5 -4
View File
@@ -10,6 +10,7 @@ import { ArchiveMessage } from './types/ArchiveTypes';
import { useEmailActions } from '@/hooks/useEmailActions';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
const navigate = useNavigate();
@@ -137,7 +138,7 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
@@ -146,8 +147,8 @@ const List: FC = () => {
const selectedActions = (
<>
<ArchiveSlash size={18} color='black' onClick={() => handleUnarchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<ArchiveSlash size={18} color={getIconColor('primary')} onClick={() => handleUnarchiveSelected()} className="cursor-pointer" />
<Trash size={18} color={getIconColor('primary')} onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
@@ -181,7 +182,7 @@ const List: FC = () => {
const getRowActions = (message: ArchiveMessage): RowActionItem[] => [
{
label: 'خروج از آرشیو',
icon: <ArchiveSlash size={16} color="black" />,
icon: <ArchiveSlash size={16} color={getIconColor('primary')} />,
onClick: () => emailActions.unarchive({ messageId: message.id, mailbox: message.mailbox }),
},
{
+7 -7
View File
@@ -63,18 +63,18 @@ const Login: FC = () => {
return (
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
<div className='flex-1 min-w-[50%] overflow-y-auto bg-card lg:px-9 px-4 py-7'>
<div className='flex-1 h-full flex flex-col'>
<div className='flex relative flex-1 flex-col h-full justify-center'>
<img src={LogoSmallImage} className='w-8 hidden xl:block' />
<img src={LogoImage} className='w-44 right-0 left-0 mx-auto absolute top-10 xl:hidden' />
<img src={LogoSmallImage} className='w-8 hidden xl:block filterWhite' />
<img src={LogoImage} className='w-44 right-0 left-0 mx-auto absolute top-10 xl:hidden filterWhite' />
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
<h2 className='text-2xl font-bold text-primary-content'>
{t('auth.welcome')}
</h2>
<p className='text-description text-sm mt-2'>
<p className='text-secondary-content text-sm mt-2'>
{t('auth.enter_info_login')}
</p>
</div>
@@ -132,8 +132,8 @@ const Login: FC = () => {
</div>
</div>
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
<img src={LogoImage} className='h-14' />
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center bg-surface'>
<img src={LogoImage} className='h-14 filterWhite' />
</div>
</div>
</div>
+5 -4
View File
@@ -12,6 +12,7 @@ import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
import { ErrorType } from '../../../helpers/types'
import { ArrowLeft } from 'iconsax-react'
import { toast } from '../../../components/Toast'
import { getIconColor } from '@/utils/colorUtils'
const LoginStep1: FC = () => {
const { t } = useTranslation('global')
@@ -56,10 +57,10 @@ const LoginStep1: FC = () => {
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
<h2 className='text-2xl font-bold text-primary-content'>
{t('auth.welcome')}
</h2>
<p className='text-description text-sm mt-2'>
<p className='text-secondary-content text-sm mt-2'>
{t('auth.enter_info_login')}
</p>
</div>
@@ -91,11 +92,11 @@ const LoginStep1: FC = () => {
onClick={() => formik.handleSubmit()}
loading={loginWithOtp.isPending || checkHasAccount.isPending}
/>
<a href='https://accounts.danakcorp.com' target='_blank' className='mt-4 flex gap-1 text-sm justify-center text-description'>
<a href='https://accounts.danakcorp.com' target='_blank' className='mt-4 flex gap-1 text-sm justify-center text-secondary-content'>
<div>
{t('auth.old_version')}
</div>
<ArrowLeft size={20} color='black' />
<ArrowLeft size={20} color={getIconColor('muted')} />
</a>
</div>
)
+6 -6
View File
@@ -95,10 +95,10 @@ const LoginStep2: FC = () => {
return (
<div>
<div className='mt-5'>
<h2 className='lg:text-2xl font-bold'>
<h2 className='lg:text-2xl font-bold text-primary-content'>
{t('auth.enter_otp')}
</h2>
<p className='text-description flex lg:flex-row flex-col lg:gap-1 gap-2 text-sm lg:mt-2 mt-3'>
<p className='text-secondary-content flex lg:flex-row flex-col lg:gap-1 gap-2 text-sm lg:mt-2 mt-3'>
<div className='flex gap-1'>
<div>{t('auth.verfify_code_text_1')}</div>
<div>{phone}</div>
@@ -106,7 +106,7 @@ const LoginStep2: FC = () => {
{t('auth.verfify_code_text_2')}
</div>
</div>
<div onClick={() => setStepLogin(1)} className='text-black cursor-pointer'>
<div onClick={() => setStepLogin(1)} className='text-primary-content cursor-pointer'>
{t('auth.change_number')}
</div>
</p>
@@ -129,7 +129,7 @@ const LoginStep2: FC = () => {
type='tel'
autoComplete="one-time-code"
inputMode="numeric"
className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />
className='w-full h-[50px] flex-1 mx-2 input-surface rounded-2.5' />
}
/>
</div>
@@ -142,12 +142,12 @@ const LoginStep2: FC = () => {
<div className='flex mt-4 justify-end'>
{
value !== '00:00' ?
<div className='flex gap-1 text-description text-xs'>
<div className='flex gap-1 text-secondary-content text-xs'>
<div>{value}</div>
<div>{t('auth.counter_otp')}</div>
</div>
:
<div onClick={reSend} className='flex cursor-pointer gap-1 pl-2 text-black text-sm'>
<div onClick={reSend} className='flex cursor-pointer gap-1 pl-2 text-primary-content text-sm'>
<div className=''>
{t('auth.try_again')}
</div>
+4 -4
View File
@@ -55,10 +55,10 @@ const LoginStep3: FC = () => {
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
<h2 className='text-2xl font-bold text-primary-content'>
{t('auth.enter_password_step3')}
</h2>
<p className='text-description text-sm mt-2'>
<p className='text-secondary-content text-sm mt-2'>
{t('auth.enter_your_password')}
</p>
</div>
@@ -90,10 +90,10 @@ const LoginStep3: FC = () => {
/>
<div onClick={() => setStepLogin(1)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
<p className='text-description'>
<p className='text-secondary-content'>
{t('auth.login_with_otp')}
</p>
<ArrowLeft size={20} color='black' />
<ArrowLeft size={20} color={'currentColor'} />
</div>
</div>
)
+7 -6
View File
@@ -12,6 +12,7 @@ import { useEmailActions } from '@/hooks/useEmailActions';
import { useSharedStore } from '@/shared/store/sharedStore';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
const navigate = useNavigate();
@@ -155,7 +156,7 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
@@ -166,19 +167,19 @@ const List: FC = () => {
<>
<Send
size={18}
color='black'
color={getIconColor('primary')}
onClick={() => handleSendSelected()}
className="cursor-pointer"
/>
<Edit
size={18}
color='black'
color={getIconColor('primary')}
onClick={() => handleEditSelected()}
className="cursor-pointer"
/>
<Trash
size={18}
color='black'
color={getIconColor('primary')}
onClick={() => handleDeleteSelected()}
className="cursor-pointer"
/>
@@ -243,12 +244,12 @@ const List: FC = () => {
const getRowActions = (message: DraftMessage): RowActionItem[] => [
{
label: 'ارسال',
icon: <Send size={16} color="black" />,
icon: <Send size={16} color={getIconColor('primary')} />,
onClick: () => handleSendDraft(message.id),
},
{
label: 'ویرایش',
icon: <Edit size={16} color="black" />,
icon: <Edit size={16} color={getIconColor('primary')} />,
onClick: () => handleEditDraft(message.id),
},
{
+5 -4
View File
@@ -11,6 +11,7 @@ import { useEmailActions } from '@/hooks/useEmailActions';
import Favorite from '../received/Components/Favorite';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
const navigate = useNavigate();
@@ -138,7 +139,7 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
@@ -148,8 +149,8 @@ const List: FC = () => {
const selectedActions = (
<>
<Star1 variant='Bold' size={18} color="#FFC107" onClick={() => handleUnfavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<Archive size={18} color={getIconColor('primary')} onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color={getIconColor('primary')} onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
@@ -202,7 +203,7 @@ const List: FC = () => {
},
{
label: 'بایگانی',
icon: <Archive size={16} color="black" />,
icon: <Archive size={16} color={getIconColor('primary')} />,
onClick: () => emailActions.archive({ messageId: message.id, mailbox: message.mailbox }),
},
{
+11 -10
View File
@@ -1,6 +1,7 @@
import { CloseCircle, Notification } from 'iconsax-react';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getIconColor } from '@/utils/colorUtils';
import { clx } from '../../helpers/utils';
import StatusCircle from '../../components/StatusCircle';
import { useOutsideClick } from '../../hooks/useOutSideClick';
@@ -81,7 +82,7 @@ const Notifications: FC = () => {
return (
<div ref={ref}>
<div onClick={() => setShowModal(!showModal)} className='relative cursor-pointer'>
<Notification size={18} color="black" />
<Notification size={18} color={getIconColor('primary')} />
<div className="absolute top-0 right-0 -mt-1 -mr-1 rounded-full bg-red-500 text-white text-[8px] size-3 flex pt-0.5 pl-[1px] justify-center items-center">
{data?.pages[0]?.data?.notificationCount}
</div>
@@ -91,19 +92,19 @@ const Notifications: FC = () => {
<div id='notificationsContainer' className="xl:h-[calc(100%-110px)] h-[70%] p-6 z-[300] xl:w-[300px] w-full xl:left-7 left-0 xl:top-[90px] top-0 overflow-y-auto xl:rounded-3xl rounded-b-3xl rounded-t-none fixed modalGlass3 ">
<div className="flex justify-between items-center">
<div>{t('notif.natification')}</div>
<div className="size-7 rounded-full bg-white/30 flex justify-center items-center">
<CloseCircle size={20} color="black" onClick={() => setShowModal(false)} />
<div className="size-7 rounded-full bg-white/30 dark:bg-white/10 flex justify-center items-center">
<CloseCircle size={20} color={getIconColor('primary')} onClick={() => setShowModal(false)} />
</div>
</div>
<div className="mt-6 flex h-8 border border-white border-opacity-35 text-xs rounded-lg overflow-hidden">
<div className="mt-6 flex h-8 border border-white border-opacity-35 dark:border-white/20 text-xs rounded-lg overflow-hidden">
<div
onClick={() => setActiveTab('unread')}
className={clx(
'flex-1 border-l cursor-pointer text-white border-white border-opacity-70 bg-transparent flex justify-center items-center',
activeTab === 'unread' ? 'bg-white/70 text-black' : ''
activeTab === 'unread' ? 'bg-white/70 text-black dark:bg-white/15 dark:text-white' : ''
)}
>
خوانده نشده
@@ -112,7 +113,7 @@ const Notifications: FC = () => {
onClick={() => setActiveTab('read')}
className={clx(
'flex-1 cursor-pointer text-white border-white bg-transparent flex justify-center items-center',
activeTab === 'read' ? 'bg-white/70 text-black' : ''
activeTab === 'read' ? 'bg-white/70 text-black dark:bg-white/15 dark:text-white' : ''
)}
>
خوانده شده
@@ -124,7 +125,7 @@ const Notifications: FC = () => {
loading={readAll.isPending}
onClick={handleAllRead}
className={clx(
'flex-1 border bg-transparent text-xs h-7 rounded-md cursor-pointer text-white border-white border-opacity-70 flex justify-center items-center',
'flex-1 border bg-transparent text-xs h-7 rounded-md cursor-pointer text-white dark:text-white border-white border-opacity-70 dark:border-white/40 flex justify-center items-center',
)}
>
{t('notif.all_read')}
@@ -136,12 +137,12 @@ const Notifications: FC = () => {
dataLength={posts.length}
next={fetchNextPage}
hasMore={hasNextPage}
loader={<div className='flex justify-center'><MoonLoader color="black" size={20} /></div>}
loader={<div className='flex justify-center'><MoonLoader color={getIconColor('primary')} size={20} /></div>}
scrollableTarget="notificationsContainer"
>
{
posts.map((item: NotificationItemType) => (
<div onClick={() => handleRedirect(item.type)} className="bg-white cursor-pointer h-fit gap-4 items-start rounded-xl bg-opacity-60 p-3 mb-4 flex" key={item.id}>
<div onClick={() => handleRedirect(item.type)} className="bg-white/60 dark:bg-white/10 dark:text-white cursor-pointer h-fit gap-4 items-start rounded-xl p-3 mb-4 flex" key={item.id}>
{
!item.isRead &&
<div className="mt-1">
@@ -150,7 +151,7 @@ const Notifications: FC = () => {
}
<div>
<div className="truncate max-w-[200px]">{item.message}</div>
<div className="mt-2 flex gap-2 text-[10px] text-description">
<div className="mt-2 flex gap-2 text-[10px] text-description dark:text-gray-400">
<div>{timeAgo(item.createdAt)}</div>
<div className="flex gap-1 items-center">
<StatusCircle color="#8C90A3" />
+4 -4
View File
@@ -100,13 +100,13 @@ const Profile: FC = () => {
getProfile.isPending ?
<div></div>
:
<div className='bg-white rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
<div className='bg-card border border-border rounded-3xl xl:p-6 p-4 mt-8 text-sm'>
<div className='xl:mt-10 mt-4 flex xl:flex-row flex-col xl:gap-0 gap-7 xl:items-center border-b pb-7'>
<div className='flex-1'>
<div>
{t('profile.image_profile')}
</div>
<div className='text-description text-xs mt-2'>
<div className='text-muted-foreground text-xs mt-2'>
{t('profile.image_profile_desc')}
</div>
</div>
@@ -123,7 +123,7 @@ const Profile: FC = () => {
<div>{t('profile.upload_image')}</div>
</div>
</Button>
<p className='mt-3 items-center flex gap-0.5 text-description'>
<p className='mt-3 items-center flex gap-0.5 text-muted-foreground'>
{t('profile.formats')}
<div className='-mt-0.5'>{t('profile.format_image')}</div>
<span>{t('profile.max_size')}</span>
@@ -137,7 +137,7 @@ const Profile: FC = () => {
<div>
{t('profile.info_account')}
</div>
<div className='text-description text-xs mt-2'>
<div className='text-muted-foreground text-xs mt-2'>
{t('profile.auth_after_change_info')}
</div>
</div>
+1 -1
View File
@@ -41,7 +41,7 @@ const Email: FC<Props> = (props: Props) => {
/>
{
isReadOnly ?
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-primary text-xs items-center'>
{/* <Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>
{t('edit')}
+2 -2
View File
@@ -65,7 +65,7 @@ const Phone: FC<Props> = (props: Props) => {
/>
{
isReadOnly ?
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-[#0047FF] text-xs items-center'>
<div onClick={() => setIsReadOnly(false)} className='flex cursor-pointer relative -top-3 gap-1 text-primary text-xs items-center'>
{/* <Edit className='xl:size-[18px] size-4' color='#0047FF' />
<div>
{t('edit')}
@@ -114,7 +114,7 @@ const Phone: FC<Props> = (props: Props) => {
<div className='mt-14 flex justify-end border-t border-border pt-8'>
<div className='flex gap-5'>
<Button
className='bg-white bg-opacity-40 text-description w-[150px] text-xs'
className='bg-card text-muted-foreground w-[150px] text-xs border border-border'
label='لغو'
onClick={() => setShowModal(false)}
/>
+1 -1
View File
@@ -91,7 +91,7 @@ const Username: FC<Props> = (props: Props) => {
</div>
}
</div>
<div className='flex relative gap-1 text-[#0047FF] text-xs items-center'>
<div className='flex relative gap-1 text-primary text-xs items-center'>
{
!canEdit ?
<div onClick={() => setCanEdit(true)} className='flex gap-1 cursor-pointer relative -top-3'>
+4 -3
View File
@@ -1,5 +1,6 @@
import Button from '@/components/Button'
import { ArrowRight, DocumentDownload, Printer } from 'iconsax-react'
import { getIconColor } from '@/utils/colorUtils'
import { useNavigate } from 'react-router-dom'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@@ -124,7 +125,7 @@ const Header: FC<{
return (
<div className='flex justify-between items-center border-b border-border pb-6'>
<div className='flex flex-1 lg:gap-4'>
<ArrowRight size={18} color='black' onClick={() => navigate(-1)} />
<ArrowRight size={18} color={getIconColor('primary')} onClick={() => navigate(-1)} />
<div className='flex xl:gap-4 gap-4 flex-1 lg:justify-start justify-center '>
<MessageUnread />
<MessageSpam isActiveSpam={mailBoxName === MailboxEnum.Junk} />
@@ -143,7 +144,7 @@ const Header: FC<{
onClick={handleDownloadEmail}
>
<div className='flex gap-2 items-center xl:px-5'>
<DocumentDownload size={20} color='black' />
<DocumentDownload size={20} color={getIconColor('primary')} />
<span className='pt-0.5 xl:block hidden'>{t('mail.download')}</span>
</div>
</Button>
@@ -152,7 +153,7 @@ const Header: FC<{
onClick={handlePrint}
>
<div className='flex gap-2 items-center xl:px-5'>
<Printer size={20} color='black' />
<Printer size={20} color={getIconColor('primary')} />
<span className='pt-0.5 xl:block hidden'>{t('mail.print')}</span>
</div>
</Button>
@@ -1,5 +1,6 @@
import { useMoveToArchive, useUnArchiveMessage } from '../hooks/useEmailData'
import { ArchiveTick } from 'iconsax-react'
import { getIconColor } from '@/utils/colorUtils'
import { FC, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
@@ -26,7 +27,7 @@ const MessageArchive: FC<{ isActiveArchive: boolean }> = ({ isActiveArchive }) =
})
}
setIsArchive(!isArchive)
}} className='lg:size-[18px] size-[20px]' variant={isArchive ? 'Bold' : 'Outline'} color='black' />
}} className='lg:size-[18px] size-[20px]' variant={isArchive ? 'Bold' : 'Outline'} color={getIconColor('primary')} />
)
}
@@ -1,4 +1,5 @@
import { InfoCircle } from 'iconsax-react'
import { getIconColor } from '@/utils/colorUtils'
import { FC, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useMoveToJunk, useMoveToNotJunk } from '../hooks/useEmailData'
@@ -27,7 +28,7 @@ const MessageSpam: FC<{ isActiveSpam: boolean }> = ({ isActiveSpam }) => {
})
}
setIsSpam(!isSpam)
}} className='lg:size-[18px] size-[20px]' variant={isSpam ? 'Bold' : 'Outline'} color='black' />
}} className='lg:size-[18px] size-[20px]' variant={isSpam ? 'Bold' : 'Outline'} color={getIconColor('primary')} />
)
}
@@ -1,4 +1,5 @@
import { SmsNotification } from 'iconsax-react'
import { getIconColor } from '@/utils/colorUtils'
import { FC, useState } from 'react'
import { useMarkAsRead, useMarkAsUnread } from '../hooks/useEmailData'
import { useParams } from 'react-router-dom'
@@ -18,7 +19,7 @@ const MessageUnread: FC = () => {
markAsRead({ messageId: id || '', mailbox: mailbox || '' })
}
setIsRead(!isRead)
}} className='lg:size-[18px] size-[22px]' variant={isRead ? 'Outline' : 'Bold'} color='black' />
}} className='lg:size-[18px] size-[22px]' variant={isRead ? 'Outline' : 'Bold'} color={getIconColor('primary')} />
)
}
+7 -6
View File
@@ -19,6 +19,7 @@ import ModalConfrim from '@/components/ModalConfrim';
import { ErrorType } from '@/helpers/types';
import { toast } from '@/components/Toast';
import { Checkbox } from '@/components/ui/checkbox';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
@@ -159,7 +160,7 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
@@ -167,7 +168,7 @@ const List: FC = () => {
<RowActionsDropdown actions={[
{
label: 'خواندن همه',
icon: <img src={MarkAsRead} className='w-[18px]' />,
icon: <img src={MarkAsRead} className='w-[18px] filterWhite' />,
onClick: () => handleMarkAll()
}
]} />
@@ -181,18 +182,18 @@ const List: FC = () => {
<>
<img
src={MarkAsRead}
className='w-[18px] cursor-pointer'
className='w-[18px] cursor-pointer filterWhite'
onClick={() => handleMarkAsReadSelected()}
/>
<ArchiveTick
color='black'
color={getIconColor('primary')}
onClick={() => handleArchiveSelected()}
className="cursor-pointer lg:size-[18px] size-[20px]"
/>
<InfoCircle color='black' className="cursor-pointer lg:size-[18px] size-[20px]" />
<InfoCircle color={getIconColor('primary')} className="cursor-pointer lg:size-[18px] size-[20px]" />
<Star1
color='black'
color={getIconColor('primary')}
className="cursor-pointer lg:size-[18px] size-[20px]"
onClick={() => handleFavoriteSelected()}
/>
+8 -7
View File
@@ -10,6 +10,7 @@ import { SearchMessage } from './types/SearchTypes';
import { useEmailActions } from '@/hooks/useEmailActions';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
const navigate = useNavigate();
@@ -143,19 +144,19 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
<More size={18} color={getIconColor('primary')} className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<Star size={18} color='black' onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<Star size={18} color={getIconColor('primary')} onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color={getIconColor('primary')} onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color={getIconColor('primary')} onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
@@ -195,12 +196,12 @@ const List: FC = () => {
const getRowActions = (message: SearchMessage): RowActionItem[] => [
{
label: 'اضافه به علاقه‌مندی‌ها',
icon: <Star size={16} color="black" />,
icon: <Star size={16} color={getIconColor('primary')} />,
onClick: () => emailActions.favorite({ messageId: message.id, mailbox: message.mailbox }),
},
{
label: 'بایگانی',
icon: <Archive size={16} color="black" />,
icon: <Archive size={16} color={getIconColor('primary')} />,
onClick: () => emailActions.archive({ messageId: message.id, mailbox: message.mailbox }),
},
{
+7 -6
View File
@@ -11,6 +11,7 @@ import { useEmailActions } from '@/hooks/useEmailActions';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
const navigate = useNavigate();
@@ -148,7 +149,7 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
@@ -157,9 +158,9 @@ const List: FC = () => {
const selectedActions = (
<>
<Star size={18} color='black' onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<Star size={18} color={getIconColor('primary')} onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color={getIconColor('primary')} onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color={getIconColor('primary')} onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
@@ -203,12 +204,12 @@ const List: FC = () => {
const getRowActions = (message: InboxMessage): RowActionItem[] => [
{
label: 'اضافه به علاقه‌مندی‌ها',
icon: <Star size={16} color="black" />,
icon: <Star size={16} color={getIconColor('primary')} />,
onClick: () => emailActions.favorite({ messageId: message.id, mailbox: message.mailbox }),
},
{
label: 'بایگانی',
icon: <Archive size={16} color="black" />,
icon: <Archive size={16} color={getIconColor('primary')} />,
onClick: () => emailActions.archive({ messageId: message.id, mailbox: message.mailbox }),
},
{
+5 -4
View File
@@ -8,6 +8,7 @@ import ChangePassword from './components/ChangePassword'
import TwoFactor from './components/TwoFactor'
import TitleLine from '@/components/TitleLine'
import { SettingCategoryType, SettingItemType } from './types/SettingTypes'
import { getIconColor } from '@/utils/colorUtils'
const Setting: FC = () => {
@@ -25,17 +26,17 @@ const Setting: FC = () => {
<Tabs
items={[
{
icon: <Notification1 color={activeTab === 'notification' ? 'black' : '#8C90A3'} size={22} />,
icon: <Notification1 color={activeTab === 'notification' ? getIconColor('primary') : getIconColor('muted')} size={22} />,
label: t('setting.notification'),
value: 'notification'
},
{
icon: <KeySquare color={activeTab === 'password' ? 'black' : '#8C90A3'} size={22} />,
icon: <KeySquare color={activeTab === 'password' ? getIconColor('primary') : getIconColor('muted')} size={22} />,
label: t('setting.password'),
value: 'password'
},
{
icon: <Key color={activeTab === '2fa' ? 'black' : '#8C90A3'} size={22} />,
icon: <Key color={activeTab === '2fa' ? getIconColor('primary') : getIconColor('muted')} size={22} />,
label: t('setting.2fa'),
value: '2fa'
},
@@ -47,7 +48,7 @@ const Setting: FC = () => {
{
activeTab === 'notification' ?
<div className='mt-10 bg-white xl:p-8 p-4 rounded-3xl'>
<div className='mt-10 bg-white dark:bg-[#252526] xl:p-8 p-4 rounded-3xl'>
<div>
{t('setting.setting_email_message')}
</div>
@@ -47,7 +47,7 @@ const ChangePassword: FC = () => {
})
return (
<div className='mt-10 bg-white p-8 rounded-3xl'>
<div className='mt-10 bg-white dark:bg-[#252526] p-8 rounded-3xl'>
<div>
{t('setting.change_password')}
</div>
@@ -94,7 +94,7 @@ const ChangePassword: FC = () => {
</div>
</div>
<div className='flex-1 xl:p-8 p-4 bg-[#EEF0F7] rounded-3xl text-sm'>
<div className='flex-1 xl:p-8 p-4 bg-[#EEF0F7] dark:bg-[#2d2d30] dark:text-gray-300 rounded-3xl text-sm'>
<div>
{t('setting.password_is')}
</div>
+3 -3
View File
@@ -35,12 +35,12 @@ const Enabled: FC = () => {
return (
<div>
<div className='mt-10 bg-white xl:p-8 p-4 rounded-3xl'>
<div className='mt-10 bg-white dark:bg-[#252526] xl:p-8 p-4 rounded-3xl'>
<div className='text-center py-8'>
<div className='inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4'>
<div className='inline-flex items-center justify-center w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full mb-4'>
<TickCircle size={32} color='#22c55e' />
</div>
<h2 className='text-lg font-semibold text-green-600 mb-2'>
<h2 className='text-lg font-semibold text-green-600 dark:text-green-400 mb-2'>
{t('setting.2fa_enabled_successfully')}
</h2>
</div>
+16 -16
View File
@@ -79,7 +79,7 @@ const TwoFactor: FC = () => {
}
return (
<div className='mt-10 bg-white xl:p-8 p-4 rounded-3xl'>
<div className='mt-10 bg-white dark:bg-[#252526] xl:p-8 p-4 rounded-3xl'>
<div className='mb-6 border-b border-border pb-4'>
<h2>
{t('setting.2fa_setup')}
@@ -94,10 +94,10 @@ const TwoFactor: FC = () => {
<div className='flex-1'>
{!qrData ? (
<div className='text-center py-8'>
<div className='inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4'>
<div className='inline-flex items-center justify-center w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full mb-4'>
<Key size={32} color='#3b82f6' />
</div>
<p className='text-sm text-gray-600 mb-6'>
<p className='text-sm text-gray-600 dark:text-gray-400 mb-6'>
{t('setting.2fa_setup_description')}
</p>
<Button
@@ -119,7 +119,7 @@ const TwoFactor: FC = () => {
{t('setting.scan_qr_code')}
</h3>
<div className='flex justify-center mb-4'>
<div className='p-4 bg-white border-2 border-gray-200 rounded-xl shadow-sm'>
<div className='p-4 bg-white dark:bg-[#2d2d30] border-2 border-gray-200 dark:border-[#3e3e42] rounded-xl shadow-sm'>
<img
src={qrData.qr}
alt="QR Code"
@@ -127,9 +127,9 @@ const TwoFactor: FC = () => {
/>
</div>
</div>
<div className='text-xs text-gray-500 bg-gray-50 p-3 rounded-lg'>
<div className='text-xs text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-[#2d2d30] p-3 rounded-lg'>
<div className='font-medium mb-1'>{t('setting.manual_entry')}:</div>
<div onClick={() => handleCopySeed(qrData.seed)} className='font-mono break-all text-gray-700 cursor-pointer'>{qrData.seed}</div>
<div onClick={() => handleCopySeed(qrData.seed)} className='font-mono break-all text-gray-700 dark:text-gray-300 cursor-pointer'>{qrData.seed}</div>
</div>
</div>
@@ -162,39 +162,39 @@ const TwoFactor: FC = () => {
</div>
{/* Information Section */}
<div className='flex-1 xl:p-8 p-4 bg-[#EEF0F7] rounded-3xl text-sm'>
<div className='flex-1 xl:p-8 p-4 bg-[#EEF0F7] dark:bg-[#2d2d30] rounded-3xl text-sm'>
<div className='mb-4'>
<h3 className='font-medium mb-2'>{t('setting.2fa_info_title')}</h3>
<p className='text-xs text-gray-600'>
<p className='text-xs text-gray-600 dark:text-gray-400'>
{t('setting.2fa_info_description')}
</p>
</div>
<div className='space-y-3'>
<div className='flex items-start gap-3'>
<div className='w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
<span className='text-xs font-medium text-blue-600'>1</span>
<div className='w-6 h-6 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
<span className='text-xs font-medium text-blue-600 dark:text-blue-400'>1</span>
</div>
<p className='text-xs'>{t('setting.2fa_step_1')}</p>
</div>
<div className='flex items-start gap-3'>
<div className='w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
<span className='text-xs font-medium text-blue-600'>2</span>
<div className='w-6 h-6 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
<span className='text-xs font-medium text-blue-600 dark:text-blue-400'>2</span>
</div>
<p className='text-xs'>{t('setting.2fa_step_2')}</p>
</div>
<div className='flex items-start gap-3'>
<div className='w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
<span className='text-xs font-medium text-blue-600'>3</span>
<div className='w-6 h-6 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5'>
<span className='text-xs font-medium text-blue-600 dark:text-blue-400'>3</span>
</div>
<p className='text-xs'>{t('setting.2fa_step_3')}</p>
</div>
</div>
<div className='mt-6 p-3 bg-yellow-50 border border-yellow-200 rounded-lg'>
<p className='text-xs text-yellow-800'>
<div className='mt-6 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg'>
<p className='text-xs text-yellow-800 dark:text-yellow-300'>
<span className='font-medium'>{t('setting.note')}:</span> {t('setting.2fa_note')}
</p>
</div>
+6 -5
View File
@@ -13,6 +13,7 @@ import { ErrorType } from '@/helpers/types';
import { toast } from '@/components/Toast';
import { Checkbox } from '@/components/ui/checkbox';
import React from 'react';
import { getIconColor } from '@/utils/colorUtils';
const List: FC = () => {
const navigate = useNavigate();
@@ -137,14 +138,14 @@ const List: FC = () => {
</div>
<Refresh2
size={18}
color='black'
color={getIconColor('primary')}
className={`cursor-pointer min-w-[18px] ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<RowActionsDropdown actions={[
{
label: 'حذف همه',
icon: <Trash size={18} color='black' />,
icon: <Trash size={18} color={getIconColor('primary')} />,
onClick: () => setShowModal(true)
}
]} />
@@ -153,8 +154,8 @@ const List: FC = () => {
const selectedActions = (
<>
<RecoveryConvert size={18} color='black' onClick={() => handleNotSpamSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<RecoveryConvert size={18} color={getIconColor('primary')} onClick={() => handleNotSpamSelected()} className="cursor-pointer" />
<Trash size={18} color={getIconColor('primary')} onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
@@ -198,7 +199,7 @@ const List: FC = () => {
const getRowActions = (message: SpamMessage): RowActionItem[] => [
{
label: 'غیر اسپم',
icon: <RecoveryConvert size={16} color="black" />,
icon: <RecoveryConvert size={16} color={getIconColor('primary')} />,
onClick: () => null,
},
{
+31 -27
View File
@@ -11,6 +11,8 @@ import SideBarItem from './SideBarItem'
import AvatarImage from '@/assets/images/avatar_image.png'
import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
import { useGetInbox } from '@/pages/received/hooks/useEmailData'
import ThemeToggle from '@/components/ThemeToggle'
import { getIconColor } from '@/utils/colorUtils'
const Header: FC = () => {
@@ -122,7 +124,7 @@ const Header: FC = () => {
}, [pathname])
return (
<div className='fixed z-1 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
<div className='fixed z-1 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-surface justify-between rounded-[32px] xl:w-[calc(100%-305px)] border border-surface'>
{/* Desktop Search */}
<div className='min-w-[270px] hidden xl:block relative'>
<div id="search-input-container">
@@ -138,7 +140,7 @@ const Header: FC = () => {
{showSearchResults && search && inputFocused && (
<div
id="search-dropdown-container"
className='absolute z-20 mt-1 w-full bg-white rounded-xl shadow-lg max-h-80 overflow-y-auto'
className='absolute z-20 mt-1 w-full bg-surface border border-surface rounded-xl shadow-lg max-h-80 overflow-y-auto'
>
{inboxData?.data?.results && inboxData.data.results.length > 0 ? (
<div className='py-2'>
@@ -146,7 +148,7 @@ const Header: FC = () => {
<Link
key={message.id}
to={`/mail/${message.id}/${message.mailbox}`}
className='flex items-center gap-3 px-4 py-2 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0'
className='flex items-center gap-3 px-4 py-2 hover:bg-accent cursor-pointer border-b border-border last:border-b-0'
onClick={clearSearch}
>
<div className='flex-1 min-w-0'>
@@ -154,18 +156,18 @@ const Header: FC = () => {
{!message.seen && (
<div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />
)}
<span className='text-sm font-medium text-gray-900 truncate'>
<span className='text-sm font-medium text-foreground truncate'>
{message.from.name || message.from.address}
</span>
</div>
<div className='text-sm text-gray-800 font-medium truncate mb-1'>
<div className='text-sm text-foreground font-medium truncate mb-1'>
{message.subject || 'بدون موضوع'}
</div>
<div className='text-xs text-gray-500 truncate'>
<div className='text-xs text-muted-foreground truncate'>
{message.intro}
</div>
</div>
<div className='text-xs text-gray-400 flex-shrink-0'>
<div className='text-xs text-muted-foreground flex-shrink-0'>
{new Date(message.date).toLocaleDateString('fa-IR')}
</div>
</Link>
@@ -173,7 +175,7 @@ const Header: FC = () => {
</div>
) : (
<div className='p-4 text-center text-sm text-description'>
<div className='p-4 text-center text-sm text-muted-foreground'>
نتیجهای یافت نشد
</div>
)}
@@ -185,7 +187,7 @@ const Header: FC = () => {
<div className={`${showMobileSearch ? 'w-full' : ''} items-center`}>
{!showMobileSearch && (
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
<HambergerMenu size={24} color='black' />
<HambergerMenu size={24} color={getIconColor('primary')} />
</div>
)}
@@ -207,7 +209,7 @@ const Header: FC = () => {
onClick={toggleMobileSearch}
className='p-2 flex-shrink-0 flex items-center justify-center'
>
<CloseCircle size={20} color='#8C90A3' />
<CloseCircle size={20} color={getIconColor('muted')} />
</div>
</div>
)}
@@ -217,7 +219,7 @@ const Header: FC = () => {
{showMobileSearch && showSearchResults && search && (
<div
id="mobile-search-dropdown-container"
className='fixed right-4 left-4 top-16 z-1 bg-white rounded-xl shadow-lg max-h-[calc(100vh-80px)] overflow-y-auto'
className='fixed right-4 left-4 top-16 z-1 bg-surface border border-surface rounded-xl shadow-lg max-h-[calc(100vh-80px)] overflow-y-auto'
>
{inboxData?.data?.results && inboxData.data.results.length > 0 ? (
<div className='py-2'>
@@ -225,7 +227,7 @@ const Header: FC = () => {
<Link
key={message.id}
to={`/mail/${message.id}/${message.mailbox}`}
className='flex items-center gap-3 px-4 py-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0'
className='flex items-center gap-3 px-4 py-3 hover:bg-accent cursor-pointer border-b border-border last:border-b-0'
onClick={clearSearchMobile}
>
<div className='flex-1 min-w-0'>
@@ -233,18 +235,18 @@ const Header: FC = () => {
{!message.seen && (
<div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />
)}
<span className='text-sm font-medium text-gray-900 truncate'>
<span className='text-sm font-medium text-foreground truncate'>
{message.from.name || message.from.address}
</span>
</div>
<div className='text-sm text-gray-800 font-medium truncate mb-1'>
<div className='text-sm text-foreground font-medium truncate mb-1'>
{message.subject || 'بدون موضوع'}
</div>
<div className='text-xs text-gray-500 truncate'>
<div className='text-xs text-muted-foreground truncate'>
{message.intro}
</div>
</div>
<div className='text-xs text-gray-400 flex-shrink-0'>
<div className='text-xs text-muted-foreground flex-shrink-0'>
{new Date(message.date).toLocaleDateString('fa-IR')}
</div>
</Link>
@@ -252,7 +254,7 @@ const Header: FC = () => {
</div>
) : (
<div className='p-4 text-center text-sm text-description'>
<div className='p-4 text-center text-sm text-muted-foreground'>
نتیجهای یافت نشد
</div>
)}
@@ -263,12 +265,14 @@ const Header: FC = () => {
{/* Mobile search icon */}
{!showMobileSearch && (
<div onClick={toggleMobileSearch} className='xl:hidden block'>
<SearchNormal size={20} color='black' />
<SearchNormal size={20} color={getIconColor('primary')} />
</div>
)}
<Notifications />
<ThemeToggle />
{
data && (
<Popover className="relative" key={popoverKey}>
@@ -281,16 +285,16 @@ const Header: FC = () => {
<div className='text-xs'>
{data?.data?.user?.emailAddress}
</div>
<ArrowDown2 size={14} color='#8C90A3' />
<ArrowDown2 size={14} color={getIconColor('muted')} />
</div>
</div>
</PopoverButton>
<PopoverPanel style={{ minHeight: window.innerWidth < 1140 ? window.innerHeight : undefined }} anchor="bottom" className="flex xl:ml-6 overflow-auto flex-col gap-3 bg-white boxShadow xl:mt-7 z-30 py-4 text-xs rounded-2.5 xl:w-[300px] -mt-[50px] w-full fixed xl:h-fit h-full shadow-md">
<PopoverPanel style={{ minHeight: window.innerWidth < 1140 ? window.innerHeight : undefined }} anchor="bottom" className="flex xl:ml-6 overflow-auto flex-col gap-3 bg-surface border border-surface boxShadow xl:mt-7 z-30 py-4 text-xs rounded-2.5 xl:w-[300px] -mt-[50px] w-full fixed xl:h-fit h-full shadow-md">
<div className='absolute xl:hidden top-6 left-6'>
<CloseCircle onClick={() => setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' />
<CloseCircle onClick={() => setPopoverKey((prevKey) => prevKey + 1)} size={20} color={getIconColor('primary')} />
</div>
<Link to={Paths.profile} className='flex flex-col gap-2 items-center justify-center border-b border-[#EAEDF5] pb-4'>
<Link to={Paths.profile} className='flex flex-col gap-2 items-center justify-center border-b border-border pb-4'>
<div className='size-14 rounded-full overflow-hidden'>
<img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
</div>
@@ -299,15 +303,15 @@ const Header: FC = () => {
{data?.data?.user?.emailAddress}
</div>
<div className='text-description'>
<div className='text-muted-foreground'>
{data?.data?.user?.email}
</div>
</Link>
<div className='pb-6 px-6 border-b border-[#EAEDF5]'>
<div className='pb-6 px-6 border-b border-border'>
<SideBarItem
icon={<ProfileCircle color={'black'} size={20} />}
icon={<ProfileCircle color={getIconColor('primary')} size={20} />}
title={t('header.profile')}
isActive
link={Paths.profile}
@@ -315,7 +319,7 @@ const Header: FC = () => {
/>
<SideBarItem
icon={<Setting2 color={'black'} size={20} />}
icon={<Setting2 color={getIconColor('primary')} size={20} />}
title={t('header.setting')}
isActive
link={Paths.setting}
@@ -325,7 +329,7 @@ const Header: FC = () => {
<div className='px-6 -mt-[2px]'>
<SideBarItem
icon={<Logout color={'black'} size={20} />}
icon={<Logout color={getIconColor('primary')} size={20} />}
title={t('sidebar.logout')}
isActive={false}
link={'#'}
+18 -17
View File
@@ -11,6 +11,7 @@ import Button from '@/components/Button'
import { useMailboxCount } from './hooks/useShareData'
import { MailboxEnum } from '@/pages/received/enum/Enum'
import { MailboxCount } from './types/SharedTypes'
import { getIconColor } from '@/utils/colorUtils'
const SideBar: FC = () => {
@@ -38,12 +39,12 @@ const SideBar: FC = () => {
}
<div
className={clx(
'fixed xl:flex flex translate-x-[400px] xl:translate-x-0 transition-all ease-in-out opacity-0 invisible xl:visible xl:opacity-100 xl:right-2 xl:md:right-4 right-0 xl:top-2 xl:md:top-4 top-0 xl:bottom-2 xl:md:bottom-4 bottom-0 xl:rounded-[20px] xl:md:rounded-[32px] w-[250px] sm:w-[280px] xl:w-[250px] bg-white flex-col py-8 md:py-10 xl:py-12',
'fixed xl:flex flex translate-x-[400px] xl:translate-x-0 transition-all ease-in-out opacity-0 invisible xl:visible xl:opacity-100 xl:right-2 xl:md:right-4 right-0 xl:top-2 xl:md:top-4 top-0 xl:bottom-2 xl:md:bottom-4 bottom-0 xl:rounded-[20px] xl:md:rounded-[32px] w-[250px] sm:w-[280px] xl:w-[250px] bg-surface border border-surface flex-col py-8 md:py-10 xl:py-12',
openSidebar && 'opacity-100 visible -translate-x-[0px] flex z-40'
)}
>
<div className='flex justify-center px-4'>
<img src={LogoImage} className='w-[120px] md:w-[140px]' />
<img src={LogoImage} className='w-[120px] md:w-[140px] filterWhite' />
</div>
<div className='flex justify-center mt-10 md:mt-14 px-4'>
@@ -55,20 +56,20 @@ const SideBar: FC = () => {
}}
>
<div className='flex gap-2 items-center'>
<Add size={18} color='black' />
<Add size={18} color={getIconColor('primary')} />
<div>{t('sidebar.new_message')}</div>
</div>
</Button>
</div>
<div className='flex-1 flex flex-col h-full overflow-y-auto no-scrollbar min-h-0'>
<div className='mt-8 md:mt-10 px-8 md:px-12 text-[#C3C7DD] text-sm font-bold'>
<div className='mt-8 md:mt-10 px-8 md:px-12 text-secondary-content text-sm font-bold'>
{t('sidebar.menu')}
</div>
<div className='text-xs text-[#8C90A3]'>
<div className='text-xs text-secondary-content'>
<SideBarItem
icon={<DirectInbox variant={isActive(Paths.received) ? 'Bold' : 'Outline'} color={isActive(Paths.received) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<DirectInbox variant={isActive(Paths.received) ? 'Bold' : 'Outline'} color={isActive(Paths.received) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.received')}
isActive={isActive(Paths.received)}
link={Paths.received}
@@ -76,14 +77,14 @@ const SideBar: FC = () => {
/>
<SideBarItem
icon={<DirectSend variant={isActive(Paths.sent) ? 'Bold' : 'Outline'} color={isActive(Paths.sent) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<DirectSend variant={isActive(Paths.sent) ? 'Bold' : 'Outline'} color={isActive(Paths.sent) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.sent')}
isActive={isActive(Paths.sent)}
link={Paths.sent}
/>
<SideBarItem
icon={<Edit variant={isActive(Paths.draft) ? 'Bold' : 'Outline'} color={isActive(Paths.draft) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<Edit variant={isActive(Paths.draft) ? 'Bold' : 'Outline'} color={isActive(Paths.draft) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.draft')}
isActive={isActive(Paths.draft)}
link={Paths.draft}
@@ -91,34 +92,34 @@ const SideBar: FC = () => {
/>
<SideBarItem
icon={<ArchiveTick variant={isActive(Paths.archive) ? 'Bold' : 'Outline'} color={isActive(Paths.archive) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<ArchiveTick variant={isActive(Paths.archive) ? 'Bold' : 'Outline'} color={isActive(Paths.archive) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.archive')}
isActive={isActive(Paths.archive)}
link={Paths.archive}
/>
<SideBarItem
icon={<Trash variant={isActive(Paths.trash) ? 'Bold' : 'Outline'} color={isActive(Paths.trash) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<Trash variant={isActive(Paths.trash) ? 'Bold' : 'Outline'} color={isActive(Paths.trash) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.trash')}
isActive={isActive(Paths.trash)}
link={Paths.trash}
/>
</div>
<div className='mt-8 md:mt-10 px-8 md:px-12 text-[#C3C7DD] text-sm font-bold'>
<div className='mt-8 md:mt-10 px-8 md:px-12 text-secondary-content text-sm font-bold'>
{t('sidebar.other')}
</div>
<div className='text-xs text-[#8C90A3]'>
<div className='text-xs text-secondary-content'>
<SideBarItem
icon={<Star1 variant={isActive(Paths.favorite) ? 'Bold' : 'Outline'} color={isActive(Paths.favorite) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<Star1 variant={isActive(Paths.favorite) ? 'Bold' : 'Outline'} color={isActive(Paths.favorite) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.favorite')}
isActive={isActive(Paths.favorite)}
link={Paths.favorite}
/>
<SideBarItem
icon={<InfoCircle variant={isActive(Paths.spam) ? 'Bold' : 'Outline'} color={isActive(Paths.spam) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<InfoCircle variant={isActive(Paths.spam) ? 'Bold' : 'Outline'} color={isActive(Paths.spam) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.spam')}
isActive={isActive(Paths.spam)}
link={Paths.spam}
@@ -127,15 +128,15 @@ const SideBar: FC = () => {
</div>
<div className='flex-1 flex flex-col justify-end mt-10 md:mt-14 pb-6 md:pb-8'>
<div className='text-xs text-[#8C90A3]'>
<div className='text-xs text-secondary-content'>
<SideBarItem
icon={<Setting2 variant={isActive(Paths.setting) ? 'Bold' : 'Outline'} color={isActive(Paths.setting) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<Setting2 variant={isActive(Paths.setting) ? 'Bold' : 'Outline'} color={isActive(Paths.setting) ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.settings')}
isActive={isActive(Paths.setting)}
link={Paths.setting}
/>
<SideBarItem
icon={<Logout variant={isActive('logout') ? 'Bold' : 'Outline'} color={isActive('logout') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<Logout variant={isActive('logout') ? 'Bold' : 'Outline'} color={isActive('logout') ? getIconColor('primary') : getIconColor('muted')} size={iconSizeSideBar} />}
title={t('sidebar.logout')}
isActive={isActive('logout')}
link={`#`}
+3 -3
View File
@@ -28,17 +28,17 @@ const SideBarItem: FC<Props> = (props: Props) => {
{
!props.isWithoutLine &&
<div className={clx(
'w-1 bg-black h-6',
'w-1 bg-foreground h-6',
!props.isActive && 'invisible'
)}></div>
}
<div className='flex flex-1 gap-3 items-center'>
{props.icon}
<div className={props.isActive || !!props.count ? 'text-black' : ''}>
<div className={props.isActive || !!props.count ? 'text-foreground' : 'text-foreground'}>
{props.title}
</div>
<div className='flex flex-1 justify-end pl-6'>
{!!props.count && <div className='h-[18px] w-fit rounded-[6px] bg-black text-white text-xs flex items-center justify-center px-2.5 pt-0.5'>
{!!props.count && <div className='h-[18px] w-fit rounded-[6px] bg-foreground text-background text-xs flex items-center justify-center px-2.5 pt-0.5'>
{props.count > 100 ? '+100' : props.count}
</div>}
</div>
+28
View File
@@ -0,0 +1,28 @@
/**
* تابع برای دریافت رنگ مناسب آیکون‌ها بر اساس theme
* @param variant - نوع رنگ: 'primary' یا 'muted'
* @returns رنگ مناسب برای theme فعلی
*/
export const getIconColor = (
variant: "primary" | "muted" = "primary"
): string => {
// تشخیص theme از document
const isDark = document.documentElement.classList.contains("dark");
if (variant === "muted") {
return isDark ? "#9ca3af" : "#6b7280"; // خاکستری متوسط
}
// primary color
return isDark ? "#ffffff" : "#000000"; // سفید برای dark، سیاه برای light
};
/**
* Hook برای استفاده در React components
*/
export const useIconColor = () => {
return {
primary: getIconColor("primary"),
muted: getIconColor("muted"),
};
};