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:
@@ -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
|
||||||
|
|||||||
@@ -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": "Закрыть",
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user