feat: enhance Mini App design guidelines and refactor layout components
- Updated Mini App design guidelines to include detailed instructions on UI changes, accessibility rules, and verification processes. - Refactored multiple components to utilize `MiniAppScreen` and `MiniAppScreenContent` for consistent layout structure across the application. - Improved error handling in `GlobalError` and `NotFound` components by integrating new layout components for better user experience. - Introduced new hooks for admin functionality, streamlining access checks and data loading processes. - Enhanced documentation to reflect changes in design policies and component usage, ensuring clarity for future development.
This commit is contained in:
@@ -6,64 +6,54 @@
|
||||
|
||||
"use client";
|
||||
|
||||
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";
|
||||
|
||||
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";
|
||||
import { MiniAppScreen, MiniAppScreenContent, MiniAppStickyHeader } from "@/components/layout/MiniAppScreen";
|
||||
import { useScreenReady } from "@/hooks/use-screen-ready";
|
||||
|
||||
export default function AdminPage() {
|
||||
const { t, monthName } = useTranslation();
|
||||
const admin = useAdminPage();
|
||||
const setAppContentReady = useAppStore((s) => s.setAppContentReady);
|
||||
|
||||
// Signal ready so Telegram hides loader when opening /admin directly.
|
||||
useEffect(() => {
|
||||
setAppContentReady(true);
|
||||
}, [setAppContentReady]);
|
||||
useScreenReady(true);
|
||||
|
||||
if (!admin.isAllowed) {
|
||||
return (
|
||||
<div className={OUTER_CLASS}>
|
||||
<div className={INNER_CLASS}>
|
||||
<MiniAppScreen>
|
||||
<MiniAppScreenContent>
|
||||
<AccessDeniedScreen primaryAction="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</MiniAppScreenContent>
|
||||
</MiniAppScreen>
|
||||
);
|
||||
}
|
||||
|
||||
if (admin.adminCheckComplete === null) {
|
||||
return (
|
||||
<div className={OUTER_CLASS}>
|
||||
<div className={INNER_CLASS}>
|
||||
<MiniAppScreen>
|
||||
<MiniAppScreenContent>
|
||||
<div className="py-4 flex flex-col items-center gap-2">
|
||||
<LoadingState />
|
||||
<p className="text-sm text-muted-foreground">{t("admin.loading_users")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MiniAppScreenContent>
|
||||
</MiniAppScreen>
|
||||
);
|
||||
}
|
||||
|
||||
if (admin.adminAccessDenied) {
|
||||
return (
|
||||
<div className={OUTER_CLASS}>
|
||||
<div className={INNER_CLASS}>
|
||||
<MiniAppScreen>
|
||||
<MiniAppScreenContent>
|
||||
<div className="flex flex-col gap-4 py-6">
|
||||
<p className="text-muted-foreground">
|
||||
{admin.adminAccessDeniedDetail ?? t("admin.access_denied")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MiniAppScreenContent>
|
||||
</MiniAppScreen>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,9 +61,9 @@ export default function AdminPage() {
|
||||
const year = admin.adminMonth.getFullYear();
|
||||
|
||||
return (
|
||||
<div className={OUTER_CLASS}>
|
||||
<div className={INNER_CLASS}>
|
||||
<header className="sticky top-[var(--app-safe-top)] z-10 flex flex-col items-center border-b border-border bg-background py-3">
|
||||
<MiniAppScreen>
|
||||
<MiniAppScreenContent>
|
||||
<MiniAppStickyHeader className="flex flex-col items-center border-b border-border py-3">
|
||||
<MonthNavHeader
|
||||
month={admin.adminMonth}
|
||||
disabled={admin.loading}
|
||||
@@ -82,7 +72,7 @@ export default function AdminPage() {
|
||||
titleAriaLabel={`${t("admin.title")}, ${monthName(month)} ${year}`}
|
||||
className="w-full px-1"
|
||||
/>
|
||||
</header>
|
||||
</MiniAppStickyHeader>
|
||||
|
||||
{admin.successMessage && (
|
||||
<p className="mt-3 text-sm text-[var(--duty)]" role="status" aria-live="polite">
|
||||
@@ -135,7 +125,7 @@ export default function AdminPage() {
|
||||
onCloseAnimationEnd={admin.closeReassign}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MiniAppScreenContent>
|
||||
</MiniAppScreen>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getLang, translate } from "@/i18n/messages";
|
||||
import { callMiniAppReadyOnce } from "@/lib/telegram-ready";
|
||||
import { THEME_BOOTSTRAP_SCRIPT } from "@/lib/theme-bootstrap-script";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MiniAppScreen, MiniAppScreenContent } from "@/components/layout/MiniAppScreen";
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
@@ -35,17 +36,19 @@ export default function GlobalError({
|
||||
<script dangerouslySetInnerHTML={{ __html: THEME_BOOTSTRAP_SCRIPT }} />
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
<div className="content-safe flex min-h-[var(--tg-viewport-stable-height,100vh)] flex-col items-center justify-center gap-4 bg-background px-4 text-foreground">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{translate(lang, "error_boundary.message")}
|
||||
</h1>
|
||||
<p className="text-center text-muted-foreground">
|
||||
{translate(lang, "error_boundary.description")}
|
||||
</p>
|
||||
<Button type="button" onClick={() => reset()}>
|
||||
{translate(lang, "error_boundary.reload")}
|
||||
</Button>
|
||||
</div>
|
||||
<MiniAppScreen>
|
||||
<MiniAppScreenContent className="items-center justify-center gap-4 px-4 text-foreground">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{translate(lang, "error_boundary.message")}
|
||||
</h1>
|
||||
<p className="text-center text-muted-foreground">
|
||||
{translate(lang, "error_boundary.description")}
|
||||
</p>
|
||||
<Button type="button" onClick={() => reset()}>
|
||||
{translate(lang, "error_boundary.reload")}
|
||||
</Button>
|
||||
</MiniAppScreenContent>
|
||||
</MiniAppScreen>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -5,20 +5,15 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "@/i18n/use-translation";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FullScreenStateShell } from "@/components/states/FullScreenStateShell";
|
||||
import { useScreenReady } from "@/hooks/use-screen-ready";
|
||||
|
||||
export default function NotFound() {
|
||||
const { t } = useTranslation();
|
||||
const setAppContentReady = useAppStore((s) => s.setAppContentReady);
|
||||
|
||||
useEffect(() => {
|
||||
setAppContentReady(true);
|
||||
}, [setAppContentReady]);
|
||||
useScreenReady(true);
|
||||
|
||||
return (
|
||||
<FullScreenStateShell
|
||||
|
||||
@@ -15,6 +15,8 @@ import { getLang } from "@/i18n/messages";
|
||||
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
|
||||
import { CurrentDutyView } from "@/components/current-duty/CurrentDutyView";
|
||||
import { CalendarPage } from "@/components/CalendarPage";
|
||||
import { MiniAppScreen, MiniAppScreenContent } from "@/components/layout/MiniAppScreen";
|
||||
import { useScreenReady } from "@/hooks/use-screen-ready";
|
||||
|
||||
export default function Home() {
|
||||
const { initDataRaw, startParam, isLocalhost } = useTelegramAuth();
|
||||
@@ -31,7 +33,7 @@ export default function Home() {
|
||||
fetchAdminMe(initDataRaw, getLang()).then(({ is_admin }) => setIsAdmin(is_admin));
|
||||
}, [isAllowed, initDataRaw, setIsAdmin]);
|
||||
|
||||
const { accessDenied, currentView, setCurrentView, setSelectedDay, appContentReady, setAppContentReady } =
|
||||
const { accessDenied, currentView, setCurrentView, setSelectedDay, appContentReady } =
|
||||
useAppStore(
|
||||
useShallow((s: AppState) => ({
|
||||
accessDenied: s.accessDenied,
|
||||
@@ -39,16 +41,10 @@ export default function Home() {
|
||||
setCurrentView: s.setCurrentView,
|
||||
setSelectedDay: s.setSelectedDay,
|
||||
appContentReady: s.appContentReady,
|
||||
setAppContentReady: s.setAppContentReady,
|
||||
}))
|
||||
);
|
||||
|
||||
// When showing access-denied or current-duty view, mark content ready so ReadyGate can call miniAppReady().
|
||||
useEffect(() => {
|
||||
if (accessDenied || currentView === "currentDuty") {
|
||||
setAppContentReady(true);
|
||||
}
|
||||
}, [accessDenied, currentView, setAppContentReady]);
|
||||
useScreenReady(accessDenied || currentView === "currentDuty");
|
||||
|
||||
const handleBackFromCurrentDuty = useCallback(() => {
|
||||
setCurrentView("calendar");
|
||||
@@ -56,20 +52,20 @@ export default function Home() {
|
||||
}, [setCurrentView, setSelectedDay]);
|
||||
|
||||
const content = accessDenied ? (
|
||||
<div className="content-safe min-h-[var(--tg-viewport-stable-height,100vh)] bg-background">
|
||||
<div className="mx-auto flex w-full max-w-[var(--max-width-app)] flex-col">
|
||||
<MiniAppScreen>
|
||||
<MiniAppScreenContent>
|
||||
<AccessDeniedScreen primaryAction="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</MiniAppScreenContent>
|
||||
</MiniAppScreen>
|
||||
) : currentView === "currentDuty" ? (
|
||||
<div className="content-safe min-h-[var(--tg-viewport-stable-height,100vh)] bg-background">
|
||||
<div className="mx-auto flex w-full max-w-[var(--max-width-app)] flex-col">
|
||||
<MiniAppScreen>
|
||||
<MiniAppScreenContent>
|
||||
<CurrentDutyView
|
||||
onBack={handleBackFromCurrentDuty}
|
||||
openedFromPin={startParam === "duty"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MiniAppScreenContent>
|
||||
</MiniAppScreen>
|
||||
) : (
|
||||
<CalendarPage isAllowed={isAllowed} initDataRaw={initDataRaw} />
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user