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 ? ( { /> - + Submit diff --git a/schema.gql b/schema.gql index 7f6840b..752e35c 100644 --- a/schema.gql +++ b/schema.gql @@ -3,6 +3,42 @@ # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- +input Coordinates { + latitude: Float! + longitude: Float! +} + +type ImageSignature { + signature: String! + timestamp: Int! +} + +type Mutation { + createImageSignature: ImageSignature! + createPlace(input: PlaceInput!): Place +} + +type Place { + address: String! + description: String! + id: ID! + image: String! + latitude: Int! + longitude: Int! + placeName: String! + placeType: String! + publicId: String! +} + +input PlaceInput { + address: String! + coordinates: Coordinates! + description: String! + image: String! + placeName: String! + placeType: String! +} + type Query { hello: String! } diff --git a/src/components/LocationSearch.tsx b/src/components/LocationSearch.tsx index a3f4159..2d1e32e 100644 --- a/src/components/LocationSearch.tsx +++ b/src/components/LocationSearch.tsx @@ -67,8 +67,8 @@ const PlacesAutoComplete: FC = ({ const { lat, lng } = await getLatLng(results[0]); onSelectAddress({ address, lat, lng }); - } catch (error) { - console.error(`😱 Error:`, error); + } catch (err) { + console.error(`😱: ${err}`); } } else { clearSuggestions(); diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index a7e9d47..f313614 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -22,7 +22,7 @@ export const Button: FC = ({ rel, ...props }) => { - const btnClass = `border-none bg-purple-700 hover:bg-purple-800 transition duration-150 ease text-white px-5 py-3 rounded-md select-none outline-none focus:outline-none active:bg-purple-900 ${className}`; + const btnClass = `border-none bg-purple-700 hover:bg-purple-800 transition duration-150 ease text-white px-5 py-3 rounded-md select-none outline-none focus:outline-none active:bg-purple-900 disabled:bg-purple-400 disabled:cursor-not-allowed ${className}`; return ( <> diff --git a/src/components/ui/FileInput.tsx b/src/components/ui/FileInput.tsx index a3e5149..6f9edf4 100644 --- a/src/components/ui/FileInput.tsx +++ b/src/components/ui/FileInput.tsx @@ -61,7 +61,7 @@ export const FileInput: FC = ({ }; return ( - + = ({ ...rest }) => { return ( - + = ({ }, [value]); return ( - + = ({ ...rest }) => { return ( - + = ({ context }) => { const { uid } = context; + if (!uid) { + throw new AuthenticationError("You must be logged in"); + } return !!uid; }; diff --git a/src/schema/image.ts b/src/schema/image.ts new file mode 100644 index 0000000..79a0f8b --- /dev/null +++ b/src/schema/image.ts @@ -0,0 +1,35 @@ +import { AuthenticationError } from "apollo-server-micro"; +import { + Authorized, + Field, + Int, + Mutation, + ObjectType, + Resolver, +} from "type-graphql"; +const cloudinary = require("cloudinary").v2; + +@ObjectType() +class ImageSignature { + @Field((_type) => String) + signature!: string; + + @Field((_type) => Int) + timestamp!: number; +} + +@Resolver() +export class ImageResolver { + @Authorized() + @Mutation((_returns) => ImageSignature) + createImageSignature(): ImageSignature { + const timestamp = Math.round(new Date().getTime() / 1000); + const signature = cloudinary.utils.api_sign_request( + { + timestamp, + }, + process.env.CLOUDINARY_SECRET + ); + return { timestamp, signature }; + } +} diff --git a/src/schema/index.ts b/src/schema/index.ts index 0fec4d0..edcaba6 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -1,5 +1,7 @@ -import { buildSchemaSync, Resolver, Query } from "type-graphql"; +import { buildSchemaSync, Query, Resolver } from "type-graphql"; import { authChecker } from "./authChecker"; +import { ImageResolver } from "./image"; +import { PlaceResolver } from "./place"; @Resolver() class DummyResolver { @@ -10,7 +12,7 @@ class DummyResolver { } export const schema = buildSchemaSync({ - resolvers: [DummyResolver], + resolvers: [DummyResolver, ImageResolver, PlaceResolver], emitSchemaFile: process.env.NODE_ENV === "development", authChecker, }); diff --git a/src/schema/place.ts b/src/schema/place.ts new file mode 100644 index 0000000..400d741 --- /dev/null +++ b/src/schema/place.ts @@ -0,0 +1,105 @@ +import { Max, Min } from "class-validator"; +import { + Arg, + Authorized, + Ctx, + Field, + Float, + ID, + InputType, + Int, + Mutation, + ObjectType, + Resolver, +} from "type-graphql"; +import { AuthorizedContext } from "./context"; + +@InputType() +class Coordinates { + @Min(-90) + @Max(90) + @Field((_type) => Float) + latitude!: number; + + @Min(-180) + @Max(180) + @Field((_type) => Float) + longitude!: number; +} + +@InputType() +class PlaceInput { + @Field((_type) => String) + placeName!: string; + + @Field((_type) => String) + placeType!: string; + + @Field((_type) => String) + description!: string; + + @Field((_type) => String) + address!: string; + + @Field((_type) => String) + image!: string; + + @Field((_type) => Coordinates) + coordinates!: Coordinates; +} + +@ObjectType() +class Place { + @Field((_type) => ID) + id!: number; + + @Field((_type) => String) + placeName!: string; + + @Field((_type) => String) + placeType!: string; + + @Field((_type) => String) + description!: string; + + @Field((_type) => String) + address!: string; + + @Field((_type) => Int) + latitude!: number; + + @Field((_type) => Int) + longitude!: number; + + @Field((_type) => String) + image!: string; + + @Field((_type) => String) + publicId(): string { + const parts = this.image.split("/"); + return parts[parts.length - 1] ?? ""; + } +} + +@Resolver() +export class PlaceResolver { + @Authorized() + @Mutation((_returns) => Place, { nullable: true }) + async createPlace( + @Arg("input") input: PlaceInput, + @Ctx() ctx: AuthorizedContext + ) { + return await ctx.prisma.place.create({ + data: { + userId: ctx.uid, + image: input.image, + address: input.address, + latitude: input.coordinates.latitude, + longitude: input.coordinates.longitude, + placeName: input.placeName, + placeType: input.placeType, + description: input.description, + }, + }); + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 73763b1..212cc78 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -60,7 +60,8 @@ module.exports = { }, variants: { extend: { - backgroundColor: ["active"], + backgroundColor: ["active", "disabled"], + cursor: ["disabled"], translate: ["active"], }, }, diff --git a/yarn.lock b/yarn.lock index 85c0f14..56ea4d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -193,7 +193,7 @@ dependencies: "@babel/types" "^7.12.7" -"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.7.0": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.7.0": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" dependencies: @@ -368,6 +368,15 @@ source-map "^0.5.7" stylis "^4.0.3" +"@emotion/cache@^10.0.27": + version "10.0.29" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/cache@^11.0.0", "@emotion/cache@^11.1.3": version "11.1.3" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.1.3.tgz#c7683a9484bcd38d5562f2b9947873cf66829afd" @@ -378,6 +387,25 @@ "@emotion/weak-memoize" "^0.2.5" stylis "^4.0.3" +"@emotion/core@^10.0.14": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3" + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + +"@emotion/css@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" + dependencies: + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + babel-plugin-emotion "^10.0.27" + "@emotion/css@^11.0.0": version "11.1.3" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.1.3.tgz#9ed44478b19e5d281ccbbd46d74d123d59be793f" @@ -388,7 +416,7 @@ "@emotion/sheet" "^1.0.0" "@emotion/utils" "^1.0.0" -"@emotion/hash@^0.8.0": +"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" @@ -418,6 +446,16 @@ "@emotion/weak-memoize" "^0.2.5" hoist-non-react-statics "^3.3.1" +"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16": + version "0.11.16" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad" + dependencies: + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + "@emotion/serialize@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.0.tgz#1a61f4f037cf39995c97fc80ebe99abc7b191ca9" @@ -428,19 +466,31 @@ "@emotion/utils" "^1.0.0" csstype "^3.0.2" +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + "@emotion/sheet@^1.0.0", "@emotion/sheet@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.1.tgz#245f54abb02dfd82326e28689f34c27aa9b2a698" -"@emotion/unitless@^0.7.5": +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + +"@emotion/unitless@0.7.5", "@emotion/unitless@^0.7.5": version "0.7.5" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + "@emotion/utils@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af" -"@emotion/weak-memoize@^0.2.5": +"@emotion/weak-memoize@0.2.5", "@emotion/weak-memoize@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" @@ -1311,6 +1361,12 @@ "@types/react-dom" "*" "@types/react-transition-group" "*" +"@types/react-toast-notifications@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/react-toast-notifications/-/react-toast-notifications-2.4.0.tgz#0ca0732cfae5a6ef5939a676fffac6e64c78bc25" + dependencies: + "@types/react" "*" + "@types/react-transition-group@*": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" @@ -1339,6 +1395,10 @@ version "0.3.1" resolved "https://registry.yarnpkg.com/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz#18ce9f657da556037a29d50604335614ce703f4c" +"@types/validator@^13.1.3": + version "13.1.3" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.1.3.tgz#366b394aa3fbeed2392bf0a20ded606fa4a3d35e" + "@types/viewport-mercator-project@*": version "6.1.1" resolved "https://registry.yarnpkg.com/@types/viewport-mercator-project/-/viewport-mercator-project-6.1.1.tgz#07cd2ae62e0d54a1265274c8b112d376550b031a" @@ -2219,7 +2279,22 @@ babel-eslint@^10.1.0: eslint-visitor-keys "^1.0.0" resolve "^1.12.0" -babel-plugin-macros@^2.6.1: +babel-plugin-emotion@^10.0.27: + version "10.0.33" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz#ce1155dcd1783bbb9286051efee53f4e2be63e03" + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.8.0" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.16" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + +babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.6.1: version "2.8.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" dependencies: @@ -2231,7 +2306,7 @@ babel-plugin-parameter-decorator@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/babel-plugin-parameter-decorator/-/babel-plugin-parameter-decorator-1.0.16.tgz#1c889c3a1f3bbf03801fcbc2e95b8bae7468df3f" -babel-plugin-syntax-jsx@6.18.0: +babel-plugin-syntax-jsx@6.18.0, babel-plugin-syntax-jsx@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" @@ -2721,6 +2796,14 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +class-validator@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.13.1.tgz#381b2001ee6b9e05afd133671fbdf760da7dec67" + dependencies: + "@types/validator" "^13.1.3" + libphonenumber-js "^1.9.7" + validator "^13.5.2" + classnames@2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -2812,6 +2895,26 @@ cli-ux@^4.9.0: treeify "^1.1.0" tslib "^1.9.3" +cloudinary-core@^2.10.2, cloudinary-core@^2.11.3: + version "2.11.3" + resolved "https://registry.yarnpkg.com/cloudinary-core/-/cloudinary-core-2.11.3.tgz#1440f61c6280485094aac87021b7e10f746dc69e" + +cloudinary-react@^1.6.8: + version "1.6.8" + resolved "https://registry.yarnpkg.com/cloudinary-react/-/cloudinary-react-1.6.8.tgz#e13b229553521aa375db655e9e8a5d3a5a087ca2" + dependencies: + cloudinary-core "^2.11.3" + prop-types "^15.6.2" + +cloudinary@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/cloudinary/-/cloudinary-1.24.0.tgz#66709c9ee8f8f026aedd9c94dc2d922d14a46252" + dependencies: + cloudinary-core "^2.10.2" + core-js "3.6.5" + lodash "^4.17.11" + q "^1.5.1" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -3179,6 +3282,10 @@ cssnano-simple@1.2.1: cssnano-preset-simple "1.2.1" postcss "^7.0.32" +csstype@^2.5.7: + version "2.6.14" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de" + csstype@^3.0.2: version "3.0.6" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef" @@ -5246,6 +5353,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libphonenumber-js@^1.9.7: + version "1.9.9" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.9.tgz#e097f00f834f92fe44abd8377b4118c6c6358e40" + line-column@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" @@ -6951,6 +7062,10 @@ purgecss@^3.1.3: postcss "^8.2.1" postcss-selector-parser "^6.0.2" +q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + qs@^6.9.4: version "6.9.6" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" @@ -7075,6 +7190,13 @@ react-select@^4.0.2: react-input-autosize "^3.0.0" react-transition-group "^4.3.0" +react-toast-notifications@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-toast-notifications/-/react-toast-notifications-2.4.0.tgz#6213730bd1fe158fc01aeef200687ea94c5c5b24" + dependencies: + "@emotion/core" "^10.0.14" + react-transition-group "^4.3.0" + react-transition-group@^4.3.0: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" @@ -8513,6 +8635,10 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.5.2: + version "13.5.2" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.5.2.tgz#c97ae63ed4224999fb6f42c91eaca9567fe69a46" + "viewport-mercator-project@^6.2.3 || ^7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/viewport-mercator-project/-/viewport-mercator-project-7.0.1.tgz#9d7248072f2cbb122f93b63d2b346a5763b8d79a"