Updates from beta testing

This commit is contained in:
2025-11-24 09:40:05 -06:00
parent 3781646b7f
commit 5c443bd1b2
7 changed files with 307 additions and 109 deletions

View File

@@ -1,3 +1,3 @@
VITE_API_URL=https://beta.backend.ditchtheagent.com/api/ VITE_API_URL=https://beta.backend.ditchtheagent.com/api/
ENABLE_REGISTRATION=true ENABLE_REGISTRATION=true
USE_LIVE_DATA=false USE_LIVE_DATA=true

View File

@@ -27,6 +27,7 @@
"formik": "^2.4.6", "formik": "^2.4.6",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"material-ui-popup-state": "^5.1.0", "material-ui-popup-state": "^5.1.0",
"react": "^18.2.0", "react": "^18.2.0",
@@ -4689,6 +4690,11 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" "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": { "node_modules/lodash.defaults": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",

View File

@@ -6,8 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"build:beta": "vite build --mode beta", "build:beta": "vite build --mode beta && cp -r ./dist/* /var/www/beta.app.ditchtheagent/html/",
"build:prod": "tsc && vite build --mode production", "build:prod": "vite build --mode production",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
"predeploy": "vite build && cp ./dist/index.html ./dist/404.html", "predeploy": "vite build && cp ./dist/index.html ./dist/404.html",
@@ -33,6 +33,7 @@
"formik": "^2.4.6", "formik": "^2.4.6",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"material-ui-popup-state": "^5.1.0", "material-ui-popup-state": "^5.1.0",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { import {
Dialog, Dialog,
DialogTitle, DialogTitle,
@@ -13,7 +13,7 @@ import {
Alert, Alert,
} from '@mui/material'; } from '@mui/material';
import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi'; import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi';
import debounce from 'lodash/debounce';
import MapComponent from '../../../../base/MapComponent'; import MapComponent from '../../../../base/MapComponent';
import { import {
AutocompleteDataResponseAPI, AutocompleteDataResponseAPI,
@@ -41,6 +41,26 @@ export interface PlacePrediction {
place_id: string; 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<AutocompleteResponseAPI>('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<AddPropertyDialogProps> = ({ open, onClose, onAddProperty }) => { const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, onAddProperty }) => {
const initalValues: Omit<PropertiesAPI, 'id' | 'owner' | 'created_at' | 'last_updated'> = { const initalValues: Omit<PropertiesAPI, 'id' | 'owner' | 'created_at' | 'last_updated'> = {
address: '', address: '',
@@ -76,6 +96,11 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, on
const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({}); const [formErrors, setFormErrors] = useState<{ [key: string]: string }>({});
const [selectedPlace, setSelectedPlace] = useState<PlacePrediction | null>(null); const [selectedPlace, setSelectedPlace] = useState<PlacePrediction | null>(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) // 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 // This is a simplified approach. For a more robust solution, use @vis.gl/react-google-maps useMapsLibrary hook
useEffect(() => { useEffect(() => {
@@ -84,7 +109,6 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, on
// You might want to handle this by displaying a message or disabling autocomplete // You might want to handle this by displaying a message or disabling autocomplete
} }
}, []); }, []);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setNewProperty((prev) => ({ ...prev, [name]: value })); setNewProperty((prev) => ({ ...prev, [name]: value }));
@@ -96,51 +120,81 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ 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 ( // 2. Clear options if the value is too short
event: React.SyntheticEvent, if (value.length < 3) {
value: string, setAutocompleteOptions([]);
) => { // Important: Cancel any pending debounced calls
const test: boolean = !import.meta.env.USE_LIVE_DATA; debouncedFetch.cancel();
let data: AutocompleteDataResponseAPI[] = []; return;
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<AutocompleteDataResponseAPI[]>(
'AutoComplete',
{
search: value,
},
);
} }
setAutocompleteOptions( console.log('attempting the function');
data.map((item) => ({
description: item.address,
place_id: item.id,
})),
);
} else {
console.log('we need more characters');
}
setNewProperty((prev) => ({ ...prev, address: value })); // 3. Call the debounced function, which will fire the API call
// if (value.length > 2 && window.google && window.google.maps && window.google.maps.places) { // only after 300ms of inactivity.
// setAutocompleteLoading(true); const result = debouncedFetch(value, setAutocompleteOptions);
// const service = new window.google.maps.places.AutocompleteService(); console.log(result);
// service.getPlacePredictions({ input: value }, (predictions, status) => { },
// if (status === window.google.maps.places.PlacesServiceStatus.OK && predictions) { [debouncedFetch],
// setAutocompleteOptions(predictions.map(p => ({ description: p.description, place_id: p.place_id }))); );
// } else { // const handleAddressAutocompleteInputChange = async (
// setAutocompleteOptions([]); // event: React.SyntheticEvent,
// } // value: string,
// setAutocompleteLoading(false); // ) => {
// }); // const test: boolean = import.meta.env.USE_LIVE_DATA;
// } else { // console.log(test);
// setAutocompleteOptions([]); // 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<AutocompleteResponseAPI>('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 ( const handleAddressAutocompleteChange = async (
event: React.SyntheticEvent, event: React.SyntheticEvent,
@@ -150,7 +204,7 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, on
console.log('here we go', value); console.log('here we go', value);
if (value) { if (value) {
console.log('find the test data'); console.log('find the test data');
const test: boolean = true; const test: boolean = import.meta.env.USE_LIVE_DATA;
if (test) { if (test) {
const parts: string[] = const parts: string[] =
test_property_search.data.currentMortgages[0].recordingDate.split('T'); test_property_search.data.currentMortgages[0].recordingDate.split('T');
@@ -219,6 +273,91 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, on
}, },
sale_info: sale_history, sale_info: sale_history,
}); });
} else {
console.log('using live data');
let { data } = await axiosInstance.post<PropertyResponseAPI>('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<SchoolAPI, 'id' | 'created_at' | 'last_updated'>[] = 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<SaleHistoryAPI, 'id' | 'created_at' | 'last_updated'>[] =
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,
});
} }
} }
}; };

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useMemo, useCallback } from 'react';
import { import {
Card, Card,
CardContent, CardContent,
@@ -25,6 +25,8 @@ import { AutocompleteDataResponseAPI } from 'types';
import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi'; import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi';
import { extractLatLon } from 'utils'; import { extractLatLon } from 'utils';
import { PlacePrediction } from './AddPropertyDialog'; import { PlacePrediction } from './AddPropertyDialog';
import { PropertyResponseAPI } from '../../../../../types';
import debounce from 'lodash/debounce';
interface VendorProfileCardProps { interface VendorProfileCardProps {
vendor: VendorAPI; vendor: VendorAPI;
@@ -32,6 +34,26 @@ interface VendorProfileCardProps {
onSave: (updatedVendor: VendorAPI) => void; 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<AutocompleteResponseAPI>('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<VendorProfileCardProps> = ({ vendor, onUpgrade, onSave }) => { const VendorProfileCard: React.FC<VendorProfileCardProps> = ({ vendor, onUpgrade, onSave }) => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editedVendor, setEditedVendor] = useState<VendorAPI>(vendor); const [editedVendor, setEditedVendor] = useState<VendorAPI>(vendor);
@@ -107,6 +129,11 @@ const VendorProfileCard: React.FC<VendorProfileCardProps> = ({ vendor, onUpgrade
} }
}; };
const debouncedFetch = useMemo(
() => debounce(fetchAutocompleteOptions, 200),
[], // Dependency array ensures the debounced function is created only once
);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
//setNewProperty((prev) => ({ ...prev, [name]: value })); //setNewProperty((prev) => ({ ...prev, [name]: value }));
@@ -119,58 +146,80 @@ const VendorProfileCard: React.FC<VendorProfileCardProps> = ({ vendor, onUpgrade
} }
}; };
const handleAddressAutocompleteInputChange = async ( const handleAddressAutocompleteInputChange = useCallback(
event: React.SyntheticEvent, (event: React.SyntheticEvent, value: string) => {
value: string, // 1. Update the local state immediately for a smooth input experience
) => { setEditedVendor((prev) => ({ ...prev, address: value })); // <--- THIS IS THE CRITICAL LINE
const test: boolean = !import.meta.env.USE_LIVE_DATA;
let data: AutocompleteDataResponseAPI[] = []; // 2. Clear options if the value is too short
if (value.length > 2) { if (value.length < 3) {
if (test) { setAutocompleteOptions([]);
data = test_autocomplete.data.filter((item) => item.address.includes(value)); // Important: Cancel any pending debounced calls
// filter the data here debouncedFetch.cancel();
} else { return;
const { data } = await axiosRealEstateApi.post<AutocompleteDataResponseAPI[]>(
'AutoComplete',
{
search: value,
},
);
} }
setAutocompleteOptions( console.log('attempting the function');
data.map((item) => ({
description: item.address, // 3. Call the debounced function, which will fire the API call
place_id: item.id, // only after 300ms of inactivity.
})), const result = debouncedFetch(value, setAutocompleteOptions);
); console.log(result);
} else { },
console.log('we need more characters'); [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<AutocompleteResponseAPI>('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 ( const handleAddressAutocompleteChange = async (
event: React.SyntheticEvent, event: React.SyntheticEvent,
value: PlacePrediction | null, value: PlacePrediction | null,
) => { ) => {
console.log('here we go', value); let { data } = await axiosInstance.post<PropertyResponseAPI>('property-details-proxy/', {
if (1) { comps: false,
const data = test_autocomplete.data.filter((item) => item.id === value.place_id); id: value.place_id,
if (data.length > 0) { exact_match: true,
const item = data[0]; });
const coordinates = extractLatLon(item.location); data = data.data;
setEditedVendor((prev) => ({ console.log(data);
...prev,
address: item.address, setEditedVendor((prev) => ({
city: item.city, ...prev,
state: item.state, address: data.propertyInfo.address.address,
zip_code: item.zip, street: data.propertyInfo.address.address,
latitude: Number(coordinates.latitude), city: data.propertyInfo.address.city,
longitude: Number(coordinates.longitude), state: data.propertyInfo.address.state,
})); zip_code: data.propertyInfo.address.zip,
} latitude: data.propertyInfo.latitude,
} else { longitude: data.propertyInfo.longitude,
// use the api here }));
}
}; };
return ( return (

View File

@@ -54,7 +54,7 @@ const SignUp = (): ReactElement => {
password2, password2,
}: SignUpValues): Promise<void> => { }: SignUpValues): Promise<void> => {
try { try {
const response = await axiosInstance.post('/register/', { const response = await axiosInstance.post('register/', {
email: email, email: email,
first_name: first_name, first_name: first_name,
last_name: last_name, last_name: last_name,
@@ -62,8 +62,11 @@ const SignUp = (): ReactElement => {
password: password, password: password,
password2: password2, password2: password2,
}); });
console.log(response);
if (response.status == 201) { if (response.status == 201) {
navigate('/authentication/login'); navigate('/authentication/login');
console.log('Good response');
} else { } else {
console.log(`No good: ${response}`); console.log(`No good: ${response}`);
} }

View File

@@ -379,23 +379,23 @@ export interface AutocompleteResponseAPI {
} }
export interface AutocompleteDataResponseAPI { export interface AutocompleteDataResponseAPI {
zip: string; zip: string;
address: string; address?: string;
city: string; city?: string;
searchType: string; searchType: string;
stateId: string; stateId: string;
latitude: number; latitude?: number;
county: string; county?: string;
fips: string; fips?: string;
title: string; title: string;
house: string; house?: string;
unit?: string; unit?: string;
countyId: string; countyId?: string;
street: string; street?: string;
location: string; location?: string;
id: string; id?: string;
state: string; state: string;
apn: string; apn?: string;
longitude: number; longitude?: number;
} }
export interface PropertyResponseDataMortgageAPI { export interface PropertyResponseDataMortgageAPI {