feat: implement AccessDeniedScreen and enhance error handling
- Introduced AccessDeniedScreen component for improved user experience when access is denied, replacing the previous AccessDenied component. - Updated CurrentDutyView and CalendarPage to handle access denied scenarios, displaying the new screen appropriately. - Enhanced tests for CurrentDutyView and AccessDeniedScreen to ensure correct rendering and functionality under access denied conditions. - Refactored localization messages to include new labels for access denied scenarios in both English and Russian.
This commit is contained in:
84
webapp-next/src/components/states/AccessDeniedScreen.tsx
Normal file
84
webapp-next/src/components/states/AccessDeniedScreen.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Full-screen access denied view. Used when the user is not allowed (no initData / not localhost)
|
||||
* or when API returns 403. Matches global-error and not-found layout: no extra chrome, one action.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { getLang, translate } from "@/i18n/messages";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
export interface AccessDeniedScreenProps {
|
||||
/** Optional detail from API 403 response, shown below the hint. */
|
||||
serverDetail?: string | null;
|
||||
/** Primary button: reload (main page) or back/close (deep link). */
|
||||
primaryAction: "reload" | "back";
|
||||
/** Called when primaryAction is "back" (e.g. Back to calendar or Close). */
|
||||
onBack?: () => void;
|
||||
/** When true and primaryAction is "back", button label is "Close" instead of "Back to calendar". */
|
||||
openedFromPin?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-screen access denied: title, hint, optional server detail, single action button.
|
||||
* Calls setAppContentReady(true) on mount so Telegram receives ready().
|
||||
*/
|
||||
export function AccessDeniedScreen({
|
||||
serverDetail,
|
||||
primaryAction,
|
||||
onBack,
|
||||
openedFromPin = false,
|
||||
}: AccessDeniedScreenProps) {
|
||||
const lang = getLang();
|
||||
const setAppContentReady = useAppStore((s) => s.setAppContentReady);
|
||||
|
||||
useEffect(() => {
|
||||
setAppContentReady(true);
|
||||
}, [setAppContentReady]);
|
||||
|
||||
const hasDetail = Boolean(serverDetail && String(serverDetail).trim());
|
||||
|
||||
const handleClick = () => {
|
||||
if (primaryAction === "reload") {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
onBack?.();
|
||||
}
|
||||
};
|
||||
|
||||
const buttonLabel =
|
||||
primaryAction === "reload"
|
||||
? translate(lang, "access_denied.reload")
|
||||
: openedFromPin
|
||||
? translate(lang, "current_duty.close")
|
||||
: translate(lang, "current_duty.back");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-screen flex-col items-center justify-center gap-4 bg-background px-4 text-foreground"
|
||||
role="alert"
|
||||
>
|
||||
<h1 className="text-xl font-semibold">
|
||||
{translate(lang, "access_denied")}
|
||||
</h1>
|
||||
<p className="text-center text-muted-foreground">
|
||||
{translate(lang, "access_denied.hint")}
|
||||
</p>
|
||||
{hasDetail && (
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
{serverDetail}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className="rounded-lg bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user