diff --git a/.env.development b/.env.development index 1aa7b4e..e4820e9 100755 --- a/.env.development +++ b/.env.development @@ -1,7 +1,6 @@ # Backend NEXT_PUBLIC_GRAPHQL_URI="https://dev.api.marathon.perimetre.co/graphql" -#NEXT_PUBLIC_GRAPHQL_URI="http://localhost:3001/graphql" -#NEXT_PUBLIC_STATUS_URI="http://localhost:3001/v1/status" +NEXT_PUBLIC_STATUS_URI="https://dev.api.marathon.perimetre.co/v1/status" # Unity NEXT_PUBLIC_UNITY_PUBLIC_FOLDER="unity" @@ -18,4 +17,8 @@ NEXT_PUBLIC_UNITY_PUBLIC_MEDIA_URI="https://marathon-media-dev01.nyc3.digitaloce NEXT_PUBLIC_UNITY_ASSET_BUNDLE_FOLDER="asset-bundles" NEXT_PUBLIC_UNITY_DEFAULT_PLATFORM="webgl" NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER="materials/modules" -NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME="WebGL" \ No newline at end of file +NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME="WebGL" + +# Marathon +NEXT_PUBLIC_MARATHON_API="http://new-web.marathonhardware.com" +NEXT_PUBLIC_MARATHON_API_LIST="/action/account/listdetail" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c1723bf..6a56602 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,6 +70,10 @@ ARG NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER ENV NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER=${NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER} ARG NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME ENV NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME=${NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME} +ARG NEXT_PUBLIC_MARATHON_API +ENV NEXT_PUBLIC_MARATHON_API=${NEXT_PUBLIC_MARATHON_API} +ARG NEXT_PUBLIC_MARATHON_API_LIST +ENV NEXT_PUBLIC_MARATHON_API_LIST=${NEXT_PUBLIC_MARATHON_API_LIST} WORKDIR /build diff --git a/locales/en.json b/locales/en.json index 0054613..e7391e7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -19,7 +19,9 @@ "ECONNREFUSED": "Could not contact the server. The server might be down or your internet is turned off. Please try again later.", "unauthorized": "You are not permitted to do this action", "wrongCredentials": "Email or password is incorrect", - "unreachableService": "An error has occurred. Please try again later. (Unreachable service)" + "unreachableService": "An error has occurred. Please try again later. (Unreachable service)", + "createListNoExternalId": "An error has occurred and a list could not be created (No external id)", + "createListCannotComplete": "An error has occurred. Could not complete the request." }, "build": { "loadingModule": "Please wait...", @@ -121,7 +123,8 @@ "headers": { "product": "Product", "quantity": "Qty" - } + }, + "listCreatedSuccessfully": "Your list {name} was successfully created. Click here to open it in a new tab" }, "help": { "selectModule": "Select module", @@ -131,4 +134,4 @@ "rotateModule": "Rotate module", "moveModule": "Move module" } -} \ No newline at end of file +} diff --git a/src/apollo/cart/index.tsx b/src/apollo/cart/index.tsx index 7196d92..7e3d164 100644 --- a/src/apollo/cart/index.tsx +++ b/src/apollo/cart/index.tsx @@ -23,6 +23,7 @@ export const CREATE_LIST = gql` mutation CreateList($projectId: Int!) { createList(id: $projectId) { id + externalId name project { id diff --git a/src/apollo/generated/graphql.tsx b/src/apollo/generated/graphql.tsx index 61edcf3..b4984e2 100644 --- a/src/apollo/generated/graphql.tsx +++ b/src/apollo/generated/graphql.tsx @@ -5618,6 +5618,7 @@ export type CreateListMutation = { | { __typename?: 'List'; id: number; + externalId?: string | null | undefined; name?: string | null | undefined; project?: { __typename?: 'Project'; id: number } | null | undefined; } @@ -7573,6 +7574,7 @@ export const CreateListDocument = gql` mutation CreateList($projectId: Int!) { createList(id: $projectId) { id + externalId name project { id diff --git a/src/components/Elements/CreateListButton/index.tsx b/src/components/Elements/CreateListButton/index.tsx new file mode 100644 index 0000000..539b39d --- /dev/null +++ b/src/components/Elements/CreateListButton/index.tsx @@ -0,0 +1,121 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { FilePlus, X } from 'react-feather'; +import { FormattedMessage } from 'react-intl'; +import { CreateListMutation, useCreateListMutation } from '../../../apollo/generated/graphql'; +import env from '../../../env'; +import { CatchGraphqlError, getLocaleIdFromGraphqlError } from '../../../lib/apollo/exceptions'; +import Button from '../../UI/Button'; +import ReactPortal from '../../UI/ReactPortal'; +import Spinner from '../../UI/Spinner'; +import Toastr, { ToastrRef } from '../../UI/Toastr'; + +type CreateListButtonProps = { + projectId: number; +}; + +const CreateListButton: React.FC = ({ projectId }) => { + // *********** + // ** Grapqhl declarations + // *********** + + // ** Mutations + const [createList, { loading: createListLoading }] = useCreateListMutation(); + + // *********** + // ** Business logic + // *********** + + const { NEXT_PUBLIC_MARATHON_API, NEXT_PUBLIC_MARATHON_API_LIST } = useMemo(env, []); + + const [createListError, setCreateListError] = useState(); + const [lastCreatedList, setLastCreatedList] = useState(undefined); + const toastRef = useRef(null); + + const listUrl = useMemo(() => { + if (!NEXT_PUBLIC_MARATHON_API_LIST || !NEXT_PUBLIC_MARATHON_API) return; + + const url = new URL(NEXT_PUBLIC_MARATHON_API_LIST, NEXT_PUBLIC_MARATHON_API); + + if (lastCreatedList?.createList?.externalId) { + url.searchParams.append('oid', lastCreatedList.createList.externalId); + } + + return url.toString(); + }, [NEXT_PUBLIC_MARATHON_API, NEXT_PUBLIC_MARATHON_API_LIST, lastCreatedList]); + + const handleCreateList = useCallback(async () => { + setLastCreatedList(undefined); + if (projectId) { + try { + const { data: result } = await createList({ + variables: { + projectId + } + }); + + setLastCreatedList(result || undefined); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + const { graphQLErrors, networkError }: CatchGraphqlError = err; + // If failed we'll get here + // Get the locale id for the error + const error = getLocaleIdFromGraphqlError(graphQLErrors, networkError); + + // Update the error state, which will display the error + if (error) { + toastRef.current?.open(3, () => { + setCreateListError(undefined); + }); + setCreateListError(error); + } + } + } + }, [createList, projectId]); + + return ( + <> + + {createListLoading ? ( + + ) : ( + <> + + + + + > + )} + + + {!!lastCreatedList && ( + + + + {msg}, + url: (msg: string) => ( + + {msg} + + ) + }} + /> + + setLastCreatedList(undefined)}> + + + + + )} + + + {createListError && } + + > + ); +}; + +export default CreateListButton; diff --git a/src/components/Templates/Cart/index.tsx b/src/components/Templates/Cart/index.tsx index bc1766c..cf6ba64 100644 --- a/src/components/Templates/Cart/index.tsx +++ b/src/components/Templates/Cart/index.tsx @@ -1,16 +1,17 @@ +import classNames from 'classnames'; +import Head from 'next/head'; +import Link from 'next/link'; import React from 'react'; +import { ChevronLeft, File } from 'react-feather'; +import { FormattedMessage, useIntl } from 'react-intl'; import { CartQuery } from '../../../apollo/generated/graphql'; +import CreateListButton from '../../Elements/CreateListButton'; import AppLayout from '../../Layouts/AppLayout'; -import Head from 'next/head'; -import { FormattedMessage, useIntl } from 'react-intl'; -import SkeletonImage from '../../UI/SkeletonImage'; -import classNames from 'classnames'; import Button from '../../UI/Button'; -import Link from 'next/link'; import ErrorMessage from '../../UI/ErrorMessage'; -import Skeleton from '../../UI/Skeleton'; -import { ChevronLeft, File } from 'react-feather'; import NavbarButton from '../../UI/NavbarButton'; +import Skeleton from '../../UI/Skeleton'; +import SkeletonImage from '../../UI/SkeletonImage'; type CartProjectModulesProps = { cart: NonNullable['cart']>; @@ -70,17 +71,9 @@ type CartTemplateProps = { loading: boolean; error?: string; handleTryAgain: () => void; - handleCreateList: () => void; }; -const CartTemplate: React.FC = ({ - data, - slug, - error, - loading, - handleTryAgain - // handleCreateList -}) => { +const CartTemplate: React.FC = ({ data, slug, error, loading, handleTryAgain }) => { const intl = useIntl(); return ( @@ -111,6 +104,7 @@ const CartTemplate: React.FC = ({ + {!error ? ( !loading ? ( @@ -130,14 +124,9 @@ const CartTemplate: React.FC = ({ - {/**/} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/**/} + + {data?.project?.id && } + {data?.project?.cart && ( diff --git a/src/components/UI/Toastr/index.tsx b/src/components/UI/Toastr/index.tsx new file mode 100644 index 0000000..e5fb946 --- /dev/null +++ b/src/components/UI/Toastr/index.tsx @@ -0,0 +1,76 @@ +import React, { forwardRef, useImperativeHandle, useState } from 'react'; +import ReactPortal from '../ReactPortal'; +import classnames from 'classnames'; + +const placementClassnameMap = { + // top: 'top-0', + 'top-start': 'top-0 left-0', + 'top-end': 'top-0 right-0', + // right: '', + 'right-start': 'top-0 right-0', + 'right-end': 'bottom-0 right-0', + // bottom: '', + 'bottom-start': 'bottom-0 left-0', + 'bottom-end': 'bottom-0 right-0', + // left: '', + 'left-start': 'top-0 left-0', + 'left-end': 'bottom-0 left-0' +}; + +const variantClassnameMap = { + default: 'bg-white border-gray-200', + error: 'bg-mui-error font-bold text-white' +}; + +export type ToastrRef = { + open: (timer?: number, done?: () => void) => void; +}; + +export type ToastrProps = { + placement?: keyof typeof placementClassnameMap; + variant?: keyof typeof variantClassnameMap; + timer?: number; + className?: string; + children?: React.ReactNode | string; +}; + +const Toastr = forwardRef(function Toastr( + { placement = 'bottom-end', variant = 'default', timer = 3, className, children }, + ref +) { + const [isOpen, setIsOpen] = useState(false); + + useImperativeHandle( + ref, + () => ({ + open: (newTimer, done) => { + setIsOpen(true); + setTimeout(() => { + setIsOpen(false); + + if (done) done(); + }, (newTimer || timer) * 1000); + } + }), + [timer] + ); + + return ( + + {isOpen && ( + + {children} + + )} + + ); +}); + +export default Toastr; diff --git a/src/env.ts b/src/env.ts index 63059d5..95a084e 100644 --- a/src/env.ts +++ b/src/env.ts @@ -18,7 +18,10 @@ const env = () => { NEXT_PUBLIC_UNITY_ASSET_BUNDLE_FOLDER: process.env.NEXT_PUBLIC_UNITY_ASSET_BUNDLE_FOLDER, NEXT_PUBLIC_UNITY_DEFAULT_PLATFORM: process.env.NEXT_PUBLIC_UNITY_DEFAULT_PLATFORM, NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER: process.env.NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER, - NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME: process.env.NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME + NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME: process.env.NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME, + // Marathon + NEXT_PUBLIC_MARATHON_API: process.env.NEXT_PUBLIC_MARATHON_API, + NEXT_PUBLIC_MARATHON_API_LIST: process.env.NEXT_PUBLIC_MARATHON_API_LIST }; }; diff --git a/src/lib/apollo/exceptions.tsx b/src/lib/apollo/exceptions.tsx index 743559f..5a11373 100644 --- a/src/lib/apollo/exceptions.tsx +++ b/src/lib/apollo/exceptions.tsx @@ -5,10 +5,14 @@ import logging from '../logging'; const codesToIgnoreOnLogging: string[] = []; -export const getLocaleIdFromGraphqlError = ( - graphQLErrors?: readonly GraphQLError[] | null, - networkError?: Error | ServerError | ServerParseError | null -) => { +export type GraphQLErrors = readonly GraphQLError[] | null | undefined; +export type NetworkError = Error | ServerError | ServerParseError | null | undefined; +export type CatchGraphqlError = { + graphQLErrors?: GraphQLErrors; + networkError?: NetworkError; +}; + +export const getLocaleIdFromGraphqlError = (graphQLErrors?: GraphQLErrors, networkError?: NetworkError) => { if ( networkError?.message === 'Failed to fetch' || (networkError as any)?.code === 'ECONNREFUSED' || diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 6b9e2e9..5a6d624 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -54,6 +54,7 @@ class MyDocument extends Document { /> + diff --git a/src/pages/api/status.ts b/src/pages/api/status.ts index c8f1264..98310ad 100644 --- a/src/pages/api/status.ts +++ b/src/pages/api/status.ts @@ -43,7 +43,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< NEXT_PUBLIC_UNITY_ASSET_BUNDLE_FOLDER: envs.NEXT_PUBLIC_UNITY_ASSET_BUNDLE_FOLDER, NEXT_PUBLIC_UNITY_DEFAULT_PLATFORM: envs.NEXT_PUBLIC_UNITY_DEFAULT_PLATFORM, NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER: envs.NEXT_PUBLIC_UNITY_MODULE_MATERIALS_FOLDER, - NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME: envs.NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME + NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME: envs.NEXT_PUBLIC_UNITY_MANIFEST_ASSET_NAME, + // Marathon + NEXT_PUBLIC_MARATHON_API: envs.NEXT_PUBLIC_MARATHON_API, + NEXT_PUBLIC_MARATHON_API_LIST: envs.NEXT_PUBLIC_MARATHON_API_LIST }; res.json(status); diff --git a/src/pages/project/[slug]/cart.tsx b/src/pages/project/[slug]/cart.tsx index 8f439fe..96a8f5e 100644 --- a/src/pages/project/[slug]/cart.tsx +++ b/src/pages/project/[slug]/cart.tsx @@ -1,12 +1,12 @@ -import type { GetServerSideProps, InferGetServerSidePropsType, NextPage } from 'next'; import React, { useCallback, useMemo } from 'react'; -import { CartQuery, CartQueryVariables, useCartQuery, useCreateListMutation } from '../../../apollo/generated/graphql'; -import { addApolloState, initializeApollo, WithApolloProps } from '../../../lib/apollo'; -import { getLocaleIdFromGraphqlError, hasGraphqlUnauthorizedError } from '../../../lib/apollo/exceptions'; import { CART_QUERY } from '../../../apollo/cart'; +import { CartQuery, CartQueryVariables, useCartQuery } from '../../../apollo/generated/graphql'; import CartTemplate from '../../../components/Templates/Cart'; -import { requiredAuthWithRedirectProp } from '../../../utils/auth'; +import { addApolloState, initializeApollo, WithApolloProps } from '../../../lib/apollo'; +import { getLocaleIdFromGraphqlError, hasGraphqlUnauthorizedError } from '../../../lib/apollo/exceptions'; import logging from '../../../lib/logging'; +import { requiredAuthWithRedirectProp } from '../../../utils/auth'; +import type { GetServerSideProps, InferGetServerSidePropsType, NextPage } from 'next'; type CartParams = { slug?: string; @@ -36,9 +36,6 @@ const CartContainer: NextPage = ({ slug }) => { } }); - // ** Mutations - const [createList] = useCreateListMutation(); - // *********** // ** Business logic // *********** @@ -57,25 +54,8 @@ const CartContainer: NextPage = ({ slug }) => { } }, [refetch]); - const handleCreateList = useCallback(async () => { - if (data?.project?.id) { - await createList({ - variables: { - projectId: data.project.id - } - }); - } - }, [createList, data]); - return ( - + ); };
+ {msg}, + url: (msg: string) => ( + + {msg} + + ) + }} + /> +