Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New User Onboarding Flow UI #16501

Merged
merged 29 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cb6af81
disable rule for better dx - linter will catch
selfcontained Feb 21, 2023
e853507
add styles for other input types
selfcontained Feb 21, 2023
d4b7832
adding more options to input components
selfcontained Feb 21, 2023
6cc4f18
Building out onboarding form
selfcontained Feb 21, 2023
5307855
adjusting form
selfcontained Feb 21, 2023
f78b0b2
breaking onboarding flow into pieces
selfcontained Feb 22, 2023
066820a
adding personalize step
selfcontained Feb 22, 2023
74263bc
removing old code
selfcontained Feb 22, 2023
05ebdd7
removing un-needed code
selfcontained Feb 22, 2023
f078fcd
Plug in ThemeSelector component
selfcontained Feb 22, 2023
aa05a79
cleanup
selfcontained Feb 22, 2023
4ec1fd7
update onboarding logic
selfcontained Feb 22, 2023
02b3cd3
disable or members query if no current org present
selfcontained Feb 22, 2023
0f12ff5
adjusting where we save onboarding data
selfcontained Feb 22, 2023
273525e
make label optional
selfcontained Feb 23, 2023
189542b
change signup goals to an array
selfcontained Feb 23, 2023
f2d868e
change to company
selfcontained Feb 23, 2023
6b91b5d
adjust spacing/layout
selfcontained Feb 23, 2023
1a5d15c
Add additional comments for context
selfcontained Feb 23, 2023
a676453
rename isLoading to isSaving for clarity
selfcontained Feb 23, 2023
cae16dd
fix typo
selfcontained Feb 23, 2023
1cc9482
set type on button, don't submit if invalid
selfcontained Feb 23, 2023
3cb0b60
adding required on required fields
selfcontained Feb 23, 2023
c019664
fix typos
selfcontained Feb 23, 2023
9f526a8
Adjusting titles, styles and adding avatar
selfcontained Feb 23, 2023
3be00ab
account for new profile fields for tracking
selfcontained Feb 23, 2023
6a9e763
remove check for signupGoals
selfcontained Feb 23, 2023
f49cd2f
updating options
selfcontained Feb 23, 2023
452874b
Adding exploration reasons question
selfcontained Feb 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions components/dashboard/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { ContextURL, Team, User } from "@gitpod/gitpod-protocol";
import React, { FunctionComponent, useContext, useState } from "react";
import { Redirect, Route, Switch, useLocation } from "react-router";
import { Redirect, Route, Switch, useLocation, useParams } from "react-router";
import { AppNotifications } from "../AppNotifications";
import Menu from "../menu/Menu";
import OAuthClientApproval from "../OauthClientApproval";
Expand Down Expand Up @@ -102,6 +102,7 @@ export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) =>
const newCreateWsPage = useNewCreateWorkspacePage();
const location = useLocation();
const { newSignupFlow } = useFeatureFlags();
const search = new URLSearchParams(location.search);

// Prefix with `/#referrer` will specify an IDE for workspace
// We don't need to show IDE preference in this case
Expand All @@ -123,8 +124,8 @@ export const AppRoutes: FunctionComponent<AppRoutesProps> = ({ user, teams }) =>
return <WhatsNew onClose={() => setWhatsNewShown(false)} />;
}

// Placeholder for new signup flow
if (newSignupFlow && User.isOnboardingUser(user)) {
// Placeholder for new signup flow - check we make here tbd still
if (newSignupFlow && search.get("onboarding") === "1") {
return <UserOnboarding user={user} />;
}

Expand Down
96 changes: 96 additions & 0 deletions components/dashboard/src/components/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
filiptronicek marked this conversation as resolved.
Show resolved Hide resolved
* 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 classNames from "classnames";
import { FC, useCallback, useContext, useState } from "react";
import { ThemeContext } from "../theme-context";
import SelectableCardSolid from "./SelectableCardSolid";

type Theme = "light" | "dark" | "system";

type Props = {
className?: string;
};
// Theme Selection is purely clientside, so this component handles all state and writes to localStorage
export const ThemeSelector: FC<Props> = ({ className }) => {
const { setIsDark } = useContext(ThemeContext);
const [theme, setTheme] = useState<Theme>(localStorage.theme || "system");

const actuallySetTheme = useCallback(
(theme: Theme) => {
if (theme === "dark" || theme === "light") {
localStorage.theme = theme;
} else {
localStorage.removeItem("theme");
}
const isDark =
localStorage.theme === "dark" ||
(localStorage.theme !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
setIsDark(isDark);
setTheme(theme);
},
[setIsDark],
);

return (
<div className={classNames(className)}>
<h3>Theme</h3>
<p className="text-base text-gray-500 dark:text-gray-400">Early bird or night owl? Choose your side.</p>
<div className="mt-4 flex items-center justify-between flex-wrap">
<SelectableCardSolid
className="w-36 h-32 m-1"
title="Light"
selected={theme === "light"}
onClick={() => actuallySetTheme("light")}
>
<div className="flex-grow flex items-end p-1">
<svg width="112" height="64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 8a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8ZM0 32a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8ZM0 56a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8ZM40 6a6 6 0 0 1 6-6h60a6 6 0 0 1 6 6v28a6 6 0 0 1-6 6H46a6 6 0 0 1-6-6V6Z"
fill="#D6D3D1"
/>
</svg>
</div>
</SelectableCardSolid>
<SelectableCardSolid
className="w-36 h-32 m-1"
title="Dark"
selected={theme === "dark"}
onClick={() => actuallySetTheme("dark")}
>
<div className="flex-grow flex items-end p-1">
<svg width="112" height="64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 8a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8ZM0 32a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8ZM0 56a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8ZM40 6a6 6 0 0 1 6-6h60a6 6 0 0 1 6 6v28a6 6 0 0 1-6 6H46a6 6 0 0 1-6-6V6Z"
fill="#78716C"
/>
</svg>
</div>
</SelectableCardSolid>
<SelectableCardSolid
className="w-36 h-32 m-1"
title="System"
selected={theme === "system"}
onClick={() => actuallySetTheme("system")}
>
<div className="flex-grow flex items-end p-1">
<svg width="112" height="64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 8a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8ZM40 6a6 6 0 0 1 6-6h60a6 6 0 0 1 6 6v28a6 6 0 0 1-6 6H46a6 6 0 0 1-6-6V6Z"
fill="#D9D9D9"
/>
<path
d="M84 0h22a6 6 0 0 1 6 6v28a6 6 0 0 1-6 6H68L84 0ZM0 32a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8Z"
fill="#78716C"
/>
<path d="M0 56a8 8 0 0 1 8-8h16a8 8 0 1 1 0 16H8a8 8 0 0 1-8-8Z" fill="#D9D9D9" />
</svg>
</div>
</SelectableCardSolid>
</div>
</div>
);
};
5 changes: 3 additions & 2 deletions components/dashboard/src/components/forms/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ type Props = {
id?: string;
hint?: ReactNode;
error?: ReactNode;
className?: string;
};

export const InputField: FunctionComponent<Props> = memo(({ label, id, hint, error, children }) => {
export const InputField: FunctionComponent<Props> = memo(({ label, id, hint, error, className, children }) => {
return (
<div className="mt-4 flex flex-col space-y-2">
<div className={classNames("mt-4 flex flex-col space-y-2", className)}>
<label
className={classNames(
"text-sm font-semibold dark:text-gray-400",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { InputField } from "./InputField";

type Props = {
label: ReactNode;
value: string;
value?: string;
id?: string;
hint?: ReactNode;
error?: ReactNode;
Expand All @@ -31,6 +31,7 @@ export const SelectInputField: FunctionComponent<Props> = memo(
<SelectInput
id={elementId}
value={value}
className={error ? "error" : ""}
onChange={onChange}
disabled={disabled}
required={required}
Expand All @@ -44,7 +45,7 @@ export const SelectInputField: FunctionComponent<Props> = memo(
);

type SelectInputProps = {
value: string;
value?: string;
className?: string;
id?: string;
disabled?: boolean;
Expand Down
12 changes: 8 additions & 4 deletions components/dashboard/src/components/forms/TextInputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import { FunctionComponent, memo, ReactNode, useCallback } from "react";
import { useId } from "../../hooks/useId";
import { InputField } from "./InputField";

type TextInputFieldTypes = "text" | "password" | "email" | "url";

type Props = {
type?: "text" | "password";
type?: TextInputFieldTypes;
label: ReactNode;
value: string;
id?: string;
Expand All @@ -19,6 +21,7 @@ type Props = {
placeholder?: string;
disabled?: boolean;
required?: boolean;
containerClassName?: string;
onChange: (newValue: string) => void;
onBlur?: () => void;
};
Expand All @@ -34,22 +37,23 @@ export const TextInputField: FunctionComponent<Props> = memo(
error,
disabled = false,
required = false,
containerClassName,
onChange,
onBlur,
}) => {
const maybeId = useId();
const elementId = id || maybeId;

return (
<InputField id={elementId} label={label} hint={hint} error={error}>
<InputField id={elementId} label={label} hint={hint} error={error} className={containerClassName}>
<TextInput
id={elementId}
value={value}
type={type}
placeholder={placeholder}
disabled={disabled}
required={required}
className={error ? "border-red-500" : ""}
className={error ? "error" : ""}
onChange={onChange}
onBlur={onBlur}
/>
Expand All @@ -59,7 +63,7 @@ export const TextInputField: FunctionComponent<Props> = memo(
);

type TextInputProps = {
type?: "text" | "password";
type?: TextInputFieldTypes;
value: string;
className?: string;
id?: string;
Expand Down
19 changes: 19 additions & 0 deletions components/dashboard/src/data/current-user/update-mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* 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 { User } from "@gitpod/gitpod-protocol";
import { useMutation } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";

type UpdateCurrentUserArgs = Partial<User>;

export const useUpdateCurrentUserMutation = () => {
return useMutation({
mutationFn: async (partialUser: UpdateCurrentUserArgs) => {
return await getGitpodService().server.updateLoggedInUser(partialUser);
},
});
};
Comment on lines +13 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion (non-blocking): How about we split it out in chunks, it can be also useful in the future to reuse the similar functionality any other place

Suggested change
export const useUpdateCurrentUserMutation = () => {
return useMutation({
mutationFn: async (partialUser: UpdateCurrentUserArgs) => {
return await getGitpodService().server.updateLoggedInUser(partialUser);
},
});
};
export const useUpdateCurrentUserMutation = () => {
return useMutation({
mutationFn: updateUser,
});
};
const updateUser = async (partialUser: UpdateCurrentUserArgs) => {
return await getGitpodService().server.updateLoggedInUser(partialUser);
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, good thinking. I'm going to hold off on extracting these for now until there's another mutation that would use the function, but that would be a good way to share some logic.

8 changes: 7 additions & 1 deletion components/dashboard/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
input[type="tel"],
input[type="number"],
input[type="password"],
input[type="email"],
input[type="url"],
select {
@apply block w-56 text-gray-600 dark:text-gray-400 dark:bg-gray-800 bg-white rounded-md border border-gray-300 dark:border-gray-500 focus:border-gray-400 dark:focus:border-gray-400 focus:ring-0;
}
Expand All @@ -93,14 +95,18 @@
input[type="tel"]::placeholder,
input[type="number"]::placeholder,
input[type="search"]::placeholder,
input[type="password"]::placeholder {
input[type="password"]::placeholder,
input[type="email"]::placeholder,
input[type="url"]::placeholder {
@apply text-gray-400 dark:text-gray-500;
}
input[type="text"].error,
input[type="tel"].error,
input[type="number"].error,
input[type="search"].error,
input[type="password"].error,
input[type="email"].error,
input[type="url"].error,
select.error {
@apply border-gitpod-red dark:border-gitpod-red focus:border-gitpod-red dark:focus:border-gitpod-red;
}
Expand Down
57 changes: 57 additions & 0 deletions components/dashboard/src/onboarding/OnboardingStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* 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 { User } from "@gitpod/gitpod-protocol";
import { FC, FormEvent, useCallback } from "react";
import Alert from "../components/Alert";
import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation";

type Props = {
title: string;
subtitle: string;
isValid: boolean;
onUpdated(user: User): void;
prepareUpdates(): Partial<User>;
};
export const OnboardingStep: FC<Props> = ({ title, subtitle, isValid, children, prepareUpdates, onUpdated }) => {
const updateUser = useUpdateCurrentUserMutation();

const handleSubmit = useCallback(
async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

const updates = prepareUpdates();
try {
const updatedUser = await updateUser.mutateAsync(updates);
onUpdated(updatedUser);
} catch (e) {
console.error(e);
}
},
[onUpdated, prepareUpdates, updateUser],
);

return (
// flex classes here to account for an upcoming image on the right
<div className="flex flex-col items-center justify-center max-w-full">
<h1>{title}</h1>
<p>{subtitle}</p>

<form className="mt-8 max-w-md" onSubmit={handleSubmit}>
{/* Form contents provided as children */}
{children}

{updateUser.isError && <Alert type="error">There was a problem updating your profile</Alert>}

<div>
<button disabled={!isValid || updateUser.isLoading} className="w-full mt-8">
Continue
</button>
</div>
</form>
</div>
);
};
Loading