-
Notifications
You must be signed in to change notification settings - Fork 891
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
feat: Onboarding revamp #2073
feat: Onboarding revamp #2073
Conversation
…boarding-revamp
…boarding-revamp
…boarding-revamp
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 2 Ignored Deployments
|
Thank you for following the naming conventions for pull request titles! 🙏 |
packages/ui/Onboarding/components/InviteTeamMate.tsxIt's a good practice to not expose the actual error message to the end user. Instead, log the error message for debugging and show a generic error message to the user. try {
// code
} catch (error) {
console.error(error);
toast.error("An unexpected error occurred");
}
packages/ui/Onboarding/components/Connect.tsxInstead of using the // code
Consider using a more descriptive name for the const [loading, setLoadingState] = useState(false);
packages/ui/Onboarding/components/templates.ts
Create Issue function createQuestion(id: string, type: TSurveyQuestionType, headline: string, required: boolean, subheader: string = '', inputType: string = 'text'): QuestionType {
return {
id,
type,
headline,
required,
subheader,
inputType,
};
}
Create Issue const BUG_REPORT = 'Bug report 🐞';
const FEATURE_REQUEST = 'Feature Request 💡';
const SUBMITTED = 'submitted';
const EQUALS = 'equals';
const END = 'end';
apps/web/playwright/survey.spec.tsThere is a lot of repetition in the test code where similar actions are performed for different types of questions. This can be improved by creating a helper function that performs these actions and takes the necessary parameters as arguments. This will make the code more readable and easier to maintain. async function createQuestion(page, questionType, questionDetails) {
await page
.locator("div")
.filter({ hasText: new RegExp(`^${addQuestion}$`) })
.nth(1)
.click();
await page.getByRole("button", { name: questionType }).click();
await page.getByLabel("Question").fill(questionDetails.question);
// ... other common actions
}
|
try { | ||
if (!isValidEmail(email)) { | ||
toast.error("Invalid Email"); | ||
return; | ||
} | ||
await inviteTeamMateAction(team.id, email, "developer", inviteMessage); | ||
toast.success("Invite sent successful"); | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
toast.error(error.message); | ||
} else { | ||
toast.error("An unexpected error occurred"); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message is now logged for debugging and a generic error message is shown to the user.
try { | |
if (!isValidEmail(email)) { | |
toast.error("Invalid Email"); | |
return; | |
} | |
await inviteTeamMateAction(team.id, email, "developer", inviteMessage); | |
toast.success("Invite sent successful"); | |
} catch (error) { | |
if (error instanceof Error) { | |
toast.error(error.message); | |
} else { | |
toast.error("An unexpected error occurred"); | |
} | |
} | |
try { | |
if (!isValidEmail(email)) { | |
toast.error("Invalid Email"); | |
return; | |
} | |
await inviteTeamMateAction(team.id, email, "developer", inviteMessage); | |
toast.success("Invite sent successful"); | |
} catch (error) { | |
console.error(error); | |
toast.error("An unexpected error occurred"); | |
} |
useEffect(() => { | ||
const handleVisibilityChange = async () => { | ||
if (document.visibilityState === "visible") { | ||
const refetchedEnvironment = await fetchEnvironment(environment.id); | ||
if (!refetchedEnvironment) return; | ||
setLocalEnvironment(refetchedEnvironment); | ||
} | ||
}; | ||
document.addEventListener("visibilitychange", handleVisibilityChange); | ||
return () => { | ||
document.removeEventListener("visibilitychange", handleVisibilityChange); | ||
}; | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace the visibilitychange
event with a more efficient method such as polling or websockets to keep the environment data up-to-date.
useEffect(() => { | |
const handleVisibilityChange = async () => { | |
if (document.visibilityState === "visible") { | |
const refetchedEnvironment = await fetchEnvironment(environment.id); | |
if (!refetchedEnvironment) return; | |
setLocalEnvironment(refetchedEnvironment); | |
} | |
}; | |
document.addEventListener("visibilitychange", handleVisibilityChange); | |
return () => { | |
document.removeEventListener("visibilitychange", handleVisibilityChange); | |
}; | |
}, []); | |
useEffect(() => { | |
const interval = setInterval(async () => { | |
const refetchedEnvironment = await fetchEnvironment(environment.id); | |
if (!refetchedEnvironment) return; | |
setLocalEnvironment(refetchedEnvironment); | |
}, 5000); | |
return () => { | |
clearInterval(interval); | |
}; | |
}, []); |
|
||
export function Connect({ environment, webAppUrl }: { environment: TEnvironment; webAppUrl: string }) { | ||
const router = useRouter(); | ||
const [loading, setloading] = useState(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed the 'setloading' function to 'setLoadingState' to improve code readability.
const [loading, setloading] = useState(false); | |
const [loading, setLoadingState] = useState(false); |
apps/web/playwright/survey.spec.ts
Outdated
@@ -1,7 +1,7 @@ | |||
import { surveys, users } from "@/playwright/utils/mock"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The helper function 'createQuestion' is created to reduce the repetition of similar actions performed for different types of questions. This function takes 'page', 'questionType', and 'questionDetails' as parameters and performs the common actions.
import { surveys, users } from "@/playwright/utils/mock"; | |
async function createQuestion(page, questionType, questionDetails) { | |
await page | |
.locator("div") | |
.filter({ hasText: new RegExp(`^${addQuestion}`) }) | |
.nth(1) | |
.click(); | |
await page.getByRole("button", { name: questionType }).click(); | |
await page.getByLabel("Question").fill(questionDetails.question); | |
// ... other common actions | |
} |
Need to merge #2074 before testing this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey dhru! Thanks for shipping this :)
A few notes regarding code organisation:
- In the UI folder we only add components which we use in several locations within Formbricks. You have added all of the Onboarding specific components to the UI package. This creates quite some inconsistency, because you added Onboarding specific (hard-coded) content like in the PathwaySelect to the UI package.
Please make sure to separate and reorganize your code as follows:
UI Package: Everything we will likely reuse in other parts of Formbricks (like the new card) add here. Make sure to follow the current setup with having a folder which includes only the index.tsx
Component Folder within Onboarding: Keep all components you only need in the onboarding > components folder within the Formbricks app.
Please refactor this PR also taking into account the feedback I shared on Slack.
I'll push my half-finished refactored PathwaySelect.tsx
where the separation of concerns due to the current code organisation (mixing reusable components with specific content).
Please mark this as ready for review once your done and ping me 🤓
</div> | ||
</div> | ||
<div | ||
className="flex h-96 w-80 flex-col items-center justify-center rounded-2xl border border-slate-300 bg-white p-3 shadow-lg transition ease-in-out hover:scale-105" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
card code redundant, please refactor. we will use this kind of card a lot more going forward, pls add to UI package.
I've done it bc I was getting tired of making all changes on the cards twice.
Asking GPT to "refactor folllwing all major coding best practices" led to quite a good outcome:
setselectedPathway: (pathway: "link" | "in-app" | null) => void;
}
// Define a type for the pathway options to ensure type safety and reusability
type PathwayOptionType = "link" | "in-app";
interface PathwayOptionProps {
title: string;
description: string;
onSelect: () => void;
}
// Reusable PathwayOption component for each option
const PathwayOption: React.FC<PathwayOptionProps> = ({ title, description, onSelect }) => (
<div
className="flex h-96 w-80 cursor-pointer flex-col items-center justify-center rounded-2xl border border-slate-300 bg-white p-3 shadow-lg transition ease-in-out hover:scale-105"
onClick={onSelect}
role="button" // Improve accessibility
tabIndex={0} // Make it focusable
>
<div className="h-full w-full rounded-xl bg-gray-500">Image</div>
<div className="my-4 space-y-2">
<p className="text-xl font-medium text-slate-800">{title}</p>
<p className="text-sm text-slate-500">{description}</p>
</div>
</div>
);
export default function PathwaySelect({ setselectedPathway }: PathwaySelectProps) {
// Helper function to handle selection
const handleSelect = (pathway: PathwayOptionType) => {
localStorage.setItem("isNewUser", "true");
setselectedPathway(pathway);
};
return (
<div className="space-y-16 text-center">
<div className="space-y-4">
<p className="text-4xl font-medium text-slate-800">How would you like to start?</p>
<p className="text-sm text-slate-500">You can always use both types of surveys.</p>
</div>
<div className="flex space-x-8">
<PathwayOption
title="Link Surveys"
description="Create a new survey and share a link."
onSelect={() => handleSelect("link")}
/>
<PathwayOption
title="In-app Surveys"
description="Run a survey on a website or in-app."
onSelect={() => handleSelect("in-app")}
/>
</div>
</div>
);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
almost there :)
{renderOnboardingStep()} | ||
{iframeVisible && ( | ||
<iframe | ||
src="http://localhost:3000/s/clsui9a7x0000fbh5orp0g7c5" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mattinannt you need to create a survey with an animation or image bg locally and replace this URL to test the Link Survey onboarding path.
Before merging into prod, you need to add the link to the Onboarding Survey we have in the Formbricks Cloud account.
Now that I think about it, we should make use of the user identification in link surveys via URL to attribute the responses to the user who just signed up. We have the user email in the session object so it shouldn't be an issue. Let me try that first!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Dhruwang Onboarding looks great! :-) On the code side there are a few smaller things I would like to see changed for more consistency 😊
Can you please also solve the merge conflicts? Then we should be ready to merge this 😊💪
@@ -29,6 +29,12 @@ export default function SurveysList({ | |||
}: SurveysListProps) { | |||
const [filteredSurveys, setFilteredSurveys] = useState<TSurvey[]>(surveys); | |||
const [orientation, setOrientation] = useState("grid"); | |||
|
|||
useEffect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also call this at the end of the onboarding to remove them once instead of accessing the local storage on every survey list page call?
|
||
import { ConnectWithFormbricks } from "@/app/(app)/onboarding/components/inapp/ConnectWithFormbricks"; | ||
import { InviteTeamMate } from "@/app/(app)/onboarding/components/inapp/InviteTeamMate"; | ||
import Objective from "@/app/(app)/onboarding/components/inapp/SurveyObjective"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use module exports instead of default exports:
import { Objective } from "@/app/(app)/onboarding/components/inapp/SurveyObjective";
html: "You're all set up. Create your own survey to gather exactly the feedback you need :)", | ||
buttonLabel: "Create survey", | ||
buttonExternal: true, | ||
buttonUrl: "https://app.formbricks.com", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be dynamic for self hosters and should have the WEBAPP_URL
instead of app.formbricks.com
What does this PR do?
Fixes 1666
How should this be tested?
Checklist
Required
pnpm build
console.logs
git pull origin main
Appreciated