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:
@@ -8,7 +8,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { backButton, closeMiniApp } from "@telegram-apps/sdk-react";
|
||||
import { Calendar } from "lucide-react";
|
||||
import { useTranslation } from "@/i18n/use-translation";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
@@ -33,6 +32,9 @@ import {
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { AccessDeniedScreen } from "@/components/states/AccessDeniedScreen";
|
||||
import type { DutyWithUser } from "@/types";
|
||||
import { useTelegramBackButton, useTelegramCloseAction } from "@/hooks/telegram";
|
||||
import { useScreenReady } from "@/hooks/use-screen-ready";
|
||||
import { useRequestState } from "@/hooks/use-request-state";
|
||||
|
||||
export interface CurrentDutyViewProps {
|
||||
/** Called when user taps Back (in-app button or Telegram BackButton). */
|
||||
@@ -41,25 +43,30 @@ export interface CurrentDutyViewProps {
|
||||
openedFromPin?: boolean;
|
||||
}
|
||||
|
||||
type ViewState = "loading" | "error" | "accessDenied" | "ready";
|
||||
|
||||
/**
|
||||
* Full-screen current duty view with Telegram BackButton and auto-updating remaining time.
|
||||
*/
|
||||
export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const lang = useAppStore((s) => s.lang);
|
||||
const setAppContentReady = useAppStore((s) => s.setAppContentReady);
|
||||
const { initDataRaw } = useTelegramAuth();
|
||||
|
||||
const [state, setState] = useState<ViewState>("loading");
|
||||
const [duty, setDuty] = useState<DutyWithUser | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [accessDeniedDetail, setAccessDeniedDetail] = useState<string | null>(null);
|
||||
const [remaining, setRemaining] = useState<{ hours: number; minutes: number } | null>(null);
|
||||
const {
|
||||
state: requestState,
|
||||
setLoading,
|
||||
setSuccess,
|
||||
setError,
|
||||
setAccessDenied,
|
||||
isLoading,
|
||||
isError,
|
||||
isAccessDenied,
|
||||
} = useRequestState("loading");
|
||||
|
||||
const loadTodayDuties = useCallback(
|
||||
async (signal?: AbortSignal | null) => {
|
||||
setLoading();
|
||||
const today = new Date();
|
||||
const from = localDateString(today);
|
||||
const to = from;
|
||||
@@ -69,7 +76,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
if (signal?.aborted) return;
|
||||
const active = findCurrentDuty(duties);
|
||||
setDuty(active);
|
||||
setState("ready");
|
||||
setSuccess();
|
||||
if (active) {
|
||||
setRemaining(getRemainingTime(active.end_at));
|
||||
} else {
|
||||
@@ -78,19 +85,17 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
} catch (e) {
|
||||
if (signal?.aborted) return;
|
||||
if (e instanceof AccessDeniedError) {
|
||||
setState("accessDenied");
|
||||
setAccessDeniedDetail(e.serverDetail ?? null);
|
||||
setAccessDenied(e.serverDetail ?? null);
|
||||
setDuty(null);
|
||||
setRemaining(null);
|
||||
} else {
|
||||
setState("error");
|
||||
setErrorMessage(t("error_generic"));
|
||||
setError(t("error_generic"));
|
||||
setDuty(null);
|
||||
setRemaining(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
[initDataRaw, lang, t]
|
||||
[initDataRaw, lang, t, setLoading, setSuccess, setAccessDenied, setError]
|
||||
);
|
||||
|
||||
// Fetch today's duties on mount; abort on unmount to avoid setState after unmount.
|
||||
@@ -100,12 +105,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
return () => controller.abort();
|
||||
}, [loadTodayDuties]);
|
||||
|
||||
// Mark content ready when data is loaded or error, so page can call ready() and show content.
|
||||
useEffect(() => {
|
||||
if (state !== "loading") {
|
||||
setAppContentReady(true);
|
||||
}
|
||||
}, [state, setAppContentReady]);
|
||||
useScreenReady(!isLoading);
|
||||
|
||||
// Auto-update remaining time every second when there is an active duty.
|
||||
useEffect(() => {
|
||||
@@ -116,47 +116,21 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
return () => clearInterval(interval);
|
||||
}, [duty]);
|
||||
|
||||
// Telegram BackButton: show on mount, hide on unmount, handle click.
|
||||
useEffect(() => {
|
||||
let offClick: (() => void) | undefined;
|
||||
try {
|
||||
if (backButton.mount.isAvailable()) {
|
||||
backButton.mount();
|
||||
}
|
||||
if (backButton.show.isAvailable()) {
|
||||
backButton.show();
|
||||
}
|
||||
if (backButton.onClick.isAvailable()) {
|
||||
offClick = backButton.onClick(onBack);
|
||||
}
|
||||
} catch {
|
||||
// Non-Telegram environment; BackButton not available.
|
||||
}
|
||||
|
||||
return () => {
|
||||
try {
|
||||
if (typeof offClick === "function") offClick();
|
||||
if (backButton.hide.isAvailable()) {
|
||||
backButton.hide();
|
||||
}
|
||||
} catch {
|
||||
// Ignore cleanup errors in non-Telegram environment.
|
||||
}
|
||||
};
|
||||
}, [onBack]);
|
||||
useTelegramBackButton({
|
||||
enabled: true,
|
||||
onClick: onBack,
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
triggerHapticLight();
|
||||
onBack();
|
||||
};
|
||||
|
||||
const closeMiniAppOrFallback = useTelegramCloseAction(onBack);
|
||||
|
||||
const handleClose = () => {
|
||||
triggerHapticLight();
|
||||
if (closeMiniApp.isAvailable()) {
|
||||
closeMiniApp();
|
||||
} else {
|
||||
onBack();
|
||||
}
|
||||
closeMiniAppOrFallback();
|
||||
};
|
||||
|
||||
const primaryButtonLabel = openedFromPin ? t("current_duty.close") : t("current_duty.back");
|
||||
@@ -165,7 +139,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
: t("current_duty.back");
|
||||
const handlePrimaryAction = openedFromPin ? handleClose : handleBack;
|
||||
|
||||
if (state === "loading") {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className="flex min-h-[50vh] flex-col items-center justify-center gap-4"
|
||||
@@ -203,10 +177,10 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
);
|
||||
}
|
||||
|
||||
if (state === "accessDenied") {
|
||||
if (isAccessDenied) {
|
||||
return (
|
||||
<AccessDeniedScreen
|
||||
serverDetail={accessDeniedDetail}
|
||||
serverDetail={requestState.accessDeniedDetail}
|
||||
primaryAction="back"
|
||||
onBack={handlePrimaryAction}
|
||||
openedFromPin={openedFromPin}
|
||||
@@ -214,17 +188,16 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
);
|
||||
}
|
||||
|
||||
if (state === "error") {
|
||||
if (isError) {
|
||||
const handleRetry = () => {
|
||||
triggerHapticLight();
|
||||
setState("loading");
|
||||
loadTodayDuties();
|
||||
};
|
||||
return (
|
||||
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4">
|
||||
<Card className="w-full max-w-[var(--max-width-app)]">
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-error">{errorMessage}</p>
|
||||
<p className="text-error">{requestState.error}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user