From f7800a910b365f1a937e1ea9a0bff6142c6911f7 Mon Sep 17 00:00:00 2001 From: StefanWout Date: Mon, 18 Nov 2024 17:10:25 +0000 Subject: [PATCH 01/15] form in modal for remote assesment in place --- package-lock.json | 228 ++++++++++++++++-- package.json | 9 +- src/app/components/portfolio/AddNew.tsx | 17 +- src/app/components/portfolio/Toolbar.tsx | 9 + .../components/RemoteAssesmentModal.tsx | 96 ++++++++ src/app/shadcn_components/ui/form.tsx | 178 ++++++++++++++ src/app/shadcn_components/ui/label.tsx | 1 + 7 files changed, 514 insertions(+), 24 deletions(-) create mode 100644 src/app/portfolio/[slug]/components/RemoteAssesmentModal.tsx create mode 100644 src/app/shadcn_components/ui/form.tsx diff --git a/package-lock.json b/package-lock.json index 65e8eb2..ac28db5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,16 @@ "@headlessui-float/react": "^0.11.2", "@headlessui/react": "^1.7.14", "@heroicons/react": "^2.0.18", + "@hookform/resolvers": "^3.9.1", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-hover-card": "^1.0.6", - "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.1.3", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", - "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.0.7", "@remixicon/react": "^4.2.0", "@tanstack/react-query": "^4.29.12", @@ -44,10 +45,12 @@ "postcss": "8.4.23", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.53.2", "tailwind-merge": "^1.13.2", "tailwindcss": "^3.4.3", "tailwindcss-animate": "^1.0.6", - "typescript": "5.0.4" + "typescript": "5.0.4", + "zod": "^3.23.8" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", @@ -1181,6 +1184,15 @@ "react": ">= 16" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -1608,6 +1620,25 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -1678,6 +1709,25 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -1843,18 +1893,41 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", - "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", + "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.0.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1905,6 +1978,25 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-navigation-menu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.3.tgz", @@ -2043,6 +2135,25 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", @@ -2117,6 +2228,25 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz", @@ -2141,16 +2271,31 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -2274,6 +2419,25 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -7374,6 +7538,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/next/node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/node-releases": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", @@ -8319,6 +8492,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.53.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.2.tgz", + "integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10015,9 +10204,10 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 1e0a2c7..382e746 100644 --- a/package.json +++ b/package.json @@ -17,15 +17,16 @@ "@headlessui-float/react": "^0.11.2", "@headlessui/react": "^1.7.14", "@heroicons/react": "^2.0.18", + "@hookform/resolvers": "^3.9.1", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-hover-card": "^1.0.6", - "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.1.3", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", - "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.0.7", "@remixicon/react": "^4.2.0", "@tanstack/react-query": "^4.29.12", @@ -50,10 +51,12 @@ "postcss": "8.4.23", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.53.2", "tailwind-merge": "^1.13.2", "tailwindcss": "^3.4.3", "tailwindcss-animate": "^1.0.6", - "typescript": "5.0.4" + "typescript": "5.0.4", + "zod": "^3.23.8" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", diff --git a/src/app/components/portfolio/AddNew.tsx b/src/app/components/portfolio/AddNew.tsx index aefb36f..5a347e0 100644 --- a/src/app/components/portfolio/AddNew.tsx +++ b/src/app/components/portfolio/AddNew.tsx @@ -5,7 +5,7 @@ import { NavigationMenuLink, NavigationMenuTrigger, } from "@/app/shadcn_components/ui/navigation-menu"; -import { PlusIcon, TableCellsIcon } from "@heroicons/react/24/outline"; +import { PlusIcon, TableCellsIcon, DocumentMagnifyingGlassIcon } from "@heroicons/react/24/outline"; import * as React from "react"; import { cn } from "@/lib/utils"; @@ -39,9 +39,13 @@ ListItem.displayName = "ListItem"; export default function AddNewDropDown({ isUploadCsvOpen, setIsUploadCsvOpen, + isRemoteAssesmentOpen, + setIsRemoteAssesmentOpen, }: { isUploadCsvOpen: boolean; setIsUploadCsvOpen: React.Dispatch>; + isRemoteAssesmentOpen: boolean; + setIsRemoteAssesmentOpen: React.Dispatch>; }) { function handleCickAddUnit() { console.log("Add unit"); @@ -51,6 +55,10 @@ export default function AddNewDropDown({ setIsUploadCsvOpen(!isUploadCsvOpen); } + function handleClickRemoteAssesment() { + setIsRemoteAssesmentOpen(!isRemoteAssesmentOpen); + } + return ( @@ -63,7 +71,12 @@ export default function AddNewDropDown({ Add Unit - + +
+ Remote Assesment +
+ Schedule a remote assesment +
Upload CSV diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index d09da5c..b375443 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -13,6 +13,7 @@ import { import AddNewDropDown from "./AddNew"; import { cva } from "class-variance-authority"; import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal"; +import RemoteAssesmentModal from "@/app/portfolio/[slug]/components/RemoteAssesmentModal"; import { useState } from "react"; import { useRouter } from "next/navigation"; @@ -40,6 +41,7 @@ export function Toolbar({ portfolioId }: ToolbarProps) { } const [modalIsOpen, setModalIsOpen] = useState(false); + const [isRemoteAssesmentOpen, setIsRemoteAssesmentOpen] = useState(false); return ( @@ -71,8 +73,15 @@ export function Toolbar({ portfolioId }: ToolbarProps) { + void; + portfolioId: string; +}) { + const [scenario, setScenario] = useState(undefined); + const [housingType, sethousingType] = useState(""); + const [selectedGoal, setSelectedGoal] = useState(""); + const [goalValue, setGoalValue] = useState(""); + const [addressLineOne, setAddressLineOne] = useState(""); + const [postcode, setPostcode] = useState(""); + const [buttonDisabled, setButtonDisabled] = useState(true); + + const form = useForm(); + + + return ( + <> + + setIsOpen(false)} + > + +
+ + +
+
+ + + + Title Goes Here + +
+ Body Goes here +
+ +
+ +
+
+
+
+
+
+
+ + ); +} \ No newline at end of file diff --git a/src/app/shadcn_components/ui/form.tsx b/src/app/shadcn_components/ui/form.tsx new file mode 100644 index 0000000..895f6e8 --- /dev/null +++ b/src/app/shadcn_components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/app/shadcn_components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +
@@ -93,4 +279,5 @@ export default function RemoteAssesmentModal({ ); -} \ No newline at end of file +} + diff --git a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx index a046403..0ebae80 100644 --- a/src/app/portfolio/[slug]/components/UploadCsvModal.tsx +++ b/src/app/portfolio/[slug]/components/UploadCsvModal.tsx @@ -96,7 +96,7 @@ const selectGoalOptions = [ { label: "Reduce energy consumption", value: "Reduce energy consumption", - disabled: true, + disabled: false, }, ]; @@ -272,7 +272,7 @@ export default function UploadCsvModal({ ); }} /> - {selectedGoal && ( + {selectedGoal === "Increase EPC" && (
- Address Line One + Address Line 1
@@ -286,6 +288,7 @@ export default function RemoteAssesmentModal({ type="button" className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" onClick={() => setIsOpen(false)} + > Cancel From eab898fb83618ea2ad562f9e1499bdece94adfbf Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 Nov 2024 15:56:48 +0000 Subject: [PATCH 05/15] generating presigned url --- .../components/RemoteAssesmentModal.tsx | 129 ++++++++++++++---- 1 file changed, 104 insertions(+), 25 deletions(-) diff --git a/src/app/portfolio/[slug]/components/RemoteAssesmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssesmentModal.tsx index a98dd0b..818c326 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssesmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssesmentModal.tsx @@ -6,6 +6,8 @@ import { Input } from "@/app/shadcn_components/ui/input"; import { Button } from "@/app/shadcn_components/ui/button"; import { Float } from "@headlessui-float/react"; import { ChevronDownIcon } from "@heroicons/react/20/solid"; +import { useMutation } from "@tanstack/react-query"; +import { useSession } from "next-auth/react"; type Option = { label: string; @@ -41,7 +43,7 @@ const selectGoalOptions = [ { label: "Reduce energy consumption", value: "Reduce energy consumption", - disabled: false, + disabled: false, // TODO: Disable }, ]; @@ -63,8 +65,6 @@ const goalValueOptions = [ }, ]; - - export function SelectDropdown({ options, selectedOption, @@ -113,6 +113,68 @@ export function SelectDropdown({ ); } +async function generatePresignedUrl({ + userId, + portfolioId, + fileKey, +}: { + userId: string; + portfolioId: string; + fileKey: string; +}) { + // fileKey is a location in S3 where we want to upload the file + const response = await fetch("/api/upload/csv", { + method: "POST", + body: JSON.stringify({ + userId, + portfolioId, + fileKey, + }), + }); + + if (!response.ok) { + throw new Error("Failed to generate presigned url"); + } + + return response.json(); +} + +function useCreateRemoteAssessment({ portfolioId }: { portfolioId: string }) { + // 1) We want to upload the asset data. To do this, we format the asset data, generate a presigned URL, and upload the data to S3. + // 2) We then want to upload valuation data. To do this, we format the valuation data, generate a presigned URL, and upload the data to S3. + // 3) Trigger the engine!!!! This is an api at /api/plan/trigger with our body that we looked at in Miro + + // Set up the mutation with react-query, to generate a presigned URL + + const session = useSession(); + const userId = String(session.data?.user.dbId); + const fileKey = "8/-1/asset_list.csv"; + + const { + mutate: mutatePresignedUrl, + isLoading: presignedUrlIsLoading, + isError: presignedUrlIsError, + } = useMutation(generatePresignedUrl, { + onSuccess: (data) => { + console.log(data.url); + // On success, upload to that URL!!!! + }, + onError: (error) => { + console.error(error); + }, + }); + + function handleSubmit() { + mutatePresignedUrl({ userId, portfolioId, fileKey }); + console.log("SUCCESS"); // This is where we would want to trigger some kind of use feedback + } + + return { + handleSubmit, + presignedUrlIsLoading, + presignedUrlIsError, + }; +} export default function RemoteAssesmentModal({ portfolioId, @@ -137,7 +199,9 @@ export default function RemoteAssesmentModal({ setScenario(event.target.value); } - function handleAddressLineOneChange(event: React.ChangeEvent) { + function handleAddressLineOneChange( + event: React.ChangeEvent + ) { setAddressLineOne(event.target.value); } @@ -153,6 +217,11 @@ export default function RemoteAssesmentModal({ setValuation(event.target.value); } + const { handleSubmit, presignedUrlIsLoading, presignedUrlIsError } = + useCreateRemoteAssessment({ + portfolioId, + }); + useEffect(() => { function handleButtonDisabled(): boolean { return !( @@ -163,11 +232,19 @@ export default function RemoteAssesmentModal({ postcode && uprn && valuation - ); + ); } setButtonDisabled(handleButtonDisabled()); - }, [scenario, selectedGoal, housingType, addressLineOne, postcode, uprn, valuation]); + }, [ + scenario, + selectedGoal, + housingType, + addressLineOne, + postcode, + uprn, + valuation, + ]); return ( <> @@ -203,12 +280,17 @@ export default function RemoteAssesmentModal({ - {scenario} + className="text-lg font-medium leading-6 text-brandblue mb-3" + > + {scenario}
Scenario Name - +
- +
- Remote Assesment + Remote Assessment
- Schedule a remote assesment + Schedule a remote assessment
diff --git a/src/app/components/portfolio/Toolbar.tsx b/src/app/components/portfolio/Toolbar.tsx index b375443..c12bbbf 100644 --- a/src/app/components/portfolio/Toolbar.tsx +++ b/src/app/components/portfolio/Toolbar.tsx @@ -13,7 +13,7 @@ import { import AddNewDropDown from "./AddNew"; import { cva } from "class-variance-authority"; import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal"; -import RemoteAssesmentModal from "@/app/portfolio/[slug]/components/RemoteAssesmentModal"; +import RemoteAssessmentModal from "@/app/portfolio/[slug]/components/RemoteAssessmentModal"; import { useState } from "react"; import { useRouter } from "next/navigation"; @@ -41,7 +41,7 @@ export function Toolbar({ portfolioId }: ToolbarProps) { } const [modalIsOpen, setModalIsOpen] = useState(false); - const [isRemoteAssesmentOpen, setIsRemoteAssesmentOpen] = useState(false); + const [isRemoteAssessmentOpen, setIsRemoteAssessmentOpen] = useState(false); return ( @@ -73,13 +73,13 @@ export function Toolbar({ portfolioId }: ToolbarProps) { - void; -}; - -const selecthousingTypeOptions = [ - { - label: "Social", - value: "Social", - disabled: false, - }, - { - label: "Private", - value: "Private", - disabled: false, - }, -]; - -const selectGoalOptions = [ - { - label: "Increase EPC", - value: "Increase EPC", - disabled: false, - }, - { - label: "Reduce energy consumption", - value: "Reduce energy consumption", - disabled: false, // TODO: Disable - }, -]; - -const goalValueOptions = [ - { - label: "C", - value: "C", - disabled: false, - }, - { - label: "B", - value: "B", - disabled: false, - }, - { - label: "A", - value: "A", - disabled: false, - }, -]; - -export function SelectDropdown({ - options, - selectedOption, - onSelectOption, -}: DropdownProps) { - return ( - - - - {selectedOption || "Select an option"} - - - - {options.map((option) => ( - - {({ active }) => ( - - )} - - ))} - - - - - ); -} - -async function uploadCsvToS3({ - presignedUrl, - file, -}: { - presignedUrl: string; - file: Blob; -}) { - try { - const response = await fetch(presignedUrl, { - method: "PUT", - body: file, - headers: { "Content-Type": "text/csv" }, - }); - - if (!response.ok) { - console.error(response); - throw new Error("Network response was not ok"); - } - } catch (error) { - console.error(error); - throw new Error("Upload failed."); - } - - return { success: true }; -} - -async function generatePresignedUrl({ - userId, - portfolioId, - fileKey, -}: { - userId: string; - portfolioId: string; - fileKey: string; -}) { - // fileKey is a location in S3 where we want to upload the file - const response = await fetch("/api/upload/csv", { - method: "POST", - body: JSON.stringify({ - userId, - portfolioId, - fileKey, - }), - }); - - if (!response.ok) { - throw new Error("Failed to generate presigned url"); - } - - return response.json(); -} - -function generateS3Keys(userId: string, portfolioId: string) { - const timestamp = new Date().toISOString().replace(/[:.-]/g, ""); - const assetListFileKey = `${userId}/${portfolioId}/${timestamp}/asset_list.csv`; - const valuationDataFileKey = `${userId}/${portfolioId}/${timestamp}/valuation_data.csv`; - return { assetListFileKey, valuationDataFileKey }; -} - -type GenericObject = Record; - -const convertToCSV = (data: T[]): string => { - // Get headers (keys from the first object) - const headers = Object.keys(data[0]) as (keyof T)[]; - - // Create CSV rows - const rows = data.map((row) => - headers.map((header) => row[header]).join(",") - ); - - // Combine headers and rows into CSV string - return [headers.join(","), ...rows].join("\n"); -}; - -function useCreateRemoteAssessment({ - portfolioId, - uprn, - addressLineOne, - postcode, -}: { - portfolioId: string; - uprn: number | null; - addressLineOne: string; - postcode: string; -}) { - // 1) We want to upload the asset data. To do this, we format the asset data, generate a presigned URL, and upload the data to S3. - // 2) We then want to upload valuation data. To do this, we format the valuation data, generate a presigned URL, and upload the data to S3. - // 3) Trigger the engine!!!! This is an api at /api/plan/trigger with our body that we looked at in Miro - - // Set up the mutation with react-query, to generate a presigned URL - - const session = useSession(); - const userId = String(session.data?.user.dbId); - - const { assetListFileKey, valuationDataFileKey } = useMemo( - () => generateS3Keys(userId, portfolioId), - [userId, portfolioId] - ); - - const { - mutate: mutateUploadAssetList, - isLoading: uploadAssetListIsLoading, - isError: uploadAssetListIsError, - } = useMutation(uploadCsvToS3, { - onSuccess: (data) => { - console.log("WAS IT A SUCCESS?", data.success); - console.log("TRIGGERING THE ENGINE"); - // This is where we trigger the engine!!! - const body = { - trigger_file_path: assetListFileKey, - }; - // engine API call goes here - }, - onError: (error) => { - console.error(error); - }, - }); - - const { - mutate: mutatePresignedUrl, - isLoading: presignedUrlIsLoading, - isError: presignedUrlIsError, - } = useMutation(generatePresignedUrl, { - onSuccess: (data) => { - console.log(data.url); - // On success, upload to that URL!!!! - const assetList = [ - { - uprn: uprn, - address: addressLineOne, - postcode: postcode, - }, - ]; - const assetListCsvString = convertToCSV(assetList); - const assetListCsv = new Blob([assetListCsvString], { - type: "text/csv", - }); - - mutateUploadAssetList({ presignedUrl: data.url, file: assetListCsv }); - }, - onError: (error) => { - console.error(error); - }, - }); - - function handleSubmit() { - mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey }); - console.log("SUCCESS"); // This is where we would want to trigger some kind of use feedback - } - - return { - handleSubmit, - presignedUrlIsLoading, - presignedUrlIsError, - }; -} - -export default function RemoteAssesmentModal({ - portfolioId, - isOpen, - setIsOpen, -}: { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; - portfolioId: string; -}) { - const [scenario, setScenario] = useState(undefined); - const [housingType, sethousingType] = useState(""); - const [selectedGoal, setSelectedGoal] = useState(""); - const [goalValue, setGoalValue] = useState(""); - const [addressLineOne, setAddressLineOne] = useState(""); - const [postcode, setPostcode] = useState(""); - const [uprn, setUprn] = useState(null); - const [valuation, setValuation] = useState(""); - const [buttonDisabled, setButtonDisabled] = useState(true); - - function handleScenarioChange(event: React.ChangeEvent) { - setScenario(event.target.value); - } - - function handleAddressLineOneChange( - event: React.ChangeEvent - ) { - setAddressLineOne(event.target.value); - } - - function handlePostcodeChange(event: React.ChangeEvent) { - setPostcode(event.target.value); - } - - function handleUprnChange(event: React.ChangeEvent) { - setUprn(Number(event.target.value)); - } - - function handleValuationChange(event: React.ChangeEvent) { - setValuation(event.target.value); - } - - const { handleSubmit, presignedUrlIsLoading, presignedUrlIsError } = - useCreateRemoteAssessment({ - portfolioId, - uprn, - addressLineOne, - postcode, - }); - - useEffect(() => { - function handleButtonDisabled(): boolean { - return !( - scenario && - selectedGoal && - housingType && - addressLineOne && - postcode && - uprn && - valuation - ); - } - - setButtonDisabled(handleButtonDisabled()); - }, [ - scenario, - selectedGoal, - housingType, - addressLineOne, - postcode, - uprn, - valuation, - ]); - - return ( - <> - - setIsOpen(false)} - > - -
- - -
-
- - - - {scenario} - -
- Scenario Name - -
-
- - sethousingType(option.value)} - /> -
-
- - setSelectedGoal(option.value)} - /> - {selectedGoal === "Increase EPC" && ( -
- - { - setGoalValue(option.value); - }} - /> -
- )} -
-
- Address Line 1 - -
-
- Postcode - -
-
- UPRN - -
-
- Valuation - -
-
- -
- -
-
-
-
-
-
- - ); -} From 20b4d93b7796f36d3166f8e116e57f326f1867dd Mon Sep 17 00:00:00 2001 From: StefanWout Date: Wed, 20 Nov 2024 17:17:27 +0000 Subject: [PATCH 09/15] disabled the reduce energy consumption option --- .../components/RemoteAssessmentModal.tsx | 480 ++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx new file mode 100644 index 0000000..38ea37a --- /dev/null +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -0,0 +1,480 @@ +"use client"; + +import { Dialog, Transition, Menu } from "@headlessui/react"; +import { useState, Fragment, useEffect, useMemo } from "react"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { Float } from "@headlessui-float/react"; +import { ChevronDownIcon } from "@heroicons/react/20/solid"; +import { useMutation } from "@tanstack/react-query"; +import { useSession } from "next-auth/react"; + +type Option = { + label: string; + value: string; + disabled: boolean; +}; + +type DropdownProps = { + options: Option[]; + selectedOption: string; + onSelectOption: (option: Option) => void; +}; + +const selecthousingTypeOptions = [ + { + label: "Social", + value: "Social", + disabled: false, + }, + { + label: "Private", + value: "Private", + disabled: false, + }, +]; + +const selectGoalOptions = [ + { + label: "Increase EPC", + value: "Increase EPC", + disabled: false, + }, + { + label: "Reduce energy consumption", + value: "Reduce energy consumption", + disabled: true, + }, +]; + +const goalValueOptions = [ + { + label: "C", + value: "C", + disabled: false, + }, + { + label: "B", + value: "B", + disabled: false, + }, + { + label: "A", + value: "A", + disabled: false, + }, +]; + +export function SelectDropdown({ + options, + selectedOption, + onSelectOption, +}: DropdownProps) { + return ( + + + + {selectedOption || "Select an option"} + + + + {options.map((option) => ( + + {({ active }) => ( + + )} + + ))} + + + + + ); +} + +async function uploadCsvToS3({ + presignedUrl, + file, +}: { + presignedUrl: string; + file: Blob; +}) { + try { + const response = await fetch(presignedUrl, { + method: "PUT", + body: file, + headers: { "Content-Type": "text/csv" }, + }); + + if (!response.ok) { + console.error(response); + throw new Error("Network response was not ok"); + } + } catch (error) { + console.error(error); + throw new Error("Upload failed."); + } + + return { success: true }; +} + +async function generatePresignedUrl({ + userId, + portfolioId, + fileKey, +}: { + userId: string; + portfolioId: string; + fileKey: string; +}) { + // fileKey is a location in S3 where we want to upload the file + const response = await fetch("/api/upload/csv", { + method: "POST", + body: JSON.stringify({ + userId, + portfolioId, + fileKey, + }), + }); + + if (!response.ok) { + throw new Error("Failed to generate presigned url"); + } + + return response.json(); +} + +function generateS3Keys(userId: string, portfolioId: string) { + const timestamp = new Date().toISOString().replace(/[:.-]/g, ""); + const assetListFileKey = `${userId}/${portfolioId}/${timestamp}/asset_list.csv`; + const valuationDataFileKey = `${userId}/${portfolioId}/${timestamp}/valuation_data.csv`; + return { assetListFileKey, valuationDataFileKey }; +} + +type GenericObject = Record; + +const convertToCSV = (data: T[]): string => { + // Get headers (keys from the first object) + const headers = Object.keys(data[0]) as (keyof T)[]; + + // Create CSV rows + const rows = data.map((row) => + headers.map((header) => row[header]).join(",") + ); + + // Combine headers and rows into CSV string + return [headers.join(","), ...rows].join("\n"); +}; + +function useCreateRemoteAssessment({ + portfolioId, + uprn, + addressLineOne, + postcode, +}: { + portfolioId: string; + uprn: number | null; + addressLineOne: string; + postcode: string; +}) { + // 1) We want to upload the asset data. To do this, we format the asset data, generate a presigned URL, and upload the data to S3. + // 2) We then want to upload valuation data. To do this, we format the valuation data, generate a presigned URL, and upload the data to S3. + // 3) Trigger the engine!!!! This is an api at /api/plan/trigger with our body that we looked at in Miro + + // Set up the mutation with react-query, to generate a presigned URL + + const session = useSession(); + const userId = String(session.data?.user.dbId); + + const { assetListFileKey, valuationDataFileKey } = useMemo( + () => generateS3Keys(userId, portfolioId), + [userId, portfolioId] + ); + + const { + mutate: mutateUploadAssetList, + isLoading: uploadAssetListIsLoading, + isError: uploadAssetListIsError, + } = useMutation(uploadCsvToS3, { + onSuccess: (data) => { + console.log("WAS IT A SUCCESS?", data.success); + console.log("TRIGGERING THE ENGINE"); + // This is where we trigger the engine!!! + const body = { + trigger_file_path: assetListFileKey, + }; + // engine API call goes here + }, + onError: (error) => { + console.error(error); + }, + }); + + const { + mutate: mutatePresignedUrl, + isLoading: presignedUrlIsLoading, + isError: presignedUrlIsError, + } = useMutation(generatePresignedUrl, { + onSuccess: (data) => { + console.log(data.url); + // On success, upload to that URL!!!! + const assetList = [ + { + uprn: uprn, + address: addressLineOne, + postcode: postcode, + }, + ]; + const assetListCsvString = convertToCSV(assetList); + const assetListCsv = new Blob([assetListCsvString], { + type: "text/csv", + }); + + mutateUploadAssetList({ presignedUrl: data.url, file: assetListCsv }); + }, + onError: (error) => { + console.error(error); + }, + }); + + function handleSubmit() { + mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey }); + console.log("SUCCESS"); // This is where we would want to trigger some kind of use feedback + } + + return { + handleSubmit, + presignedUrlIsLoading, + presignedUrlIsError, + }; +} + +export default function RemoteAssessmentModal({ + portfolioId, + isOpen, + setIsOpen, +}: { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + portfolioId: string; +}) { + const [scenario, setScenario] = useState(undefined); + const [housingType, sethousingType] = useState(""); + const [selectedGoal, setSelectedGoal] = useState(""); + const [goalValue, setGoalValue] = useState(""); + const [addressLineOne, setAddressLineOne] = useState(""); + const [postcode, setPostcode] = useState(""); + const [uprn, setUprn] = useState(null); + const [valuation, setValuation] = useState(""); + const [buttonDisabled, setButtonDisabled] = useState(true); + + function handleScenarioChange(event: React.ChangeEvent) { + setScenario(event.target.value); + } + + function handleAddressLineOneChange( + event: React.ChangeEvent + ) { + setAddressLineOne(event.target.value); + } + + function handlePostcodeChange(event: React.ChangeEvent) { + setPostcode(event.target.value); + } + + function handleUprnChange(event: React.ChangeEvent) { + setUprn(Number(event.target.value)); + } + + function handleValuationChange(event: React.ChangeEvent) { + setValuation(event.target.value); + } + + const { handleSubmit, presignedUrlIsLoading, presignedUrlIsError } = + useCreateRemoteAssessment({ + portfolioId, + uprn, + addressLineOne, + postcode, + }); + + useEffect(() => { + function handleButtonDisabled(): boolean { + return !( + scenario && + selectedGoal && + housingType && + addressLineOne && + postcode && + uprn && + valuation + ); + } + + setButtonDisabled(handleButtonDisabled()); + }, [ + scenario, + selectedGoal, + housingType, + addressLineOne, + postcode, + uprn, + valuation, + ]); + + return ( + <> + + setIsOpen(false)} + > + +
+ + +
+
+ + + + {scenario} + +
+ Scenario Name + +
+
+ + sethousingType(option.value)} + /> +
+
+ + setSelectedGoal(option.value)} + /> + {selectedGoal === "Increase EPC" && ( +
+ + { + setGoalValue(option.value); + }} + /> +
+ )} +
+
+ Address Line 1 + +
+
+ Postcode + +
+
+ UPRN + +
+
+ Valuation + +
+
+ +
+ +
+
+
+
+
+
+ + ); +} From 3520d0858dba7ff28841d3e8745d878f48b230c4 Mon Sep 17 00:00:00 2001 From: StefanWout Date: Thu, 21 Nov 2024 14:35:54 +0000 Subject: [PATCH 10/15] succesfully implemented the valuation data upload functionality on handleSubmit now to turn this into a form --- .../components/RemoteAssessmentModal.tsx | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 38ea37a..15d7810 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -136,6 +136,7 @@ async function uploadCsvToS3({ throw new Error("Upload failed."); } + console.log("S3 got the stuff"); return { success: true }; } @@ -192,11 +193,13 @@ function useCreateRemoteAssessment({ uprn, addressLineOne, postcode, + valuation, }: { portfolioId: string; uprn: number | null; addressLineOne: string; postcode: string; + valuation: string | number | null; }) { // 1) We want to upload the asset data. To do this, we format the asset data, generate a presigned URL, and upload the data to S3. // 2) We then want to upload valuation data. To do this, we format the valuation data, generate a presigned URL, and upload the data to S3. @@ -218,8 +221,8 @@ function useCreateRemoteAssessment({ isError: uploadAssetListIsError, } = useMutation(uploadCsvToS3, { onSuccess: (data) => { - console.log("WAS IT A SUCCESS?", data.success); - console.log("TRIGGERING THE ENGINE"); + console.log("WAS THE ASSET LIST A SUCCESS?", data.success); + console.log("ASSETS TRIGGERING THE ENGINE"); // This is where we trigger the engine!!! const body = { trigger_file_path: assetListFileKey, @@ -231,6 +234,25 @@ function useCreateRemoteAssessment({ }, }); + const { + mutate: mutateUploadValuationData, + isLoading: uploadValuationDataIsLoading, + isError: uploadValuationDataIsError, + } = useMutation(uploadCsvToS3, { + onSuccess: (data) => { + console.log("WAS VALUATION DATA A SUCCESS?", data.success); + console.log("VALUATION TRIGGERING THE ENGINE"); + // This is where we trigger the engine!!! + const body = { + trigger_file_path: valuationDataFileKey, + }; + // engine API call goes here + }, + onError: (error) => { + console.error(error); + }, + }); + const { mutate: mutatePresignedUrl, isLoading: presignedUrlIsLoading, @@ -246,21 +268,45 @@ function useCreateRemoteAssessment({ postcode: postcode, }, ]; + const valuationData = [ + { + uprn: uprn, + valuation: 100000, + }, + ]; const assetListCsvString = convertToCSV(assetList); const assetListCsv = new Blob([assetListCsvString], { type: "text/csv", }); - mutateUploadAssetList({ presignedUrl: data.url, file: assetListCsv }); + + const valuationDataCsvString = convertToCSV(valuationData); + const valuationDataCsv = new Blob([valuationDataCsvString], { + type: "text/csv", + }); + mutateUploadValuationData({ presignedUrl: data.url, file: valuationDataCsv }); }, onError: (error) => { console.error(error); }, }); - function handleSubmit() { - mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey }); - console.log("SUCCESS"); // This is where we would want to trigger some kind of use feedback + async function handleSubmit() { + try { + // Mutate presigned URL for asset list file + await mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey }); + console.log("ASSET LIST SUCCESS"); + + // Mutate presigned URL for valuation data file + await mutatePresignedUrl({ userId, portfolioId, fileKey: valuationDataFileKey }); + console.log("VALUATION DATA SUCCESS"); + + // Trigger user feedback (e.g., show a success message) + console.log("Both uploads SUCCESS"); + } catch (error) { + // Handle error (e.g., show an error message) + console.error("Error uploading files:", error); + } } return { @@ -317,6 +363,7 @@ export default function RemoteAssessmentModal({ uprn, addressLineOne, postcode, + valuation, }); useEffect(() => { From 710d60b5edbf5ac967d9e2579ae97c070f106d10 Mon Sep 17 00:00:00 2001 From: StefanWout Date: Thu, 21 Nov 2024 16:44:34 +0000 Subject: [PATCH 11/15] got the react hook form with zod to work, FormProvder got rid of the getField errors somehow, all hail Overlord Claude, aesthetic clean up needed as well as linked up to trigger the engine --- .../components/RemoteAssessmentModal.tsx | 355 ++++++++++-------- 1 file changed, 208 insertions(+), 147 deletions(-) diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 15d7810..fe736b7 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -8,6 +8,10 @@ import { Float } from "@headlessui-float/react"; import { ChevronDownIcon } from "@heroicons/react/20/solid"; import { useMutation } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; +import { Form, useForm, FormProvider } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/app/shadcn_components/ui/form"; type Option = { label: string; @@ -291,6 +295,8 @@ function useCreateRemoteAssessment({ }, }); + + async function handleSubmit() { try { // Mutate presigned URL for asset list file @@ -325,71 +331,58 @@ export default function RemoteAssessmentModal({ setIsOpen: (isOpen: boolean) => void; portfolioId: string; }) { - const [scenario, setScenario] = useState(undefined); - const [housingType, sethousingType] = useState(""); - const [selectedGoal, setSelectedGoal] = useState(""); - const [goalValue, setGoalValue] = useState(""); - const [addressLineOne, setAddressLineOne] = useState(""); - const [postcode, setPostcode] = useState(""); - const [uprn, setUprn] = useState(null); - const [valuation, setValuation] = useState(""); - const [buttonDisabled, setButtonDisabled] = useState(true); + + const formSchema = z.object({ + scenario: z.string().min(1, "Scenario is required"), + goal: z.string().min(1, "Goal is required"), + goalValue: z.string().min(1, "Goal value is required"), + housingType: z.string().min(1, "Housing type is required"), + addressLineOne: z.string().min(1, "Address is required"), + postcode: z.string().min(1, "Postcode is required"), + uprn: z.number().min(1, "UPRN is required"), + valuation: z.number().min(1, "Valuation is required"), + }); - function handleScenarioChange(event: React.ChangeEvent) { - setScenario(event.target.value); - } + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + scenario: "", + housingType: "", + goal: "", + goalValue: "", + addressLineOne: "", + postcode: "", + uprn: 0, + valuation: 0, + }, + }); - function handleAddressLineOneChange( - event: React.ChangeEvent - ) { - setAddressLineOne(event.target.value); - } + type FormValues = z.infer; - function handlePostcodeChange(event: React.ChangeEvent) { - setPostcode(event.target.value); - } - - function handleUprnChange(event: React.ChangeEvent) { - setUprn(Number(event.target.value)); - } - - function handleValuationChange(event: React.ChangeEvent) { - setValuation(event.target.value); - } + const onSubmit = async (data: FormValues) => { + try { + // First handle the form submission from react-hook-form + const formData = form.getValues(); + + // Then trigger the data upload using handleSubmit from useCreateRemoteAssessment + await handleSubmit(); + + // Close the modal on success + setIsOpen(false); + } catch (error) { + console.error('Error submitting form:', error); + } + }; const { handleSubmit, presignedUrlIsLoading, presignedUrlIsError } = useCreateRemoteAssessment({ portfolioId, - uprn, - addressLineOne, - postcode, - valuation, + uprn: form.watch("uprn"), + addressLineOne: form.watch("addressLineOne"), + postcode: form.watch("postcode"), + valuation: form.watch("valuation"), }); - useEffect(() => { - function handleButtonDisabled(): boolean { - return !( - scenario && - selectedGoal && - housingType && - addressLineOne && - postcode && - uprn && - valuation - ); - } - - setButtonDisabled(handleButtonDisabled()); - }, [ - scenario, - selectedGoal, - housingType, - addressLineOne, - postcode, - uprn, - valuation, - ]); - return ( <> @@ -422,101 +415,169 @@ export default function RemoteAssessmentModal({ leaveTo="opacity-0 scale-95" > - - {scenario} - -
- Scenario Name - + Remote Assessment Details + + +
+ ( + + Scenario Name + + + + + + )} /> -
-
- - sethousingType(option.value)} + + ( + + Housing Type + + field.onChange(option.value)} + /> + + + + )} /> -
-
- - setSelectedGoal(option.value)} + + ( + + Goal + + field.onChange(option.value)} + /> + + + + )} /> - {selectedGoal === "Increase EPC" && ( -
- - { - setGoalValue(option.value); - }} - /> -
+ + ( + + Goal Value + + field.onChange(option.value)} + /> + + + + )} + /> + + ( + + Address + + + + + + )} + /> + + ( + + Postcode + + + + + + )} + /> + + ( + + UPRN + + field.onChange(Number(e.target.value))} + /> + + + + )} + /> + + ( + + Valuation + + field.onChange(Number(e.target.value))} + /> + + + + )} + /> + +
+ + +
+ {presignedUrlIsError && ( +

Error uploading files

)} -
-
- Address Line 1 - -
-
- Postcode - -
-
- UPRN - -
-
- Valuation - -
-
- -
- -
+ + +
From cad43681ad3964a0a09ced2c6acd8a3aa6e90f9c Mon Sep 17 00:00:00 2001 From: StefanWout Date: Thu, 21 Nov 2024 16:53:44 +0000 Subject: [PATCH 12/15] noticed that the form wasn't clearing, added that functionality to the onSubmit function, which also triggers handle submit, could add the handle submit functionality to the onSubmit function but thats a tomorrow problem --- src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index fe736b7..383a6df 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -366,6 +366,9 @@ export default function RemoteAssessmentModal({ // Then trigger the data upload using handleSubmit from useCreateRemoteAssessment await handleSubmit(); + + // Reset the form + form.reset(); // Close the modal on success setIsOpen(false); From 78d3716899e1c2e6fe337fe0ef0704aa5997d43d Mon Sep 17 00:00:00 2001 From: StefanWout Date: Tue, 26 Nov 2024 14:27:54 +0000 Subject: [PATCH 13/15] all kinds of weird attempts --- package-lock.json | 299 ++++++++++++++++++ package.json | 1 + .../components/RemoteAssessmentModal.tsx | 173 ++++++---- 3 files changed, 403 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac28db5..687b492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", "@remixicon/react": "^4.2.0", "@tanstack/react-query": "^4.29.12", @@ -2303,6 +2304,304 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz", + "integrity": "sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", + "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", diff --git a/package.json b/package.json index 382e746..12d2af0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", "@remixicon/react": "^4.2.0", "@tanstack/react-query": "^4.29.12", diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 383a6df..0bf4b89 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -12,6 +12,8 @@ import { Form, useForm, FormProvider } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/app/shadcn_components/ui/form"; +import { Toast } from "@/app/shadcn_components/ui/toast"; +// import { Form } from "aws-sdk/clients/amplifyuibuilder"; type Option = { label: string; @@ -69,6 +71,35 @@ const goalValueOptions = [ }, ]; +interface EngineTriggerBody { + portfolio_id: string; + housing_type: string; + goal: string; + goal_value: string; + trigger_file_path: string; + already_installed_file_path: string; + patches_file_path: string; + non_invasive_recommendations_file_path: string; + valuation_file_path: string; + scenario_name: string; + multi_plan: boolean; + budget: null; + event_type: string; +} + +const formSchema = z.object({ + scenario: z.string().min(1, "Scenario is required"), + goal: z.string().min(1, "Goal is required"), + goalValue: z.string().min(1, "Goal value is required"), + housingType: z.string().min(1, "Housing type is required"), + addressLineOne: z.string().min(1, "Address is required"), + postcode: z.string().min(1, "Postcode is required"), + uprn: z.number().min(1, "UPRN is required"), + valuation: z.number().min(1, "Valuation is required"), +}); + +type FormValues = z.infer; + export function SelectDropdown({ options, selectedOption, @@ -220,42 +251,26 @@ function useCreateRemoteAssessment({ ); const { - mutate: mutateUploadAssetList, - isLoading: uploadAssetListIsLoading, - isError: uploadAssetListIsError, - } = useMutation(uploadCsvToS3, { - onSuccess: (data) => { - console.log("WAS THE ASSET LIST A SUCCESS?", data.success); - console.log("ASSETS TRIGGERING THE ENGINE"); - // This is where we trigger the engine!!! - const body = { - trigger_file_path: assetListFileKey, - }; - // engine API call goes here + mutate: mutateUploadFiles, + isLoading: uploadFilesIsLoading, + isError: uploadFilesIsError, + } = useMutation(async ({ assetList, valuationData }: { assetList: { presignedUrl: string; file: Blob }; valuationData: { presignedUrl: string; file: Blob } }) => { + + // Upload asset list + await uploadCsvToS3({ presignedUrl: assetList.presignedUrl, file: assetList.file }); + + // Upload valuation data + await uploadCsvToS3({ presignedUrl: valuationData.presignedUrl, file: valuationData.file }); + }, { + onSuccess: (data) => { // Callback for successful mutation + console.log("Files uploaded successfully"); + // Trigger the engine here if needed }, - onError: (error) => { - console.error(error); + onError: (error) => { // Callback for failed mutation + console.error("Error uploading files:", error); }, }); - const { - mutate: mutateUploadValuationData, - isLoading: uploadValuationDataIsLoading, - isError: uploadValuationDataIsError, - } = useMutation(uploadCsvToS3, { - onSuccess: (data) => { - console.log("WAS VALUATION DATA A SUCCESS?", data.success); - console.log("VALUATION TRIGGERING THE ENGINE"); - // This is where we trigger the engine!!! - const body = { - trigger_file_path: valuationDataFileKey, - }; - // engine API call goes here - }, - onError: (error) => { - console.error(error); - }, - }); const { mutate: mutatePresignedUrl, @@ -275,20 +290,21 @@ function useCreateRemoteAssessment({ const valuationData = [ { uprn: uprn, - valuation: 100000, + valuation: 0, }, ]; const assetListCsvString = convertToCSV(assetList); const assetListCsv = new Blob([assetListCsvString], { type: "text/csv", }); - mutateUploadAssetList({ presignedUrl: data.url, file: assetListCsv }); - + const valuationDataCsvString = convertToCSV(valuationData); const valuationDataCsv = new Blob([valuationDataCsvString], { type: "text/csv", }); - mutateUploadValuationData({ presignedUrl: data.url, file: valuationDataCsv }); + + mutateUploadFiles({ assetList: { presignedUrl: data.url, file: assetListCsv }, valuationData: { presignedUrl: data.url, file: valuationDataCsv } }); + }, onError: (error) => { console.error(error); @@ -296,32 +312,66 @@ function useCreateRemoteAssessment({ }); - - async function handleSubmit() { + async function triggerEngine(data: FormValues) { try { - // Mutate presigned URL for asset list file - await mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey }); - console.log("ASSET LIST SUCCESS"); + const triggerBody: EngineTriggerBody ={ + portfolio_id: portfolioId, + housing_type: data.housingType, + goal: data.goal, + goal_value: data.goalValue, + trigger_file_path: assetListFileKey, + already_installed_file_path: "", + patches_file_path: "", + non_invasive_recommendations_file_path: "", + valuation_file_path: valuationDataFileKey, + scenario_name: data.scenario, + multi_plan: true, + budget: null, + event_type: "Remote Assessment" + }; - // Mutate presigned URL for valuation data file - await mutatePresignedUrl({ userId, portfolioId, fileKey: valuationDataFileKey }); - console.log("VALUATION DATA SUCCESS"); + const response = await fetch("/api/plan/trigger", { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(triggerBody), + }); - // Trigger user feedback (e.g., show a success message) - console.log("Both uploads SUCCESS"); + if (!response.ok) { + throw new Error('Failed to trigger engine'); + } } catch (error) { - // Handle error (e.g., show an error message) - console.error("Error uploading files:", error); + console.error('Error triggering engine:', error); + throw error; + } + } + + async function handleSubmit(formData: FormValues) { + try { + const [assetListUrl, valuationDataUrl] = await Promise.all([ + mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey }), + mutatePresignedUrl({ userId, portfolioId, fileKey: valuationDataFileKey }) + ]); + + await triggerEngine(formData); + + } catch (error) { + console.error("Error in submission process:", error); } } return { handleSubmit, + triggerEngine, presignedUrlIsLoading, presignedUrlIsError, + uploadFilesIsLoading, + uploadFilesIsError, }; } + export default function RemoteAssessmentModal({ portfolioId, isOpen, @@ -331,17 +381,6 @@ export default function RemoteAssessmentModal({ setIsOpen: (isOpen: boolean) => void; portfolioId: string; }) { - - const formSchema = z.object({ - scenario: z.string().min(1, "Scenario is required"), - goal: z.string().min(1, "Goal is required"), - goalValue: z.string().min(1, "Goal value is required"), - housingType: z.string().min(1, "Housing type is required"), - addressLineOne: z.string().min(1, "Address is required"), - postcode: z.string().min(1, "Postcode is required"), - uprn: z.number().min(1, "UPRN is required"), - valuation: z.number().min(1, "Valuation is required"), - }); const form = useForm({ resolver: zodResolver(formSchema), @@ -357,20 +396,10 @@ export default function RemoteAssessmentModal({ }, }); - type FormValues = z.infer; - const onSubmit = async (data: FormValues) => { try { - // First handle the form submission from react-hook-form - const formData = form.getValues(); - - // Then trigger the data upload using handleSubmit from useCreateRemoteAssessment - await handleSubmit(); - - // Reset the form + await handleSubmit(data); form.reset(); - - // Close the modal on success setIsOpen(false); } catch (error) { console.error('Error submitting form:', error); @@ -589,3 +618,7 @@ export default function RemoteAssessmentModal({ ); } +function setIsOpen(arg0: boolean) { + throw new Error("Function not implemented."); +} + From 369c4ca93c355bebd87d1f794efe59ed1bffc61e Mon Sep 17 00:00:00 2001 From: StefanWout Date: Tue, 26 Nov 2024 18:00:53 +0000 Subject: [PATCH 14/15] fixed the overlap uploads by addinga an if statement to check which data file to manage --- src/app/hooks/use-toast.ts | 194 +++++++ .../components/RemoteAssessmentModal.tsx | 500 ++++++++++-------- src/app/shadcn_components/ui/toast.tsx | 129 +++++ src/app/shadcn_components/ui/toaster.tsx | 35 ++ 4 files changed, 626 insertions(+), 232 deletions(-) create mode 100644 src/app/hooks/use-toast.ts create mode 100644 src/app/shadcn_components/ui/toast.tsx create mode 100644 src/app/shadcn_components/ui/toaster.tsx diff --git a/src/app/hooks/use-toast.ts b/src/app/hooks/use-toast.ts new file mode 100644 index 0000000..b18ceb8 --- /dev/null +++ b/src/app/hooks/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "src/app/shadcn_components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index 0bf4b89..a0001b1 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -1,7 +1,7 @@ "use client"; import { Dialog, Transition, Menu } from "@headlessui/react"; -import { useState, Fragment, useEffect, useMemo } from "react"; +import { useState, Fragment, useMemo, useRef } from "react"; import { Input } from "@/app/shadcn_components/ui/input"; import { Button } from "@/app/shadcn_components/ui/button"; import { Float } from "@headlessui-float/react"; @@ -11,9 +11,15 @@ import { useSession } from "next-auth/react"; import { Form, useForm, FormProvider } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; -import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/app/shadcn_components/ui/form"; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from "@/app/shadcn_components/ui/form"; import { Toast } from "@/app/shadcn_components/ui/toast"; -// import { Form } from "aws-sdk/clients/amplifyuibuilder"; +import { FileKey } from "lucide-react"; type Option = { label: string; @@ -198,7 +204,11 @@ async function generatePresignedUrl({ throw new Error("Failed to generate presigned url"); } - return response.json(); + const data = await response.json(); + + data.fileKey = fileKey; + + return data; } function generateS3Keys(userId: string, portfolioId: string) { @@ -251,26 +261,22 @@ function useCreateRemoteAssessment({ ); const { - mutate: mutateUploadFiles, - isLoading: uploadFilesIsLoading, - isError: uploadFilesIsError, - } = useMutation(async ({ assetList, valuationData }: { assetList: { presignedUrl: string; file: Blob }; valuationData: { presignedUrl: string; file: Blob } }) => { - - // Upload asset list - await uploadCsvToS3({ presignedUrl: assetList.presignedUrl, file: assetList.file }); - - // Upload valuation data - await uploadCsvToS3({ presignedUrl: valuationData.presignedUrl, file: valuationData.file }); - }, { - onSuccess: (data) => { // Callback for successful mutation - console.log("Files uploaded successfully"); - // Trigger the engine here if needed - }, - onError: (error) => { // Callback for failed mutation - console.error("Error uploading files:", error); - }, - }); - + mutate: mutateUploadFile, + isLoading: uploadFileIsLoading, + isError: uploadFileIsError, + } = useMutation(uploadCsvToS3, + { + onSuccess: (data) => { + // Callback for successful mutation + console.log("Files uploaded successfully"); + // Trigger the engine here if needed + }, + onError: (error) => { + // Callback for failed mutation + console.error("Error uploading files:", error); + }, + } + ); const { mutate: mutatePresignedUrl, @@ -278,71 +284,81 @@ function useCreateRemoteAssessment({ isError: presignedUrlIsError, } = useMutation(generatePresignedUrl, { onSuccess: (data) => { - console.log(data.url); - // On success, upload to that URL!!!! - const assetList = [ - { - uprn: uprn, - address: addressLineOne, - postcode: postcode, - }, - ]; - const valuationData = [ - { - uprn: uprn, - valuation: 0, - }, - ]; - const assetListCsvString = convertToCSV(assetList); - const assetListCsv = new Blob([assetListCsvString], { - type: "text/csv", - }); + // console.log(data.url); + // // On success, upload to that URL!!!! - const valuationDataCsvString = convertToCSV(valuationData); - const valuationDataCsv = new Blob([valuationDataCsvString], { - type: "text/csv", - }); + let csvFile: Blob = new Blob(); - mutateUploadFiles({ assetList: { presignedUrl: data.url, file: assetListCsv }, valuationData: { presignedUrl: data.url, file: valuationDataCsv } }); - + if (data.fileKey === assetListFileKey) { + + const assetList = [ + { + uprn: uprn, + address: addressLineOne, + postcode: postcode, + }, + ]; + + csvFile = new Blob([convertToCSV(assetList)], { + type: "text/csv", + }); + + } else if (data.fileKey === valuationDataFileKey) { + + const valuationData = [ + { + uprn: uprn, + valuation: valuation, + }, + ]; + + csvFile = new Blob([convertToCSV(valuationData)], { + type: "text/csv", + }); + } + + mutateUploadFile({ + file: csvFile, + presignedUrl: data.url, + }); }, onError: (error) => { console.error(error); }, - }); - + } + ); async function triggerEngine(data: FormValues) { try { - const triggerBody: EngineTriggerBody ={ - portfolio_id: portfolioId, - housing_type: data.housingType, - goal: data.goal, - goal_value: data.goalValue, - trigger_file_path: assetListFileKey, - already_installed_file_path: "", - patches_file_path: "", - non_invasive_recommendations_file_path: "", - valuation_file_path: valuationDataFileKey, - scenario_name: data.scenario, - multi_plan: true, - budget: null, - event_type: "Remote Assessment" + const triggerBody: EngineTriggerBody = { + portfolio_id: portfolioId, + housing_type: data.housingType, + goal: data.goal, + goal_value: data.goalValue, + trigger_file_path: assetListFileKey, + already_installed_file_path: "", + patches_file_path: "", + non_invasive_recommendations_file_path: "", + valuation_file_path: valuationDataFileKey, + scenario_name: data.scenario, + multi_plan: true, + budget: null, + event_type: "Remote Assessment", }; const response = await fetch("/api/plan/trigger", { method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify(triggerBody), }); if (!response.ok) { - throw new Error('Failed to trigger engine'); + throw new Error("Failed to trigger engine"); } } catch (error) { - console.error('Error triggering engine:', error); + console.error("Error triggering engine:", error); throw error; } } @@ -350,12 +366,18 @@ function useCreateRemoteAssessment({ async function handleSubmit(formData: FormValues) { try { const [assetListUrl, valuationDataUrl] = await Promise.all([ - mutatePresignedUrl({ userId, portfolioId, fileKey: assetListFileKey }), - mutatePresignedUrl({ userId, portfolioId, fileKey: valuationDataFileKey }) + mutatePresignedUrl({ + userId, + portfolioId, + fileKey: assetListFileKey }), + mutatePresignedUrl({ + userId, + portfolioId, + fileKey: valuationDataFileKey, + }), ]); await triggerEngine(formData); - } catch (error) { console.error("Error in submission process:", error); } @@ -364,14 +386,14 @@ function useCreateRemoteAssessment({ return { handleSubmit, triggerEngine, + mutateUploadFile, presignedUrlIsLoading, presignedUrlIsError, - uploadFilesIsLoading, - uploadFilesIsError, + uploadFileIsLoading, + uploadFileIsError, }; } - export default function RemoteAssessmentModal({ portfolioId, isOpen, @@ -381,7 +403,6 @@ export default function RemoteAssessmentModal({ setIsOpen: (isOpen: boolean) => void; portfolioId: string; }) { - const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -396,13 +417,14 @@ export default function RemoteAssessmentModal({ }, }); + const onSubmit = async (data: FormValues) => { try { await handleSubmit(data); form.reset(); setIsOpen(false); } catch (error) { - console.error('Error submitting form:', error); + console.error("Error submitting form:", error); } }; @@ -447,169 +469,184 @@ export default function RemoteAssessmentModal({ leaveTo="opacity-0 scale-95" > - - Remote Assessment Details - - -
- ( - - Scenario Name - - - - - - )} - /> + + Remote Assessment Details + + + + ( + + Scenario Name + + + + + + )} + /> - ( - - Housing Type - - field.onChange(option.value)} - /> - - - - )} - /> + ( + + Housing Type + + + field.onChange(option.value) + } + /> + + + + )} + /> - ( - - Goal - - field.onChange(option.value)} - /> - - - - )} - /> + ( + + Goal + + + field.onChange(option.value) + } + /> + + + + )} + /> - ( - - Goal Value - - field.onChange(option.value)} - /> - - - - )} - /> + ( + + Goal Value + + + field.onChange(option.value) + } + /> + + + + )} + /> - ( - - Address - - - - - - )} - /> + ( + + Address + + + + + + )} + /> - ( - - Postcode - - - - - - )} - /> + ( + + Postcode + + + + + + )} + /> - ( - - UPRN - - field.onChange(Number(e.target.value))} - /> - - - - )} - /> + ( + + UPRN + + + field.onChange(Number(e.target.value)) + } + /> + + + + )} + /> - ( - - Valuation - - field.onChange(Number(e.target.value))} - /> - - - - )} - /> + ( + + Valuation + + + field.onChange(Number(e.target.value)) + } + /> + + + + )} + /> -
- - -
- {presignedUrlIsError && ( -

Error uploading files

- )} - -
-
+
+ + +
+ {presignedUrlIsError && ( +

+ Error uploading files +

+ )} + + +
@@ -621,4 +658,3 @@ export default function RemoteAssessmentModal({ function setIsOpen(arg0: boolean) { throw new Error("Function not implemented."); } - diff --git a/src/app/shadcn_components/ui/toast.tsx b/src/app/shadcn_components/ui/toast.tsx new file mode 100644 index 0000000..84a7326 --- /dev/null +++ b/src/app/shadcn_components/ui/toast.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "s/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/src/app/shadcn_components/ui/toaster.tsx b/src/app/shadcn_components/ui/toaster.tsx new file mode 100644 index 0000000..04165e3 --- /dev/null +++ b/src/app/shadcn_components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useToast } from "src/app/shadcn_components/hooks/use-toast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "src/app/shadcn_components/ui/toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} From 64ac833d8cc840ea44779b6f93db81abf9c517ec Mon Sep 17 00:00:00 2001 From: StefanWout Date: Tue, 26 Nov 2024 18:15:06 +0000 Subject: [PATCH 15/15] version 1 of the remote assesment modal complete, budget and user feedback to be added --- src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx index a0001b1..85b191b 100644 --- a/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx +++ b/src/app/portfolio/[slug]/components/RemoteAssessmentModal.tsx @@ -365,7 +365,7 @@ function useCreateRemoteAssessment({ async function handleSubmit(formData: FormValues) { try { - const [assetListUrl, valuationDataUrl] = await Promise.all([ + await Promise.all([ mutatePresignedUrl({ userId, portfolioId,