diff --git a/ditch-the-agent/.env.beta b/ditch-the-agent/.env.beta index bcb9557..8058cc2 100644 --- a/ditch-the-agent/.env.beta +++ b/ditch-the-agent/.env.beta @@ -1,3 +1,3 @@ VITE_API_URL=https://beta.backend.ditchtheagent.com/api/ ENABLE_REGISTRATION=true -USE_LIVE_DATA=false +USE_LIVE_DATA=true diff --git a/ditch-the-agent/package-lock.json b/ditch-the-agent/package-lock.json index 289f2f1..b84be6c 100644 --- a/ditch-the-agent/package-lock.json +++ b/ditch-the-agent/package-lock.json @@ -27,6 +27,7 @@ "formik": "^2.4.6", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", + "lodash.debounce": "^4.0.8", "lucide-react": "^0.525.0", "material-ui-popup-state": "^5.1.0", "react": "^18.2.0", @@ -4689,6 +4690,11 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", diff --git a/ditch-the-agent/package.json b/ditch-the-agent/package.json index 628e2b3..517dba8 100644 --- a/ditch-the-agent/package.json +++ b/ditch-the-agent/package.json @@ -6,8 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "build:beta": "vite build --mode beta", - "build:prod": "tsc && vite build --mode production", + "build:beta": "vite build --mode beta && cp -r ./dist/* /var/www/beta.app.ditchtheagent/html/", + "build:prod": "vite build --mode production", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "predeploy": "vite build && cp ./dist/index.html ./dist/404.html", @@ -33,6 +33,7 @@ "formik": "^2.4.6", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", + "lodash.debounce": "^4.0.8", "lucide-react": "^0.525.0", "material-ui-popup-state": "^5.1.0", "react": "^18.2.0", diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx index b2841ff..ab2ca7e 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/AddPropertyDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Dialog, DialogTitle, @@ -13,7 +13,7 @@ import { Alert, } from '@mui/material'; import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi'; - +import debounce from 'lodash/debounce'; import MapComponent from '../../../../base/MapComponent'; import { AutocompleteDataResponseAPI, @@ -41,6 +41,26 @@ export interface PlacePrediction { place_id: string; } +const fetchAutocompleteOptions = async ( + value: string, + setAutocompleteOptions: (options: any[]) => void, +) => { + // ... your existing API call logic from handleAddressAutocompleteInputChange + try { + let { data } = await axiosInstance.post('autocomplete-proxy/', { + search: value, + search_types: ['A'], + }); + let temp = data.data.map((item) => ({ description: item.address, place_id: item.id })); + console.log(temp); + + setAutocompleteOptions(temp); + } catch (error) { + console.error('Autocomplete fetch failed:', error); + return []; + } +}; + const AddPropertyDialog: React.FC = ({ open, onClose, onAddProperty }) => { const initalValues: Omit = { address: '', @@ -76,6 +96,11 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({}); const [selectedPlace, setSelectedPlace] = useState(null); + const debouncedFetch = useMemo( + () => debounce(fetchAutocompleteOptions, 200), + [], // Dependency array ensures the debounced function is created only once + ); + // Initialize Google Maps Places Service (requires Google Maps API key loaded globally) // This is a simplified approach. For a more robust solution, use @vis.gl/react-google-maps useMapsLibrary hook useEffect(() => { @@ -84,7 +109,6 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on // You might want to handle this by displaying a message or disabling autocomplete } }, []); - const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setNewProperty((prev) => ({ ...prev, [name]: value })); @@ -96,51 +120,81 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on }); } }; + const handleAddressAutocompleteInputChange = useCallback( + (event: React.SyntheticEvent, value: string) => { + // 1. Update the local state immediately for a smooth input experience + setNewProperty((prev) => ({ ...prev, address: value })); // <--- THIS IS THE CRITICAL LINE - const handleAddressAutocompleteInputChange = async ( - event: React.SyntheticEvent, - value: string, - ) => { - const test: boolean = !import.meta.env.USE_LIVE_DATA; - let data: AutocompleteDataResponseAPI[] = []; - if (value.length > 2) { - if (test) { - data = test_autocomplete.data.filter((item) => item.address.includes(value)); - // filter the data here - } else { - const { data } = await axiosRealEstateApi.post( - 'AutoComplete', - { - search: value, - }, - ); + // 2. Clear options if the value is too short + if (value.length < 3) { + setAutocompleteOptions([]); + // Important: Cancel any pending debounced calls + debouncedFetch.cancel(); + return; } - setAutocompleteOptions( - data.map((item) => ({ - description: item.address, - place_id: item.id, - })), - ); - } else { - console.log('we need more characters'); - } + console.log('attempting the function'); - setNewProperty((prev) => ({ ...prev, address: value })); - // if (value.length > 2 && window.google && window.google.maps && window.google.maps.places) { - // setAutocompleteLoading(true); - // const service = new window.google.maps.places.AutocompleteService(); - // service.getPlacePredictions({ input: value }, (predictions, status) => { - // if (status === window.google.maps.places.PlacesServiceStatus.OK && predictions) { - // setAutocompleteOptions(predictions.map(p => ({ description: p.description, place_id: p.place_id }))); - // } else { - // setAutocompleteOptions([]); - // } - // setAutocompleteLoading(false); - // }); - // } else { - // setAutocompleteOptions([]); - // } - }; + // 3. Call the debounced function, which will fire the API call + // only after 300ms of inactivity. + const result = debouncedFetch(value, setAutocompleteOptions); + console.log(result); + }, + [debouncedFetch], + ); + // const handleAddressAutocompleteInputChange = async ( + // event: React.SyntheticEvent, + // value: string, + // ) => { + // const test: boolean = import.meta.env.USE_LIVE_DATA; + // console.log(test); + // let data: AutocompleteDataResponseAPI[] = []; + // if (value.length > 2) { + // if (test) { + // data = test_autocomplete.data.filter((item) => item.address.includes(value)); + // // filter the data here + // setAutocompleteOptions( + // data.map((item) => ({ + // description: item.address, + // place_id: item.id, + // })), + // ); + // } else { + // let { data } = await axiosInstance.post('autocomplete-proxy/', { + // search: value, + // search_types: ['A'], + // }); + // data = data.data; + // console.log(data); + // const temp = data.map((item) => ({ description: item.address, place_id: item.id })); + // console.log(temp); + // setAutocompleteOptions(temp); + // } + // } else { + // console.log('we need more characters'); + // } + + // setNewProperty((prev) => ({ ...prev, address: value })); + // // if (value.length > 2 && window.google && window.google.maps && window.google.maps.places) { + // // setAutocompleteLoading(true); + // // const service = new window.google.maps.places.AutocompleteService(); + // // service.getPlacePredictions({ input: value }, (predictions, status) => { + // // if (status === window.google.maps.places.PlacesServiceStatus.OK && predictions) { + // // setAutocompleteOptions(predictions.map(p => ({ description: p.description, place_id: p.place_id }))); + // // } else { + // // setAutocompleteOptions([]); + // // } + // // setAutocompleteLoading(false); + // // }); + // // } else { + // // setAutocompleteOptions([]); + // // } + // }; + + React.useEffect(() => { + return () => { + debouncedFetch.cancel(); + }; + }, [debouncedFetch]); const handleAddressAutocompleteChange = async ( event: React.SyntheticEvent, @@ -150,7 +204,7 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on console.log('here we go', value); if (value) { console.log('find the test data'); - const test: boolean = true; + const test: boolean = import.meta.env.USE_LIVE_DATA; if (test) { const parts: string[] = test_property_search.data.currentMortgages[0].recordingDate.split('T'); @@ -219,6 +273,91 @@ const AddPropertyDialog: React.FC = ({ open, onClose, on }, sale_info: sale_history, }); + } else { + console.log('using live data'); + let { data } = await axiosInstance.post('property-details-proxy/', { + comps: false, + id: value.place_id, + exact_match: true, + }); + data = data.data; + console.log(data); + console.log(data.currentMortgages); + console.log(data.currentMortgages); + let parts: string; + let loan_amount: string; + let term: string; + if (data.currentMortgages.length > 0) { + parts = data.currentMortgages[0].recordingDate.split('T')[0]; + loan_amount = data.currentMortgages[0].amount.toString(); + term = data.currentMortgages[0].term; + } else { + parts = ''; + loan_amount = ''; + term = ''; + } + + const schools: Omit[] = data.schools.map( + (item) => { + const coordinates = extractLatLon(item.location); + return { + city: item.city, + state: item.state, + zip_code: item.zip, + latitude: coordinates?.latitude, + longitude: coordinates?.longitude, + school_type: item.type, + enrollment: item.enrollment, + grades: item.grades, + name: item.name, + parent_rating: item.parentRating, + rating: item.rating, + }; + }, + ); + console.log(schools); + + // get the sale history + const sale_history: Omit[] = + data.saleHistory.map((item) => { + return { + seq_no: item.seqNo, + sale_date: item.saleDate, + sale_amount: item.saleAmount, + }; + }); + + setNewProperty({ + address: data.propertyInfo.address.address, + street: data.ownerInfo.mailAddress.address, + city: data.propertyInfo.address.city, + state: data.propertyInfo.address.state, + zip_code: data.propertyInfo.address.zip, + latitude: data.propertyInfo.latitude, + longitude: data.propertyInfo.longitude, + market_value: data.estimatedValue.toString(), + loan_amount: loan_amount, + loan_term: term, + loan_start_date: parts, + description: '', + features: [], + pictures: [], + num_bedrooms: data.propertyInfo.bedrooms, + num_bathrooms: data.propertyInfo.bathrooms, + sq_ft: data.propertyInfo.buildingSquareFeet, + realestate_api_id: data.id, + views: 0, + saves: 0, + property_status: 'off_market', + schools: schools, + tax_info: { + assessed_value: data.taxInfo.assessedValue, + assessment_year: data.taxInfo.assessmentYear, + tax_amount: Number(data.taxInfo.taxAmount), + year: data.taxInfo.year, + }, + sale_info: sale_history, + }); } } }; diff --git a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfileCard.tsx b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfileCard.tsx index 7b45729..df47a24 100644 --- a/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfileCard.tsx +++ b/ditch-the-agent/src/components/sections/dashboard/Home/Profile/VendorProfileCard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { Card, CardContent, @@ -25,6 +25,8 @@ import { AutocompleteDataResponseAPI } from 'types'; import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi'; import { extractLatLon } from 'utils'; import { PlacePrediction } from './AddPropertyDialog'; +import { PropertyResponseAPI } from '../../../../../types'; +import debounce from 'lodash/debounce'; interface VendorProfileCardProps { vendor: VendorAPI; @@ -32,6 +34,26 @@ interface VendorProfileCardProps { onSave: (updatedVendor: VendorAPI) => void; } +const fetchAutocompleteOptions = async ( + value: string, + setAutocompleteOptions: (options: any[]) => void, +) => { + // ... your existing API call logic from handleAddressAutocompleteInputChange + try { + let { data } = await axiosInstance.post('autocomplete-proxy/', { + search: value, + search_types: ['A'], + }); + let temp = data.data.map((item) => ({ description: item.address, place_id: item.id })); + console.log(temp); + + setAutocompleteOptions(temp); + } catch (error) { + console.error('Autocomplete fetch failed:', error); + return []; + } +}; + const VendorProfileCard: React.FC = ({ vendor, onUpgrade, onSave }) => { const [isEditing, setIsEditing] = useState(false); const [editedVendor, setEditedVendor] = useState(vendor); @@ -107,6 +129,11 @@ const VendorProfileCard: React.FC = ({ vendor, onUpgrade } }; + const debouncedFetch = useMemo( + () => debounce(fetchAutocompleteOptions, 200), + [], // Dependency array ensures the debounced function is created only once + ); + const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; //setNewProperty((prev) => ({ ...prev, [name]: value })); @@ -119,58 +146,80 @@ const VendorProfileCard: React.FC = ({ vendor, onUpgrade } }; - const handleAddressAutocompleteInputChange = async ( - event: React.SyntheticEvent, - value: string, - ) => { - const test: boolean = !import.meta.env.USE_LIVE_DATA; - let data: AutocompleteDataResponseAPI[] = []; - if (value.length > 2) { - if (test) { - data = test_autocomplete.data.filter((item) => item.address.includes(value)); - // filter the data here - } else { - const { data } = await axiosRealEstateApi.post( - 'AutoComplete', - { - search: value, - }, - ); + const handleAddressAutocompleteInputChange = useCallback( + (event: React.SyntheticEvent, value: string) => { + // 1. Update the local state immediately for a smooth input experience + setEditedVendor((prev) => ({ ...prev, address: value })); // <--- THIS IS THE CRITICAL LINE + + // 2. Clear options if the value is too short + if (value.length < 3) { + setAutocompleteOptions([]); + // Important: Cancel any pending debounced calls + debouncedFetch.cancel(); + return; } - setAutocompleteOptions( - data.map((item) => ({ - description: item.address, - place_id: item.id, - })), - ); - } else { - console.log('we need more characters'); - } - }; + console.log('attempting the function'); + + // 3. Call the debounced function, which will fire the API call + // only after 300ms of inactivity. + const result = debouncedFetch(value, setAutocompleteOptions); + console.log(result); + }, + [debouncedFetch], + ); + + // const handleAddressAutocompleteInputChange = async ( + // event: React.SyntheticEvent, + // value: string, + // ) => { + // const test: boolean = import.meta.env.USE_LIVE_DATA; + // let data: AutocompleteDataResponseAPI[] = []; + // if (value.length > 2) { + // if (test) { + // data = test_autocomplete.data.filter((item) => item.address.includes(value)); + // setAutocompleteOptions( + // data.map((item) => ({ + // description: item.address, + // place_id: item.id, + // })), + // ); + // // filter the data here + // } else { + // let { data } = await axiosInstance.post('autocomplete-proxy/', { + // search: value, + // search_types: ['A'], + // }); + // data = data.data; + // const temp = data.map((item) => ({ description: item.address, place_id: item.id })); + // setAutocompleteOptions(temp); + // } + // } else { + // console.log('we need more characters'); + // } + // }; const handleAddressAutocompleteChange = async ( event: React.SyntheticEvent, value: PlacePrediction | null, ) => { - console.log('here we go', value); - if (1) { - const data = test_autocomplete.data.filter((item) => item.id === value.place_id); - if (data.length > 0) { - const item = data[0]; - const coordinates = extractLatLon(item.location); - setEditedVendor((prev) => ({ - ...prev, - address: item.address, - city: item.city, - state: item.state, - zip_code: item.zip, - latitude: Number(coordinates.latitude), - longitude: Number(coordinates.longitude), - })); - } - } else { - // use the api here - } + let { data } = await axiosInstance.post('property-details-proxy/', { + comps: false, + id: value.place_id, + exact_match: true, + }); + data = data.data; + console.log(data); + + setEditedVendor((prev) => ({ + ...prev, + address: data.propertyInfo.address.address, + street: data.propertyInfo.address.address, + city: data.propertyInfo.address.city, + state: data.propertyInfo.address.state, + zip_code: data.propertyInfo.address.zip, + latitude: data.propertyInfo.latitude, + longitude: data.propertyInfo.longitude, + })); }; return ( diff --git a/ditch-the-agent/src/pages/authentication/SignUp.tsx b/ditch-the-agent/src/pages/authentication/SignUp.tsx index 569d420..8143c84 100644 --- a/ditch-the-agent/src/pages/authentication/SignUp.tsx +++ b/ditch-the-agent/src/pages/authentication/SignUp.tsx @@ -54,7 +54,7 @@ const SignUp = (): ReactElement => { password2, }: SignUpValues): Promise => { try { - const response = await axiosInstance.post('/register/', { + const response = await axiosInstance.post('register/', { email: email, first_name: first_name, last_name: last_name, @@ -62,8 +62,11 @@ const SignUp = (): ReactElement => { password: password, password2: password2, }); + console.log(response); if (response.status == 201) { navigate('/authentication/login'); + + console.log('Good response'); } else { console.log(`No good: ${response}`); } diff --git a/ditch-the-agent/src/types.ts b/ditch-the-agent/src/types.ts index 6b7177b..da30177 100644 --- a/ditch-the-agent/src/types.ts +++ b/ditch-the-agent/src/types.ts @@ -379,23 +379,23 @@ export interface AutocompleteResponseAPI { } export interface AutocompleteDataResponseAPI { zip: string; - address: string; - city: string; + address?: string; + city?: string; searchType: string; stateId: string; - latitude: number; - county: string; - fips: string; + latitude?: number; + county?: string; + fips?: string; title: string; - house: string; + house?: string; unit?: string; - countyId: string; - street: string; - location: string; - id: string; + countyId?: string; + street?: string; + location?: string; + id?: string; state: string; - apn: string; - longitude: number; + apn?: string; + longitude?: number; } export interface PropertyResponseDataMortgageAPI {