diff --git a/package.json b/package.json index a6387f3..55dd0e9 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,10 @@ "lucide-react": "^0.511.0", "quill": "^2.0.3", "react": "^19.0.0", + "react-colorful": "^5.6.1", "react-date-object": "^2.1.9", "react-dom": "^19.0.0", + "react-dropzone": "^14.3.8", "react-i18next": "^15.5.1", "react-loading-skeleton": "^3.5.0", "react-multi-date-picker": "^4.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 214fbe4..74c6b06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,12 +41,18 @@ importers: react: specifier: ^19.0.0 version: 19.1.0 + react-colorful: + specifier: ^5.6.1 + version: 5.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-date-object: specifier: ^2.1.9 version: 2.1.9 react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@19.1.0) react-i18next: specifier: ^15.5.1 version: 15.5.2(i18next@25.2.1(typescript@5.7.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.7.3) @@ -948,6 +954,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1219,6 +1229,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1679,6 +1693,12 @@ packages: resolution: {integrity: sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==} engines: {npm: '>=8.2.3'} + react-colorful@5.6.1: + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-date-object@2.1.9: resolution: {integrity: sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==} @@ -1687,6 +1707,12 @@ packages: peerDependencies: react: ^19.1.0 + react-dropzone@14.3.8: + resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-element-popper@2.1.7: resolution: {integrity: sha512-tuM2OxKlW32h+6uFSK6EENHPeZ2OGgOipHfOAl+VLWEv9/j3QkSGbD+ADX3A9uJlmq24i37n28RjJmAbGTfpEg==} peerDependencies: @@ -1934,6 +1960,9 @@ packages: typescript: optional: true + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tunnel-rat@0.1.2: resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} @@ -2834,6 +2863,8 @@ snapshots: argparse@2.0.1: {} + attr-accept@2.2.5: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -3136,6 +3167,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3534,6 +3569,11 @@ snapshots: parchment: 3.0.0 quill-delta: 5.1.0 + react-colorful@5.6.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-date-object@2.1.9: {} react-dom@19.1.0(react@19.1.0): @@ -3541,6 +3581,13 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-dropzone@14.3.8(react@19.1.0): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 19.1.0 + react-element-popper@2.1.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -3779,6 +3826,8 @@ snapshots: optionalDependencies: typescript: 5.7.3 + tslib@2.8.1: {} + tunnel-rat@0.1.2(@types/react@19.1.6)(react@19.1.0): dependencies: zustand: 4.5.7(@types/react@19.1.6)(react@19.1.0) diff --git a/src/assets/images/color.png b/src/assets/images/color.png new file mode 100644 index 0000000..3560bcf Binary files /dev/null and b/src/assets/images/color.png differ diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx new file mode 100644 index 0000000..0c92681 --- /dev/null +++ b/src/components/ColorPicker.tsx @@ -0,0 +1,41 @@ +import { FC, useState } from 'react' +import ColorImage from '@/assets/images/color.png' +import { HexColorPicker } from 'react-colorful' + +type Props = { + defaultColor?: string + changeColor: (color: string) => void, + label: string +} + +const ColorPicker: FC = ({ defaultColor, changeColor, label }) => { + + const [showPicker, setShowPicker] = useState(false) + const [color, setColor] = useState(defaultColor || '#000') + + return ( +
+
+ {label} +
+
setShowPicker(!showPicker)} + className='mt-2 relative cursor-pointer h-10 border border-[#D0D0D0] rounded-xl px-4 flex justify-between items-center'> +
+
+
{color}
+
+ color + {showPicker && ( +
+ { + setColor(newColor) + changeColor(newColor) + }} /> +
+ )} +
+
+ ) +} + +export default ColorPicker \ No newline at end of file diff --git a/src/components/Input.tsx b/src/components/Input.tsx index b20c24d..de90b15 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -15,6 +15,7 @@ type Props = { seprator?: boolean; isNotRequired?: boolean; onChangeSearchFinal?: (value: string) => void; + endIcon?: React.ReactNode; } & InputHTMLAttributes const formatNumber = (value: string | number): string => { @@ -103,6 +104,13 @@ const Input: FC = (props: Props) => { } + { + props.endIcon && +
+ {props.endIcon} +
+ } + { props.error_text && void; + isMultiple?: boolean; + isFile?: boolean, + preview?: string[], + onChangePreview?: (preview: string[]) => void, + getCover?: (url: string) => void, + coverUrl?: string +} + +const UploadBoxDraggble: FC = (props: Props) => { + + const { t } = useTranslation('global') + const [files, setFiles] = useState([]) + const [perviews, setPerviews] = useState(props.preview ? props.preview : []) + const [isFill, setIsFill] = useState(false) + const [cover, setCover] = useState(props.coverUrl ? props.coverUrl : '') + + const onDrop = useCallback((acceptedFiles: File[]) => { + if (props.isMultiple) { + const array = [...files] + array.push(acceptedFiles[0]) + setFiles(array) + props.onChange(array) + } else { + setFiles([acceptedFiles[0]]) + props.onChange([acceptedFiles[0]]) + } + }, [files]) + const { getRootProps, getInputProps } = useDropzone({ onDrop }) + + const handleDelete = (index: number) => { + const array = [...files] + array.splice(index, 1) + setFiles(array) + props.onChange(array) + } + + const handleDeletePreview = (index: number) => { + const array = [...perviews]; + array.splice(index, 1); + setPerviews(array); + if (props.onChangePreview) { + props.onChangePreview(array); + } + } + + useEffect(() => { + + if (props.preview && props.preview.length > 0 && !isFill) { + setPerviews(props.preview) + console.log('preview', props.preview); + + setIsFill(true) + } + + }, [props.preview, isFill]) + + useEffect(() => { + setCover(props.coverUrl ? props.coverUrl : '') + }, [props.coverUrl]) + + + return ( +
+
+ + +
+ {props.label} +
+ +
+ +
{t('upload')}
+
+
+ + { + files.length > 0 || perviews ? ( +
+ { + perviews && perviews.map((item, index) => { + return ( +
+
+ { + if (props.getCover) { + setCover(item) + props.getCover(item) + } + }} src={item} + className={clx( + 'size-10 rounded-full object-cover', + cover === item ? 'border-2 border-red-400' : '' + )} /> +
handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'> + +
+
+
+ ) + }) + } + { + files.map((file, index) => { + if (props.isFile) { + return ( +
+
+
{file.name}
+
+ handleDelete(index)} className='size-4 ' color='red' /> +
+
+
+ ) + } + else + return ( +
+
+ {file.name} +
+ handleDelete(index)} className='size-4 ' color='red' /> +
+
+
+ ) + }) + } +
+ ) : null + } + +
+ ) +} + +export default UploadBoxDraggble \ No newline at end of file diff --git a/src/components/newMessage/NewMessage.tsx b/src/components/newMessage/NewMessage.tsx index 71a74ad..4e9d21e 100644 --- a/src/components/newMessage/NewMessage.tsx +++ b/src/components/newMessage/NewMessage.tsx @@ -5,6 +5,7 @@ import { useSharedStore } from '@/shared/store/sharedStore' import Input from '../Input' import ReactQuill from 'react-quill-new'; import Button from '../Button' +import Select from '../Select' const NewMessage: FC = () => { @@ -17,10 +18,10 @@ const NewMessage: FC = () => { {openNewMessage && ( <>
setOpenNewMessage(false)} /> -
+
{/* Header */}
@@ -61,15 +62,35 @@ const NewMessage: FC = () => { />
-
- +
+ +
+ ) +} + +export default ButtonSidebar \ No newline at end of file diff --git a/src/pages/setting/personality/components/ImageSideBar.tsx b/src/pages/setting/personality/components/ImageSideBar.tsx new file mode 100644 index 0000000..20c8565 --- /dev/null +++ b/src/pages/setting/personality/components/ImageSideBar.tsx @@ -0,0 +1,61 @@ +import Button from '@/components/Button' +import UploadBoxDraggble from '@/components/UploadBoxDraggble' +import { Add, AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' + +const ImageSideBar: FC = () => { + + const { t } = useTranslation() + + return ( +
+
{t('setting.image')}
+ +
+ null} + /> +
+ +
+
+
+ {t('setting.horizontal_position')} +
+
+ + + +
+
+ +
+
+ {t('setting.horizontal_position')} +
+
+ + + +
+
+
+ +
+ +
+
+ ) +} + +export default ImageSideBar \ No newline at end of file diff --git a/src/pages/setting/personality/components/SettingSideBar.tsx b/src/pages/setting/personality/components/SettingSideBar.tsx new file mode 100644 index 0000000..2a0c9f4 --- /dev/null +++ b/src/pages/setting/personality/components/SettingSideBar.tsx @@ -0,0 +1,89 @@ +import { FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import UploadBoxDraggble from '@/components/UploadBoxDraggble'; +import Select from '@/components/Select'; +import { AlignBottom, AlignHorizontally, AlignLeft, AlignRight, AlignTop, AlignVertically } from 'iconsax-react'; +import ColorPicker from '@/components/ColorPicker'; + + +const SettingSideBar: FC = () => { + + const { t } = useTranslation() + const [color, setColor] = useState("#aabbcc"); + + return ( +
+
{t('setting.setting')}
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+ null} + /> +
+ +
+