restructuring the plan submit components

This commit is contained in:
Khalim Conn-Kowlessar 2023-07-24 09:27:12 +01:00
parent 0e2ffe97c3
commit cb7ba9e0b3
5 changed files with 247 additions and 219 deletions

View file

@ -8,7 +8,7 @@ import {
} from "@/app/shadcn_components/ui/navigation-menu";
import AddNewDropDown from "./AddNew";
import { cva } from "class-variance-authority";
import UploadCsvModal from "./UploadCsvModal";
import UploadCsvModal from "@/app/portfolio/[slug]/components/UploadCsvModal";
import { useState } from "react";
interface ToolbarProps {

View file

@ -0,0 +1,50 @@
"use client";
import { Input } from "@/app/shadcn_components/ui/input";
import { Label } from "@/app/shadcn_components/ui/label";
export function InputFile({
handleButtonDisabled,
setCsvFile,
selectedGoal,
housingType,
goalValue,
}: {
handleButtonDisabled: (
goal?: string,
scheme?: string,
value?: string,
file?: File | null
) => void;
setCsvFile: (file: File) => void;
selectedGoal: string;
housingType: string;
goalValue: string;
}) {
function handleOnChange(e: React.ChangeEvent<HTMLInputElement>) {
if (e.target.files) {
// Check if files is not null
const file = e.target.files[0];
if (file.type !== "text/csv") {
// Show an error message
console.error("File is not a CSV");
return;
}
setCsvFile(file); // Assuming you have a state to keep the file
handleButtonDisabled(selectedGoal, housingType, goalValue, file);
}
}
return (
<div className="grid w-full max-w-sm items-center gap-1.5 text-sm font-semibold text-gray-600">
<Label htmlFor="csv-uploader">Upload your csv</Label>
<Input
id="csv-uploader"
type="file"
accept=".csv, text/csv"
className="cursor-pointer"
onChange={handleOnChange}
/>
</div>
);
}

View file

@ -0,0 +1,194 @@
"use client";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
function generateS3Key(userId: number, portfolioId: number, filename: string) {
const timestamp = new Date().toISOString().replace(/[:.-]/g, "");
const key = `${userId}/${portfolioId}/${filename}-${timestamp}.csv`;
return key;
}
interface UseCreatePlanProps {
portfolioId: number;
housingType: string;
goal: string;
goalValue: string;
file: File;
}
interface UseCreatePlanReturn {
handlePlanBuild: () => void;
isGeneratingUrlLoading: boolean;
isUploadLoading: boolean;
}
const useCreatePlan = ({
portfolioId,
housingType,
goal,
goalValue,
file,
}: UseCreatePlanProps): UseCreatePlanReturn => {
const router = useRouter();
const session = useSession();
const userId = session.data?.user.dbId;
const fileKey = generateS3Key(
userId,
portfolioId,
"portfolio_plan_properties"
);
const { mutate: mutateUploadCsv, isLoading: isUploadLoading } = useMutation(
uploadCsvToS3,
{
onSuccess: (data) => {
const body = JSON.stringify({
portfolio_id: portfolioId,
housing_type: housingType,
goal: goal,
goal_value: goalValue,
trigger_file_path: fileKey,
});
const response = fetch(`/api/plan/trigger`, {
method: "POST",
body: body,
});
return response;
},
onError: (error) => {
console.error(error);
},
}
);
const { mutate, isLoading: isGeneratingUrlLoading } = useMutation(
generatePresignedUrl,
{
onSuccess: (data) => {
try {
const response = mutateUploadCsv({
presignedUrl: data.url,
file: file,
});
return response;
} catch (error) {
console.error(error);
}
},
onError: (error) => {
console.error(error);
},
}
);
const handlePlanBuild = () => {
mutate({ userId: userId, portfolioId: portfolioId, fileKey: fileKey });
router.push(`/portfolio/${portfolioId}/plan-loading`);
};
return {
handlePlanBuild,
isGeneratingUrlLoading,
isUploadLoading,
};
};
async function generatePresignedUrl({
userId,
portfolioId,
fileKey,
}: {
userId: number;
portfolioId: number;
fileKey: string;
}) {
const body = JSON.stringify({
userId: userId,
portfolioId: portfolioId,
fileKey: fileKey,
});
const presignedResponse = await fetch(`/api/upload/csv`, {
method: "POST",
body: body,
});
if (!presignedResponse.ok) {
throw new Error("Network response was not ok");
}
const presignedUrl = await presignedResponse.json();
return presignedUrl;
}
async function uploadCsvToS3({
presignedUrl,
file,
}: {
presignedUrl: string;
file: File;
}) {
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 };
}
export const SubmitPlan = ({
buttonDisabled,
goal,
housingType,
goalValue,
file,
portfolioId,
}: {
buttonDisabled: boolean;
goal: string;
housingType: string;
goalValue: string;
file: File;
portfolioId: number;
}) => {
const router = useRouter();
const session = useSession();
const { handlePlanBuild, isGeneratingUrlLoading, isUploadLoading } =
useCreatePlan({ portfolioId, housingType, goal, goalValue, file });
let buttonText;
if (isUploadLoading) {
buttonText = "Uploading file...";
} else if (isGeneratingUrlLoading) {
buttonText = "Creating plan...";
} else {
buttonText = "Create";
}
return (
<button
type="button"
className="text-white inline-flex justify-center rounded-md border border-transparent bg-brandblue px-4 py-2 text-sm font-medium text-grey-900 hover:bg-hoverblue focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:bg-gray-300 disabled:opacity-50"
onClick={handlePlanBuild}
disabled={buttonDisabled || isGeneratingUrlLoading || isUploadLoading}
>
{buttonText}
</button>
);
};

View file

@ -4,224 +4,8 @@ import { Menu, Dialog, Transition } from "@headlessui/react";
import { Fragment, useState } from "react";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import { Float } from "@headlessui-float/react";
import { Input } from "@/app/shadcn_components/ui/input";
import { Label } from "@/app/shadcn_components/ui/label";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
function generateS3Key(userId: number, portfolioId: number, filename: string) {
const timestamp = new Date().toISOString().replace(/[:.-]/g, "");
const key = `${userId}/${portfolioId}/${filename}-${timestamp}.csv`;
return key;
}
async function generatePresignedUrl({
userId,
portfolioId,
fileKey,
}: {
userId: number;
portfolioId: number;
fileKey: string;
}) {
const body = JSON.stringify({
userId: userId,
portfolioId: portfolioId,
fileKey: fileKey,
});
const presignedResponse = await fetch(`/api/upload/csv`, {
method: "POST",
body: body,
});
if (!presignedResponse.ok) {
throw new Error("Network response was not ok");
}
const presignedUrl = await presignedResponse.json();
return presignedUrl;
}
async function uploadCsvToS3({
presignedUrl,
file,
}: {
presignedUrl: string;
file: File;
}) {
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 };
}
export const SubmitPlan = ({
buttonDisabled,
goal,
housingType,
goalValue,
file,
portfolioId,
}: {
buttonDisabled: boolean;
goal: string;
housingType: string;
goalValue: string;
file: File;
portfolioId: number;
}) => {
const router = useRouter();
const session = useSession();
const userId = session.data?.user.dbId;
const fileKey = generateS3Key(
userId,
portfolioId,
"portfolio_plan_properties"
);
const { mutate: mutateUploadCsv, isLoading: isUploadLoading } = useMutation(
uploadCsvToS3,
{
onSuccess: (data) => {
const body = JSON.stringify({
// userId: session.data?.user.dbId,
portfolio_id: portfolioId,
housing_type: housingType,
goal: goal,
goal_value: goalValue,
trigger_file_path: fileKey,
});
// After the file has been uploaded, we can trigger the job to build the plan
const response = fetch(`/api/plan/trigger`, {
method: "POST",
body: body,
});
return response;
},
onError: (error) => {
// handle error
console.error(error);
},
}
);
const { mutate, isLoading } = useMutation(generatePresignedUrl, {
onSuccess: (data) => {
// After the presigned URL has been generated, we can upload the file to S3
try {
const response = mutateUploadCsv({
presignedUrl: data.url,
file: file,
});
return response;
} catch (error) {
console.error(error);
}
},
onError: (error) => {
// handle error
console.error(error);
},
});
if (!session.data) {
// The user is not logged in, redirect them to sign in
router.push("/");
return null;
}
const handlePlanBuild = () => {
// The plan build is triggered by clicking submit which will:
// 1) Generate a pre-signed url to upload to
// 2) Upload the csv to the pre-signed url
// 3) Trigger the job to build the plan
// 4) Redirect the user to some loading page - this could be the portfolio page itself and we just trigger a regresh with skeleton cards for the properties
mutate({ userId: userId, portfolioId: portfolioId, fileKey: fileKey });
// TODO: Make api call to backend service to trigger the plan build
// Probably need to pass in the file key to mutate (define it outside)
// because of the async
// We could also trigger it inside of mutateUploadCsv but the nested nature is kind of ugly
console.log("Redirect user to loading page");
router.push("/temp-loading");
};
return (
<button
type="button"
className="text-white inline-flex justify-center rounded-md border border-transparent bg-brandblue px-4 py-2 text-sm font-medium text-grey-900 hover:bg-hoverblue focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:bg-gray-300 disabled:opacity-50"
onClick={handlePlanBuild}
disabled={buttonDisabled || isLoading}
>
{isLoading ? "Creating..." : "Create"}
</button>
);
};
export function InputFile({
handleButtonDisabled,
setCsvFile,
selectedGoal,
housingType,
goalValue,
}: {
handleButtonDisabled: (
goal?: string,
scheme?: string,
value?: string,
file?: File | null
) => void;
setCsvFile: (file: File) => void;
selectedGoal: string;
housingType: string;
goalValue: string;
}) {
function handleOnChange(e: React.ChangeEvent<HTMLInputElement>) {
if (e.target.files) {
// Check if files is not null
const file = e.target.files[0];
if (file.type !== "text/csv") {
// Show an error message
console.error("File is not a CSV");
return;
}
setCsvFile(file); // Assuming you have a state to keep the file
handleButtonDisabled(selectedGoal, housingType, goalValue, file);
}
}
return (
<div className="grid w-full max-w-sm items-center gap-1.5 text-sm font-semibold text-gray-600">
<Label htmlFor="csv-uploader">Upload your csv</Label>
<Input
id="csv-uploader"
type="file"
accept=".csv, text/csv"
className="cursor-pointer"
onChange={handleOnChange}
/>
</div>
);
}
import { InputFile } from "@/app/portfolio/[slug]/components/InputFile";
import { SubmitPlan } from "@/app/portfolio/[slug]/components/SubmitPlan";
type Option = {
label: string;