Route client users to portal experience

This commit is contained in:
Flatlogic Bot 2026-06-09 13:31:08 +00:00
parent a6aef66d1e
commit 928e45b084
3 changed files with 78 additions and 30 deletions

View File

@ -62,6 +62,7 @@ export default function LayoutAuthenticated({ children, permission }: Props) {
const router = useRouter();
const { token, currentUser } = useAppSelector((state) => state.auth);
const [isAsideOpen, setIsAsideOpen] = useState(false);
const isClientUser = currentUser?.app_role?.name === 'Client';
function getLocalToken() {
if (typeof window === 'undefined') {
@ -109,6 +110,21 @@ export default function LayoutAuthenticated({ children, permission }: Props) {
}
}, [currentUser, permission]);
useEffect(() => {
if (!isClientUser) {
return;
}
if (
router.pathname === '/client-portal' ||
router.pathname === '/profile'
) {
return;
}
router.push('/client-portal');
}, [isClientUser, router.pathname]);
useEffect(() => {
function closeAside() {
setIsAsideOpen(false);
@ -122,6 +138,10 @@ export default function LayoutAuthenticated({ children, permission }: Props) {
}, [router.events]);
const visibleNavItems = navItems.filter((item) => {
if (isClientUser) {
return item.href === '/client-portal' || item.href === '/profile';
}
if (!item.permission) {
return true;
}
@ -170,8 +190,9 @@ export default function LayoutAuthenticated({ children, permission }: Props) {
Today
</p>
<p className='mt-2 text-sm leading-6 text-[#72798a]'>
Prepare, follow up, and keep every coaching relationship warm
between sessions.
{isClientUser
? 'Review shared notes, commitments, and resources from your coach.'
: 'Prepare, follow up, and keep every coaching relationship warm between sessions.'}
</p>
</div>
@ -242,7 +263,9 @@ export default function LayoutAuthenticated({ children, permission }: Props) {
Workspace status
</p>
<p className='mt-1 text-sm text-[#72798a]'>
Review sessions, clients, tasks, and shared client materials.
{isClientUser
? 'Review your commitments, notes, and shared resources.'
: 'Review sessions, clients, tasks, and shared client materials.'}
</p>
</div>
<Link

View File

@ -13,6 +13,7 @@ import BaseIcon from '../components/BaseIcon';
import SectionMain from '../components/SectionMain';
import { getPageTitle } from '../config';
import LayoutAuthenticated from '../layouts/Authenticated';
import { useAppSelector } from '../stores/hooks';
type PortalClient = {
id: string;
@ -45,26 +46,38 @@ function Panel({
}
const ClientPortal = () => {
const { currentUser } = useAppSelector((state) => state.auth);
const [clients, setClients] = React.useState<
Array<{ id: string; name: string }>
Array<{ id: string; name: string; email?: string }>
>([]);
const [clientId, setClientId] = React.useState('');
const [portalClient, setPortalClient] = React.useState<PortalClient | null>(
null,
);
const isClientUser = currentUser?.app_role?.name === 'Client';
React.useEffect(() => {
async function loadClients() {
const response = await axios.get('/coaching/clients');
setClients(response.data);
if (response.data.length > 0) {
const currentClient = response.data.find((client) => {
return client.email === currentUser?.email;
});
if (isClientUser && currentClient) {
setClientId(currentClient.id);
return;
}
setClientId(response.data[0].id);
}
}
loadClients();
}, []);
}, [currentUser?.email, isClientUser]);
React.useEffect(() => {
async function loadPortal() {
@ -86,7 +99,11 @@ const ClientPortal = () => {
</Head>
<SectionMain>
<div className='mx-auto max-w-7xl'>
<div className='mb-4 grid gap-4 xl:grid-cols-[0.95fr_1.05fr]'>
<div
className={`mb-4 grid gap-4 ${
isClientUser ? '' : 'xl:grid-cols-[0.95fr_1.05fr]'
}`}
>
<div className='rounded-lg bg-[#19192d] p-5 text-white'>
<div className='flex items-center gap-3 text-[#b17a1e]'>
<BaseIcon path={mdiAccountCircle} size={18} />
@ -95,33 +112,36 @@ const ClientPortal = () => {
</span>
</div>
<h1 className='mt-3 text-xl font-semibold'>
Client portal preview
{isClientUser ? 'Your client portal' : 'Client portal preview'}
</h1>
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
Preview the client-facing workspace with shared notes,
commitments, resources, and a pre-session reflection prompt.
{isClientUser
? 'Review shared notes, commitments, resources, and reflections for your coaching work.'
: 'Preview the client-facing workspace with shared notes, commitments, resources, and a pre-session reflection prompt.'}
</p>
</div>
<Panel className='p-4'>
<label className='block text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
Preview as client
</label>
<select
value={clientId}
onChange={(event) => setClientId(event.target.value)}
className='mt-4 w-full rounded-lg border border-[#19192d]/10 bg-white px-3 py-2 text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'
>
{clients.map((client) => (
<option key={client.id} value={client.id}>
{client.name}
</option>
))}
</select>
<p className='mt-4 text-sm leading-6 text-[#72798a]'>
MVP note: this is still a coach-visible preview. Final client
access should be a client role or magic-link route.
</p>
</Panel>
{!isClientUser && (
<Panel className='p-4'>
<label className='block text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
Preview as client
</label>
<select
value={clientId}
onChange={(event) => setClientId(event.target.value)}
className='mt-4 w-full rounded-lg border border-[#19192d]/10 bg-white px-3 py-2 text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'
>
{clients.map((client) => (
<option key={client.id} value={client.id}>
{client.name}
</option>
))}
</select>
<p className='mt-4 text-sm leading-6 text-[#72798a]'>
Coaches can preview what each client sees before sharing
notes, commitments, or resources.
</p>
</Panel>
)}
</div>
{portalClient && (

View File

@ -120,9 +120,14 @@ export default function Login() {
useEffect(() => {
if (currentUser?.id) {
if (currentUser?.app_role?.name === 'Client') {
router.push('/client-portal');
return;
}
router.push('/dashboard');
}
}, [currentUser?.id, router]);
}, [currentUser?.id, currentUser?.app_role?.name, router]);
useEffect(() => {
if (errorMessage) {