add new user controller

This commit is contained in:
Nate Kelley 2025-09-26 10:21:31 -06:00
parent 6599e2a97e
commit e5ad359a36
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
24 changed files with 233 additions and 16 deletions

View File

@ -9,6 +9,7 @@ import {
import { organizationQueryKeys } from '@/api/query_keys/organization';
import { userQueryKeys } from '@/api/query_keys/users';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { timeout } from '@/lib/timeout';
import type { RustApiError } from '../../errors';
import { useCreateOrganization } from '../organizations/queryRequests';
import {
@ -100,12 +101,12 @@ export const useInviteUser = () => {
};
export const useCreateUserOrganization = () => {
const { data: userResponse, refetch: refetchUserResponse } = useGetMyUserInfo({});
const { data: userResponse, refetch: refetchUserResponse } = useGetMyUserInfo();
const { mutateAsync: createOrganization } = useCreateOrganization();
const { mutateAsync: updateUserInfo } = useUpdateUser();
const onCreateUserOrganization = useMemoizedFn(
async ({ name, company }: { name: string; company: string }) => {
return useMutation({
mutationFn: async ({ name, company }: { name: string; company: string }) => {
const alreadyHasOrganization = !!userResponse?.organizations?.[0];
if (!alreadyHasOrganization) await createOrganization({ name: company });
if (userResponse) {
@ -113,13 +114,10 @@ export const useCreateUserOrganization = () => {
userId: userResponse.user.id,
name,
});
await refetchUserResponse();
}
await refetchUserResponse();
}
);
return onCreateUserOrganization;
await Promise.all([timeout(450), refetchUserResponse()]);
},
});
};
export const useGetSuggestedPrompts = (params: Parameters<typeof getSuggestedPrompts>[0]) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

View File

@ -41,8 +41,6 @@ import {
toggleInviteModal,
useInviteModalStore,
} from '@/context/GlobalStore/useInviteModalStore';
import { useGetSelectedAssetTypeLoose } from '@/context/Routes/useAppRoutes';
import { useWhyDidYouUpdate } from '@/hooks/useWhyDidYouUpdate';
import { cn } from '@/lib/classMerge';
import { InvitePeopleModal } from '../../modals/InvitePeopleModal';
import { SupportModal } from '../../modals/SupportModal';

View File

@ -75,7 +75,9 @@ const PageLayout: React.FC<
className
)}
>
<div className={cn('bg-background h-full overflow-hidden', floating && 'rounded border')}>
<div
className={cn('bg-page-background h-full overflow-hidden', floating && 'rounded border')}
>
{children}
</div>
</div>

View File

@ -0,0 +1,123 @@
import { useNavigate } from '@tanstack/react-router';
import { AnimatePresence, motion } from 'framer-motion';
import { useMemo, useState } from 'react';
import { useCreateUserOrganization } from '@/api/buster_rest/users';
import {
useGetUserBasicInfo,
useGetUserOrganization,
} from '@/api/buster_rest/users/useGetUserInfo';
import { Button } from '@/components/ui/buttons';
import { Input } from '@/components/ui/inputs';
import { Paragraph, Title } from '@/components/ui/typography';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useConfetti } from '@/hooks/useConfetti';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { inputHasText } from '@/lib/text';
export const NewUserController = () => {
const navigate = useNavigate();
const user = useGetUserBasicInfo();
const userOrganizations = useGetUserOrganization();
const { mutateAsync: onCreateUserOrganization, isPending: submitting } =
useCreateUserOrganization();
const [started, setStarted] = useState(false);
const [name, setName] = useState<string | undefined>(user?.name);
const [company, setCompany] = useState<string | undefined>(userOrganizations?.name);
const { fireConfetti } = useConfetti();
const { openInfoMessage } = useBusterNotifications();
const canSubmit = useMemo(() => inputHasText(name) && inputHasText(company), [name, company]);
const handleSubmit = useMemoizedFn(async () => {
if (!canSubmit || !name || !company) {
openInfoMessage('Please fill in all fields');
return;
}
try {
await onCreateUserOrganization({
name,
company,
});
fireConfetti();
await navigate({
to: '/app/home',
replace: true,
});
} catch (error) {
//
}
});
return (
<AnimatePresence mode="wait" initial={false}>
{!started && (
<motion.div
initial={{ opacity: 0, y: -10, filter: 'blur(4px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
exit={{
opacity: 0,
y: 10,
filter: 'blur(4px)',
transition: {
duration: 0.2,
opacity: { duration: 0.175 },
filter: { duration: 0.18 },
},
}}
key={'no-started'}
className="flex h-full w-full flex-col items-start justify-center space-y-5 p-12"
>
<Title as={'h3'}>Welcome to Buster</Title>
<Paragraph variant="secondary">
With Buster, you can ask data questions in plain english & instantly get back data.
</Paragraph>
<Button variant="black" onClick={() => setStarted(true)}>
Get Started
</Button>
</motion.div>
)}
{started && (
<motion.div
initial={{ opacity: 0, y: -30 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 0 }}
transition={{ duration: 0.2 }}
key={'started'}
className="flex h-full w-full flex-col items-start justify-center space-y-5 p-12"
>
<Title as={'h4'}>Tell us about yourself</Title>
<Input
placeholder="What is your full name"
className="w-full"
value={name || ''}
name="name"
onChange={(e) => setName(e.target.value)}
onPressEnter={handleSubmit}
/>
<Input
placeholder="What is the name of your company"
className="w-full"
name="company"
disabled={!!userOrganizations?.name}
value={company || ''}
onChange={(e) => setCompany(e.target.value)}
onPressEnter={handleSubmit}
/>
<Button
variant="black"
loading={submitting}
onClick={async () => {
handleSubmit();
}}
>
Create your account
</Button>
</motion.div>
)}
</AnimatePresence>
);
};

View File

@ -0,0 +1 @@
export * from './NewUserController';

View File

@ -29,10 +29,12 @@ import { Route as EmbedMetricMetricIdRouteImport } from './routes/embed/metric.$
import { Route as EmbedDashboardDashboardIdRouteImport } from './routes/embed/dashboard.$dashboardId'
import { Route as AppSettingsRestricted_layoutRouteImport } from './routes/app/_settings/_restricted_layout'
import { Route as AppSettingsPermissionsRouteImport } from './routes/app/_settings/_permissions'
import { Route as AppAppNewUserRouteImport } from './routes/app/_app/new-user'
import { Route as AppAppHomeRouteImport } from './routes/app/_app/home'
import { Route as AppAppAssetRouteImport } from './routes/app/_app/_asset'
import { Route as AppSettingsSettingsIndexRouteImport } from './routes/app/_settings/settings.index'
import { Route as AppAppReportsIndexRouteImport } from './routes/app/_app/reports.index'
import { Route as AppAppNewUserIndexRouteImport } from './routes/app/_app/new-user/index'
import { Route as AppAppMetricsIndexRouteImport } from './routes/app/_app/metrics.index'
import { Route as AppAppLogsIndexRouteImport } from './routes/app/_app/logs.index'
import { Route as AppAppDatasetsIndexRouteImport } from './routes/app/_app/datasets.index'
@ -245,6 +247,11 @@ const AppSettingsPermissionsRoute = AppSettingsPermissionsRouteImport.update({
id: '/_permissions',
getParentRoute: () => AppSettingsRoute,
} as any)
const AppAppNewUserRoute = AppAppNewUserRouteImport.update({
id: '/new-user',
path: '/new-user',
getParentRoute: () => AppAppRoute,
} as any)
const AppAppHomeRoute = AppAppHomeRouteImport.update({
id: '/home',
path: '/home',
@ -265,6 +272,11 @@ const AppAppReportsIndexRoute = AppAppReportsIndexRouteImport.update({
path: '/reports/',
getParentRoute: () => AppAppRoute,
} as any)
const AppAppNewUserIndexRoute = AppAppNewUserIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AppAppNewUserRoute,
} as any)
const AppAppMetricsIndexRoute = AppAppMetricsIndexRouteImport.update({
id: '/metrics/',
path: '/metrics/',
@ -945,6 +957,7 @@ export interface FileRoutesByFullPath {
'/info/getting-started': typeof InfoGettingStartedRoute
'/app/': typeof AppIndexRoute
'/app/home': typeof AppAppHomeRoute
'/app/new-user': typeof AppAppNewUserRouteWithChildren
'/embed/dashboard/$dashboardId': typeof EmbedDashboardDashboardIdRoute
'/embed/metric/$metricId': typeof EmbedMetricMetricIdRoute
'/embed/report/$reportId': typeof EmbedReportReportIdRoute
@ -955,6 +968,7 @@ export interface FileRoutesByFullPath {
'/app/datasets': typeof AppAppDatasetsIndexRoute
'/app/logs': typeof AppAppLogsIndexRoute
'/app/metrics': typeof AppAppMetricsIndexRoute
'/app/new-user/': typeof AppAppNewUserIndexRoute
'/app/reports': typeof AppAppReportsIndexRoute
'/app/settings': typeof AppSettingsSettingsIndexRoute
'/app/chats/$chatId': typeof AppAppAssetChatsChatIdRouteWithChildren
@ -1062,6 +1076,7 @@ export interface FileRoutesByTo {
'/app/datasets': typeof AppAppDatasetsIndexRoute
'/app/logs': typeof AppAppLogsIndexRoute
'/app/metrics': typeof AppAppMetricsIndexRoute
'/app/new-user': typeof AppAppNewUserIndexRoute
'/app/reports': typeof AppAppReportsIndexRoute
'/app/settings': typeof AppSettingsSettingsIndexRoute
'/app/datasets/$datasetId/editor': typeof AppAppDatasetsDatasetIdEditorRoute
@ -1147,6 +1162,7 @@ export interface FileRoutesById {
'/app/': typeof AppIndexRoute
'/app/_app/_asset': typeof AppAppAssetRouteWithChildren
'/app/_app/home': typeof AppAppHomeRoute
'/app/_app/new-user': typeof AppAppNewUserRouteWithChildren
'/app/_settings/_permissions': typeof AppSettingsPermissionsRouteWithChildren
'/app/_settings/_restricted_layout': typeof AppSettingsRestricted_layoutRouteWithChildren
'/embed/dashboard/$dashboardId': typeof EmbedDashboardDashboardIdRoute
@ -1160,6 +1176,7 @@ export interface FileRoutesById {
'/app/_app/datasets/': typeof AppAppDatasetsIndexRoute
'/app/_app/logs/': typeof AppAppLogsIndexRoute
'/app/_app/metrics/': typeof AppAppMetricsIndexRoute
'/app/_app/new-user/': typeof AppAppNewUserIndexRoute
'/app/_app/reports/': typeof AppAppReportsIndexRoute
'/app/_settings/settings/': typeof AppSettingsSettingsIndexRoute
'/app/_app/_asset/chats/$chatId': typeof AppAppAssetChatsChatIdRouteWithChildren
@ -1270,6 +1287,7 @@ export interface FileRouteTypes {
| '/info/getting-started'
| '/app/'
| '/app/home'
| '/app/new-user'
| '/embed/dashboard/$dashboardId'
| '/embed/metric/$metricId'
| '/embed/report/$reportId'
@ -1280,6 +1298,7 @@ export interface FileRouteTypes {
| '/app/datasets'
| '/app/logs'
| '/app/metrics'
| '/app/new-user/'
| '/app/reports'
| '/app/settings'
| '/app/chats/$chatId'
@ -1387,6 +1406,7 @@ export interface FileRouteTypes {
| '/app/datasets'
| '/app/logs'
| '/app/metrics'
| '/app/new-user'
| '/app/reports'
| '/app/settings'
| '/app/datasets/$datasetId/editor'
@ -1471,6 +1491,7 @@ export interface FileRouteTypes {
| '/app/'
| '/app/_app/_asset'
| '/app/_app/home'
| '/app/_app/new-user'
| '/app/_settings/_permissions'
| '/app/_settings/_restricted_layout'
| '/embed/dashboard/$dashboardId'
@ -1484,6 +1505,7 @@ export interface FileRouteTypes {
| '/app/_app/datasets/'
| '/app/_app/logs/'
| '/app/_app/metrics/'
| '/app/_app/new-user/'
| '/app/_app/reports/'
| '/app/_settings/settings/'
| '/app/_app/_asset/chats/$chatId'
@ -1736,6 +1758,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppSettingsPermissionsRouteImport
parentRoute: typeof AppSettingsRoute
}
'/app/_app/new-user': {
id: '/app/_app/new-user'
path: '/new-user'
fullPath: '/app/new-user'
preLoaderRoute: typeof AppAppNewUserRouteImport
parentRoute: typeof AppAppRoute
}
'/app/_app/home': {
id: '/app/_app/home'
path: '/home'
@ -1764,6 +1793,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppAppReportsIndexRouteImport
parentRoute: typeof AppAppRoute
}
'/app/_app/new-user/': {
id: '/app/_app/new-user/'
path: '/'
fullPath: '/app/new-user/'
preLoaderRoute: typeof AppAppNewUserIndexRouteImport
parentRoute: typeof AppAppNewUserRoute
}
'/app/_app/metrics/': {
id: '/app/_app/metrics/'
path: '/metrics'
@ -2912,6 +2948,18 @@ const AppAppAssetRouteWithChildren = AppAppAssetRoute._addFileChildren(
AppAppAssetRouteChildren,
)
interface AppAppNewUserRouteChildren {
AppAppNewUserIndexRoute: typeof AppAppNewUserIndexRoute
}
const AppAppNewUserRouteChildren: AppAppNewUserRouteChildren = {
AppAppNewUserIndexRoute: AppAppNewUserIndexRoute,
}
const AppAppNewUserRouteWithChildren = AppAppNewUserRoute._addFileChildren(
AppAppNewUserRouteChildren,
)
interface AppAppDatasetsDatasetIdPermissionsRouteChildren {
AppAppDatasetsDatasetIdPermissionsDatasetGroupsRoute: typeof AppAppDatasetsDatasetIdPermissionsDatasetGroupsRoute
AppAppDatasetsDatasetIdPermissionsOverviewRoute: typeof AppAppDatasetsDatasetIdPermissionsOverviewRoute
@ -2961,6 +3009,7 @@ const AppAppDatasetsDatasetIdRouteWithChildren =
interface AppAppRouteChildren {
AppAppAssetRoute: typeof AppAppAssetRouteWithChildren
AppAppHomeRoute: typeof AppAppHomeRoute
AppAppNewUserRoute: typeof AppAppNewUserRouteWithChildren
AppAppDatasetsDatasetIdRoute: typeof AppAppDatasetsDatasetIdRouteWithChildren
AppAppChatsIndexRoute: typeof AppAppChatsIndexRoute
AppAppCollectionsIndexRoute: typeof AppAppCollectionsIndexRoute
@ -2974,6 +3023,7 @@ interface AppAppRouteChildren {
const AppAppRouteChildren: AppAppRouteChildren = {
AppAppAssetRoute: AppAppAssetRouteWithChildren,
AppAppHomeRoute: AppAppHomeRoute,
AppAppNewUserRoute: AppAppNewUserRouteWithChildren,
AppAppDatasetsDatasetIdRoute: AppAppDatasetsDatasetIdRouteWithChildren,
AppAppChatsIndexRoute: AppAppChatsIndexRoute,
AppAppCollectionsIndexRoute: AppAppCollectionsIndexRoute,

View File

@ -30,15 +30,20 @@ export const Route = createFileRoute('/app')({
if (user && user?.organizations?.length === 0) {
throw redirect({ href: BUSTER_SIGN_UP_URL, replace: true, statusCode: 307 });
}
if (user && !user.user.name) {
throw redirect({ to: '/app/new-user', replace: true, statusCode: 307 });
}
return {
supabaseSession,
};
} catch (error) {
// Re-throw redirect Responses so the router can handle them (e.g., getting-started)
if (error instanceof Response) {
throw error;
if (error instanceof Response && error.status === 307) {
return {
supabaseSession,
};
}
console.error('Error in app route loader:', error);
throw redirect({ to: '/auth/login', replace: true, statusCode: 307 });
}
},

View File

@ -0,0 +1,30 @@
import { createFileRoute, Outlet } from '@tanstack/react-router';
import NewUserWelcome from '@/assets/png/new-user-welcome.png';
export const Route = createFileRoute('/app/_app/new-user')({
component: RouteComponent,
});
function RouteComponent() {
return (
<section className="h-[100vh]">
<div className="flex h-[100vh] items-center">
<div className="mx-auto flex min-h-full w-full">
<div className="hidden w-1/2 min-w-[400px] max-w-[650px] md:flex">
<Outlet />
</div>
<div className="relative flex w-full flex-col items-center justify-center">
<div
className="w-full bg-backgroud"
style={{
height: '85vh',
background: `url(${NewUserWelcome}) no-repeat left center`,
backgroundSize: 'cover',
}}
/>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router';
import { NewUserController } from '@/controllers/NewUserController';
export const Route = createFileRoute('/app/_app/new-user/')({
component: RouteComponent,
});
function RouteComponent() {
return <NewUserController />;
}