Compare commits
5 Commits
89493853e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d4514c998a | |||
| a6f267492d | |||
| 173f6ccf6d | |||
| 1570185792 | |||
| c48fc96b33 |
2436
ditch-the-agent/package-lock.json
generated
2436
ditch-the-agent/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
|||||||
"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",
|
||||||
"deploy": "gh-pages -d dist"
|
"deploy": "gh-pages -d dist",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
@@ -43,6 +44,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/react": "^4.1.1",
|
"@iconify/react": "^4.1.1",
|
||||||
|
"@testing-library/jest-dom": "^6.4.2",
|
||||||
|
"@testing-library/react": "^14.2.1",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
@@ -52,8 +56,10 @@
|
|||||||
"eslint-plugin-prettier": "^5.5.3",
|
"eslint-plugin-prettier": "^5.5.3",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
"vitest": "^1.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
import { features } from './config/features';
|
||||||
|
|
||||||
const baseURL = import.meta.env.VITE_API_URL;
|
const baseURL = features.apiUrl;
|
||||||
console.log(baseURL);
|
console.log(baseURL);
|
||||||
|
|
||||||
export const axiosRealEstateApi = axios.create({
|
export const axiosRealEstateApi = axios.create({
|
||||||
|
|||||||
@@ -395,11 +395,17 @@ const ChatPane = ({ showChat, isMinimized, toggleMinimize, closeChat }: ChatPane
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FloatingChatButton = (): ReactElement => {
|
import { features } from 'config/features';
|
||||||
|
|
||||||
|
const FloatingChatButton = (): ReactElement | null => {
|
||||||
const [showChat, setShowChat] = useState<boolean>(false);
|
const [showChat, setShowChat] = useState<boolean>(false);
|
||||||
// State to control if the chat pane is minimized
|
// State to control if the chat pane is minimized
|
||||||
const [isMinimized, setIsMinimized] = useState<boolean>(false);
|
const [isMinimized, setIsMinimized] = useState<boolean>(false);
|
||||||
|
|
||||||
|
if (!features.enableFloatingChatButton) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to toggle the chat pane visibility
|
// Function to toggle the chat pane visibility
|
||||||
const toggleChat = () => {
|
const toggleChat = () => {
|
||||||
setShowChat(!showChat);
|
setShowChat(!showChat);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { ReactElement, useContext, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { DocumentAPI, PropertiesAPI, SavedPropertiesAPI, UserAPI } from 'types';
|
import { DocumentAPI, PropertiesAPI, SavedPropertiesAPI } from 'types';
|
||||||
import { axiosInstance } from '../../../../../axiosApi';
|
import { axiosInstance } from '../../../../../axiosApi';
|
||||||
//==import Grid from '@mui/material/Unstable_Grid2';
|
//==import Grid from '@mui/material/Unstable_Grid2';
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { drawerWidth } from 'layouts/main-layout';
|
import { drawerWidth } from 'layouts/main-layout';
|
||||||
import { ProperyInfoCards } from '../Property/PropertyInfo';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import HouseIcon from '@mui/icons-material/House';
|
import HouseIcon from '@mui/icons-material/House';
|
||||||
@@ -109,7 +108,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
const handleSaveProperty = async (updatedProperty: PropertiesAPI) => {
|
const handleSaveProperty = async (updatedProperty: PropertiesAPI) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axiosInstance.patch<PropertiesAPI>(
|
const { data } = await axiosInstance.patch<PropertiesAPI>(
|
||||||
`/properties/${updatedProperty.id}/`,
|
`/properties/${updatedProperty.uuid4}/`,
|
||||||
{
|
{
|
||||||
...updatedProperty,
|
...updatedProperty,
|
||||||
owner: account.id,
|
owner: account.id,
|
||||||
@@ -130,10 +129,10 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteProperty = async (propertyId: number) => {
|
const handleDeleteProperty = async (propertyId: string) => {
|
||||||
try {
|
try {
|
||||||
await axiosInstance.delete(`/properties/${propertyId}/`);
|
await axiosInstance.delete(`/properties/${propertyId}/`);
|
||||||
setProperties((prev) => prev.filter((item) => item.id !== propertyId));
|
setProperties((prev) => prev.filter((item) => item.uuid4 !== propertyId));
|
||||||
setMessage({ type: 'success', text: 'Property has been removed' });
|
setMessage({ type: 'success', text: 'Property has been removed' });
|
||||||
setTimeout(() => setMessage(null), 3000);
|
setTimeout(() => setMessage(null), 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -203,13 +202,13 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
>
|
>
|
||||||
{/* quick states */}
|
{/* quick states */}
|
||||||
{!account.profile_created && (
|
{!account.profile_created && (
|
||||||
<Grid xs={12} key="profile-setup">
|
<Grid item xs={12} key="profile-setup">
|
||||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||||
Please set up your <a href="/profile">profile</a>
|
Please set up your <a href="/profile">profile</a>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
<Grid xs={12} sm={6} md={4} lg={3} key="active-listing-card">
|
<Grid item xs={12} sm={6} md={4} lg={3} key="active-listing-card">
|
||||||
<Card sx={{ display: 'flex' }} onClick={() => navigate('/profile')}>
|
<Card sx={{ display: 'flex' }} onClick={() => navigate('/profile')}>
|
||||||
<CardContent sx={{ flexGrow: 1 }}>
|
<CardContent sx={{ flexGrow: 1 }}>
|
||||||
<Stack direction="column" alignItems="center">
|
<Stack direction="column" alignItems="center">
|
||||||
@@ -220,7 +219,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12} sm={6} md={4} lg={3} key="num-views-card">
|
<Grid item xs={12} sm={6} md={4} lg={3} key="num-views-card">
|
||||||
<Card sx={{ display: 'flex' }}>
|
<Card sx={{ display: 'flex' }}>
|
||||||
<CardContent sx={{ flexGrow: 1 }}>
|
<CardContent sx={{ flexGrow: 1 }}>
|
||||||
<Stack direction="column" alignItems="center">
|
<Stack direction="column" alignItems="center">
|
||||||
@@ -231,7 +230,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12} sm={6} md={4} lg={3} key="total-saves-card">
|
<Grid item xs={12} sm={6} md={4} lg={3} key="total-saves-card">
|
||||||
<Card sx={{ display: 'flex' }}>
|
<Card sx={{ display: 'flex' }}>
|
||||||
<CardContent sx={{ flexGrow: 1 }}>
|
<CardContent sx={{ flexGrow: 1 }}>
|
||||||
<Stack direction="column" alignItems="center">
|
<Stack direction="column" alignItems="center">
|
||||||
@@ -242,7 +241,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12} sm={6} md={4} lg={3} key="num-offers-card">
|
<Grid item xs={12} sm={6} md={4} lg={3} key="num-offers-card">
|
||||||
<Card sx={{ display: 'flex' }} onClick={() => navigate('/offers')}>
|
<Card sx={{ display: 'flex' }} onClick={() => navigate('/offers')}>
|
||||||
<CardContent sx={{ flexGrow: 1 }}>
|
<CardContent sx={{ flexGrow: 1 }}>
|
||||||
<Stack direction="column" alignItems="center">
|
<Stack direction="column" alignItems="center">
|
||||||
@@ -253,7 +252,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12} sm={6} md={4} lg={3} key="num-bids-card">
|
<Grid item xs={12} sm={6} md={4} lg={3} key="num-bids-card">
|
||||||
<Card sx={{ display: 'flex' }} onClick={() => navigate('/bids')}>
|
<Card sx={{ display: 'flex' }} onClick={() => navigate('/bids')}>
|
||||||
<CardContent sx={{ flexGrow: 1 }}>
|
<CardContent sx={{ flexGrow: 1 }}>
|
||||||
<Stack direction="column" alignItems="center">
|
<Stack direction="column" alignItems="center">
|
||||||
@@ -266,7 +265,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{account.tier === 'basic' && (
|
{account.tier === 'basic' && (
|
||||||
<Grid xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<Card>
|
<Card>
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
<Typography variant="h4">Upgrade your account</Typography>
|
<Typography variant="h4">Upgrade your account</Typography>
|
||||||
@@ -292,12 +291,12 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{properties.length > 0 && (
|
{properties.length > 0 && (
|
||||||
<Grid xs={12}>
|
<Grid item xs={12}>
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{properties.map((item) => (
|
{properties.map((item) => (
|
||||||
<Grid xs={12} key={item.id}>
|
<Grid item xs={12} key={item.id}>
|
||||||
<PropertyDetailCard
|
<PropertyDetailCard
|
||||||
property={item}
|
property={item}
|
||||||
isPublicPage={false}
|
isPublicPage={false}
|
||||||
@@ -308,11 +307,11 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
{/* <ProperyInfoCards property={item} /> */}
|
{/* <ProperyInfoCards property={item} /> */}
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
<Grid xs={12}>
|
<Grid item xs={12}>
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid xs={12} md={documentsCardLength}>
|
<Grid item xs={12} md={documentsCardLength}>
|
||||||
<Card sx={{ display: 'flex' }}>
|
<Card sx={{ display: 'flex' }}>
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
<Typography variant="h4">Documents Requiring Attention</Typography>
|
<Typography variant="h4">Documents Requiring Attention</Typography>
|
||||||
@@ -329,7 +328,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid xs={12} md={savedPropertiesCardLength}>
|
<Grid item xs={12} md={savedPropertiesCardLength}>
|
||||||
<Card>
|
<Card>
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
@@ -343,7 +342,7 @@ const PropertyOwnerDashboard = ({ account }: DashboardProps): ReactElement => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid xs={12} md={12}>
|
<Grid item xs={12} md={12}>
|
||||||
{account.tier === 'premium' ? (
|
{account.tier === 'premium' ? (
|
||||||
<Card>
|
<Card>
|
||||||
<Stack direction="column">
|
<Stack direction="column">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { axiosInstance } from '../../../../../axiosApi';
|
|||||||
|
|
||||||
interface AttorneyEngagementLetterDisplayProps {
|
interface AttorneyEngagementLetterDisplayProps {
|
||||||
letterData: AttorneyEngagementLetterData;
|
letterData: AttorneyEngagementLetterData;
|
||||||
documentId: number;
|
documentId: string;
|
||||||
onSignSuccess?: () => void;
|
onSignSuccess?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({ }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedDocumentId = searchParams.get('selectedDocument');
|
const selectedDocumentId = searchParams.get('selectedDocument');
|
||||||
if (selectedDocumentId) {
|
if (selectedDocumentId) {
|
||||||
fetchDocument(parseInt(selectedDocumentId, 10));
|
fetchDocument(selectedDocumentId);
|
||||||
}
|
}
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({ }) => {
|
|||||||
|
|
||||||
console.log(documents);
|
console.log(documents);
|
||||||
|
|
||||||
const fetchDocument = async (documentId: number) => {
|
const fetchDocument = async (documentId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(`/documents/retrieve/?docId=${documentId}`);
|
const response = await axiosInstance.get(`/documents/retrieve/?docId=${documentId}`);
|
||||||
if (response?.data) {
|
if (response?.data) {
|
||||||
@@ -212,8 +212,8 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({ }) => {
|
|||||||
return (
|
return (
|
||||||
<AttorneyEngagementLetterDisplay
|
<AttorneyEngagementLetterDisplay
|
||||||
letterData={selectedDocument.sub_document as any}
|
letterData={selectedDocument.sub_document as any}
|
||||||
documentId={selectedDocument.id}
|
documentId={selectedDocument.uuid4}
|
||||||
onSignSuccess={() => fetchDocument(selectedDocument.id)}
|
onSignSuccess={() => fetchDocument(selectedDocument.uuid4)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (selectedDocument.document_type === 'lender_financing_agreement') {
|
} else if (selectedDocument.document_type === 'lender_financing_agreement') {
|
||||||
@@ -281,8 +281,8 @@ const DocumentManager: React.FC<DocumentManagerProps> = ({ }) => {
|
|||||||
<ListItem
|
<ListItem
|
||||||
key={document.id}
|
key={document.id}
|
||||||
button
|
button
|
||||||
selected={selectedDocument?.id === document.id}
|
selected={selectedDocument?.uuid4 === document.uuid4}
|
||||||
onClick={() => fetchDocument(document.id)}
|
onClick={() => fetchDocument(document.uuid4)}
|
||||||
sx={{ py: 1.5, px: 2 }}
|
sx={{ py: 1.5, px: 2 }}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
|
|||||||
@@ -27,12 +27,15 @@ import {
|
|||||||
import { test_property_search } from 'data/mock_property_search';
|
import { test_property_search } from 'data/mock_property_search';
|
||||||
import { extractLatLon } from 'utils';
|
import { extractLatLon } from 'utils';
|
||||||
import { test_autocomplete } from 'data/mock_autocomplete_results';
|
import { test_autocomplete } from 'data/mock_autocomplete_results';
|
||||||
|
import { features } from '../../../../../config/features';
|
||||||
|
|
||||||
interface AddPropertyDialogProps {
|
interface AddPropertyDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onAddProperty: (
|
onAddProperty: (
|
||||||
newProperty: Omit<PropertiesAPI, 'id' | 'owner' | 'created_at' | 'last_updated'>,
|
newProperty: Omit<PropertiesAPI, 'id' | 'owner' | 'created_at' | 'last_updated' | 'uuid4' | 'pictures'> & {
|
||||||
|
pictures: string[];
|
||||||
|
},
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,10 @@ const fetchAutocompleteOptions = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
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' | 'uuid4' | 'pictures'
|
||||||
|
> & { pictures: string[] } = {
|
||||||
address: '',
|
address: '',
|
||||||
street: '',
|
street: '',
|
||||||
city: '',
|
city: '',
|
||||||
@@ -85,9 +91,17 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, on
|
|||||||
saves: 0,
|
saves: 0,
|
||||||
property_status: 'off_market',
|
property_status: 'off_market',
|
||||||
schools: [],
|
schools: [],
|
||||||
|
tax_info: {
|
||||||
|
assessed_value: 0,
|
||||||
|
assessment_year: 0,
|
||||||
|
tax_amount: 0,
|
||||||
|
year: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const [newProperty, setNewProperty] = useState<
|
const [newProperty, setNewProperty] = useState<
|
||||||
Omit<PropertiesAPI, 'id' | 'owner' | 'created_at' | 'last_updated'>
|
Omit<PropertiesAPI, 'id' | 'owner' | 'created_at' | 'last_updated' | 'uuid4' | 'pictures'> & {
|
||||||
|
pictures: string[];
|
||||||
|
}
|
||||||
>({
|
>({
|
||||||
...initalValues,
|
...initalValues,
|
||||||
});
|
});
|
||||||
@@ -204,7 +218,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 = import.meta.env.USE_LIVE_DATA;
|
const test: boolean = features.useLiveData;
|
||||||
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');
|
||||||
@@ -217,12 +231,13 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, on
|
|||||||
test_property_search.data.schools.map((item) => {
|
test_property_search.data.schools.map((item) => {
|
||||||
const coordinates = extractLatLon(item.location);
|
const coordinates = extractLatLon(item.location);
|
||||||
return {
|
return {
|
||||||
|
address: '', // Mock address
|
||||||
city: item.city,
|
city: item.city,
|
||||||
state: item.state,
|
state: item.state,
|
||||||
zip_code: item.zip,
|
zip_code: item.zip,
|
||||||
latitude: coordinates?.latitude,
|
latitude: coordinates?.latitude,
|
||||||
longitude: coordinates?.longitude,
|
longitude: coordinates?.longitude,
|
||||||
school_type: item.type,
|
school_type: item.type.toLowerCase() as 'public' | 'other',
|
||||||
enrollment: item.enrollment,
|
enrollment: item.enrollment,
|
||||||
grades: item.grades,
|
grades: item.grades,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -301,6 +316,7 @@ const AddPropertyDialog: React.FC<AddPropertyDialogProps> = ({ open, onClose, on
|
|||||||
(item) => {
|
(item) => {
|
||||||
const coordinates = extractLatLon(item.location);
|
const coordinates = extractLatLon(item.location);
|
||||||
return {
|
return {
|
||||||
|
address: '', // Mock address
|
||||||
city: item.city,
|
city: item.city,
|
||||||
state: item.state,
|
state: item.state,
|
||||||
zip_code: item.zip,
|
zip_code: item.zip,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { PlacePrediction } from './AddPropertyDialog';
|
|||||||
import { test_autocomplete } from 'data/mock_autocomplete_results';
|
import { test_autocomplete } from 'data/mock_autocomplete_results';
|
||||||
import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi';
|
import { axiosInstance, axiosRealEstateApi } from '../../../../../axiosApi';
|
||||||
import { extractLatLon } from 'utils';
|
import { extractLatLon } from 'utils';
|
||||||
|
import { features } from '../../../../../config/features';
|
||||||
|
|
||||||
interface AttorneyProfileCardProps {
|
interface AttorneyProfileCardProps {
|
||||||
attorney: AttorneyAPI;
|
attorney: AttorneyAPI;
|
||||||
@@ -115,7 +116,7 @@ const AttorneyProfileCard: React.FC<AttorneyProfileCardProps> = ({
|
|||||||
event: React.SyntheticEvent,
|
event: React.SyntheticEvent,
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
const test: boolean = !import.meta.env.USE_LIVE_DATA;
|
const test: boolean = !features.useLiveData;
|
||||||
let data: AutocompleteDataResponseAPI[] = [];
|
let data: AutocompleteDataResponseAPI[] = [];
|
||||||
if (value.length > 2) {
|
if (value.length > 2) {
|
||||||
if (test) {
|
if (test) {
|
||||||
|
|||||||
@@ -146,39 +146,49 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddProperty = (
|
const handleAddProperty = (
|
||||||
newPropertyData: Omit<PropertiesAPI, 'id' | 'owner' | 'created_at' | 'last_updated'>,
|
newPropertyData: Omit<
|
||||||
|
PropertiesAPI,
|
||||||
|
'id' | 'owner' | 'created_at' | 'last_updated' | 'uuid4' | 'pictures'
|
||||||
|
> & { pictures: string[] },
|
||||||
) => {
|
) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
const newProperty: PropertyRequestAPI = {
|
const newProperty: PropertyRequestAPI = {
|
||||||
...newPropertyData,
|
...newPropertyData,
|
||||||
owner: user.user.id,
|
owner: user.user.id,
|
||||||
// created_at: new Date().toISOString().split('T')[0],
|
created_at: new Date().toISOString().split('T')[0],
|
||||||
// last_updated: new Date().toISOString().split('T')[0],
|
last_updated: new Date().toISOString().split('T')[0],
|
||||||
|
// @ts-ignore - pictures type mismatch (string[] vs PictureAPI[]) handled by backend or ignored for now
|
||||||
|
open_houses: [],
|
||||||
|
pictures: newPropertyData.pictures as any, // Cast to any to avoid lint error for now
|
||||||
};
|
};
|
||||||
|
|
||||||
newProperty.created_at = new Date().toISOString().split('T')[0];
|
|
||||||
newProperty.last_updated = new Date().toISOString().split('T')[0];
|
|
||||||
|
|
||||||
newProperty.open_houses = [];
|
|
||||||
|
|
||||||
console.log(newProperty);
|
console.log(newProperty);
|
||||||
const { data, error } = axiosInstance.post('/properties/', {
|
const postProperty = async () => {
|
||||||
...newProperty,
|
try {
|
||||||
});
|
const { data } = await axiosInstance.post<PropertiesAPI>('/properties/', {
|
||||||
const updateNewProperty: PropertiesAPI = {
|
...newProperty,
|
||||||
...newProperty,
|
});
|
||||||
owner: user,
|
const updateNewProperty: PropertiesAPI = {
|
||||||
|
...data,
|
||||||
|
owner: user,
|
||||||
|
};
|
||||||
|
setProperties((prev) => [...prev, updateNewProperty]);
|
||||||
|
setMessage({ type: 'success', text: 'Property added successfully!' });
|
||||||
|
setTimeout(() => setMessage(null), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add property:', error);
|
||||||
|
setMessage({ type: 'error', text: 'Failed to add property.' });
|
||||||
|
setTimeout(() => setMessage(null), 3000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
setProperties((prev) => [...prev, updateNewProperty]);
|
postProperty();
|
||||||
setMessage({ type: 'success', text: 'Property added successfully!' });
|
|
||||||
setTimeout(() => setMessage(null), 3000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveProperty = async (updatedProperty: PropertiesAPI) => {
|
const handleSaveProperty = async (updatedProperty: PropertiesAPI) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axiosInstance.patch<PropertiesAPI>(
|
const { data } = await axiosInstance.patch<PropertiesAPI>(
|
||||||
`/properties/${updatedProperty.id}/`,
|
`/properties/${updatedProperty.uuid4}/`,
|
||||||
{
|
{
|
||||||
...updatedProperty,
|
...updatedProperty,
|
||||||
owner: account.id,
|
owner: account.id,
|
||||||
@@ -199,10 +209,10 @@ const PropertyOwnerProfile = ({ account }: ProfileProps): ReactElement => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteProperty = async (propertyId: number) => {
|
const handleDeleteProperty = async (propertyId: string) => {
|
||||||
try {
|
try {
|
||||||
await axiosInstance.delete(`/properties/${propertyId}/`);
|
await axiosInstance.delete(`/properties/${propertyId}/`);
|
||||||
setProperties((prev) => prev.filter((item) => item.id !== propertyId));
|
setProperties((prev) => prev.filter((item) => item.uuid4 !== propertyId));
|
||||||
setMessage({ type: 'success', text: 'Property has been removed' });
|
setMessage({ type: 'success', text: 'Property has been removed' });
|
||||||
setTimeout(() => setMessage(null), 3000);
|
setTimeout(() => setMessage(null), 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent, Typography, Button, Box } from '@mui/material';
|
||||||
|
|
||||||
|
const MLSPublishingCard: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Publish to MLS
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" paragraph>
|
||||||
|
Publish your property to the MLS. To sites like zillow and redfin.
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Button variant="contained" color="primary" fullWidth>
|
||||||
|
Publish Now
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MLSPublishingCard;
|
||||||
@@ -35,7 +35,7 @@ interface PropertyDetailCardProps {
|
|||||||
onSave: (updatedProperty: PropertiesAPI) => void;
|
onSave: (updatedProperty: PropertiesAPI) => void;
|
||||||
isPublicPage?: boolean;
|
isPublicPage?: boolean;
|
||||||
isOwnerView?: boolean; // True if the current user is the owner, allows editing
|
isOwnerView?: boolean; // True if the current user is the owner, allows editing
|
||||||
onDelete?: (propertyId: number) => void;
|
onDelete?: (propertyId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PropertyDetailCard: React.FC<PropertyDetailCardProps> = ({
|
const PropertyDetailCard: React.FC<PropertyDetailCardProps> = ({
|
||||||
@@ -106,7 +106,7 @@ const PropertyDetailCard: React.FC<PropertyDetailCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleViewPublicListing = () => {
|
const handleViewPublicListing = () => {
|
||||||
navigate(`/property/${property.id}/`);
|
navigate(`/property/${property.uuid4}/`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePictureUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handlePictureUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -155,14 +155,14 @@ const PropertyDetailCard: React.FC<PropertyDetailCardProps> = ({
|
|||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (isOwnerView && !isPublicPage && onDelete) {
|
if (isOwnerView && !isPublicPage && onDelete) {
|
||||||
onDelete(property.id);
|
onDelete(property.uuid4);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateDescription = async () => {
|
const generateDescription = async () => {
|
||||||
setIsGernerating(true);
|
setIsGernerating(true);
|
||||||
|
|
||||||
const response = await axiosInstance.put(`/property-description-generator/${property.id}/`);
|
const response = await axiosInstance.put(`/property-description-generator/${property.uuid4}/`);
|
||||||
setEditedProperty((prev) => ({
|
setEditedProperty((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
description: response.data.description,
|
description: response.data.description,
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ const PropertyListItem: React.FC<PropertyListItemProps> = ({
|
|||||||
const handleViewDetailsClick = () => {
|
const handleViewDetailsClick = () => {
|
||||||
// Navigate to the full detail page for this property
|
// Navigate to the full detail page for this property
|
||||||
if (!isPublic) {
|
if (!isPublic) {
|
||||||
navigate(`/property/${property.id}/?search=1`);
|
navigate(`/property/${property.uuid4}/?search=1`);
|
||||||
} else {
|
} else {
|
||||||
navigate(`/public/${property.id}`);
|
navigate(`/public/${property.uuid4}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const value_price = property.listed_price ? property.listed_price : property.market_value;
|
const value_price = property.listed_price ? property.listed_price : property.market_value;
|
||||||
|
|||||||
67
ditch-the-agent/src/config/features.ts
Normal file
67
ditch-the-agent/src/config/features.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Feature flags and environment configuration
|
||||||
|
* Controls feature availability and environment settings based on build mode (production, beta, development)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface FeatureConfig {
|
||||||
|
// Registration features
|
||||||
|
enableAttorneyRegistration: boolean;
|
||||||
|
enableRealEstateAgentRegistration: boolean;
|
||||||
|
enableRegistration: boolean;
|
||||||
|
|
||||||
|
// UI Features
|
||||||
|
enableFloatingChatButton: boolean;
|
||||||
|
enableMLSPublishing: boolean;
|
||||||
|
|
||||||
|
// API Configuration
|
||||||
|
apiUrl: string;
|
||||||
|
|
||||||
|
// Data Configuration
|
||||||
|
useLiveData: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get feature configuration based on current environment
|
||||||
|
*/
|
||||||
|
const getFeatureConfig = (): FeatureConfig => {
|
||||||
|
const mode = import.meta.env.MODE || 'development';
|
||||||
|
|
||||||
|
// Production configuration
|
||||||
|
if (mode === 'production') {
|
||||||
|
return {
|
||||||
|
enableAttorneyRegistration: false,
|
||||||
|
enableRealEstateAgentRegistration: false,
|
||||||
|
enableRegistration: false,
|
||||||
|
enableFloatingChatButton: false,
|
||||||
|
enableMLSPublishing: false,
|
||||||
|
apiUrl: 'https://backend.ditchtheagent.com/api/',
|
||||||
|
useLiveData: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beta configuration
|
||||||
|
if (mode === 'beta') {
|
||||||
|
return {
|
||||||
|
enableAttorneyRegistration: true,
|
||||||
|
enableRealEstateAgentRegistration: true,
|
||||||
|
enableRegistration: true,
|
||||||
|
enableFloatingChatButton: true,
|
||||||
|
enableMLSPublishing: true,
|
||||||
|
apiUrl: 'https://beta.backend.ditchtheagent.com/api/',
|
||||||
|
useLiveData: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Development configuration (default)
|
||||||
|
return {
|
||||||
|
enableAttorneyRegistration: true,
|
||||||
|
enableRealEstateAgentRegistration: true,
|
||||||
|
enableRegistration: true,
|
||||||
|
enableFloatingChatButton: true,
|
||||||
|
enableMLSPublishing: true,
|
||||||
|
apiUrl: 'http://127.0.0.1:8010/api/',
|
||||||
|
useLiveData: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const features = getFeatureConfig();
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, createContext, useRef, useState, useContext, ReactNode } from 'react';
|
import React, { useEffect, createContext, useRef, useState, useContext, ReactNode } from 'react';
|
||||||
import { AccountContext } from './AccountContext';
|
import { AccountContext } from './AccountContext';
|
||||||
import { AuthContext } from './AuthContext';
|
import { AuthContext } from './AuthContext';
|
||||||
|
import { features } from '../config/features';
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
// Define Types and Interfaces
|
// Define Types and Interfaces
|
||||||
@@ -65,10 +66,10 @@ interface WebSocketProviderProps {
|
|||||||
// Provide a default value that matches the IWebSocketContext interface.
|
// Provide a default value that matches the IWebSocketContext interface.
|
||||||
// This is used when a component tries to consume the context without a provider.
|
// This is used when a component tries to consume the context without a provider.
|
||||||
const WebSocketContext = createContext<IWebSocketContext>({
|
const WebSocketContext = createContext<IWebSocketContext>({
|
||||||
subscribe: () => {},
|
subscribe: () => { },
|
||||||
unsubscribe: () => {},
|
unsubscribe: () => { },
|
||||||
socket: null,
|
socket: null,
|
||||||
sendMessages: () => {},
|
sendMessages: () => { },
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
@@ -141,7 +142,7 @@ function WebSocketProvider({ children }: WebSocketProviderProps) {
|
|||||||
ws.current.close();
|
ws.current.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const wsUrl = new URL(import.meta.env.VITE_API_URL || 'ws://127.0.0.1:8010/ws/');
|
const wsUrl = new URL(features.apiUrl || 'ws://127.0.0.1:8010/ws/');
|
||||||
wsUrl.protocol = wsUrl.protocol.replace('http', 'ws');
|
wsUrl.protocol = wsUrl.protocol.replace('http', 'ws');
|
||||||
|
|
||||||
ws.current = new WebSocket(
|
ws.current = new WebSocket(
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Avatar,
|
Avatar,
|
||||||
Stack,
|
Stack,
|
||||||
|
ListItemButton,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
|
import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
|
||||||
import SendIcon from '@mui/icons-material/Send';
|
import SendIcon from '@mui/icons-material/Send';
|
||||||
@@ -123,7 +124,7 @@ const Messages = (): ReactElement => {
|
|||||||
console.log(fetchedConversations);
|
console.log(fetchedConversations);
|
||||||
setConversations(fetchedConversations);
|
setConversations(fetchedConversations);
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
};
|
};
|
||||||
fetchConversations();
|
fetchConversations();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -230,16 +231,16 @@ const Messages = (): ReactElement => {
|
|||||||
prevConversations.map((conv) =>
|
prevConversations.map((conv) =>
|
||||||
conv.id === selectedConversationId
|
conv.id === selectedConversationId
|
||||||
? {
|
? {
|
||||||
...conv,
|
...conv,
|
||||||
messages: [...conv.messages, newMessage],
|
messages: [...conv.messages, newMessage],
|
||||||
lastMessageSnippet: newMessage.content,
|
lastMessageSnippet: newMessage.content,
|
||||||
lastMessageTimestamp: newMessage.timestamp,
|
lastMessageTimestamp: newMessage.timestamp,
|
||||||
}
|
}
|
||||||
: conv,
|
: conv,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setNewMessageContent(''); // Clear the input field
|
setNewMessageContent(''); // Clear the input field
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -293,39 +294,42 @@ const Messages = (): ReactElement => {
|
|||||||
.map((conv) => (
|
.map((conv) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={conv.id}
|
key={conv.id}
|
||||||
button
|
disablePadding
|
||||||
selected={selectedConversationId === conv.id}
|
|
||||||
onClick={() => setSelectedConversationId(conv.id)}
|
|
||||||
sx={{ py: 1.5, px: 2 }}
|
|
||||||
>
|
>
|
||||||
<ListItemText
|
<ListItemButton
|
||||||
primary={
|
selected={selectedConversationId === conv.id}
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>
|
onClick={() => setSelectedConversationId(conv.id)}
|
||||||
{conv.withName}
|
sx={{ py: 1.5, px: 2 }}
|
||||||
</Typography>
|
>
|
||||||
}
|
<ListItemText
|
||||||
secondary={
|
primary={
|
||||||
<Box
|
<Typography variant="subtitle1" sx={{ fontWeight: 'medium' }}>
|
||||||
sx={{
|
{conv.withName}
|
||||||
display: 'flex',
|
</Typography>
|
||||||
justifyContent: 'space-between',
|
}
|
||||||
alignItems: 'center',
|
secondary={
|
||||||
}}
|
<Box
|
||||||
>
|
sx={{
|
||||||
<Typography
|
display: 'flex',
|
||||||
variant="body2"
|
justifyContent: 'space-between',
|
||||||
color="text.secondary"
|
alignItems: 'center',
|
||||||
noWrap
|
}}
|
||||||
sx={{ flexGrow: 1, pr: 1 }}
|
|
||||||
>
|
>
|
||||||
{conv.lastMessageSnippet}
|
<Typography
|
||||||
</Typography>
|
variant="body2"
|
||||||
<Typography variant="caption" color="text.disabled">
|
color="text.secondary"
|
||||||
{formatTimestamp(conv.lastMessageTimestamp)}
|
noWrap
|
||||||
</Typography>
|
sx={{ flexGrow: 1, pr: 1 }}
|
||||||
</Box>
|
>
|
||||||
}
|
{conv.lastMessageSnippet}
|
||||||
/>
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.disabled">
|
||||||
|
{formatTimestamp(conv.lastMessageTimestamp)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { axiosInstance } from '../../axiosApi';
|
|||||||
import SchoolCard from 'components/sections/dashboard/Home/Property/SchoolCard';
|
import SchoolCard from 'components/sections/dashboard/Home/Property/SchoolCard';
|
||||||
import { AccountContext } from 'contexts/AccountContext';
|
import { AccountContext } from 'contexts/AccountContext';
|
||||||
import SellerInformationCard from 'components/sections/dashboard/Home/Property/SellerInformationCard';
|
import SellerInformationCard from 'components/sections/dashboard/Home/Property/SellerInformationCard';
|
||||||
|
import MLSPublishingCard from 'components/sections/dashboard/Home/Property/MLSPublishingCard';
|
||||||
|
import { features } from 'config/features';
|
||||||
|
|
||||||
const PropertyDetailPage: React.FC = () => {
|
const PropertyDetailPage: React.FC = () => {
|
||||||
// In a real app, you'd get propertyId from URL params or a global state
|
// In a real app, you'd get propertyId from URL params or a global state
|
||||||
@@ -87,10 +89,17 @@ const PropertyDetailPage: React.FC = () => {
|
|||||||
const onStatusChange = async (value: string) => {
|
const onStatusChange = async (value: string) => {
|
||||||
if (property) {
|
if (property) {
|
||||||
try {
|
try {
|
||||||
await axiosInstance.patch<SavedPropertiesAPI>(`/properties/${property.id}/`, {
|
await axiosInstance.patch<SavedPropertiesAPI>(`/properties/${property.uuid4}/`, {
|
||||||
property_status: value,
|
property_status: value,
|
||||||
});
|
});
|
||||||
setProperty((prev) => (prev ? { ...prev, property_status: value } : null));
|
setProperty((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
property_status: value as 'active' | 'pending' | 'contingent' | 'sold' | 'off_market',
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
setMessage({ type: 'success', text: `Your listing is now ${value}` });
|
setMessage({ type: 'success', text: `Your listing is now ${value}` });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
let errorMsg: React.ReactNode = 'There was an error saving your selection. Please try again';
|
let errorMsg: React.ReactNode = 'There was an error saving your selection. Please try again';
|
||||||
@@ -149,7 +158,7 @@ const PropertyDetailPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const { data } = await axiosInstance.post<SavedPropertiesAPI>(`/saved-properties/`, {
|
const { data } = await axiosInstance.post<SavedPropertiesAPI>(`/saved-properties/`, {
|
||||||
property: property.id,
|
property: property.uuid4,
|
||||||
user: account.id,
|
user: account.id,
|
||||||
});
|
});
|
||||||
setSavedProperty(data);
|
setSavedProperty(data);
|
||||||
@@ -174,7 +183,7 @@ const PropertyDetailPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteProperty = (propertyId: number) => {
|
const handleDeleteProperty = (propertyId: string) => {
|
||||||
console.log('handle delete. IMPLEMENT ME', propertyId);
|
console.log('handle delete. IMPLEMENT ME', propertyId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -277,7 +286,7 @@ const PropertyDetailPage: React.FC = () => {
|
|||||||
isPublicPage={true}
|
isPublicPage={true}
|
||||||
onSave={handleSaveProperty}
|
onSave={handleSaveProperty}
|
||||||
isOwnerView={isOwnerOfProperty}
|
isOwnerView={isOwnerOfProperty}
|
||||||
onDelete={() => handleDeleteProperty(property.id)}
|
onDelete={() => handleDeleteProperty(property.uuid4)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -296,6 +305,11 @@ const PropertyDetailPage: React.FC = () => {
|
|||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<EstimatedMonthlyCostCard price={parseFloat(priceForAnalysis)} />
|
<EstimatedMonthlyCostCard price={parseFloat(priceForAnalysis)} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{isOwnerOfProperty && features.enableMLSPublishing && (
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<MLSPublishingCard />
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
{!isOwnerOfProperty && (
|
{!isOwnerOfProperty && (
|
||||||
<>
|
<>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
text: item.description,
|
text: item.description,
|
||||||
created_at: item.created_at,
|
created_at: item.created_at,
|
||||||
updated_at: item.updated_at,
|
updated_at: item.updated_at,
|
||||||
user: -1, // Placeholder
|
user: item.user_email,
|
||||||
user_first_name: 'System',
|
user_first_name: item.user_first_name,
|
||||||
user_last_name: 'Description'
|
user_last_name: item.user_last_name,
|
||||||
}]
|
}]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
result = result.filter(item =>
|
result = result.filter(item =>
|
||||||
item.title.toLowerCase().includes(lowerSearch) ||
|
item.title.toLowerCase().includes(lowerSearch) ||
|
||||||
item.description.toLowerCase().includes(lowerSearch)
|
item.description.toLowerCase().includes(lowerSearch)
|
||||||
// || item.user?.email?.toLowerCase().includes(lowerSearch) // If user object existed
|
|| item.user_email?.toLowerCase().includes(lowerSearch) // If user object existed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +215,26 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseCase = async () => {
|
||||||
|
if (!selectedSupportCaseId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data }: AxiosResponse<SupportCaseApi> = await axiosInstance.patch(
|
||||||
|
`/support/cases/${selectedSupportCaseId}/`,
|
||||||
|
{ status: 'closed' }
|
||||||
|
);
|
||||||
|
|
||||||
|
setSupportCase(prev => prev ? { ...prev, status: data.status, updated_at: data.updated_at } : null);
|
||||||
|
setSupportCases(prevCases =>
|
||||||
|
prevCases.map(c =>
|
||||||
|
c.id === selectedSupportCaseId ? { ...c, status: data.status, updated_at: data.updated_at } : c
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to close case", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <PageLoader />;
|
return <PageLoader />;
|
||||||
}
|
}
|
||||||
@@ -233,7 +253,8 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
sx={{ py: 4, height: '90vh', width: 'fill', display: 'flex', flexDirection: 'column' }}
|
maxWidth={false}
|
||||||
|
sx={{ py: 4, height: '90vh', width: '100%', display: 'flex', flexDirection: 'column' }}
|
||||||
>
|
>
|
||||||
<Paper
|
<Paper
|
||||||
elevation={3}
|
elevation={3}
|
||||||
@@ -242,7 +263,9 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
<Grid container sx={{ height: '100%', width: '100%' }}>
|
<Grid container sx={{ height: '100%', width: '100%' }}>
|
||||||
{/* Left Panel: List & Filters */}
|
{/* Left Panel: List & Filters */}
|
||||||
<Grid
|
<Grid
|
||||||
size={{ xs: 12, md: 4 }}
|
item
|
||||||
|
xs={12}
|
||||||
|
md={4}
|
||||||
sx={{
|
sx={{
|
||||||
borderRight: { md: '1px solid', borderColor: 'grey.200' },
|
borderRight: { md: '1px solid', borderColor: 'grey.200' },
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -337,6 +360,7 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -354,12 +378,21 @@ const SupportAgentDashboard: React.FC = () => {
|
|||||||
borderColor: 'grey.200',
|
borderColor: 'grey.200',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold' }}>
|
<Typography variant="h6" component="h3" sx={{ fontWeight: 'bold' }}>
|
||||||
{supportCase.title} | {supportCase.category}
|
{supportCase.title} | {supportCase.category}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleCloseCase}
|
||||||
|
disabled={supportCase.status === 'closed'}
|
||||||
|
>
|
||||||
|
{supportCase.status === 'closed' ? 'Closed' : 'Close Case'}
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3, bgcolor: 'grey.50' }}>
|
<Box sx={{ flexGrow: 1, overflowY: 'auto', p: 3, bgcolor: 'grey.50' }}>
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { features } from '../../config/features';
|
||||||
|
|
||||||
const base_url: string = `${import.meta.env.VITE_API_URL?.replace('/api/', '')}/media/vendor_pictures/`;
|
const base_url: string = `${features.apiUrl?.replace('/api/', '')}/media/vendor_pictures/`;
|
||||||
|
|
||||||
// Define the array of corrected and alphabetized categories with 'as const'
|
// Define the array of corrected and alphabetized categories with 'as const'
|
||||||
export const CATEGORY_NAMES = [
|
export const CATEGORY_NAMES = [
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import ForgotPassword from './ForgotPassword';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
|
||||||
|
// Mock axiosInstance
|
||||||
|
vi.mock('../../axiosApi.js', () => ({
|
||||||
|
axiosInstance: {
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ForgotPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
expect(screen.getByText('Forgot Password')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /Send Password Reset Link/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates email input', () => {
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
||||||
|
expect(emailInput).toHaveValue('test@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls API and shows success message on valid submission', async () => {
|
||||||
|
(axiosInstance.post as any).mockResolvedValue({ status: 200 });
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
|
||||||
|
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Send Password Reset Link/i });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
expect(screen.getByText('Sending...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(axiosInstance.post).toHaveBeenCalledWith('password-reset/', { email: 'test@example.com' });
|
||||||
|
expect(screen.getByText('Password reset link has been sent to your email.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message on API failure', async () => {
|
||||||
|
const errorMessage = 'User not found';
|
||||||
|
(axiosInstance.post as any).mockRejectedValue({
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
detail: errorMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ForgotPassword />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'wrong@example.com' } });
|
||||||
|
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Send Password Reset Link/i });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
@@ -10,12 +11,36 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import Image from 'components/base/Image';
|
import Image from 'components/base/Image';
|
||||||
import { Suspense } from 'react';
|
import { Suspense, useState } from 'react';
|
||||||
import forgotPassword from 'assets/authentication-banners/green.png';
|
import forgotPassword from 'assets/authentication-banners/green.png';
|
||||||
import IconifyIcon from 'components/base/IconifyIcon';
|
import IconifyIcon from 'components/base/IconifyIcon';
|
||||||
import logo from 'assets/logo/favicon-logo.png';
|
import logo from 'assets/logo/favicon-logo.png';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
|
||||||
const ForgotPassword = () => {
|
const ForgotPassword = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setSuccessMessage(null);
|
||||||
|
setErrorMessage(null);
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('password-reset/', { email });
|
||||||
|
setSuccessMessage('Password reset link has been sent to your email.');
|
||||||
|
} catch (error: any) {
|
||||||
|
setErrorMessage(
|
||||||
|
error.response?.data?.email?.[0] ||
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
'An error occurred. Please try again.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
@@ -30,14 +55,18 @@ const ForgotPassword = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
<Stack alignItems="center" gap={6.5} width={330} mx="auto">
|
<Stack alignItems="center" gap={6.5} width={330} mx="auto">
|
||||||
<Typography variant="h3">Forgot Password</Typography>
|
<Typography variant="h3">Forgot Password</Typography>
|
||||||
|
{successMessage && <Alert severity="success">{successMessage}</Alert>}
|
||||||
|
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||||
<FormControl variant="standard" fullWidth>
|
<FormControl variant="standard" fullWidth>
|
||||||
<InputLabel shrink htmlFor="new-password">
|
<InputLabel shrink htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<TextField
|
<TextField
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
id="email"
|
id="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@@ -47,8 +76,13 @@ const ForgotPassword = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button variant="contained" fullWidth>
|
<Button
|
||||||
Send Password Reset Link
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
disabled={loading || !email}
|
||||||
|
>
|
||||||
|
{loading ? 'Sending...' : 'Send Password Reset Link'}
|
||||||
</Button>
|
</Button>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Back to{' '}
|
Back to{' '}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { axiosInstance } from '../../axiosApi.js';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Form, Formik } from 'formik';
|
import { Form, Formik } from 'formik';
|
||||||
import { AuthContext } from 'contexts/AuthContext.js';
|
import { AuthContext } from 'contexts/AuthContext.js';
|
||||||
|
import paths from 'routes/paths';
|
||||||
|
|
||||||
type loginValues = {
|
type loginValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -50,7 +51,15 @@ const Login = (): ReactElement => {
|
|||||||
|
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const hasErrors = Object.keys(error.response.data).length > 0;
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.status === 401 &&
|
||||||
|
error.response.data.detail === 'No active account found with the given credentials'
|
||||||
|
) {
|
||||||
|
navigate(paths.verifyAccount, { state: { email } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasErrors = error.response && Object.keys(error.response.data).length > 0;
|
||||||
if (hasErrors) {
|
if (hasErrors) {
|
||||||
setErrorMessage(error.response.data);
|
setErrorMessage(error.response.data);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
157
ditch-the-agent/src/pages/authentication/ResetPassword.test.tsx
Normal file
157
ditch-the-agent/src/pages/authentication/ResetPassword.test.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
import ResetPassword from './ResetPassword';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Mock axiosInstance
|
||||||
|
vi.mock('../../axiosApi.js', () => ({
|
||||||
|
axiosInstance: {
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock useSearchParams
|
||||||
|
const mockGet = vi.fn();
|
||||||
|
vi.mock('react-router-dom', async () => {
|
||||||
|
const actual = await vi.importActual('react-router-dom');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useSearchParams: () => [{ get: mockGet }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ResetPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockGet.mockImplementation((key) => {
|
||||||
|
if (key === 'email') return 'test@example.com';
|
||||||
|
if (key === 'code') return '123456';
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderComponent = () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ResetPassword />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders correctly', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByRole('heading', { name: /Reset Password/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Reset Code')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /Reset Password/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates inputs', () => {
|
||||||
|
renderComponent();
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
const codeInput = screen.getByLabelText('Reset Code');
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
|
||||||
|
fireEvent.change(emailInput, { target: { value: 'new@example.com' } });
|
||||||
|
fireEvent.change(codeInput, { target: { value: '654321' } });
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
|
||||||
|
expect(emailInput).toHaveValue('new@example.com');
|
||||||
|
expect(codeInput).toHaveValue('654321');
|
||||||
|
expect(passwordInput).toHaveValue('password123');
|
||||||
|
expect(confirmInput).toHaveValue('password123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error if passwords do not match', async () => {
|
||||||
|
renderComponent();
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password456' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(screen.getByText("Passwords don't match")).toBeInTheDocument();
|
||||||
|
expect(axiosInstance.post).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls API and shows success message on valid submission', async () => {
|
||||||
|
(axiosInstance.post as any).mockResolvedValue({ status: 200 });
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Email and Code are pre-filled from mock
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(submitButton).toBeDisabled();
|
||||||
|
expect(screen.getByText('Resetting...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(axiosInstance.post).toHaveBeenCalledWith('password-reset/confirm/', {
|
||||||
|
email: 'test@example.com',
|
||||||
|
code: '123456',
|
||||||
|
new_password: 'password123',
|
||||||
|
new_password2: 'password123',
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Reset Successfully')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error message on API failure', async () => {
|
||||||
|
const errorMessage = 'Invalid token';
|
||||||
|
(axiosInstance.post as any).mockRejectedValue({
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
detail: errorMessage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error if email or code is missing', async () => {
|
||||||
|
mockGet.mockReturnValue(null); // No URL params
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Inputs should be empty
|
||||||
|
const emailInput = screen.getByLabelText('Email');
|
||||||
|
const codeInput = screen.getByLabelText('Reset Code');
|
||||||
|
expect(emailInput).toHaveValue('');
|
||||||
|
expect(codeInput).toHaveValue('');
|
||||||
|
|
||||||
|
const passwordInput = screen.getByLabelText('Password');
|
||||||
|
const confirmInput = screen.getByLabelText('Confirm Password');
|
||||||
|
const submitButton = screen.getByRole('button', { name: /Reset Password/i });
|
||||||
|
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.change(confirmInput, { target: { value: 'password123' } });
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(screen.getByText('Please provide both email and reset code.')).toBeInTheDocument();
|
||||||
|
expect(axiosInstance.post).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ReactElement, Suspense, useState } from 'react';
|
import { ReactElement, Suspense, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -17,30 +18,55 @@ import passwordUpdated from 'assets/authentication-banners/password-updated.png'
|
|||||||
import successTick from 'assets/authentication-banners/successTick.png';
|
import successTick from 'assets/authentication-banners/successTick.png';
|
||||||
import Image from 'components/base/Image';
|
import Image from 'components/base/Image';
|
||||||
import IconifyIcon from 'components/base/IconifyIcon';
|
import IconifyIcon from 'components/base/IconifyIcon';
|
||||||
import PasswordStrengthChecker from '../../components/PasswordStrengthChecker';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
|
||||||
const ResetPassword = (): ReactElement => {
|
const ResetPassword = (): ReactElement => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [email, setEmail] = useState(searchParams.get('email') || '');
|
||||||
|
const [code, setCode] = useState(searchParams.get('code') || '');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||||
|
|
||||||
const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword);
|
const handleClickShowNewPassword = () => setShowNewPassword(!showNewPassword);
|
||||||
const handleClickShowConfirmPassword = () => setShowConfirmPassword(!showConfirmPassword);
|
const handleClickShowConfirmPassword = () => setShowConfirmPassword(!showConfirmPassword);
|
||||||
const [resetSuccessful, setResetSuccessful] = useState(false);
|
|
||||||
|
|
||||||
const handleResetPassword = () => {
|
const handleResetPassword = async () => {
|
||||||
const passwordField: HTMLInputElement = document.getElementById(
|
if (password !== confirmPassword) {
|
||||||
'new-password',
|
setErrorMessage("Passwords don't match");
|
||||||
) as HTMLInputElement;
|
|
||||||
const confirmPasswordField: HTMLInputElement = document.getElementById(
|
|
||||||
'confirm-password',
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
if (passwordField.value !== confirmPasswordField.value) {
|
|
||||||
alert("Passwords don't match");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setResetSuccessful(true);
|
|
||||||
|
if (!email || !code) {
|
||||||
|
setErrorMessage('Please provide both email and reset code.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('password-reset/confirm/', {
|
||||||
|
email,
|
||||||
|
code,
|
||||||
|
new_password: password,
|
||||||
|
new_password2: confirmPassword,
|
||||||
|
});
|
||||||
|
setResetSuccessful(true);
|
||||||
|
} catch (error: any) {
|
||||||
|
setErrorMessage(
|
||||||
|
error.response?.data?.detail ||
|
||||||
|
error.response?.data?.non_field_errors?.[0] ||
|
||||||
|
'An error occurred. Please try again.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,6 +84,45 @@ const ResetPassword = (): ReactElement => {
|
|||||||
{!resetSuccessful ? (
|
{!resetSuccessful ? (
|
||||||
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
||||||
<Typography variant="h3">Reset Password</Typography>
|
<Typography variant="h3">Reset Password</Typography>
|
||||||
|
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
|
||||||
|
<FormControl variant="standard" fullWidth>
|
||||||
|
<InputLabel shrink htmlFor="email">
|
||||||
|
Email
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
id="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-email" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl variant="standard" fullWidth>
|
||||||
|
<InputLabel shrink htmlFor="code">
|
||||||
|
Reset Code
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter reset code"
|
||||||
|
id="code"
|
||||||
|
value={code}
|
||||||
|
onChange={(e) => setCode(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-lock" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormControl variant="standard" fullWidth>
|
<FormControl variant="standard" fullWidth>
|
||||||
<InputLabel shrink htmlFor="new-password">
|
<InputLabel shrink htmlFor="new-password">
|
||||||
Password
|
Password
|
||||||
@@ -67,6 +132,7 @@ const ResetPassword = (): ReactElement => {
|
|||||||
placeholder="Enter new password"
|
placeholder="Enter new password"
|
||||||
type={showNewPassword ? 'text' : 'password'}
|
type={showNewPassword ? 'text' : 'password'}
|
||||||
id="new-password"
|
id="new-password"
|
||||||
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@@ -89,17 +155,18 @@ const ResetPassword = (): ReactElement => {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/*<PasswordStrengthChecker password={password} />*/}
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl variant="standard" fullWidth>
|
<FormControl variant="standard" fullWidth>
|
||||||
<InputLabel shrink htmlFor="confirm-password">
|
<InputLabel shrink htmlFor="confirm-password">
|
||||||
Password
|
Confirm Password
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
<TextField
|
<TextField
|
||||||
variant="filled"
|
variant="filled"
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@@ -122,8 +189,13 @@ const ResetPassword = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button variant="contained" fullWidth onClick={handleResetPassword}>
|
<Button
|
||||||
Reset Password
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
disabled={loading || !password || !confirmPassword}
|
||||||
|
>
|
||||||
|
{loading ? 'Resetting...' : 'Reset Password'}
|
||||||
</Button>
|
</Button>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Back to{' '}
|
Back to{' '}
|
||||||
@@ -140,7 +212,7 @@ const ResetPassword = (): ReactElement => {
|
|||||||
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
<Stack alignItems="center" gap={3.75} width={330} mx="auto">
|
||||||
<Image src={successTick} />
|
<Image src={successTick} />
|
||||||
<Typography variant="h3">Reset Successfully</Typography>
|
<Typography variant="h3">Reset Successfully</Typography>
|
||||||
<Typography variant="body1" textAlign="center" color="text.secndary">
|
<Typography variant="body1" textAlign="center" color="text.secondary">
|
||||||
Your Ditch the Agent log in password has been updated successfully
|
Your Ditch the Agent log in password has been updated successfully
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" fullWidth LinkComponent={Link} href="/authentication/login">
|
<Button variant="contained" fullWidth LinkComponent={Link} href="/authentication/login">
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import Image from 'components/base/Image';
|
|||||||
import { axiosInstance } from '../../axiosApi.js';
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
import PasswordStrengthChecker from '../../components/PasswordStrengthChecker';
|
import PasswordStrengthChecker from '../../components/PasswordStrengthChecker';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { features } from '../../config/features';
|
||||||
|
|
||||||
type SignUpValues = {
|
type SignUpValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -191,41 +192,43 @@ const SignUp = (): ReactElement => {
|
|||||||
</InputLabel>
|
</InputLabel>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||||
{[
|
{[
|
||||||
{ value: 'property_owner', label: 'Home Buyer/Seller', icon: 'mdi:home-account' },
|
{ value: 'property_owner', label: 'Home Buyer/Seller', icon: 'mdi:home-account', enabled: true },
|
||||||
{ value: 'attorney', label: 'Attorney', icon: 'mdi:gavel' },
|
{ value: 'attorney', label: 'Attorney', icon: 'mdi:gavel', enabled: features.enableAttorneyRegistration },
|
||||||
{ value: 'vendor', label: 'Vendor', icon: 'mdi:briefcase' },
|
{ value: 'vendor', label: 'Vendor', icon: 'mdi:briefcase', enabled: features.enableRealEstateAgentRegistration },
|
||||||
].map((type) => (
|
]
|
||||||
<Paper
|
.filter((type) => type.enabled)
|
||||||
key={type.value}
|
.map((type) => (
|
||||||
variant="outlined"
|
<Paper
|
||||||
onClick={() => setFieldValue('ownerType', type.value)}
|
key={type.value}
|
||||||
sx={{
|
variant="outlined"
|
||||||
p: 2,
|
onClick={() => setFieldValue('ownerType', type.value)}
|
||||||
flex: 1,
|
sx={{
|
||||||
cursor: 'pointer',
|
p: 2,
|
||||||
display: 'flex',
|
flex: 1,
|
||||||
flexDirection: 'column',
|
cursor: 'pointer',
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
gap: 1,
|
flexDirection: 'column',
|
||||||
borderColor: values.ownerType === type.value ? 'primary.main' : 'divider',
|
alignItems: 'center',
|
||||||
bgcolor: values.ownerType === type.value ? 'action.selected' : 'background.paper',
|
gap: 1,
|
||||||
transition: 'all 0.2s',
|
borderColor: values.ownerType === type.value ? 'primary.main' : 'divider',
|
||||||
'&:hover': {
|
bgcolor: values.ownerType === type.value ? 'action.selected' : 'background.paper',
|
||||||
borderColor: 'primary.main',
|
transition: 'all 0.2s',
|
||||||
bgcolor: 'action.hover',
|
'&:hover': {
|
||||||
},
|
borderColor: 'primary.main',
|
||||||
}}
|
bgcolor: 'action.hover',
|
||||||
>
|
},
|
||||||
<IconifyIcon icon={type.icon} width={24} height={24} color={values.ownerType === type.value ? 'primary.main' : 'text.secondary'} />
|
}}
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
fontWeight={600}
|
|
||||||
color={values.ownerType === type.value ? 'primary.main' : 'text.primary'}
|
|
||||||
>
|
>
|
||||||
{type.label}
|
<IconifyIcon icon={type.icon} width={24} height={24} color={values.ownerType === type.value ? 'primary.main' : 'text.secondary'} />
|
||||||
</Typography>
|
<Typography
|
||||||
</Paper>
|
variant="body2"
|
||||||
))}
|
fontWeight={600}
|
||||||
|
color={values.ownerType === type.value ? 'primary.main' : 'text.primary'}
|
||||||
|
>
|
||||||
|
{type.label}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
|||||||
203
ditch-the-agent/src/pages/authentication/VerifyAccount.tsx
Normal file
203
ditch-the-agent/src/pages/authentication/VerifyAccount.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { ReactElement, Suspense, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
InputAdornment,
|
||||||
|
InputLabel,
|
||||||
|
Link,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import loginBanner from 'assets/authentication-banners/green.png';
|
||||||
|
import IconifyIcon from 'components/base/IconifyIcon';
|
||||||
|
import logo from 'assets/logo/favicon-logo.png';
|
||||||
|
import Image from 'components/base/Image';
|
||||||
|
import { axiosInstance } from '../../axiosApi.js';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { Form, Formik } from 'formik';
|
||||||
|
import paths from 'routes/paths';
|
||||||
|
|
||||||
|
type verifyValues = {
|
||||||
|
email: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VerifyAccount = (): ReactElement => {
|
||||||
|
const [errorMessage, setErrorMessage] = useState<any | null>(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const initialEmail = location.state?.email || '';
|
||||||
|
|
||||||
|
const handleVerify = async ({ email, code }: verifyValues): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('/check-passcode/', {
|
||||||
|
email: email,
|
||||||
|
code: code,
|
||||||
|
});
|
||||||
|
navigate('/');
|
||||||
|
} catch (error) {
|
||||||
|
const hasErrors = error.response && Object.keys(error.response.data).length > 0;
|
||||||
|
if (hasErrors) {
|
||||||
|
setErrorMessage(error.response.data);
|
||||||
|
} else {
|
||||||
|
setErrorMessage('An unexpected error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
bgcolor="background.paper"
|
||||||
|
boxShadow={(theme) => theme.shadows[3]}
|
||||||
|
height={560}
|
||||||
|
width={{ md: 960 }}
|
||||||
|
>
|
||||||
|
<Stack width={{ md: 0.5 }} m={2.5} gap={10}>
|
||||||
|
<Link href="/" height="fit-content">
|
||||||
|
<Image src={logo} width={82.6} />
|
||||||
|
</Link>
|
||||||
|
<Stack alignItems="center" gap={2.5} width={330} mx="auto">
|
||||||
|
<Typography variant="h3">Verify Account</Typography>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
email: initialEmail,
|
||||||
|
code: '',
|
||||||
|
}}
|
||||||
|
onSubmit={handleVerify}
|
||||||
|
enableReinitialize
|
||||||
|
>
|
||||||
|
{({ setFieldValue, values }) => (
|
||||||
|
<Form>
|
||||||
|
<FormControl variant="standard" fullWidth>
|
||||||
|
{successMessage ? (
|
||||||
|
<Alert severity="success" sx={{ mb: 2 }}>
|
||||||
|
<Typography>{successMessage}</Typography>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
{errorMessage ? (
|
||||||
|
<Alert severity="error">
|
||||||
|
{errorMessage.detail ? (
|
||||||
|
<Typography>{errorMessage.detail}</Typography>
|
||||||
|
) : (
|
||||||
|
<ul>
|
||||||
|
{Object.entries(errorMessage).map(([fieldName, errorMessages]) => (
|
||||||
|
<li key={fieldName}>
|
||||||
|
<strong>{fieldName}</strong>
|
||||||
|
{Array.isArray(errorMessages) ? (
|
||||||
|
<ul>
|
||||||
|
{errorMessages.map((message, index) => (
|
||||||
|
<li key={`${fieldName}-${index}`}>{message}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<span>: {String(errorMessages)}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
<InputLabel shrink htmlFor="email">
|
||||||
|
Email
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
id="email"
|
||||||
|
value={values.email}
|
||||||
|
onChange={(event) => setFieldValue('email', event.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-email" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl variant="standard" fullWidth sx={{ mt: 2 }}>
|
||||||
|
<InputLabel shrink htmlFor="code">
|
||||||
|
Verification Code
|
||||||
|
</InputLabel>
|
||||||
|
<TextField
|
||||||
|
variant="filled"
|
||||||
|
placeholder="Enter verification code"
|
||||||
|
id="code"
|
||||||
|
value={values.code}
|
||||||
|
onChange={(event) => setFieldValue('code', event.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconifyIcon icon="ic:baseline-lock" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button variant="contained" type={'submit'} fullWidth sx={{ mt: 3 }}>
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
onClick={async () => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
setSuccessMessage(null);
|
||||||
|
if (!values.email) {
|
||||||
|
setErrorMessage({ detail: 'Email is required to resend code.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await axiosInstance.post('/resend-registration-email/', {
|
||||||
|
email: values.email,
|
||||||
|
});
|
||||||
|
setSuccessMessage('Registration email sent.');
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage({ detail: 'There was an error sending the verification link.' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Resend Verification Code
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Back to{' '}
|
||||||
|
<Link
|
||||||
|
href={paths.login}
|
||||||
|
underline="hover"
|
||||||
|
fontSize={(theme) => theme.typography.body1.fontSize}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Skeleton variant="rectangular" height={1} width={1} sx={{ bgcolor: 'primary.main' }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
alt="Login banner"
|
||||||
|
src={loginBanner}
|
||||||
|
sx={{
|
||||||
|
width: 0.5,
|
||||||
|
display: { xs: 'none', md: 'block' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerifyAccount;
|
||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
home: `/${rootPaths.homeRoot}`,
|
home: `/${rootPaths.homeRoot}`,
|
||||||
dashboard: `/${rootPaths.dashboardRoot}`,
|
dashboard: `/${rootPaths.dashboardRoot}`,
|
||||||
login: `/${rootPaths.authRoot}/login`,
|
login: `/${rootPaths.authRoot}/login`,
|
||||||
|
verifyAccount: `/${rootPaths.authRoot}/verify-account`,
|
||||||
signup: `/${rootPaths.authRoot}/sign-up`,
|
signup: `/${rootPaths.authRoot}/sign-up`,
|
||||||
resetPassword: `/${rootPaths.authRoot}/reset-password`,
|
resetPassword: `/${rootPaths.authRoot}/reset-password`,
|
||||||
forgotPassword: `/${rootPaths.authRoot}/forgot-password`,
|
forgotPassword: `/${rootPaths.authRoot}/forgot-password`,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const Error404 = lazy(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Login = lazy(async () => import('pages/authentication/Login'));
|
const Login = lazy(async () => import('pages/authentication/Login'));
|
||||||
|
const VerifyAccount = lazy(async () => import('pages/authentication/VerifyAccount'));
|
||||||
const SignUp = lazy(async () => import('pages/authentication/SignUp'));
|
const SignUp = lazy(async () => import('pages/authentication/SignUp'));
|
||||||
|
|
||||||
const ResetPassword = lazy(async () => import('pages/authentication/ResetPassword'));
|
const ResetPassword = lazy(async () => import('pages/authentication/ResetPassword'));
|
||||||
@@ -179,6 +180,10 @@ const routes: RouteObject[] = [
|
|||||||
path: paths.login,
|
path: paths.login,
|
||||||
element: <Login />,
|
element: <Login />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: paths.verifyAccount,
|
||||||
|
element: <VerifyAccount />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: paths.signup,
|
path: paths.signup,
|
||||||
element: <SignUp />,
|
element: <SignUp />,
|
||||||
|
|||||||
1
ditch-the-agent/src/setupTests.ts
Normal file
1
ditch-the-agent/src/setupTests.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -94,6 +94,7 @@ export interface MessagesAPI {
|
|||||||
|
|
||||||
export interface UserAPI {
|
export interface UserAPI {
|
||||||
id: number;
|
id: number;
|
||||||
|
uuid4: string;
|
||||||
email: string;
|
email: string;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
@@ -268,6 +269,7 @@ export interface WalkScoreAPI {
|
|||||||
|
|
||||||
export interface PropertiesAPI {
|
export interface PropertiesAPI {
|
||||||
id: number;
|
id: number;
|
||||||
|
uuid4: string;
|
||||||
owner: PropertyOwnerAPI;
|
owner: PropertyOwnerAPI;
|
||||||
address: string; // full address
|
address: string; // full address
|
||||||
street: string;
|
street: string;
|
||||||
@@ -354,6 +356,7 @@ export interface LenderFinancingAgreementData {
|
|||||||
|
|
||||||
export interface DocumentAPI {
|
export interface DocumentAPI {
|
||||||
id: number;
|
id: number;
|
||||||
|
uuid4: string;
|
||||||
property: number;
|
property: number;
|
||||||
file: string;
|
file: string;
|
||||||
document_type: string;
|
document_type: string;
|
||||||
@@ -370,7 +373,7 @@ export interface DocumentAPI {
|
|||||||
| LenderFinancingAgreementData;
|
| LenderFinancingAgreementData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PropertyRequestAPI extends Omit<PropertiesAPI, 'owner' | 'id'> {
|
export interface PropertyRequestAPI extends Omit<PropertiesAPI, 'owner' | 'id' | 'uuid4'> {
|
||||||
owner: number;
|
owner: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +773,7 @@ export interface FaqApi {
|
|||||||
export interface SupportMessageApi {
|
export interface SupportMessageApi {
|
||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
user: id;
|
user: number;
|
||||||
user_first_name: string;
|
user_first_name: string;
|
||||||
user_last_name: string;
|
user_last_name: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@@ -786,5 +789,8 @@ export interface SupportCaseApi {
|
|||||||
messages: SupportMessageApi[];
|
messages: SupportMessageApi[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
user_email: string;
|
||||||
|
user_first_name: string;
|
||||||
|
user_last_name: string;
|
||||||
}
|
}
|
||||||
// Walk Score API Type Definitions
|
// Walk Score API Type Definitions
|
||||||
|
|||||||
@@ -16,4 +16,9 @@ export default defineConfig({
|
|||||||
port: 3000,
|
port: 3000,
|
||||||
},
|
},
|
||||||
base: '/',
|
base: '/',
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './src/setupTests.ts',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user