diff --git a/.env.example b/.env.example index beb420e..4336adb 100644 --- a/.env.example +++ b/.env.example @@ -9,5 +9,6 @@ FIREBASE_PRIVATE_KEY="" CLOUDINARY_SECRET="" NEXT_PUBLIC_CLOUDINARY_KEY="" NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="" +NEXT_PUBLIC_CLOUDINARY_API_BASE_URL="" DATABASE_URL="" \ No newline at end of file diff --git a/env.d.ts b/env.d.ts index 5c7da79..a47fd61 100644 --- a/env.d.ts +++ b/env.d.ts @@ -9,6 +9,7 @@ declare namespace NodeJS { CLOUDINARY_SECRET: string; NEXT_PUBLIC_CLOUDINARY_KEY: string; NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: string; + NEXT_PUBLIC_CLOUDINARY_API_BASE_URL: string; DATABASE_URL: string; } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 4cf8cfb..52391bd 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "codegen": "apollo client:codegen --target typescript --localSchemaFile schema.gql --outputFlat --includes \"{pages,src}/**\" --excludes \"src/generated/**\" src/generated", + "codegen": "apollo client:codegen --target typescript --localSchemaFile schema.gql --outputFlat --includes \"{pages,src}/gql/**\" --excludes \"src/generated/**\" src/generated", "codegen:watch": "yarn codegen --watch", "db:init": "yarn prisma init", "db:generate": "yarn prisma generate", @@ -19,6 +19,9 @@ "@hookform/resolvers": "1.3.0", "@prisma/client": "2.12.0", "apollo-server-micro": "^2.19.2", + "class-validator": "^0.13.1", + "cloudinary": "^1.24.0", + "cloudinary-react": "^1.6.8", "firebase": "^8.2.4", "firebase-admin": "^9.4.2", "framer-motion": "^3.2.2-rc.1", @@ -31,6 +34,7 @@ "react-icons": "^4.1.0", "react-map-gl": "5.2.10", "react-select": "^4.0.2", + "react-toast-notifications": "^2.4.0", "reflect-metadata": "^0.1.13", "sass": "^1.32.5", "type-graphql": "^1.1.1", @@ -50,6 +54,7 @@ "@types/react-dom": "^17.0.0", "@types/react-map-gl": "^5.2.9", "@types/react-select": "^4.0.8", + "@types/react-toast-notifications": "^2.4.0", "@typescript-eslint/eslint-plugin": "^4.14.1", "@typescript-eslint/eslint-plugin-tslint": "^4.14.1", "@typescript-eslint/parser": "^4.14.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index e376696..76a791e 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -8,6 +8,7 @@ import { NextComponentType, NextPageContext } from "next"; import { Router } from "next/router"; import { ApolloProvider } from "@apollo/client"; import { useApollo } from "src/apollo"; +import { ToastProvider } from "react-toast-notifications"; type additionalType = { title: string; @@ -33,9 +34,15 @@ function MyApp({ Component, pageProps }: AppProps) { - - - + + + + + diff --git a/pages/places/add.tsx b/pages/places/add.tsx index 3bbe75f..cd31c2c 100644 --- a/pages/places/add.tsx +++ b/pages/places/add.tsx @@ -13,15 +13,23 @@ import * as Yup from "yup"; import LocationSearch from "src/components/LocationSearch"; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; +import { useMutation } from "@apollo/client"; +import { + CREATE_IMAGE_SIGNATURE_MUTATION, + CREATE_PLACE_MUTATION, +} from "src/gql"; +import { CreateSignatureMutation } from "src/generated/CreateSignatureMutation"; +import { CreatePlaceMutation } from "src/generated/CreatePlaceMutation"; +import { useToasts } from "react-toast-notifications"; interface IFormData { placeName: string; placeType: string; description: string; address: string; - lat: number; - lng: number; - images: FileList; + latitude: number; + longitude: number; + image: FileList; } const validationSchema = Yup.object({ @@ -31,15 +39,15 @@ const validationSchema = Yup.object({ "Please provide a description for this place" ), address: Yup.string().required("Please select an address"), - lat: Yup.number() + latitude: Yup.number() .required("Please select an address") .min(-90, "") .max(90, ""), - lng: Yup.number() + longitude: Yup.number() .required("Please select an address") .min(-180, "") .max(180, ""), - images: Yup.mixed() + image: Yup.mixed() .required("Please add an image") .test("fileRequired", "Please add an image", (value) => { return value.length > 0; @@ -56,10 +64,46 @@ const validationSchema = Yup.object({ return value?.type?.match(/^image\/.*$/); }), }); +interface IUploadImageResponse { + secure_url?: string; + error?: { + message: string; + }; +} + +const uploadImage = async ( + image: File, + signature: string, + timestamp: number +): Promise => { + const url = `${process.env.NEXT_PUBLIC_CLOUDINARY_API_BASE_URL}/upload`; + + const formData = new FormData(); + formData.append("file", image); + formData.append("signature", signature); + formData.append("timestamp", timestamp.toString() + "sf"); + formData.append("api_key", process.env.NEXT_PUBLIC_CLOUDINARY_KEY ?? ""); + + try { + const response = await fetch(url, { + method: "POST", + body: formData, + }); + const data = await response.json(); + + if (data.error) { + throw new Error(data.error.message); + } + return data; + } catch (err) { + throw new Error(err.message ?? err); + } +}; const Add = () => { + const { addToast } = useToasts(); const [previewImage, setPreviewImage] = useState(""); - + const [submitting, setSubmitting] = useState(false); const { handleSubmit, register, @@ -73,17 +117,23 @@ const Add = () => { mode: "onTouched", }); + const [createSignature] = useMutation( + CREATE_IMAGE_SIGNATURE_MUTATION + ); + + const [createPlace] = useMutation(CREATE_PLACE_MUTATION); + useEffect(() => { register({ name: "address" }); - register({ name: "lat" }); - register({ name: "lng" }); + register({ name: "latitude" }); + register({ name: "longitude" }); register({ name: "placeType" }); }, [register]); useEffect(() => { - const images = watch("images"); - if (images.length > 0) { - const file = images[0] as File; + const image = watch("image"); + if (image.length > 0) { + const file = image[0] as File; const reader = new FileReader(); reader.onloadend = () => { setPreviewImage(reader.result as string); @@ -92,7 +142,80 @@ const Add = () => { } else { setPreviewImage(""); } - }, [watch("images")]); + }, [watch("image")]); + + const onSubmit = async (data: IFormData) => { + setSubmitting(true); + try { + const { + data: signatureData, + errors: signatureErrors, + } = await createSignature(); + + if (signatureErrors && signatureErrors.length) { + signatureErrors.map((error) => { + throw new Error( + error.message ?? "Something went wrong! Please try again later" + ); + }); + } + + if (signatureData) { + const { signature, timestamp } = signatureData.createImageSignature; + const imageData = await uploadImage( + data.image[0] as File, + signature, + timestamp + ); + + if (imageData.error) { + throw new Error( + imageData.error.message ?? + "Something went wrong! Please try again later" + ); + } else { + const { data: placeData, errors: placceErrors } = await createPlace({ + variables: { + input: { + placeName: data.placeName, + placeType: data.placeType, + description: data.description, + address: data.address, + image: imageData.secure_url, + coordinates: { + latitude: data.latitude, + longitude: data.longitude, + }, + }, + }, + }); + + if (placceErrors && placceErrors.length) { + placceErrors.map((error) => { + throw new Error( + error.message ?? "Something went wrong! Please try again later" + ); + }); + } + + if (placeData?.createPlace?.id) { + addToast("Place added successfully", { + appearance: "success", + }); + } else { + throw new Error("Something went wrong! Please try again later"); + } + } + } + } catch (err) { + console.error(`😱: ${err}`); + addToast(err.message ?? "Something went wrong! Please try again later.", { + appearance: "error", + }); + } finally { + setSubmitting(false); + } + }; return (
{ style={{ height: "calc(100vh - 75px)" }} >
console.log("data", data))} + onSubmit={handleSubmit(onSubmit)} className="mx-auto flex flex-col items-center" style={{ maxWidth: "480px" }} register={register} @@ -114,21 +237,21 @@ const Add = () => { shouldValidate: true, shouldDirty: true, }); - setValue("lat", lat, { + setValue("latitude", lat, { shouldValidate: true, shouldDirty: true, }); - setValue("lng", lng, { + setValue("longitude", lng, { shouldValidate: true, shouldDirty: true, }); }} - error={errors?.address || errors?.lat || errors?.lng} + error={errors?.address || errors?.latitude || errors?.longitude} /> trigger("images")} + onBlur={() => trigger("image")} /> {previewImage ? ( { />