diff options
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/lib/address/components/CreateAddress.jsx | 323 | ||||
| -rw-r--r-- | src/lib/maps/components/PinPointMap.jsx | 163 | ||||
| -rw-r--r-- | src/lib/maps/stores/useMaps.js | 13 | ||||
| -rw-r--r-- | src/lib/product/components/ProductSearch.jsx | 4 |
5 files changed, 360 insertions, 144 deletions
diff --git a/package.json b/package.json index a846749c..82ad1d33 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@hookform/resolvers": "^2.9.10", "@react-email/components": "^0.0.2", "@react-email/render": "^0.0.6", + "@react-google-maps/api": "^2.20.3", "@tailwindcss/line-clamp": "^0.4.2", "axios": "^1.1.3", "camelcase-object-deep": "^1.1.7", diff --git a/src/lib/address/components/CreateAddress.jsx b/src/lib/address/components/CreateAddress.jsx index 9d70e8fc..70307401 100644 --- a/src/lib/address/components/CreateAddress.jsx +++ b/src/lib/address/components/CreateAddress.jsx @@ -1,7 +1,7 @@ import HookFormSelect from '@/core/components/elements/Select/HookFormSelect'; import useAuth from '@/core/hooks/useAuth'; import { useRouter } from 'next/router'; -import { Controller, useForm } from 'react-hook-form'; +import { Controller, set, useForm } from 'react-hook-form'; import * as Yup from 'yup'; import cityApi from '../api/cityApi'; import districtApi from '../api/districtApi'; @@ -14,6 +14,12 @@ import Menu from '@/lib/auth/components/Menu'; import useAddresses from '../hooks/useAddresses'; import stateApi from '../api/stateApi'; +import BottomPopup from '@/core/components/elements/Popup/BottomPopup'; +import PinPointMap from '../../maps/components/PinPointMap'; +import { Button } from '@chakra-ui/react'; +import { MapPinIcon } from 'lucide-react'; +import { useMaps } from '../../maps/stores/useMaps'; + const CreateAddress = () => { const auth = useAuth(); const router = useRouter(); @@ -34,6 +40,8 @@ const CreateAddress = () => { const [districts, setDistricts] = useState([]); const [subDistricts, setSubDistricts] = useState([]); const [filteredTypes, setFilteredTypes] = useState(types); // State to manage filtered types + const [pinedMaps, setPinedMaps] = useState(false); + const { addressMaps, setAddressMaps } = useMaps(); useEffect(() => { const loadState = async () => { @@ -52,7 +60,7 @@ const CreateAddress = () => { setValue('city', ''); if (watchState) { const loadCities = async () => { - let dataCities = await cityApi({stateId: watchState}); + let dataCities = await cityApi({ stateId: watchState }); dataCities = dataCities.map((city) => ({ value: city.id, label: city.name, @@ -133,167 +141,198 @@ const CreateAddress = () => { }; return ( - <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> - <div className='hidden md:block w-3/12 pr-4'> - <Menu /> - </div> - <div className='w-full md:w-9/12 p-4 bg-white border border-gray_r-6 rounded'> - <form onSubmit={handleSubmit(onSubmitHandler)}> - <div className='grid grid-cols-1 md:grid-cols-2 gap-4'> - <div> - <label className='form-label mb-2'>Label Alamat</label> - <Controller - name='type' - control={control} - render={(props) => ( - <HookFormSelect - {...props} - isSearchable={false} - options={filteredTypes} - /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.type?.message} - </div> + <> + <BottomPopup + className=' !h-[75%]' + title='Pin Maps Address' + active={pinedMaps} + close={() => setPinedMaps(false)} + > + <div className='flex mt-4'> + <PinPointMap /> + </div> + </BottomPopup> + <div className='max-w-none md:container mx-auto flex p-0 md:py-10'> + <div className='hidden md:block w-3/12 pr-4'> + <Menu /> + </div> + <div className='w-full md:w-9/12 p-4 bg-white border border-gray_r-6 rounded'> + <form onSubmit={handleSubmit(onSubmitHandler)}> + <div className='mb-4 items-start'> + <label className='form-label mb-2'>PinPoint</label> + {addressMaps ? ( + <div className='flex gap-x-2 items-center'> + <MapPinIcon class='h-6 w-6 text-gray-500 mr-3' />{' '} + <span> {addressMaps} </span> + </div> + ) : ( + <Button variant='plain' onClick={() => setPinedMaps(true)}> + <MapPinIcon class='h-6 w-6 text-gray-500 mr-3' /> + Pin Alamat + </Button> + )} + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4'> + <div> + <label className='form-label mb-2'>Label Alamat</label> + <Controller + name='type' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + isSearchable={false} + options={filteredTypes} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.type?.message} + </div> + </div> - <div> - <label className='form-label mb-2'>Nama</label> - <input - {...register('name')} - placeholder='John Doe' - type='text' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.name?.message} + <div> + <label className='form-label mb-2'>Nama</label> + <input + {...register('name')} + placeholder='John Doe' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.name?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Email</label> - <input - {...register('email')} - placeholder='contoh@email.com' - type='email' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.email?.message} + <div> + <label className='form-label mb-2'>Email</label> + <input + {...register('email')} + placeholder='contoh@email.com' + type='email' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.email?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Mobile</label> - <input - {...register('mobile')} - placeholder='08xxxxxxxx' - type='tel' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.mobile?.message} + <div> + <label className='form-label mb-2'>Mobile</label> + <input + {...register('mobile')} + placeholder='08xxxxxxxx' + type='tel' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.mobile?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Alamat</label> - <input - {...register('street')} - placeholder='Jl. Bandengan Utara 85A' - type='text' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.street?.message} + <div> + <label className='form-label mb-2'>Alamat</label> + <input + {...register('street')} + placeholder='Jl. Bandengan Utara 85A' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.street?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kode Pos</label> - <input - {...register('zip')} - placeholder='10100' - type='number' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.zip?.message} + <div> + <label className='form-label mb-2'>Kode Pos</label> + <input + {...register('zip')} + placeholder='10100' + type='number' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.zip?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Provinsi</label> - <Controller - name='state' - control={control} - render={(props) => ( - <HookFormSelect {...props} options={states} /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.state?.message} + <div> + <label className='form-label mb-2'>Provinsi</label> + <Controller + name='state' + control={control} + render={(props) => ( + <HookFormSelect {...props} options={states} /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.state?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kota</label> - <Controller - name='city' - control={control} - render={(props) => ( - <HookFormSelect {...props} options={cities} disabled={!watchState}/> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.city?.message} + <div> + <label className='form-label mb-2'>Kota</label> + <Controller + name='city' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={cities} + disabled={!watchState} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.city?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kecamatan</label> - <Controller - name='district' - control={control} - render={(props) => ( - <HookFormSelect - {...props} - options={districts} - disabled={!watchCity} - /> - )} - /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.district?.message} + <div> + <label className='form-label mb-2'>Kecamatan</label> + <Controller + name='district' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={districts} + disabled={!watchCity} + /> + )} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.district?.message} + </div> </div> - </div> - <div> - <label className='form-label mb-2'>Kelurahan</label> - <Controller - name='subDistrict' - control={control} - render={(props) => ( - <HookFormSelect - {...props} - options={subDistricts} - disabled={!watchDistrict} - /> - )} - /> + <div> + <label className='form-label mb-2'>Kelurahan</label> + <Controller + name='subDistrict' + control={control} + render={(props) => ( + <HookFormSelect + {...props} + options={subDistricts} + disabled={!watchDistrict} + /> + )} + /> + </div> </div> - </div> - <button - type='submit' - className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto' - > - Simpan - </button> - </form> + <button + type='submit' + className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto' + > + Simpan + </button> + </form> + </div> </div> - </div> + </> ); }; diff --git a/src/lib/maps/components/PinPointMap.jsx b/src/lib/maps/components/PinPointMap.jsx new file mode 100644 index 00000000..0781d8a8 --- /dev/null +++ b/src/lib/maps/components/PinPointMap.jsx @@ -0,0 +1,163 @@ +import React, { useState, useCallback, useRef } from 'react'; +import { + GoogleMap, + useJsApiLoader, + Marker, + Autocomplete, +} from '@react-google-maps/api'; +import { useMaps } from '../stores/useMaps'; +import { LocateFixed, MapPinIcon } from 'lucide-react'; +import { Button } from '@chakra-ui/react'; + +const containerStyle = { + width: '100%', + height: '400px', +}; + +const center = { + lat: -6.2, // Default latitude (Jakarta) + lng: 106.816666, // Default longitude (Jakarta) +}; + +const PinpointLocation = () => { + const { isLoaded } = useJsApiLoader({ + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_API_KEY, // Pastikan API key ada di .env.local + libraries: ['places'], + }); + + const { addressMaps, setAddressMaps, selectedPosition, setSelectedPosition } = + useMaps(); + + const [tempAddress, setTempAddress] = useState(''); + const [tempPosition, setTempPosition] = useState(center); + + const autocompleteRef = useRef(null); + + const onMapClick = useCallback((event) => { + const lat = event.latLng.lat(); + const lng = event.latLng.lng(); + setTempPosition({ lat, lng }); + getAddress(lat, lng); + }, []); + + const handlePlaceSelect = () => { + const place = autocompleteRef.current.getPlace(); + if (place && place.geometry) { + const lat = place.geometry.location.lat(); + const lng = place.geometry.location.lng(); + setTempPosition({ lat, lng }); + setTempAddress(place.formatted_address); + } + }; + + const getAddress = async (lat, lng) => { + try { + const response = await fetch( + `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.NEXT_PUBLIC_GOOGLE_API_KEY}` + ); + const data = await response.json(); + if (data.results[0]) { + setTempAddress(data.results[0].formatted_address); + } + } catch (error) { + console.error('Error fetching address:', error); + } + }; + + const handleUseCurrentLocation = () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const lat = position.coords.latitude; + const lng = position.coords.longitude; + setTempPosition({ lat, lng }); + getAddress(lat, lng); + }, + (error) => { + console.error('Error getting current location:', error); + } + ); + } + }; + + const handleSavePinpoint = (event) => { + event.preventDefault(); + if(tempAddress === '') { + alert('Silahkan pilih lokasi terlebih dahulu'); + return; + } + setSelectedPosition(tempPosition); + setAddressMaps(tempAddress); + }; + + return ( + <div className='w-full'> + <h3>Tentukan Pinpoint Lokasi</h3> + <div style={{ marginBottom: '10px' }}> + {isLoaded ? ( + <Autocomplete + onLoad={(ref) => (autocompleteRef.current = ref)} + onPlaceChanged={handlePlaceSelect} + > + <input + type='text' + placeholder='Cari Alamat...' + style={{ width: '100%', padding: '8px' }} + /> + </Autocomplete> + ) : ( + <p>Loading autocomplete...</p> + )} + </div> + + <div> + {isLoaded ? ( + <GoogleMap + mapContainerStyle={containerStyle} + center={tempPosition} + zoom={15} + onClick={onMapClick} + > + <Marker + position={tempPosition} + draggable={true} + onDragEnd={(e) => onMapClick(e)} + icon={{ + url: 'https://maps.google.com/mapfiles/ms/icons/red-pushpin.png', + scaledSize: new window.google.maps.Size(40, 40), + }} + /> + </GoogleMap> + ) : ( + <p>Loading map...</p> + )} + </div> + + <div style={{ marginTop: '20px' }}> + <Button + variant='solid' + onClick={handleUseCurrentLocation} + > + <LocateFixed class='h-6 w-6 text-gray-500 mr-2' /> Gunakan Lokasi Saat + ini + </Button> + </div> + + <div style={{ marginTop: '10px' }}> + <p>PinPoint :</p> + <div className='flex gap-x-2 shadow-md rounded-sm text-gray-500 p-3 items-center'> + <MapPinIcon class='h-8 w-8 text-gray-500 mr-3' /> + <label> {tempAddress}</label> + </div> + </div> + + <div className='mt-6 flex justify-end'> + <button className='p-3 border border-red-500 bg-red-600 text-white font-semibold rounded-lg' onClick={handleSavePinpoint}> + Simpan Lokasi Ini + </button> + </div> + </div> + ); +}; + +export default PinpointLocation; diff --git a/src/lib/maps/stores/useMaps.js b/src/lib/maps/stores/useMaps.js new file mode 100644 index 00000000..1720e663 --- /dev/null +++ b/src/lib/maps/stores/useMaps.js @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +const center = { + lat: -6.200000, // Default latitude (Jakarta) + lng: 106.816666, // Default longitude (Jakarta) +}; + +export const useMaps = create((set) => ({ + selectedPosition: center, + addressMaps: '', + setSelectedPosition: (position) => set({ selectedPosition: position }), + setAddressMaps: (addressMaps) => set({ addressMaps }), + }));
\ No newline at end of file diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index f7b044aa..3e342bf0 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -501,7 +501,7 @@ const ProductSearch = ({ <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + url={`${prefixUrl}?${toQuery(_.omit(query, ['page', 'fq']))}`} // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='mt-6 mb-2' /> @@ -691,7 +691,7 @@ const ProductSearch = ({ <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} + url={`${prefixUrl}?${toQuery(_.omit(query, ['page', 'fq']))}`} // url={prefixUrl.includes('category') || prefixUrl.includes('lob')? `${prefixUrl}?${toQuery(_.omit(finalQuery, ['page']))}` : `${prefixUrl}?${toQuery(_.omit(query, ['page']))}`} className='!justify-end' /> |
