feat: enhance CurrentDutyView with loading skeletons and improved localization

- Added Skeleton components to CurrentDutyView for better loading state representation.
- Updated localization messages to include new labels for remaining time display.
- Refactored remaining time display logic for clarity and improved user experience.
This commit is contained in:
2026-03-04 11:16:07 +03:00
parent a8d4afb101
commit 6adec62b5f
3 changed files with 55 additions and 21 deletions

View File

@@ -29,6 +29,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { DutyWithUser } from "@/types"; import type { DutyWithUser } from "@/types";
export interface CurrentDutyViewProps { export interface CurrentDutyViewProps {
@@ -164,14 +165,33 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
aria-live="polite" aria-live="polite"
aria-label={t("loading")} aria-label={t("loading")}
> >
<span <Card className="current-duty-card w-full max-w-[var(--max-width-app)] border-t-4 border-t-duty">
className="block size-8 shrink-0 rounded-full border-2 border-transparent border-t-accent motion-reduce:animate-none animate-spin" <CardHeader>
aria-hidden <div className="flex items-center gap-2">
/> <Skeleton className="size-2.5 shrink-0 rounded-full" />
<p className="text-muted-foreground m-0">{t("loading")}</p> <Skeleton className="h-5 w-32" />
<Button variant="outline" onClick={handlePrimaryAction} aria-label={primaryButtonAriaLabel}> </div>
{primaryButtonLabel} </CardHeader>
</Button> <CardContent className="flex flex-col gap-4 pt-1">
<div className="flex flex-col gap-2">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-full max-w-[280px]" />
</div>
<Skeleton className="h-14 w-full rounded-lg" />
<div className="flex flex-col gap-2 border-t border-border/50 pt-4">
<Skeleton className="h-12 w-full rounded-md" />
<Skeleton className="h-12 w-full rounded-md" />
</div>
</CardContent>
<CardFooter>
<Button
onClick={handlePrimaryAction}
aria-label={primaryButtonAriaLabel}
>
{primaryButtonLabel}
</Button>
</CardFooter>
</Card>
</div> </div>
); );
} }
@@ -244,11 +264,10 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
const endTime = formatHHMM(duty.end_at); const endTime = formatHHMM(duty.end_at);
const shiftStr = `${startDDMM} ${startTime}${endDDMM} ${endTime}`; const shiftStr = `${startDDMM} ${startTime}${endDDMM} ${endTime}`;
const rem = remaining ?? getRemainingTime(duty.end_at); const rem = remaining ?? getRemainingTime(duty.end_at);
const remainingStr = t("current_duty.remaining", { const remainingValueStr = t("current_duty.remaining_value", {
hours: String(rem.hours), hours: String(rem.hours),
minutes: String(rem.minutes), minutes: String(rem.minutes),
}); });
const endsAtStr = t("current_duty.ends_at", { time: endTime });
const displayTz = const displayTz =
typeof window !== "undefined" && typeof window !== "undefined" &&
@@ -280,21 +299,31 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
{t("current_duty.title")} {t("current_duty.title")}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-3"> <CardContent className="flex flex-col gap-4 pt-1">
<p className="font-medium text-foreground" id="current-duty-name"> <section className="flex flex-col gap-2" aria-label={t("current_duty.shift")}>
{duty.full_name} <p
</p> className="text-lg font-semibold text-foreground leading-tight"
<p className="text-sm text-muted-foreground"> id="current-duty-name"
{shiftLabel} {shiftStr} >
</p> {duty.full_name}
</p>
<p className="text-sm text-muted-foreground break-words">
{shiftLabel} {shiftStr}
</p>
</section>
<div <div
className="rounded-lg bg-duty/10 px-3 py-2 text-sm font-medium text-foreground" className="rounded-lg bg-duty/10 px-3 py-2.5 flex flex-col gap-0.5"
aria-live="polite" aria-live="polite"
aria-atomic="true" aria-atomic="true"
> >
{remainingStr} <span className="text-xs text-muted-foreground">
{t("current_duty.remaining_label")}
</span>
<span className="text-base font-semibold text-foreground tabular-nums">
{remainingValueStr}
</span>
</div> </div>
<p className="text-sm text-muted-foreground">{endsAtStr}</p> <section className="flex flex-col gap-2 border-t border-border/50 pt-4" aria-label={t("contact.label")}>
{hasContacts ? ( {hasContacts ? (
<ContactLinks <ContactLinks
phone={duty.phone} phone={duty.phone}
@@ -308,6 +337,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
{t("current_duty.contact_info_not_set")} {t("current_duty.contact_info_not_set")}
</p> </p>
)} )}
</section>
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<Button <Button

View File

@@ -67,6 +67,8 @@ export const MESSAGES: Record<Lang, Record<string, string>> = {
"current_duty.shift_tz": "Shift ({tz}):", "current_duty.shift_tz": "Shift ({tz}):",
"current_duty.shift_local": "Shift (your time):", "current_duty.shift_local": "Shift (your time):",
"current_duty.remaining": "Remaining: {hours}h {minutes}min", "current_duty.remaining": "Remaining: {hours}h {minutes}min",
"current_duty.remaining_label": "Remaining",
"current_duty.remaining_value": "{hours}h {minutes}min",
"current_duty.ends_at": "Until end of shift at {time}", "current_duty.ends_at": "Until end of shift at {time}",
"current_duty.back": "Back to calendar", "current_duty.back": "Back to calendar",
"current_duty.close": "Close", "current_duty.close": "Close",
@@ -140,6 +142,8 @@ export const MESSAGES: Record<Lang, Record<string, string>> = {
"current_duty.shift_tz": "Смена ({tz}):", "current_duty.shift_tz": "Смена ({tz}):",
"current_duty.shift_local": "Смена (ваше время):", "current_duty.shift_local": "Смена (ваше время):",
"current_duty.remaining": "Осталось: {hours}ч {minutes}мин", "current_duty.remaining": "Осталось: {hours}ч {minutes}мин",
"current_duty.remaining_label": "Осталось",
"current_duty.remaining_value": "{hours}ч {minutes}мин",
"current_duty.ends_at": "До конца смены в {time}", "current_duty.ends_at": "До конца смены в {time}",
"current_duty.back": "Назад к календарю", "current_duty.back": "Назад к календарю",
"current_duty.close": "Закрыть", "current_duty.close": "Закрыть",

View File

@@ -3,7 +3,7 @@
* Ported from webapp/js/currentDuty.test.js. * Ported from webapp/js/currentDuty.test.js.
*/ */
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { getRemainingTime, findCurrentDuty } from "./current-duty"; import { getRemainingTime, findCurrentDuty } from "./current-duty";
import type { DutyWithUser } from "@/types"; import type { DutyWithUser } from "@/types";