login page

This commit is contained in:
hamid zarghami
2025-07-09 11:20:31 +03:30
parent 9d0a1703fe
commit 72a6eaa156
60 changed files with 953 additions and 3530 deletions
+49
View File
@@ -0,0 +1,49 @@
import { FC } from 'react'
import LogoImage from '../../assets/images/logo.svg'
import LogoSmallImage from '../../assets/images/logo-small.svg'
import { useAuthStore } from './store/AuthStore'
import LoginStep1 from './components/LoginStep1'
import LoginStep2 from './components/LoginStep2'
import LoginStep3 from './components/LoginStep3'
const Login: FC = () => {
const { stepLogin, phone, email } = useAuthStore()
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 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' />
{
stepLogin === 1 ?
<LoginStep1 />
:
stepLogin === 2 && !!phone ?
<LoginStep2 />
: stepLogin === 2 && !!email ?
<LoginStep3 />
: stepLogin === 3 ?
<LoginStep3 />
: null
}
</div>
</div>
</div>
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
<img src={LogoImage} className='h-20' />
</div>
</div>
</div>
)
}
export default Login
+104
View File
@@ -0,0 +1,104 @@
import { FC } from 'react'
import Input from '../../../components/Input'
import { useTranslation } from 'react-i18next'
import { LoginType } from '../types/AuthTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useAuthStore } from '../store/AuthStore'
import Error from '../../../components/Error'
import Button from '../../../components/Button'
import { isEmail } from '../../../config/func'
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
import { ErrorType } from '../../../helpers/types'
import { ArrowLeft } from 'iconsax-react'
import { toast } from '../../../components/Toast'
const LoginStep1: FC = () => {
const { t } = useTranslation('global')
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore()
const loginWithOtp = useLoginWithOtp()
const checkHasAccount = useCheckHasAccount()
const formik = useFormik<LoginType>({
initialValues: {
phone_email: phone,
},
validationSchema: Yup.object({
phone_email: Yup.string()
.required(t('errors.required'))
}),
onSubmit(values) {
if (isEmail(values.phone_email)) {
setEmail(values.phone_email)
checkHasAccount.mutate({ email: values.phone_email }, {
onSuccess() {
setStepLogin(2)
},
onError(error: ErrorType) {
toast(error.response?.data?.error.message?.[0], 'error')
},
})
} else {
setPhone(values.phone_email)
loginWithOtp.mutate({ phone: values.phone_email }, {
onSuccess() {
setStepLogin(2)
toast(t('auth.otp_sent'), 'success')
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], 'error')
},
})
}
},
})
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.welcome')}
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_info_login')}
</p>
</div>
<div className='mt-16'>
<Input
label={t('auth.mobile_or_email')}
placeholder={t('auth.enter_mobile_or_email')}
type='text'
className='text-right'
name='phone_email'
onChange={formik.handleChange}
value={formik.values.phone_email}
/>
{
formik.touched.phone_email && formik.errors.phone_email &&
<Error
errorText={formik.errors.phone_email}
/>
}
</div>
<Button
label={t('auth.next')}
className='mt-8'
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'>
<div>
{t('auth.old_version')}
</div>
<ArrowLeft size={20} color='black' />
</a>
</div>
)
}
export default LoginStep1
+186
View File
@@ -0,0 +1,186 @@
import { FC, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { OtpVerifyType } from '../types/AuthTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useAuthStore } from '../store/AuthStore'
import Button from '../../../components/Button'
import OTPInput from 'react-otp-input'
import { useCountDown } from '../../../hooks/useCountDown'
// import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
import { ErrorType } from '../../../helpers/types'
import { setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
import { getRedirectUrl } from '../../../config/func'
import { toast } from '../../../components/Toast'
import { Paths } from '@/utils/Paths'
const LoginStep2: FC = () => {
const { t } = useTranslation('global')
const { phone, setStepLogin } = useAuthStore()
const { value, reset } = useCountDown(2, true)
const otpVerify = useOtpVerify()
const loginWithOtp = useLoginWithOtp()
const formik = useFormik<OtpVerifyType>({
initialValues: {
phone: phone,
code: ''
},
validationSchema: Yup.object({
code: Yup.string()
.required(t('errors.required'))
}),
onSubmit(values) {
const params: OtpVerifyType = {
phone: phone,
code: values.code
}
otpVerify.mutate(params, {
onSuccess(data) {
setToken(data?.data?.accessToken?.token)
setRefreshToken(data?.data?.refreshToken?.token)
// Check if there's a redirect parameter in the URL
const redirectUrl = getRedirectUrl();
if (redirectUrl) {
window.location.href = redirectUrl;
return; // Prevent the default navigation
}
window.location.href = Paths.received
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], 'error')
},
})
},
})
useEffect(() => {
// بررسی پشتیبانی از Web OTP API
if (!("OTPCredential" in window)) return;
const ac = new AbortController();
navigator.credentials
.get({
otp: { transport: ["sms"] },
signal: ac.signal,
} as CredentialRequestOptions) // اطمینان از تطابق تایپ‌ها
.then((otpCredential) => {
if (otpCredential && "code" in otpCredential) {
formik.setFieldValue("code", (otpCredential).code);
}
})
.catch((err) => console.error("SMS AutoFill Error:", err));
return () => ac.abort();
}, []);
const reSend = () => {
loginWithOtp.mutate({ phone: phone }, {
onSuccess: () => {
reset()
toast(t('auth.toast_resend_code'), 'success')
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
}
})
}
useEffect(() => {
if (formik.values.code.length === 5) {
formik.handleSubmit()
}
}, [formik.values.code])
return (
<div>
<div className='mt-5'>
<h2 className='lg:text-2xl font-bold'>
{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'>
<div className='flex gap-1'>
<div>{t('auth.verfify_code_text_1')}</div>
<div>{phone}</div>
<div>
{t('auth.verfify_code_text_2')}
</div>
</div>
<div onClick={() => setStepLogin(1)} className='text-black cursor-pointer'>
{t('auth.change_number')}
</div>
</p>
</div>
<div className='mt-16'>
<div className='text-sm'>
{t('auth.verify_code')}
</div>
<div className='mt-2 w-full flex justify-center dltr otp'>
<OTPInput
shouldAutoFocus
value={formik.values.code}
numInputs={5}
inputType='tel'
onChange={(otp: string) => formik.setFieldValue('code', otp)}
renderInput={(props) =>
<input
{...props}
type='tel'
autoComplete="one-time-code"
inputMode="numeric"
className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />
}
/>
</div>
{
formik.touched.code && formik.errors.code &&
<div className='text-xs mt-2 text-red-500'>{formik.errors.code}</div>
}
<div className='flex mt-4 justify-end'>
{
value !== '00:00' ?
<div className='flex gap-1 text-description 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 className=''>
{t('auth.try_again')}
</div>
</div>
}
</div>
</div>
<Button
label={t('auth.login')}
className='mt-8'
onClick={() => formik.handleSubmit()}
loading={otpVerify.isPending}
/>
{/* <div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
<p className='text-description'>
{t('auth.login_with_password')}
</p>
<img src={ArrowLeftIcon} className='w-5' />
</div> */}
</div>
)
}
export default LoginStep2
+102
View File
@@ -0,0 +1,102 @@
import { FC } from 'react'
import Input from '../../../components/Input'
import { useTranslation } from 'react-i18next'
import { LoginWithPasswordType } from '../types/AuthTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useAuthStore } from '../store/AuthStore'
import Error from '../../../components/Error'
import Button from '../../../components/Button'
import { useLoginWithPassword } from '../hooks/useAuthData'
import { toast } from '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
import { setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
import { getRedirectUrl } from '../../../config/func'
import { Paths } from '@/utils/Paths'
import { ArrowLeft } from 'iconsax-react'
const LoginStep3: FC = () => {
const { t } = useTranslation('global')
const { setStepLogin, email, phone } = useAuthStore()
const loginWithPassword = useLoginWithPassword()
const formik = useFormik<LoginWithPasswordType>({
initialValues: {
password: '',
email: email ? email : undefined,
phone: phone ? phone : undefined,
},
validationSchema: Yup.object({
password: Yup.string()
.required(t('errors.required'))
}),
onSubmit(values) {
loginWithPassword.mutate(values, {
onSuccess(data) {
setToken(data?.data?.accessToken?.token)
setRefreshToken(data?.data?.refreshToken?.token)
const redirectUrl = getRedirectUrl();
if (redirectUrl) {
window.location.href = redirectUrl;
return; // Prevent the default navigation
}
window.location.href = Paths.received
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], 'error')
},
})
},
})
return (
<div>
<div className='mt-5'>
<h2 className='text-2xl font-bold'>
{t('auth.enter_password_step3')}
</h2>
<p className='text-description text-sm mt-2'>
{t('auth.enter_your_password')}
</p>
</div>
<div className='mt-16'>
<Input
label={t('auth.password')}
type='password'
className='text-right'
name='password'
onChange={formik.handleChange}
value={formik.values.password}
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
/>
{
formik.touched.password && formik.errors.password &&
<Error
errorText={formik.errors.password}
/>
}
</div>
<Button
label={t('auth.login')}
className='mt-8'
onClick={() => formik.handleSubmit()}
/>
<div onClick={() => setStepLogin(1)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
<p className='text-description'>
{t('auth.login_with_otp')}
</p>
<ArrowLeft size={20} color='black' />
</div>
</div>
)
}
export default LoginStep3
+45
View File
@@ -0,0 +1,45 @@
import { useMutation } from '@tanstack/react-query';
import * as api from '../service/AuthService'
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
export const useLoginWithPassword = () => {
return useMutation({
mutationFn: (variables: LoginWithPasswordType) => api.loginWithPassword(variables),
});
};
export const useLoginWithOtp = () => {
return useMutation({
mutationFn: (variables: LoginWithOtpType) => api.loginWithOtp(variables),
});
};
export const useOtpVerify = () => {
return useMutation({
mutationFn: (variables: OtpVerifyType) => api.otpVerify(variables),
});
};
export const useCheckHasAccount = () => {
return useMutation({
mutationFn: (variables: CheckHasAccountType) => api.checkHasAccount(variables),
});
};
export const useCheckHasAccountRegister = () => {
return useMutation({
mutationFn: (variables: CheckHasAccountPhoneType) => api.checkHasAccountRegister(variables),
});
};
export const useRegister = () => {
return useMutation({
mutationFn: (variables: RegisterType) => api.register(variables),
});
};
export const useLogout = () => {
return useMutation({
mutationFn: () => api.logout(),
});
};
+50
View File
@@ -0,0 +1,50 @@
import axios from "../../../config/axios";
import {
CheckHasAccountPhoneType,
CheckHasAccountType,
LoginWithOtpType,
LoginWithPasswordType,
OtpVerifyType,
RefreshTokenType,
RegisterType,
} from "../types/AuthTypes";
export const loginWithPassword = async (params: LoginWithPasswordType) => {
const { data } = await axios.post(`/auth/login/password`, params);
return data;
};
export const loginWithOtp = async (params: LoginWithOtpType) => {
const { data } = await axios.post(`/auth/otp/send`, params);
return data;
};
export const otpVerify = async (params: OtpVerifyType) => {
const { data } = await axios.post(`/auth/otp/verify`, params);
return data;
};
export const checkHasAccount = async (params: CheckHasAccountType) => {
const { data } = await axios.post(`/auth/check`, params);
return data;
};
export const checkHasAccountRegister = async (
params: CheckHasAccountPhoneType
) => {
const { data } = await axios.post(`/auth/register/initiate`, params);
return data;
};
export const register = async (params: RegisterType) => {
const { data } = await axios.post(`/auth/register/complete`, params);
return data;
};
export const refreshToken = async (params: RefreshTokenType) => {
const { data } = await axios.post(`/auth/refresh`, params);
return data;
};
export const logout = async () => {
const { data } = await axios.post(`/auth/logout`);
return data;
};
-7
View File
@@ -1,7 +0,0 @@
import axiosDanak from "@/config/axiosDanak";
import { RefreshTokenType } from "../types/Types";
export const refreshToken = async (params: RefreshTokenType) => {
const { data } = await axiosDanak.post(`/auth/refresh`, params);
return data;
};
+17
View File
@@ -0,0 +1,17 @@
import { create } from "zustand";
import { AuthStoreType } from "../../auth/types/AuthTypes";
export const useAuthStore = create<AuthStoreType>((set) => ({
phone: "",
setPhone(value) {
set({ phone: value });
},
email: "",
setEmail(value) {
set({ email: value });
},
stepLogin: 1,
setStepLogin(value) {
set({ stepLogin: value });
},
}));
+52
View File
@@ -0,0 +1,52 @@
export type LoginType = {
phone_email: string;
};
export type LoginPasswordType = {
password: string;
};
export type AuthStoreType = {
phone: string;
setPhone: (value: string) => void;
email: string;
setEmail: (value: string) => void;
stepLogin: number;
setStepLogin: (value: number) => void;
};
export type LoginWithPasswordType = {
email?: string;
password: string;
phone?: string;
};
export type LoginWithOtpType = {
phone: string;
};
export type OtpVerifyType = {
phone: string;
code: string;
};
export type CheckHasAccountType = {
email: string;
};
export type CheckHasAccountPhoneType = {
phone: string;
};
export type RegisterType = {
phone: string;
firstName: string;
lastName: string;
birthDate: string;
nationalCode: string;
password: string;
code: number;
};
export type RefreshTokenType = {
refreshToken: string;
};
-3
View File
@@ -1,3 +0,0 @@
export type RefreshTokenType = {
refreshToken: string;
};
-83
View File
@@ -1,83 +0,0 @@
import { FC, useEffect, useState } from 'react'
import { useWorkspaces } from './hooks/useHomeData'
import { workspaceItem } from './types/HomeTypes'
import moment from 'moment-jalaali'
const Home: FC = () => {
const getWorkspaces = useWorkspaces()
const [selectedWorkspace, setSelectedWorkspace] = useState<string | null>(null)
useEffect(() => {
setSelectedWorkspace(localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID))
}, [])
useEffect(() => {
if (getWorkspaces.isError && getWorkspaces.error instanceof Error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error = getWorkspaces.error as any;
if (error.response?.status === 403) {
localStorage.removeItem(import.meta.env.VITE_WORKSPACE_ID)
window.location.href = 'https://console.danakcorp.com'
}
}
}, [getWorkspaces.isError, getWorkspaces.data])
const handleChangeWorkspace = (id: string) => {
localStorage.setItem(import.meta.env.VITE_WORKSPACE_ID, id)
setSelectedWorkspace(id)
window.location.reload()
}
return (
<div className='w-full flex gap-6'>
<div className='flex-1'>
<div className='mt-8 flex-wrap text-sm flex gap-6 items-center'>
{
getWorkspaces.data?.data?.workspaces?.map((item: workspaceItem) => {
return (
<div className={`p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 bg-white rounded-3xl cursor-pointer transition-all duration-300 hover:shadow-lg ${selectedWorkspace === item.id ? 'border border-black' : ''}`}
onClick={() => handleChangeWorkspace(item.id)}
>
<div className='mt-5'>
{item.businessName}
</div>
<div className='mt-5'>
{item.plan?.name}
</div>
<div className='flex justify-between items-center mt-5 w-full'>
<div className=' flex gap-1'>
<div>تاریخ پایان: </div>
<div className='text-description text-sm'>{moment(item?.endDate).format('jYYYY/jMM/jDD')}</div>
</div>
</div>
<div className='mt-5 flex justify-end w-full'>
<a onClick={(e) => {
e.stopPropagation()
}} target='_blank' href={`${import.meta.env.VITE_DZONE_URL}/${item.slug}`}>
{`${import.meta.env.VITE_DZONE_URL}/${item.slug}`}
</a>
</div>
</div>
)
})
}
<div className='px-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px]'></div>
</div>
</div>
</div>
)
}
export default Home
@@ -1,146 +0,0 @@
import { Add } from 'iconsax-react'
import { FC, useState } from 'react'
import DefaulModal from '../../../components/DefaulModal'
import { useTranslation } from 'react-i18next'
import Input from '../../../components/Input'
import Select from '../../../components/Select'
import SwitchComponent from '../../../components/Switch'
const BoxNewAccessbility: FC = () => {
const [open, setOpen] = useState<boolean>(false)
const { t } = useTranslation('global')
return (
<>
<div
onClick={() => setOpen(true)}
className="flex-1 min-w-[40%] xl:min-w-[20%] flex justify-center items-center h-[160px]"
style={{
position: 'relative',
}}
>
<svg
width="100%"
height="100%"
xmlns="http://www.w3.org/2000/svg"
style={{
position: 'absolute',
top: 0,
left: 0,
zIndex: -1,
borderRadius: '22px',
}}
>
<rect
width="100%"
height="100%"
fill="none"
// rx="22"
ry="22"
stroke="#8C90A3"
strokeWidth="2"
strokeDasharray="6,14"
// strokeDashoffset="25"
/>
</svg>
<div style={{ zIndex: 1 }}>
<Add size={38} color="#8C90A3" />
</div>
</div>
<DefaulModal
open={open}
close={() => setOpen(false)}
isHeader={true}
title_header={t('home.create_new_access')}
>
<div className='mt-6'>
<div className='flex items-center gap-6'>
<div className='min-w-[210px]'>
<Input
variant='search'
className='bg-opacity-25 bg-white'
/>
</div>
<Select
items={[
{ value: '1', label: '1' },
{ value: '2', label: '2' },
{ value: '3', label: '3' },
]}
placeholder={t('all')}
className='bg-opacity-25 bg-white w-[100px]'
/>
</div>
<div className='mt-6 flex flex-wrap gap-6'>
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
<div className='flex gap-4'>
<div className='size-10 rounded-xl bg-red-300'></div>
<div>
<div className='text-sm'>
دی منو
</div>
<div className='text-xs text-description'>
منو رستوران
</div>
</div>
</div>
<div>
<SwitchComponent
active
onChange={() => { }}
/>
</div>
</div>
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
<div className='flex gap-4'>
<div className='size-10 rounded-xl bg-blue-300'></div>
<div>
<div className='text-sm'>
دی منو
</div>
<div className='text-xs text-description'>
منو رستوران
</div>
</div>
</div>
<div>
<SwitchComponent
active
onChange={() => { }}
/>
</div>
</div>
<div className='flex-1 xl:min-w-[40%] min-w-full flex justify-between items-center border-b border-[#8C90A3] border-opacity-30 py-2'>
<div className='flex gap-4'>
<div className='size-10 rounded-xl bg-green-300'></div>
<div>
<div className='text-sm'>
دی منو
</div>
<div className='text-xs text-description'>
منو رستوران
</div>
</div>
</div>
<div>
<SwitchComponent
active
onChange={() => { }}
/>
</div>
</div>
<div className='flex-1 min-w-[40%]'></div>
</div>
</div>
</DefaulModal>
</>
)
}
export default BoxNewAccessbility
@@ -1,46 +0,0 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { ArrowLeft } from 'iconsax-react'
import CoverImage from '../../../assets/images/banner.png'
const DanakLearning: FC = () => {
const { t } = useTranslation('global')
return (
<div className='bg-white w-sidebar text-xs hidden 2xl:block h-fit px-5 py-7 rounded-3xl'>
<div className='flex justify-between items-center'>
<div>
{t('home.danak_learning')}
</div>
<div className='flex gap-2 items-center text-[10px]'>
<div>{t('home.see_all')}</div>
<ArrowLeft size={12} color='black' />
</div>
</div>
<div className='mt-7'>
<div className='flex gap-3'>
<div>
<img src={CoverImage} alt='danak-learning-1' className='w-36 h-24 object-cover rounded-2xl' />
</div>
<div className='text-xs'>
<div className='leading-5'>
لورم ایپسوم متن ساختگی با تولید سادگی
</div>
<div className='flex gap-1 text-[11px] mt-3 text-description'>
<div>دیزاین ,</div>
<div>۲۴ دقیقه</div>
</div>
<div className='text-description text-[11px] mt-2'>
آذر ماه 1403
</div>
</div>
</div>
</div>
</div>
)
}
export default DanakLearning
@@ -1,37 +0,0 @@
import { FC, ReactNode } from 'react'
import { Link } from 'react-router-dom'
type Props = {
title: string
icon: ReactNode,
color: string,
count: number,
description: string,
to?: string
}
const ItemDashboard: FC<Props> = (props: Props) => {
return (
<Link to={props.to ? props.to : ''} className='p-6 min-w-[40%] xl:min-w-[15%] flex flex-col items-center xl:items-start flex-1 h-[178px] bg-white rounded-3xl'>
<div className='size-10 rounded-full bg-[#EEF0F7] flex justify-center items-center'>
{props.icon}
</div>
<div className='mt-5'>
{props.title}
</div>
<div className='mt-5 text-xs text-description flex gap-1.5 items-center'>
<div style={{ background: props.color }} className='size-1.5 rounded-full'></div>
<div className='flex gap-0.5'>
<div>{props.count}</div>
<div className='whitespace-nowrap'>
{props.description}
</div>
</div>
</div>
</Link>
)
}
export default ItemDashboard
-9
View File
@@ -1,9 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/HomeService";
export const useWorkspaces = () => {
return useQuery({
queryKey: ["workspaces"],
queryFn: api.getWorkspaces,
});
};
-8
View File
@@ -1,8 +0,0 @@
import danakAxios from "../../../config/axiosDanak";
export const getWorkspaces = async () => {
const { data } = await danakAxios.get(
`/subscriptions/workspaces/${import.meta.env.VITE_SERVICE_ID}`
);
return data;
};
-48
View File
@@ -1,48 +0,0 @@
import { UserItemType } from "../../users/types/UserTypes";
export type workspaceItem = {
id: string;
createdAt: string;
updatedAt: string;
plan: {
id: string;
createdAt: string;
updatedAt: string;
name: string;
duration: number;
price: number;
originalPrice: number;
isActive: boolean;
service: {
id: string;
createdAt: string;
updatedAt: string;
name: string;
title: string;
slug: string;
isDanakSuggest: boolean;
description: string;
author: string;
createDate: string;
userCount: number;
serviceLanguage: string;
softwareLanguage: string;
metaDescription: string;
isActive: boolean;
showInSlider: boolean;
link: string;
icon: string;
coverUrl: string;
deletedAt: string | null;
};
deletedAt: string | null;
};
startDate: string;
endDate: string;
businessName: string;
businessPhone: string;
description: string;
slug: string;
status: string;
staff: UserItemType[];
};
-88
View File
@@ -1,88 +0,0 @@
import Tabs from '@/components/Tabs'
import { Brush2, Global, PenClose, People, Setting3 } from 'iconsax-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingTabEnum } from './enum/SettingEnum'
import Personality from './personality/Personality'
import Domain from './domain/Domain'
import MailServer from './mail-server/MailServer'
import Address from './address/Address'
import PersonalitySidebar from './personality/SideBar'
import Signture from './signture/Signture'
const Setting: FC = () => {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
const renderActiveTab = () => {
switch (activeTab) {
case SettingTabEnum.SETTING_MAIL_SERVER:
return <MailServer />;
case SettingTabEnum.SETTING_DOMAIN:
return <Domain />;
case SettingTabEnum.SETTING_ADDRESS:
return <Address />;
case SettingTabEnum.SETTING_SIGNATURE:
return <Signture />;
case SettingTabEnum.SETTING_PERSONALITY:
return (
<div className='flex xl:flex-row-reverse gap-2 md:gap-6 mt-6 md:mt-8'>
<div className="xl:w-auto">
<PersonalitySidebar />
</div>
<div className="flex-1">
<Personality />
</div>
</div>
);
default:
return null;
}
};
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className='text-lg mb-4 md:mb-0'>{t('setting.title')}</h1>
<div className='mt-4 md:mt-7'>
<Tabs
items={[
{
icon: <Setting3 size={20} color={activeTab === SettingTabEnum.SETTING_MAIL_SERVER ? 'black' : '#8C90A3'} />,
label: t('setting.mail_server'),
value: SettingTabEnum.SETTING_MAIL_SERVER
},
{
icon: <Global size={20} color={activeTab === SettingTabEnum.SETTING_DOMAIN ? 'black' : '#8C90A3'} />,
label: t('setting.domain'),
value: SettingTabEnum.SETTING_DOMAIN
},
{
icon: <People size={20} color={activeTab === SettingTabEnum.SETTING_ADDRESS ? 'black' : '#8C90A3'} />,
label: t('setting.address'),
value: SettingTabEnum.SETTING_ADDRESS
},
{
icon: <Brush2 size={20} color={activeTab === SettingTabEnum.SETTING_PERSONALITY ? 'black' : '#8C90A3'} />,
label: t('setting.personality'),
value: SettingTabEnum.SETTING_PERSONALITY
},
{
icon: <PenClose size={20} color={activeTab === SettingTabEnum.SETTING_SIGNATURE ? 'black' : '#8C90A3'} />,
label: t('setting.signature'),
value: SettingTabEnum.SETTING_SIGNATURE
}
]}
active={activeTab}
onChange={(value) => setActiveTab(value as SettingTabEnum)}
/>
</div>
<div>
{renderActiveTab()}
</div>
</div>
)
}
export default Setting
-218
View File
@@ -1,218 +0,0 @@
import Input from '@/components/Input'
import Select from '@/components/Select'
import Table from '@/components/Table'
import { ColumnType } from '@/components/types/TableTypes'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/components/Button'
import { TickCircle } from 'iconsax-react'
import { useForm } from 'react-hook-form'
import { AddressData, CreateAddressType } from './types/Types'
import { useGetDomains } from '../domain/hooks/useDomainData'
import { yupResolver } from "@hookform/resolvers/yup"
import * as yup from "yup"
import { useCreateAddress, useGetAddress } from './hooks/useAddressData'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
import ProgressBarEmail from './components/ProgressBarEmail'
import ToggleStatus from './components/ToggleStatus'
const Address: FC = () => {
const { t } = useTranslation()
const schema = yup.object().shape({
title: yup.string().required(t('errors.required')),
username: yup.string().required(t('errors.required')),
password: yup.string().required(t('errors.required')),
domainId: yup.string().required(t('errors.required')),
})
const [search, setSearch] = useState<string>('')
const [status, setStatus] = useState<string>('all')
const { data: domain } = useGetDomains()
const { mutate: createAddress, isPending } = useCreateAddress()
const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status)
const { register, handleSubmit, formState: { errors },
} = useForm<CreateAddressType>({
resolver: yupResolver(schema),
defaultValues: {
domainId: domain?.data?.domain?.id || '',
}
})
const columns: ColumnType<AddressData>[] = [
{
title: t('setting.address_title'),
key: 'title',
width: '200px'
},
{
title: t('setting.email'),
key: 'email',
width: '300px',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
{item.emailAddress}
</div>
)
},
{
title: t('setting.quota'),
key: 'quota',
width: '300px',
render: (item: AddressData) => {
return (
<ProgressBarEmail key={item.id} item={item} />
)
}
},
{
title: t('setting.status'),
key: 'status',
width: '120px',
align: 'center',
render: (item: AddressData) => (
<div key={item.id} className='dltr flex justify-end'>
<ToggleStatus defaultStatus={item.isActive} id={item.id} />
</div>
)
}
]
const statusOptions = [
{ value: 'all', label: 'همه' },
{ value: 'active', label: t('setting.active') },
{ value: 'inactive', label: t('setting.inactive') }
]
const onSubmit = (params: CreateAddressType) => {
createAddress(params, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
return (
<div className='flex xl:flex-row flex-col-reverse gap-8 mt-8'>
<div className='flex-1'>
<div className='flex gap-2 justify-between'>
<div>
<Select
placeholder={t('setting.status')}
items={statusOptions}
className='w-[120px]'
onChange={(e) => setStatus(e.target.value)}
/>
</div>
<div className='flex-1'>
<Input
variant='search'
placeholder={t('setting.search')}
className='xl:w-[300px] flex-1'
onChangeSearchFinal={(e) => setSearch(e)}
/>
</div>
</div>
<Table
isLoading={isPendingAddress}
columns={columns}
data={addresses?.data?.users}
pagination={{
currentPage: 1,
totalPages: 6,
onPageChange: (page) => console.log('Page:', page)
}}
/>
</div>
<div className='xl:w-[350px] w-full bg-white rounded-4xl p-8'>
<div>
{t('setting.add_address')}
</div>
{/* <div className='mt-8 flex items-center justify-between'>
<div>
{t('setting.status')}
</div>
<div className='dltr pt-2'>
<Switch />
</div>
</div> */}
<div className='mt-6'>
<Input
label={t('setting.domain1')}
value={domain?.data?.domain?.name}
readOnly
/>
</div>
<div className='mt-6'>
<Input
label={t('setting.title1')}
{...register('title')}
error_text={errors.title?.message}
/>
</div>
<div className='mt-6 relative'>
<Input
label={t('setting.username')}
className='text-left'
{...register('username')}
error_text={errors.username?.message}
/>
<div className='absolute flex items-center dltr text-xs z-10 right-[1px] top-[29px] rounded-r-2.5 h-[38px] bg-[#EBEEF5] px-3'>
@ {domain?.data?.domain?.name}
</div>
</div>
<div className='mt-6'>
<Input
label={t('setting.password')}
type='password'
{...register('password')}
error_text={errors.password?.message}
/>
</div>
<div className='mt-2 text-xs'>
<div>رمز عبور میبایست:</div>
<ul className='list-disc pr-3 mt-1 flex flex-col gap-1'>
<li>حداقل ۸ کاراکتر باشد</li>
<li>ترکیبی از حروف کوچک و بزرگ باشد</li>
<li>شامل اعداد باشد</li>
<li>شامل کاراکتر های خاص (نماد ها) باشد</li>
</ul>
</div>
<div className='mt-9 flex justify-end'>
<Button
className='w-fit px-20'
onClick={handleSubmit(onSubmit)}
loading={isPending}
>
<div className='flex gap-2'>
<TickCircle size={20} color='white' />
<div>
{t('setting.create')}
</div>
</div>
</Button>
</div>
</div>
</div>
)
}
export default Address
@@ -1,33 +0,0 @@
import { convertToGB } from '@/config/func'
import { FC } from 'react'
import { AddressData } from '../types/Types'
const ProgressBarEmail: FC<{ item: AddressData }> = ({ item }) => {
const usedQuota = item.emailQuotaUsed || 0;
const totalQuota = item.emailQuota || 0;
const percentage = Math.min((usedQuota / totalQuota) * 100, 100);
return (
<div className='flex flex-col gap-2'>
<div className='flex justify-between text-sm'>
<span className='text-gray-600 dltr'>
{convertToGB(usedQuota)} GB / {convertToGB(totalQuota)} GB
</span>
{/* <span className='text-gray-500'>
{percentage.toFixed(1)}%
</span> */}
</div>
<div className='w-full bg-gray-200 dltr overflow-hidden rounded-full h-2'>
<div
className={`h-2 rounded-full transition-all duration-300 ${percentage > 90 ? 'bg-red-500' :
percentage > 70 ? 'bg-yellow-500' :
'bg-green-500'
}`}
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
)
}
export default ProgressBarEmail
@@ -1,18 +0,0 @@
import { Switch } from '@/components/ui/switch'
import { FC, useState } from 'react'
import { useToggleStatus } from '../hooks/useAddressData'
const ToggleStatus: FC<{ defaultStatus: boolean, id: string }> = ({ defaultStatus, id }) => {
const [isActive, setIsActive] = useState(defaultStatus)
const { mutate: toggleStatus } = useToggleStatus()
const handleToggleStatus = () => {
toggleStatus(id)
setIsActive(!isActive)
}
return (
<Switch checked={isActive} onCheckedChange={handleToggleStatus} />
)
}
export default ToggleStatus
@@ -1,26 +0,0 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/Service";
import { useQueryClient } from "@tanstack/react-query";
export const useGetAddress = (search: string, status: string) => {
return useQuery({
queryKey: ["address", search, status],
queryFn: () => api.getAddress(search, status),
});
};
export const useCreateAddress = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createAddress,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["address"] });
},
});
};
export const useToggleStatus = () => {
return useMutation({
mutationFn: api.toggleStatus,
});
};
@@ -1,21 +0,0 @@
import axios from "@/config/axios";
import { CreateAddressType } from "../types/Types";
export const getAddress = async (search: string, status: string) => {
const query = new URLSearchParams();
if (search) query.set("q", search);
if (status && status !== "all")
query.set("isActive", status === "active" ? "1" : "0");
const { data } = await axios.get(`/users?${query.toString()}`);
return data;
};
export const createAddress = async (params: CreateAddressType) => {
const { data } = await axios.post(`/users`, params);
return data;
};
export const toggleStatus = async (id: string) => {
const { data } = await axios.patch(`/users/${id}/toggle-status`);
return data;
};
-58
View File
@@ -1,58 +0,0 @@
export type CreateAddressType = {
username: string;
password: string;
domainId: string;
title: string;
};
export interface AddressData extends Record<string, unknown> {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
title: string;
userName: string;
password: string;
emailAddress: string;
emailEnabled: boolean;
emailQuota: number;
emailQuotaUsed: number;
emailAccountId: string;
displayName: string;
isActive: boolean;
emailSignature: string | null;
role: string;
business: {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
danakSubscriptionId: string;
name: string;
slug: string;
logoUrl: string | null;
domain: string;
};
domain: {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
name: string;
status: string;
isVerified: boolean;
isActive: boolean;
verifiedAt: string;
lastCheckedAt: string | null;
notes: string;
dkimEnabled: boolean;
dkimSelector: string;
dkimPrivateKey: string;
dkimPublicKey: string;
spfEnabled: boolean;
spfRecord: string | null;
dmarcEnabled: boolean;
dmarcPolicy: string | null;
business: string;
};
}
-156
View File
@@ -1,156 +0,0 @@
import Table from '@/components/Table'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TickCircle, Copy, CloseCircle, Clock, TickSquare } from 'iconsax-react'
import CreateDomain from './components/CreateDomain'
import { useGetDnsRecords } from './hooks/useDomainData'
import { DnsRecordType } from './types/Types'
const Domain: FC = () => {
const { t } = useTranslation()
const [disabled, setDisabled] = useState(false)
const { data, isLoading } = useGetDnsRecords(disabled)
const [copiedField, setCopiedField] = useState<string | null>(null)
useEffect(() => {
if (data?.data?.overallStatus?.isVerified && !disabled) {
setDisabled(true)
}
}, [data])
const getStatusIcon = (status: DnsRecordType['status']) => {
switch (status) {
case 'VERIFIED':
return (
<div className="flex items-center gap-2">
<TickCircle size={20} color="#22C55E" variant="Bold" />
<span className="text-green-500 text-xs">{t('domain.status.verified')}</span>
</div>
)
case 'PENDING':
return (
<div className="flex items-center gap-2">
<Clock size={20} color="#F59E0B" variant="Bold" />
<span className="text-yellow-500 text-xs">{t('domain.status.pending')}</span>
</div>
)
case 'FAILED':
return (
<div className="flex items-center gap-2">
<CloseCircle size={20} color="#EF4444" variant="Bold" />
<span className="text-red-500 text-xs">{t('domain.status.failed')}</span>
</div>
)
default:
return null
}
}
const copyToClipboard = (text: string, fieldKey: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedField(fieldKey)
setTimeout(() => {
setCopiedField(null)
}, 2000)
})
}
const columns = [
{
title: t('domain.table.record_type'),
key: 'type',
width: '100px',
align: 'center' as const,
},
{
title: t('domain.table.record_name'),
key: 'name',
render: (record: DnsRecordType) => {
const fieldKey = `name-${record.id}`
const isCopied = copiedField === fieldKey
return (
<div className="flex items-center gap-2">
<button
onClick={() => copyToClipboard(record.name, fieldKey)}
className={`transition-all duration-200 ${isCopied ? 'text-green-500' : 'text-blue-500 hover:text-blue-700'}`}
>
{isCopied ? (
<TickSquare size={16} color='#22C55E' variant="Bold" />
) : (
<Copy size={16} color='#0038FF' />
)}
</button>
<span className="truncate max-w-[200px] dltr">{record.name}</span>
</div>
)
}
},
{
title: t('domain.table.record_value'),
key: 'value',
render: (record: DnsRecordType) => {
const fieldKey = `value-${record.id}`
const isCopied = copiedField === fieldKey
return (
<div className="flex items-center gap-2">
<button
onClick={() => copyToClipboard(record.value, fieldKey)}
className={`transition-all duration-200 ${isCopied ? 'text-green-500' : 'text-blue-500 hover:text-blue-700'}`}
>
{isCopied ? (
<TickSquare size={16} color='#22C55E' variant="Bold" />
) : (
<Copy size={16} color='#0038FF' />
)}
</button>
<span className="truncate max-w-[300px] dltr">{record.value}</span>
</div>
)
}
},
{
title: t('domain.table.priority'),
key: 'priority',
width: '80px',
align: 'center' as const,
render: (record: DnsRecordType) => (
<span>{record.priority || '-'}</span>
)
},
{
title: t('domain.table.ttl'),
key: 'ttl',
width: '80px',
align: 'center' as const,
render: (record: DnsRecordType) => (
<span>{record.ttl}</span>
)
},
{
title: t('domain.table.status'),
key: 'status',
render: (record: DnsRecordType) => getStatusIcon(record.status)
},
]
const dnsRecords = data?.data?.dnsRecords || []
return (
<div>
<CreateDomain />
<div className='mt-9'>
<Table
columns={columns}
data={dnsRecords}
className="!mt-0"
isLoading={isLoading}
/>
</div>
</div>
)
}
export default Domain
@@ -1,76 +0,0 @@
import Button from '@/components/Button'
import Input from '@/components/Input'
import { toast } from '@/components/Toast'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useCreateDomain, useGetDomains } from '../hooks/useDomainData'
import { ErrorType } from '@/helpers/types'
const CreateDomain: FC = () => {
const { t } = useTranslation()
const { mutate: createDomain, isPending } = useCreateDomain()
const { data: domains } = useGetDomains()
const [domain, setDomain] = useState('')
const [isVerified, setIsVerified] = useState<boolean>(false)
useEffect(() => {
if (domains?.data?.domain) {
setDomain(domains?.data?.domain?.name)
if (domains?.data?.domain?.isVerified) {
setIsVerified(true)
}
}
}, [domains])
const handleCreateDomain = () => {
createDomain({
name: domain,
notes: domain
}, {
onSuccess: (data) => {
toast(data.message, 'success')
setDomain('')
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
return (
<div className='flex xl:flex-row gap-4 flex-col justify-between mt-9 items-end'>
<div>
<div className='xl:text-lg'>
{t('setting.record_dns')}
</div>
<p className='mt-2 xl:text-sm text-xs font-extralight'>
{t('setting.record_dns_description')}
</p>
</div>
<div className='flex gap-2'>
<Input
placeholder={t('setting.your_domain')}
className='xl:w-[300px]'
value={domain}
onChange={(e) => setDomain(e.target.value)}
readOnly={isVerified}
/>
{
!isVerified &&
<Button
variant='secondary'
className='w-fit xl:px-14 px-8 border border-black'
label={t('setting.submit')}
onClick={handleCreateDomain}
loading={isPending}
/>
}
</div>
</div>
)
}
export default CreateDomain
@@ -1,32 +0,0 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { CreateDomainType } from "../types/Types";
import * as api from "../service/DomainService";
export const useCreateDomain = () => {
return useMutation({
mutationFn: (variables: CreateDomainType) => api.createDomain(variables),
});
};
export const useGetDomains = () => {
return useQuery({
queryKey: ["domains"],
queryFn: () => api.getDomains(),
});
};
export const useGetDnsRecords = (disabled?: boolean) => {
return useQuery({
queryKey: ["dns-records"],
queryFn: () => api.getDnsRecords(),
refetchInterval: 3000,
enabled: !disabled,
});
};
export const useVerifyDnsRecord = () => {
return useQuery({
queryKey: ["verify-dns-record"],
queryFn: () => api.verifyDnsRecord(),
});
};
@@ -1,22 +0,0 @@
import axios from "@/config/axios";
import { CreateDomainType } from "../types/Types";
export const createDomain = async (params: CreateDomainType) => {
const { data } = await axios.post(`/domains`, params);
return data;
};
export const getDomains = async () => {
const { data } = await axios.get(`/domains`);
return data;
};
export const getDnsRecords = async () => {
const { data } = await axios.get(`/domains/dns-records`);
return data;
};
export const verifyDnsRecord = async () => {
const { data } = await axios.get(`/domains/verify-dns`);
return data;
};
-33
View File
@@ -1,33 +0,0 @@
import { IResponse } from "@/types/response.types";
export type CreateDomainType = {
name: string;
notes?: string;
};
export type DnsRecordType = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
wildduckId: string | null;
name: string;
type: "MX" | "SPF" | "DMARC" | "DKIM" | "TXT" | "A" | "AAAA" | "CNAME";
value: string;
ttl: number;
priority: number | null;
status: "PENDING" | "VERIFIED" | "FAILED";
isRequired: boolean;
isActive: boolean;
lastVerifiedAt: string | null;
lastCheckedAt: string | null;
description: string;
errorMessage: string | null;
verificationAttempts: number;
nextVerificationAt: string | null;
domain: string;
};
export type DnsRecordResponseType = IResponse<{
dnsRecords: DnsRecordType[];
}>;
-15
View File
@@ -1,15 +0,0 @@
export enum SettingTabEnum {
SETTING_MAIL_SERVER = "setting_mail_server",
SETTING_DOMAIN = "setting_domain",
SETTING_ADDRESS = "setting_address",
SETTING_PERSONALITY = "setting_personality",
SETTING_SIGNATURE = "setting_signature",
}
export enum SideBarTab {
SETTING = "setting",
TEXT = "text",
BUTTON = "button",
IMAGE = "image",
NONE = "none",
}
@@ -1,79 +0,0 @@
import Input from '@/components/Input'
import RadioGroup from '@/components/RadioGroup'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
const Domain: FC = () => {
const { t } = useTranslation()
return (
<div className='bg-white rounded-4xl p-8 mt-9'>
<div>
<div className='flex xl:flex-row gap-4 flex-col justify-between xl:items-end border-b pb-10 broder-border'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.setting_fetch')}
</div>
<div className='xl:text-sm text-xs text-description mt-1.5'>
{t('setting.setting_fetch_description')}
</div>
</div>
<div className='flex-1'>
<RadioGroup
items={[
{
label: '۱ دقیقه',
value: '1'
},
{
label: '۲ دقیقه',
value: '2'
},
{
label: '۳ دقیقه',
value: '3'
}
]}
onChange={() => null}
selected='1'
/>
</div>
</div>
</div>
<div className='mt-9'>
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.setting_smtp')}
</div>
<div className='xl:text-sm text-xs text-description mt-1.5'>
{t('setting.setting_smtp_description')}
</div>
</div>
<div className='flex-1'>
<Input
label={t('setting.smtp_server')}
/>
<div className='mt-8'>
<Input
label={t('setting.smtp_port')}
/>
</div>
<div className='mt-8'>
<Input
label={t('setting.username')}
/>
</div>
<div className='mt-8'>
<Input
label={t('setting.password')}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default Domain
@@ -1,20 +0,0 @@
import { FC } from 'react'
import HeaderSection from './components/HeaderSection'
import ContentSection from './components/ContentSection'
import FooterSection from './components/FooterSection'
const Personality: FC = () => {
return (
<div className='flex-1 bg-white rounded-4xl p-8'>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
<tbody>
<HeaderSection />
<ContentSection />
<FooterSection />
</tbody>
</table>
</div>
)
}
export default Personality
-67
View File
@@ -1,67 +0,0 @@
import { FC, useState, useRef, useEffect } from 'react'
import { Gallery, LinkSquare, Setting4, Text } from 'iconsax-react'
import Logo from '@/assets/images/logo-small.svg'
import SettingSideBar from './components/SettingSideBar'
import { SideBarTab } from '../enum/SettingEnum'
import TextSidebar from './components/TextSidebar'
import ButtonSidebar from './components/ButtonSidebar'
import ImageSideBar from './components/ImageSideBar'
import { clx } from '@/helpers/utils'
const PersonalitySidebar: FC = () => {
const [active, setActive] = useState<SideBarTab>(SideBarTab.SETTING)
const sidebarRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// فقط در موبایل (عرض کمتر از 1280px) کار کند
if (window.innerWidth < 1280 && sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
setActive(SideBarTab.NONE)
}
}
if (active !== SideBarTab.NONE) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [active])
return (
<div ref={sidebarRef} className={clx(
'bg-white xl:rounded-4xl xl:overflow-hidden xl:w-[360px] flex h-full sticky top-0',
active === SideBarTab.NONE ? 'rounded-4xl' : 'rounded-r-4xl'
)}>
{active !== SideBarTab.NONE && (
<div className='absolute rounded-l-4xl shadow-[-2px_0_4px_-1px_rgba(0,0,0,0.05)] xl:shadow-none xl:static bg-white right-10 h-full z-10 xl:w-[360px] w-[250px] p-6 overflow-y-auto'>
{
active === SideBarTab.SETTING ? <SettingSideBar />
: active === SideBarTab.TEXT ? <TextSidebar />
: active === SideBarTab.BUTTON ? <ButtonSidebar />
: active === SideBarTab.IMAGE ? <ImageSideBar />
: null
}
</div>
)}
<div className='xl:w-20 xl:border-r py-5 border-gray-200 w-10 flex flex-col items-center pt-6 pb-6 h-full'>
{
active !== SideBarTab.NONE && <img src={Logo} className='w-8' alt="Logo" />
}
<div className={clx(
'flex flex-col gap-10 mt-16',
active === SideBarTab.NONE && 'mt-4'
)}>
<Setting4 size={22} color='black' variant={SideBarTab.SETTING === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.SETTING)} className='cursor-pointer' />
<Text size={22} color='black' variant={SideBarTab.TEXT === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.TEXT)} className='cursor-pointer' />
<LinkSquare size={22} color='black' variant={SideBarTab.BUTTON === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.BUTTON)} className='cursor-pointer' />
<Gallery size={22} color='black' variant={SideBarTab.IMAGE === active ? 'Bold' : 'Outline'} onClick={() => setActive(SideBarTab.IMAGE)} className='cursor-pointer' />
</div>
</div>
</div>
)
}
export default PersonalitySidebar
@@ -1,94 +0,0 @@
import { FC } from 'react'
import { ButtonType, ButtonSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
interface ButtonRendererProps {
buttons: ButtonType[]
}
const ButtonRenderer: FC<ButtonRendererProps> = ({ buttons }) => {
if (!buttons || buttons.length === 0) return null
// تبدیل enum به string برای HTML attributes
const getHorizontalAlign = (alignment?: HorizontalAlignment) => {
switch (alignment) {
case HorizontalAlignment.LEFT:
return 'left'
case HorizontalAlignment.RIGHT:
return 'right'
case HorizontalAlignment.CENTER:
default:
return 'center'
}
}
const getVerticalAlign = (verticalAlignment?: VerticalAlignment) => {
switch (verticalAlignment) {
case VerticalAlignment.TOP:
return 'top'
case VerticalAlignment.BOTTOM:
return 'bottom'
case VerticalAlignment.MIDDLE:
default:
return 'middle'
}
}
return (
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
<tbody>
<tr>
<td
align={getHorizontalAlign(buttons[0]?.alignment)}
valign={getVerticalAlign(buttons[0]?.verticalAlignment)}
style={{ width: '100%' }}
>
<table cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
<tbody>
<tr>
{buttons.slice(0, 2).map((button, buttonIndex) => (
<td key={button.id} style={{ paddingRight: buttonIndex === 0 && buttons.length > 1 ? '4px' : '0' }}>
<table cellPadding="0" cellSpacing="0" border={0} style={{
borderCollapse: 'collapse',
backgroundColor: button.backgroundColor,
border: button.isBorder ? `${button.borderSize}px solid ${button.borderColor}` : 'none',
borderRadius: '4px'
}}>
<tbody>
<tr>
<td
align="center"
valign="middle"
style={{
padding: button.size === ButtonSize.SMALL ? '2px 4px' : button.size === ButtonSize.MEDIUM ? '4px 8px' : '8px 12px',
fontSize: button.size === ButtonSize.SMALL ? '11px' : button.size === ButtonSize.MEDIUM ? '13px' : '15px',
color: button.textColor,
textDecoration: 'none',
whiteSpace: 'nowrap',
maxWidth: 'fit-content',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<a href={button.link || '#'} style={{
color: button.textColor,
textDecoration: 'none',
}}>
{button.text}
</a>
</td>
</tr>
</tbody>
</table>
</td>
))}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
)
}
export default ButtonRenderer
@@ -1,200 +0,0 @@
import Button from '@/components/Button'
import ColorPicker from '@/components/ColorPicker'
import Input from '@/components/Input'
import Select from '@/components/Select'
import { Checkbox } from '@/components/ui/checkbox'
import { AlignRight, AlignBottom, AlignHorizontally, AlignLeft, AlignTop, AlignVertically, Link2, Add } from 'iconsax-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePersonalityStore } from '../store/Store'
import { ButtonSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
const ButtonSidebar: FC = () => {
const { t } = useTranslation()
const { addButtonToActiveItem } = usePersonalityStore()
const [buttonText, setButtonText] = useState<string>('')
const [buttonLink, setButtonLink] = useState<string>('')
const [buttonSize, setButtonSize] = useState<ButtonSize>(ButtonSize.MEDIUM)
const [isBorder, setIsBorder] = useState<boolean>(false)
const [borderSize, setBorderSize] = useState<number>(1)
const [textColor, setTextColor] = useState<string>('#fff')
const [backgroundColor, setBackgroundColor] = useState<string>('#000')
const [borderColor, setBorderColor] = useState<string>('#fff')
const [horizontalAlignment, setHorizontalAlignment] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER)
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
const sizeOptions = [
{ label: 'کوچک', value: ButtonSize.SMALL },
{ label: 'متوسط', value: ButtonSize.MEDIUM },
{ label: 'بزرگ', value: ButtonSize.LARGE }
]
const handleAddButton = () => {
addButtonToActiveItem({
text: buttonText,
link: buttonLink,
size: buttonSize,
isBorder,
borderSize,
textColor,
backgroundColor,
borderColor,
alignment: horizontalAlignment,
verticalAlignment,
})
// Reset form after adding
setButtonText('')
setButtonLink('')
setButtonSize(ButtonSize.MEDIUM)
setIsBorder(false)
setBorderSize(1)
setTextColor('#fff')
setBackgroundColor('#000')
setBorderColor('#fff')
setHorizontalAlignment(HorizontalAlignment.CENTER)
setVerticalAlignment(VerticalAlignment.MIDDLE)
}
return (
<div>
<div className='text-lg'>{t('setting.button')}</div>
<div className='mt-8'>
<Input
label={t('setting.button_text')}
value={buttonText}
onChange={(e) => setButtonText(e.target.value)}
/>
</div>
<div className='mt-5'>
<Input
label={t('setting.button_link')}
value={buttonLink}
onChange={(e) => setButtonLink(e.target.value)}
endIcon={<Link2 size={18} color='#888' />}
/>
</div>
<div className='mt-5'>
<Select
label={t('setting.size')}
items={sizeOptions}
value={buttonSize}
onChange={(e) => setButtonSize(e.target.value as ButtonSize)}
placeholder={t('setting.select')}
/>
</div>
<div className='mt-5 flex gap-2 items-center text-sm'>
<Checkbox
checked={isBorder}
onCheckedChange={() => setIsBorder(!isBorder)}
/>
<div>{t('setting.border')}</div>
</div>
{
isBorder &&
<div className='mt-5'>
<Input
type='number'
label={t('setting.border')}
value={borderSize.toString()}
onChange={(e) => setBorderSize(Number(e.target.value))}
/>
</div>
}
<div className='mt-5'>
<ColorPicker
label={t('setting.text_color')}
defaultColor={textColor}
changeColor={setTextColor}
/>
</div>
<div className='mt-5'>
<ColorPicker
label={t('setting.background_color')}
defaultColor={backgroundColor}
changeColor={setBackgroundColor}
/>
</div>
<div className='mt-5'>
<ColorPicker
label={t('setting.border_color')}
defaultColor={borderColor}
changeColor={setBorderColor}
/>
</div>
<div className='mt-5 flex justify-between'>
<div>
<div className='text-sm'>
{t('setting.horizontal_position')}
</div>
<div className='mt-3 flex gap-4'>
<div
onClick={() => setHorizontalAlignment(HorizontalAlignment.RIGHT)}
>
<AlignRight size={20} color={horizontalAlignment === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setHorizontalAlignment(HorizontalAlignment.CENTER)}
>
<AlignVertically size={20} color={horizontalAlignment === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setHorizontalAlignment(HorizontalAlignment.LEFT)}
>
<AlignLeft size={20} color={horizontalAlignment === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
</div>
</div>
</div>
<div>
<div className='text-sm'>
{t('setting.vertical_position')}
</div>
<div className='mt-3 flex gap-4'>
<div
onClick={() => setVerticalAlignment(VerticalAlignment.TOP)}
>
<AlignTop size={20} color={verticalAlignment === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setVerticalAlignment(VerticalAlignment.MIDDLE)}
>
<AlignHorizontally size={20} color={verticalAlignment === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setVerticalAlignment(VerticalAlignment.BOTTOM)}
>
<AlignBottom size={20} color={verticalAlignment === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
</div>
</div>
</div>
</div>
<div className='mt-7'>
<Button
onClick={handleAddButton}
className='bg-[#ECEEF5] text-black'
>
<div className='flex justify-center items-center gap-2'>
<Add size={24} color='black' />
<span>{t('setting.add_button')}</span>
</div>
</Button>
</div>
</div>
)
}
export default ButtonSidebar
@@ -1,198 +0,0 @@
import { FC } from 'react'
import { usePersonalityStore } from '../store/Store'
import { useTranslation } from 'react-i18next'
import { AddCircle } from 'iconsax-react'
import TextRenderer from './TextRenderer'
import ButtonRenderer from './ButtonRenderer'
import ImageRenderer from './ImageRenderer'
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
const ContentSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
const handleSectionClick = (section: SectionName) => {
setActiveSection(section)
}
const handleItemClick = (index: number) => {
setActiveItemIndex(index)
}
// تابع‌های helper برای تعیین alignment دکمه‌ها
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
if (!button || !button.alignment) return 'center'
switch (button.alignment) {
case HorizontalAlignment.LEFT:
return 'left'
case HorizontalAlignment.RIGHT:
return 'right'
case HorizontalAlignment.CENTER:
default:
return 'center'
}
}
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
if (!button || !button.verticalAlignment) return 'middle'
switch (button.verticalAlignment) {
case VerticalAlignment.TOP:
return 'top'
case VerticalAlignment.BOTTOM:
return 'bottom'
case VerticalAlignment.MIDDLE:
default:
return 'middle'
}
}
return (
<tr onClick={() => handleSectionClick(SectionName.CONTENT)}>
<td style={{ paddingTop: '16px' }}>
{
data.content.columnsCount > 0 ?
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
padding: '10px',
border: activeSection === SectionName.CONTENT ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px'
}}>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
<tbody>
<tr>
{
Array.from({ length: data.content.columnsCount }, (_, index) => {
const item = data.content.items[index];
const isActive = activeSection === SectionName.CONTENT && activeItemIndex === index;
return (
<>
<td
onClick={(e) => {
e.stopPropagation();
handleItemClick(index);
}}
key={index}
style={{
width: `${100 / data.content.columnsCount}%`,
height: '246px',
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
backgroundColor: item?.backgroundColor || '#ffffff',
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
backgroundSize: item?.style?.backgroundSize || 'cover',
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
backgroundPosition: item?.style?.backgroundPosition || 'center',
cursor: 'pointer',
padding: '0',
verticalAlign: 'top',
borderRadius: '8px',
position: 'relative'
}}
>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
borderCollapse: 'collapse',
height: '246px'
}}>
<tbody>
{/* اگر فقط دکمه داریم و متن نداریم */}
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
<tr>
<td
width="100%"
height="246"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{
padding: '8px'
}}
>
<ButtonRenderer buttons={item.buttons} />
</td>
</tr>
) : (
<>
{/* ردیف اصلی برای متن */}
<tr>
<td
width="100%"
height={item?.buttons && item.buttons.length > 0 ? "203" : "230"}
align={item?.texts?.[0]?.alignment || 'center'}
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
style={{
padding: '8px',
fontSize: '14px',
lineHeight: '1.4'
}}
>
<TextRenderer texts={item?.texts || []} />
<ImageRenderer images={item?.images || []} />
</td>
</tr>
{/* ردیف دکمه‌ها */}
{item?.buttons && item.buttons.length > 0 && (
<tr>
<td
width="100%"
height="27"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{
padding: '0 8px 8px 8px'
}}
>
<ButtonRenderer buttons={item.buttons} />
</td>
</tr>
)}
</>
)}
</tbody>
</table>
</td>
{index < data.content.columnsCount - 1 && (
<td style={{ width: '16px' }}></td>
)}
</>
)
})
}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
:
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
height: '286px',
border: activeSection === SectionName.CONTENT ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px',
textAlign: 'center',
verticalAlign: 'middle',
padding: '10px'
}}>
<div style={{ color: '#888', fontSize: '14px', marginBottom: '8px' }}>
{t('setting.content_email')}
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<AddCircle size={24} color='#888' />
</div>
</td>
</tr>
</tbody>
</table>
}
</td>
</tr>
)
}
export default ContentSection
@@ -1,198 +0,0 @@
import { FC } from 'react'
import { usePersonalityStore } from '../store/Store'
import { useTranslation } from 'react-i18next'
import { AddCircle } from 'iconsax-react'
import TextRenderer from './TextRenderer'
import ButtonRenderer from './ButtonRenderer'
import ImageRenderer from './ImageRenderer'
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
const FooterSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
const handleSectionClick = (section: SectionName) => {
setActiveSection(section)
}
const handleItemClick = (index: number) => {
setActiveItemIndex(index)
}
// تابع‌های helper برای تعیین alignment دکمه‌ها
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
if (!button || !button.alignment) return 'center'
switch (button.alignment) {
case HorizontalAlignment.LEFT:
return 'left'
case HorizontalAlignment.RIGHT:
return 'right'
case HorizontalAlignment.CENTER:
default:
return 'center'
}
}
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
if (!button || !button.verticalAlignment) return 'middle'
switch (button.verticalAlignment) {
case VerticalAlignment.TOP:
return 'top'
case VerticalAlignment.BOTTOM:
return 'bottom'
case VerticalAlignment.MIDDLE:
default:
return 'middle'
}
}
return (
<tr onClick={() => handleSectionClick(SectionName.FOOTER)}>
<td style={{ paddingTop: '16px' }}>
{
data.footer.columnsCount > 0 ?
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
padding: '10px',
border: activeSection === SectionName.FOOTER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px'
}}>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
<tbody>
<tr>
{
Array.from({ length: data.footer.columnsCount }, (_, index) => {
const item = data.footer.items[index];
const isActive = activeSection === SectionName.FOOTER && activeItemIndex === index;
return (
<>
<td
onClick={(e) => {
e.stopPropagation();
handleItemClick(index);
}}
key={index}
style={{
width: `${100 / data.footer.columnsCount}%`,
height: '123px',
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
backgroundColor: item?.backgroundColor || '#ffffff',
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
backgroundSize: item?.style?.backgroundSize || 'cover',
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
backgroundPosition: item?.style?.backgroundPosition || 'center',
cursor: 'pointer',
padding: '0',
verticalAlign: 'top',
borderRadius: '8px',
position: 'relative'
}}
>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
borderCollapse: 'collapse',
height: '123px'
}}>
<tbody>
{/* اگر فقط دکمه داریم و متن نداریم */}
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
<tr>
<td
width="100%"
height="123"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{
padding: '8px'
}}
>
<ButtonRenderer buttons={item.buttons} />
</td>
</tr>
) : (
<>
{/* ردیف اصلی برای متن */}
<tr>
<td
width="100%"
height={item?.buttons && item.buttons.length > 0 ? "40" : "67"}
align={item?.texts?.[0]?.alignment || 'center'}
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
style={{
padding: '8px',
fontSize: '14px',
lineHeight: '1.4'
}}
>
<TextRenderer texts={item?.texts || []} />
<ImageRenderer images={item?.images || []} />
</td>
</tr>
{/* ردیف دکمه‌ها */}
{item?.buttons && item.buttons.length > 0 && (
<tr>
<td
width="100%"
height="27"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{
padding: '0 8px 8px 8px'
}}
>
<ButtonRenderer buttons={item.buttons} />
</td>
</tr>
)}
</>
)}
</tbody>
</table>
</td>
{index < data.footer.columnsCount - 1 && (
<td style={{ width: '16px' }}></td>
)}
</>
)
})
}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
:
<table width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td style={{
height: '123px',
border: activeSection === SectionName.FOOTER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px',
textAlign: 'center',
verticalAlign: 'middle',
padding: '10px'
}}>
<div style={{ color: '#888', fontSize: '14px', marginBottom: '8px' }}>
{t('setting.footer_email')}
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<AddCircle size={24} color='#888' />
</div>
</td>
</tr>
</tbody>
</table>
}
</td>
</tr>
)
}
export default FooterSection
@@ -1,184 +0,0 @@
import { FC } from 'react'
import { usePersonalityStore } from '../store/Store'
import { useTranslation } from 'react-i18next'
import { AddCircle } from 'iconsax-react'
import TextRenderer from './TextRenderer'
import ButtonRenderer from './ButtonRenderer'
import ImageRenderer from './ImageRenderer'
import { SectionName, VerticalAlignment, HorizontalAlignment } from '../types/Types'
const HeaderSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
const handleSectionClick = (section: SectionName) => {
setActiveSection(section)
}
const handleItemClick = (index: number) => {
setActiveItemIndex(index)
}
// تابع‌های helper برای تعیین alignment دکمه‌ها
const getButtonHorizontalAlign = (button?: { alignment?: HorizontalAlignment }) => {
if (!button || !button.alignment) return 'center'
switch (button.alignment) {
case HorizontalAlignment.LEFT:
return 'left'
case HorizontalAlignment.RIGHT:
return 'right'
case HorizontalAlignment.CENTER:
default:
return 'center'
}
}
const getButtonVerticalAlign = (button?: { verticalAlignment?: VerticalAlignment }) => {
if (!button || !button.verticalAlignment) return 'middle'
switch (button.verticalAlignment) {
case VerticalAlignment.TOP:
return 'top'
case VerticalAlignment.BOTTOM:
return 'bottom'
case VerticalAlignment.MIDDLE:
default:
return 'middle'
}
}
return (
<tr onClick={() => handleSectionClick(SectionName.HEADER)}>
{
data.header.columnsCount > 0 ?
<td style={{
padding: '10px',
border: activeSection === SectionName.HEADER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px'
}}>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ tableLayout: 'fixed' }}>
<tbody>
<tr>
{
Array.from({ length: data.header.columnsCount }, (_, index) => {
const item = data.header.items[index];
const isActive = activeSection === SectionName.HEADER && activeItemIndex === index;
return (
<>
<td
onClick={(e) => {
e.stopPropagation();
handleItemClick(index);
}}
key={index}
style={{
width: `${100 / data.header.columnsCount}%`,
height: '123px',
border: isActive ? '1px dashed #0038FF' : '1px dashed #EAECF4',
backgroundColor: item?.backgroundColor || '#ffffff',
backgroundImage: item?.backgroundImage ? `url(${item.backgroundImage})` : 'none',
backgroundSize: item?.style?.backgroundSize || 'cover',
backgroundRepeat: item?.style?.backgroundRepeat || 'no-repeat',
backgroundPosition: item?.style?.backgroundPosition || 'center',
cursor: 'pointer',
padding: '0',
verticalAlign: 'top',
borderRadius: '8px',
position: 'relative'
}}
>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{
borderCollapse: 'collapse',
height: '123px'
}}>
<tbody>
{/* اگر فقط دکمه داریم و متن نداریم */}
{(!item?.texts || item.texts.length === 0) && item?.buttons && item.buttons.length > 0 ? (
<tr>
<td
width="100%"
height="123"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{
padding: '8px'
}}
>
<ButtonRenderer buttons={item.buttons} />
</td>
</tr>
) : (
<>
{/* ردیف اصلی برای متن */}
<tr>
<td
width="100%"
height={item?.buttons && item.buttons.length > 0 ? "60" : "87"}
align={item?.texts?.[0]?.alignment || 'center'}
valign={item?.texts?.[0]?.verticalAlignment === VerticalAlignment.TOP ? 'top' :
item?.texts?.[0]?.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'middle'}
style={{
padding: '8px',
fontSize: '14px',
lineHeight: '1.4'
}}
>
<TextRenderer texts={item?.texts || []} />
<ImageRenderer images={item?.images || []} />
</td>
</tr>
{/* ردیف دکمه‌ها */}
{item?.buttons && item.buttons.length > 0 && (
<tr>
<td
width="100%"
height="27"
align={getButtonHorizontalAlign(item.buttons[0])}
valign={getButtonVerticalAlign(item.buttons[0])}
style={{
padding: '0 8px 8px 8px'
}}
>
<ButtonRenderer buttons={item.buttons} />
</td>
</tr>
)}
</>
)}
</tbody>
</table>
</td>
{index < data.header.columnsCount - 1 && (
<td style={{ width: '16px' }}></td>
)}
</>
)
})
}
</tr>
</tbody>
</table>
</td>
:
<td style={{
height: '123px',
border: activeSection === SectionName.HEADER ? '1px dashed #0038FF' : '1px dashed #E5E7EB',
borderRadius: '24px',
textAlign: 'center',
verticalAlign: 'middle',
padding: '10px'
}}>
<div style={{ color: '#888', fontSize: '14px', marginBottom: '8px' }}>
{t('setting.header_email')}
</div>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<AddCircle size={24} color='#888' />
</div>
</td>
}
</tr>
)
}
export default HeaderSection
@@ -1,149 +0,0 @@
import { FC } from 'react'
import { ImageType, ImageSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
interface ImageRendererProps {
images: ImageType[]
}
const ImageRenderer: FC<ImageRendererProps> = ({ images }) => {
if (!images || images.length === 0) return null
// تابع‌های helper برای تبدیل enum به string
const getHorizontalAlign = (alignment?: HorizontalAlignment) => {
switch (alignment) {
case HorizontalAlignment.LEFT:
return 'left'
case HorizontalAlignment.RIGHT:
return 'right'
case HorizontalAlignment.CENTER:
default:
return 'center'
}
}
const getVerticalAlign = (verticalAlignment?: VerticalAlignment) => {
switch (verticalAlignment) {
case VerticalAlignment.TOP:
return 'top'
case VerticalAlignment.BOTTOM:
return 'bottom'
case VerticalAlignment.MIDDLE:
default:
return 'middle'
}
}
const getImageStyle = (image: ImageType) => {
return {
width: image.width || '100%',
height: image.height || 'auto',
border: '0'
}
}
const shouldUseBackgroundImage = (imageSize: ImageSize) => {
return imageSize === ImageSize.REPEAT ||
imageSize === ImageSize.REPEAT_X ||
imageSize === ImageSize.REPEAT_Y
}
const getBackgroundStyle = (image: ImageType) => {
let backgroundRepeat = 'no-repeat'
switch (image.size) {
case ImageSize.REPEAT:
backgroundRepeat = 'repeat'
break
case ImageSize.REPEAT_X:
backgroundRepeat = 'repeat-x'
break
case ImageSize.REPEAT_Y:
backgroundRepeat = 'repeat-y'
break
default:
backgroundRepeat = 'no-repeat'
break
}
const horizontal = image.alignment === HorizontalAlignment.LEFT ? 'left' :
image.alignment === HorizontalAlignment.RIGHT ? 'right' : 'center'
const vertical = image.verticalAlignment === VerticalAlignment.TOP ? 'top' :
image.verticalAlignment === VerticalAlignment.BOTTOM ? 'bottom' : 'center'
return {
backgroundImage: `url(${image.src})`,
backgroundPosition: `${horizontal} ${vertical}`,
backgroundRepeat,
width: image.width || '100%',
height: image.height || '100px'
}
}
const renderImage = (image: ImageType) => {
if (shouldUseBackgroundImage(image.size)) {
// برای pattern های repeat از background استفاده می‌کنیم
return (
<div style={getBackgroundStyle(image)}>
&nbsp;
</div>
)
}
// برای تصاویر عادی
if (image.size === ImageSize.CONTAIN) {
// برای contain از table wrapper استفاده می‌کنیم تا نسبت حفظ شود
return (
<table width={image.width || '100%'} cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td align="center" valign="middle" style={{ textAlign: 'center' }}>
<img
src={image.src}
alt={image.alt || 'تصویر آپلود شده'}
width={image.width || '100%'}
height={image.height || 'auto'}
style={{ border: '0' }}
/>
</td>
</tr>
</tbody>
</table>
)
}
// برای cover و auto
return (
<img
src={image.src}
alt={image.alt || 'تصویر آپلود شده'}
width={image.width || '100%'}
height={image.height || 'auto'}
style={getImageStyle(image)}
/>
)
}
return (
<>
{images.map((image, imageIndex) => (
<table key={image.id} width="100%" cellPadding="0" cellSpacing="0" border={0}>
<tbody>
<tr>
<td
align={getHorizontalAlign(image.alignment)}
valign={getVerticalAlign(image.verticalAlignment)}
style={{
paddingBottom: imageIndex < images.length - 1 ? '4px' : '0'
}}
>
{renderImage(image)}
</td>
</tr>
</tbody>
</table>
))}
</>
)
}
export default ImageRenderer
@@ -1,194 +0,0 @@
import Button from '@/components/Button'
import Select from '@/components/Select'
import UploadBoxDraggble from '@/components/UploadBoxDraggble'
import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePersonalityStore } from '../store/Store'
import { ImageSize, HorizontalAlignment, VerticalAlignment } from '../types/Types'
const ImageSideBar: FC = () => {
const { t } = useTranslation()
const { addImageToActiveItem, activeSection, activeItemIndex, data } = usePersonalityStore()
const [imageSrc, setImageSrc] = useState<string>('')
const [imageSize, setImageSize] = useState<ImageSize>(ImageSize.COVER)
const [horizontalAlignment, setHorizontalAlignment] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER)
const [verticalAlignment, setVerticalAlignment] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE)
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isReset, setIsReset] = useState<boolean>(false)
const sizeOptions = [
{ label: 'کاور - تمام فضا را پوشش دهد (Cover)', value: ImageSize.COVER },
{ label: 'در مقیاس - در فضا جا شود (Contain)', value: ImageSize.CONTAIN },
{ label: 'تکرار - عکس تکرار شود (Repeat)', value: ImageSize.REPEAT },
{ label: 'تکرار افقی (Repeat-X)', value: ImageSize.REPEAT_X },
{ label: 'تکرار عمودی (Repeat-Y)', value: ImageSize.REPEAT_Y },
{ label: 'بدون تکرار (No-Repeat)', value: ImageSize.NO_REPEAT },
{ label: 'اندازه اصلی (Auto)', value: ImageSize.AUTO }
]
const handleImageUpload = (files: File[]) => {
if (files.length === 0) return
const file = files[0] // فقط اولین فایل را در نظر می‌گیریم
// در اینجا باید فایل آپلود شود و URL برگردانده شود
// فعلاً از FileReader استفاده می‌کنم برای تست
const reader = new FileReader()
reader.onload = (e) => {
const result = e.target?.result as string
setImageSrc(result)
}
reader.readAsDataURL(file)
}
const handleAddImage = () => {
if (!imageSrc) return
console.log('Store state before adding image:', {
activeSection,
activeItemIndex,
hasActiveItem: activeSection && data[activeSection as keyof typeof data]?.items[activeItemIndex],
})
// Validation
if (!activeSection) {
console.error('No active section selected')
return
}
const sectionData = data[activeSection as keyof typeof data]
if (!sectionData?.items || activeItemIndex >= sectionData.items.length) {
console.error('No active item found or invalid index', {
activeItemIndex,
itemsLength: sectionData?.items?.length
})
return
}
console.log('Adding image with data:', {
src: imageSrc,
alt: 'عکس آپلود شده',
size: imageSize,
alignment: horizontalAlignment,
verticalAlignment,
})
setIsLoading(true)
try {
addImageToActiveItem({
src: imageSrc,
alt: 'عکس آپلود شده',
size: imageSize,
alignment: horizontalAlignment,
verticalAlignment,
})
console.log('Image added successfully')
// Reset form after adding
setImageSrc('')
setImageSize(ImageSize.COVER)
setHorizontalAlignment(HorizontalAlignment.CENTER)
setVerticalAlignment(VerticalAlignment.MIDDLE)
setIsReset(true)
setTimeout(() => {
setIsReset(false)
}, 1000)
} catch (error) {
console.error('Error adding image:', error)
} finally {
setIsLoading(false)
}
}
return (
<div>
<div className='text-lg'>{t('setting.image')}</div>
<div className='mt-8'>
<UploadBoxDraggble
label={t('setting.upload_image')}
onChange={handleImageUpload}
isReset={isReset}
/>
</div>
<div className='mt-5'>
<Select
items={sizeOptions}
label={t('setting.size')}
value={imageSize}
onChange={(e) => setImageSize(e.target.value as ImageSize)}
placeholder={t('setting.size')}
/>
</div>
<div className='mt-5 flex justify-between'>
<div>
<div className='text-sm'>
{t('setting.horizontal_position')}
</div>
<div className='mt-3 flex gap-4'>
<div
onClick={() => setHorizontalAlignment(HorizontalAlignment.RIGHT)}
>
<AlignRight size={20} color={horizontalAlignment === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setHorizontalAlignment(HorizontalAlignment.CENTER)}
>
<AlignVertically size={20} color={horizontalAlignment === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setHorizontalAlignment(HorizontalAlignment.LEFT)}
>
<AlignLeft size={20} color={horizontalAlignment === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
</div>
</div>
</div>
<div>
<div className='text-sm'>
{t('setting.vertical_position')}
</div>
<div className='mt-3 flex gap-4'>
<div
onClick={() => setVerticalAlignment(VerticalAlignment.BOTTOM)}
>
<AlignBottom size={20} color={verticalAlignment === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setVerticalAlignment(VerticalAlignment.MIDDLE)}
>
<AlignHorizontally size={20} color={verticalAlignment === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setVerticalAlignment(VerticalAlignment.TOP)}
>
<AlignTop size={20} color={verticalAlignment === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
</div>
</div>
</div>
</div>
<div className='mt-7'>
<Button
onClick={handleAddImage}
className='bg-[#ECEEF5] text-black'
disabled={!imageSrc}
loading={isLoading}
>
<div className='flex justify-center items-center gap-2'>
<Add size={24} color='black' />
<span>{t('setting.add_image')}</span>
</div>
</Button>
</div>
</div>
)
}
export default ImageSideBar
@@ -1,187 +0,0 @@
import { FC, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import UploadBoxDraggble from '@/components/UploadBoxDraggble';
import ColorPicker from '@/components/ColorPicker';
import { usePersonalityStore } from '../store/Store';
import { SectionItemType, BackgroundSize, SectionName } from '../types/Types';
import Select from '@/components/Select';
const SettingSideBar: FC = () => {
const { t } = useTranslation()
const { activeSection, setColumnsCount, updateActiveItem, data, activeItemIndex } = usePersonalityStore()
const [color, setColor] = useState("#aabbcc");
const [backgroundSize, setBackgroundSize] = useState<BackgroundSize>(BackgroundSize.COVER)
// تشخیص background size بر اساس style موجود
const getCurrentBackgroundSizeType = (style?: SectionItemType['style']) => {
if (!style) return BackgroundSize.COVER
const bgSize = style.backgroundSize
const bgRepeat = style.backgroundRepeat
if (bgSize === 'cover') return BackgroundSize.COVER
if (bgSize === 'contain') return BackgroundSize.CONTAIN
if (bgSize === 'auto' || !bgSize) {
if (bgRepeat === 'round') return BackgroundSize.ROUND
}
return BackgroundSize.COVER
}
// به‌روزرسانی state وقتی activeSection یا activeItem تغییر می‌کند
useEffect(() => {
if (activeSection && data[activeSection as keyof typeof data]) {
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
if (currentItem?.style) {
const currentType = getCurrentBackgroundSizeType(currentItem.style)
setBackgroundSize(currentType)
}
}
}, [activeSection, activeItemIndex, data])
const handleColumnClick = (columns: number) => {
if (activeSection) {
setColumnsCount(activeSection as SectionName, columns);
}
}
const handleColorChange = (color: string) => {
setColor(color)
updateActiveItem({ backgroundColor: color })
}
const handleUploadBackground = (file: File[]) => {
if (activeSection && file.length > 0) {
const imageUrl = URL.createObjectURL(file[0])
console.log('Setting background image:', imageUrl)
const currentItem = data[activeSection as keyof typeof data]?.items[activeItemIndex]
updateActiveItem({
backgroundImage: imageUrl,
style: {
...currentItem?.style,
backgroundSize: getBackgroundSize(backgroundSize),
backgroundRepeat: getBackgroundRepeat(backgroundSize),
backgroundPosition: 'center center'
}
})
}
}
const getBackgroundRepeat = (size: BackgroundSize) => {
switch (size) {
case BackgroundSize.ROUND:
return 'round'
case BackgroundSize.COVER:
case BackgroundSize.CONTAIN:
default:
return 'no-repeat'
}
}
const getBackgroundSize = (size: BackgroundSize) => {
switch (size) {
case BackgroundSize.COVER:
return 'cover'
case BackgroundSize.CONTAIN:
return 'contain'
case BackgroundSize.ROUND:
return 'auto'
default:
return 'cover'
}
}
const sizeOptions = [
{ label: 'کاور - تمام فضا را پوشش دهد (Cover)', value: BackgroundSize.COVER },
{ label: 'در مقیاس - در فضا جا شود (Contain)', value: BackgroundSize.CONTAIN },
{ label: 'گرد - عکس به صورت گرد تکرار شود (Round)', value: BackgroundSize.ROUND }
]
if (!activeSection) {
return (
<div className='text-center text-gray-500 mt-8'>
{t('setting.select_section')}
</div>
)
}
return (
<div>
<div className='text-lg'>{t('setting.setting')}</div>
<div className='flex h-[51px] gap-5 mt-6'>
<div
className='w-[102px] h-full bg-[#EAECF4] cursor-pointer hover:bg-[#d1d5db] transition-colors'
onClick={() => handleColumnClick(1)}
></div>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(2)}
>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
</div>
</div>
<div className='flex h-[51px] gap-5 mt-5'>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(3)}
>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
</div>
<div
className='w-[102px] h-full flex gap-1 cursor-pointer group'
onClick={() => handleColumnClick(4)}
>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
<div className='flex-1 bg-[#EAECF4] group-hover:bg-[#d1d5db] transition-colors'></div>
</div>
</div>
<div className='mt-5'>
<ColorPicker
label={t('setting.background')}
defaultColor={color}
changeColor={handleColorChange}
/>
</div>
<div className='mt-5'>
<UploadBoxDraggble
label={t('setting.upload_background')}
onChange={handleUploadBackground}
/>
</div>
<div className='mt-5'>
<Select
items={sizeOptions}
label={t('setting.size')}
value={backgroundSize}
onChange={(e) => {
const newSize = e.target.value as BackgroundSize
setBackgroundSize(newSize)
const currentItem = activeSection ? data[activeSection as keyof typeof data]?.items[activeItemIndex] : null
updateActiveItem({
style: {
...currentItem?.style,
backgroundSize: getBackgroundSize(newSize),
backgroundRepeat: getBackgroundRepeat(newSize),
backgroundPosition: 'center center'
}
})
}}
placeholder={t('setting.size')}
/>
</div>
</div>
)
}
export default SettingSideBar
@@ -1,33 +0,0 @@
import { FC } from 'react'
import { TextType } from '../types/Types'
interface TextRendererProps {
texts: TextType[]
}
const TextRenderer: FC<TextRendererProps> = ({ texts }) => {
return (
<>
{texts?.map((textItem, textIndex) => (
<table key={textItem.id} width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
<tbody>
<tr>
<td
align={textItem.alignment || 'center'}
style={{
color: textItem.color || '#000000',
fontSize: `${textItem.fontSize || 14}px`,
lineHeight: '1.4',
paddingBottom: textIndex < (texts?.length || 0) - 1 ? '4px' : '0'
}}
dangerouslySetInnerHTML={{ __html: textItem.text }}
/>
</tr>
</tbody>
</table>
))}
</>
)
}
export default TextRenderer
@@ -1,136 +0,0 @@
import Button from '@/components/Button';
import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react';
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ReactQuill from 'react-quill-new';
import { usePersonalityStore } from '../store/Store';
import ColorPicker from '@/components/ColorPicker';
import { HorizontalAlignment, VerticalAlignment } from '../types/Types';
const TextSidebar: FC = () => {
const { t } = useTranslation()
const [value, setValue] = useState('');
const [color, setColor] = useState('#000');
const [horizontalPosition, setHorizontalPosition] = useState<HorizontalAlignment>(HorizontalAlignment.CENTER);
const [verticalPosition, setVerticalPosition] = useState<VerticalAlignment>(VerticalAlignment.MIDDLE);
const { addTextToActiveItem } = usePersonalityStore();
const handleChange = (value: string) => {
setValue(value);
}
const handleAddText = () => {
if (value.trim()) {
addTextToActiveItem({
text: value,
color: color,
verticalAlignment: verticalPosition,
alignment: horizontalPosition,
});
// خالی کردن فیلدها بعد از ذخیره
setValue('');
setColor('#000');
setHorizontalPosition(HorizontalAlignment.CENTER);
setVerticalPosition(VerticalAlignment.MIDDLE);
}
}
return (
<div>
<div className='text-lg'>{t('setting.text')}</div>
<div className='mt-8'>
<ReactQuill
modules={{
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ align: [] }],
['clean']
]
}}
theme="snow"
value={value}
onChange={handleChange}
style={{ minHeight: '120px' }}
className='text-sm'
/>
</div>
<div className='mt-5'>
<ColorPicker
changeColor={setColor}
label={t('setting.text_color')}
defaultColor={color}
/>
</div>
<div className='mt-5 flex justify-between'>
<div>
<div className='text-sm'>
{t('setting.horizontal_position')}
</div>
<div className='mt-3 flex gap-4'>
<div
onClick={() => setHorizontalPosition(HorizontalAlignment.RIGHT)}
>
<AlignRight size={20} color={horizontalPosition === HorizontalAlignment.RIGHT ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setHorizontalPosition(HorizontalAlignment.CENTER)}
>
<AlignVertically size={20} color={horizontalPosition === HorizontalAlignment.CENTER ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setHorizontalPosition(HorizontalAlignment.LEFT)}
>
<AlignLeft size={20} color={horizontalPosition === HorizontalAlignment.LEFT ? '#0038FF' : '#000'} />
</div>
</div>
</div>
<div>
<div className='text-sm'>
{t('setting.vertical_position')}
</div>
<div className='mt-3 flex gap-4'>
<div
onClick={() => setVerticalPosition(VerticalAlignment.TOP)}
>
<AlignTop size={20} color={verticalPosition === VerticalAlignment.TOP ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setVerticalPosition(VerticalAlignment.MIDDLE)}
>
<AlignHorizontally size={20} color={verticalPosition === VerticalAlignment.MIDDLE ? '#0038FF' : '#000'} />
</div>
<div
onClick={() => setVerticalPosition(VerticalAlignment.BOTTOM)}
>
<AlignBottom size={20} color={verticalPosition === VerticalAlignment.BOTTOM ? '#0038FF' : '#000'} />
</div>
</div>
</div>
</div>
<div className='mt-7'>
<Button
onClick={handleAddText}
className='bg-[#ECEEF5] text-black'
>
<div className='flex justify-center items-center gap-2'>
<Add size={24} color='black' />
<span>{t('setting.add_text')}</span>
</div>
</Button>
</div>
</div>
)
}
export default TextSidebar
@@ -1,376 +0,0 @@
import { create } from "zustand";
import { v4 as uuidv4 } from "uuid";
import {
PersonalityStore,
PersonalityDataType,
SectionItemType,
TextType,
ButtonType,
ImageType,
SectionName,
HorizontalAlignment,
VerticalAlignment,
} from "../types/Types";
export const usePersonalityStore = create<PersonalityStore>((set) => ({
data: {
header: { columnsCount: 0, items: [] },
content: { columnsCount: 0, items: [] },
footer: { columnsCount: 0, items: [] },
},
activeSection: "",
activeItemIndex: 0,
setActiveSection: (section: SectionName) =>
set({ activeSection: section, activeItemIndex: 0 }),
setActiveItemIndex: (index: number) => set({ activeItemIndex: index }),
setColumnsCount: (
sectionKey: keyof PersonalityDataType,
columnsCount: number
) =>
set((state) => {
const currentItems = state.data[sectionKey].items;
const newItems = Array.from(
{ length: columnsCount },
(_, index) =>
currentItems[index] || {
id: uuidv4(),
backgroundColor: "#ffffff",
texts: [],
buttons: [],
images: [],
alignment: HorizontalAlignment.CENTER,
verticalAlignment: VerticalAlignment.MIDDLE,
}
);
return {
data: {
...state.data,
[sectionKey]: {
columnsCount,
items: newItems,
},
},
activeItemIndex: 0,
};
}),
setItems: (sectionKey: keyof PersonalityDataType, items: SectionItemType[]) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items,
},
},
})),
addItem: (
sectionKey: keyof PersonalityDataType,
item: Omit<SectionItemType, "id">
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: [...state.data[sectionKey].items, { ...item, id: uuidv4() }],
},
},
})),
updateActiveItem: (updates: Partial<SectionItemType>) =>
set((state) => {
if (!state.activeSection) return state;
const sectionKey = state.activeSection as keyof PersonalityDataType;
const items = state.data[sectionKey].items;
const activeIndex = state.activeItemIndex;
if (activeIndex >= items.length) return state;
console.log("Updating active item:", {
sectionKey,
activeIndex,
updates,
});
const updatedItems = items.map((item, index) =>
index === activeIndex ? { ...item, ...updates } : item
);
return {
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: updatedItems,
},
},
};
}),
updateItem: (
sectionKey: keyof PersonalityDataType,
id: string,
updates: Partial<SectionItemType>
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === id ? { ...item, ...updates } : item
),
},
},
})),
removeItem: (sectionKey: keyof PersonalityDataType, id: string) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.filter((item) => item.id !== id),
},
},
})),
addText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
text: Omit<TextType, "id">
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
texts: [...item.texts, { ...text, id: uuidv4() }],
}
: item
),
},
},
})),
addTextToActiveItem: (text: Omit<TextType, "id">) =>
set((state) => {
if (!state.activeSection) return state;
const sectionKey = state.activeSection as keyof PersonalityDataType;
const items = state.data[sectionKey].items;
const activeIndex = state.activeItemIndex;
if (activeIndex >= items.length) return state;
const updatedItems = items.map((item, index) =>
index === activeIndex
? {
...item,
texts: [...item.texts, { ...text, id: uuidv4() }],
}
: item
);
return {
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: updatedItems,
},
},
};
}),
addButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
button: Omit<ButtonType, "id">
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
buttons: [...item.buttons, { ...button, id: uuidv4() }],
}
: item
),
},
},
})),
addButtonToActiveItem: (button: Omit<ButtonType, "id">) =>
set((state) => {
if (!state.activeSection) return state;
const sectionKey = state.activeSection as keyof PersonalityDataType;
const items = state.data[sectionKey].items;
const activeIndex = state.activeItemIndex;
if (activeIndex >= items.length) return state;
const updatedItems = items.map((item, index) =>
index === activeIndex
? {
...item,
buttons: [...item.buttons, { ...button, id: uuidv4() }],
}
: item
);
return {
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: updatedItems,
},
},
};
}),
updateText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
textId: string,
updates: Partial<TextType>
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
texts: item.texts.map((text) =>
text.id === textId ? { ...text, ...updates } : text
),
}
: item
),
},
},
})),
updateButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
buttonId: string,
updates: Partial<ButtonType>
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
buttons: item.buttons.map((button) =>
button.id === buttonId ? { ...button, ...updates } : button
),
}
: item
),
},
},
})),
addImage: (
sectionKey: keyof PersonalityDataType,
itemId: string,
image: Omit<ImageType, "id">
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
images: [...item.images, { ...image, id: uuidv4() }],
}
: item
),
},
},
})),
addImageToActiveItem: (image: Omit<ImageType, "id">) =>
set((state) => {
if (!state.activeSection) return state;
const sectionKey = state.activeSection as keyof PersonalityDataType;
const items = state.data[sectionKey].items;
const activeIndex = state.activeItemIndex;
if (activeIndex >= items.length) return state;
const updatedItems = items.map((item, index) =>
index === activeIndex
? {
...item,
images: [...item.images, { ...image, id: uuidv4() }],
}
: item
);
return {
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: updatedItems,
},
},
};
}),
updateImage: (
sectionKey: keyof PersonalityDataType,
itemId: string,
imageId: string,
updates: Partial<ImageType>
) =>
set((state) => ({
data: {
...state.data,
[sectionKey]: {
...state.data[sectionKey],
items: state.data[sectionKey].items.map((item) =>
item.id === itemId
? {
...item,
images: item.images.map((image) =>
image.id === imageId ? { ...image, ...updates } : image
),
}
: item
),
},
},
})),
}));
@@ -1,170 +0,0 @@
// Enums for common types
export enum HorizontalAlignment {
LEFT = "left",
CENTER = "center",
RIGHT = "right",
}
export enum VerticalAlignment {
TOP = "top",
MIDDLE = "middle",
BOTTOM = "bottom",
}
export enum ButtonSize {
SMALL = "small",
MEDIUM = "medium",
LARGE = "large",
}
export enum ImageSize {
COVER = "cover",
CONTAIN = "contain",
AUTO = "auto",
REPEAT = "repeat",
REPEAT_X = "repeat-x",
REPEAT_Y = "repeat-y",
NO_REPEAT = "no-repeat",
}
export enum BackgroundSize {
COVER = "cover",
CONTAIN = "contain",
ROUND = "round",
}
export enum SectionName {
HEADER = "header",
CONTENT = "content",
FOOTER = "footer",
}
export type PersonalityDataType = {
header: SectionType;
content: SectionType;
footer: SectionType;
};
export type SectionType = {
columnsCount: number;
items: SectionItemType[];
};
export type SectionItemType = {
id: string;
backgroundColor: string;
backgroundImage?: string;
texts: TextType[];
buttons: ButtonType[];
images: ImageType[];
alignment?: HorizontalAlignment;
verticalAlignment?: VerticalAlignment;
style?: {
borderRadius?: number;
padding?: string;
fontFamily?: string;
backgroundSize?: string;
backgroundRepeat?: string;
backgroundPosition?: string;
};
};
export type TextType = {
id: string;
text: string;
fontSize?: number;
fontFamily?: string;
color?: string;
alignment?: HorizontalAlignment;
verticalAlignment?: VerticalAlignment;
};
export type ButtonType = {
id: string;
text: string;
link: string;
size: ButtonSize;
isBorder: boolean;
borderSize: number;
textColor: string;
backgroundColor: string;
borderColor: string;
alignment?: HorizontalAlignment;
verticalAlignment?: VerticalAlignment;
};
export type ImageType = {
id: string;
src: string;
alt?: string;
size: ImageSize;
width?: string;
height?: string;
alignment?: HorizontalAlignment;
verticalAlignment?: VerticalAlignment;
};
export type PersonalityStore = {
data: PersonalityDataType;
activeSection: SectionName | "";
activeItemIndex: number;
setActiveSection: (section: SectionName) => void;
setActiveItemIndex: (index: number) => void;
setItems: (
sectionKey: keyof PersonalityDataType,
items: SectionItemType[]
) => void;
addItem: (
sectionKey: keyof PersonalityDataType,
item: Omit<SectionItemType, "id">
) => void;
updateItem: (
sectionKey: keyof PersonalityDataType,
id: string,
updates: Partial<SectionItemType>
) => void;
updateActiveItem: (updates: Partial<SectionItemType>) => void;
removeItem: (sectionKey: keyof PersonalityDataType, id: string) => void;
setColumnsCount: (
sectionKey: keyof PersonalityDataType,
columnsCount: number
) => void;
addText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
text: Omit<TextType, "id">
) => void;
addTextToActiveItem: (text: Omit<TextType, "id">) => void;
addButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
button: Omit<ButtonType, "id">
) => void;
addButtonToActiveItem: (button: Omit<ButtonType, "id">) => void;
updateText: (
sectionKey: keyof PersonalityDataType,
itemId: string,
textId: string,
updates: Partial<TextType>
) => void;
updateButton: (
sectionKey: keyof PersonalityDataType,
itemId: string,
buttonId: string,
updates: Partial<ButtonType>
) => void;
addImage: (
sectionKey: keyof PersonalityDataType,
itemId: string,
image: Omit<ImageType, "id">
) => void;
addImageToActiveItem: (image: Omit<ImageType, "id">) => void;
updateImage: (
sectionKey: keyof PersonalityDataType,
itemId: string,
imageId: string,
updates: Partial<ImageType>
) => void;
};
-38
View File
@@ -1,38 +0,0 @@
import Textarea from '@/components/Textarea'
import UploadBox from '@/components/UploadBox'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
const Signture: FC = () => {
const { t } = useTranslation()
return (
<div className='bg-white rounded-4xl p-8 mt-9'>
<div className='mt-9'>
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.signature')}
</div>
<div className='xl:text-sm text-xs text-description mt-1.5'>
{t('setting.signature_description')}
</div>
</div>
<div className='flex-1 mt-4 xl:mt-0'>
<UploadBox
label={t('setting.upload_sign')}
/>
<div className='xl:mt-8 mt-5'>
<Textarea
label={t('setting.description_sign')}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default Signture