Compare commits

...

4 Commits

Author SHA1 Message Date
Moaz Dawalibi
9644b4e6ba Merge branch 'dev' of into dev 2024-09-21 15:48:36 +03:00
Moaz Dawalibi
25161d4afa complete setting page 2024-09-21 14:43:27 +03:00
Moaz Dawalibi
887b3a5e75 Merge branch 'dev' of https://git.point-dev.net/Karimaldeen/Quiz_dashboard into dev 2024-09-18 13:18:00 +03:00
Moaz Dawalibi
e0e2839cf3 settings 2024-09-18 13:17:48 +03:00
45 changed files with 1196 additions and 30 deletions

View File

@ -0,0 +1,24 @@
import React from 'react';
import { Switch } from 'antd';
export interface SwitchProps {
onChange?: (checked: any, event: any) => any;
checked?: boolean;
}
const onSwitchChange = (checked: boolean) => {
console.log(`switch to ${checked}`);
};
const SwitchButton = ({onChange,checked}:SwitchProps) => {
return(
<Switch
className='switch_button'
defaultChecked
onChange={(checked: any, event: any) =>
onChange ? onChange(checked, event) : onSwitchChange(checked)
}
// checked={checked}
/>
)
}
export default SwitchButton;

View File

@ -0,0 +1,18 @@
import { Button } from 'antd'
import { useTranslation } from 'react-i18next'
import { CiEdit } from "react-icons/ci";
const EditSettingButton = ({buttonName,onClick}:{buttonName?:string,onClick?:() => void}) => {
const {t} = useTranslation();
return (
<div>
<Button className=' setting_edit_button' onClick={onClick}>
<CiEdit/>
{t(`header.edit`) ?? (`header.${buttonName}`)}
</Button>
</div>
)
}
export default EditSettingButton

View File

@ -0,0 +1,13 @@
import { Button } from 'antd'
import { useTranslation } from 'react-i18next'
const SecuritySettingButton = ({name,danger = false}:{name:string,danger?:boolean}) => {
const {t} = useTranslation();
return (
<div>
<Button className={`security_setting_button ${danger ? "security_setting_button_danger" :""}`}>{t(name)}</Button>
</div>
)
}
export default SecuritySettingButton

View File

@ -28,7 +28,6 @@ const SelectField = ({
formik.setFieldValue(name, value);
};
const options = translateOptions(option, t);
console.log(options);
return (
<div className="ValidationField w-100">

View File

@ -1,5 +1,4 @@
export const translateOptions = (options: any, t: any) => {
console.log(options);
return options?.map((opt: any) => ({
...opt,

View File

@ -0,0 +1,20 @@
import React, { useEffect, useState } from 'react'
export const useWindowResize = () => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
window.addEventListener('resize', handleResize);
// Cleanup function to remove the event listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, [windowWidth]);
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
return {windowWidth , handleResize};
}

View File

@ -81,10 +81,6 @@ const LayoutModel = ({
formik.resetForm();
}
}, [isOpen]);
console.log(formik.initialValues);
console.log(formik?.values);
return <Form className="w-100">

View File

@ -66,7 +66,7 @@ const SideBar = ({
</div>
<div className="side_bar_setting">
<p>{t("sidebar.setting")}</p>
<div>
<div onClick={() => {navigate("/setting")}}>
<CiSettings />
<span>{t("sidebar.setting")}</span>
</div>

View File

@ -32,8 +32,8 @@ const TableHeader = () => {
const handleOpenModel = () =>{
handel_open_model(ModalEnum?.RE_SELLER_COLLECTION_ADD);
}
const deleteMutation = useDeleteReseller();
return (
<div className="TableWithHeader single_student">

View File

@ -0,0 +1,42 @@
import TabHeader from './TabHeader'
import { Form, Formik } from 'formik'
import { getInitialValues, getValidationSchema } from './FileSetting/formUtils'
import PersonalDetailsForm from './FileSetting/PersonalDetailsForm'
import TitleDetailsForm from './FileSetting/TitleDetailsForm'
import AttachmentForm from './FileSetting/AttachmentForm'
import { useTranslation } from 'react-i18next'
const FileSetting = () => {
const {t} = useTranslation()
const handelSubmit = (values: any) => {
console.log(values, "values");
};
return (
<div className='file_setting'>
<TabHeader
name='file_setting'
description='upload_your_photo_and_personal_data_here'
>
<div className="file_setting_buttons">
<button type="button">{t("practical.cancel")}</button>
<button type="submit">
{t("practical.save")}
</button>
</div>
</TabHeader>
<Formik
initialValues={getInitialValues({})}
validationSchema={getValidationSchema}
onSubmit={handelSubmit}
>
<Form className="Form_details_container">
<PersonalDetailsForm />
<TitleDetailsForm />
<AttachmentForm />
</Form>
</Formik>
</div>
)
}
export default FileSetting

View File

@ -0,0 +1,22 @@
import { useTranslation } from "react-i18next";
import {FaStore } from "react-icons/fa";
import ImageBoxField from "./ImageBoxField/ImageBoxField";
const AttachmentForm = () => {
const [t] = useTranslation();
return (
<div className="AttachmentForm">
<header className="header_form">
<FaStore />
<h4>{t("header.attachments")}</h4>
</header>
<main className="main_form_body">
<ImageBoxField name="personal_image" />
<ImageBoxField name="id_image" />
</main>
</div>
);
};
export default AttachmentForm;

View File

@ -0,0 +1,38 @@
.ImageBoxField {
.ImageBox {
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
border: max(1.5px, 0.1vw) dashed #a9c3f1;
margin-block: 10px;
border-radius: 5px;
z-index: 9999999 !important;
.ImageBoxIcon {
cursor: pointer;
}
.imagePreview {
max-width: 99%;
height: auto;
max-height: 99%;
object-fit: contain;
border-radius: 5px;
}
}
.ImageHeader {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
.ImageCancelIcon {
width: 16px !important;
height: 16px !important;
}
.ImageBoxIcon {
width: 20px !important;
height: 20px !important;
}
}

View File

@ -0,0 +1,87 @@
import { useFormikContext } from "formik";
import { useState, useRef, useEffect } from "react";
import "./ImageBoxField.scss";
import ImageIcon from "./ImageIcon";
import ImageCancelIcon from "./ImageCancelIcon";
import { generateImagePreview } from "./generateImagePreview";
import { getNestedValue } from "../../../../../../utils/getNestedValue";
import { useTranslation } from "react-i18next";
// Helper function to generate image preview from a File
const ImageBoxField = ({ name }: any) => {
const formik = useFormikContext<any>();
const value = getNestedValue(formik.values, name);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (value instanceof File) {
generateImagePreview(value, setImagePreview);
} else if (typeof value === "string") {
setImagePreview(value);
} else {
setImagePreview(null);
}
}, [value]);
const handleFileChange = (event: any) => {
const file = event.target.files[0];
if (file) {
generateImagePreview(file, setImagePreview);
formik.setFieldValue(name, file);
}
};
const handleButtonClick = () => {
const fileInput = fileInputRef.current;
if (fileInput) {
fileInput.click();
}
};
const handleCancel = () => {
setImagePreview("");
formik.setFieldValue(name, "");
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const [t] = useTranslation();
return (
<div className="ImageBoxField">
<header>{t(`input.${name}`)}</header>
<div className="ImageHeader">
{imagePreview ? (
<>
<ImageCancelIcon
onClick={handleCancel}
className="ImageCancelIcon"
/>
<ImageIcon onClick={handleButtonClick} className="ImageBoxIcon" />
</>
) : (
<div className="VisibleHidden">hidden</div>
)}
</div>
<div className="ImageBox">
{imagePreview ? (
<img src={imagePreview} alt="Preview" className="imagePreview" />
) : (
<ImageIcon onClick={handleButtonClick} className="ImageBoxIcon" />
)}
</div>
<input
id={`file-input-${name}`}
type="file"
accept="image/png, image/jpeg, image/webp"
style={{ display: "none" }}
onChange={handleFileChange}
ref={fileInputRef}
/>
</div>
);
};
export default ImageBoxField;

View File

@ -0,0 +1,18 @@
import React from "react";
interface ImageCancelIconProps extends React.HTMLAttributes<HTMLDivElement> {}
const ImageCancelIcon: React.FC<ImageCancelIconProps> = (props) => {
return (
<div {...props}>
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7 5.44469L12.4447 0L14 1.55531L8.55531 7L14 12.4447L12.4436 14L6.9989 8.55531L1.55531 14L0 12.4436L5.44469 6.9989L0 1.55421L1.55531 0.00109986L7 5.44469Z"
fill="#515B73"
/>
</svg>
</div>
);
};
export default ImageCancelIcon;

View File

@ -0,0 +1,18 @@
import React from "react";
interface ImageIconProps extends React.HTMLAttributes<HTMLDivElement> {}
const ImageIcon: React.FC<ImageIconProps> = (props) => {
return (
<div {...props}>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.25 5.625C11.25 7.11684 10.6574 8.54758 9.60248 9.60248C8.54758 10.6574 7.11684 11.25 5.625 11.25C4.13316 11.25 2.70242 10.6574 1.64752 9.60248C0.592632 8.54758 0 7.11684 0 5.625C0 4.13316 0.592632 2.70242 1.64752 1.64752C2.70242 0.592632 4.13316 0 5.625 0C7.11684 0 8.54758 0.592632 9.60248 1.64752C10.6574 2.70242 11.25 4.13316 11.25 5.625ZM6.25 3.125C6.25 2.95924 6.18415 2.80027 6.06694 2.68306C5.94973 2.56585 5.79076 2.5 5.625 2.5C5.45924 2.5 5.30027 2.56585 5.18306 2.68306C5.06585 2.80027 5 2.95924 5 3.125V5H3.125C2.95924 5 2.80027 5.06585 2.68306 5.18306C2.56585 5.30027 2.5 5.45924 2.5 5.625C2.5 5.79076 2.56585 5.94973 2.68306 6.06694C2.80027 6.18415 2.95924 6.25 3.125 6.25H5V8.125C5 8.29076 5.06585 8.44973 5.18306 8.56694C5.30027 8.68415 5.45924 8.75 5.625 8.75C5.79076 8.75 5.94973 8.68415 6.06694 8.56694C6.18415 8.44973 6.25 8.29076 6.25 8.125V6.25H8.125C8.29076 6.25 8.44973 6.18415 8.56694 6.06694C8.68415 5.94973 8.75 5.79076 8.75 5.625C8.75 5.45924 8.68415 5.30027 8.56694 5.18306C8.44973 5.06585 8.29076 5 8.125 5H6.25V3.125ZM16.25 3.75H12.2413C12.1187 3.3183 11.9542 2.89964 11.75 2.5H16.25C17.2446 2.5 18.1984 2.89509 18.9017 3.59835C19.6049 4.30161 20 5.25544 20 6.25V16.25C20 17.2446 19.6049 18.1984 18.9017 18.9017C18.1984 19.6049 17.2446 20 16.25 20H6.25C5.25544 20 4.30161 19.6049 3.59835 18.9017C2.89509 18.1984 2.5 17.2446 2.5 16.25V11.75C2.89667 11.9533 3.31333 12.1171 3.75 12.2413V16.25C3.75 16.7162 3.8775 17.1525 4.1 17.525L9.93625 11.79C10.2869 11.4457 10.7586 11.2528 11.25 11.2528C11.7414 11.2528 12.2131 11.4457 12.5637 11.79L18.4012 17.525C18.6298 17.139 18.7502 16.6986 18.75 16.25V6.25C18.75 5.58696 18.4866 4.95107 18.0178 4.48223C17.5489 4.01339 16.913 3.75 16.25 3.75ZM16.25 8.125C16.25 8.37123 16.2015 8.61505 16.1073 8.84253C16.013 9.07002 15.8749 9.27672 15.7008 9.45083C15.5267 9.62494 15.32 9.76305 15.0925 9.85727C14.865 9.9515 14.6212 10 14.375 10C14.1288 10 13.885 9.9515 13.6575 9.85727C13.43 9.76305 13.2233 9.62494 13.0492 9.45083C12.8751 9.27672 12.737 9.07002 12.6427 8.84253C12.5485 8.61505 12.5 8.37123 12.5 8.125C12.5 7.62772 12.6975 7.15081 13.0492 6.79917C13.4008 6.44754 13.8777 6.25 14.375 6.25C14.8723 6.25 15.3492 6.44754 15.7008 6.79917C16.0525 7.15081 16.25 7.62772 16.25 8.125ZM15 8.125C15 7.95924 14.9342 7.80027 14.8169 7.68306C14.6997 7.56585 14.5408 7.5 14.375 7.5C14.2092 7.5 14.0503 7.56585 13.9331 7.68306C13.8158 7.80027 13.75 7.95924 13.75 8.125C13.75 8.29076 13.8158 8.44973 13.9331 8.56694C14.0503 8.68415 14.2092 8.75 14.375 8.75C14.5408 8.75 14.6997 8.68415 14.8169 8.56694C14.9342 8.44973 15 8.29076 15 8.125ZM4.985 18.4075C5.36871 18.6321 5.80538 18.7504 6.25 18.75H16.25C16.7125 18.75 17.1437 18.625 17.515 18.4075L11.6875 12.6825C11.5707 12.568 11.4136 12.5038 11.25 12.5038C11.0864 12.5038 10.9293 12.568 10.8125 12.6825L4.985 18.4075Z"
fill="#515B73"
/>
</svg>
</div>
);
};
export default ImageIcon;

View File

@ -0,0 +1,10 @@
export const generateImagePreview = (
file: File,
setImagePreview: (result: string) => void,
) => {
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
};

View File

@ -0,0 +1,51 @@
import { useTranslation } from "react-i18next";
import { FaStore } from "react-icons/fa";
import ValidationField from "../../../../../Components/ValidationField/ValidationField";
import { statusType } from "../../../../../config/statusType";
const PersonalDetailsForm = () => {
const [t] = useTranslation();
return (
<div className="PersonalDetailsForm">
<header className="header_form">
<FaStore />
<h4>{t("header.personal_information")}</h4>
</header>
<main className="main_form_body">
<ValidationField
name={"first_name"}
placeholder={"_"}
label={"first_name"}
/>
<ValidationField
name={"last_name"}
placeholder={"_"}
type="Date"
label={"last_name"}
/>
<ValidationField
name={"email_address"}
placeholder={"_"}
label={"email_address"}
type="Select"
option={statusType}
/>
<ValidationField
name={"username"}
placeholder={"_"}
label={"username"}
/>
<ValidationField
name={"full_name"}
placeholder={"_"}
label={"Phone Number"}
/>
</main>
</div>
);
};
export default PersonalDetailsForm;

View File

@ -0,0 +1,29 @@
import { useTranslation } from "react-i18next";
import { FaStore } from "react-icons/fa";
import ValidationField from "../../../../../Components/ValidationField/ValidationField";
import { nationalities } from "../../../../../types/App";
const TitleDetailsForm = () => {
const [t] = useTranslation();
return (
<div className="TitleDetailsForm">
<header className="header_form">
<FaStore />
<h4>{t("header.address")}</h4>
</header>
<main className="main_form_body">
<ValidationField
name={"city_id"}
placeholder={"_"}
label={"city"}
type="Select"
option={nationalities}
/>
<ValidationField name={"address"} placeholder={"_"} label={"address"} />
</main>
</div>
);
};
export default TitleDetailsForm;

View File

@ -0,0 +1,13 @@
import * as Yup from "yup";
export const getInitialValues = (objectToEdit: Partial<any>) => {
return {
id: objectToEdit?.id ?? null,
name: objectToEdit?.name ?? null,
};
};
export const getValidationSchema = () => {
// validate input
return Yup.object().shape({});
};

View File

@ -0,0 +1,35 @@
import TabHeader from './TabHeader'
import NotificationCard from './Notification/NotificationCard'
import { NotificationData } from '../../../../faker/item'
import { Form, Formik } from 'formik'
import { getInitialValues, getValidationSchema } from './Notification/formUtils'
const Notification = () => {
const handelSubmit = (values: any) => {
console.log(values, "values");
};
return (
<div className='notification'>
<TabHeader
name='notification'
description='get_notified_of_whats_happening_now_you_can_turn_it_off_at_any_time'
>
</TabHeader>
<Formik
initialValues={getInitialValues({})}
validationSchema={getValidationSchema}
onSubmit={handelSubmit}
>
<Form className="Form_details_container">
<div className='setting_notification_body'>
{NotificationData?.map((not: any) => (
<NotificationCard name={not.label} description={not?.value} />
))}
</div>
</Form>
</Formik>
</div>
)
}
export default Notification

View File

@ -0,0 +1,27 @@
import { useTranslation } from "react-i18next"
import ValidationField from "../../../../../Components/ValidationField/ValidationField";
const NotificationCard = ({
name,
description,
}:{
name:string,
description:string,
}) => {
const {t} = useTranslation();
return (
<div className='notification_card'>
<div>
<h5>{t(`${name}`)}</h5>
<p>{t(`${description}`)}</p>
</div>
<div>
<ValidationField type="Checkbox" name="name" label="empty"/>
</div>
</div>
)
}
export default NotificationCard

View File

@ -0,0 +1,13 @@
import * as Yup from "yup";
export const getInitialValues = (objectToEdit: Partial<any>) => {
return {
id: objectToEdit?.id ?? null,
name: objectToEdit?.name ?? null,
};
};
export const getValidationSchema = () => {
// validate input
return Yup.object().shape({});
};

View File

@ -0,0 +1,26 @@
import TabHeader from './TabHeader'
import SecurityCard from './SecuritySetting/SecurityCard'
import { SecurityData } from './SecuritySetting/SecurityData'
import { SettingType } from '../../../../types/Setting'
const SecuritySetting = () => {
return (
<div className='security_setting'>
<TabHeader
name='security_setting'
description='upload_your_photo_and_personal_data_here'
>
</TabHeader>
<div className='security_setting_body'>
{SecurityData?.map((e:SettingType)=>(
<SecurityCard
name={e?.name}
description={e?.description}
children={e?.children}/>
))}
</div>
</div>
)
}
export default SecuritySetting

View File

@ -0,0 +1,27 @@
import EditSettingButton from '../../../../../Components/Ui/Buttons/EditSettingButton'
import { useTranslation } from 'react-i18next'
import { FaCheck } from 'react-icons/fa6';
import useModalHandler from '../../../../../utils/useModalHandler';
import { ModalEnum } from '../../../../../enums/Model';
import { canEditEmail } from '../../../../../utils/hasAbilityFn';
import EditEmailModel from './Model/EditEmailModel';
const EmailAddress = () => {
const {t} = useTranslation();
const { handel_open_model } = useModalHandler();
const handleOpenModel = () =>{
handel_open_model(ModalEnum?.Email_EDIT);
}
return (
<div className='setting_email_address'>
{canEditEmail && <EditSettingButton onClick={handleOpenModel}/>}
<EditEmailModel />
<span>{t("input.verify")} <FaCheck/></span>
<p>{t("admin@example.com")}</p>
</div>
)
}
export default EmailAddress

View File

@ -0,0 +1,36 @@
import React from "react";
import { getInitialValuesEmail, getValidationSchemaEmail } from "./formUtil";
import { ModalEnum } from "../../../../../../enums/Model";
import LayoutModel from "../../../../../../Layout/Dashboard/LayoutModel";
import {EmailForm} from "./ModelForm";
import { QueryStatusEnum } from "../../../../../../enums/QueryStatus";
import { useObjectToEdit } from "../../../../../../zustand/ObjectToEditState";
import { useUpdateReseller } from "../../../../../../api/reseller";
const EditModel: React.FC = () => {
const { mutate, status } = useUpdateReseller();
const { objectToEdit } = useObjectToEdit((state) => state);
const handleSubmit = (values: any) => {
mutate({
...values,
});
};
return (
<>
<LayoutModel
status={status as QueryStatusEnum}
ModelEnum={ModalEnum.Email_EDIT}
modelTitle="email_address"
handleSubmit={handleSubmit}
getInitialValues={getInitialValuesEmail(objectToEdit)}
getValidationSchema={getValidationSchemaEmail}
isAddModal={false}
>
<EmailForm />
</LayoutModel>
</>
);
};
export default EditModel;

View File

@ -0,0 +1,36 @@
import React from "react";
import { getInitialValuesPhone, getValidationSchemaPhone } from "./formUtil";
import { ModalEnum } from "../../../../../../enums/Model";
import LayoutModel from "../../../../../../Layout/Dashboard/LayoutModel";
import {PhoneForm} from "./ModelForm";
import { QueryStatusEnum } from "../../../../../../enums/QueryStatus";
import { useObjectToEdit } from "../../../../../../zustand/ObjectToEditState";
import { useUpdateReseller } from "../../../../../../api/reseller";
const EditModel: React.FC = () => {
const { mutate, status } = useUpdateReseller();
const { objectToEdit } = useObjectToEdit((state) => state);
const handleSubmit = (values: any) => {
mutate({
...values,
});
};
return (
<>
<LayoutModel
status={status as QueryStatusEnum}
ModelEnum={ModalEnum.PHONE_EDIT}
modelTitle="phone_number"
handleSubmit={handleSubmit}
getInitialValues={getInitialValuesPhone(objectToEdit)}
getValidationSchema={getValidationSchemaPhone}
isAddModal={false}
>
<PhoneForm />
</LayoutModel>
</>
);
};
export default EditModel;

View File

@ -0,0 +1,27 @@
import { Col, Row } from "reactstrap";
import ValidationField from "../../../../../../Components/ValidationField/ValidationField";
export const PhoneForm = () => {
return (
<Row className="w-100">
<Col>
<ValidationField placeholder="phone_number" label="phone_number" name="phone_number" />
</Col>
</Row>
);
};
PhoneForm;
export const EmailForm = () => {
return (
<Row className="w-100">
<Col>
<ValidationField placeholder="email_address" label="email_address" name="email_address" />
</Col>
</Row>
);
};
EmailForm;

View File

@ -0,0 +1,31 @@
import * as Yup from "yup";
export const getInitialValuesPhone = (objectToEdit: any): any => {
return {
id: objectToEdit?.id ?? null,
phone_number: objectToEdit?.phone_number ?? null,
};
};
export const getValidationSchemaPhone = () => {
return Yup.object().shape({
phone_number: Yup.string().required("validation.required"),
});
};
export const getInitialValuesEmail = (objectToEdit: any): any => {
return {
id: objectToEdit?.id ?? null,
email_address: objectToEdit?.email_address ?? null,
};
};
export const getValidationSchemaEmail = () => {
return Yup.object().shape({
email_address: Yup.string().required("validation.required"),
});
};

View File

@ -0,0 +1,24 @@
import { useTranslation } from "react-i18next"
import { SettingType } from "../../../../../types/Setting";
const SecurityCard = ({
name,
description,
children}:SettingType) => {
const {t} = useTranslation();
return (
<div className='security_card'>
<div>
<h5>{t(`practical.${name}`)}</h5>
<p>{t(`practical.${description}`)}</p>
</div>
<div>
{children}
</div>
</div>
)
}
export default SecurityCard

View File

@ -0,0 +1,53 @@
import { SettingType } from "../../../../../types/Setting";
import SwitchButton from "../../../../../Components/Switch/Switch"
import SecuritySettingButton from "../../../../../Components/Ui/Buttons/SecuritySettingButton";
import VerifyPhone from "./VerifyPhone";
import EmailAddress from "./EmailAddress";
export const SecurityData:SettingType[] = [
{
name:"password",
description:"Set a unique password to protect the account",
children:<SecuritySettingButton name="تغيير كلمة المرور"/>,
},
{
name:"two_factors",
description:"Receive codes via SMS or email every time you log in",
children:<SwitchButton />,
},
{
name:"verify_phone_number",
description:"The phone number associated with the account",
children:<VerifyPhone/>,
},
{
name:"email_address",
description:"The email address associated with the account",
children:<EmailAddress/>,
},
{
name:"device_management",
description:"Devices associated with the account",
children:<SecuritySettingButton name="إدارة"/>,
},
{
name:"account_activity",
description:"account_activities",
children:<SecuritySettingButton name="عرض"/>,
},
{
name:"deactivate_the_account",
description:"This will close your account. Your account will be interactive when you log in again",
children:<SecuritySettingButton name="الغاء تنشيط"/>,
},
{
name:"delete_account",
description:"Your account will be permanently deleted",
children:<SecuritySettingButton name="حذف" danger={true}/>,
},
]

View File

@ -0,0 +1,26 @@
import EditSettingButton from '../../../../../Components/Ui/Buttons/EditSettingButton'
import { t } from 'i18next'
import { FaCheck } from 'react-icons/fa6'
import useModalHandler from '../../../../../utils/useModalHandler';
import { ModalEnum } from '../../../../../enums/Model';
import { canEditPhone } from '../../../../../utils/hasAbilityFn';
import EditPhoneModel from './Model/EditPhoneModel';
const VerifyPhone = () => {
const { handel_open_model } = useModalHandler();
const handleOpenModel = () => {
handel_open_model(ModalEnum?.PHONE_EDIT);
}
return (
<div className='setting_verify_phone'>
{canEditPhone && <EditSettingButton onClick={handleOpenModel} />}
<EditPhoneModel/>
<span>{t("input.verify")} <FaCheck/></span>
<p>{t("0965289543")}</p>
</div>
)
}
export default VerifyPhone

View File

@ -0,0 +1,49 @@
import React, { lazy, useState } from 'react';
import type { TabsProps } from 'antd';
import { Tabs } from 'antd';
import { useTranslation } from 'react-i18next';
import { useWindowResize } from '../../../../Hooks/useWindowResize';
const FileSetting = lazy(() => import("./FileSetting"));
const SecuritySetting = lazy(() => import("./SecuritySetting"));
const Notification = lazy(() => import("./Notification"));
type TabPosition = 'left' | 'right' | 'top' | 'bottom';
const SettingTabs: React.FC = () => {
const {windowWidth} = useWindowResize()
const {t} = useTranslation();
const [tabPosition, setTabPosition] = useState<TabPosition>(windowWidth < 800 ? 'top' : 'left');
const items: TabsProps['items'] = [
{
key: '1',
label: t('table.file_setting'),
children: <FileSetting />,
},
{
key: '2',
label: t('table.security_setting'),
children: <SecuritySetting/>,
},
{
key: '3',
label: t('table.notification'),
children: <Notification/>,
},
];
return (
<>
<Tabs
tabPosition={tabPosition}
addIcon
items={items}
className='setting_tabs'
defaultActiveKey='1'
/>
</>
);
};
export default SettingTabs;

View File

@ -0,0 +1,29 @@
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next'
const TabHeader = ({
name,
description,
children
}:{
name:string,
description:string,
children?:ReactElement
}) => {
const {t} = useTranslation();
return (
<div className='setting_tab_header'>
<div>
<h5>{t(`table.${name}`)}</h5>
<p>{t(`table.${description}`)}</p>
</div>
<div>
{children}
</div>
</div>
)
}
export default TabHeader

View File

@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next";
import { Suspense } from "react";
import { Spin } from "antd";
import useSetPageTitle from "../../../Hooks/useSetPageTitle";
import PageHeader from "../../../Layout/Dashboard/PageHeader";
import SettingTabs from "./Form/SettingTabs";
const TableHeader = () => {
const [t] = useTranslation();
useSetPageTitle([
{name:`${t(`page_header.home`)}`, path:"/"},
{name:`${t(`page_header.setting`)}`, path:"setting"}
]);
return (
<div className="TableWithHeader">
<Suspense fallback={<Spin />}>
<PageHeader
pageTitle="setting"
/>
<SettingTabs/>
</Suspense>
</div>
);
};
export default TableHeader;

View File

@ -18,24 +18,43 @@ const PersonalDetailsForm = () => {
</header>
<main className="main_form_body">
<ValidationField
name={"first_name"}
name={"id_number"}
placeholder={"_"}
label={"first_name"}
label={"ID Number"}
/>
<ValidationField
name={"last_name"}
name={"addition_date"}
placeholder={"_"}
type="Date"
label={"last_name"}
label={"Addition Date"}
/>
<ValidationField
name={"email_address"}
name={"status"}
placeholder={"_"}
label={"email_address"}
label={"status"}
type="Select"
option={statusType}
/>
<ValidationField
name={"full_name"}
placeholder={"_"}
label={"Full Name"}
/>
<ValidationField
name={"phone_number"}
placeholder={"_"}
label={"phone_number"}
/>
<ValidationField
name={"mobile_number"}
placeholder={"_"}
label={"Mobile Number"}
/>
<ValidationField
name={"username"}
placeholder={"_"}
@ -43,11 +62,10 @@ const PersonalDetailsForm = () => {
/>
<ValidationField
name={"full_name"}
name={"seller_percentage"}
placeholder={"_"}
label={"Phone Number"}
label={"Seller Percentage"}
/>
</main>
</div>
);

View File

@ -37,10 +37,11 @@ const User = React.lazy(() => import("./Pages/Admin/User/Page"));
const QuestionBank = React.lazy(() => import("./Pages/Admin/QuestionBank/Page"));
const Notifications = React.lazy(() => import("./Pages/Admin/Notifications/Page"));
const Profile = React.lazy(() => import("./Pages/Admin/Profile/Page"));
const Setting = React.lazy(() => import("./Pages/Admin/Setting/Page"));
const Roles = React.lazy(() => import("./Pages/Admin/Roles/Page"));
const Permissions = React.lazy(() => import("./Pages/Admin/Roles/Permissions/Page"));
const Roles = React.lazy(() => import("./Pages/Admin/Roles/Page"));
const Report = React.lazy(() => import("./Pages/Admin/Report/Page"));
const Param = React.lazy(() => import("./Pages/Admin/Param/Page"));
@ -205,7 +206,7 @@ export const menuItems: TMenuItem[] = [
element: <ProfileReSeller />,
icon: <FaSellcast />,
text: "sidebar.profile",
path: `//${ABILITIES_ENUM?.PROFILE}`,
path: `/${ABILITIES_ENUM?.PROFILE}`,
abilities: ABILITIES_ENUM?.Profile_RE_SELLER,
abilities_value: ABILITIES_VALUES_ENUM.INDEX,
prevPath: 0,
@ -338,6 +339,14 @@ export const CrudRoute: TCrudRoute[] = [
abilities_value: ABILITIES_VALUES_ENUM.INDEX,
prevPath: 0,
},
// {
// header: "page_header.setting",
// element: <Setting />,
// path: `/${ABILITIES_ENUM?.SETTING}`,
// abilities: ABILITIES_ENUM?.SETTING,
// abilities_value: ABILITIES_VALUES_ENUM.INDEX,
// prevPath: 0,
// },
//// RE_SELLER
{
header: "page_header.notifications",

View File

@ -13,4 +13,5 @@
@import './InfoCard.scss';
@import './notifications.scss';
@import './profile.scss';
@import './collections.scss'
@import './collections.scss';
@import './setting.scss';

View File

@ -4,6 +4,13 @@
gap: 20px;
background: var(--bg);
padding: 40px 10px;
.ValidationField{
label{
font-weight: 600;
font-size: 16px !important;
color: var(--secondary) !important;
}
}
> * {
// max-width: 30%;
flex-basis: 31%;
@ -21,7 +28,7 @@
}
}
.resellerButton {
.resellerButton,.file_setting_buttons {
display: flex;
align-items: center;
justify-content: flex-end;

View File

@ -0,0 +1,197 @@
.setting_tabs {
border-top: 1px solid var(--border-color-2);
.ant-tabs-nav {
width: 20%;
}
.ant-tabs-nav-list {
width: 100% !important;
border-left: 1px solid var(--border-color-2);
padding-left: 20px;
}
.ant-tabs-tab {
margin-top: 10px;
color: var(--secondary);
}
.ant-tabs-tab-active {
background: #3D5EE11A !important;
border-radius: 8px 0 0 8px;
border-right: 1px solid #3D5EE11A;
margin-left: 0 !important;
}
.ant-tabs-content-holder {
padding: 20px 0 !important;
}
.ant-tabs-content-holder {
border: none;
margin-right: 20px;
padding-top: 20px !important;
padding-left: 0 !important;
}
.ant-tabs-tabpane-active {
padding-left: 0 !important;
}
.notification {
.setting_notification_body {
padding: 10px 20px;
box-shadow: 0px 0px 32px 0px #080F3414;
border: 1.5px solid #E9EDF4;
border-radius: 10px;
background: #fff;
}
}
}
.setting_tab_header,
.notification_card,
.security_card {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color-2);
padding-block: 10px 20px;
div {
display: flex;
flex-direction: column;
gap: 10px;
h5 {
color: var(--secondary);
font-size: 1.4vw;
}
p {
font-size: 1.1vw;
color: var(--value);
}
.file_setting_buttons {
display: flex !important;
flex-direction: row;
}
}
}
.setting_tab_header {
width: 100%;
margin-bottom: 30px;
}
.notification_card{
&:nth-last-child(1) {
border: none;
}
h5 {
font-size: 1.2vw !important;
}
p {
font-size: .9vw !important;
}
.ant-form-item,
.ant-form-item-control-input-content {
height: 3vh;
gap: 0 !important;
margin-bottom: 0 !important;
}
.ant-checkbox .ant-checkbox-inner {
width: 25px;
height: 25px;
border: 2px solid var(--border-color-2);
}
}
.security_card{
padding: 20px 20px;
box-shadow: 0px 0px 32px 0px #080F3414;
border: 1.5px solid #E9EDF4;
border-radius: 10px;
background: #fff;
margin-block: 20px ;
h5 {
font-size: 1.2vw !important;
}
p {
font-size: .9vw !important;
}
}
.security_setting_button{
display: flex;
align-items: center;
justify-content: center;
outline: none;
border: 2px solid var(--primary);
border-radius: 7px;
padding: 22px 15px !important;
color: var(--primary);
gap: 5px;
&:hover{
border: 2px solid var(--primary) !important;
background: var(--primary) !important;
color: var(--white) !important;
}
}
.security_setting_button_danger{
border-color:var(--warning);
color: var(--warning);
&:hover{
border: 2px solid var(--warning) !important;
background: var(--warning) !important;
}
}
.setting_edit_button{
@include Flex;
background: #F2F4F8;
border: none;
color: #515B73;
svg{
font-size: 20px;
}
&:hover{
background: #F2F4F8 !important;
border: none !important;
color: #515B73 !important;
scale: 1 !important;
}
}
.setting_email_address,
.setting_verify_phone{
display: flex; align-items: center;flex-direction: row-reverse !important;
> span{
@include Flex;
gap: 5px;
font-size: 10px;
padding: 8px ;
direction: ltr;
color: #1ABE17;
background: #E7F9E7;
border-radius: 8px;
cursor: pointer;
svg{
font-size: 10px;
}
}
p{
color: var(--value);
font-size: 14px !important;
}
}
.switch_button{
background: var(--primary) !important;
}

View File

@ -215,4 +215,12 @@ export enum ModalEnum {
/// sales
Sales_ADD = "Sales.add",
/// phone
PHONE_EDIT = "Phone.edit",
/// email
Email_EDIT = "Email.edit",
}

View File

@ -55,8 +55,10 @@ export enum ABILITIES_ENUM {
NOTIFICATIONS_RE_SELLER = "notification_re_seller" ,
Profile_RE_SELLER = "profile_re_seller" ,
Sales = "sales",
Collections = "collections"
Collections = "collections",
SETTING = "setting",
Email = "email",
Phone = "phone"
////
}

View File

@ -4,4 +4,14 @@ export const CollectionData = [
{label:"المستحقات",value:"2.000.000"},
{label:"تم تحصيله",value:"2.000.000"},
{label:"المتبقي",value:"2.000.000"},
]
export const NotificationData = [
{label:"إشعارات البريد الإلكتروني",value:"يمكن أن ترسل لك Substance إشعارات عبر البريد الإلكتروني لأي رسائل مباشرة جديدة"},
{label:"إشعارات البريد الإلكتروني",value:"يمكن أن ترسل لك Substance إشعارات عبر البريد الإلكتروني لأي رسائل مباشرة جديدة"},
{label:"إشعارات البريد الإلكتروني",value:"يمكن أن ترسل لك Substance إشعارات عبر البريد الإلكتروني لأي رسائل مباشرة جديدة"},
{label:"إشعارات البريد الإلكتروني",value:"يمكن أن ترسل لك Substance إشعارات عبر البريد الإلكتروني لأي رسائل مباشرة جديدة"},
{label:"إشعارات البريد الإلكتروني",value:"يمكن أن ترسل لك Substance إشعارات عبر البريد الإلكتروني لأي رسائل مباشرة جديدة"},
]

View File

@ -144,6 +144,7 @@
"sales":"المبيعات",
"hide_hint":"اخفاء الشرح",
"show_hint":"عرض الشرح",
"setting":"الإعدادات",
"past_your_MMl_text":"ضع نص MMl الخاص بك",
"add_MML":"إضافة MML",
"show_preview":"عرض المعاينة",
@ -289,7 +290,23 @@
"sorry_something_went_wrong": "عفوا ، حدث خطأ ما",
"error_404_Page_not_found._Sorry,_the_page_you_are_looking_for_does_not_exist": "خطأ 404 لم يتم العثور على الصفحة. عذرا الصفحة التي تبحث عنها غير موجودة ",
"return_to_the_dashboard": "العودة إلى لوحة القيادة",
"save_changes":"حفظ التغييرات"
"save_changes":"حفظ التغييرات",
"password":"كلمة المرور",
"two_factors":"عاملان",
"verify_phone_number":"التحقق من رقم الهاتف",
"email_address":"عنوان البريد الإلكتروني",
"device_management":"إدارة الأجهزة",
"account_activity":"نشاط الحساب",
"deactivate_the_account":"إلغاء تنشيط الحساب",
"delete_account":"حذف الحساب",
"Set a unique password to protect the account":"تعيين كلمة مرور فريدة لحماية الحساب",
"Receive codes via SMS or email every time you log in":"تلقي الرموز عبر الرسائل القصيرة أو البريد الإلكتروني في كل مرة تقوم فيها بتسجيل الدخول",
"The phone number associated with the account":"رقم الهاتف المرتبط بالحساب",
"The email address associated with the account":"عنوان البريد الإلكتروني المرتبط بالحساب",
"Devices associated with the account":"الأجهزة المرتبطة بالحساب",
"account_activities":"أنشطة الحساب",
"This will close your account. Your account will be interactive when you log in again":"سيؤدي هذا إلى إغلاق حسابك. سيكون حسابك تفاعليا عند تسجيل الدخول مرة أخرى",
"Your account will be permanently deleted":"سيتم حذف حسابك نهائيا"
},
"Table": {
"header": "",
@ -368,7 +385,9 @@
"managers":"مدراء",
"manager":"مدير",
"sale":"عملية بيع",
"collections": "التحصيلات"
"collections": "التحصيلات",
"phone_number":"رقم الهاتف",
"email_address":"عنوان البريد الإلكتروني"
},
"education_class_actions": {
"Student_Records": "سجلات الطلاب",
@ -496,7 +515,8 @@
"empty":"",
"role":"الدور",
"submit_password":"تأكيد كلمة المرور",
"join_date":"تاريخ الانضمام"
"join_date":"تاريخ الانضمام",
"verify":"التحقق"
},
"select": {
"enums": {
@ -876,7 +896,8 @@
"add_manager":"إضافة مدير",
"collections": "التحصيلات",
"sales":"المبيعات",
"edit_manager":"تعديل مدير"
"edit_manager":"تعديل مدير",
"setting":"الإعدادات"
},
"page_header": {
"home": "لوحة القيادة",
@ -922,7 +943,8 @@
"permissions":"اذونات",
"managers":"مدراء",
"collections": "التحصيلات",
"sales":"المبيعات"
"sales":"المبيعات",
"setting":"الإعدادات"
},
"table": {
"student": "قائمة الطلاب",
@ -933,7 +955,13 @@
"managers":"مدراء",
"managers_list":"قائمة المدراء",
"sales":"المبيعات",
"collections": "التحصيلات"
"collections": "التحصيلات",
"setting":"الإعدادات",
"file_setting":"إعدادات الملف",
"security_setting":"إعدادات الأمان",
"notification":"الاشعارات",
"upload_your_photo_and_personal_data_here":"قم بتحميل صورتك وبياناتك الشخصية هنا",
"get_notified_of_whats_happening_now_you_can_turn_it_off_at_any_time":"احصل على إشعار بما يحدث الآن ، يمكنك إيقاف تشغيله في أي وقت"
},
"alphabet": {
"A": "A",

7
src/types/Setting.ts Normal file
View File

@ -0,0 +1,7 @@
import { ReactElement } from "react";
export type SettingType = {
name:string;
description:string;
children?:ReactElement;
}

View File

@ -736,3 +736,18 @@ export const canDeleteSales = hasAbility(
ABILITIES_ENUM.Sales,
ABILITIES_VALUES_ENUM.DELETE,
);
/// Phone
export const canEditPhone = hasAbility(
ABILITIES_ENUM.Phone,
ABILITIES_VALUES_ENUM.UPDATE,
);
/// Email
export const canEditEmail = hasAbility(
ABILITIES_ENUM.Email,
ABILITIES_VALUES_ENUM.UPDATE,
);