mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
frontend: fix pagination styling, patient-sheet header, message avatars
- Patients pagination: render the prev/next + page-number controls as COSS Buttons inside the Pagination structure. PaginationLink drops its buttonVariants styling when given a `render` prop, so the controls were unstyled and wrapping; using Button restores proper pill/number buttons. - Patient detail sheet: move the action buttons (Download summary / Transfer / Edit / Delete) to their own wrapping row beneath the identity block so the patient name is no longer truncated on the narrow sheet. - Messages thread: add sender/recipient avatars (MessageAvatar) at the bottom of each message group, with a spacer to keep stacked bubbles aligned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,7 @@ import { Bubble, BubbleContent } from "@/components/ui/bubble";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Message,
|
||||
MessageAvatar,
|
||||
MessageContent,
|
||||
MessageFooter,
|
||||
MessageHeader,
|
||||
@@ -200,6 +201,7 @@ export function MessagesView() {
|
||||
const { t } = useTranslation();
|
||||
const { data: session } = authClient.useSession();
|
||||
const myId = session?.user?.id ?? "";
|
||||
const myInitials = initials(session?.user?.name ?? "");
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -694,6 +696,19 @@ export function MessagesView() {
|
||||
!newDay && (startsGroup ? "mt-4" : "mt-1"),
|
||||
)}
|
||||
>
|
||||
{/* Avatar at the bottom of each run (messenger-style); a
|
||||
spacer keeps stacked bubbles aligned otherwise. */}
|
||||
{endsGroup ? (
|
||||
<MessageAvatar>
|
||||
<Avatar className="size-8">
|
||||
<AvatarFallback className="text-[11px]">
|
||||
{out ? myInitials : initials(m.senderName)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</MessageAvatar>
|
||||
) : (
|
||||
<div className="w-8 shrink-0" />
|
||||
)}
|
||||
<MessageContent>
|
||||
{selected.isGroup && !out && startsGroup && (
|
||||
<MessageHeader>{m.senderName}</MessageHeader>
|
||||
|
||||
@@ -238,31 +238,35 @@ export function PatientDetail({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="size-12">
|
||||
<AvatarFallback>{patient.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="truncate font-semibold text-base text-foreground">
|
||||
{patient.name}
|
||||
</span>
|
||||
<Badge variant={statusVariant[patient.status]}>
|
||||
{t(`patients.status.${patient.status}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-muted-foreground text-sm">{idLine}</span>
|
||||
{patient.alerts.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1.5">
|
||||
{patient.alerts.map((alert) => (
|
||||
<Badge key={alert} variant="outline">
|
||||
{alert}
|
||||
</Badge>
|
||||
))}
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Identity — full width so the name never gets squeezed by the actions. */}
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar className="size-12">
|
||||
<AvatarFallback>{patient.initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-semibold text-base text-foreground">
|
||||
{patient.name}
|
||||
</span>
|
||||
<Badge variant={statusVariant[patient.status]}>
|
||||
{t(`patients.status.${patient.status}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-muted-foreground text-sm">{idLine}</span>
|
||||
{patient.alerts.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1.5">
|
||||
{patient.alerts.map((alert) => (
|
||||
<Badge key={alert} variant="outline">
|
||||
{alert}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{/* Actions — their own wrapping row beneath the identity. */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
onClick={() => printPatientSummary(patient, t)}
|
||||
size="sm"
|
||||
@@ -292,6 +296,7 @@ export function PatientDetail({
|
||||
{onDelete && (
|
||||
<Button
|
||||
aria-label={t("patients.delete.action")}
|
||||
className="ml-auto"
|
||||
onClick={onDelete}
|
||||
size="sm"
|
||||
type="button"
|
||||
|
||||
@@ -17,10 +17,8 @@ import {
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
} from "@/components/ui/pagination";
|
||||
import { listPatients, type Patient } from "@/lib/patients";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Rows shown per page on the patients table before paginating.
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -290,22 +288,20 @@ export function PatientsView() {
|
||||
>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
aria-disabled={safePage === 1}
|
||||
<Button
|
||||
aria-label={t("patients.pagination.previous")}
|
||||
className={cn(
|
||||
"gap-1 px-2.5",
|
||||
safePage === 1 && "pointer-events-none opacity-50"
|
||||
)}
|
||||
className="gap-1"
|
||||
disabled={safePage === 1}
|
||||
onClick={() => setPage(Math.max(1, safePage - 1))}
|
||||
render={<button type="button" />}
|
||||
size="default"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
<span className="max-sm:hidden">
|
||||
{t("patients.pagination.previous")}
|
||||
</span>
|
||||
</PaginationLink>
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
{pageWindow(safePage, totalPages).map((p, i) =>
|
||||
p === null ? (
|
||||
@@ -314,34 +310,34 @@ export function PatientsView() {
|
||||
</PaginationItem>
|
||||
) : (
|
||||
<PaginationItem key={p}>
|
||||
<PaginationLink
|
||||
<Button
|
||||
aria-current={p === safePage ? "page" : undefined}
|
||||
aria-label={t("patients.pagination.page", { page: p })}
|
||||
isActive={p === safePage}
|
||||
onClick={() => setPage(p)}
|
||||
render={<button type="button" />}
|
||||
size="icon-sm"
|
||||
type="button"
|
||||
variant={p === safePage ? "outline" : "ghost"}
|
||||
>
|
||||
{p}
|
||||
</PaginationLink>
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
)
|
||||
)}
|
||||
<PaginationItem>
|
||||
<PaginationLink
|
||||
aria-disabled={safePage === totalPages}
|
||||
<Button
|
||||
aria-label={t("patients.pagination.next")}
|
||||
className={cn(
|
||||
"gap-1 px-2.5",
|
||||
safePage === totalPages && "pointer-events-none opacity-50"
|
||||
)}
|
||||
className="gap-1"
|
||||
disabled={safePage === totalPages}
|
||||
onClick={() => setPage(Math.min(totalPages, safePage + 1))}
|
||||
render={<button type="button" />}
|
||||
size="default"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<span className="max-sm:hidden">
|
||||
{t("patients.pagination.next")}
|
||||
</span>
|
||||
<ChevronRight className="size-4" />
|
||||
</PaginationLink>
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
|
||||
Reference in New Issue
Block a user