change name logind all component

This commit is contained in:
hamid zarghami
2025-11-23 08:39:30 +03:30
parent 9eadbb568b
commit 42960e581a
32 changed files with 526 additions and 47 deletions
+55 -3
View File
@@ -18,12 +18,14 @@
"formik": "^2.4.6",
"i18next": "^25.4.2",
"iconsax-react": "^0.0.8",
"leaflet": "^1.9.4",
"moment-jalaali": "^0.10.4",
"rc-rate": "^2.13.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-dropzone": "^14.3.8",
"react-i18next": "^15.7.2",
"react-leaflet": "^5.0.0-rc.2",
"react-loading-skeleton": "^3.5.0",
"react-multi-date-picker": "^4.5.2",
"react-otp-input": "^3.1.1",
@@ -37,6 +39,7 @@
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/leaflet": "^1.9.21",
"@types/moment-jalaali": "^0.7.9",
"@types/node": "^24.10.1",
"@types/react": "^19.1.10",
@@ -1456,6 +1459,17 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-leaflet/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
@@ -2128,6 +2142,13 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
@@ -2147,6 +2168,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/leaflet": {
"version": "1.9.21",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/moment-jalaali": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
@@ -3759,9 +3790,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3828,6 +3859,13 @@
"json-buffer": "3.0.1"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -4663,6 +4701,20 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-leaflet": {
"version": "5.0.0-rc.2",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0-rc.2.tgz",
"integrity": "sha512-1xQGYG9mEIW+nfkQhqgHImwUuB1UDlnzYFSzv6PrBFDBeYrFmv0BbpwpNAFdJg/UQ2yz5UZSL7ZwlUxjwb8MZw==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^3.0.0-rc.2"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/react-loading-skeleton": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.5.0.tgz",
+3
View File
@@ -20,12 +20,14 @@
"formik": "^2.4.6",
"i18next": "^25.4.2",
"iconsax-react": "^0.0.8",
"leaflet": "^1.9.4",
"moment-jalaali": "^0.10.4",
"rc-rate": "^2.13.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-dropzone": "^14.3.8",
"react-i18next": "^15.7.2",
"react-leaflet": "^5.0.0-rc.2",
"react-loading-skeleton": "^3.5.0",
"react-multi-date-picker": "^4.5.2",
"react-otp-input": "^3.1.1",
@@ -39,6 +41,7 @@
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/leaflet": "^1.9.21",
"@types/moment-jalaali": "^0.7.9",
"@types/node": "^24.10.1",
"@types/react": "^19.1.10",
+1
View File
@@ -3,6 +3,7 @@ import { BrowserRouter } from 'react-router-dom'
import 'swiper/swiper-bundle.css';
// import 'rc-rate/assets/index.css';
// import "react-multi-date-picker/styles/layouts/mobile.css"
import 'leaflet/dist/leaflet.css';
import './assets/fonts/irancell/style.css'
import i18next from 'i18next'
import { I18nextProvider } from 'react-i18next'
+3 -3
View File
@@ -5,7 +5,7 @@ import { clx } from '../helpers/utils'
type Props = {
className?: string;
isLoading?: boolean,
isloading?: boolean,
} & ButtonHTMLAttributes<HTMLButtonElement> &
XOR<{ children: ReactNode }, { label: string }>;
@@ -18,9 +18,9 @@ const Button: FC<Props> = memo((props: Props) => {
);
return (
<button disabled={props.isLoading} {...props} className={`${buttonClass} ${props.className}`} >
<button disabled={props.isloading} {...props} className={`${buttonClass} ${props.className}`} >
{
props.isLoading ?
props.isloading ?
<MoonLoader color="white" size={16} />
:
props.label || props.children
+2 -2
View File
@@ -5,7 +5,7 @@ import Button from './Button'
import Textarea from './Textarea'
type Props = {
isLoading?: boolean,
isloading?: boolean,
close: () => void,
isOpen: boolean,
onConfrim: (text?: string) => void,
@@ -51,7 +51,7 @@ const ModalConfrim: FC<Props> = (props: Props) => {
<Button
label={t('confrim.yes')}
onClick={() => props.onConfrim(props.isHasDescription ? description : undefined)}
isLoading={props.isLoading}
isloading={props.isloading}
/>
<Button
label={t('confrim.cancel')}
+3 -3
View File
@@ -9,7 +9,7 @@ import Pagination from '../components/Pagination';
const Table = <T extends RowDataType>({
columns,
data,
isLoading = false,
isloading = false,
onRowClick,
className = '',
rowClassName = '',
@@ -74,7 +74,7 @@ const Table = <T extends RowDataType>({
const hasActions = (actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions));
const roundedClass = hasActions ? 'rounded-b-2xl' : 'rounded-2xl';
if (isLoading) {
if (isloading) {
return (
<div className={`bg-white ${roundedClass} overflow-hidden`}>
{Array.from({ length: emptyRowsCount }).map((_, index) => (
@@ -145,7 +145,7 @@ const Table = <T extends RowDataType>({
};
const renderTableBody = () => {
if (isLoading) {
if (isloading) {
return (
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
);
+3 -3
View File
@@ -4,10 +4,10 @@ import ModalConfrim from './ModalConfrim'
interface Props {
onDelete: () => void,
isLoading?: boolean
isloading?: boolean
}
const TrashWithConfrim: FC<Props> = ({ onDelete, isLoading = false }) => {
const TrashWithConfrim: FC<Props> = ({ onDelete, isloading = false }) => {
const [isConfirm, setIsConfirm] = useState(false)
return (
<div>
@@ -21,7 +21,7 @@ const TrashWithConfrim: FC<Props> = ({ onDelete, isLoading = false }) => {
isOpen={isConfirm}
close={() => setIsConfirm(false)}
onConfrim={() => onDelete()}
isLoading={isLoading}
isloading={isloading}
/>
</div>
)
+1 -1
View File
@@ -29,7 +29,7 @@ export interface ActionType {
export interface TableProps<T extends RowDataType> {
columns: ColumnType<T>[];
data?: T[];
isLoading?: boolean;
isloading?: boolean;
onRowClick?: (id: string | number, item: T) => void;
className?: string;
rowClassName?: string | ((item: T, index: number) => string);
+1 -1
View File
@@ -103,7 +103,7 @@ const LoginStep1: FC = () => {
label={t('auth.next')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={loginWithOtp.isPending}
isloading={loginWithOtp.isPending}
/>
</div>
+1 -1
View File
@@ -109,7 +109,7 @@ const LoginStep2: FC = () => {
label={t('auth.login')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={otpVerify.isPending}
isloading={otpVerify.isPending}
/>
{/* <div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
+1 -1
View File
@@ -68,7 +68,7 @@ const RegisterStep1: FC = () => {
label={t('auth.next')}
className='mt-8'
onClick={() => formik.handleSubmit()}
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
isloading={loginWithOtp.isPending || checkHasAccount.isPending}
/>
</div>
)
+1 -1
View File
@@ -180,7 +180,7 @@ const CategoryFood: FC = () => {
<Table
columns={columns}
data={filteredCategories}
isLoading={isLoading}
isloading={isLoading}
selectable
/>
</div>
+1 -1
View File
@@ -116,7 +116,7 @@ const CreateFood: FC = () => {
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={isPending || isUploading}
isloading={isPending || isUploading}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
+1 -1
View File
@@ -60,7 +60,7 @@ const FoodsList: FC = () => {
<Table
columns={columns}
data={foods}
isLoading={isLoading}
isloading={isLoading}
pagination={{
currentPage,
totalPages,
+1 -1
View File
@@ -182,7 +182,7 @@ const UpdateFood: FC = () => {
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={isPending || isUploading}
isloading={isPending || isUploading}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
+1 -1
View File
@@ -66,7 +66,7 @@ const CategoryForm: FC<Props> = ({
<Button
onClick={() => formik.handleSubmit()}
className="w-full"
isLoading={isSubmitting}
isloading={isSubmitting}
>
<Add color='#fff' size={20} className="ml-2" />
ذخیره
@@ -59,7 +59,7 @@ const CategoryTableColumns = ({ onStatusChange, onEdit, onDelete, isDeleting = f
/>
<TrashWithConfrim
onDelete={() => onDelete(item.id)}
isLoading={isDeleting}
isloading={isDeleting}
/>
</div>
)
@@ -80,7 +80,7 @@ export const getFoodTableColumns = ({ onDelete, isDeleting }: GetFoodTableColumn
</Link>
<TrashWithConfrim
onDelete={() => onDelete(item.id)}
isLoading={isDeleting}
isloading={isDeleting}
/>
</div>
)
+1 -1
View File
@@ -49,7 +49,7 @@ const CreateRole: FC = () => {
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={isPending}
isloading={isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
+1 -1
View File
@@ -58,7 +58,7 @@ const RolesList: FC = () => {
<Table
columns={columns}
data={roles}
isLoading={isLoading}
isloading={isLoading}
pagination={{
currentPage,
totalPages,
+1 -1
View File
@@ -81,7 +81,7 @@ const UpdateRole: FC = () => {
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={isPending}
isloading={isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
@@ -46,7 +46,7 @@ export const getRoleTableColumns = ({ onDelete, isDeleting }: GetRoleTableColumn
{onDelete && (
<TrashWithConfrim
onDelete={() => onDelete(item.id)}
isLoading={isDeleting}
isloading={isDeleting}
/>
)}
</div>
+1 -1
View File
@@ -56,7 +56,7 @@ const CreateSchedule: FC = () => {
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={isPending}
isloading={isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
+1 -1
View File
@@ -61,7 +61,7 @@ const ScheduleList: FC = () => {
<Table
columns={columns}
data={schedules}
isLoading={isLoading}
isloading={isLoading}
pagination={{
currentPage,
totalPages,
+1 -1
View File
@@ -83,7 +83,7 @@ const UpdateSchedule: FC = () => {
<Button
className='px-5'
onClick={() => formik.handleSubmit()}
isLoading={isPending}
isloading={isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle className='size-5' color='white' />
@@ -62,7 +62,7 @@ export const getScheduleTableColumns = ({ onDelete, isDeleting, t }: GetSchedule
{onDelete && (
<TrashWithConfrim
onDelete={() => onDelete(item.id)}
isLoading={isDeleting}
isloading={isDeleting}
/>
)}
</div>
+243 -14
View File
@@ -1,24 +1,253 @@
import Button from '@/components/Button'
import ColorPicker from '@/components/ColorPicker'
import Input from '@/components/Input'
import { type FC } from 'react'
import Textarea from '@/components/Textarea'
import UploadBox from '@/components/UploadBox'
import DefaulModal from '@/components/DefaulModal'
import LocationPicker from './components/LocationPicker'
import LocationInput from './components/LocationInput'
import { TickCircle } from 'iconsax-react'
import { type FC, useEffect, useState } from 'react'
import { useGetRestaurant, useUpdateRestaurant } from './hooks/useSettingData'
import { useSingleUpload } from '../uploader/hooks/useUploaderData'
import type { UpdateRestaurantType } from './types/Types'
import type { ErrorType } from '@/helpers/types'
import { useFormik } from 'formik'
import * as yup from 'yup'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/config/func'
const validationSchema = yup.object({
name: yup.string().required('نام رستوران الزامی است'),
menuColor: yup.string().required('رنگ منو الزامی است'),
phone: yup.string().required('شماره تماس الزامی است'),
instagram: yup.string().url('لینک اینستاگرام معتبر نیست').nullable(),
address: yup.string().required('آدرس الزامی است'),
latitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
longitude: yup.number().required('انتخاب موقعیت روی نقشه الزامی است'),
description: yup.string().nullable(),
})
const GeneralSettings: FC = () => {
const { data: restaurant, refetch } = useGetRestaurant()
const { mutate: updateRestaurant, isPending } = useUpdateRestaurant()
const { mutate: singleUpload, isPending: isUploading } = useSingleUpload()
const [isLocationModalOpen, setIsLocationModalOpen] = useState(false)
const [logoFile, setLogoFile] = useState<File[]>([])
const formik = useFormik<UpdateRestaurantType>({
initialValues: {
name: '',
menuColor: '#000000',
phone: '',
instagram: '',
address: '',
latitude: 0,
longitude: 0,
description: '',
logo: '',
},
validationSchema,
onSubmit: (values) => {
const submitSettings = (logoUrl?: string) => {
const settingsData: UpdateRestaurantType = {
...values,
logo: logoUrl || values.logo || ''
}
updateRestaurant(settingsData, {
onSuccess: () => {
toast.success('تنظیمات با موفقیت ذخیره شد')
refetch()
setLogoFile([])
},
onError: (error: unknown) => {
toast.error('خطا در ذخیره تنظیمات')
console.error(error)
},
})
}
if (logoFile.length > 0) {
singleUpload(logoFile[0], {
onSuccess: (response) => {
const logoUrl = response?.data?.url || ''
submitSettings(logoUrl)
},
onError: (error: ErrorType) => {
toast.error(extractErrorMessage(error, 'خطا در آپلود لوگو'))
},
})
} else {
submitSettings()
}
},
enableReinitialize: true,
})
useEffect(() => {
if (restaurant?.data) {
const restaurantData = restaurant.data
formik.setValues({
name: restaurantData.name || '',
menuColor: restaurantData.menuColor || '#000000',
phone: restaurantData.phoneNumber || '',
instagram: restaurantData.instagram || '',
address: restaurantData.address || '',
latitude: restaurantData.latitude || 0,
longitude: restaurantData.longitude || 0,
description: restaurantData.description || '',
logo: restaurantData.logo || '',
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [restaurant])
const handleLocationSelect = (lat: number, lng: number) => {
formik.setFieldValue('latitude', lat)
formik.setFieldValue('longitude', lng)
setIsLocationModalOpen(false)
}
return (
<div className='bg-white rounded-4xl p-8'>
<div className='text-lg font-light'>
تنظیمات عمومی
<>
<div className='bg-white rounded-4xl p-8'>
<div className='text-lg font-light'>
تنظیمات عمومی
</div>
<form onSubmit={formik.handleSubmit}>
<div className='mt-8 rowTwoInput'>
<Input
label='نام رستوران'
name='name'
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error_text={formik.touched.name && formik.errors.name ? formik.errors.name : undefined}
/>
<ColorPicker
label='رنگ منو'
value={formik.values.menuColor}
onChange={(value) => formik.setFieldValue('menuColor', value)}
error_text={formik.touched.menuColor && formik.errors.menuColor ? formik.errors.menuColor : undefined}
/>
</div>
<div className='mt-8'>
<UploadBox
label='لوگو'
onChange={(files) => setLogoFile(files)}
isReset={logoFile.length === 0 && !formik.values.logo}
/>
{
formik.values.logo && (
<div className='mt-3'>
<img src={formik.values.logo} alt='logo' className='w-20 h-20 rounded-full' />
</div>
)
}
</div>
<div className='mt-8 rowTwoInput'>
<Input
label='شماره تماس'
name='phone'
value={formik.values.phone}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : undefined}
/>
<Input
label='اینستاگرام'
name='instagram'
value={formik.values.instagram}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error_text={formik.touched.instagram && formik.errors.instagram ? formik.errors.instagram : undefined}
placeholder='https://instagram.com/...'
/>
</div>
<div className='mt-8'>
<Textarea
label='آدرس'
name='address'
value={formik.values.address}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error_text={formik.touched.address && formik.errors.address ? formik.errors.address : undefined}
/>
</div>
<LocationInput
label='موقعیت روی نقشه'
latitude={formik.values.latitude}
longitude={formik.values.longitude}
onClick={() => setIsLocationModalOpen(true)}
error_text={
(formik.touched.latitude || formik.touched.longitude) && (formik.errors.latitude || formik.errors.longitude)
? formik.errors.latitude || formik.errors.longitude
: undefined
}
/>
<div className='mt-8'>
<Textarea
label='توضیحات'
name='description'
value={formik.values.description || ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : undefined}
/>
</div>
<div className='mt-8 flex justify-end'>
<Button
type='submit'
className='w-fit px-6'
isloading={isPending || isUploading}
disabled={isPending || isUploading}
>
<div className='flex gap-2 items-center'>
<TickCircle
size={20}
color='#fff'
/>
<span>ذخیره</span>
</div>
</Button>
</div>
</form>
</div>
<div className='mt-8 rowTwoInput'>
<Input
label='نام رستوران'
/>
<ColorPicker
label='رنگ منو'
/>
</div>
</div>
<DefaulModal
open={isLocationModalOpen}
close={() => setIsLocationModalOpen(false)}
isHeader={true}
title_header='انتخاب موقعیت روی نقشه'
width={800}
>
<div className='p-4'>
<LocationPicker
initialPosition={
formik.values.latitude && formik.values.longitude
? [formik.values.latitude, formik.values.longitude]
: undefined
}
onLocationSelect={handleLocationSelect}
/>
<div className='mt-4 text-xs text-gray-600 text-center'>
روی نقشه کلیک کنید تا موقعیت را انتخاب کنید
</div>
</div>
</DefaulModal>
</>
)
}
@@ -0,0 +1,45 @@
import { type FC } from 'react'
import { Location } from 'iconsax-react'
import Error from '@/components/Error'
type LocationInputProps = {
label?: string
latitude?: number
longitude?: number
onClick: () => void
error_text?: string
}
const LocationInput: FC<LocationInputProps> = ({
label,
latitude,
longitude,
onClick,
error_text,
}) => {
const getLocationDisplay = () => {
if (latitude && longitude) {
return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`
}
return 'روی نقشه کلیک کنید'
}
return (
<div className='mt-8'>
{label && <div className='text-sm mb-1'>{label}</div>}
<div
onClick={onClick}
className='w-full bg-white h-10 text-black px-4 text-xs rounded-xl border border-border flex items-center gap-2 cursor-pointer mt-1'
>
<Location size={20} color='#8C90A3' />
<span className='flex-1 text-right'>{getLocationDisplay()}</span>
</div>
{error_text && (
<Error errorText={error_text} />
)}
</div>
)
}
export default LocationInput
@@ -0,0 +1,72 @@
import { type FC, useState, useEffect } from 'react'
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet'
import { Icon } from 'leaflet'
import type { LatLngTuple } from 'leaflet'
interface LocationPickerProps {
initialPosition?: [number, number]
onLocationSelect: (lat: number, lng: number) => void
}
const ChangeView = ({ center, zoom }: { center: LatLngTuple; zoom: number }) => {
const map = useMap()
useEffect(() => {
map.setView(center, zoom)
}, [map, center, zoom])
return null
}
const LocationPicker: FC<LocationPickerProps> = ({ initialPosition, onLocationSelect }) => {
const [position, setPosition] = useState<LatLngTuple>(
initialPosition || [35.6892, 51.3890] // تهران به عنوان پیش‌فرض
)
useEffect(() => {
if (initialPosition) {
setPosition(initialPosition)
}
}, [initialPosition])
const LocationMarker = () => {
useMapEvents({
click(e) {
const newPosition: LatLngTuple = [e.latlng.lat, e.latlng.lng]
setPosition(newPosition)
onLocationSelect(newPosition[0], newPosition[1])
},
})
const customIcon = new Icon({
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41],
})
return position ? <Marker position={position} icon={customIcon} /> : null
}
return (
<div className="w-full h-[400px] rounded-xl overflow-hidden">
<MapContainer
center={position}
zoom={13}
style={{ height: '100%', width: '100%' }}
scrollWheelZoom={true}
>
<ChangeView center={position} zoom={13} />
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LocationMarker />
</MapContainer>
</div>
)
}
export default LocationPicker
@@ -0,0 +1,15 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/SettingService";
export const useGetRestaurant = () => {
return useQuery({
queryKey: ["restaurant"],
queryFn: api.getRestaurant,
});
};
export const useUpdateRestaurant = () => {
return useMutation({
mutationFn: api.updateRestaurant,
});
};
@@ -0,0 +1,20 @@
import axios from "@/config/axios";
import type {
GetRestaurantResponse,
UpdateRestaurantType,
} from "../types/Types";
export const getRestaurant = async (): Promise<GetRestaurantResponse> => {
const { data } = await axios.get<GetRestaurantResponse>(
"/admin/restaurants/my-restaurant"
);
return data;
};
export const updateRestaurant = async (params: UpdateRestaurantType) => {
const { data } = await axios.patch(
"/admin/restaurants/my-restaurant",
params
);
return data;
};
+42
View File
@@ -0,0 +1,42 @@
import type { IResponse } from "@/types/response.types";
export type UpdateRestaurantType = {
name: string;
menuColor: string;
phone: string;
instagram: string;
address: string;
logo?: string;
latitude: number;
longitude: number;
description: string;
};
export type Restaurant = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
name: string;
slug: string;
logo: string | null;
address: string | null;
menuColor: string | null;
latitude: number | null;
longitude: number | null;
serviceArea: unknown | null;
isActive: boolean;
establishedYear: number | null;
phoneNumber: string | null;
instagram: string | null;
telegram: string | null;
whatsapp: string | null;
description: string | null;
seoTitle: string | null;
seoDescription: string | null;
tagNames: unknown | null;
images: unknown | null;
vat: number;
};
export type GetRestaurantResponse = IResponse<Restaurant>;