Add functional client portal workflow
This commit is contained in:
parent
fb01c003c6
commit
227ec4cb9a
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
async up(queryInterface, Sequelize) {
|
||||||
|
await queryInterface.addColumn("prep_briefs", "client_reflection", {
|
||||||
|
type: Sequelize.DataTypes.TEXT,
|
||||||
|
});
|
||||||
|
|
||||||
|
await queryInterface.addColumn("prep_briefs", "client_reflection_at", {
|
||||||
|
type: Sequelize.DataTypes.DATE,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
await queryInterface.removeColumn("prep_briefs", "client_reflection_at");
|
||||||
|
await queryInterface.removeColumn("prep_briefs", "client_reflection");
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -8,6 +8,8 @@ module.exports = function(sequelize, DataTypes) {
|
|||||||
open_commitments: { type: DataTypes.TEXT },
|
open_commitments: { type: DataTypes.TEXT },
|
||||||
suggested_questions: { type: DataTypes.TEXT },
|
suggested_questions: { type: DataTypes.TEXT },
|
||||||
sensitive_topics: { type: DataTypes.TEXT },
|
sensitive_topics: { type: DataTypes.TEXT },
|
||||||
|
client_reflection: { type: DataTypes.TEXT },
|
||||||
|
client_reflection_at: { type: DataTypes.DATE },
|
||||||
status: { type: DataTypes.ENUM("draft", "ready", "archived"), defaultValue: "ready" },
|
status: { type: DataTypes.ENUM("draft", "ready", "archived"), defaultValue: "ready" },
|
||||||
importHash: { type: DataTypes.STRING(255), allowNull: true, unique: true },
|
importHash: { type: DataTypes.STRING(255), allowNull: true, unique: true },
|
||||||
},
|
},
|
||||||
|
|||||||
@ -33,7 +33,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
id: clientAId,
|
id: clientAId,
|
||||||
name: "Maya Chen",
|
name: "Maya Chen",
|
||||||
email: "maya@example.com",
|
email: "client@hello.com",
|
||||||
status: "active",
|
status: "active",
|
||||||
goals: "Delegate operational decisions, build a steadier weekly planning rhythm, and prepare for a senior leadership transition.",
|
goals: "Delegate operational decisions, build a steadier weekly planning rhythm, and prepare for a senior leadership transition.",
|
||||||
notes: "Prefers direct feedback and concise written follow-ups. Avoid overloading with frameworks.",
|
notes: "Prefers direct feedback and concise written follow-ups. Avoid overloading with frameworks.",
|
||||||
@ -205,6 +205,8 @@ module.exports = {
|
|||||||
open_commitments: "Decision-rights matrix; three decisions no longer requiring founder approval.",
|
open_commitments: "Decision-rights matrix; three decisions no longer requiring founder approval.",
|
||||||
suggested_questions: "What felt risky to delegate? Where did the team ask for more clarity? What decision still needs your direct voice?",
|
suggested_questions: "What felt risky to delegate? Where did the team ask for more clarity? What decision still needs your direct voice?",
|
||||||
sensitive_topics: "Founder control and trust in senior leadership.",
|
sensitive_topics: "Founder control and trust in senior leadership.",
|
||||||
|
client_reflection: "I shared the decision-rights draft with the COO. The hardest part was naming which decisions I no longer need to approve.",
|
||||||
|
client_reflection_at: now,
|
||||||
status: "ready",
|
status: "ready",
|
||||||
createdById: coachUserId,
|
createdById: coachUserId,
|
||||||
updatedById: coachUserId,
|
updatedById: coachUserId,
|
||||||
|
|||||||
@ -5,6 +5,19 @@ const { LocalAIApi } = require("../ai/LocalAIApi");
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
function isClientUser(req) {
|
||||||
|
return req.currentUser?.app_role?.name === "Client";
|
||||||
|
}
|
||||||
|
|
||||||
|
function portalClientIncludes() {
|
||||||
|
return [
|
||||||
|
{ model: db.sessions, as: "sessions", order: [["session_at", "DESC"]] },
|
||||||
|
{ model: db.action_items, as: "action_items", order: [["due_at", "ASC"]] },
|
||||||
|
{ model: db.resources, as: "resources", where: { is_shared: true }, required: false },
|
||||||
|
{ model: db.prep_briefs, as: "prep_briefs", order: [["updatedAt", "DESC"]] },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/summary",
|
"/summary",
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
@ -51,6 +64,7 @@ router.get(
|
|||||||
{ model: db.packages, as: "package" },
|
{ model: db.packages, as: "package" },
|
||||||
{ model: db.sessions, as: "sessions", limit: 2, order: [["session_at", "DESC"]] },
|
{ model: db.sessions, as: "sessions", limit: 2, order: [["session_at", "DESC"]] },
|
||||||
{ model: db.action_items, as: "action_items", limit: 3, order: [["due_at", "ASC"]] },
|
{ model: db.action_items, as: "action_items", limit: 3, order: [["due_at", "ASC"]] },
|
||||||
|
{ model: db.prep_briefs, as: "prep_briefs", limit: 2, order: [["updatedAt", "DESC"]] },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,14 +240,16 @@ router.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/client-portal/:clientId",
|
"/client-portal/me",
|
||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
const client = await db.clients.findByPk(req.params.clientId, {
|
if (!isClientUser(req)) {
|
||||||
include: [
|
res.status(403).send({ error: "client_role_required" });
|
||||||
{ model: db.sessions, as: "sessions", order: [["session_at", "DESC"]] },
|
return;
|
||||||
{ model: db.action_items, as: "action_items", order: [["due_at", "ASC"]] },
|
}
|
||||||
{ model: db.resources, as: "resources", where: { is_shared: true }, required: false },
|
|
||||||
],
|
const client = await db.clients.findOne({
|
||||||
|
where: { email: req.currentUser.email },
|
||||||
|
include: portalClientIncludes(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@ -245,4 +261,127 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/client-portal/:clientId",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
let client;
|
||||||
|
|
||||||
|
if (isClientUser(req)) {
|
||||||
|
client = await db.clients.findOne({
|
||||||
|
where: { id: req.params.clientId, email: req.currentUser.email },
|
||||||
|
include: portalClientIncludes(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client = await db.clients.findByPk(req.params.clientId, {
|
||||||
|
include: portalClientIncludes(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).send({ error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(client);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
"/client-portal/action-items/:id",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const allowedStatuses = ["not_started", "in_progress", "done"];
|
||||||
|
const { status } = req.body;
|
||||||
|
|
||||||
|
if (!allowedStatuses.includes(status)) {
|
||||||
|
res.status(400).send({ error: "invalid_status" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionItem = await db.action_items.findByPk(req.params.id, {
|
||||||
|
include: [{ model: db.clients, as: "client" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!actionItem) {
|
||||||
|
res.status(404).send({ error: "action_item_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClientUser(req) && actionItem.client?.email !== req.currentUser.email) {
|
||||||
|
res.status(404).send({ error: "action_item_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await actionItem.update({
|
||||||
|
status,
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send(actionItem);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/client-portal/reflection",
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const reflection = String(req.body.reflection || "").trim();
|
||||||
|
|
||||||
|
if (!reflection) {
|
||||||
|
res.status(400).send({ error: "reflection_required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let client;
|
||||||
|
|
||||||
|
if (isClientUser(req)) {
|
||||||
|
client = await db.clients.findOne({ where: { email: req.currentUser.email } });
|
||||||
|
} else {
|
||||||
|
client = await db.clients.findByPk(req.body.clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
res.status(404).send({ error: "client_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestSession = await db.sessions.findOne({
|
||||||
|
where: { clientId: client.id },
|
||||||
|
order: [["session_at", "DESC"]],
|
||||||
|
});
|
||||||
|
const openActionItems = await db.action_items.findAll({
|
||||||
|
where: { clientId: client.id, status: ["not_started", "in_progress"] },
|
||||||
|
order: [["due_at", "ASC"]],
|
||||||
|
});
|
||||||
|
const existingBrief = await db.prep_briefs.findOne({
|
||||||
|
where: { clientId: client.id },
|
||||||
|
order: [["updatedAt", "DESC"]],
|
||||||
|
});
|
||||||
|
const openCommitments = openActionItems.map((item) => item.title).join("; ");
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
clientId: client.id,
|
||||||
|
sessionId: latestSession?.id,
|
||||||
|
next_session_at: client.next_session_at,
|
||||||
|
previous_summary: latestSession?.ai_summary,
|
||||||
|
open_commitments: openCommitments,
|
||||||
|
client_reflection: reflection,
|
||||||
|
client_reflection_at: new Date(),
|
||||||
|
status: "ready",
|
||||||
|
updatedById: req.currentUser.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prepBrief;
|
||||||
|
|
||||||
|
if (existingBrief) {
|
||||||
|
prepBrief = await existingBrief.update(data);
|
||||||
|
} else {
|
||||||
|
prepBrief = await db.prep_briefs.create({
|
||||||
|
...data,
|
||||||
|
createdById: req.currentUser.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(prepBrief);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -26,6 +26,11 @@ type PortalClient = {
|
|||||||
next_session_at?: 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 }>;
|
||||||
|
prep_briefs?: Array<{
|
||||||
|
id: string;
|
||||||
|
client_reflection?: string;
|
||||||
|
client_reflection_at?: string;
|
||||||
|
}>;
|
||||||
resources?: Array<{
|
resources?: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@ -70,9 +75,7 @@ const ClientPortal = () => {
|
|||||||
const [portalClient, setPortalClient] = React.useState<PortalClient | null>(
|
const [portalClient, setPortalClient] = React.useState<PortalClient | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [completedItems, setCompletedItems] = React.useState<Set<string>>(
|
const [updatingItemId, setUpdatingItemId] = React.useState('');
|
||||||
new Set(),
|
|
||||||
);
|
|
||||||
const [reflection, setReflection] = React.useState('');
|
const [reflection, setReflection] = React.useState('');
|
||||||
const [reflectionSaved, setReflectionSaved] = React.useState(false);
|
const [reflectionSaved, setReflectionSaved] = React.useState(false);
|
||||||
|
|
||||||
@ -80,19 +83,21 @@ const ClientPortal = () => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function loadClients() {
|
async function loadClients() {
|
||||||
|
if (isClientUser) {
|
||||||
|
const response = await axios.get('/coaching/client-portal/me');
|
||||||
|
setPortalClient(response.data);
|
||||||
|
setClientId(response.data.id);
|
||||||
|
const latestReflection =
|
||||||
|
response.data.prep_briefs?.[0]?.client_reflection || '';
|
||||||
|
setReflection(latestReflection);
|
||||||
|
setReflectionSaved(Boolean(latestReflection));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios.get('/coaching/clients');
|
const response = await axios.get('/coaching/clients');
|
||||||
setClients(response.data);
|
setClients(response.data);
|
||||||
|
|
||||||
if (response.data.length > 0) {
|
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);
|
setClientId(response.data[0].id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,33 +113,76 @@ 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());
|
const latestReflection =
|
||||||
setReflection('');
|
response.data.prep_briefs?.[0]?.client_reflection || '';
|
||||||
setReflectionSaved(false);
|
setReflection(latestReflection);
|
||||||
|
setReflectionSaved(Boolean(latestReflection));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPortal();
|
if (!isClientUser) {
|
||||||
}, [clientId]);
|
loadPortal();
|
||||||
|
}
|
||||||
|
}, [clientId, isClientUser]);
|
||||||
|
|
||||||
function toggleItem(itemId: string) {
|
async function toggleItem(item: { id: string; status: string }) {
|
||||||
setCompletedItems((current) => {
|
const nextStatus = item.status === 'done' ? 'in_progress' : 'done';
|
||||||
const next = new Set(current);
|
setUpdatingItemId(item.id);
|
||||||
if (next.has(itemId)) {
|
|
||||||
next.delete(itemId);
|
const response = await axios.patch(
|
||||||
} else {
|
`/coaching/client-portal/action-items/${item.id}`,
|
||||||
next.add(itemId);
|
{ status: nextStatus },
|
||||||
|
);
|
||||||
|
|
||||||
|
setPortalClient((current) => {
|
||||||
|
if (!current) {
|
||||||
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return {
|
||||||
|
...current,
|
||||||
|
action_items: (current.action_items || []).map((currentItem) => {
|
||||||
|
if (currentItem.id === item.id) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentItem;
|
||||||
|
}),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setUpdatingItemId('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveReflection() {
|
||||||
|
const response = await axios.post('/coaching/client-portal/reflection', {
|
||||||
|
clientId: portalClient?.id,
|
||||||
|
reflection,
|
||||||
|
});
|
||||||
|
|
||||||
|
setPortalClient((current) => {
|
||||||
|
if (!current) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBriefs = current.prep_briefs || [];
|
||||||
|
const otherBriefs = existingBriefs.filter((brief) => {
|
||||||
|
return brief.id !== response.data.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
prep_briefs: [response.data, ...otherBriefs],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setReflectionSaved(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const openItems = (portalClient?.action_items || []).filter((item) => {
|
const openItems = (portalClient?.action_items || []).filter((item) => {
|
||||||
return item.status !== 'done' && !completedItems.has(item.id);
|
return item.status !== 'done';
|
||||||
});
|
});
|
||||||
const finishedCount =
|
const finishedCount =
|
||||||
(portalClient?.action_items || []).filter((item) => item.status === 'done')
|
(portalClient?.action_items || []).filter((item) => item.status === 'done')
|
||||||
.length + completedItems.size;
|
.length;
|
||||||
const latestSession = portalClient?.sessions?.[0];
|
const latestSession = portalClient?.sessions?.[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -244,7 +292,7 @@ const ClientPortal = () => {
|
|||||||
<div className='space-y-3 p-4'>
|
<div className='space-y-3 p-4'>
|
||||||
{(portalClient.action_items || []).map((item) => {
|
{(portalClient.action_items || []).map((item) => {
|
||||||
const isDone =
|
const isDone =
|
||||||
item.status === 'done' || completedItems.has(item.id);
|
item.status === 'done';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -255,7 +303,8 @@ const ClientPortal = () => {
|
|||||||
? 'border-[#35b7a5]/30 bg-[#f3fbf8]'
|
? 'border-[#35b7a5]/30 bg-[#f3fbf8]'
|
||||||
: 'border-[#19192d]/10 bg-white hover:bg-[#fffdf9]'
|
: 'border-[#19192d]/10 bg-white hover:bg-[#fffdf9]'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleItem(item.id)}
|
disabled={updatingItemId === item.id}
|
||||||
|
onClick={() => toggleItem(item)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-full border ${
|
className={`mt-0.5 grid h-8 w-8 flex-none place-items-center rounded-full border ${
|
||||||
@ -312,13 +361,13 @@ const ClientPortal = () => {
|
|||||||
<p className='text-sm text-[#72798a]'>
|
<p className='text-sm text-[#72798a]'>
|
||||||
{reflectionSaved
|
{reflectionSaved
|
||||||
? 'Reflection saved for this session.'
|
? 'Reflection saved for this session.'
|
||||||
: 'Draft is kept on this page for now.'}
|
: 'Your coach will see this before the next call.'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='rounded-full bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50'
|
className='rounded-full bg-[#35b7a5] px-4 py-2 text-sm font-semibold text-white disabled:opacity-50'
|
||||||
disabled={!reflection.trim()}
|
disabled={!reflection.trim()}
|
||||||
onClick={() => setReflectionSaved(true)}
|
onClick={saveReflection}
|
||||||
>
|
>
|
||||||
Save reflection
|
Save reflection
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -21,6 +21,12 @@ type ActionItem = {
|
|||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PrepBrief = {
|
||||||
|
id: string;
|
||||||
|
client_reflection?: string;
|
||||||
|
client_reflection_at?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Session = {
|
type Session = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@ -40,6 +46,7 @@ type Client = {
|
|||||||
next_session_at?: string;
|
next_session_at?: string;
|
||||||
sessions?: Session[];
|
sessions?: Session[];
|
||||||
action_items?: ActionItem[];
|
action_items?: ActionItem[];
|
||||||
|
prep_briefs?: PrepBrief[];
|
||||||
package?: {
|
package?: {
|
||||||
title?: string;
|
title?: string;
|
||||||
duration?: string;
|
duration?: string;
|
||||||
@ -98,6 +105,9 @@ const Clients = () => {
|
|||||||
|
|
||||||
const selectedClient =
|
const selectedClient =
|
||||||
clients.find((client) => client.id === selectedClientId) || clients[0];
|
clients.find((client) => client.id === selectedClientId) || clients[0];
|
||||||
|
const latestReflection = selectedClient?.prep_briefs?.find((brief) => {
|
||||||
|
return Boolean(brief.client_reflection);
|
||||||
|
});
|
||||||
|
|
||||||
function selectClient(clientId: string) {
|
function selectClient(clientId: string) {
|
||||||
setSelectedClientId(clientId);
|
setSelectedClientId(clientId);
|
||||||
@ -261,6 +271,28 @@ const Clients = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<div className='border-b border-[#19192d]/10 p-5'>
|
||||||
|
<h3 className='text-lg font-semibold text-[#19192d]'>
|
||||||
|
Pre-session reflection
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className='p-5'>
|
||||||
|
{latestReflection ? (
|
||||||
|
<div className='rounded-lg border border-[#19192d]/10 bg-[#fffdf9] p-4'>
|
||||||
|
<p className='text-sm leading-6 text-[#72798a]'>
|
||||||
|
{latestReflection.client_reflection}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyState label='No client reflection submitted yet.' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-4 lg:grid-cols-2'>
|
||||||
|
|
||||||
<Panel>
|
<Panel>
|
||||||
<div className='border-b border-[#19192d]/10 p-5'>
|
<div className='border-b border-[#19192d]/10 p-5'>
|
||||||
<h3 className='text-lg font-semibold text-[#19192d]'>
|
<h3 className='text-lg font-semibold text-[#19192d]'>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user