diff --git a/components/dashboard/package.json b/components/dashboard/package.json index 23e009eb720bf2..3cc861b8f8f040 100644 --- a/components/dashboard/package.json +++ b/components/dashboard/package.json @@ -23,12 +23,14 @@ "monaco-editor": "^0.25.2", "query-string": "^7.1.1", "react": "^17.0.1", + "react-confetti": "^6.1.0", "react-datepicker": "^4.8.0", "react-dom": "^17.0.1", "react-intl-tel-input": "^8.2.0", "react-popper": "^2.3.0", "react-portal": "^4.2.2", "react-router-dom": "^5.2.0", + "validator": "^13.9.0", "xterm": "^4.11.0", "xterm-addon-fit": "^0.5.0" }, @@ -49,6 +51,7 @@ "@types/react-router": "^5.1.13", "@types/react-router-dom": "^5.1.7", "@types/uuid": "^8.3.1", + "@types/validator": "^13.7.12", "@typescript-eslint/eslint-plugin": "^4.21.0", "@typescript-eslint/parser": "^4.21.0", "autoprefixer": "^9.8.6", diff --git a/components/dashboard/src/app/AppRoutes.tsx b/components/dashboard/src/app/AppRoutes.tsx index f10a1f4e132bf8..09a737d6a423d4 100644 --- a/components/dashboard/src/app/AppRoutes.tsx +++ b/components/dashboard/src/app/AppRoutes.tsx @@ -6,7 +6,7 @@ import { ContextURL, Team, User } from "@gitpod/gitpod-protocol"; import React, { FunctionComponent, useContext, useState } from "react"; -import { Redirect, Route, Switch, useLocation, useParams } from "react-router"; +import { Redirect, Route, Switch, useLocation } from "react-router"; import { AppNotifications } from "../AppNotifications"; import Menu from "../menu/Menu"; import OAuthClientApproval from "../OauthClientApproval"; diff --git a/components/dashboard/src/contexts/ConfettiContext.tsx b/components/dashboard/src/contexts/ConfettiContext.tsx new file mode 100644 index 00000000000000..a3507f58a98542 --- /dev/null +++ b/components/dashboard/src/contexts/ConfettiContext.tsx @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2023 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License.AGPL.txt in the project root for license information. + */ + +import { lazy, createContext, FC, useMemo, useState, useContext, Suspense } from "react"; + +const Confetti = lazy(() => import(/* webpackPrefetch: true */ "react-confetti")); + +type ConfettiContextType = { + isConfettiDropping: boolean; + dropConfetti(): void; + hideConfetti(): void; +}; +const ConfettiContext = createContext({ + isConfettiDropping: false, + dropConfetti: () => undefined, + hideConfetti: () => undefined, +}); + +export const ConfettiContextProvider: FC = ({ children }) => { + const [isConfettiDropping, setIsConfettiDropping] = useState(false); + const value = useMemo(() => { + return { + isConfettiDropping: isConfettiDropping, + dropConfetti: () => setIsConfettiDropping(true), + hideConfetti: () => setIsConfettiDropping(false), + }; + }, [isConfettiDropping]); + + return ( + + {children} + {isConfettiDropping && ( + }> + + + )} + + ); +}; + +export const useConfetti = () => { + return useContext(ConfettiContext); +}; diff --git a/components/dashboard/src/index.tsx b/components/dashboard/src/index.tsx index f2f0de983823a0..b3dce8cde2f60d 100644 --- a/components/dashboard/src/index.tsx +++ b/components/dashboard/src/index.tsx @@ -25,6 +25,7 @@ import { isWebsiteSlug } from "./utils"; import "./index.css"; import { setupQueryClientProvider } from "./data/setup"; +import { ConfettiContextProvider } from "./contexts/ConfettiContext"; const bootApp = () => { // gitpod.io specific boot logic @@ -57,27 +58,29 @@ const bootApp = () => { ReactDOM.render( - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + , document.getElementById("root"), diff --git a/components/dashboard/src/onboarding/StepOrgInfo.tsx b/components/dashboard/src/onboarding/StepOrgInfo.tsx index d6fc0f49c2d543..fb6118ad35180f 100644 --- a/components/dashboard/src/onboarding/StepOrgInfo.tsx +++ b/components/dashboard/src/onboarding/StepOrgInfo.tsx @@ -15,6 +15,7 @@ import { getExplorationReasons } from "./exploration-reasons"; import { getJobRoleOptions, JOB_ROLE_OTHER } from "./job-roles"; import { OnboardingStep } from "./OnboardingStep"; import { getSignupGoalsOptions, SIGNUP_GOALS_OTHER } from "./signup-goals"; +import isURL from "validator/lib/isURL"; type Props = { user: User; @@ -127,7 +128,9 @@ export const StepOrgInfo: FC = ({ user, onComplete }) => { ]); const jobRoleError = useOnBlurError("Please select one", !!jobRole); - const isValid = jobRoleError.isValid && signupGoals.length > 0; + const websiteError = useOnBlurError("Please enter a valid url", !companyWebsite || isURL(companyWebsite)); + const isValid = + jobRoleError.isValid && websiteError.isValid && signupGoals.length > 0 && explorationReasons.length > 0; return ( = ({ user, onComplete }) => { diff --git a/components/dashboard/src/onboarding/UserOnboarding.tsx b/components/dashboard/src/onboarding/UserOnboarding.tsx index 4a3236524912b8..e2222a03f901ad 100644 --- a/components/dashboard/src/onboarding/UserOnboarding.tsx +++ b/components/dashboard/src/onboarding/UserOnboarding.tsx @@ -15,6 +15,7 @@ import { StepOrgInfo } from "./StepOrgInfo"; import { StepPersonalize } from "./StepPersonalize"; import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation"; import Alert from "../components/Alert"; +import { useConfetti } from "../contexts/ConfettiContext"; // This param is optionally present to force an onboarding flow // Can be used if other conditions aren't true, i.e. if user has already onboarded, but we want to force the flow again @@ -34,6 +35,7 @@ const UserOnboarding: FunctionComponent = ({ user }) => { const location = useLocation(); const { setUser } = useContext(UserContext); const updateUser = useUpdateCurrentUserMutation(); + const { dropConfetti } = useConfetti(); const [step, setStep] = useState(STEPS.ONE); const [completingError, setCompletingError] = useState(""); @@ -73,6 +75,7 @@ const UserOnboarding: FunctionComponent = ({ user }) => { try { const onboardedUser = await updateUser.mutateAsync(updates); + dropConfetti(); setUser(onboardedUser); // Look for the `onboarding=force` query param, and remove if present @@ -86,6 +89,7 @@ const UserOnboarding: FunctionComponent = ({ user }) => { }); } } catch (e) { + console.log("error caught", e); console.error(e); setCompletingError("There was a problem completing your onboarding"); } @@ -101,6 +105,7 @@ const UserOnboarding: FunctionComponent = ({ user }) => { location.pathname, location.search, setUser, + dropConfetti, updateUser, ], ); diff --git a/yarn.lock b/yarn.lock index d3004bf09e061d..159c7e853ef98b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3428,6 +3428,11 @@ resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz" integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== +"@types/validator@^13.7.12": + version "13.7.12" + resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.12.tgz#a285379b432cc8d103b69d223cbb159a253cf2f7" + integrity sha512-YVtyAPqpefU+Mm/qqnOANW6IkqKpCSrarcyV269C8MA8Ux0dbkEuQwM/4CjL47kVEM2LgBef/ETfkH+c6+moFA== + "@types/webpack-sources@*": version "3.2.0" resolved "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz" @@ -15129,6 +15134,13 @@ react-app-polyfill@^2.0.0: regenerator-runtime "^0.13.7" whatwg-fetch "^3.4.1" +react-confetti@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.1.0.tgz#03dc4340d955acd10b174dbf301f374a06e29ce6" + integrity sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw== + dependencies: + tween-functions "^1.2.0" + react-datepicker@^4.8.0: version "4.8.0" resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.8.0.tgz#11b8918d085a1ce4781eee4c8e4641b3cd592010" @@ -17740,6 +17752,11 @@ tunnel@0.0.6: resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +tween-functions@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff" + integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" @@ -18252,6 +18269,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.9.0: + version "13.9.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.9.0.tgz#33e7b85b604f3bbce9bb1a05d5c3e22e1c2ff855" + integrity sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA== + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz"