[#40] Address search #89
68 changed files with 18028 additions and 22275 deletions
|
@ -8,7 +8,7 @@ from right_tree.api.models import Habitat, HabitatImage, Plant, EcologicalDistri
|
|||
from right_tree.api.serializers import HabitatImageSerializer, HabitatSerializer, PlantSerializer, SoilOrderSerializer, EcologicalDistrictLayerSerializer, AddressSerializer, ZoneSerializer
|
||||
|
||||
from .filters import *
|
||||
from .wms_utils import get_address_from_coordinates
|
||||
from .wms_utils import get_address_from_coordinates, search_address
|
||||
from .resource_generation_utils import create_plant_csv_file, get_plant_resource_filepath, create_planting_guide_pdf, PLANTING_GUIDE_PDF_FILENAME
|
||||
|
||||
|
||||
|
@ -61,12 +61,17 @@ class LINZPropertyViewSet(viewsets.ViewSet):
|
|||
|
||||
def list(self, request):
|
||||
coordinates = self.request.query_params.get('coordinates')
|
||||
if coordinates is not None:
|
||||
address = self.request.query_params.get('search')
|
||||
|
||||
if address is not None:
|
||||
results = search_address(address)
|
||||
return Response(results)
|
||||
elif coordinates is not None:
|
||||
address_data = get_address_from_coordinates(coordinates)
|
||||
serializer = AddressSerializer(address_data)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return HttpResponseBadRequest("No coordinate given.")
|
||||
return HttpResponseBadRequest("No parameters given.")
|
||||
|
||||
class AuckCHCHRegionInformation(viewsets.ViewSet):
|
||||
""" Filtered viewset defining if coordinate falls inside auckland and chch regions.
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
from urllib.parse import urlencode
|
||||
from unicodedata import normalize
|
||||
|
||||
from django.contrib.gis.geos import Point, GEOSGeometry
|
||||
from django.conf import settings
|
||||
|
@ -12,6 +14,10 @@ PROPERTY_TILE_LAYER = "layer-50804"
|
|||
PROPERTY_INFO_LAYER = "layer-53353"
|
||||
|
||||
|
||||
search_num = re.compile(r'((?<!\S)\d+)')
|
||||
search_str = re.compile(r'((?<!\S)[a-zA-Z\-]+)')
|
||||
|
||||
|
||||
class WFSError(Exception):
|
||||
pass
|
||||
|
||||
|
@ -85,3 +91,44 @@ def get_address_from_coordinates(coordinates):
|
|||
if propertyPolygon is not None:
|
||||
prop_geom = GEOSGeometry(json.dumps(propertyPolygon['geometry']))
|
||||
return get_address(prop_geom)
|
||||
|
||||
|
||||
def search_address(address):
|
||||
|
||||
# normalize accent characters etc.
|
||||
address = normalize(
|
||||
"NFKD",
|
||||
address.strip().lower(),
|
||||
).encode("ascii", "ignore").decode()
|
||||
|
||||
nums = search_num.findall(address)
|
||||
strings = search_str.findall(address)
|
||||
|
||||
num_terms = [f"address_number = {n}" for n in nums]
|
||||
string_terms = []
|
||||
|
||||
for s in strings:
|
||||
string_terms += [
|
||||
f"full_road_name_ascii ILIKE '{s}%'",
|
||||
f"town_city_ascii ILIKE '{s}%'",
|
||||
f"suburb_locality_ascii ILIKE '{s}%'",
|
||||
]
|
||||
|
||||
num_expr = " OR ".join(f"({term})" for term in num_terms)
|
||||
string_expr = " OR ".join(f"({term})" for term in string_terms)
|
||||
|
||||
cql_filter = f"({num_expr}) AND ({string_expr})"
|
||||
|
||||
resp = linz_wfs_getfeature(
|
||||
typeNames=PROPERTY_INFO_LAYER,
|
||||
cql_filter=cql_filter,
|
||||
count=10,
|
||||
)
|
||||
features = resp.get("features", [])
|
||||
|
||||
return [
|
||||
{
|
||||
'coordinates': feature['geometry']['coordinates'],
|
||||
'address': feature['properties']['full_address'],
|
||||
} for feature in features
|
||||
]
|
||||
|
|
37949
frontend/package-lock.json
generated
37949
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -13,11 +13,12 @@
|
|||
"axios": "^0.22.0",
|
||||
"bootstrap": "^5.1.2",
|
||||
"leaflet": "^1.7.1",
|
||||
"node-sass": "^6.0.1",
|
||||
"node-sass": "^8.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-leaflet": "^3.2.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"reactstrap": "^8.10.0",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import MainPage from './pages/MainPage';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
function App() {
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
},
|
||||
typography: {
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<MainPage />
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,8 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"steps": {
|
||||
"address": {
|
||||
"title": "Right Plant Right Place Right Time\nPlant Selector Tool for New Zealand.",
|
||||
"description": "Your native plant selection starts here! Register by entering your address. On the following pages you will provide more details on your project until the system has enough information to create your plant species list and planting plan. To start, click on the map and pan and zoom to the site location. Once the location is selected, click on the “next step” button to complete the process. Repeat this process for sites at different locations."
|
||||
},
|
||||
"location": {
|
||||
"title": "Right Plant Right Place Right Time\nPlant Selector Tool for New Zealand.",
|
||||
"description": "Your native plant selection starts here! Use the map to select a planting site location within New Zealand. On the following pages you will provide more details on your project until the system has enough information to create your plant species list and planting plan. To start, click on the map and pan and zoom to the site location. Once the location is selected, click on the “next step” button to complete the process. Repeat this process for sites at different locations."
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import TopNav from './TopNav'
|
||||
import logo from '../assets/img/logo.png'
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className='d-flex justify-content-between top-header'>
|
||||
<a href="https://www.b4c3.com/">
|
||||
<img className='header-logo' src={logo} alt="Biosphere Capital Limited" />
|
||||
</a>
|
||||
<TopNav />
|
||||
</div>
|
||||
);
|
||||
}
|
17
frontend/src/components/Header.jsx
Normal file
17
frontend/src/components/Header.jsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import TopNav from "./TopNav";
|
||||
import logo from "../assets/img/logo.png";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="d-flex justify-content-between top-header">
|
||||
<a href="https://www.b4c3.com/">
|
||||
<img
|
||||
className="header-logo"
|
||||
src={logo}
|
||||
alt="Biosphere Capital Limited"
|
||||
/>
|
||||
</a>
|
||||
<TopNav />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stepper from '@mui/material/Stepper';
|
||||
import Step from '@mui/material/Step';
|
||||
import StepLabel from '@mui/material/StepLabel';
|
||||
import Button from '@mui/material/Button';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
import LocationStep from './steps/location/LocationStep'
|
||||
import SoilStep from './steps/soilvariant/SoilStep'
|
||||
import HabitatStep from './steps/habitat/HabitatStep';
|
||||
import ZoneStep from './steps/zone/ZoneStep';
|
||||
import SummaryStep from './steps/summary/SummaryStep';
|
||||
import ResultsStep from './steps/results/ResultsStep'
|
||||
|
||||
const steps = [
|
||||
{ 'label': 'Select location', 'component': LocationStep, 'tooltip': "Click on a location on the map" },
|
||||
{ 'label': 'Choose soil', 'component': SoilStep, 'tooltip': "Describe the moisture content of your soil" },
|
||||
{ 'label': 'Choose habitat', 'component': HabitatStep, 'tooltip': "Specify type of landscape to be planted" },
|
||||
{ 'label': 'Select zone', 'component': ZoneStep, 'tooltip': "Specify geographical detail" },
|
||||
{ 'label': 'Summary', 'component': SummaryStep, 'tooltip': "Check your inputs" },
|
||||
{ 'label': 'Results', 'component': ResultsStep, 'tooltip': "List of plant species and user guide" }
|
||||
];
|
||||
|
||||
export default function StepperWizard(props) {
|
||||
const [activeStep, setActiveStep] = React.useState(0);
|
||||
const [nextDisabled, setNextDisabled] = React.useState(true);
|
||||
const [redirectBack, setRedirectBack] = React.useState(false);
|
||||
|
||||
const resetStepState = () => {
|
||||
setNextDisabled(true);
|
||||
setRedirectBack(false);
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (redirectBack) {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
} else {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
}
|
||||
resetStepState();
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
resetStepState();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveStep(0);
|
||||
resetStepState();
|
||||
props.resetFilterState()
|
||||
};
|
||||
|
||||
let CurrentStep = activeStep >= steps.length ? steps[steps.length - 1].component : steps[activeStep].component;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', height: '100%', display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<Stepper activeStep={activeStep} sx={{ paddingRight: '3vw', paddingLeft: '3vw', marginBottom: '2vw' }}>
|
||||
{steps.map((step) => {
|
||||
return (
|
||||
<Tooltip title={step.tooltip}>
|
||||
<Step key={step.label}>
|
||||
<StepLabel>{step.label}</StepLabel>
|
||||
</Step>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
<React.Fragment>
|
||||
<CurrentStep {...props} setNextDisabled={setNextDisabled} setRedirectBack={setRedirectBack} />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2, pb: 2, paddingRight: '3vw', paddingLeft: '3vw' }}>
|
||||
<Button
|
||||
color="inherit"
|
||||
disabled={activeStep === 0}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
{activeStep === steps.length - 1 ?
|
||||
<Button onClick={handleReset}>Reset</Button> : <Button onClick={handleNext} disabled={nextDisabled}>Next</Button>}
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
</Box>
|
||||
);
|
||||
}
|
81
frontend/src/components/Stepper.jsx
Normal file
81
frontend/src/components/Stepper.jsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stepper from '@mui/material/Stepper';
|
||||
import Step from '@mui/material/Step';
|
||||
import StepLabel from '@mui/material/StepLabel';
|
||||
import Button from '@mui/material/Button';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
|
||||
export default function StepperWizard({ steps }) {
|
||||
const [filters, setFilters] = useState({});
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
const [redirectBack, setRedirectBack] = useState(false);
|
||||
|
||||
const resetStepState = () => {
|
||||
setNextDisabled(true);
|
||||
setRedirectBack(false);
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (redirectBack) {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
} else {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep + 1);
|
||||
}
|
||||
resetStepState();
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep((prevActiveStep) => prevActiveStep - 1);
|
||||
resetStepState();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveStep(0);
|
||||
resetStepState();
|
||||
setFilters({});
|
||||
};
|
||||
|
||||
const updateFilterState = (newFilters) => {
|
||||
setFilters(f => ({...f, ...newFilters}));
|
||||
};
|
||||
|
||||
let CurrentStep = activeStep >= steps.length ? steps[steps.length - 1].component : steps[activeStep].component;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', height: '100%', display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<Stepper activeStep={activeStep} sx={{ paddingRight: '3vw', paddingLeft: '3vw', marginBottom: '2vw' }}>
|
||||
{steps.map((step) => {
|
||||
return (
|
||||
<Tooltip title={step.tooltip}>
|
||||
<Step key={step.label}>
|
||||
<StepLabel>{step.label}</StepLabel>
|
||||
</Step>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
<CurrentStep
|
||||
filters={filters}
|
||||
updateFilterState={updateFilterState}
|
||||
resetFilterState={() => setFilters({})}
|
||||
setNextDisabled={setNextDisabled}
|
||||
setRedirectBack={setRedirectBack}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2, pb: 2, paddingRight: '3vw', paddingLeft: '3vw' }}>
|
||||
<Button
|
||||
color="inherit"
|
||||
disabled={activeStep === 0}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
{activeStep === steps.length - 1 ?
|
||||
<Button onClick={handleReset}>Reset</Button> : <Button onClick={handleNext} disabled={nextDisabled}>Next</Button>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
export default function TopNav() {
|
||||
return (
|
||||
<nav className="navbar navbar-expand-lg">
|
||||
<div className="container-fluid">
|
||||
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNavDropdown">
|
||||
<ul className="navbar-nav">
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">Advisory</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">Ecology</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">Renewables</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">About</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com/contact">Contact</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
48
frontend/src/components/TopNav.jsx
Normal file
48
frontend/src/components/TopNav.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
export default function TopNav() {
|
||||
return (
|
||||
<nav className="navbar navbar-expand-lg">
|
||||
<div className="container-fluid">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNavDropdown">
|
||||
<ul className="navbar-nav">
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">
|
||||
Advisory
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">
|
||||
Ecology
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">
|
||||
Renewables
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="https://www.b4c3.com/contact">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/coastal_dune_and_bank_system.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/hill_slopes.jsx
Normal file
2
frontend/src/components/diagrams/hill_slopes.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/hill_slopes.svg';
|
||||
export default ReactComponent;
|
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/hill_slopes_features.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/mangroves_section.jsx
Normal file
2
frontend/src/components/diagrams/mangroves_section.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/mangroves_section.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/maori_garden.jsx
Normal file
2
frontend/src/components/diagrams/maori_garden.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/maori_garden.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/open_wetland_lake.jsx
Normal file
2
frontend/src/components/diagrams/open_wetland_lake.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/open_wetland_lake.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/riparian.jsx
Normal file
2
frontend/src/components/diagrams/riparian.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/riparian.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/rural_section.jsx
Normal file
2
frontend/src/components/diagrams/rural_section.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/rural_section.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/salt_marsh.jsx
Normal file
2
frontend/src/components/diagrams/salt_marsh.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/salt_marsh.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/sand_dunes.jsx
Normal file
2
frontend/src/components/diagrams/sand_dunes.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/sand_dunes.svg';
|
||||
export default ReactComponent;
|
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/stormwater_treatment.svg';
|
||||
export default ReactComponent;
|
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/transport_corridor_rail_section.svg';
|
||||
export default ReactComponent;
|
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/transport_corridor_road_section.svg';
|
||||
export default ReactComponent;
|
2
frontend/src/components/diagrams/urban_section.jsx
Normal file
2
frontend/src/components/diagrams/urban_section.jsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
import { ReactComponent } from '../../assets/img/habitatSVG/urban_section.svg';
|
||||
export default ReactComponent;
|
|
@ -1,27 +1,15 @@
|
|||
import React from 'react';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { CircularProgress, Stack } from "@mui/material";
|
||||
|
||||
export const HabitatSVG = ({ name, ...rest }) => {
|
||||
const ImportedIconRef = React.useRef(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
const importIcon = async () => {
|
||||
try {
|
||||
ImportedIconRef.current = (await import(`!!@svgr/webpack?-svgo,+titleProp,+ref!../../assets/img/habitatSVG/${name}.svg`)).default;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
importIcon();
|
||||
}, [name]);
|
||||
|
||||
if (!loading && ImportedIconRef.current) {
|
||||
const { current: ImportedIcon } = ImportedIconRef;
|
||||
return <ImportedIcon {...rest} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
const Diagram = lazy(() => import(`../diagrams/${name}`));
|
||||
const loadingComponent = (
|
||||
<Stack alignItems="center">
|
||||
<CircularProgress />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Suspense fallback={loadingComponent}> {/* experiemental in React 17. TODO: upgrade to 18 */}
|
||||
<Diagram {...rest} />
|
||||
</Suspense>);
|
||||
};
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Row, Col } from 'reactstrap'
|
||||
import SampleBackground from '../../assets/img/stepBackgrounds/step1.jpg';
|
||||
|
||||
export default function Step(props) {
|
||||
|
||||
const [stepBackgroundImage, setStepBackground] = useState(SampleBackground)
|
||||
|
||||
useEffect(() => setStepBackground(props.backgroundImage || SampleBackground), [props.backgroundImage])
|
||||
|
||||
return (
|
||||
<div className='step-container' style={{backgroundImage: `url(${stepBackgroundImage})`}}>
|
||||
<div className='step-overlay'>
|
||||
{props.contentComponent ? (
|
||||
props.contentComponent
|
||||
) : (
|
||||
<Row style={{height: '100%'}}>
|
||||
<Col lg={{ size: "4" }} md="5" sm="12" style={{display: 'flex', alignItems: 'center', justifyContent: 'center', paddingRight: '50px', paddingLeft: '50px'}}>
|
||||
{props.informationComponent}
|
||||
</Col>
|
||||
<Col lg={{ size: "8" }} md="7" sm="12" style={{display: 'flex', alignItems: 'center'}}>
|
||||
{props.selectionComponent}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
50
frontend/src/components/steps/Step.jsx
Normal file
50
frontend/src/components/steps/Step.jsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Row, Col } from "reactstrap";
|
||||
import SampleBackground from "../../assets/img/stepBackgrounds/step1.jpg";
|
||||
|
||||
export default function Step(props) {
|
||||
const [stepBackgroundImage, setStepBackground] = useState(SampleBackground);
|
||||
|
||||
useEffect(
|
||||
() => setStepBackground(props.backgroundImage || SampleBackground),
|
||||
[props.backgroundImage]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="step-container"
|
||||
style={{ backgroundImage: `url(${stepBackgroundImage})` }}
|
||||
>
|
||||
<div className="step-overlay">
|
||||
{props.contentComponent ? (
|
||||
props.contentComponent
|
||||
) : (
|
||||
<Row style={{ height: "100%" }}>
|
||||
<Col
|
||||
lg={{ size: "4" }}
|
||||
md="5"
|
||||
sm="12"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingRight: "50px",
|
||||
paddingLeft: "50px",
|
||||
}}
|
||||
>
|
||||
{props.informationComponent}
|
||||
</Col>
|
||||
<Col
|
||||
lg={{ size: "8" }}
|
||||
md="7"
|
||||
sm="12"
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
{props.selectionComponent}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
export default function StepInformation(props) {
|
||||
return (
|
||||
<div className='ph-4'>
|
||||
<h3>{props.title}</h3>
|
||||
<br/>
|
||||
{props.description}
|
||||
</div>
|
||||
)
|
||||
}
|
9
frontend/src/components/steps/StepInformation.jsx
Normal file
9
frontend/src/components/steps/StepInformation.jsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default function StepInformation(props) {
|
||||
return (
|
||||
<div className="ph-4">
|
||||
<h3>{props.title}</h3>
|
||||
<br />
|
||||
{props.description}
|
||||
</div>
|
||||
);
|
||||
}
|
88
frontend/src/components/steps/address/AddressSearch.jsx
Normal file
88
frontend/src/components/steps/address/AddressSearch.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { InputAdornment, TextField } from '@mui/material';
|
||||
import PlaceIcon from '@mui/icons-material/Place';
|
||||
import Box from '@mui/material/Box';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import LocationRepsostory from '../../../repository/LocationRepository';
|
||||
import { CircularProgress } from "@mui/material";
|
||||
|
||||
|
||||
const AddressSearchSuggestions = ({results, onClick}) => (
|
||||
(Array.isArray(results) && results.length > 0)
|
||||
? <Box sx={{ width: '100%', bgcolor: 'background.paper' }}>
|
||||
<List>
|
||||
{results.map((r, idx) => (
|
||||
<ListItem disablePadding key={idx}>
|
||||
<ListItemButton onClick={() => onClick(r)}>
|
||||
<ListItemText primary={r.address} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
: null);
|
||||
|
||||
const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDisabled, setRedirectBack, classNames}) => {
|
||||
const [value, setValue] = useState("");
|
||||
const [enable, setEnable] = useState(true);
|
||||
const [results, setResults] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setResults([]);
|
||||
|
||||
if (enable && value && value.length > 5) {
|
||||
setNextDisabled(true);
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(true);
|
||||
LocationRepsostory.getPropertyDetails({search: value}).then(resp => {
|
||||
setResults(resp.data);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [value, enable, setNextDisabled]);
|
||||
|
||||
return (
|
||||
<div classNames={classNames}>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
placeholder="Enter address..."
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
{isLoading ? <CircularProgress size="1.2rem" color="inherit" /> : <PlaceIcon />}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
setEnable(true);
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
<AddressSearchSuggestions
|
||||
results={results}
|
||||
onClick={(r) => {
|
||||
setValue(r.address);
|
||||
updateFilterState({
|
||||
coordinates: {
|
||||
lng: r.coordinates[0],
|
||||
lat: r.coordinates[1],
|
||||
},
|
||||
});
|
||||
setNextDisabled(false);
|
||||
setEnable(false);
|
||||
}}
|
||||
/>
|
||||
</div>);
|
||||
};
|
||||
|
||||
export default AddressSearch;
|
29
frontend/src/components/steps/address/AddressStep.jsx
Normal file
29
frontend/src/components/steps/address/AddressStep.jsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Step from '../Step';
|
||||
import StepInformation from '../StepInformation';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
import addressBackgroundImage from '../../../assets/img/stepBackgrounds/step1.jpg';
|
||||
import AddressSearch from './AddressSearch';
|
||||
|
||||
|
||||
const AddressStep = (props) => {
|
||||
|
||||
const addressPanel = (
|
||||
<div className="p-5">
|
||||
<StepInformation
|
||||
title={staticText.steps.address.title}
|
||||
description={<p>{staticText.steps.address.description}</p>}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<AddressSearch {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
contentComponent={addressPanel}
|
||||
backgroundImage={addressBackgroundImage}
|
||||
/>);
|
||||
};
|
||||
|
||||
export default AddressStep;
|
|
@ -1,133 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import SiteRepository from '../../../repository/SiteRepository';
|
||||
import * as React from 'react';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormLabel from '@mui/material/FormLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
import { HabitatSVG } from '../HabitatSVG'
|
||||
|
||||
|
||||
export default function HabitatSelector(props) {
|
||||
const [habitats, setHabitats] = useState([])
|
||||
const [habitatsMap, setHabitatsMap] = useState({})
|
||||
const [value, setValue] = React.useState(props.filters.habitat && props.filters.habitat.id);
|
||||
const [selectedHabitat, setSelectedHabitat] = React.useState({});
|
||||
const [imageValue, setImageValue] = React.useState(props.filters.habitatImage);
|
||||
|
||||
const getHabitats = () => {
|
||||
SiteRepository.getHabitats().then(response => {
|
||||
if (response.status === 200) {
|
||||
// Create a mapping from the habitat id to the object
|
||||
let habitatsJson = {}
|
||||
response.data.forEach(habitat => {
|
||||
habitatsJson[habitat.id] = habitat
|
||||
})
|
||||
setHabitats(response.data);
|
||||
setHabitatsMap(habitatsJson);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Retrieves the habitats from the api if they are not loaded already
|
||||
habitats.length === 0 && getHabitats()
|
||||
|
||||
// Sets the selected habitat if its already set in filters
|
||||
if (props.filters.habitat && props.filters.habitat.id) {
|
||||
setSelectedHabitat(habitatsMap[props.filters.habitat.id])
|
||||
}
|
||||
|
||||
// If both the habitat and the image is selected, then enable the next step
|
||||
if (props.filters.habitat && props.filters.habitatImage) {
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
}, [habitats.length, props, habitatsMap]);
|
||||
|
||||
const setHabitatImage = (imageId) => {
|
||||
// Sets the selected image radio, updates filter state for image and enable the next button
|
||||
setImageValue(imageId);
|
||||
props.updateFilterState({ "habitatImage": imageId });
|
||||
props.updateFilterState({ "zone": null });
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
|
||||
const handleRadioChange = (event) => {
|
||||
// Retrieve and set the value of the radio button
|
||||
const habitatSelection = event.target.value;
|
||||
setValue(habitatSelection);
|
||||
setHabitatImage(null)
|
||||
|
||||
|
||||
// Set the image default selection if there is only one image variation available
|
||||
const habitatObject = habitatsMap[habitatSelection]
|
||||
setSelectedHabitat(habitatObject)
|
||||
if (habitatObject.images.length === 1) {
|
||||
setHabitatImage(habitatObject.images[0].id)
|
||||
} else {
|
||||
props.setNextDisabled(true);
|
||||
}
|
||||
|
||||
// Update the filters for the selected habitat
|
||||
props.updateFilterState({ "habitat": {"id": habitatObject.id, "name": habitatObject.name} });
|
||||
};
|
||||
|
||||
const handleImageRadioChange = (event) => {
|
||||
const habitatImageSelection = event.target.value;
|
||||
setHabitatImage(habitatImageSelection)
|
||||
};
|
||||
|
||||
const getImageLabel = (image) => {
|
||||
return (
|
||||
<div>
|
||||
<p>{image.name}</p>
|
||||
<HabitatSVG name={image.image_filename} style={{ "height": 'auto', "width": "300px" }}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='d-flex align-top justify-space-between'>
|
||||
<form >
|
||||
<FormControl
|
||||
sx={{ m: 3 }}
|
||||
component="fieldset"
|
||||
variant="standard"
|
||||
>
|
||||
<FormLabel component="legend">{staticText.steps.habitat.optionsLabel}</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="habitat-selection"
|
||||
name="habitat selection"
|
||||
value={value ?? ""}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
{habitats && Array.isArray(habitats) && habitats.map(habitat =>
|
||||
<FormControlLabel key={habitat.id} value={habitat.id} control={<Radio />} label={habitat.name} />
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</form>
|
||||
<form >
|
||||
<FormControl
|
||||
sx={{ m: 3 }}
|
||||
component="fieldset"
|
||||
variant="standard"
|
||||
>
|
||||
<FormLabel component="legend">{staticText.steps.habitat.imageOptionsLabel}</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="habitat-image-selection"
|
||||
name="habitat image selection"
|
||||
value={imageValue ?? ""}
|
||||
onChange={handleImageRadioChange}
|
||||
>
|
||||
{selectedHabitat && selectedHabitat.images && Array.isArray(selectedHabitat.images) && selectedHabitat.images.map(image =>
|
||||
<FormControlLabel key={image.id} value={image.id} control={<Radio />} label={getImageLabel(image)} sx={{ paddingTop: '15px', paddingBottom: '15px' }} />
|
||||
)}
|
||||
{(!selectedHabitat || (selectedHabitat.images && selectedHabitat.images.length === 0)) && <p>No images available.</p>}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
157
frontend/src/components/steps/habitat/HabitatSelector.jsx
Normal file
157
frontend/src/components/steps/habitat/HabitatSelector.jsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import SiteRepository from "../../../repository/SiteRepository";
|
||||
import * as React from "react";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import FormLabel from "@mui/material/FormLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import { HabitatSVG } from "../HabitatSVG";
|
||||
|
||||
export default function HabitatSelector(props) {
|
||||
const [habitats, setHabitats] = useState([]);
|
||||
const [habitatsMap, setHabitatsMap] = useState({});
|
||||
const [value, setValue] = React.useState(
|
||||
props.filters.habitat && props.filters.habitat.id
|
||||
);
|
||||
const [selectedHabitat, setSelectedHabitat] = React.useState({});
|
||||
const [imageValue, setImageValue] = React.useState(
|
||||
props.filters.habitatImage
|
||||
);
|
||||
|
||||
const getHabitats = () => {
|
||||
SiteRepository.getHabitats().then((response) => {
|
||||
if (response.status === 200) {
|
||||
// Create a mapping from the habitat id to the object
|
||||
let habitatsJson = {};
|
||||
response.data.forEach((habitat) => {
|
||||
habitatsJson[habitat.id] = habitat;
|
||||
});
|
||||
setHabitats(response.data);
|
||||
setHabitatsMap(habitatsJson);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Retrieves the habitats from the api if they are not loaded already
|
||||
habitats.length === 0 && getHabitats();
|
||||
|
||||
// Sets the selected habitat if its already set in filters
|
||||
if (props.filters.habitat && props.filters.habitat.id) {
|
||||
setSelectedHabitat(habitatsMap[props.filters.habitat.id]);
|
||||
}
|
||||
|
||||
// If both the habitat and the image is selected, then enable the next step
|
||||
if (props.filters.habitat && props.filters.habitatImage) {
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
}, [habitats.length, props, habitatsMap]);
|
||||
|
||||
const setHabitatImage = (imageId) => {
|
||||
// Sets the selected image radio, updates filter state for image and enable the next button
|
||||
setImageValue(imageId);
|
||||
props.updateFilterState({ habitatImage: imageId });
|
||||
props.updateFilterState({ zone: null });
|
||||
props.setNextDisabled(false);
|
||||
};
|
||||
|
||||
const handleRadioChange = (event) => {
|
||||
// Retrieve and set the value of the radio button
|
||||
const habitatSelection = event.target.value;
|
||||
setValue(habitatSelection);
|
||||
setHabitatImage(null);
|
||||
|
||||
// Set the image default selection if there is only one image variation available
|
||||
const habitatObject = habitatsMap[habitatSelection];
|
||||
setSelectedHabitat(habitatObject);
|
||||
if (habitatObject.images.length === 1) {
|
||||
setHabitatImage(habitatObject.images[0].id);
|
||||
} else {
|
||||
props.setNextDisabled(true);
|
||||
}
|
||||
|
||||
// Update the filters for the selected habitat
|
||||
props.updateFilterState({
|
||||
habitat: { id: habitatObject.id, name: habitatObject.name },
|
||||
});
|
||||
};
|
||||
|
||||
const handleImageRadioChange = (event) => {
|
||||
const habitatImageSelection = event.target.value;
|
||||
setHabitatImage(habitatImageSelection);
|
||||
};
|
||||
|
||||
const getImageLabel = (image) => {
|
||||
return (
|
||||
<div>
|
||||
<p>{image.name}</p>
|
||||
<HabitatSVG
|
||||
name={image.image_filename}
|
||||
style={{ height: "auto", width: "300px" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex align-top justify-space-between">
|
||||
<form>
|
||||
<FormControl sx={{ m: 3 }} component="fieldset" variant="standard">
|
||||
<FormLabel component="legend">
|
||||
{staticText.steps.habitat.optionsLabel}
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="habitat-selection"
|
||||
name="habitat selection"
|
||||
value={value ?? ""}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
{habitats &&
|
||||
Array.isArray(habitats) &&
|
||||
habitats.map((habitat) => (
|
||||
<FormControlLabel
|
||||
key={habitat.id}
|
||||
value={habitat.id}
|
||||
control={<Radio />}
|
||||
label={habitat.name}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</form>
|
||||
<form>
|
||||
<FormControl sx={{ m: 3 }} component="fieldset" variant="standard">
|
||||
<FormLabel component="legend">
|
||||
{staticText.steps.habitat.imageOptionsLabel}
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="habitat-image-selection"
|
||||
name="habitat image selection"
|
||||
value={imageValue ?? ""}
|
||||
onChange={handleImageRadioChange}
|
||||
>
|
||||
{selectedHabitat &&
|
||||
selectedHabitat.images &&
|
||||
Array.isArray(selectedHabitat.images) &&
|
||||
selectedHabitat.images.map((image) => (
|
||||
<FormControlLabel
|
||||
key={image.id}
|
||||
value={image.id}
|
||||
control={<Radio />}
|
||||
label={getImageLabel(image)}
|
||||
sx={{ paddingTop: "15px", paddingBottom: "15px" }}
|
||||
/>
|
||||
))}
|
||||
{(!selectedHabitat ||
|
||||
(selectedHabitat.images &&
|
||||
selectedHabitat.images.length === 0)) && (
|
||||
<p>No images available.</p>
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import Step from '../Step';
|
||||
import HabitatSelector from './HabitatSelector'
|
||||
import StepInformation from '../StepInformation';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
import habitatBackgroundImage from '../../../assets/img/stepBackgrounds/step3.jpg';
|
||||
|
||||
export default function HabitatStep(props) {
|
||||
const habitatInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.habitat.title}
|
||||
description={<p>{staticText.steps.habitat.description}</p>}
|
||||
/>
|
||||
)
|
||||
|
||||
const habitatSelectionPanel = (
|
||||
<HabitatSelector {...props} />
|
||||
)
|
||||
|
||||
return (
|
||||
<Step informationComponent={habitatInfoPanel} selectionComponent={habitatSelectionPanel} backgroundImage={habitatBackgroundImage} />
|
||||
)
|
||||
}
|
24
frontend/src/components/steps/habitat/HabitatStep.jsx
Normal file
24
frontend/src/components/steps/habitat/HabitatStep.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Step from "../Step";
|
||||
import HabitatSelector from "./HabitatSelector";
|
||||
import StepInformation from "../StepInformation";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import habitatBackgroundImage from "../../../assets/img/stepBackgrounds/step3.jpg";
|
||||
|
||||
export default function HabitatStep(props) {
|
||||
const habitatInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.habitat.title}
|
||||
description={<p>{staticText.steps.habitat.description}</p>}
|
||||
/>
|
||||
);
|
||||
|
||||
const habitatSelectionPanel = <HabitatSelector {...props} />;
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={habitatInfoPanel}
|
||||
selectionComponent={habitatSelectionPanel}
|
||||
backgroundImage={habitatBackgroundImage}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import Step from '../Step';
|
||||
import LocationSelectorMap from './Map'
|
||||
import StepInformation from '../StepInformation';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
import locationBackgroundImage from '../../../assets/img/stepBackgrounds/step1.jpg';
|
||||
|
||||
export default function LocationStep(props) {
|
||||
|
||||
const locationInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.location.title}
|
||||
description={<p>{staticText.steps.location.description}</p>}
|
||||
/>
|
||||
)
|
||||
|
||||
const locationSelectionPanel = (
|
||||
<LocationSelectorMap {...props} />
|
||||
)
|
||||
|
||||
return (
|
||||
<Step informationComponent={locationInfoPanel} selectionComponent={locationSelectionPanel} backgroundImage={locationBackgroundImage} />
|
||||
)
|
||||
}
|
24
frontend/src/components/steps/location/LocationStep.jsx
Normal file
24
frontend/src/components/steps/location/LocationStep.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Step from "../Step";
|
||||
import LocationSelectorMap from "./Map";
|
||||
import StepInformation from "../StepInformation";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import locationBackgroundImage from "../../../assets/img/stepBackgrounds/step1.jpg";
|
||||
|
||||
export default function LocationStep(props) {
|
||||
const locationInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.location.title}
|
||||
description={<p>{staticText.steps.location.description}</p>}
|
||||
/>
|
||||
);
|
||||
|
||||
const locationSelectionPanel = <LocationSelectorMap {...props} />;
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={locationInfoPanel}
|
||||
selectionComponent={locationSelectionPanel}
|
||||
backgroundImage={locationBackgroundImage}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,72 +1,72 @@
|
|||
import { useState, useEffect } from "react"
|
||||
import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet'
|
||||
import LocationRepository from '../../../repository/LocationRepository'
|
||||
import { useState, useEffect } from "react";
|
||||
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
|
||||
import LocationRepository from "../../../repository/LocationRepository";
|
||||
|
||||
const NZ_BOUNDS = [
|
||||
[-47.204642, 165.344238],
|
||||
[-34.307144, 179.824219]
|
||||
]
|
||||
[-34.307144, 179.824219],
|
||||
];
|
||||
|
||||
function LocationMarker(props) {
|
||||
const [position, setPosition] = useState(null)
|
||||
const [position, setPosition] = useState(null);
|
||||
const map = useMapEvents({
|
||||
click(e) {
|
||||
const newPosition = e.latlng;
|
||||
setPosition(newPosition);
|
||||
props.updateFilterState({ "coordinates": newPosition });
|
||||
props.updateFilterState({ coordinates: newPosition });
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
map.whenReady(() => {
|
||||
const savedCoordinates = props.filters["coordinates"];
|
||||
if (!position && savedCoordinates) {
|
||||
setPosition(savedCoordinates);
|
||||
props.setNextDisabled(false);
|
||||
map.flyTo(savedCoordinates, 9)
|
||||
map.flyTo(savedCoordinates, 9);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return position === null ? null : (
|
||||
<Marker position={position} />
|
||||
)
|
||||
return position === null ? null : <Marker position={position} />;
|
||||
}
|
||||
|
||||
const fitNZBounds = (map) => {
|
||||
map.fitBounds(NZ_BOUNDS);
|
||||
}
|
||||
};
|
||||
|
||||
function LocationDetailsDisplay(props) {
|
||||
const [locationDetails, setLocationDetails] = useState({})
|
||||
const [locationDetails, setLocationDetails] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (props.filters["coordinates"]) {
|
||||
LocationRepository.getLocationData(props.filters).then(result => {
|
||||
LocationRepository.getLocationData(props.filters).then((result) => {
|
||||
setLocationDetails(result);
|
||||
})
|
||||
}
|
||||
}, [props.filters, props.filters.coordinates] );
|
||||
});
|
||||
}
|
||||
}, [props.filters, props.filters.coordinates]);
|
||||
|
||||
const savedCoordinates = props.filters["coordinates"];
|
||||
|
||||
if (savedCoordinates) {
|
||||
const soilString = `${locationDetails.soil_name} (${locationDetails.soil_code})`
|
||||
const soilString = `${locationDetails.soil_name} (${locationDetails.soil_code})`;
|
||||
return (
|
||||
<div className='coordinates-display'>
|
||||
<div className="coordinates-display">
|
||||
<strong>Latitude:</strong> {savedCoordinates.lat} <br />
|
||||
<strong>Longitude:</strong> {savedCoordinates.lng} <br />
|
||||
<strong>Longitude:</strong> {savedCoordinates.lng} <br />
|
||||
<strong>Address:</strong> {locationDetails.full_address} <br />
|
||||
<strong>Ecological Region:</strong> {locationDetails.ecological_region} <br />
|
||||
<strong>Ecological District:</strong> {locationDetails.ecological_district} <br />
|
||||
<strong>Soil Order:</strong> {locationDetails.soil_name ? soilString : ""}
|
||||
<strong>Ecological Region:</strong> {locationDetails.ecological_region}{" "}
|
||||
<br />
|
||||
<strong>Ecological District:</strong>{" "}
|
||||
{locationDetails.ecological_district} <br />
|
||||
<strong>Soil Order:</strong>{" "}
|
||||
{locationDetails.soil_name ? soilString : ""}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
export default function Map(props) {
|
||||
return (
|
||||
<div className="map-container">
|
||||
|
@ -79,5 +79,5 @@ export default function Map(props) {
|
|||
<LocationMarker {...props} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormLabel from '@mui/material/FormLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
|
||||
const WET_SOIL_DESCRIPTION = (<p>
|
||||
Is your ground/soil <strong>VERY WET</strong>? i.e. is it soft and squishy under foot with ground water at or near surface for three months of year or more? Your soil is considered wet if the <Tooltip arrow title="Topsoil is the uppermost layer of soil – usually 5-20cm of dark soil with high concentrations of organic matter. Subsoil is the layer under the topsoil and generally contains less organic matter and more of the small particles such as sand, silt and clay that characterize the mineral and geology of the location."><strong>subsoil</strong></Tooltip> is either very dark brown and <Tooltip arrow title="Peat is an accumulation of partially decayed vegetation or organic matter, usually brown and often formed in waterlogged areas."><strong>peat</strong></Tooltip>-like, or grey rather than a red, brown, or yellow rusty colour?
|
||||
</p>);
|
||||
|
||||
const DRY_SOIL_DESCRIPTION = (<p>
|
||||
Is your ground/soil <strong>VERY DRY</strong>? i.e. If your soil is very well drained, stony, sandy or shallow (less than one metre depth to bed rock, stones or other hard/artificial surface), or is it completely dry for three months of year or more, and not irrigated then we consider it dry.
|
||||
</p>);
|
||||
|
||||
const MESIC_SOIL_DESCRIPTION = (<p>
|
||||
If your ground/soil does not meet either of the two previous definitions, we’d likely classify it as <strong>MOIST, or somewhere in between very wet and very dry</strong>. Your ground is considered moist if it has compacted soil that is fine or has a soft/crumbly texture. The sand, silt and clay mixture known as loam is considered moist in this context. Moist soils are more than one metre above an impervious layer, and are never water-logged nor are they completely dry for more than three months. If you are unsure, or if you plan to irrigate your site, then select this option.
|
||||
</p>);
|
||||
|
||||
export default function SoilSelector(props) {
|
||||
const [value, setValue] = React.useState(props.filters.soilVariant);
|
||||
const [helperText, setHelperText] = React.useState(staticText.steps.soil.optionsHelperText);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.filters.soilVariant) {
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
const handleRadioChange = (event) => {
|
||||
const soilVariantSelection = event.target.value;
|
||||
setValue(soilVariantSelection);
|
||||
setHelperText(' ');
|
||||
props.updateFilterState({ "soilVariant": soilVariantSelection });
|
||||
props.setNextDisabled(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form >
|
||||
<FormControl
|
||||
sx={{ m: 3 }}
|
||||
component="fieldset"
|
||||
variant="standard"
|
||||
>
|
||||
<FormLabel component="legend">{staticText.steps.soil.optionsLabel}</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="soil-varient-selection"
|
||||
name="soil varient selection"
|
||||
value={value ?? ""}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
<FormControlLabel value="D" control={<Radio />} label={DRY_SOIL_DESCRIPTION} sx={{ paddingTop: '15px', paddingBottom: '15px' }}/>
|
||||
<FormControlLabel value="W" control={<Radio />} label={WET_SOIL_DESCRIPTION} sx={{ paddingTop: '15px', paddingBottom: '15px' }} />
|
||||
<FormControlLabel value="M" control={<Radio />} label={MESIC_SOIL_DESCRIPTION} sx={{ paddingTop: '15px', paddingBottom: '15px' }} />
|
||||
</RadioGroup>
|
||||
<FormHelperText>{helperText}</FormHelperText>
|
||||
</FormControl>
|
||||
</form>
|
||||
);
|
||||
}
|
111
frontend/src/components/steps/soilvariant/SoilSelector.jsx
Normal file
111
frontend/src/components/steps/soilvariant/SoilSelector.jsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import * as React from "react";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import FormLabel from "@mui/material/FormLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import FormHelperText from "@mui/material/FormHelperText";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
|
||||
const WET_SOIL_DESCRIPTION = (
|
||||
<p>
|
||||
Is your ground/soil <strong>VERY WET</strong>? i.e. is it soft and squishy
|
||||
under foot with ground water at or near surface for three months of year or
|
||||
more? Your soil is considered wet if the{" "}
|
||||
<Tooltip
|
||||
arrow
|
||||
title="Topsoil is the uppermost layer of soil – usually 5-20cm of dark soil with high concentrations of organic matter. Subsoil is the layer under the topsoil and generally contains less organic matter and more of the small particles such as sand, silt and clay that characterize the mineral and geology of the location."
|
||||
>
|
||||
<strong>subsoil</strong>
|
||||
</Tooltip>{" "}
|
||||
is either very dark brown and{" "}
|
||||
<Tooltip
|
||||
arrow
|
||||
title="Peat is an accumulation of partially decayed vegetation or organic matter, usually brown and often formed in waterlogged areas."
|
||||
>
|
||||
<strong>peat</strong>
|
||||
</Tooltip>
|
||||
-like, or grey rather than a red, brown, or yellow rusty colour?
|
||||
</p>
|
||||
);
|
||||
|
||||
const DRY_SOIL_DESCRIPTION = (
|
||||
<p>
|
||||
Is your ground/soil <strong>VERY DRY</strong>? i.e. If your soil is very
|
||||
well drained, stony, sandy or shallow (less than one metre depth to bed
|
||||
rock, stones or other hard/artificial surface), or is it completely dry for
|
||||
three months of year or more, and not irrigated then we consider it dry.
|
||||
</p>
|
||||
);
|
||||
|
||||
const MESIC_SOIL_DESCRIPTION = (
|
||||
<p>
|
||||
If your ground/soil does not meet either of the two previous definitions,
|
||||
we’d likely classify it as{" "}
|
||||
<strong>MOIST, or somewhere in between very wet and very dry</strong>. Your
|
||||
ground is considered moist if it has compacted soil that is fine or has a
|
||||
soft/crumbly texture. The sand, silt and clay mixture known as loam is
|
||||
considered moist in this context. Moist soils are more than one metre above
|
||||
an impervious layer, and are never water-logged nor are they completely dry
|
||||
for more than three months. If you are unsure, or if you plan to irrigate
|
||||
your site, then select this option.
|
||||
</p>
|
||||
);
|
||||
|
||||
export default function SoilSelector(props) {
|
||||
const [value, setValue] = React.useState(props.filters.soilVariant);
|
||||
const [helperText, setHelperText] = React.useState(
|
||||
staticText.steps.soil.optionsHelperText
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.filters.soilVariant) {
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
const handleRadioChange = (event) => {
|
||||
const soilVariantSelection = event.target.value;
|
||||
setValue(soilVariantSelection);
|
||||
setHelperText(" ");
|
||||
props.updateFilterState({ soilVariant: soilVariantSelection });
|
||||
props.setNextDisabled(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form>
|
||||
<FormControl sx={{ m: 3 }} component="fieldset" variant="standard">
|
||||
<FormLabel component="legend">
|
||||
{staticText.steps.soil.optionsLabel}
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="soil-varient-selection"
|
||||
name="soil varient selection"
|
||||
value={value ?? ""}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="D"
|
||||
control={<Radio />}
|
||||
label={DRY_SOIL_DESCRIPTION}
|
||||
sx={{ paddingTop: "15px", paddingBottom: "15px" }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="W"
|
||||
control={<Radio />}
|
||||
label={WET_SOIL_DESCRIPTION}
|
||||
sx={{ paddingTop: "15px", paddingBottom: "15px" }}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="M"
|
||||
control={<Radio />}
|
||||
label={MESIC_SOIL_DESCRIPTION}
|
||||
sx={{ paddingTop: "15px", paddingBottom: "15px" }}
|
||||
/>
|
||||
</RadioGroup>
|
||||
<FormHelperText>{helperText}</FormHelperText>
|
||||
</FormControl>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import Step from '../Step';
|
||||
import SoilSelector from './SoilSelector';
|
||||
import StepInformation from '../StepInformation';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
import soilBackgroundImage from '../../../assets/img/stepBackgrounds/step2.jpg';
|
||||
|
||||
export default function SoilVariantStep(props) {
|
||||
const SOIL_DESCRIPTION = (<p>
|
||||
From your site location, we use <a href="https://soils.landcareresearch.co.nz/describing-soils/nzsc/" target="blank">New Zealand Soil Classification</a> data and additional <a href="https://www.landcareresearch.co.nz" target="blank">Manaaki Whenua Landcare Research</a> data to determine overall soil type. However we also need to know as much as possible about the moisture content of the soil at your planting site location.
|
||||
<br /><br />
|
||||
Please select an option from the choices listed to the right:
|
||||
</p>)
|
||||
|
||||
const soilVarientInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.soil.title}
|
||||
description={SOIL_DESCRIPTION}
|
||||
/>
|
||||
)
|
||||
|
||||
const soilVarientSelectionPanel = (
|
||||
<div className='p-5'>
|
||||
<SoilSelector {...props} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Step informationComponent={soilVarientInfoPanel} selectionComponent={soilVarientSelectionPanel} backgroundImage={soilBackgroundImage} />
|
||||
)
|
||||
}
|
50
frontend/src/components/steps/soilvariant/SoilStep.jsx
Normal file
50
frontend/src/components/steps/soilvariant/SoilStep.jsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import Step from "../Step";
|
||||
import SoilSelector from "./SoilSelector";
|
||||
import StepInformation from "../StepInformation";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import soilBackgroundImage from "../../../assets/img/stepBackgrounds/step2.jpg";
|
||||
|
||||
export default function SoilVariantStep(props) {
|
||||
const SOIL_DESCRIPTION = (
|
||||
<p>
|
||||
From your site location, we use{" "}
|
||||
<a
|
||||
href="https://soils.landcareresearch.co.nz/describing-soils/nzsc/"
|
||||
target="blank"
|
||||
>
|
||||
New Zealand Soil Classification
|
||||
</a>{" "}
|
||||
data and additional{" "}
|
||||
<a href="https://www.landcareresearch.co.nz" target="blank">
|
||||
Manaaki Whenua Landcare Research
|
||||
</a>{" "}
|
||||
data to determine overall soil type. However we also need to know as much
|
||||
as possible about the moisture content of the soil at your planting site
|
||||
location.
|
||||
<br />
|
||||
<br />
|
||||
Please select an option from the choices listed to the right:
|
||||
</p>
|
||||
);
|
||||
|
||||
const soilVarientInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.soil.title}
|
||||
description={SOIL_DESCRIPTION}
|
||||
/>
|
||||
);
|
||||
|
||||
const soilVarientSelectionPanel = (
|
||||
<div className="p-5">
|
||||
<SoilSelector {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={soilVarientInfoPanel}
|
||||
selectionComponent={soilVarientSelectionPanel}
|
||||
backgroundImage={soilBackgroundImage}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export default function ProjectSpecificsSelector(props) {
|
||||
|
||||
useEffect(() => {
|
||||
props.setNextDisabled(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<h2>Project Specifics Selector</h2>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
export default function ProjectSpecificsSelector(props) {
|
||||
useEffect(() => {
|
||||
props.setNextDisabled(false);
|
||||
});
|
||||
|
||||
return <h2>Project Specifics Selector</h2>;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import Step from '../Step';
|
||||
import ProjectSpecificsSelector from './ProjectSpecificsSelector'
|
||||
import StepInformation from '../StepInformation';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
|
||||
export default function ProjectSpecificsStep(props) {
|
||||
const projectSpecificsInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.projectSpecifics.title}
|
||||
description={<p>{staticText.steps.projectSpecifics.description}</p>}
|
||||
/>
|
||||
)
|
||||
|
||||
const projectSpecificsSelectionPanel = (
|
||||
<ProjectSpecificsSelector {...props} />
|
||||
)
|
||||
|
||||
return (
|
||||
<Step informationComponent={projectSpecificsInfoPanel} selectionComponent={projectSpecificsSelectionPanel} />
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import Step from "../Step";
|
||||
import ProjectSpecificsSelector from "./ProjectSpecificsSelector";
|
||||
import StepInformation from "../StepInformation";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
|
||||
export default function ProjectSpecificsStep(props) {
|
||||
const projectSpecificsInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.projectSpecifics.title}
|
||||
description={<p>{staticText.steps.projectSpecifics.description}</p>}
|
||||
/>
|
||||
);
|
||||
|
||||
const projectSpecificsSelectionPanel = (
|
||||
<ProjectSpecificsSelector {...props} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={projectSpecificsInfoPanel}
|
||||
selectionComponent={projectSpecificsSelectionPanel}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import Accordion from '@mui/material/Accordion';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
import SummaryTable from './SummaryTable'
|
||||
import LocationRepository from '../../../repository/LocationRepository'
|
||||
|
||||
|
||||
export default function SummaryContent(props) {
|
||||
const [expanded, setExpanded] = React.useState(null);
|
||||
const [locationDetails, setLocationDetails] = React.useState({})
|
||||
|
||||
const getLocationDetails = () => {
|
||||
LocationRepository.getLocationData(props.filters).then(result => {
|
||||
setLocationDetails(result);
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (panel) => (event, isExpanded) => {
|
||||
setExpanded(isExpanded ? panel : false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
props.setNextDisabled(false);
|
||||
!Object.keys(locationDetails).length && getLocationDetails()
|
||||
});
|
||||
|
||||
function createData(name, value) {
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
const locationData = [
|
||||
createData('Geographical Coordinates (latitude, longitude)', `(${props.filters.coordinates.lat}, ${props.filters.coordinates.lng})`),
|
||||
createData('Ecological Region', locationDetails.ecological_region || ''),
|
||||
createData('Ecological District', locationDetails.ecological_district || ''),
|
||||
createData('Property Name', locationDetails.full_address || ''),
|
||||
];
|
||||
|
||||
const soilData = [
|
||||
createData('Soil Order', `${locationDetails.soil_name} (${locationDetails.soil_code})` || ''),
|
||||
createData('Soil Variant', props.filters.soilVariant)
|
||||
];
|
||||
|
||||
const siteData = [
|
||||
createData('Habitat', props.filters.habitat.name ?? ""),
|
||||
createData('Zone Name', (props.filters.zone && props.filters.zone.name) ?? ""),
|
||||
createData('Zone Variant', (props.filters.zone && props.filters.zone.variant) ?? ""),
|
||||
createData('Zone Refined Variant', (props.filters.zone && props.filters.zone.refined_variant) ?? "")
|
||||
];
|
||||
|
||||
const regionInformation = () => {
|
||||
if (locationDetails.in_chch) {
|
||||
return <Typography variant='body2'>Your location falls within the ecosystem type covered by the Christchurch Council ecosystem maps - further information can be obtained from <a href="https://ccc.govt.nz/environment/land/ecosystem-map" target="blank" >Ōtautahi/Christchurch ecosystems map</a>.</Typography>
|
||||
} else if (locationDetails.in_auckland) {
|
||||
return <Typography variant='body2'>"Your location falls within the ecosystem type covered by the Auckland Council Tiaki Tāmaki Makaurau Conservation map - further information can be obtained from <a href="https://www.tiakitamakimakaurau.nz/conservation-map/" target="blank" >tiaki Tāmaki Makaurau conservation Auckland</a></Typography>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='summary-content'>
|
||||
<Typography mb={2} mt={2} >Please review your choices presented below: </Typography>
|
||||
<Accordion expanded={expanded === 'panel1'} onChange={handleChange('panel1')} >
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
<Typography>1. Location Data</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<SummaryTable rows={locationData} />
|
||||
<Box ml={2} mt={2}>
|
||||
{regionInformation()}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion expanded={expanded === 'panel2'} onChange={handleChange('panel2')} >
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
<Typography>2. Soil Properties</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<SummaryTable rows={soilData} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion expanded={expanded === 'panel3'} onChange={handleChange('panel3')} >
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
<Typography>3. Project Site Details</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<SummaryTable rows={siteData} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
148
frontend/src/components/steps/summary/SummaryContent.jsx
Normal file
148
frontend/src/components/steps/summary/SummaryContent.jsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
import * as React from "react";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
|
||||
import SummaryTable from "./SummaryTable";
|
||||
import LocationRepository from "../../../repository/LocationRepository";
|
||||
|
||||
export default function SummaryContent(props) {
|
||||
const [expanded, setExpanded] = React.useState(null);
|
||||
const [locationDetails, setLocationDetails] = React.useState({});
|
||||
|
||||
const getLocationDetails = () => {
|
||||
LocationRepository.getLocationData(props.filters).then((result) => {
|
||||
setLocationDetails(result);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (panel) => (event, isExpanded) => {
|
||||
setExpanded(isExpanded ? panel : false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
props.setNextDisabled(false);
|
||||
!Object.keys(locationDetails).length && getLocationDetails();
|
||||
});
|
||||
|
||||
function createData(name, value) {
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
const locationData = [
|
||||
createData(
|
||||
"Geographical Coordinates (latitude, longitude)",
|
||||
`(${props.filters.coordinates.lat}, ${props.filters.coordinates.lng})`
|
||||
),
|
||||
createData("Ecological Region", locationDetails.ecological_region || ""),
|
||||
createData(
|
||||
"Ecological District",
|
||||
locationDetails.ecological_district || ""
|
||||
),
|
||||
createData("Property Name", locationDetails.full_address || ""),
|
||||
];
|
||||
|
||||
const soilData = [
|
||||
createData(
|
||||
"Soil Order",
|
||||
`${locationDetails.soil_name} (${locationDetails.soil_code})` || ""
|
||||
),
|
||||
createData("Soil Variant", props.filters.soilVariant),
|
||||
];
|
||||
|
||||
const siteData = [
|
||||
createData("Habitat", props.filters.habitat.name ?? ""),
|
||||
createData(
|
||||
"Zone Name",
|
||||
(props.filters.zone && props.filters.zone.name) ?? ""
|
||||
),
|
||||
createData(
|
||||
"Zone Variant",
|
||||
(props.filters.zone && props.filters.zone.variant) ?? ""
|
||||
),
|
||||
createData(
|
||||
"Zone Refined Variant",
|
||||
(props.filters.zone && props.filters.zone.refined_variant) ?? ""
|
||||
),
|
||||
];
|
||||
|
||||
const regionInformation = () => {
|
||||
if (locationDetails.in_chch) {
|
||||
return (
|
||||
<Typography variant="body2">
|
||||
Your location falls within the ecosystem type covered by the
|
||||
Christchurch Council ecosystem maps - further information can be
|
||||
obtained from{" "}
|
||||
<a
|
||||
href="https://ccc.govt.nz/environment/land/ecosystem-map"
|
||||
target="blank"
|
||||
>
|
||||
Ōtautahi/Christchurch ecosystems map
|
||||
</a>
|
||||
.
|
||||
</Typography>
|
||||
);
|
||||
} else if (locationDetails.in_auckland) {
|
||||
return (
|
||||
<Typography variant="body2">
|
||||
"Your location falls within the ecosystem type covered by the Auckland
|
||||
Council Tiaki Tāmaki Makaurau Conservation map - further information
|
||||
can be obtained from{" "}
|
||||
<a
|
||||
href="https://www.tiakitamakimakaurau.nz/conservation-map/"
|
||||
target="blank"
|
||||
>
|
||||
tiaki Tāmaki Makaurau conservation Auckland
|
||||
</a>
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="summary-content">
|
||||
<Typography mb={2} mt={2}>
|
||||
Please review your choices presented below:{" "}
|
||||
</Typography>
|
||||
<Accordion
|
||||
expanded={expanded === "panel1"}
|
||||
onChange={handleChange("panel1")}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>1. Location Data</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<SummaryTable rows={locationData} />
|
||||
<Box ml={2} mt={2}>
|
||||
{regionInformation()}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
expanded={expanded === "panel2"}
|
||||
onChange={handleChange("panel2")}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>2. Soil Properties</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<SummaryTable rows={soilData} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion
|
||||
expanded={expanded === "panel3"}
|
||||
onChange={handleChange("panel3")}
|
||||
>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>3. Project Site Details</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<SummaryTable rows={siteData} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import Step from '../Step';
|
||||
import SummaryContent from './SummaryContent'
|
||||
import StepInformation from '../StepInformation';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
import summaryBackgroundImage from '../../../assets/img/stepBackgrounds/step5.jpg';
|
||||
|
||||
export default function SummaryStep(props) {
|
||||
const summaryInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.summary.title}
|
||||
description={<p>{staticText.steps.summary.description}</p>}
|
||||
/>
|
||||
)
|
||||
|
||||
const summaryContent = (
|
||||
<SummaryContent {...props} />
|
||||
)
|
||||
|
||||
return (
|
||||
<Step informationComponent={summaryInfoPanel} selectionComponent={summaryContent} backgroundImage={summaryBackgroundImage} />
|
||||
)
|
||||
}
|
24
frontend/src/components/steps/summary/SummaryStep.jsx
Normal file
24
frontend/src/components/steps/summary/SummaryStep.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Step from "../Step";
|
||||
import SummaryContent from "./SummaryContent";
|
||||
import StepInformation from "../StepInformation";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import summaryBackgroundImage from "../../../assets/img/stepBackgrounds/step5.jpg";
|
||||
|
||||
export default function SummaryStep(props) {
|
||||
const summaryInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.summary.title}
|
||||
description={<p>{staticText.steps.summary.description}</p>}
|
||||
/>
|
||||
);
|
||||
|
||||
const summaryContent = <SummaryContent {...props} />;
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={summaryInfoPanel}
|
||||
selectionComponent={summaryContent}
|
||||
backgroundImage={summaryBackgroundImage}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
import * as React from 'react';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import * as React from "react";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
|
||||
export default function BasicTable(props) {
|
||||
return (
|
||||
<TableContainer >
|
||||
<TableContainer>
|
||||
<Table sx={{ minWidth: 650 }} size="small" aria-label="simple table">
|
||||
<TableBody>
|
||||
{props.rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.name}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell component="th" scope="row">
|
||||
{row.name}
|
|
@ -1,117 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import SiteRepository from '../../../repository/SiteRepository';
|
||||
import { HabitatSVG } from '../HabitatSVG'
|
||||
|
||||
export default function ZoneSelector(props) {
|
||||
|
||||
const [habitatImageObject, setHabitatImageObject] = useState({});
|
||||
const [segmentMapping, setSegmentMapping] = useState({});
|
||||
const [selectedZoneSegment, setZoneSegment] = useState(null);
|
||||
|
||||
|
||||
const setZoneOrRedirect = (zone) => {
|
||||
const redirectHabitat = zone && zone.redirect_habitat;
|
||||
|
||||
// If there is a redirect habitat set then redirect otherwise set the zone as state
|
||||
const newFilterState = redirectHabitat ? { "habitat": { "id": redirectHabitat.id, "name": redirectHabitat.name } } : { "zone": zone };
|
||||
|
||||
props.updateFilterState(newFilterState);
|
||||
props.setRedirectBack(Boolean(redirectHabitat));
|
||||
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const getHabitatImage = () => {
|
||||
SiteRepository.getHabitatImage(props.filters.habitatImage).then(response => {
|
||||
if (response.status === 200) {
|
||||
const imageData = response.data
|
||||
setHabitatImageObject(imageData)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setInitialZone = () => {
|
||||
if (props.filters.zone) {
|
||||
const zone = props.filters.zone
|
||||
const zoneSegment = document.querySelectorAll(`.zone-selector-svg svg path[inkscapelabel="${zone.related_svg_segment}"]`)[0];
|
||||
|
||||
// If the previously selected zone segment is located in the diagram then highlight it
|
||||
if (zoneSegment) {
|
||||
setZoneSegment(zoneSegment)
|
||||
zoneSegment.style.fill = "#eeeeee"
|
||||
zoneSegment.style['fill-opacity'] = 0.5;
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getZones = () => {
|
||||
SiteRepository.getZones().then(response => {
|
||||
if (response.status === 200) {
|
||||
const zoneData = response.data
|
||||
let newSegmentMapping = {}
|
||||
zoneData.forEach(zone => {
|
||||
newSegmentMapping[zone.related_svg_segment] = zone
|
||||
});
|
||||
setSegmentMapping(newSegmentMapping);
|
||||
setInitialZone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Retrieves the habitat image from the api if it's not loaded already
|
||||
Object.keys(habitatImageObject).length === 0 && getHabitatImage()
|
||||
|
||||
// Retrieves the habitat image from the api if it's not loaded already
|
||||
Object.keys(segmentMapping).length === 0 && getZones()
|
||||
|
||||
}, [habitatImageObject, segmentMapping, props, props.filters.zone]);
|
||||
|
||||
const selectZone = (element) => {
|
||||
if (['path', 'rect'].includes(element.tagName) && element.attributes.inkscapelabel && element.attributes.inkscapelabel.nodeValue) {
|
||||
if (selectedZoneSegment) {
|
||||
selectedZoneSegment.style['fill-opacity'] = 0;
|
||||
}
|
||||
|
||||
setZoneSegment(element)
|
||||
element.style.fill = "#eeeeee"
|
||||
element.style['fill-opacity'] = 0.5;
|
||||
|
||||
const zone = segmentMapping[element.attributes.inkscapelabel.nodeValue]
|
||||
|
||||
setZoneOrRedirect(zone)
|
||||
}
|
||||
}
|
||||
|
||||
const showTooltip = (evt) => {
|
||||
const element = evt.target;
|
||||
if (['path', 'rect'].includes(element.tagName) && element.attributes.inkscapelabel && element.attributes.inkscapelabel.nodeValue) {
|
||||
const zone = segmentMapping[element.attributes.inkscapelabel.nodeValue]
|
||||
|
||||
if (zone) {
|
||||
let tooltip = document.getElementById("svg-tooltip");
|
||||
tooltip.innerHTML = zone.tooltip_display_text;
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.left = evt.pageX + 10 + 'px';
|
||||
tooltip.style.top = evt.pageY + 10 + 'px';
|
||||
tooltip.style.opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hideTooltip = () => {
|
||||
var tooltip = document.getElementById("svg-tooltip");
|
||||
tooltip.style.display = "none";
|
||||
}
|
||||
|
||||
return Object.keys(habitatImageObject).length > 0 ? (
|
||||
<div style={{ width: '100%' }}>
|
||||
<p>{habitatImageObject.name}</p>
|
||||
<div className="zone-selector-svg">
|
||||
<div id="svg-tooltip" className="MuiTooltip-tooltip MuiTooltip-tooltipArrow MuiTooltip-tooltipPlacementBottom" display="none" style={{ "position": 'absolute', "display": "none" }} ></div>
|
||||
<HabitatSVG onClick={(event) => selectZone(event.target)} onMouseMove={(event) => showTooltip(event)} onMouseOut={() => hideTooltip()} name={habitatImageObject.image_filename} style={{ "height": 'auto', "width": "100%" }} />
|
||||
</div>
|
||||
</div>) : <div></div>
|
||||
}
|
141
frontend/src/components/steps/zone/ZoneSelector.jsx
Normal file
141
frontend/src/components/steps/zone/ZoneSelector.jsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import SiteRepository from "../../../repository/SiteRepository";
|
||||
import { HabitatSVG } from "../HabitatSVG";
|
||||
|
||||
export default function ZoneSelector(props) {
|
||||
const [habitatImageObject, setHabitatImageObject] = useState({});
|
||||
const [segmentMapping, setSegmentMapping] = useState({});
|
||||
const [selectedZoneSegment, setZoneSegment] = useState(null);
|
||||
|
||||
const setZoneOrRedirect = (zone) => {
|
||||
const redirectHabitat = zone && zone.redirect_habitat;
|
||||
|
||||
// If there is a redirect habitat set then redirect otherwise set the zone as state
|
||||
const newFilterState = redirectHabitat
|
||||
? { habitat: { id: redirectHabitat.id, name: redirectHabitat.name } }
|
||||
: { zone: zone };
|
||||
|
||||
props.updateFilterState(newFilterState);
|
||||
props.setRedirectBack(Boolean(redirectHabitat));
|
||||
|
||||
props.setNextDisabled(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getHabitatImage = () => {
|
||||
SiteRepository.getHabitatImage(props.filters.habitatImage).then(
|
||||
(response) => {
|
||||
if (response.status === 200) {
|
||||
const imageData = response.data;
|
||||
setHabitatImageObject(imageData);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setInitialZone = () => {
|
||||
if (props.filters.zone) {
|
||||
const zone = props.filters.zone;
|
||||
const zoneSegment = document.querySelectorAll(
|
||||
`.zone-selector-svg svg path[inkscapelabel="${zone.related_svg_segment}"]`
|
||||
)[0];
|
||||
|
||||
// If the previously selected zone segment is located in the diagram then highlight it
|
||||
if (zoneSegment) {
|
||||
setZoneSegment(zoneSegment);
|
||||
zoneSegment.style.fill = "#eeeeee";
|
||||
zoneSegment.style["fill-opacity"] = 0.5;
|
||||
props.setNextDisabled(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getZones = () => {
|
||||
SiteRepository.getZones().then((response) => {
|
||||
if (response.status === 200) {
|
||||
const zoneData = response.data;
|
||||
let newSegmentMapping = {};
|
||||
zoneData.forEach((zone) => {
|
||||
newSegmentMapping[zone.related_svg_segment] = zone;
|
||||
});
|
||||
setSegmentMapping(newSegmentMapping);
|
||||
setInitialZone();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Retrieves the habitat image from the api if it's not loaded already
|
||||
Object.keys(habitatImageObject).length === 0 && getHabitatImage();
|
||||
|
||||
// Retrieves the habitat image from the api if it's not loaded already
|
||||
Object.keys(segmentMapping).length === 0 && getZones();
|
||||
}, [habitatImageObject, segmentMapping, props, props.filters.zone]);
|
||||
|
||||
const selectZone = (element) => {
|
||||
if (
|
||||
["path", "rect"].includes(element.tagName) &&
|
||||
element.attributes.inkscapelabel &&
|
||||
element.attributes.inkscapelabel.nodeValue
|
||||
) {
|
||||
if (selectedZoneSegment) {
|
||||
selectedZoneSegment.style["fill-opacity"] = 0;
|
||||
}
|
||||
|
||||
setZoneSegment(element);
|
||||
element.style.fill = "#eeeeee";
|
||||
element.style["fill-opacity"] = 0.5;
|
||||
|
||||
const zone = segmentMapping[element.attributes.inkscapelabel.nodeValue];
|
||||
|
||||
setZoneOrRedirect(zone);
|
||||
}
|
||||
};
|
||||
|
||||
const showTooltip = (evt) => {
|
||||
const element = evt.target;
|
||||
if (
|
||||
["path", "rect"].includes(element.tagName) &&
|
||||
element.attributes.inkscapelabel &&
|
||||
element.attributes.inkscapelabel.nodeValue
|
||||
) {
|
||||
const zone = segmentMapping[element.attributes.inkscapelabel.nodeValue];
|
||||
|
||||
if (zone) {
|
||||
let tooltip = document.getElementById("svg-tooltip");
|
||||
tooltip.innerHTML = zone.tooltip_display_text;
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.left = evt.pageX + 10 + "px";
|
||||
tooltip.style.top = evt.pageY + 10 + "px";
|
||||
tooltip.style.opacity = 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hideTooltip = () => {
|
||||
var tooltip = document.getElementById("svg-tooltip");
|
||||
tooltip.style.display = "none";
|
||||
};
|
||||
|
||||
return Object.keys(habitatImageObject).length > 0 ? (
|
||||
<div style={{ width: "100%" }}>
|
||||
<p>{habitatImageObject.name}</p>
|
||||
<div className="zone-selector-svg">
|
||||
<div
|
||||
id="svg-tooltip"
|
||||
className="MuiTooltip-tooltip MuiTooltip-tooltipArrow MuiTooltip-tooltipPlacementBottom"
|
||||
display="none"
|
||||
style={{ position: "absolute", display: "none" }}
|
||||
></div>
|
||||
<HabitatSVG
|
||||
onClick={(event) => selectZone(event.target)}
|
||||
onMouseMove={(event) => showTooltip(event)}
|
||||
onMouseOut={() => hideTooltip()}
|
||||
name={habitatImageObject.image_filename}
|
||||
style={{ height: "auto", width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import Step from '../Step';
|
||||
import ZoneSelector from './ZoneSelector'
|
||||
import StepInformation from '../StepInformation';
|
||||
import staticText from '../../../assets/data/staticText.json'
|
||||
import zoneBackgroundImage from '../../../assets/img/stepBackgrounds/step4.jpg';
|
||||
|
||||
export default function ZoneStep(props) {
|
||||
const zoneInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.zone.title}
|
||||
description={<p>{staticText.steps.zone.description}</p>}
|
||||
/>
|
||||
)
|
||||
|
||||
const zoneSelectionPanel = (
|
||||
<div style={{padding: '30px', height: "100%", width: "90%", display: "flex", justifyContent: 'center', alignItems: "center"}}>
|
||||
<ZoneSelector {...props} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Step informationComponent={zoneInfoPanel} selectionComponent={zoneSelectionPanel} backgroundImage={zoneBackgroundImage} />
|
||||
)
|
||||
}
|
37
frontend/src/components/steps/zone/ZoneStep.jsx
Normal file
37
frontend/src/components/steps/zone/ZoneStep.jsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import Step from "../Step";
|
||||
import ZoneSelector from "./ZoneSelector";
|
||||
import StepInformation from "../StepInformation";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import zoneBackgroundImage from "../../../assets/img/stepBackgrounds/step4.jpg";
|
||||
|
||||
export default function ZoneStep(props) {
|
||||
const zoneInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.zone.title}
|
||||
description={<p>{staticText.steps.zone.description}</p>}
|
||||
/>
|
||||
);
|
||||
|
||||
const zoneSelectionPanel = (
|
||||
<div
|
||||
style={{
|
||||
padding: "30px",
|
||||
height: "100%",
|
||||
width: "90%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ZoneSelector {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={zoneInfoPanel}
|
||||
selectionComponent={zoneSelectionPanel}
|
||||
backgroundImage={zoneBackgroundImage}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,17 +1,44 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import MainPage from "./pages/MainPage";
|
||||
|
||||
// Styles
|
||||
import './assets/styles/main.scss';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import "./assets/styles/main.scss";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import ApplyPage from "./pages/ApplyPage";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <MainPage />,
|
||||
},
|
||||
{
|
||||
path: "/apply",
|
||||
element: <ApplyPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: "dark",
|
||||
},
|
||||
typography: {
|
||||
fontFamily: "Poppins, sans-serif",
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<div className="App">
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
|
45
frontend/src/pages/ApplyPage.jsx
Normal file
45
frontend/src/pages/ApplyPage.jsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { Container } from "reactstrap";
|
||||
import Stepper from "../components/Stepper";
|
||||
import Header from "../components/Header";
|
||||
import AddressStep from "../components/steps/address/AddressStep";
|
||||
import SoilStep from "../components/steps/soilvariant/SoilStep";
|
||||
import HabitatStep from "../components/steps/habitat/HabitatStep";
|
||||
import ZoneStep from "../components/steps/zone/ZoneStep";
|
||||
import SummaryStep from "../components/steps/summary/SummaryStep";
|
||||
|
||||
const ApplyPage = () => (
|
||||
<Container fluid className="main-container p-0">
|
||||
<Header />
|
||||
<Stepper
|
||||
steps={[
|
||||
{
|
||||
label: "Enter address",
|
||||
component: AddressStep,
|
||||
tooltip: "Enter your address",
|
||||
},
|
||||
{
|
||||
label: "Choose soil",
|
||||
component: SoilStep,
|
||||
tooltip: "Describe the moisture content of your soil",
|
||||
},
|
||||
{
|
||||
label: "Choose habitat",
|
||||
component: HabitatStep,
|
||||
tooltip: "Specify type of landscape to be planted",
|
||||
},
|
||||
{
|
||||
label: "Select zone",
|
||||
component: ZoneStep,
|
||||
tooltip: "Specify geographical detail",
|
||||
},
|
||||
{
|
||||
label: "Submit",
|
||||
component: SummaryStep,
|
||||
tooltip: "Submit your application",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default ApplyPage;
|
|
@ -1,39 +0,0 @@
|
|||
import React from 'react'
|
||||
import { Container } from 'reactstrap';
|
||||
import Stepper from '../components/Stepper'
|
||||
import Header from '../components/Header'
|
||||
export default class MainPage extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filters: {}
|
||||
}
|
||||
|
||||
this.updateFilterState = this.updateFilterState.bind(this);
|
||||
this.resetFilterState = this.resetFilterState.bind(this);
|
||||
}
|
||||
|
||||
|
||||
updateFilterState(newFilter) {
|
||||
this.setState({ filters: Object.assign(this.state.filters, newFilter) })
|
||||
}
|
||||
|
||||
resetFilterState() {
|
||||
this.setState({ filters: {} })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container fluid className='main-container p-0'>
|
||||
<Header/>
|
||||
<Stepper
|
||||
filters={this.state.filters}
|
||||
updateFilterState={this.updateFilterState}
|
||||
resetFilterState={this.resetFilterState} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
51
frontend/src/pages/MainPage.jsx
Normal file
51
frontend/src/pages/MainPage.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { Container } from "reactstrap";
|
||||
import Stepper from "../components/Stepper";
|
||||
import Header from "../components/Header";
|
||||
import LocationStep from "../components/steps/location/LocationStep";
|
||||
import SoilStep from "../components/steps/soilvariant/SoilStep";
|
||||
import HabitatStep from "../components/steps/habitat/HabitatStep";
|
||||
import ZoneStep from "../components/steps/zone/ZoneStep";
|
||||
import SummaryStep from "../components/steps/summary/SummaryStep";
|
||||
import ResultsStep from "../components/steps/results/ResultsStep";
|
||||
|
||||
const MainPage = () => (
|
||||
<Container fluid className="main-container p-0">
|
||||
<Header />
|
||||
<Stepper
|
||||
steps={[
|
||||
{
|
||||
label: "Select location",
|
||||
component: LocationStep,
|
||||
tooltip: "Click on a location on the map",
|
||||
},
|
||||
{
|
||||
label: "Choose soil",
|
||||
component: SoilStep,
|
||||
tooltip: "Describe the moisture content of your soil",
|
||||
},
|
||||
{
|
||||
label: "Choose habitat",
|
||||
component: HabitatStep,
|
||||
tooltip: "Specify type of landscape to be planted",
|
||||
},
|
||||
{
|
||||
label: "Select zone",
|
||||
component: ZoneStep,
|
||||
tooltip: "Specify geographical detail",
|
||||
},
|
||||
{
|
||||
label: "Summary",
|
||||
component: SummaryStep,
|
||||
tooltip: "Check your inputs",
|
||||
},
|
||||
{
|
||||
label: "Results",
|
||||
component: ResultsStep,
|
||||
tooltip: "List of plant species and user guide",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default MainPage;
|
|
@ -1,40 +1,55 @@
|
|||
import API from "./Repository";
|
||||
|
||||
|
||||
const LocationRepsostory = {
|
||||
getSoilDetails(filters) {
|
||||
return API.get(`/soil/`, { params: filters });
|
||||
},
|
||||
|
||||
getSoilDetails(filters) {
|
||||
return API.get(`/soil/`, { params: filters });
|
||||
},
|
||||
getEcologicalDistrictDetails(filters) {
|
||||
return API.get(`/ecologicaldistrict/`, { params: filters });
|
||||
},
|
||||
|
||||
getEcologicalDistrictDetails(filters) {
|
||||
return API.get(`/ecologicaldistrict/`, { params: filters });
|
||||
},
|
||||
getRegionDetails(filters) {
|
||||
return API.get(`/region/`, { params: filters });
|
||||
},
|
||||
|
||||
getRegionDetails(filters) {
|
||||
return API.get(`/region/`, { params: filters });
|
||||
},
|
||||
getPropertyDetails(filters) {
|
||||
return API.get(`/address/`, { params: filters });
|
||||
},
|
||||
|
||||
getPropertyDetails(filters) {
|
||||
return API.get(`/address/`, { params: filters });
|
||||
},
|
||||
async getLocationData(filters) {
|
||||
const [
|
||||
soilDetails,
|
||||
ecologicalDistrictDetails,
|
||||
propertyDetails,
|
||||
regionDetails,
|
||||
] = await Promise.all([
|
||||
this.getSoilDetails(filters),
|
||||
this.getEcologicalDistrictDetails(filters),
|
||||
this.getPropertyDetails(filters),
|
||||
this.getRegionDetails(filters),
|
||||
]);
|
||||
|
||||
async getLocationData(filters) {
|
||||
const [soilDetails, ecologicalDistrictDetails, propertyDetails, regionDetails] = await Promise.all([
|
||||
this.getSoilDetails(filters),
|
||||
this.getEcologicalDistrictDetails(filters),
|
||||
this.getPropertyDetails(filters),
|
||||
this.getRegionDetails(filters)
|
||||
]);
|
||||
let locationData = {};
|
||||
locationData =
|
||||
soilDetails.status === 200
|
||||
? Object.assign(locationData, soilDetails.data[0])
|
||||
: locationData;
|
||||
locationData =
|
||||
ecologicalDistrictDetails.status === 200
|
||||
? Object.assign(locationData, ecologicalDistrictDetails.data[0])
|
||||
: locationData;
|
||||
locationData =
|
||||
propertyDetails.status === 200
|
||||
? Object.assign(locationData, propertyDetails.data)
|
||||
: locationData;
|
||||
locationData =
|
||||
regionDetails.status === 200
|
||||
? Object.assign(locationData, regionDetails.data)
|
||||
: locationData;
|
||||
|
||||
let locationData = {};
|
||||
locationData = soilDetails.status === 200 ? Object.assign(locationData, soilDetails.data[0]) : locationData;
|
||||
locationData = ecologicalDistrictDetails.status === 200 ? Object.assign(locationData, ecologicalDistrictDetails.data[0]) : locationData;
|
||||
locationData = propertyDetails.status === 200 ? Object.assign(locationData, propertyDetails.data) : locationData;
|
||||
locationData = regionDetails.status === 200 ? Object.assign(locationData, regionDetails.data) : locationData;
|
||||
|
||||
return locationData
|
||||
}
|
||||
}
|
||||
return locationData;
|
||||
},
|
||||
};
|
||||
|
||||
export default LocationRepsostory;
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import Repository from "./Repository";
|
||||
|
||||
const PlantRepository = {
|
||||
getFilteredPlants(filters) {
|
||||
return Repository.get(`/plants/`, { params: filters });
|
||||
},
|
||||
|
||||
getFilteredPlants(filters) {
|
||||
return Repository.get(`/plants/`, { params: filters });
|
||||
},
|
||||
getPlantsCSV(filters) {
|
||||
return Repository.get("/download/csv/", { params: filters });
|
||||
},
|
||||
|
||||
getPlantsCSV(filters) {
|
||||
return Repository.get('/download/csv/', { params: filters })
|
||||
},
|
||||
|
||||
getPlantsPDF(filters) {
|
||||
return Repository.get('/download/pdf/', { params: filters, responseType: 'arraybuffer' })
|
||||
}
|
||||
|
||||
}
|
||||
getPlantsPDF(filters) {
|
||||
return Repository.get("/download/pdf/", {
|
||||
params: filters,
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default PlantRepository;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import axios from "axios"
|
||||
import axios from "axios";
|
||||
|
||||
// Create the axios object
|
||||
const repo = axios.create({
|
||||
baseURL: window.location.hostname === 'localhost' ? "http://localhost:9000/api" : "/api",
|
||||
baseURL:
|
||||
window.location.hostname === "localhost"
|
||||
? "http://localhost:9000/api"
|
||||
: "/api",
|
||||
});
|
||||
|
||||
repo.defaults.headers.post["access-control-allow-origin"] = "*";
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import Repository from "./Repository";
|
||||
|
||||
const SiteRepository = {
|
||||
getHabitats() {
|
||||
return Repository.get(`/habitats/`);
|
||||
},
|
||||
|
||||
getHabitats() {
|
||||
return Repository.get(`/habitats/`);
|
||||
},
|
||||
getZones() {
|
||||
return Repository.get(`/zones/`);
|
||||
},
|
||||
|
||||
getZones() {
|
||||
return Repository.get(`/zones/`);
|
||||
},
|
||||
getHabitatImage(habitatImageID) {
|
||||
return Repository.get(`/habitatimage/${habitatImageID}/`);
|
||||
},
|
||||
};
|
||||
|
||||
getHabitatImage(habitatImageID) {
|
||||
return Repository.get(`/habitatimage/${habitatImageID}/`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SiteRepository;
|
||||
export default SiteRepository;
|
||||
|
|
Loading…
Reference in a new issue