diff --git a/webapp-next/src/app/admin/page.tsx b/webapp-next/src/app/admin/page.tsx index 87d55ca..fca4f10 100644 --- a/webapp-next/src/app/admin/page.tsx +++ b/webapp-next/src/app/admin/page.tsx @@ -10,14 +10,15 @@ import { useEffect } from "react"; import { useAdminPage, AdminDutyList, ReassignSheet } from "@/components/admin"; import { useTranslation } from "@/i18n/use-translation"; import { useAppStore } from "@/store/app-store"; +import { MonthNavHeader } from "@/components/calendar/MonthNavHeader"; import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen"; import { LoadingState } from "@/components/states/LoadingState"; import { ErrorState } from "@/components/states/ErrorState"; -import { Button } from "@/components/ui/button"; -import { ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon } from "lucide-react"; -const PAGE_WRAPPER_CLASS = - "content-safe mx-auto flex min-h-[var(--tg-viewport-stable-height,100vh)] w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6"; +const OUTER_CLASS = + "content-safe min-h-[var(--tg-viewport-stable-height,100vh)] bg-background"; +const INNER_CLASS = + "mx-auto flex w-full max-w-[var(--max-width-app)] flex-col"; export default function AdminPage() { const { t, monthName } = useTranslation(); @@ -31,31 +32,37 @@ export default function AdminPage() { if (!admin.isAllowed) { return ( -
- +
+
+ +
); } if (admin.adminCheckComplete === null) { return ( -
+
+

{t("admin.loading_users")}

+
); } if (admin.adminAccessDenied) { return ( -
+
+

{admin.adminAccessDeniedDetail ?? t("admin.access_denied")}

+
); } @@ -64,46 +71,17 @@ export default function AdminPage() { const year = admin.adminMonth.getFullYear(); return ( -
+
+
-
- -

- - {year} - - - {monthName(month)} - -

- -
-

- {admin.loading ? "…" : t("admin.duties_count", { count: String(admin.dutyOnly.length) })} -

+
{admin.successMessage && ( @@ -157,6 +135,7 @@ export default function AdminPage() { onCloseAnimationEnd={admin.closeReassign} t={t} /> +
); } diff --git a/webapp-next/src/app/page.tsx b/webapp-next/src/app/page.tsx index 03eff0d..4610415 100644 --- a/webapp-next/src/app/page.tsx +++ b/webapp-next/src/app/page.tsx @@ -56,15 +56,19 @@ export default function Home() { }, [setCurrentView, setSelectedDay]); const content = accessDenied ? ( -
- +
+
+ +
) : currentView === "currentDuty" ? ( -
- +
+
+ +
) : ( diff --git a/webapp-next/src/components/CalendarPage.tsx b/webapp-next/src/components/CalendarPage.tsx index 46d0e51..a8bc8a4 100644 --- a/webapp-next/src/components/CalendarPage.tsx +++ b/webapp-next/src/components/CalendarPage.tsx @@ -168,41 +168,43 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) { }, [isAdmin, router]); return ( -
-
- - +
+
+ + +
+ + {error && ( + + )} + {!error && ( + + )} + +
- - {error && ( - - )} - {!error && ( - - )} - -
); } diff --git a/webapp-next/src/components/calendar/CalendarHeader.tsx b/webapp-next/src/components/calendar/CalendarHeader.tsx index f58e8ff..6151f2b 100644 --- a/webapp-next/src/components/calendar/CalendarHeader.tsx +++ b/webapp-next/src/components/calendar/CalendarHeader.tsx @@ -5,13 +5,9 @@ "use client"; -import { Button } from "@/components/ui/button"; import { useTranslation } from "@/i18n/use-translation"; import { cn } from "@/lib/utils"; -import { - ChevronLeft as ChevronLeftIcon, - ChevronRight as ChevronRightIcon, -} from "lucide-react"; +import { MonthNavHeader } from "@/components/calendar/MonthNavHeader"; export interface CalendarHeaderProps { /** Currently displayed month (used for title). */ @@ -30,51 +26,19 @@ export function CalendarHeader({ onNextMonth, className, }: CalendarHeaderProps) { - const { t, monthName, weekdayLabels } = useTranslation(); - const year = month.getFullYear(); - const monthIndex = month.getMonth(); + const { weekdayLabels } = useTranslation(); const labels = weekdayLabels(); return (
-
- -
-

- - {year} - - - {monthName(monthIndex)} - -

-
- -
+
{labels.map((label, i) => ( diff --git a/webapp-next/src/components/calendar/MonthNavHeader.tsx b/webapp-next/src/components/calendar/MonthNavHeader.tsx new file mode 100644 index 0000000..ca316d7 --- /dev/null +++ b/webapp-next/src/components/calendar/MonthNavHeader.tsx @@ -0,0 +1,86 @@ +/** + * Shared month navigation row: prev button, year + month title, next button. + * Used by CalendarHeader and admin page for consistent layout and spacing. + */ + +"use client"; + +import { Button } from "@/components/ui/button"; +import { useTranslation } from "@/i18n/use-translation"; +import { cn } from "@/lib/utils"; +import { + ChevronLeft as ChevronLeftIcon, + ChevronRight as ChevronRightIcon, +} from "lucide-react"; + +export interface MonthNavHeaderProps { + /** Currently displayed month (used for title). */ + month: Date; + /** Whether month navigation is disabled (e.g. during loading). */ + disabled?: boolean; + onPrevMonth: () => void; + onNextMonth: () => void; + /** Optional aria-label for the month title (e.g. admin page). */ + titleAriaLabel?: string; + /** When true, title is announced on change (e.g. calendar). */ + ariaLive?: boolean; + className?: string; +} + +const NAV_BUTTON_CLASS = + "size-10 rounded-[10px] bg-surface text-accent hover:bg-[var(--surface-hover)] focus-visible:outline-accent active:scale-95 disabled:opacity-50"; + +export function MonthNavHeader({ + month, + disabled = false, + onPrevMonth, + onNextMonth, + titleAriaLabel, + ariaLive = false, + className, +}: MonthNavHeaderProps) { + const { t, monthName } = useTranslation(); + const year = month.getFullYear(); + const monthIndex = month.getMonth(); + + return ( +
+ +
+

+ + {year} + + + {monthName(monthIndex)} + +

+
+ +
+ ); +} diff --git a/webapp-next/src/components/current-duty/CurrentDutyView.tsx b/webapp-next/src/components/current-duty/CurrentDutyView.tsx index eb17b3c..9c9b674 100644 --- a/webapp-next/src/components/current-duty/CurrentDutyView.tsx +++ b/webapp-next/src/components/current-duty/CurrentDutyView.tsx @@ -168,7 +168,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi if (state === "loading") { return (
+

{errorMessage}

@@ -249,7 +249,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi if (!duty) { return ( -
+
{t("current_duty.title")} @@ -307,7 +307,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi Boolean(duty.username && String(duty.username).trim()); return ( -
+
( if (!open || !selectedDay) return null; const panelClassName = - "max-w-[min(360px,calc(100vw-24px))] max-h-[70vh] overflow-auto bg-surface text-[var(--text)] rounded-xl shadow-lg p-4 pt-9"; + "max-w-[min(360px,calc(100vw - var(--app-safe-left, 0) - var(--app-safe-right, 0) - 24px))] max-h-[70vh] overflow-auto bg-surface text-[var(--text)] rounded-xl shadow-lg p-4 pt-9"; const closeButton = (
); } diff --git a/webapp-next/src/components/ui/card.tsx b/webapp-next/src/components/ui/card.tsx index acf57dc..8781c22 100644 --- a/webapp-next/src/components/ui/card.tsx +++ b/webapp-next/src/components/ui/card.tsx @@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
) { return (
) @@ -75,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { return (
) diff --git a/webapp-next/src/i18n/messages.ts b/webapp-next/src/i18n/messages.ts index 341ad9a..26143de 100644 --- a/webapp-next/src/i18n/messages.ts +++ b/webapp-next/src/i18n/messages.ts @@ -96,7 +96,6 @@ export const MESSAGES: Record> = { "admin.no_duties": "No duties this month.", "admin.no_users_for_assign": "No users available for assignment.", "admin.save": "Save", - "admin.duties_count": "{count} duties this month", "admin.list_aria": "List of duties to reassign", "admin.reassign_aria": "Reassign duty: {date}, {time}, {name}", "admin.section_aria": "Duties for {date}", @@ -195,7 +194,6 @@ export const MESSAGES: Record> = { "admin.no_duties": "В этом месяце дежурств нет.", "admin.no_users_for_assign": "Нет пользователей для назначения.", "admin.save": "Сохранить", - "admin.duties_count": "{count} дежурств в этом месяце", "admin.list_aria": "Список дежурств для перераспределения", "admin.reassign_aria": "Переназначить дежурство: {date}, {time}, {name}", "admin.section_aria": "Дежурства за {date}",