mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 20:08:14 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3767c1689b | |||
| edf42e9741 | |||
| 31adbab87b |
@@ -62,7 +62,25 @@ jobs:
|
||||
${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:${{ steps.meta.outputs.version }}
|
||||
${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:latest
|
||||
|
||||
# Pull this version's section out of CHANGELOG.md so the release has real,
|
||||
# human-written notes (the auto "Full Changelog" link is still appended
|
||||
# below via generate_release_notes). Falls back to a generic line if the
|
||||
# version has no CHANGELOG entry.
|
||||
- name: Extract changelog notes
|
||||
id: notes
|
||||
run: |
|
||||
version="${{ steps.meta.outputs.version }}"
|
||||
awk -v v="$version" '
|
||||
$0 ~ "^## \\[" v "\\]" {flag=1; next}
|
||||
/^## \[/ {flag=0}
|
||||
flag {print}
|
||||
' CHANGELOG.md | sed '/./,$!d' > release-notes.md
|
||||
if [ ! -s release-notes.md ]; then
|
||||
echo "Release $version. See the changelog for details." > release-notes.md
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: release-notes.md
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -7,6 +7,21 @@ for how releases are cut and published.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.1] — 2026-06-27
|
||||
|
||||
### Fixed
|
||||
- **Patients pagination** controls now render as proper buttons — the prev/next
|
||||
and page-number controls were unstyled and wrapping (the COSS `PaginationLink`
|
||||
drops its button styling when given a `render` prop).
|
||||
- **Patient detail sheet header** reflowed: actions (Download summary / Transfer
|
||||
/ Edit / Delete) moved to their own wrapping row so the patient name is no
|
||||
longer truncated.
|
||||
- **Messages thread** now shows **sender and recipient avatars** alongside the
|
||||
chat bubbles.
|
||||
- **Release notes** — the `release` workflow now publishes the matching
|
||||
`CHANGELOG.md` section as the GitHub Release body (instead of only the
|
||||
auto-generated "Full Changelog" link).
|
||||
|
||||
## [0.2.0] — 2026-06-27
|
||||
|
||||
### Added
|
||||
|
||||
@@ -9,7 +9,7 @@ information as rich record cards — backed by a **patient-owned data model**.
|
||||
|
||||
[](./LICENSE)
|
||||
[](https://hub.docker.com/u/khalidxv)
|
||||
[](./CHANGELOG.md)
|
||||
[](./CHANGELOG.md)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temetro-backend",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "temetro backend — Express + Postgres API with Better Auth (email/password, organizations) and org-scoped patient records.",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temetro",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"shadcn": "^4.11.0"
|
||||
|
||||
Reference in New Issue
Block a user