[#41] Allow users to download the user/plang guide for a payment
- frontend changes to support physical and digital checkouts
This commit is contained in:
parent
a7d7581c1b
commit
d39e7da6f1
18 changed files with 381 additions and 327 deletions
|
@ -6,11 +6,7 @@
|
|||
},
|
||||
"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."
|
||||
},
|
||||
"address": {
|
||||
"title": "Address",
|
||||
"description": "Thank you for purchasing an activation key. Please start entering your address and select an option from the suggestions provided. 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. Once the location is selected, click on the “next step” button to complete the process."
|
||||
"description": "Your native plant selection starts here! Use the map or enter an address 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."
|
||||
},
|
||||
"soil": {
|
||||
"title": "Soil Variant Selection",
|
||||
|
@ -39,10 +35,6 @@
|
|||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
68
frontend/src/components/providers/ActivationProvider.jsx
Normal file
68
frontend/src/components/providers/ActivationProvider.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { createContext, useState, useContext } from 'react';
|
||||
import Repository from '../../repository/Repository';
|
||||
import { useError } from './ErrorProvider';
|
||||
|
||||
const ActivationContext = createContext(null);
|
||||
|
||||
const ActivationProvider = ({children}) => {
|
||||
const [key, setKey] = useState("");
|
||||
const [isPhysicalKey, setIsPhysicalKey] = useState(false);
|
||||
const { setError, resetError } = useError();
|
||||
|
||||
const validateKey = (value) => new Promise(resolve => {
|
||||
|
||||
const data = {key: value};
|
||||
|
||||
setKey(value);
|
||||
|
||||
Repository.post("/key/validate/", data).then(resp => {
|
||||
if (resp.data) {
|
||||
setIsPhysicalKey(resp.data?.type === "Stripe - physical");
|
||||
resetError();
|
||||
try {
|
||||
const coordinates = resp.data.location.match(/(-?\d+\.\d+)\s(-?\d+\.\d+)/);
|
||||
resolve({
|
||||
coordinates: {
|
||||
lng: parseFloat(coordinates[1]),
|
||||
lat: parseFloat(coordinates[2]),
|
||||
},
|
||||
soilVariant: resp.data.soil_variant[0],
|
||||
zone: {id: resp.data.zone},
|
||||
...data,
|
||||
});
|
||||
} catch {
|
||||
resolve({...data, ...resp.data});
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
}).catch(e => {
|
||||
switch (e.response.status) {
|
||||
case 400:
|
||||
case 404:
|
||||
setError("Invalid or expired activation key. Please try again.");
|
||||
break;
|
||||
default:
|
||||
setError("Something went wrong. Please try again.");
|
||||
break;
|
||||
}
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
const value = {
|
||||
key,
|
||||
isDigitalKey: !isPhysicalKey,
|
||||
isPhysicalKey,
|
||||
validateKey,
|
||||
};
|
||||
|
||||
return <ActivationContext.Provider value={value}>{children}</ActivationContext.Provider>;
|
||||
};
|
||||
|
||||
const useActivator = () => useContext(ActivationContext);
|
||||
|
||||
export {
|
||||
ActivationProvider,
|
||||
useActivator,
|
||||
};
|
24
frontend/src/components/providers/ErrorProvider.jsx
Normal file
24
frontend/src/components/providers/ErrorProvider.jsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createContext, useState, useContext } from 'react';
|
||||
|
||||
const ErrorContext = createContext(null);
|
||||
|
||||
const ErrorProvider = ({children}) => {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const resetError = () => setError("");
|
||||
|
||||
const value = {
|
||||
error,
|
||||
setError,
|
||||
resetError,
|
||||
};
|
||||
|
||||
return <ErrorContext.Provider value={value}>{children}</ErrorContext.Provider>;
|
||||
};
|
||||
|
||||
const useError = () => useContext(ErrorContext);
|
||||
|
||||
export {
|
||||
ErrorProvider,
|
||||
useError,
|
||||
};
|
|
@ -15,12 +15,14 @@ const StepperWizard = ({children}) => {
|
|||
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 setStepLast = () => setStep(children.length - 1);
|
||||
|
||||
const value = {
|
||||
step,
|
||||
setStep,
|
||||
setStepNext,
|
||||
setStepBack,
|
||||
setStepLast,
|
||||
isStep,
|
||||
};
|
||||
|
||||
|
|
|
@ -4,39 +4,36 @@ import StepInformation from '../StepInformation';
|
|||
import staticText from '../../../assets/data/staticText.json'
|
||||
import keyBackgroundImage from '../../../assets/img/stepBackgrounds/step6.jpg';
|
||||
import { StepperFooter, useStepper } from '../../providers/StepperProvider';
|
||||
import { useActivator } from '../../providers/ActivationProvider';
|
||||
import { useFilter } from '../../providers/FilterProvider';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Repository from '../../../repository/Repository';
|
||||
import PurchaseButton from './PurchaseButton';
|
||||
|
||||
const ActivationStep = () => {
|
||||
const MAX_LENGTH = 20;
|
||||
const [value, setValue] = useState(new URLSearchParams(window.location.search).get("key") || "");
|
||||
const [nextDisabled, setNextDisabled] = useState(value.length < MAX_LENGTH);
|
||||
const [error, setError] = useState("");
|
||||
const { updateFilters } = useFilter();
|
||||
const { setStepNext } = useStepper();
|
||||
const { setFilters, updateFilters } = useFilter();
|
||||
const { setStepNext, setStepLast } = useStepper();
|
||||
const { validateKey } = useActivator();
|
||||
|
||||
const onChange = e => {
|
||||
const newValue = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, MAX_LENGTH);
|
||||
setNextDisabled(newValue.length !== MAX_LENGTH);
|
||||
setValue(newValue);
|
||||
setError("");
|
||||
};
|
||||
|
||||
const onNext = () => {
|
||||
const data = { key: value };
|
||||
Repository.post("/key/validate/", data).then(resp => {
|
||||
const onNext = async () => {
|
||||
const data = await validateKey(value);
|
||||
|
||||
if (data?.coordinates) {
|
||||
setFilters(data);
|
||||
setStepLast();
|
||||
} else if (data) {
|
||||
updateFilters(data);
|
||||
setStepNext();
|
||||
}).catch(e => {
|
||||
setError(
|
||||
e.response.status === 404
|
||||
? "Invalid or expired activation key. Please try again."
|
||||
: "Something went wrong. Please try again."
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const keyPanel = (
|
||||
|
@ -51,14 +48,12 @@ const ActivationStep = () => {
|
|||
variant="outlined"
|
||||
placeholder="Enter activation key..."
|
||||
autoComplete="off"
|
||||
error={error.length > 0}
|
||||
helperText={error}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', py: 2, paddingRight: '10pt', paddingLeft: '10pt', minHeight: '68px' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
<Button href="/api/key/purchase">Purchase Key</Button>
|
||||
<PurchaseButton />
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
|
|
44
frontend/src/components/steps/activation/PurchaseButton.jsx
Normal file
44
frontend/src/components/steps/activation/PurchaseButton.jsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
|
||||
const PurchaseButton = () => {
|
||||
const [anchor, setAnchor] = useState(null);
|
||||
const isOpen = Boolean(anchor);
|
||||
|
||||
const toggleMenu = (e) => {
|
||||
setAnchor(isOpen ? null : e.currentTarget);
|
||||
};
|
||||
|
||||
const onSelect = (href) => {
|
||||
setAnchor(null);
|
||||
window.location = href;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
id="purchase-button"
|
||||
aria-controls={isOpen ? "basic-menu" : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen ? "true" : undefined}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
Purchase Key
|
||||
</Button>
|
||||
<Menu
|
||||
id="purchase-menu"
|
||||
anchorEl={anchor}
|
||||
open={isOpen}
|
||||
onClose={() => toggleMenu(null)}
|
||||
MenuListProps={{"aria-labelledby": "purchase-button"}}
|
||||
>
|
||||
<MenuItem onClick={() => onSelect("/api/key/purchase")}>Purchase digital copy</MenuItem>
|
||||
<MenuItem onClick={() => onSelect("/api/key/purchase?physical=true")}>Purchase physical copy</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseButton;
|
|
@ -1,49 +0,0 @@
|
|||
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 = () => {
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
const { updateFilters } = useFilter();
|
||||
|
||||
const addressPanel = (
|
||||
<div className="p-5">
|
||||
<StepInformation
|
||||
title={staticText.steps.address.title}
|
||||
description={<p>{staticText.steps.address.description}</p>}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<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}
|
||||
/>
|
||||
<StepperFooter nextDisabled={nextDisabled} />
|
||||
</>);
|
||||
};
|
||||
|
||||
export default AddressStep;
|
|
@ -1,24 +0,0 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -25,7 +25,7 @@ const AddressSearchSuggestions = ({results, onClick}) => (
|
|||
</Box>
|
||||
: null);
|
||||
|
||||
const AddressSearch = ({onSelect, classNames}) => {
|
||||
const AddressSearch = ({onSelect}) => {
|
||||
const [value, setValue] = useState("");
|
||||
const [enable, setEnable] = useState(true);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
@ -53,7 +53,7 @@ const AddressSearch = ({onSelect, classNames}) => {
|
|||
}, [selected, onSelect]);
|
||||
|
||||
return (
|
||||
<div classNames={classNames}>
|
||||
<Box sx={{width: '100%', height: '20%'}}>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
|
@ -74,6 +74,7 @@ const AddressSearch = ({onSelect, classNames}) => {
|
|||
}
|
||||
}}
|
||||
value={value}
|
||||
sx={{top: 0}}
|
||||
/>
|
||||
<AddressSearchSuggestions
|
||||
results={results}
|
||||
|
@ -83,7 +84,7 @@ const AddressSearch = ({onSelect, classNames}) => {
|
|||
setSelected(r);
|
||||
}}
|
||||
/>
|
||||
</div>);
|
||||
</Box>);
|
||||
};
|
||||
|
||||
export default AddressSearch;
|
|
@ -1,31 +1,57 @@
|
|||
import { useState } from "react";
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
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';
|
||||
import AddressSearch from "./AddressSearch";
|
||||
import { useFilter } from '../../providers/FilterProvider';
|
||||
|
||||
export default function LocationStep(props) {
|
||||
export default function LocationStep({defaultIsSearch = false}) {
|
||||
const [nextDisabled, setNextDisabled] = useState(true);
|
||||
const [showSearch, setShowSearch] = useState(defaultIsSearch);
|
||||
const { updateFilters } = useFilter();
|
||||
|
||||
const locationInfoPanel = (
|
||||
<StepInformation
|
||||
title={staticText.steps.location.title}
|
||||
description={<p>{staticText.steps.location.description}</p>}
|
||||
/>
|
||||
<div>
|
||||
<StepInformation
|
||||
title={staticText.steps.location.title}
|
||||
description={<p>{staticText.steps.location.description}</p>}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', py: 2, paddingRight: '10pt', paddingLeft: '10pt', minHeight: '68px' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
<Button onClick={() => setShowSearch(s => !s)}>Switch to {showSearch ? "map" : "search"}</Button>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
|
||||
const locationSelectionPanel = <LocationSelectorMap setNextDisabled={setNextDisabled} />;
|
||||
const selectionComponent = showSearch
|
||||
? <AddressSearch onSelect={address => {
|
||||
if (address) {
|
||||
setNextDisabled(false);
|
||||
updateFilters({
|
||||
coordinates: {
|
||||
lat: address.coordinates[1],
|
||||
lng: address.coordinates[0],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setNextDisabled(true);
|
||||
}
|
||||
}}/>
|
||||
: <LocationSelectorMap setNextDisabled={setNextDisabled} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Step
|
||||
informationComponent={locationInfoPanel}
|
||||
selectionComponent={locationSelectionPanel}
|
||||
selectionComponent={selectionComponent}
|
||||
backgroundImage={locationBackgroundImage}
|
||||
/>
|
||||
<StepperFooter nextDisabled={nextDisabled} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,9 +23,8 @@ import staticText from "../../../assets/data/staticText.json";
|
|||
import forestGraphic from "../../../assets/img/habitats/1a_Forest_Section.png";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
|
||||
function TablePaginationActions(props) {
|
||||
function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
|
||||
const theme = useTheme();
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
|
||||
const handleFirstPageButtonClick = (event) => {
|
||||
onPageChange(event, 0);
|
||||
|
@ -95,18 +94,21 @@ TablePaginationActions.propTypes = {
|
|||
export default function PlantResultsTable(props) {
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(25);
|
||||
const isLoading = props?.rows === null;
|
||||
|
||||
let rows = [];
|
||||
if (props.rows) {
|
||||
if (!isLoading) {
|
||||
rows =
|
||||
rowsPerPage > 0
|
||||
? props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: props.rows;
|
||||
}
|
||||
|
||||
const totalRows = props?.rows?.length ?? 0;
|
||||
|
||||
// Avoid a layout jump when reaching the last page with empty rows.
|
||||
const emptyRows =
|
||||
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - props.rows.length) : 0;
|
||||
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - totalRows) : 0;
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
|
@ -157,30 +159,28 @@ export default function PlantResultsTable(props) {
|
|||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.length === 0 && (
|
||||
<TableRow style={{ height: 150 }}>
|
||||
<TableCell colSpan={7}>
|
||||
<Stack alignItems="center">
|
||||
<CircularProgress />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{rows.length > 0 &&
|
||||
rows.map((row) => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell component="th" scope="row">
|
||||
{row.name}
|
||||
{isLoading
|
||||
? <TableRow style={{ height: 150 }}>
|
||||
<TableCell colSpan={7}>
|
||||
<Stack alignItems="center">
|
||||
<CircularProgress />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.growthForm}</TableCell>
|
||||
<TableCell align="right">{row.moisturePreferences}</TableCell>
|
||||
<TableCell align="right">{row.plantTolerances}</TableCell>
|
||||
<TableCell align="right">{row.ecosystemServices}</TableCell>
|
||||
<TableCell align="right">{row.carbonSequestration}</TableCell>
|
||||
<TableCell align="right">{row.plantingStage}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
: rows.map((row) => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell component="th" scope="row">
|
||||
{row.name}
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.growthForm}</TableCell>
|
||||
<TableCell align="right">{row.moisturePreferences}</TableCell>
|
||||
<TableCell align="right">{row.plantTolerances}</TableCell>
|
||||
<TableCell align="right">{row.ecosystemServices}</TableCell>
|
||||
<TableCell align="right">{row.carbonSequestration}</TableCell>
|
||||
<TableCell align="right">{row.plantingStage}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: 53 * emptyRows }}>
|
||||
<TableCell colSpan={7} />
|
||||
|
@ -193,7 +193,7 @@ export default function PlantResultsTable(props) {
|
|||
className="plant-list-pagination"
|
||||
rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
|
||||
colSpan={7}
|
||||
count={props.rows.length}
|
||||
count={totalRows}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
SelectProps={{
|
||||
|
|
|
@ -5,10 +5,10 @@ import PlantList from "./PlantList";
|
|||
import Stack from "@mui/material/Stack";
|
||||
import Button from "@mui/material/Button";
|
||||
import PlantRepository from "../../../repository/PlantRepository";
|
||||
import { Typography, Box } from "@mui/material";
|
||||
import { Typography, Box, Modal } from "@mui/material";
|
||||
import resultsBackgroundImage from "../../../assets/img/stepBackgrounds/step6.jpg";
|
||||
import staticText from "../../../assets/data/staticText.json";
|
||||
import { useFilter } from "../../providers/FilterProvider";
|
||||
import { useActivator } from "../../providers/ActivationProvider";
|
||||
import { StepperFooter } from "../../providers/StepperProvider";
|
||||
|
||||
const RESULTS_DESCRIPTION = (
|
||||
|
@ -30,56 +30,32 @@ const RESULTS_DESCRIPTION = (
|
|||
);
|
||||
|
||||
export default function ResultsStep(props) {
|
||||
const [plants, setPlants] = useState([]);
|
||||
const { filters } = useFilter();
|
||||
const [plants, setPlants] = useState(null);
|
||||
const [showModal, setShowModal] = useState(true);
|
||||
const { key } = useActivator();
|
||||
|
||||
const closeModal = () => setShowModal(false);
|
||||
|
||||
useEffect(() => {
|
||||
const updatePlants = () => {
|
||||
PlantRepository.getFilteredPlants(filters)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
setPlants(response.data);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
this.setState({ plants: ["No plants found."] });
|
||||
});
|
||||
};
|
||||
updatePlants();
|
||||
}, [filters]);
|
||||
|
||||
function createData(
|
||||
name,
|
||||
growthForm,
|
||||
moisturePreferences,
|
||||
plantTolerances,
|
||||
ecosystemServices,
|
||||
carbonSequestration,
|
||||
plantingStage
|
||||
) {
|
||||
return {
|
||||
name,
|
||||
growthForm,
|
||||
moisturePreferences,
|
||||
plantTolerances,
|
||||
ecosystemServices,
|
||||
carbonSequestration,
|
||||
plantingStage,
|
||||
};
|
||||
}
|
||||
PlantRepository.getFilteredPlants(key)
|
||||
.then((response) => {
|
||||
setPlants(response.status === 200 ? response.data : []);
|
||||
}).catch(e => {
|
||||
setPlants([]);
|
||||
});
|
||||
}, [key]);
|
||||
|
||||
const getTableRows = () => {
|
||||
return plants.map((plant) => {
|
||||
return createData(
|
||||
plant.display_name,
|
||||
plant.display_growth_form,
|
||||
plant.moisture_preferences,
|
||||
plant.plant_tolerances,
|
||||
plant.ecosystem_services,
|
||||
plant.carbon_sequestration,
|
||||
plant.stage
|
||||
);
|
||||
});
|
||||
// null if unloaded, empty if loaded but no results
|
||||
return plants?.map((plant) => ({
|
||||
name: plant.display_name,
|
||||
growthForm: plant.display_growth_form,
|
||||
moisturePreferences: plant.moisture_preferences,
|
||||
plantTolerances: plant.plant_tolerances,
|
||||
ecosystemServices: plant.ecosystem_services,
|
||||
carbonSequestration: plant.carbon_sequestration,
|
||||
stage: plant.stage,
|
||||
})) ?? null;
|
||||
};
|
||||
|
||||
const download = (response, fileType, fileName) => {
|
||||
|
@ -94,13 +70,13 @@ export default function ResultsStep(props) {
|
|||
};
|
||||
|
||||
const downloadCSV = () => {
|
||||
PlantRepository.getPlantsCSV(filters).then((response) => {
|
||||
PlantRepository.getPlantsCSV(key).then((response) => {
|
||||
download(response, "text/csv", "plants.csv");
|
||||
});
|
||||
};
|
||||
|
||||
const downloadPDF = () => {
|
||||
PlantRepository.getPlantsPDF(filters).then((response) => {
|
||||
PlantRepository.getPlantsPDF(key).then((response) => {
|
||||
download(response, "application/pdf", "planting_guide.pdf");
|
||||
});
|
||||
};
|
||||
|
@ -142,6 +118,28 @@ export default function ResultsStep(props) {
|
|||
backgroundImage={resultsBackgroundImage}
|
||||
/>
|
||||
<StepperFooter />
|
||||
<Modal open={showModal} onClose={closeModal}>
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
color: 'white',
|
||||
p: 4,
|
||||
}}>
|
||||
<Typography variant="h5" component="h2">Questionnaire Complete</Typography>
|
||||
<Typography sx={{ mt: 2 }}>You have completed the questionnaire for your chosen habitat. If you have purchased a physical copy of the planting guide, it should arrive at your chosen delivery address in a number of business days.</Typography>
|
||||
<Typography sx={{ mt: 2 }}>You may now dismiss this popup to view a table of results. You are able to access this page at any time in future by supplying the activation key used at the start of this questionnaire.</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
<Button onClick={closeModal}>Close</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,45 +29,53 @@ export default function SummaryContent() {
|
|||
!Object.keys(locationDetails).length && getLocationDetails();
|
||||
});
|
||||
|
||||
function createData(name, value) {
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
const locationData = [
|
||||
createData(
|
||||
"Geographical Coordinates (latitude, longitude)",
|
||||
`(${filters.coordinates.lat}, ${filters.coordinates.lng})`
|
||||
),
|
||||
createData("Ecological Region", locationDetails.ecological_region || ""),
|
||||
createData(
|
||||
"Ecological District",
|
||||
locationDetails.ecological_district || ""
|
||||
),
|
||||
createData("Property Name", locationDetails.full_address || ""),
|
||||
{
|
||||
name: "Geographical Coordinates (latitude, longitude)",
|
||||
value: `(${filters.coordinates.lat}, ${filters.coordinates.lng})`,
|
||||
},
|
||||
{
|
||||
name: "Ecological Region",
|
||||
value: locationDetails.ecological_region || "",
|
||||
},
|
||||
{
|
||||
name: "Ecological District",
|
||||
value: locationDetails.ecological_district || "",
|
||||
},
|
||||
{
|
||||
name: "Property Name",
|
||||
value: locationDetails.full_address || "",
|
||||
},
|
||||
];
|
||||
|
||||
const soilData = [
|
||||
createData(
|
||||
"Soil Order",
|
||||
`${locationDetails.soil_name} (${locationDetails.soil_code})` || ""
|
||||
),
|
||||
createData("Soil Variant", filters.soilVariant),
|
||||
{
|
||||
name: "Soil Order",
|
||||
value: `${locationDetails.soil_name} (${locationDetails.soil_code})` || "",
|
||||
},
|
||||
{
|
||||
name: "Soil Variant",
|
||||
value: filters.soilVariant,
|
||||
},
|
||||
];
|
||||
|
||||
const siteData = [
|
||||
createData("Habitat", filters.habitat.name ?? ""),
|
||||
createData(
|
||||
"Zone Name",
|
||||
(filters.zone && filters.zone.name) ?? ""
|
||||
),
|
||||
createData(
|
||||
"Zone Variant",
|
||||
(filters.zone && filters.zone.variant) ?? ""
|
||||
),
|
||||
createData(
|
||||
"Zone Refined Variant",
|
||||
(filters.zone && filters.zone.refined_variant) ?? ""
|
||||
),
|
||||
{
|
||||
name: "Habitat",
|
||||
value: filters.habitat.name ?? "",
|
||||
},
|
||||
{
|
||||
name: "Zone Name",
|
||||
value: (filters.zone && filters.zone.name) ?? "",
|
||||
},
|
||||
{
|
||||
name: "Zone Variant",
|
||||
value: (filters.zone && filters.zone.variant) ?? "",
|
||||
},
|
||||
{
|
||||
name: "Zone Refined Variant",
|
||||
value: (filters.zone && filters.zone.refined_variant) ?? "",
|
||||
},
|
||||
];
|
||||
|
||||
const regionInformation = () => {
|
||||
|
|
|
@ -9,17 +9,14 @@ import { FilterProvider } from "./components/providers/FilterProvider";
|
|||
// Styles
|
||||
import "./assets/styles/main.scss";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import ApplyPage from "./pages/ApplyPage";
|
||||
import { ActivationProvider } from "./components/providers/ActivationProvider";
|
||||
import { ErrorProvider } from "./components/providers/ErrorProvider";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <MainPage />,
|
||||
},
|
||||
{
|
||||
path: "/apply",
|
||||
element: <ApplyPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
const darkTheme = createTheme({
|
||||
|
@ -34,11 +31,15 @@ const darkTheme = createTheme({
|
|||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<div className="App">
|
||||
<FilterProvider>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</FilterProvider>
|
||||
<ErrorProvider>
|
||||
<FilterProvider>
|
||||
<ActivationProvider>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</ActivationProvider>
|
||||
</FilterProvider>
|
||||
</ErrorProvider>
|
||||
</div>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { Container } from "reactstrap";
|
||||
import Header from "../components/Header";
|
||||
import ActivationStep from "../components/steps/activation/ActivationStep";
|
||||
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 CompleteStep from "../components/steps/complete/CompleteStep";
|
||||
import { StepperWizard } from "../components/providers/StepperProvider";
|
||||
import { useFilter } from "../components/providers/FilterProvider";
|
||||
import { Box, Typography, Modal, Button } from '@mui/material';
|
||||
|
||||
|
||||
const ApplyPage = () => {
|
||||
const [error, setError] = useState("");
|
||||
const { submit } = useFilter();
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
await submit();
|
||||
} catch (e) {
|
||||
setError("There was a problem sending data to the API. Please try again.");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container fluid className="main-container p-0">
|
||||
<Header />
|
||||
<StepperWizard>
|
||||
<ActivationStep label="Activate" tooltip="Enter your activation key" />
|
||||
<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={onSubmit} />
|
||||
<CompleteStep label="Complete" tooltip="Complete your application" />
|
||||
</StepperWizard>
|
||||
</Container>
|
||||
<Modal open={error.length > 0} onClose={() => setError("")}>
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
color: 'white',
|
||||
p: 4,
|
||||
}}>
|
||||
<Typography variant="h4" component="h2">Error</Typography>
|
||||
<Typography sx={{ mt: 2 }}>{error}</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
<Button onClick={() => setError("")}>Close</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>);
|
||||
};
|
||||
|
||||
export default ApplyPage;
|
|
@ -1,5 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { Container } from "reactstrap";
|
||||
import Header from "../components/Header";
|
||||
import ActivationStep from "../components/steps/activation/ActivationStep";
|
||||
import LocationStep from "../components/steps/location/LocationStep";
|
||||
import SoilStep from "../components/steps/soilvariant/SoilStep";
|
||||
import HabitatStep from "../components/steps/habitat/HabitatStep";
|
||||
|
@ -7,20 +9,52 @@ 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";
|
||||
import { useFilter } from "../components/providers/FilterProvider";
|
||||
import { Box, Typography, Modal, Button } from '@mui/material';
|
||||
import { useActivator } from '../components/providers/ActivationProvider';
|
||||
import { useError } from '../components/providers/ErrorProvider';
|
||||
|
||||
const MainPage = () => {
|
||||
const { isPhysicalKey } = useActivator();
|
||||
const { submit } = useFilter();
|
||||
const { error, resetError } = useError();
|
||||
|
||||
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>);
|
||||
<>
|
||||
<Container fluid className="main-container p-0">
|
||||
<Header />
|
||||
<StepperWizard>
|
||||
<ActivationStep label="Activate" tooltip="Enter your activation key" />
|
||||
<LocationStep label="Select location" tooltip="Click on a location on the map" defaultIsSearch={isPhysicalKey} />
|
||||
<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} />
|
||||
<ResultsStep label="Results" tooltip="List of plant species and user guide" />
|
||||
</StepperWizard>
|
||||
</Container>
|
||||
<Modal open={error.length > 0} onClose={resetError}>
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'background.paper',
|
||||
border: '2px solid #000',
|
||||
boxShadow: 24,
|
||||
color: 'white',
|
||||
p: 4,
|
||||
}}>
|
||||
<Typography variant="h4" component="h2">Error</Typography>
|
||||
<Typography sx={{ mt: 2 }}>{error}</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<Box sx={{ flex: '1 1 auto' }} />
|
||||
<Button onClick={resetError}>Close</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>);
|
||||
};
|
||||
|
||||
export default MainPage;
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
import API from "./Repository";
|
||||
|
||||
const LocationRepsostory = {
|
||||
getSoilDetails(filters) {
|
||||
return API.get(`/soil/`, { params: filters });
|
||||
getSoilDetails(params) {
|
||||
return API.get(`/soil/`, {params});
|
||||
},
|
||||
|
||||
getEcologicalDistrictDetails(filters) {
|
||||
return API.get(`/ecologicaldistrict/`, { params: filters });
|
||||
getEcologicalDistrictDetails(params) {
|
||||
return API.get(`/ecologicaldistrict/`, {params});
|
||||
},
|
||||
|
||||
getRegionDetails(filters) {
|
||||
return API.get(`/region/`, { params: filters });
|
||||
getRegionDetails(params) {
|
||||
return API.get(`/region/`, {params});
|
||||
},
|
||||
|
||||
getPropertyDetails(filters) {
|
||||
return API.get(`/address/`, { params: filters });
|
||||
getPropertyDetails(params) {
|
||||
return API.get(`/address/`, {params});
|
||||
},
|
||||
|
||||
async getLocationData(filters) {
|
||||
const params = { lat: filters.coordinates.lat, lng: filters.coordinates.lng };
|
||||
const [
|
||||
soilDetails,
|
||||
ecologicalDistrictDetails,
|
||||
propertyDetails,
|
||||
regionDetails,
|
||||
] = await Promise.all([
|
||||
this.getSoilDetails(filters),
|
||||
this.getEcologicalDistrictDetails(filters),
|
||||
this.getPropertyDetails(filters),
|
||||
this.getRegionDetails(filters),
|
||||
this.getSoilDetails(params),
|
||||
this.getEcologicalDistrictDetails(params),
|
||||
this.getPropertyDetails(params),
|
||||
this.getRegionDetails(params),
|
||||
]);
|
||||
|
||||
let locationData = {};
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import Repository from "./Repository";
|
||||
|
||||
const PlantRepository = {
|
||||
getFilteredPlants(filters) {
|
||||
return Repository.get(`/plants/`, { params: filters });
|
||||
getFilteredPlants(key) {
|
||||
return Repository.get(`/plants/`, { params: {key} });
|
||||
},
|
||||
|
||||
getPlantsCSV(filters) {
|
||||
return Repository.get("/download/csv/", { params: filters });
|
||||
getPlantsCSV(key) {
|
||||
return Repository.get("/download/csv/", { params: {key} });
|
||||
},
|
||||
|
||||
getPlantsPDF(filters) {
|
||||
getPlantsPDF(key) {
|
||||
return Repository.get("/download/pdf/", {
|
||||
params: filters,
|
||||
params: {key},
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue