feat(workspace): mentor + applicant message previews (§F.2)

mentor.getRecentMessages: last N unread messages from teams across all
of a mentor's assignments. Drives a Recent Messages card on /mentor.

applicant.getMentorConversationPreview: last 3 messages + unread count
for a given project. Drives a 'Conversation with [Mentor]' card on
/applicant — auto-hides when no mentor is assigned.

Both procedures use the existing MentorMessage(projectId, createdAt)
composite index — no new index needed.

Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
This commit is contained in:
Matt
2026-04-28 16:14:11 +02:00
parent 70a9752d73
commit 26ff8ed111
7 changed files with 352 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
'use client'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { MessageCircle } from 'lucide-react'
interface Props {
projectId: string
}
export function MentorConversationCard({ projectId }: Props) {
const { data, isLoading } = trpc.applicant.getMentorConversationPreview.useQuery(
{ projectId, limit: 3 },
{ refetchInterval: 30_000 },
)
if (isLoading) return <Skeleton className="h-44 w-full rounded-md" />
if (!data || !data.mentor) return null // No mentor assigned — render nothing
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2 text-base">
<span className="flex items-center gap-2">
<MessageCircle className="h-4 w-4" /> Conversation with{' '}
{data.mentor.name ?? data.mentor.email}
</span>
{data.unreadCount > 0 && <Badge variant="default">{data.unreadCount} new</Badge>}
</CardTitle>
</CardHeader>
<CardContent>
{data.messages.length === 0 ? (
<p className="text-muted-foreground text-sm">
Say hi to your mentor they&apos;re here to help you sharpen your project.
</p>
) : (
<ul className="space-y-2">
{data.messages.map((m) => (
<li key={m.id} className="bg-muted/20 rounded-md border p-2.5">
<div className="text-muted-foreground text-xs">
{m.sender.name ?? m.sender.email}
</div>
<div className="mt-0.5 line-clamp-2 text-sm">{m.message}</div>
</li>
))}
</ul>
)}
<div className="mt-3 flex justify-end">
<Button asChild size="sm">
<Link href="/applicant/mentor">Open chat</Link>
</Button>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,66 @@
'use client'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { MessageCircle } from 'lucide-react'
function formatRelativePast(date: Date | string | null): string {
if (!date) return ''
const d = typeof date === 'string' ? new Date(date) : date
const ms = Date.now() - d.getTime()
const minutes = Math.floor(ms / 60_000)
const hours = Math.floor(ms / 3_600_000)
const days = Math.floor(ms / 86_400_000)
if (days > 0) return `${days}d ago`
if (hours > 0) return `${hours}h ago`
return `${Math.max(0, minutes)}m ago`
}
export function RecentMessagesCard() {
const { data, isLoading } = trpc.mentor.getRecentMessages.useQuery({ limit: 5 })
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<MessageCircle className="h-4 w-4" /> Recent Messages
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
) : !data || data.unread.length === 0 ? (
<p className="text-muted-foreground text-sm">
No new messages. Your mentees will appear here when they reach out.
</p>
) : (
<ul className="space-y-3">
{data.unread.map((m) => (
<li key={m.id}>
<Link
href={`/mentor/workspace/${m.project.id}`}
className="hover:bg-muted/40 block rounded-md border p-3"
>
<div className="flex items-baseline justify-between gap-2">
<div className="text-sm font-medium">{m.sender.name ?? m.sender.email}</div>
<div className="text-muted-foreground text-xs">
{formatRelativePast(m.createdAt as unknown as Date)}
</div>
</div>
<div className="text-muted-foreground mt-0.5 text-xs">{m.project.title}</div>
<div className="mt-1 line-clamp-2 text-sm">{m.message}</div>
</Link>
</li>
))}
</ul>
)}
</CardContent>
</Card>
)
}