[#40] Bulk PDF export - frontend #91
24 changed files with 393 additions and 301 deletions
|
@ -35,6 +35,10 @@
|
|||
"results": {
|
||||
"title": "Plant List Results",
|
||||
"forestDiagramDescription": "Forest Position Information Diagram"
|
||||
},
|
||||
"complete": {
|
||||
"title": "Application Complete",
|
||||
"description": "You have completed your application and submitted your results. You may now return to the homepage or fill out another application for a different habitat or zone."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
37
frontend/src/components/providers/FilterProvider.jsx
Normal file
37
frontend/src/components/providers/FilterProvider.jsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { createContext, useState, useContext } from 'react';
|
||||
import Repository from '../../repository/Repository';
|
||||
|
||||
const FilterContext = createContext(null);
|
||||
|
||||
const FilterProvider = ({children}) => {
|
||||
const [filters, setFilters] = useState({});
|
||||
|
||||
const resetFilters = () => setFilters({});
|
||||
|
||||
const updateFilters = newFilters => setFilters(oldFilters => ({...oldFilters, ...newFilters}));
|
||||
|
||||
const submit = async () => {
|
||||
return await Repository.post("/questionnaire/", {
|
||||
location: `SRID=4326;POINT (${filters.coordinates.lng} ${filters.coordinates.lat})`,
|
||||
soil_variant: filters.soilVariant,
|
||||
zone: filters.zone.id,
|
||||
});
|
||||
};
|
||||
|
||||
const value = {
|
||||
filters,
|
||||
setFilters,
|
||||
resetFilters,
|
||||
updateFilters,
|
||||
submit,
|
||||
};
|
||||
|
||||
return <FilterContext.Provider value={value}>{children}</FilterContext.Provider>;
|
||||
};
|
||||
|
||||
const useFilter = () => useContext(FilterContext);
|
||||
|
||||
export {
|
||||
FilterProvider,
|
||||
useFilter,
|
||||
};
|
100
frontend/src/components/providers/StepperProvider.jsx
Normal file
100
frontend/src/components/providers/StepperProvider.jsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { createContext, useState, useContext } 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 Tooltip from '@mui/material/Tooltip';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useFilter } from './FilterProvider';
|
||||
|
||||
const StepContext = createContext(null);
|
||||
|
||||
const StepperWizard = ({children}) => {
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
const isStep = n => (0 <= n && n < children.length);
|
||||
const setStepNext = () => setStep(n => isStep(n + 1) ? n + 1 : n);
|
||||
const setStepBack = () => setStep(n => isStep(n - 1) ? n - 1 : n);
|
||||
|
||||
const value = {
|
||||
step,
|
||||
setStep,
|
||||
setStepNext,
|
||||
setStepBack,
|
||||
isStep,
|
||||
};
|
||||
|
||||
return (
|
||||
<StepContext.Provider value={value}>
|
||||
<Box sx={{ width: '100%', height: '100%', display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<Stepper activeStep={step} sx={{ paddingRight: '3vw', paddingLeft: '3vw', marginBottom: '2vw' }}>
|
||||
{children.map(child => (
|
||||
<Tooltip title={child.props.tooltip}>
|
||||
<Step key={child.props.label}>
|
||||
<StepLabel>{child.props.label}</StepLabel>
|
||||
</Step>
|
||||
</Tooltip>)
|
||||
)}
|
||||
</Stepper>
|
||||
{children[step]}
|
||||
</Box>
|
||||
</StepContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useStepper = () => useContext(StepContext);
|
||||
|
||||
const StepperFooter = ({nextDisabled, backDisabled, onBack = null, onNext = null, onSubmit = () => {}}) => {
|
||||
const { step, isStep, setStepNext, setStepBack, setStep } = useStepper();
|
||||
const { resetFilters } = useFilter();
|
||||
const isSubmit = !isStep(step + 2);
|
||||
|
||||
const _onBack = () => {
|
||||
if (isStep(step + 1)) {
|
||||
setStepBack();
|
||||
} else {
|
||||
resetFilters();
|
||||
setStep(0);
|
||||
}
|
||||
};
|
||||
|
||||
const _onNext = () => {
|
||||
setStepNext();
|
||||
};
|
||||
|
||||
const _onSubmit = () => {
|
||||
onSubmit();
|
||||
if (onNext) {
|
||||
onNext();
|
||||
} else {
|
||||
_onNext();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2, pb: 2, paddingRight: '3vw', paddingLeft: '3vw', minHeight: '68px' }}>
|
||||
{isStep(step - 1) &&
|
||||
<Button
|
||||
onClick={onBack ?? _onBack}
|
||||
disabled={backDisabled}
|
||||
>
|
||||
{isStep(step + 1) ? "Back" : "Reset"}
|
||||
</Button>
|
||||
}
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
{isStep(step + 1) &&
|
||||
<Button
|
||||
onClick={isSubmit ? _onSubmit : (onNext ?? _onNext)}
|
||||
disabled={nextDisabled}
|
||||
>
|
||||
{isSubmit ? "Submit" : "Next"}
|
||||
</Button>
|
||||
}
|
||||
</Box>);
|
||||
};
|
||||
|
||||
export {
|
||||
StepperWizard,
|
||||
StepperFooter,
|
||||
useStepper,
|
||||
};
|
|
@ -25,9 +25,10 @@ const AddressSearchSuggestions = ({results, onClick}) => (
|
|||
</Box>
|
||||
: null);
|
||||
|
||||
const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDisabled, setRedirectBack, classNames}) => {
|
||||
const AddressSearch = ({onSelect, classNames}) => {
|
||||
const [value, setValue] = useState("");
|
||||
const [enable, setEnable] = useState(true);
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [results, setResults] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
@ -35,7 +36,6 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
|
|||
setResults([]);
|
||||
|
||||
if (enable && value && value.length > 5) {
|
||||
setNextDisabled(true);
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(true);
|
||||
LocationRepsostory.getPropertyDetails({search: value}).then(resp => {
|
||||
|
@ -46,7 +46,11 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
|
|||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [value, enable, setNextDisabled]);
|
||||
}, [value, enable]);
|
||||
|
||||
useEffect(() => {
|
||||
onSelect(selected);
|
||||
}, [selected, onSelect]);
|
||||
|
||||
return (
|
||||
<div classNames={classNames}>
|
||||
|
@ -65,6 +69,9 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
|
|||
onChange={(e) => {
|
||||
setEnable(true);
|
||||
setValue(e.target.value);
|
||||
if (selected && e.target.value !== selected.address) {
|
||||
setSelected(null);
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
|
@ -72,14 +79,8 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
|
|||
results={results}
|
||||
onClick={(r) => {
|
||||
setValue(r.address);
|
||||
updateFilterState({
|
||||
coordinates: {
|
||||
lng: r.coordinates[0],
|
||||
lat: r.coordinates[1],
|
||||
},
|
||||
});
|
||||
setNextDisabled(false);
|
||||
setEnable(false);
|
||||
setSelected(r);
|
||||
}}
|
||||
/>
|
||||
</div>);
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import { useState } from 'react';
|
||||
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';
|
||||
import { StepperFooter } from '../../providers/StepperProvider';
|
||||
import { useFilter } from '../../providers/FilterProvider';
|
||||
|
||||
|
||||
const AddressStep = (props) => {
|
||||
const AddressStep = () => {
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
const { updateFilters } = useFilter();
|
||||
|
||||
const addressPanel = (
|
||||
<div className="p-5">
|
||||
|
@ -14,16 +19,31 @@ const AddressStep = (props) => {
|
|||
description={<p>{staticText.steps.address.description}</p>}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<AddressSearch {...props} />
|
||||
<AddressSearch onSelect={address => {
|
||||
if (address) {
|
||||
setNextDisabled(false);
|
||||
updateFilters({
|
||||
coordinates: {
|
||||
lat: address.coordinates[1],
|
||||
lng: address.coordinates[0],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setNextDisabled(true);
|
||||
}
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
contentComponent={addressPanel}
|
||||
backgroundImage={addressBackgroundImage}
|
||||
/>);
|
||||
<>
|
||||
<Step
|
||||
contentComponent={addressPanel}
|
||||
backgroundImage={addressBackgroundImage}
|
||||
/>
|
||||
<StepperFooter nextDisabled={nextDisabled} />
|
||||
</>);
|
||||
};
|
||||
|
||||
export default AddressStep;
|
||||
|
|
24
frontend/src/components/steps/complete/CompleteStep.jsx
Normal file
24
frontend/src/components/steps/complete/CompleteStep.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Step from "../Step";
|
||||
import StepInformation from "../StepInformation";
|
||||
import completeBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import { StepperFooter } from "../../providers/StepperProvider";
|
||||
|
||||
export default function CompleteStep() {
|
||||
const completeInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.complete.title}
|
||||
description={<p>{staticText.steps.complete.description}</p>}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Step
|
||||
informationComponent={completeInfoPanel}
|
||||
backgroundImage={completeBackgroundImage}
|
||||
/>
|
||||
<StepperFooter />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -8,16 +8,18 @@ import FormLabel from "@mui/material/FormLabel";
|
|||
import FormControl from "@mui/material/FormControl";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import { HabitatSVG } from "../HabitatSVG";
|
||||
import { useFilter } from '../../providers/FilterProvider';
|
||||
|
||||
export default function HabitatSelector(props) {
|
||||
export default function HabitatSelector({setNextDisabled}) {
|
||||
const { filters, updateFilters } = useFilter();
|
||||
const [habitats, setHabitats] = useState([]);
|
||||
const [habitatsMap, setHabitatsMap] = useState({});
|
||||
const [value, setValue] = React.useState(
|
||||
props.filters.habitat && props.filters.habitat.id
|
||||
filters.habitat && filters.habitat.id
|
||||
);
|
||||
const [selectedHabitat, setSelectedHabitat] = React.useState({});
|
||||
const [imageValue, setImageValue] = React.useState(
|
||||
props.filters.habitatImage
|
||||
filters.habitatImage
|
||||
);
|
||||
|
||||
const getHabitats = () => {
|
||||
|
@ -39,22 +41,22 @@ export default function HabitatSelector(props) {
|
|||
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 (filters.habitat && filters.habitat.id) {
|
||||
setSelectedHabitat(habitatsMap[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);
|
||||
if (filters.habitat && filters.habitatImage) {
|
||||
setNextDisabled(false);
|
||||
}
|
||||
}, [habitats.length, props, habitatsMap]);
|
||||
}, [habitats.length, setNextDisabled, habitatsMap, filters]);
|
||||
|
||||
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);
|
||||
updateFilters({ habitatImage: imageId });
|
||||
updateFilters({ zone: null });
|
||||
setNextDisabled(false);
|
||||
};
|
||||
|
||||
const handleRadioChange = (event) => {
|
||||
|
@ -69,11 +71,11 @@ export default function HabitatSelector(props) {
|
|||
if (habitatObject.images.length === 1) {
|
||||
setHabitatImage(habitatObject.images[0].id);
|
||||
} else {
|
||||
props.setNextDisabled(true);
|
||||
setNextDisabled(true);
|
||||
}
|
||||
|
||||
// Update the filters for the selected habitat
|
||||
props.updateFilterState({
|
||||
updateFilters({
|
||||
habitat: { id: habitatObject.id, name: habitatObject.name },
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { useState } from 'react';
|
||||
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";
|
||||
import { StepperFooter } from '../../providers/StepperProvider';
|
||||
|
||||
export default function HabitatStep(props) {
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
|
||||
const habitatInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.habitat.title}
|
||||
|
@ -12,13 +16,16 @@ export default function HabitatStep(props) {
|
|||
/>
|
||||
);
|
||||
|
||||
const habitatSelectionPanel = <HabitatSelector {...props} />;
|
||||
const habitatSelectionPanel = <HabitatSelector setNextDisabled={setNextDisabled} />;
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={habitatInfoPanel}
|
||||
selectionComponent={habitatSelectionPanel}
|
||||
backgroundImage={habitatBackgroundImage}
|
||||
/>
|
||||
<>
|
||||
<Step
|
||||
informationComponent={habitatInfoPanel}
|
||||
selectionComponent={habitatSelectionPanel}
|
||||
backgroundImage={habitatBackgroundImage}
|
||||
/>
|
||||
<StepperFooter nextDisabled={nextDisabled} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { useState } from "react";
|
||||
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";
|
||||
import { StepperFooter } from '../../providers/StepperProvider';
|
||||
|
||||
export default function LocationStep(props) {
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
|
||||
const locationInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.location.title}
|
||||
|
@ -12,13 +16,16 @@ export default function LocationStep(props) {
|
|||
/>
|
||||
);
|
||||
|
||||
const locationSelectionPanel = <LocationSelectorMap {...props} />;
|
||||
const locationSelectionPanel = <LocationSelectorMap setNextDisabled={setNextDisabled} />;
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={locationInfoPanel}
|
||||
selectionComponent={locationSelectionPanel}
|
||||
backgroundImage={locationBackgroundImage}
|
||||
/>
|
||||
<>
|
||||
<Step
|
||||
informationComponent={locationInfoPanel}
|
||||
selectionComponent={locationSelectionPanel}
|
||||
backgroundImage={locationBackgroundImage}
|
||||
/>
|
||||
<StepperFooter nextDisabled={nextDisabled} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
|
||||
import LocationRepository from "../../../repository/LocationRepository";
|
||||
import { useFilter } from "../../providers/FilterProvider";
|
||||
|
||||
const NZ_BOUNDS = [
|
||||
[-47.204642, 165.344238],
|
||||
[-34.307144, 179.824219],
|
||||
];
|
||||
|
||||
function LocationMarker(props) {
|
||||
function LocationMarker({setNextDisabled}) {
|
||||
const [position, setPosition] = useState(null);
|
||||
const { filters, updateFilters } = useFilter();
|
||||
|
||||
const map = useMapEvents({
|
||||
click(e) {
|
||||
const newPosition = e.latlng;
|
||||
setPosition(newPosition);
|
||||
props.updateFilterState({ coordinates: newPosition });
|
||||
props.setNextDisabled(false);
|
||||
updateFilters({ coordinates: newPosition });
|
||||
setNextDisabled(false);
|
||||
},
|
||||
});
|
||||
|
||||
map.whenReady(() => {
|
||||
const savedCoordinates = props.filters["coordinates"];
|
||||
const savedCoordinates = filters.coordinates;
|
||||
if (!position && savedCoordinates) {
|
||||
setPosition(savedCoordinates);
|
||||
props.setNextDisabled(false);
|
||||
setNextDisabled(false);
|
||||
map.flyTo(savedCoordinates, 9);
|
||||
}
|
||||
});
|
||||
|
@ -36,16 +39,17 @@ const fitNZBounds = (map) => {
|
|||
|
||||
function LocationDetailsDisplay(props) {
|
||||
const [locationDetails, setLocationDetails] = useState({});
|
||||
const { filters } = useFilter();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.filters["coordinates"]) {
|
||||
LocationRepository.getLocationData(props.filters).then((result) => {
|
||||
if (filters.coordinates) {
|
||||
LocationRepository.getLocationData(filters).then((result) => {
|
||||
setLocationDetails(result);
|
||||
});
|
||||
}
|
||||
}, [props.filters, props.filters.coordinates]);
|
||||
}, [filters]);
|
||||
|
||||
const savedCoordinates = props.filters["coordinates"];
|
||||
const savedCoordinates = filters.coordinates;
|
||||
|
||||
if (savedCoordinates) {
|
||||
const soilString = `${locationDetails.soil_name} (${locationDetails.soil_code})`;
|
||||
|
|
|
@ -8,6 +8,8 @@ import PlantRepository from "../../../repository/PlantRepository";
|
|||
import { Typography, Box } from "@mui/material";
|
||||
import resultsBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import { useFilter } from "../../providers/FilterProvider";
|
||||
import { StepperFooter } from "../../providers/StepperProvider";
|
||||
|
||||
const RESULTS_DESCRIPTION = (
|
||||
<Typography>
|
||||
|
@ -29,10 +31,11 @@ const RESULTS_DESCRIPTION = (
|
|||
|
||||
export default function ResultsStep(props) {
|
||||
const [plants, setPlants] = useState([]);
|
||||
const { filters } = useFilter();
|
||||
|
||||
useEffect(() => {
|
||||
const updatePlants = () => {
|
||||
PlantRepository.getFilteredPlants(props.filters)
|
||||
PlantRepository.getFilteredPlants(filters)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setPlants(response.data);
|
||||
|
@ -43,7 +46,7 @@ export default function ResultsStep(props) {
|
|||
});
|
||||
};
|
||||
updatePlants();
|
||||
}, [props.filters]);
|
||||
}, [filters]);
|
||||
|
||||
function createData(
|
||||
name,
|
||||
|
@ -91,13 +94,13 @@ export default function ResultsStep(props) {
|
|||
};
|
||||
|
||||
const downloadCSV = () => {
|
||||
PlantRepository.getPlantsCSV(props.filters).then((response) => {
|
||||
PlantRepository.getPlantsCSV(filters).then((response) => {
|
||||
download(response, "text/csv", "plants.csv");
|
||||
});
|
||||
};
|
||||
|
||||
const downloadPDF = () => {
|
||||
PlantRepository.getPlantsPDF(props.filters).then((response) => {
|
||||
PlantRepository.getPlantsPDF(filters).then((response) => {
|
||||
download(response, "application/pdf", "planting_guide.pdf");
|
||||
});
|
||||
};
|
||||
|
@ -133,9 +136,12 @@ export default function ResultsStep(props) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
contentComponent={stepContent}
|
||||
backgroundImage={resultsBackgroundImage}
|
||||
/>
|
||||
<>
|
||||
<Step
|
||||
contentComponent={stepContent}
|
||||
backgroundImage={resultsBackgroundImage}
|
||||
/>
|
||||
<StepperFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ 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";
|
||||
import { useFilter } from "../../providers/FilterProvider";
|
||||
|
||||
const WET_SOIL_DESCRIPTION = (
|
||||
<p>
|
||||
|
@ -53,24 +54,26 @@ const MESIC_SOIL_DESCRIPTION = (
|
|||
</p>
|
||||
);
|
||||
|
||||
export default function SoilSelector(props) {
|
||||
const [value, setValue] = React.useState(props.filters.soilVariant);
|
||||
export default function SoilSelector({setNextDisabled}) {
|
||||
const { filters, updateFilters } = useFilter();
|
||||
|
||||
const [value, setValue] = React.useState(filters.soilVariant);
|
||||
const [helperText, setHelperText] = React.useState(
|
||||
staticText.steps.soil.optionsHelperText
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.filters.soilVariant) {
|
||||
props.setNextDisabled(false);
|
||||
if (filters.soilVariant) {
|
||||
setNextDisabled(false);
|
||||
}
|
||||
});
|
||||
}, [filters, setNextDisabled]);
|
||||
|
||||
const handleRadioChange = (event) => {
|
||||
const soilVariantSelection = event.target.value;
|
||||
setValue(soilVariantSelection);
|
||||
setHelperText(" ");
|
||||
props.updateFilterState({ soilVariant: soilVariantSelection });
|
||||
props.setNextDisabled(false);
|
||||
updateFilters({ soilVariant: soilVariantSelection });
|
||||
setNextDisabled(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { useState } from 'react';
|
||||
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";
|
||||
import { StepperFooter } from '../../providers/StepperProvider';
|
||||
|
||||
|
||||
export default function SoilVariantStep(props) {
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
|
||||
const SOIL_DESCRIPTION = (
|
||||
<p>
|
||||
From your site location, we use{" "}
|
||||
|
@ -36,15 +41,18 @@ export default function SoilVariantStep(props) {
|
|||
|
||||
const soilVarientSelectionPanel = (
|
||||
<div className="p-5">
|
||||
<SoilSelector {...props} />
|
||||
<SoilSelector setNextDisabled={setNextDisabled} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={soilVarientInfoPanel}
|
||||
selectionComponent={soilVarientSelectionPanel}
|
||||
backgroundImage={soilBackgroundImage}
|
||||
/>
|
||||
<>
|
||||
<Step
|
||||
informationComponent={soilVarientInfoPanel}
|
||||
selectionComponent={soilVarientSelectionPanel}
|
||||
backgroundImage={soilBackgroundImage}
|
||||
/>
|
||||
<StepperFooter nextDisabled={nextDisabled} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
export default function ProjectSpecificsSelector(props) {
|
||||
useEffect(() => {
|
||||
props.setNextDisabled(false);
|
||||
});
|
||||
|
||||
return <h2>Project Specifics Selector</h2>;
|
||||
}
|
|
@ -1,24 +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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -8,13 +8,15 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|||
|
||||
import SummaryTable from "./SummaryTable";
|
||||
import LocationRepository from "../../../repository/LocationRepository";
|
||||
import { useFilter } from "../../providers/FilterProvider";
|
||||
|
||||
export default function SummaryContent(props) {
|
||||
export default function SummaryContent() {
|
||||
const [expanded, setExpanded] = React.useState(null);
|
||||
const [locationDetails, setLocationDetails] = React.useState({});
|
||||
const { filters } = useFilter();
|
||||
|
||||
const getLocationDetails = () => {
|
||||
LocationRepository.getLocationData(props.filters).then((result) => {
|
||||
LocationRepository.getLocationData(filters).then((result) => {
|
||||
setLocationDetails(result);
|
||||
});
|
||||
};
|
||||
|
@ -24,7 +26,6 @@ export default function SummaryContent(props) {
|
|||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
props.setNextDisabled(false);
|
||||
!Object.keys(locationDetails).length && getLocationDetails();
|
||||
});
|
||||
|
||||
|
@ -35,7 +36,7 @@ export default function SummaryContent(props) {
|
|||
const locationData = [
|
||||
createData(
|
||||
"Geographical Coordinates (latitude, longitude)",
|
||||
`(${props.filters.coordinates.lat}, ${props.filters.coordinates.lng})`
|
||||
`(${filters.coordinates.lat}, ${filters.coordinates.lng})`
|
||||
),
|
||||
createData("Ecological Region", locationDetails.ecological_region || ""),
|
||||
createData(
|
||||
|
@ -50,22 +51,22 @@ export default function SummaryContent(props) {
|
|||
"Soil Order",
|
||||
`${locationDetails.soil_name} (${locationDetails.soil_code})` || ""
|
||||
),
|
||||
createData("Soil Variant", props.filters.soilVariant),
|
||||
createData("Soil Variant", filters.soilVariant),
|
||||
];
|
||||
|
||||
const siteData = [
|
||||
createData("Habitat", props.filters.habitat.name ?? ""),
|
||||
createData("Habitat", filters.habitat.name ?? ""),
|
||||
createData(
|
||||
"Zone Name",
|
||||
(props.filters.zone && props.filters.zone.name) ?? ""
|
||||
(filters.zone && filters.zone.name) ?? ""
|
||||
),
|
||||
createData(
|
||||
"Zone Variant",
|
||||
(props.filters.zone && props.filters.zone.variant) ?? ""
|
||||
(filters.zone && filters.zone.variant) ?? ""
|
||||
),
|
||||
createData(
|
||||
"Zone Refined Variant",
|
||||
(props.filters.zone && props.filters.zone.refined_variant) ?? ""
|
||||
(filters.zone && filters.zone.refined_variant) ?? ""
|
||||
),
|
||||
];
|
||||
|
||||
|
|
|
@ -3,8 +3,10 @@ import SummaryContent from "./SummaryContent";
|
|||
import StepInformation from "../StepInformation";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import summaryBackgroundImage from "../../../assets/img/stepBackgrounds/step5.jpg";
|
||||
import { StepperFooter } from '../../providers/StepperProvider';
|
||||
|
||||
export default function SummaryStep(props) {
|
||||
|
||||
export default function SummaryStep({ onSubmit }) {
|
||||
const summaryInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.summary.title}
|
||||
|
@ -12,13 +14,16 @@ export default function SummaryStep(props) {
|
|||
/>
|
||||
);
|
||||
|
||||
const summaryContent = <SummaryContent {...props} />;
|
||||
const summaryContent = <SummaryContent />;
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={summaryInfoPanel}
|
||||
selectionComponent={summaryContent}
|
||||
backgroundImage={summaryBackgroundImage}
|
||||
/>
|
||||
<>
|
||||
<Step
|
||||
informationComponent={summaryInfoPanel}
|
||||
selectionComponent={summaryContent}
|
||||
backgroundImage={summaryBackgroundImage}
|
||||
/>
|
||||
<StepperFooter onSubmit={onSubmit} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import SiteRepository from "../../../repository/SiteRepository";
|
||||
import { useFilter } from "../../providers/FilterProvider";
|
||||
import { HabitatSVG } from "../HabitatSVG";
|
||||
|
||||
export default function ZoneSelector(props) {
|
||||
export default function ZoneSelector({setNextDisabled}) {
|
||||
const [habitatImageObject, setHabitatImageObject] = useState({});
|
||||
const [segmentMapping, setSegmentMapping] = useState({});
|
||||
const [selectedZoneSegment, setZoneSegment] = useState(null);
|
||||
const { filters, updateFilters } = useFilter();
|
||||
|
||||
const setZoneOrRedirect = (zone) => {
|
||||
const redirectHabitat = zone && zone.redirect_habitat;
|
||||
|
@ -15,15 +17,13 @@ export default function ZoneSelector(props) {
|
|||
? { habitat: { id: redirectHabitat.id, name: redirectHabitat.name } }
|
||||
: { zone: zone };
|
||||
|
||||
props.updateFilterState(newFilterState);
|
||||
props.setRedirectBack(Boolean(redirectHabitat));
|
||||
|
||||
props.setNextDisabled(false);
|
||||
updateFilters(newFilterState);
|
||||
setNextDisabled(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getHabitatImage = () => {
|
||||
SiteRepository.getHabitatImage(props.filters.habitatImage).then(
|
||||
SiteRepository.getHabitatImage(filters.habitatImage).then(
|
||||
(response) => {
|
||||
if (response.status === 200) {
|
||||
const imageData = response.data;
|
||||
|
@ -34,8 +34,8 @@ export default function ZoneSelector(props) {
|
|||
};
|
||||
|
||||
const setInitialZone = () => {
|
||||
if (props.filters.zone) {
|
||||
const zone = props.filters.zone;
|
||||
if (filters.zone) {
|
||||
const zone = filters.zone;
|
||||
const zoneSegment = document.querySelectorAll(
|
||||
`.zone-selector-svg svg path[inkscapelabel="${zone.related_svg_segment}"]`
|
||||
)[0];
|
||||
|
@ -45,7 +45,7 @@ export default function ZoneSelector(props) {
|
|||
setZoneSegment(zoneSegment);
|
||||
zoneSegment.style.fill = "#eeeeee";
|
||||
zoneSegment.style["fill-opacity"] = 0.5;
|
||||
props.setNextDisabled(false);
|
||||
setNextDisabled(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -69,7 +69,7 @@ export default function ZoneSelector(props) {
|
|||
|
||||
// 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]);
|
||||
}, [habitatImageObject, segmentMapping, setNextDisabled, filters]);
|
||||
|
||||
const selectZone = (element) => {
|
||||
if (
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
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";
|
||||
import { StepperFooter, useStepper } from '../../providers/StepperProvider';
|
||||
import { useFilter } from '../../providers/FilterProvider';
|
||||
|
||||
export default function ZoneStep({repeatable}) {
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
const { setStepBack, setStepNext } = useStepper();
|
||||
const { filters } = useFilter();
|
||||
|
||||
const redirect = !nextDisabled && !filters.zone;
|
||||
|
||||
export default function ZoneStep(props) {
|
||||
const zoneInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.zone.title}
|
||||
|
@ -23,15 +33,21 @@ export default function ZoneStep(props) {
|
|||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ZoneSelector {...props} />
|
||||
<ZoneSelector setNextDisabled={setNextDisabled} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Step
|
||||
informationComponent={zoneInfoPanel}
|
||||
selectionComponent={zoneSelectionPanel}
|
||||
backgroundImage={zoneBackgroundImage}
|
||||
/>
|
||||
<>
|
||||
<Step
|
||||
informationComponent={zoneInfoPanel}
|
||||
selectionComponent={zoneSelectionPanel}
|
||||
backgroundImage={zoneBackgroundImage}
|
||||
/>
|
||||
<StepperFooter
|
||||
nextDisabled={nextDisabled}
|
||||
onNext={redirect ? setStepBack : setStepNext}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import { createTheme, ThemeProvider } from "@mui/material/styles";
|
|||
import reportWebVitals from "./reportWebVitals";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import MainPage from "./pages/MainPage";
|
||||
import { FilterProvider } from "./components/providers/FilterProvider";
|
||||
|
||||
// Styles
|
||||
import "./assets/styles/main.scss";
|
||||
|
@ -33,9 +34,11 @@ const darkTheme = createTheme({
|
|||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<div className="App">
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
<FilterProvider>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</FilterProvider>
|
||||
</div>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
|
|
|
@ -1,45 +1,29 @@
|
|||
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";
|
||||
import { StepperWizard } from "../components/providers/StepperProvider";
|
||||
import CompleteStep from "../components/steps/complete/CompleteStep";
|
||||
import { useFilter } from "../components/providers/FilterProvider";
|
||||
|
||||
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>
|
||||
);
|
||||
const ApplyPage = () => {
|
||||
const { submit } = useFilter();
|
||||
|
||||
return (
|
||||
<Container fluid className="main-container p-0">
|
||||
<Header />
|
||||
<StepperWizard>
|
||||
<AddressStep label="Enter address" tooltip="Enter your address" />
|
||||
<SoilStep label="Choose soil" tooltip="Describe the moisture content of your soil" />
|
||||
<HabitatStep label="Choose habitat" tooltip="Specify type of landscape to be planted" />
|
||||
<ZoneStep label="Select zone" tooltip="Specify geographical detail" />
|
||||
<SummaryStep label="Summary" tooltip="Check your inputs" onSubmit={submit} />
|
||||
<CompleteStep label="Complete" tooltip="Complete your application" />
|
||||
</StepperWizard>
|
||||
</Container>);
|
||||
};
|
||||
|
||||
export default ApplyPage;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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";
|
||||
|
@ -7,45 +6,21 @@ 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";
|
||||
import { StepperWizard } from "../components/providers/StepperProvider";
|
||||
|
||||
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>
|
||||
);
|
||||
const MainPage = () => {
|
||||
return (
|
||||
<Container fluid className="main-container p-0">
|
||||
<Header />
|
||||
<StepperWizard>
|
||||
<LocationStep label="Select location" tooltip="Click on a location on the map" />
|
||||
<SoilStep label="Choose soil" tooltip="Describe the moisture content of your soil" />
|
||||
<HabitatStep label="Choose habitat" tooltip="Specify type of landscape to be planted" />
|
||||
<ZoneStep label="Select zone" tooltip="Specify geographical detail" />
|
||||
<SummaryStep label="Summary" tooltip="Check your inputs" />
|
||||
<ResultsStep label="Results" tooltip="List of plant species and user guide" />
|
||||
</StepperWizard>
|
||||
</Container>);
|
||||
};
|
||||
|
||||
export default MainPage;
|
||||
|
|
|
@ -2,10 +2,9 @@ import axios from "axios";
|
|||
|
||||
// Create the axios object
|
||||
const repo = axios.create({
|
||||
baseURL:
|
||||
window.location.hostname === "localhost"
|
||||
? "http://localhost:9000/api"
|
||||
: "/api",
|
||||
baseURL: "/api",
|
||||
xsrfHeaderName: "X-CSRFToken",
|
||||
xsrfCookieName: "csrftoken",
|
||||
});
|
||||
|
||||
repo.defaults.headers.post["access-control-allow-origin"] = "*";
|
||||
|
|
Loading…
Reference in a new issue