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 (
+
+
+
+
+
+ );
+}