From 09f6f1b571e44afcea4f4c30fd0765a72e87b378 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Fri, 27 Mar 2026 17:20:02 +0530 Subject: [PATCH] feat: profile page revamp --- web/apps/client-demo/src/Router.tsx | 2 + web/apps/client-demo/src/pages/Settings.tsx | 3 +- .../src/pages/settings/Profile.tsx | 5 + web/sdk/react/index.ts | 1 + web/sdk/react/views-new/profile/index.ts | 1 + .../views-new/profile/profile-view.module.css | 8 + .../react/views-new/profile/profile-view.tsx | 185 ++++++++++++++++++ 7 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 web/apps/client-demo/src/pages/settings/Profile.tsx create mode 100644 web/sdk/react/views-new/profile/index.ts create mode 100644 web/sdk/react/views-new/profile/profile-view.module.css create mode 100644 web/sdk/react/views-new/profile/profile-view.tsx diff --git a/web/apps/client-demo/src/Router.tsx b/web/apps/client-demo/src/Router.tsx index c7798371e..7848cf4bb 100644 --- a/web/apps/client-demo/src/Router.tsx +++ b/web/apps/client-demo/src/Router.tsx @@ -10,6 +10,7 @@ import Organization from './pages/Organization'; import Settings from './pages/Settings'; import General from './pages/settings/General'; import Preferences from './pages/settings/Preferences'; +import Profile from './pages/settings/Profile'; function Router() { return ( @@ -26,6 +27,7 @@ function Router() { }> } /> } /> + } /> } /> diff --git a/web/apps/client-demo/src/pages/Settings.tsx b/web/apps/client-demo/src/pages/Settings.tsx index f87964cf7..926920e41 100644 --- a/web/apps/client-demo/src/pages/Settings.tsx +++ b/web/apps/client-demo/src/pages/Settings.tsx @@ -5,7 +5,8 @@ import { useFrontier } from '@raystack/frontier/react'; const NAV_ITEMS = [ { label: 'General', path: 'general' }, - { label: 'Preferences', path: 'preferences' } + { label: 'Preferences', path: 'preferences' }, + { label: 'Profile', path: 'profile' } ]; export default function Settings() { diff --git a/web/apps/client-demo/src/pages/settings/Profile.tsx b/web/apps/client-demo/src/pages/settings/Profile.tsx new file mode 100644 index 000000000..37cc67ad4 --- /dev/null +++ b/web/apps/client-demo/src/pages/settings/Profile.tsx @@ -0,0 +1,5 @@ +import { ProfileView } from '@raystack/frontier/react'; + +export default function Profile() { + return ; +} diff --git a/web/sdk/react/index.ts b/web/sdk/react/index.ts index eb15cbb30..1d6d9deb7 100644 --- a/web/sdk/react/index.ts +++ b/web/sdk/react/index.ts @@ -31,6 +31,7 @@ export { ViewContainer } from './components/view-container'; export { ViewHeader } from './components/view-header'; export { GeneralView } from './views-new/general'; export { PreferencesView, PreferenceRow } from './views-new/preferences'; +export { ProfileView } from './views-new/profile'; export type { FrontierClientOptions, diff --git a/web/sdk/react/views-new/profile/index.ts b/web/sdk/react/views-new/profile/index.ts new file mode 100644 index 000000000..b58061c0d --- /dev/null +++ b/web/sdk/react/views-new/profile/index.ts @@ -0,0 +1 @@ +export { ProfileView } from './profile-view'; diff --git a/web/sdk/react/views-new/profile/profile-view.module.css b/web/sdk/react/views-new/profile/profile-view.module.css new file mode 100644 index 000000000..77cebe778 --- /dev/null +++ b/web/sdk/react/views-new/profile/profile-view.module.css @@ -0,0 +1,8 @@ +.section { + padding: var(--rs-space-9) 0; + border-bottom: 1px solid var(--rs-color-border-base-primary); +} + +.formFields { + max-width: 320px; +} diff --git a/web/sdk/react/views-new/profile/profile-view.tsx b/web/sdk/react/views-new/profile/profile-view.tsx new file mode 100644 index 000000000..77e614e71 --- /dev/null +++ b/web/sdk/react/views-new/profile/profile-view.tsx @@ -0,0 +1,185 @@ +'use client'; + +import { useEffect } from 'react'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useForm } from 'react-hook-form'; +import * as yup from 'yup'; +import { + createConnectQueryKey, + useMutation +} from '@connectrpc/connect-query'; +import { FrontierServiceQueries } from '@raystack/proton/frontier'; +import { useQueryClient } from '@tanstack/react-query'; +import { + Button, + Flex, + InputField, + Skeleton, + toastManager +} from '@raystack/apsara-v1'; +import { useFrontier } from '../../contexts/FrontierContext'; +import { ViewContainer } from '../../components/view-container'; +import { ViewHeader } from '../../components/view-header'; +import { ImageUpload } from '../../components/image-upload'; +import styles from './profile-view.module.css'; + +const profileSchema = yup + .object({ + avatar: yup.string().optional(), + title: yup + .string() + .required('Name is required') + .min(2, 'Name must be at least 2 characters') + .matches( + /^[\p{L} .'-]+$/u, + 'Name can only contain letters, spaces, periods, hyphens, and apostrophes' + ) + .matches(/^\p{L}/u, 'Name must start with a letter') + .matches( + /^\p{L}[\p{L} .'-]*\p{L}$|^\p{L}$/u, + 'Name must end with a letter' + ) + .matches(/^(?!.* {2}).*$/, 'Name cannot have consecutive spaces') + .matches(/^(?!.* [^\p{L}]).*$/u, 'Spaces must be followed by a letter') + .matches(/^(?!.*-[^\p{L}]).*$/u, 'Hyphens must be followed by a letter') + .matches( + /^(?!.*'[^\p{L}]).*$/u, + 'Apostrophes must be followed by a letter' + ), + email: yup.string().email().required() + }) + .required(); + +type FormData = yup.InferType; + +export function ProfileView() { + const { user, isUserLoading: isLoading } = useFrontier(); + const queryClient = useQueryClient(); + + const { mutateAsync: updateCurrentUser } = useMutation( + FrontierServiceQueries.updateCurrentUser, + { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: createConnectQueryKey({ + schema: FrontierServiceQueries.getCurrentUser, + cardinality: 'finite' + }) + }); + } + } + ); + + const { + reset, + register, + handleSubmit, + watch, + setValue, + formState: { errors, isSubmitting, isDirty } + } = useForm({ + resolver: yupResolver(profileSchema) + }); + + useEffect(() => { + reset(user, { keepDirtyValues: true }); + }, [user, reset]); + + async function onSubmit(data: FormData) { + try { + if (!user?.id) return; + await updateCurrentUser({ body: data }); + toastManager.add({ title: 'Updated user', type: 'success' }); + } catch (err: unknown) { + toastManager.add({ + title: 'Something went wrong', + description: err instanceof Error ? err.message : 'Failed to update', + type: 'error' + }); + } + } + + return ( + + + +
+ + {/* Avatar section */} + + {isLoading ? ( + + + + + ) : ( + + setValue('avatar', value, { shouldDirty: true }) + } + description="Pick a profile picture for your avatar. Max size: 5 Mb" + initials={user?.title?.[0]} + data-test-id="frontier-sdk-profile-avatar-upload" + /> + )} + + + {/* Form section */} + + + {isLoading ? ( + <> + + + + ) : ( + <> + + + + )} + + + {isLoading ? ( + + ) : ( + + )} + + +
+
+ ); +}