[#40] Bulk PDF export - frontend #91

Merged
mattn merged 1 commit from matt/40-batch-frontend into main 2023-02-22 16:41:48 +13:00
24 changed files with 393 additions and 301 deletions

View file

@ -35,6 +35,10 @@
"results": { "results": {
"title": "Plant List Results", "title": "Plant List Results",
"forestDiagramDescription": "Forest Position Information Diagram" "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."
} }
} }
} }

View file

@ -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>
);
}

View 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,
};

View 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,
};

View file

@ -25,9 +25,10 @@ const AddressSearchSuggestions = ({results, onClick}) => (
</Box> </Box>
: null); : null);
const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDisabled, setRedirectBack, classNames}) => { const AddressSearch = ({onSelect, classNames}) => {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [enable, setEnable] = useState(true); const [enable, setEnable] = useState(true);
const [selected, setSelected] = useState(null);
const [results, setResults] = useState([]); const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -35,7 +36,6 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
setResults([]); setResults([]);
if (enable && value && value.length > 5) { if (enable && value && value.length > 5) {
setNextDisabled(true);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setIsLoading(true); setIsLoading(true);
LocationRepsostory.getPropertyDetails({search: value}).then(resp => { LocationRepsostory.getPropertyDetails({search: value}).then(resp => {
@ -46,7 +46,11 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [value, enable, setNextDisabled]); }, [value, enable]);
useEffect(() => {
onSelect(selected);
}, [selected, onSelect]);
return ( return (
<div classNames={classNames}> <div classNames={classNames}>
@ -65,6 +69,9 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
onChange={(e) => { onChange={(e) => {
setEnable(true); setEnable(true);
setValue(e.target.value); setValue(e.target.value);
if (selected && e.target.value !== selected.address) {
setSelected(null);
}
}} }}
value={value} value={value}
/> />
@ -72,14 +79,8 @@ const AddressSearch = ({filters, updateFilterState, resetFilterState, setNextDis
results={results} results={results}
onClick={(r) => { onClick={(r) => {
setValue(r.address); setValue(r.address);
updateFilterState({
coordinates: {
lng: r.coordinates[0],
lat: r.coordinates[1],
},
});
setNextDisabled(false);
setEnable(false); setEnable(false);
setSelected(r);
}} }}
/> />
</div>); </div>);

View file

@ -1,11 +1,16 @@
import { useState } from 'react';
import Step from '../Step'; import Step from '../Step';
import StepInformation from '../StepInformation'; import StepInformation from '../StepInformation';
import staticText from '../../../assets/data/staticText.json' import staticText from '../../../assets/data/staticText.json'
import addressBackgroundImage from '../../../assets/img/stepBackgrounds/step1.jpg'; import addressBackgroundImage from '../../../assets/img/stepBackgrounds/step1.jpg';
import AddressSearch from './AddressSearch'; 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 = ( const addressPanel = (
<div className="p-5"> <div className="p-5">
@ -14,16 +19,31 @@ const AddressStep = (props) => {
description={<p>{staticText.steps.address.description}</p>} description={<p>{staticText.steps.address.description}</p>}
/> />
<div className="p-4"> <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>
</div> </div>
); );
return ( return (
<Step <>
contentComponent={addressPanel} <Step
backgroundImage={addressBackgroundImage} contentComponent={addressPanel}
/>); backgroundImage={addressBackgroundImage}
/>
<StepperFooter nextDisabled={nextDisabled} />
</>);
}; };
export default AddressStep; export default AddressStep;

View 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 />
</>
);
};

View file

@ -8,16 +8,18 @@ import FormLabel from "@mui/material/FormLabel";
import FormControl from "@mui/material/FormControl"; import FormControl from "@mui/material/FormControl";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import { HabitatSVG } from "../HabitatSVG"; 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 [habitats, setHabitats] = useState([]);
const [habitatsMap, setHabitatsMap] = useState({}); const [habitatsMap, setHabitatsMap] = useState({});
const [value, setValue] = React.useState( const [value, setValue] = React.useState(
props.filters.habitat && props.filters.habitat.id filters.habitat && filters.habitat.id
); );
const [selectedHabitat, setSelectedHabitat] = React.useState({}); const [selectedHabitat, setSelectedHabitat] = React.useState({});
const [imageValue, setImageValue] = React.useState( const [imageValue, setImageValue] = React.useState(
props.filters.habitatImage filters.habitatImage
); );
const getHabitats = () => { const getHabitats = () => {
@ -39,22 +41,22 @@ export default function HabitatSelector(props) {
habitats.length === 0 && getHabitats(); habitats.length === 0 && getHabitats();
// Sets the selected habitat if its already set in filters // Sets the selected habitat if its already set in filters
if (props.filters.habitat && props.filters.habitat.id) { if (filters.habitat && filters.habitat.id) {
setSelectedHabitat(habitatsMap[props.filters.habitat.id]); setSelectedHabitat(habitatsMap[filters.habitat.id]);
} }
// If both the habitat and the image is selected, then enable the next step // If both the habitat and the image is selected, then enable the next step
if (props.filters.habitat && props.filters.habitatImage) { if (filters.habitat && filters.habitatImage) {
props.setNextDisabled(false); setNextDisabled(false);
} }
}, [habitats.length, props, habitatsMap]); }, [habitats.length, setNextDisabled, habitatsMap, filters]);
const setHabitatImage = (imageId) => { const setHabitatImage = (imageId) => {
// Sets the selected image radio, updates filter state for image and enable the next button // Sets the selected image radio, updates filter state for image and enable the next button
setImageValue(imageId); setImageValue(imageId);
props.updateFilterState({ habitatImage: imageId }); updateFilters({ habitatImage: imageId });
props.updateFilterState({ zone: null }); updateFilters({ zone: null });
props.setNextDisabled(false); setNextDisabled(false);
}; };
const handleRadioChange = (event) => { const handleRadioChange = (event) => {
@ -69,11 +71,11 @@ export default function HabitatSelector(props) {
if (habitatObject.images.length === 1) { if (habitatObject.images.length === 1) {
setHabitatImage(habitatObject.images[0].id); setHabitatImage(habitatObject.images[0].id);
} else { } else {
props.setNextDisabled(true); setNextDisabled(true);
} }
// Update the filters for the selected habitat // Update the filters for the selected habitat
props.updateFilterState({ updateFilters({
habitat: { id: habitatObject.id, name: habitatObject.name }, habitat: { id: habitatObject.id, name: habitatObject.name },
}); });
}; };

View file

@ -1,10 +1,14 @@
import { useState } from 'react';
import Step from "../Step"; import Step from "../Step";
import HabitatSelector from "./HabitatSelector"; import HabitatSelector from "./HabitatSelector";
import StepInformation from "../StepInformation"; import StepInformation from "../StepInformation";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import habitatBackgroundImage from "../../../assets/img/stepBackgrounds/step3.jpg"; import habitatBackgroundImage from "../../../assets/img/stepBackgrounds/step3.jpg";
import { StepperFooter } from '../../providers/StepperProvider';
export default function HabitatStep(props) { export default function HabitatStep(props) {
const [nextDisabled, setNextDisabled] = useState(true);
const habitatInfoPanel = ( const habitatInfoPanel = (
<StepInformation <StepInformation
title={staticText.steps.habitat.title} title={staticText.steps.habitat.title}
@ -12,13 +16,16 @@ export default function HabitatStep(props) {
/> />
); );
const habitatSelectionPanel = <HabitatSelector {...props} />; const habitatSelectionPanel = <HabitatSelector setNextDisabled={setNextDisabled} />;
return ( return (
<Step <>
informationComponent={habitatInfoPanel} <Step
selectionComponent={habitatSelectionPanel} informationComponent={habitatInfoPanel}
backgroundImage={habitatBackgroundImage} selectionComponent={habitatSelectionPanel}
/> backgroundImage={habitatBackgroundImage}
/>
<StepperFooter nextDisabled={nextDisabled} />
</>
); );
} }

View file

@ -1,10 +1,14 @@
import { useState } from "react";
import Step from "../Step"; import Step from "../Step";
import LocationSelectorMap from "./Map"; import LocationSelectorMap from "./Map";
import StepInformation from "../StepInformation"; import StepInformation from "../StepInformation";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import locationBackgroundImage from "../../../assets/img/stepBackgrounds/step1.jpg"; import locationBackgroundImage from "../../../assets/img/stepBackgrounds/step1.jpg";
import { StepperFooter } from '../../providers/StepperProvider';
export default function LocationStep(props) { export default function LocationStep(props) {
const [nextDisabled, setNextDisabled] = useState(true);
const locationInfoPanel = ( const locationInfoPanel = (
<StepInformation <StepInformation
title={staticText.steps.location.title} title={staticText.steps.location.title}
@ -12,13 +16,16 @@ export default function LocationStep(props) {
/> />
); );
const locationSelectionPanel = <LocationSelectorMap {...props} />; const locationSelectionPanel = <LocationSelectorMap setNextDisabled={setNextDisabled} />;
return ( return (
<Step <>
informationComponent={locationInfoPanel} <Step
selectionComponent={locationSelectionPanel} informationComponent={locationInfoPanel}
backgroundImage={locationBackgroundImage} selectionComponent={locationSelectionPanel}
/> backgroundImage={locationBackgroundImage}
/>
<StepperFooter nextDisabled={nextDisabled} />
</>
); );
} }

View file

@ -1,28 +1,31 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet"; import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
import LocationRepository from "../../../repository/LocationRepository"; import LocationRepository from "../../../repository/LocationRepository";
import { useFilter } from "../../providers/FilterProvider";
const NZ_BOUNDS = [ const NZ_BOUNDS = [
[-47.204642, 165.344238], [-47.204642, 165.344238],
[-34.307144, 179.824219], [-34.307144, 179.824219],
]; ];
function LocationMarker(props) { function LocationMarker({setNextDisabled}) {
const [position, setPosition] = useState(null); const [position, setPosition] = useState(null);
const { filters, updateFilters } = useFilter();
const map = useMapEvents({ const map = useMapEvents({
click(e) { click(e) {
const newPosition = e.latlng; const newPosition = e.latlng;
setPosition(newPosition); setPosition(newPosition);
props.updateFilterState({ coordinates: newPosition }); updateFilters({ coordinates: newPosition });
props.setNextDisabled(false); setNextDisabled(false);
}, },
}); });
map.whenReady(() => { map.whenReady(() => {
const savedCoordinates = props.filters["coordinates"]; const savedCoordinates = filters.coordinates;
if (!position && savedCoordinates) { if (!position && savedCoordinates) {
setPosition(savedCoordinates); setPosition(savedCoordinates);
props.setNextDisabled(false); setNextDisabled(false);
map.flyTo(savedCoordinates, 9); map.flyTo(savedCoordinates, 9);
} }
}); });
@ -36,16 +39,17 @@ const fitNZBounds = (map) => {
function LocationDetailsDisplay(props) { function LocationDetailsDisplay(props) {
const [locationDetails, setLocationDetails] = useState({}); const [locationDetails, setLocationDetails] = useState({});
const { filters } = useFilter();
useEffect(() => { useEffect(() => {
if (props.filters["coordinates"]) { if (filters.coordinates) {
LocationRepository.getLocationData(props.filters).then((result) => { LocationRepository.getLocationData(filters).then((result) => {
setLocationDetails(result); setLocationDetails(result);
}); });
} }
}, [props.filters, props.filters.coordinates]); }, [filters]);
const savedCoordinates = props.filters["coordinates"]; const savedCoordinates = filters.coordinates;
if (savedCoordinates) { if (savedCoordinates) {
const soilString = `${locationDetails.soil_name} (${locationDetails.soil_code})`; const soilString = `${locationDetails.soil_name} (${locationDetails.soil_code})`;

View file

@ -8,6 +8,8 @@ import PlantRepository from "../../../repository/PlantRepository";
import { Typography, Box } from "@mui/material"; import { Typography, Box } from "@mui/material";
import resultsBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg"; import resultsBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import { useFilter } from "../../providers/FilterProvider";
import { StepperFooter } from "../../providers/StepperProvider";
const RESULTS_DESCRIPTION = ( const RESULTS_DESCRIPTION = (
<Typography> <Typography>
@ -29,10 +31,11 @@ const RESULTS_DESCRIPTION = (
export default function ResultsStep(props) { export default function ResultsStep(props) {
const [plants, setPlants] = useState([]); const [plants, setPlants] = useState([]);
const { filters } = useFilter();
useEffect(() => { useEffect(() => {
const updatePlants = () => { const updatePlants = () => {
PlantRepository.getFilteredPlants(props.filters) PlantRepository.getFilteredPlants(filters)
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
setPlants(response.data); setPlants(response.data);
@ -43,7 +46,7 @@ export default function ResultsStep(props) {
}); });
}; };
updatePlants(); updatePlants();
}, [props.filters]); }, [filters]);
function createData( function createData(
name, name,
@ -91,13 +94,13 @@ export default function ResultsStep(props) {
}; };
const downloadCSV = () => { const downloadCSV = () => {
PlantRepository.getPlantsCSV(props.filters).then((response) => { PlantRepository.getPlantsCSV(filters).then((response) => {
download(response, "text/csv", "plants.csv"); download(response, "text/csv", "plants.csv");
}); });
}; };
const downloadPDF = () => { const downloadPDF = () => {
PlantRepository.getPlantsPDF(props.filters).then((response) => { PlantRepository.getPlantsPDF(filters).then((response) => {
download(response, "application/pdf", "planting_guide.pdf"); download(response, "application/pdf", "planting_guide.pdf");
}); });
}; };
@ -133,9 +136,12 @@ export default function ResultsStep(props) {
); );
return ( return (
<Step <>
contentComponent={stepContent} <Step
backgroundImage={resultsBackgroundImage} contentComponent={stepContent}
/> backgroundImage={resultsBackgroundImage}
/>
<StepperFooter />
</>
); );
} }

View file

@ -7,6 +7,7 @@ import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText"; import FormHelperText from "@mui/material/FormHelperText";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import { useFilter } from "../../providers/FilterProvider";
const WET_SOIL_DESCRIPTION = ( const WET_SOIL_DESCRIPTION = (
<p> <p>
@ -53,24 +54,26 @@ const MESIC_SOIL_DESCRIPTION = (
</p> </p>
); );
export default function SoilSelector(props) { export default function SoilSelector({setNextDisabled}) {
const [value, setValue] = React.useState(props.filters.soilVariant); const { filters, updateFilters } = useFilter();
const [value, setValue] = React.useState(filters.soilVariant);
const [helperText, setHelperText] = React.useState( const [helperText, setHelperText] = React.useState(
staticText.steps.soil.optionsHelperText staticText.steps.soil.optionsHelperText
); );
React.useEffect(() => { React.useEffect(() => {
if (props.filters.soilVariant) { if (filters.soilVariant) {
props.setNextDisabled(false); setNextDisabled(false);
} }
}); }, [filters, setNextDisabled]);
const handleRadioChange = (event) => { const handleRadioChange = (event) => {
const soilVariantSelection = event.target.value; const soilVariantSelection = event.target.value;
setValue(soilVariantSelection); setValue(soilVariantSelection);
setHelperText(" "); setHelperText(" ");
props.updateFilterState({ soilVariant: soilVariantSelection }); updateFilters({ soilVariant: soilVariantSelection });
props.setNextDisabled(false); setNextDisabled(false);
}; };
return ( return (

View file

@ -1,10 +1,15 @@
import { useState } from 'react';
import Step from "../Step"; import Step from "../Step";
import SoilSelector from "./SoilSelector"; import SoilSelector from "./SoilSelector";
import StepInformation from "../StepInformation"; import StepInformation from "../StepInformation";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import soilBackgroundImage from "../../../assets/img/stepBackgrounds/step2.jpg"; import soilBackgroundImage from "../../../assets/img/stepBackgrounds/step2.jpg";
import { StepperFooter } from '../../providers/StepperProvider';
export default function SoilVariantStep(props) { export default function SoilVariantStep(props) {
const [nextDisabled, setNextDisabled] = useState(true);
const SOIL_DESCRIPTION = ( const SOIL_DESCRIPTION = (
<p> <p>
From your site location, we use{" "} From your site location, we use{" "}
@ -36,15 +41,18 @@ export default function SoilVariantStep(props) {
const soilVarientSelectionPanel = ( const soilVarientSelectionPanel = (
<div className="p-5"> <div className="p-5">
<SoilSelector {...props} /> <SoilSelector setNextDisabled={setNextDisabled} />
</div> </div>
); );
return ( return (
<Step <>
informationComponent={soilVarientInfoPanel} <Step
selectionComponent={soilVarientSelectionPanel} informationComponent={soilVarientInfoPanel}
backgroundImage={soilBackgroundImage} selectionComponent={soilVarientSelectionPanel}
/> backgroundImage={soilBackgroundImage}
/>
<StepperFooter nextDisabled={nextDisabled} />
</>
); );
} }

View file

@ -1,9 +0,0 @@
import { useEffect } from "react";
export default function ProjectSpecificsSelector(props) {
useEffect(() => {
props.setNextDisabled(false);
});
return <h2>Project Specifics Selector</h2>;
}

View file

@ -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}
/>
);
}

View file

@ -8,13 +8,15 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import SummaryTable from "./SummaryTable"; import SummaryTable from "./SummaryTable";
import LocationRepository from "../../../repository/LocationRepository"; 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 [expanded, setExpanded] = React.useState(null);
const [locationDetails, setLocationDetails] = React.useState({}); const [locationDetails, setLocationDetails] = React.useState({});
const { filters } = useFilter();
const getLocationDetails = () => { const getLocationDetails = () => {
LocationRepository.getLocationData(props.filters).then((result) => { LocationRepository.getLocationData(filters).then((result) => {
setLocationDetails(result); setLocationDetails(result);
}); });
}; };
@ -24,7 +26,6 @@ export default function SummaryContent(props) {
}; };
React.useEffect(() => { React.useEffect(() => {
props.setNextDisabled(false);
!Object.keys(locationDetails).length && getLocationDetails(); !Object.keys(locationDetails).length && getLocationDetails();
}); });
@ -35,7 +36,7 @@ export default function SummaryContent(props) {
const locationData = [ const locationData = [
createData( createData(
"Geographical Coordinates (latitude, longitude)", "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("Ecological Region", locationDetails.ecological_region || ""),
createData( createData(
@ -50,22 +51,22 @@ export default function SummaryContent(props) {
"Soil Order", "Soil Order",
`${locationDetails.soil_name} (${locationDetails.soil_code})` || "" `${locationDetails.soil_name} (${locationDetails.soil_code})` || ""
), ),
createData("Soil Variant", props.filters.soilVariant), createData("Soil Variant", filters.soilVariant),
]; ];
const siteData = [ const siteData = [
createData("Habitat", props.filters.habitat.name ?? ""), createData("Habitat", filters.habitat.name ?? ""),
createData( createData(
"Zone Name", "Zone Name",
(props.filters.zone && props.filters.zone.name) ?? "" (filters.zone && filters.zone.name) ?? ""
), ),
createData( createData(
"Zone Variant", "Zone Variant",
(props.filters.zone && props.filters.zone.variant) ?? "" (filters.zone && filters.zone.variant) ?? ""
), ),
createData( createData(
"Zone Refined Variant", "Zone Refined Variant",
(props.filters.zone && props.filters.zone.refined_variant) ?? "" (filters.zone && filters.zone.refined_variant) ?? ""
), ),
]; ];

View file

@ -3,8 +3,10 @@ import SummaryContent from "./SummaryContent";
import StepInformation from "../StepInformation"; import StepInformation from "../StepInformation";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import summaryBackgroundImage from "../../../assets/img/stepBackgrounds/step5.jpg"; 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 = ( const summaryInfoPanel = (
<StepInformation <StepInformation
title={staticText.steps.summary.title} title={staticText.steps.summary.title}
@ -12,13 +14,16 @@ export default function SummaryStep(props) {
/> />
); );
const summaryContent = <SummaryContent {...props} />; const summaryContent = <SummaryContent />;
return ( return (
<Step <>
informationComponent={summaryInfoPanel} <Step
selectionComponent={summaryContent} informationComponent={summaryInfoPanel}
backgroundImage={summaryBackgroundImage} selectionComponent={summaryContent}
/> backgroundImage={summaryBackgroundImage}
/>
<StepperFooter onSubmit={onSubmit} />
</>
); );
} }

View file

@ -1,11 +1,13 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import SiteRepository from "../../../repository/SiteRepository"; import SiteRepository from "../../../repository/SiteRepository";
import { useFilter } from "../../providers/FilterProvider";
import { HabitatSVG } from "../HabitatSVG"; import { HabitatSVG } from "../HabitatSVG";
export default function ZoneSelector(props) { export default function ZoneSelector({setNextDisabled}) {
const [habitatImageObject, setHabitatImageObject] = useState({}); const [habitatImageObject, setHabitatImageObject] = useState({});
const [segmentMapping, setSegmentMapping] = useState({}); const [segmentMapping, setSegmentMapping] = useState({});
const [selectedZoneSegment, setZoneSegment] = useState(null); const [selectedZoneSegment, setZoneSegment] = useState(null);
const { filters, updateFilters } = useFilter();
const setZoneOrRedirect = (zone) => { const setZoneOrRedirect = (zone) => {
const redirectHabitat = zone && zone.redirect_habitat; const redirectHabitat = zone && zone.redirect_habitat;
@ -15,15 +17,13 @@ export default function ZoneSelector(props) {
? { habitat: { id: redirectHabitat.id, name: redirectHabitat.name } } ? { habitat: { id: redirectHabitat.id, name: redirectHabitat.name } }
: { zone: zone }; : { zone: zone };
props.updateFilterState(newFilterState); updateFilters(newFilterState);
props.setRedirectBack(Boolean(redirectHabitat)); setNextDisabled(false);
props.setNextDisabled(false);
}; };
useEffect(() => { useEffect(() => {
const getHabitatImage = () => { const getHabitatImage = () => {
SiteRepository.getHabitatImage(props.filters.habitatImage).then( SiteRepository.getHabitatImage(filters.habitatImage).then(
(response) => { (response) => {
if (response.status === 200) { if (response.status === 200) {
const imageData = response.data; const imageData = response.data;
@ -34,8 +34,8 @@ export default function ZoneSelector(props) {
}; };
const setInitialZone = () => { const setInitialZone = () => {
if (props.filters.zone) { if (filters.zone) {
const zone = props.filters.zone; const zone = filters.zone;
const zoneSegment = document.querySelectorAll( const zoneSegment = document.querySelectorAll(
`.zone-selector-svg svg path[inkscapelabel="${zone.related_svg_segment}"]` `.zone-selector-svg svg path[inkscapelabel="${zone.related_svg_segment}"]`
)[0]; )[0];
@ -45,7 +45,7 @@ export default function ZoneSelector(props) {
setZoneSegment(zoneSegment); setZoneSegment(zoneSegment);
zoneSegment.style.fill = "#eeeeee"; zoneSegment.style.fill = "#eeeeee";
zoneSegment.style["fill-opacity"] = 0.5; 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 // Retrieves the habitat image from the api if it's not loaded already
Object.keys(segmentMapping).length === 0 && getZones(); Object.keys(segmentMapping).length === 0 && getZones();
}, [habitatImageObject, segmentMapping, props, props.filters.zone]); }, [habitatImageObject, segmentMapping, setNextDisabled, filters]);
const selectZone = (element) => { const selectZone = (element) => {
if ( if (

View file

@ -1,10 +1,20 @@
import { useState } from 'react';
import Button from '@mui/material/Button';
import Step from "../Step"; import Step from "../Step";
import ZoneSelector from "./ZoneSelector"; import ZoneSelector from "./ZoneSelector";
import StepInformation from "../StepInformation"; import StepInformation from "../StepInformation";
import staticText from "../../../assets/data/staticText.json"; import staticText from "../../../assets/data/staticText.json";
import zoneBackgroundImage from "../../../assets/img/stepBackgrounds/step4.jpg"; 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 = ( const zoneInfoPanel = (
<StepInformation <StepInformation
title={staticText.steps.zone.title} title={staticText.steps.zone.title}
@ -23,15 +33,21 @@ export default function ZoneStep(props) {
alignItems: "center", alignItems: "center",
}} }}
> >
<ZoneSelector {...props} /> <ZoneSelector setNextDisabled={setNextDisabled} />
</div> </div>
); );
return ( return (
<Step <>
informationComponent={zoneInfoPanel} <Step
selectionComponent={zoneSelectionPanel} informationComponent={zoneInfoPanel}
backgroundImage={zoneBackgroundImage} selectionComponent={zoneSelectionPanel}
/> backgroundImage={zoneBackgroundImage}
/>
<StepperFooter
nextDisabled={nextDisabled}
onNext={redirect ? setStepBack : setStepNext}
/>
</>
); );
} };

View file

@ -4,6 +4,7 @@ import { createTheme, ThemeProvider } from "@mui/material/styles";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { createBrowserRouter, RouterProvider } from "react-router-dom";
import MainPage from "./pages/MainPage"; import MainPage from "./pages/MainPage";
import { FilterProvider } from "./components/providers/FilterProvider";
// Styles // Styles
import "./assets/styles/main.scss"; import "./assets/styles/main.scss";
@ -33,9 +34,11 @@ const darkTheme = createTheme({
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<div className="App"> <div className="App">
<ThemeProvider theme={darkTheme}> <FilterProvider>
<RouterProvider router={router} /> <ThemeProvider theme={darkTheme}>
</ThemeProvider> <RouterProvider router={router} />
</ThemeProvider>
</FilterProvider>
</div> </div>
</React.StrictMode>, </React.StrictMode>,
document.getElementById("root") document.getElementById("root")

View file

@ -1,45 +1,29 @@
import { Container } from "reactstrap"; import { Container } from "reactstrap";
import Stepper from "../components/Stepper";
import Header from "../components/Header"; import Header from "../components/Header";
import AddressStep from "../components/steps/address/AddressStep"; import AddressStep from "../components/steps/address/AddressStep";
import SoilStep from "../components/steps/soilvariant/SoilStep"; import SoilStep from "../components/steps/soilvariant/SoilStep";
import HabitatStep from "../components/steps/habitat/HabitatStep"; import HabitatStep from "../components/steps/habitat/HabitatStep";
import ZoneStep from "../components/steps/zone/ZoneStep"; import ZoneStep from "../components/steps/zone/ZoneStep";
import SummaryStep from "../components/steps/summary/SummaryStep"; 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 = () => ( const ApplyPage = () => {
<Container fluid className="main-container p-0"> const { submit } = useFilter();
<Header />
<Stepper return (
steps={[ <Container fluid className="main-container p-0">
{ <Header />
label: "Enter address", <StepperWizard>
component: AddressStep, <AddressStep label="Enter address" tooltip="Enter your 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" />
label: "Choose soil", <SummaryStep label="Summary" tooltip="Check your inputs" onSubmit={submit} />
component: SoilStep, <CompleteStep label="Complete" tooltip="Complete your application" />
tooltip: "Describe the moisture content of your soil", </StepperWizard>
}, </Container>);
{ };
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; export default ApplyPage;

View file

@ -1,5 +1,4 @@
import { Container } from "reactstrap"; import { Container } from "reactstrap";
import Stepper from "../components/Stepper";
import Header from "../components/Header"; import Header from "../components/Header";
import LocationStep from "../components/steps/location/LocationStep"; import LocationStep from "../components/steps/location/LocationStep";
import SoilStep from "../components/steps/soilvariant/SoilStep"; 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 ZoneStep from "../components/steps/zone/ZoneStep";
import SummaryStep from "../components/steps/summary/SummaryStep"; import SummaryStep from "../components/steps/summary/SummaryStep";
import ResultsStep from "../components/steps/results/ResultsStep"; import ResultsStep from "../components/steps/results/ResultsStep";
import { StepperWizard } from "../components/providers/StepperProvider";
const MainPage = () => ( const MainPage = () => {
<Container fluid className="main-container p-0"> return (
<Header /> <Container fluid className="main-container p-0">
<Stepper <Header />
steps={[ <StepperWizard>
{ <LocationStep label="Select location" tooltip="Click on a location on the map" />
label: "Select location", <SoilStep label="Choose soil" tooltip="Describe the moisture content of your soil" />
component: LocationStep, <HabitatStep label="Choose habitat" tooltip="Specify type of landscape to be planted" />
tooltip: "Click on a location on the map", <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" />
label: "Choose soil", </StepperWizard>
component: SoilStep, </Container>);
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; export default MainPage;

View file

@ -2,10 +2,9 @@ import axios from "axios";
// Create the axios object // Create the axios object
const repo = axios.create({ const repo = axios.create({
baseURL: baseURL: "/api",
window.location.hostname === "localhost" xsrfHeaderName: "X-CSRFToken",
? "http://localhost:9000/api" xsrfCookieName: "csrftoken",
: "/api",
}); });
repo.defaults.headers.post["access-control-allow-origin"] = "*"; repo.defaults.headers.post["access-control-allow-origin"] = "*";