Build dedicated client portal experience
This commit is contained in:
parent
928e45b084
commit
fb01c003c6
@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
mdiAccountCircle,
|
|
||||||
mdiBookOpenVariant,
|
mdiBookOpenVariant,
|
||||||
|
mdiCalendarClock,
|
||||||
|
mdiCheck,
|
||||||
mdiCheckCircleOutline,
|
mdiCheckCircleOutline,
|
||||||
mdiChevronRight,
|
mdiChevronRight,
|
||||||
|
mdiFlagVariantOutline,
|
||||||
mdiMessageReplyTextOutline,
|
mdiMessageReplyTextOutline,
|
||||||
|
mdiPencilOutline,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
@ -18,7 +21,9 @@ import { useAppSelector } from '../stores/hooks';
|
|||||||
type PortalClient = {
|
type PortalClient = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
email?: string;
|
||||||
goals?: string;
|
goals?: string;
|
||||||
|
next_session_at?: string;
|
||||||
sessions?: Array<{ id: string; title: string; shared_client_notes?: string }>;
|
sessions?: Array<{ id: string; title: string; shared_client_notes?: string }>;
|
||||||
action_items?: Array<{ id: string; title: string; status: string }>;
|
action_items?: Array<{ id: string; title: string; status: string }>;
|
||||||
resources?: Array<{
|
resources?: Array<{
|
||||||
@ -45,15 +50,31 @@ function Panel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(value?: string) {
|
||||||
|
if (!value) {
|
||||||
|
return 'Not scheduled';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('en', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).format(new Date(value));
|
||||||
|
}
|
||||||
|
|
||||||
const ClientPortal = () => {
|
const ClientPortal = () => {
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const [clients, setClients] = React.useState<
|
const [clients, setClients] = React.useState<PortalClient[]>([]);
|
||||||
Array<{ id: string; name: string; email?: string }>
|
|
||||||
>([]);
|
|
||||||
const [clientId, setClientId] = React.useState('');
|
const [clientId, setClientId] = React.useState('');
|
||||||
const [portalClient, setPortalClient] = React.useState<PortalClient | null>(
|
const [portalClient, setPortalClient] = React.useState<PortalClient | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [completedItems, setCompletedItems] = React.useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [reflection, setReflection] = React.useState('');
|
||||||
|
const [reflectionSaved, setReflectionSaved] = React.useState(false);
|
||||||
|
|
||||||
const isClientUser = currentUser?.app_role?.name === 'Client';
|
const isClientUser = currentUser?.app_role?.name === 'Client';
|
||||||
|
|
||||||
@ -87,11 +108,35 @@ const ClientPortal = () => {
|
|||||||
|
|
||||||
const response = await axios.get(`/coaching/client-portal/${clientId}`);
|
const response = await axios.get(`/coaching/client-portal/${clientId}`);
|
||||||
setPortalClient(response.data);
|
setPortalClient(response.data);
|
||||||
|
setCompletedItems(new Set());
|
||||||
|
setReflection('');
|
||||||
|
setReflectionSaved(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPortal();
|
loadPortal();
|
||||||
}, [clientId]);
|
}, [clientId]);
|
||||||
|
|
||||||
|
function toggleItem(itemId: string) {
|
||||||
|
setCompletedItems((current) => {
|
||||||
|
const next = new Set(current);
|
||||||
|
if (next.has(itemId)) {
|
||||||
|
next.delete(itemId);
|
||||||
|
} else {
|
||||||
|
next.add(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const openItems = (portalClient?.action_items || []).filter((item) => {
|
||||||
|
return item.status !== 'done' && !completedItems.has(item.id);
|
||||||
|
});
|
||||||
|
const finishedCount =
|
||||||
|
(portalClient?.action_items || []).filter((item) => item.status === 'done')
|
||||||
|
.length + completedItems.size;
|
||||||
|
const latestSession = portalClient?.sessions?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@ -99,176 +144,272 @@ const ClientPortal = () => {
|
|||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<div className='mx-auto max-w-7xl'>
|
<div className='mx-auto max-w-7xl'>
|
||||||
<div
|
{!isClientUser && (
|
||||||
className={`mb-4 grid gap-4 ${
|
<Panel className='mb-4 p-4'>
|
||||||
isClientUser ? '' : 'xl:grid-cols-[0.95fr_1.05fr]'
|
<label className='block text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
||||||
}`}
|
Preview as client
|
||||||
>
|
</label>
|
||||||
<div className='rounded-lg bg-[#19192d] p-5 text-white'>
|
<select
|
||||||
<div className='flex items-center gap-3 text-[#b17a1e]'>
|
value={clientId}
|
||||||
<BaseIcon path={mdiAccountCircle} size={18} />
|
onChange={(event) => setClientId(event.target.value)}
|
||||||
<span className='text-xs font-semibold uppercase tracking-[0.22em]'>
|
className='mt-3 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 md:w-96'
|
||||||
Client portal
|
>
|
||||||
</span>
|
{clients.map((client) => (
|
||||||
</div>
|
<option key={client.id} value={client.id}>
|
||||||
<h1 className='mt-3 text-xl font-semibold'>
|
{client.name}
|
||||||
{isClientUser ? 'Your client portal' : 'Client portal preview'}
|
</option>
|
||||||
</h1>
|
))}
|
||||||
<p className='mt-2 max-w-2xl text-sm leading-6 text-[#fffdf9]'>
|
</select>
|
||||||
{isClientUser
|
</Panel>
|
||||||
? '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>
|
|
||||||
{!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 && (
|
{portalClient && (
|
||||||
<div className='grid gap-4 xl:grid-cols-[1.05fr_0.95fr]'>
|
<div className='grid gap-4'>
|
||||||
<Panel className='overflow-hidden'>
|
<div className='rounded-lg border border-[#19192d]/10 bg-[#fffdf9] p-5'>
|
||||||
<div className='bg-[#fffdf9] p-5'>
|
<div className='grid gap-4 lg:grid-cols-[1fr_280px]'>
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
<div>
|
||||||
Your coaching workspace
|
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#b17a1e]'>
|
||||||
</p>
|
Client workspace
|
||||||
<h2 className='mt-2 text-xl font-semibold text-[#19192d]'>
|
</p>
|
||||||
{portalClient.name}
|
<h1 className='mt-2 text-2xl font-semibold text-[#19192d]'>
|
||||||
</h2>
|
Hi, {portalClient.name}
|
||||||
<p className='mt-4 max-w-2xl leading-6 text-[#72798a]'>
|
</h1>
|
||||||
{portalClient.goals}
|
<p className='mt-3 max-w-3xl text-sm leading-6 text-[#72798a]'>
|
||||||
</p>
|
This is the shared space for your coaching work: notes
|
||||||
</div>
|
your coach approved, commitments you are working on, and
|
||||||
|
resources for the next session.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='p-5'>
|
<div className='rounded-lg border border-[#19192d]/10 bg-white p-4'>
|
||||||
<div className='flex items-center gap-3'>
|
<div className='flex items-center gap-2 text-sm font-semibold text-[#35b7a5]'>
|
||||||
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#f3fbf8] text-[#35b7a5]'>
|
<BaseIcon path={mdiCalendarClock} size={18} />
|
||||||
<BaseIcon path={mdiMessageReplyTextOutline} size={18} />
|
Next session
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<h3 className='text-lg font-semibold text-[#19192d]'>
|
|
||||||
Shared session notes
|
|
||||||
</h3>
|
|
||||||
<p className='text-sm text-[#72798a]'>
|
|
||||||
Only coach-approved notes appear here.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<p className='mt-2 text-lg font-semibold text-[#19192d]'>
|
||||||
|
{formatDate(portalClient.next_session_at)}
|
||||||
<div className='mt-5 space-y-4'>
|
</p>
|
||||||
{(portalClient.sessions || []).map((session) => (
|
<p className='mt-2 text-sm leading-6 text-[#72798a]'>
|
||||||
<div
|
Add your reflection below before the call.
|
||||||
key={session.id}
|
</p>
|
||||||
className='rounded-lg border border-[#19192d]/10 bg-white p-5'
|
|
||||||
>
|
|
||||||
<p className='font-semibold text-[#19192d]'>
|
|
||||||
{session.title}
|
|
||||||
</p>
|
|
||||||
<p className='mt-3 leading-6 text-[#72798a]'>
|
|
||||||
{session.shared_client_notes}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-6'>
|
<div className='grid gap-4 md:grid-cols-3'>
|
||||||
<Panel>
|
<Panel className='p-4'>
|
||||||
<div className='border-b border-[#19192d]/10 p-5'>
|
<p className='text-sm font-semibold text-[#72798a]'>
|
||||||
<h3 className='text-lg font-semibold text-[#19192d]'>
|
Open commitments
|
||||||
Commitments
|
</p>
|
||||||
</h3>
|
<p className='mt-2 text-2xl font-semibold text-[#19192d]'>
|
||||||
</div>
|
{openItems.length}
|
||||||
<div className='space-y-3 p-5'>
|
</p>
|
||||||
{(portalClient.action_items || []).map((item) => (
|
</Panel>
|
||||||
<div
|
<Panel className='p-4'>
|
||||||
key={item.id}
|
<p className='text-sm font-semibold text-[#72798a]'>
|
||||||
className='flex gap-3 rounded-lg border border-[#19192d]/10 bg-[#fffdf9] p-4'
|
Completed
|
||||||
>
|
</p>
|
||||||
<span className='mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-full bg-[#35b7a5] text-white'>
|
<p className='mt-2 text-2xl font-semibold text-[#19192d]'>
|
||||||
|
{finishedCount}
|
||||||
|
</p>
|
||||||
|
</Panel>
|
||||||
|
<Panel className='p-4'>
|
||||||
|
<p className='text-sm font-semibold text-[#72798a]'>
|
||||||
|
Shared resources
|
||||||
|
</p>
|
||||||
|
<p className='mt-2 text-2xl font-semibold text-[#19192d]'>
|
||||||
|
{portalClient.resources?.length || 0}
|
||||||
|
</p>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-4 xl:grid-cols-[0.95fr_1.05fr]'>
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<Panel>
|
||||||
|
<div className='border-b border-[#19192d]/10 p-4'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#f3fbf8] text-[#35b7a5]'>
|
||||||
<BaseIcon path={mdiCheckCircleOutline} size={18} />
|
<BaseIcon path={mdiCheckCircleOutline} size={18} />
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<p className='font-semibold text-[#19192d]'>
|
<h2 className='font-semibold text-[#19192d]'>
|
||||||
{item.title}
|
Commitments
|
||||||
</p>
|
</h2>
|
||||||
<p className='mt-1 text-sm text-[#72798a]'>
|
<p className='text-sm text-[#72798a]'>
|
||||||
{item.status.replace('_', ' ')}
|
Mark what you completed before the next session.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
<div className='space-y-3 p-4'>
|
||||||
</Panel>
|
{(portalClient.action_items || []).map((item) => {
|
||||||
|
const isDone =
|
||||||
|
item.status === 'done' || completedItems.has(item.id);
|
||||||
|
|
||||||
<Panel>
|
return (
|
||||||
<div className='border-b border-[#19192d]/10 p-5'>
|
<button
|
||||||
<h3 className='text-lg font-semibold text-[#19192d]'>
|
key={item.id}
|
||||||
Resources
|
type='button'
|
||||||
</h3>
|
className={`flex w-full gap-3 rounded-lg border p-4 text-left transition ${
|
||||||
</div>
|
isDone
|
||||||
<div className='space-y-3 p-5'>
|
? 'border-[#35b7a5]/30 bg-[#f3fbf8]'
|
||||||
{(portalClient.resources || []).map((resource) => (
|
: 'border-[#19192d]/10 bg-white hover:bg-[#fffdf9]'
|
||||||
<a
|
}`}
|
||||||
key={resource.id}
|
onClick={() => toggleItem(item.id)}
|
||||||
href={resource.url}
|
>
|
||||||
className='flex items-center justify-between gap-4 rounded-lg border border-[#19192d]/10 bg-white p-4 transition hover:bg-[#fffdf9]'
|
<span
|
||||||
|
className={`mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-full border ${
|
||||||
|
isDone
|
||||||
|
? 'border-[#35b7a5] bg-[#35b7a5] text-white'
|
||||||
|
: 'border-[#19192d]/10 bg-white text-[#72798a]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<BaseIcon
|
||||||
|
path={isDone ? mdiCheck : mdiFlagVariantOutline}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span className='block font-semibold text-[#19192d]'>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<span className='mt-1 block text-sm text-[#72798a]'>
|
||||||
|
{isDone
|
||||||
|
? 'Marked complete'
|
||||||
|
: item.status.replace('_', ' ')}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel className='p-4'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#fbf8f1] text-[#b17a1e]'>
|
||||||
|
<BaseIcon path={mdiPencilOutline} size={18} />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h2 className='font-semibold text-[#19192d]'>
|
||||||
|
Pre-session reflection
|
||||||
|
</h2>
|
||||||
|
<p className='text-sm text-[#72798a]'>
|
||||||
|
Share what changed since the last call.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={reflection}
|
||||||
|
onChange={(event) => {
|
||||||
|
setReflection(event.target.value);
|
||||||
|
setReflectionSaved(false);
|
||||||
|
}}
|
||||||
|
className='mt-4 min-h-[140px] w-full rounded-lg border border-[#19192d]/10 bg-[#fffdf9] px-3 py-2 text-sm leading-6 text-[#19192d] outline-none focus:border-[#35b7a5] focus:ring-2 focus:ring-[#35b7a5]/15'
|
||||||
|
placeholder='What did you complete? What changed? What should we focus on next?'
|
||||||
|
/>
|
||||||
|
<div className='mt-3 flex items-center justify-between gap-3'>
|
||||||
|
<p className='text-sm text-[#72798a]'>
|
||||||
|
{reflectionSaved
|
||||||
|
? 'Reflection saved for this session.'
|
||||||
|
: 'Draft is kept on this page for now.'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='rounded-full bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
|
disabled={!reflection.trim()}
|
||||||
|
onClick={() => setReflectionSaved(true)}
|
||||||
>
|
>
|
||||||
<div className='flex gap-3'>
|
Save reflection
|
||||||
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#fbf8f1] text-[#b17a1e]'>
|
</button>
|
||||||
<BaseIcon path={mdiBookOpenVariant} size={18} />
|
</div>
|
||||||
</span>
|
</Panel>
|
||||||
<div>
|
</div>
|
||||||
<p className='font-semibold text-[#19192d]'>
|
|
||||||
{resource.title}
|
|
||||||
</p>
|
|
||||||
<p className='mt-1 text-sm leading-6 text-[#72798a]'>
|
|
||||||
{resource.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<BaseIcon
|
|
||||||
path={mdiChevronRight}
|
|
||||||
size={18}
|
|
||||||
className='text-[#35b7a5]'
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Panel className='bg-[#f3fbf8] p-4'>
|
<div className='space-y-4'>
|
||||||
<p className='text-xs font-semibold uppercase tracking-[0.22em] text-[#35b7a5]'>
|
<Panel className='overflow-hidden'>
|
||||||
Pre-session reflection
|
<div className='border-b border-[#19192d]/10 bg-[#fffdf9] p-4'>
|
||||||
</p>
|
<div className='flex items-center gap-3'>
|
||||||
<h3 className='mt-2 text-lg font-semibold text-[#19192d]'>
|
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#f3fbf8] text-[#35b7a5]'>
|
||||||
What changed since last time?
|
<BaseIcon
|
||||||
</h3>
|
path={mdiMessageReplyTextOutline}
|
||||||
<p className='mt-3 leading-6 text-[#72798a]'>
|
size={18}
|
||||||
Final MVP should let the client answer this before a
|
/>
|
||||||
session, then feed the response into the coach prep brief.
|
</span>
|
||||||
</p>
|
<div>
|
||||||
</Panel>
|
<h2 className='font-semibold text-[#19192d]'>
|
||||||
|
Shared notes
|
||||||
|
</h2>
|
||||||
|
<p className='text-sm text-[#72798a]'>
|
||||||
|
Coach-approved notes only.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-3 p-4'>
|
||||||
|
{latestSession && (
|
||||||
|
<div className='rounded-lg border border-[#19192d]/10 bg-white p-4'>
|
||||||
|
<p className='font-semibold text-[#19192d]'>
|
||||||
|
{latestSession.title}
|
||||||
|
</p>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-[#72798a]'>
|
||||||
|
{latestSession.shared_client_notes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(portalClient.sessions || []).slice(1).map((session) => (
|
||||||
|
<details
|
||||||
|
key={session.id}
|
||||||
|
className='rounded-lg border border-[#19192d]/10 bg-white p-4'
|
||||||
|
>
|
||||||
|
<summary className='cursor-pointer font-semibold text-[#19192d]'>
|
||||||
|
{session.title}
|
||||||
|
</summary>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-[#72798a]'>
|
||||||
|
{session.shared_client_notes}
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<div className='border-b border-[#19192d]/10 p-4'>
|
||||||
|
<h2 className='font-semibold text-[#19192d]'>
|
||||||
|
Resources
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-3 p-4'>
|
||||||
|
{(portalClient.resources || []).map((resource) => (
|
||||||
|
<a
|
||||||
|
key={resource.id}
|
||||||
|
href={resource.url}
|
||||||
|
className='flex items-center justify-between gap-4 rounded-lg border border-[#19192d]/10 bg-white p-4 transition hover:bg-[#fffdf9]'
|
||||||
|
>
|
||||||
|
<div className='flex gap-3'>
|
||||||
|
<span className='grid h-8 w-8 place-items-center rounded-full bg-[#fbf8f1] text-[#b17a1e]'>
|
||||||
|
<BaseIcon path={mdiBookOpenVariant} size={18} />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p className='font-semibold text-[#19192d]'>
|
||||||
|
{resource.title}
|
||||||
|
</p>
|
||||||
|
<p className='mt-1 text-sm leading-6 text-[#72798a]'>
|
||||||
|
{resource.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BaseIcon
|
||||||
|
path={mdiChevronRight}
|
||||||
|
size={18}
|
||||||
|
className='text-[#35b7a5]'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user