diff --git a/react/package-lock.json b/react/package-lock.json index dbde0e59..88d0fde5 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -29,6 +29,7 @@ "prettier": "^2.7.1", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-datepicker": "^6.6.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-esri-leaflet": "^2.0.1", @@ -2262,6 +2263,54 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.9.tgz", + "integrity": "sha512-p86wynZJVEkEq2BBjY/8p2g3biQ6TlgT4o/3KgFKyTWoJLU1GZ8wpctwRqtkEl2tseYA+kw7dBAIDFcednfI5w==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.8", + "@floating-ui/utils": "^0.2.1", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "dependencies": { + "@floating-ui/dom": "^1.6.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -6463,6 +6512,14 @@ "node": ">=12" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6954,6 +7011,15 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -14134,6 +14200,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-6.6.0.tgz", + "integrity": "sha512-ERC0/Q4pPC9bNIcGUpdCbHc+oCxhkU3WI3UOGHkyJ3A9fqALCYpEmLc5S5xvAd7DuCDdbsyW97oRPM6pWWwjww==", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -14206,6 +14288,19 @@ "react-dom": "^18.0.0" } }, + "node_modules/react-onclickoutside": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", + "integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", @@ -15142,6 +15237,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/react/package.json b/react/package.json index f4b43f70..2256fc7b 100644 --- a/react/package.json +++ b/react/package.json @@ -53,6 +53,7 @@ "prettier": "^2.7.1", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-datepicker": "^6.6.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-esri-leaflet": "^2.0.1", diff --git a/react/src/components/FiltersPanel/Filter.tsx b/react/src/components/FiltersPanel/Filter.tsx new file mode 100644 index 00000000..850f393c --- /dev/null +++ b/react/src/components/FiltersPanel/Filter.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import styles from './Filters.module.css'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; + +interface FiltersProps { + selectedAssetTypes: string[]; + onFiltersChange: (selectedAssetTypes: string[]) => void; + startDate: Date; + setStartDate: (date: Date) => void; + endDate: Date; + setEndDate: (date: Date) => void; +} + +interface CustomInputProps { + value?: string; + onClick?: () => void; +} + +export const assetTypeOptions = { + Image: 'Image', + Video: 'Video', + PointCloud: 'Point Cloud', + Streetview: 'Streetview', + Questionnaire: 'Questionnaire', + NoAssetVector: 'No Asset Vector', +}; + +const Filters: React.FC = ({ + selectedAssetTypes, + onFiltersChange, + startDate, + setStartDate, + endDate, + setEndDate, +}) => { + const handleFilterChange = (assetType: string) => { + if (selectedAssetTypes.includes(assetType)) { + onFiltersChange(selectedAssetTypes.filter((type) => type !== assetType)); + } else { + onFiltersChange([...selectedAssetTypes, assetType]); + } + }; + + const CustomInputWithTooltip = React.forwardRef< + HTMLInputElement, + CustomInputProps + >(({ value, onClick }, ref) => ( +
+ + + ? + +
+ )); + + CustomInputWithTooltip.displayName = 'CustomInputWithTooltip'; + + return ( +
+

Filters

+

Date Range

+
Start Date
+ setStartDate(date)} + selectsStart + startDate={startDate} + endDate={endDate} + customInput={} + /> +
End Date
+ setEndDate(date)} + selectsEnd + startDate={startDate} + endDate={endDate} + minDate={startDate} + customInput={} + /> +

Asset Types

+ {Object.entries(assetTypeOptions).map(([key, value]) => ( +
+ +
+ ))} +
+ ); +}; + +export default Filters; diff --git a/react/src/components/FiltersPanel/Filters.module.css b/react/src/components/FiltersPanel/Filters.module.css new file mode 100644 index 00000000..1657d61c --- /dev/null +++ b/react/src/components/FiltersPanel/Filters.module.css @@ -0,0 +1,52 @@ +.root { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding-left: 20px; +} + +.customInputContainer { + position: relative; + display: inline-block; +} + +.customInput { + padding-right: 20px; + width: 73%; +} + +.tooltip { + font-weight: bold; + position: absolute; + right: 15px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; +} + +.filterOption { + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.filterLabel { + padding-top: 5px; +} + +.filterOption input[type='checkbox'] { + margin-right: 10px; +} + +h2 { + padding-top: 9px; +} + +h3 { + color: #0f83bd; +} + +h5 { + padding-top: 5px; +} diff --git a/react/src/components/FiltersPanel/index.ts b/react/src/components/FiltersPanel/index.ts new file mode 100644 index 00000000..74949696 --- /dev/null +++ b/react/src/components/FiltersPanel/index.ts @@ -0,0 +1 @@ +export { default } from './Filter'; diff --git a/react/src/hooks/features/useFeatures.ts b/react/src/hooks/features/useFeatures.ts index 649bb904..742fb8b9 100644 --- a/react/src/hooks/features/useFeatures.ts +++ b/react/src/hooks/features/useFeatures.ts @@ -12,17 +12,19 @@ const useFeatures = ({ projectId, isPublic, options, -}: UseFeaturesParams): UseQueryResult => { + assetTypes, +}: UseFeaturesParams & { + assetTypes?: string[]; +}): UseQueryResult => { const featuresRoute = isPublic ? 'public-projects' : 'projects'; - const endpoint = `/${featuresRoute}/${projectId}/features/`; + let endpoint = `/${featuresRoute}/${projectId}/features/`; + if (assetTypes?.length) { + endpoint += `?assetType=${assetTypes.join(',')}`; + } - /* TODO_REACT add assets filter in https://tacc-main.atlassian.net/browse/WG-242. - Filter route looks like something like this in v2: - "projects/1027/features/?assetType=image%2Cvideo%2Cpoint_cloud%2Cstreetview%2Cquestionnaire%2Cno_asset_vector&&updates=null&cloneFrom=null&encoder=%5Bobject%20Object%5D&map=%5Bobject%20Map%5D&application=hazmapper" - */ const query = useGet({ endpoint, - key: ['features', { projectId, isPublic }], + key: ['features', { projectId, isPublic, assetTypes }], options, }); return query; diff --git a/react/src/pages/MapProject/MapProject.tsx b/react/src/pages/MapProject/MapProject.tsx index 4c1c492e..51c28e6c 100644 --- a/react/src/pages/MapProject/MapProject.tsx +++ b/react/src/pages/MapProject/MapProject.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useState } from 'react'; import { useLocation } from 'react-router-dom'; import Map from '../../components/Map'; import AssetsPanel from '../../components/AssetsPanel'; @@ -8,6 +9,8 @@ import { useFeatures, useProject, useTileServers } from '../../hooks'; import { useParams } from 'react-router-dom'; import styles from './MapProject.module.css'; import MapProjectNavBar from '../../components/MapProjectNavBar'; +import Filters from '../../components/FiltersPanel/Filter'; +import { assetTypeOptions } from '../../components/FiltersPanel/Filter'; interface Props { /** @@ -22,6 +25,28 @@ interface Props { */ const MapProject: React.FC = ({ isPublic = false }) => { const { projectUUID } = useParams(); + const [selectedAssetTypes, setSelectedAssetTypes] = useState( + Object.keys(assetTypeOptions) + ); + const [startDate, setStartDate] = useState(new Date()); + const [endDate, setEndDate] = useState( + new Date(Date.now() + 24 * 60 * 60 * 1000) + ); + + const formatAssetTypeName = (name: string) => { + switch (name) { + case 'PointCloud': + return 'point_cloud'; + case 'NoAssetVector': + return 'no_asset_vector'; + default: + return name.toLowerCase(); + } + }; + + const formattedAssetTypes = selectedAssetTypes.map((type) => + formatAssetTypeName(type) + ); const { data: activeProject, @@ -46,6 +71,7 @@ const MapProject: React.FC = ({ isPublic = false }) => { options: { enabled: canFetchProjectFeaturesOrLayers, }, + assetTypes: formattedAssetTypes, }); const { @@ -89,6 +115,16 @@ const MapProject: React.FC = ({ isPublic = false }) => { {activePanel === Panel.Assets && ( )} + {activePanel === Panel.Filters && ( + + )} )} {activePanel === Panel.Manage && ( diff --git a/react/src/requests.ts b/react/src/requests.ts index ab27418f..df9b90d7 100644 --- a/react/src/requests.ts +++ b/react/src/requests.ts @@ -111,7 +111,13 @@ export function useGet({ } const queryParams = new URLSearchParams(analytics_params).toString(); - url += `?${queryParams}`; + if (url.includes('?')) { + // If the URL contains other parameters, prepend with '&' + url += `&${queryParams}`; + } else { + // If the URL contains no parameters, start with '?' + url += `?${queryParams}`; + } } const getUtil = async () => { diff --git a/react/vite.config.ts b/react/vite.config.ts index a892dc35..ab82a434 100644 --- a/react/vite.config.ts +++ b/react/vite.config.ts @@ -14,7 +14,13 @@ export default defineConfig(({ command, mode }) => { rollupOptions: { output: { manualChunks(id) { - if (id.includes('node_modules')) { + if (id.includes('node_modules/react-datepicker')) { + return 'react-datepicker'; + } else if (id.includes('node_modules/leaflet')) { + return 'leaflet'; + } else if (id.includes('node_modules/@turf/turf')) { + return 'turf'; + } else if (id.includes('node_modules')) { return 'vendor'; } },