Files
NianToB/src/pages/ProductCenter/index.tsx

144 lines
5.5 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react';
import { ArrowLeft, ExternalLink, Loader2, RefreshCcw, ShoppingBag } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { useYinianStore } from '@/stores/yinian';
import { buildProductCenterSrc, getProductCenterBaseUrl } from './url';
const PRODUCT_CENTER_PARTITION = 'persist:yinian-travel-resource-ordering';
type ProductCenterWebviewElement = HTMLElement & {
reload?: () => void;
};
export function ProductCenter() {
const { t } = useTranslation('appCenter');
const navigate = useNavigate();
const session = useYinianStore((state) => state.session);
const webviewRef = useRef<ProductCenterWebviewElement | null>(null);
const [reloadKey, setReloadKey] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const baseUrl = useMemo(() => getProductCenterBaseUrl(), []);
const embeddedSrc = useMemo(() => buildProductCenterSrc({
baseUrl,
reloadKey,
session,
}), [baseUrl, reloadKey, session]);
useEffect(() => {
const webview = webviewRef.current;
if (!webview) return;
const handleStart = () => setIsLoading(true);
const handleStop = () => setIsLoading(false);
webview.addEventListener('did-start-loading', handleStart);
webview.addEventListener('did-stop-loading', handleStop);
webview.addEventListener('did-fail-load', handleStop);
return () => {
webview.removeEventListener('did-start-loading', handleStart);
webview.removeEventListener('did-stop-loading', handleStop);
webview.removeEventListener('did-fail-load', handleStop);
};
}, [reloadKey]);
const reloadApp = () => {
setIsLoading(true);
if (typeof webviewRef.current?.reload === 'function') {
webviewRef.current.reload();
return;
}
setReloadKey((value) => value + 1);
};
const openExternal = () => {
void window.electron.openExternal(baseUrl);
};
return (
<div data-testid="product-center-page" className="-m-6 flex h-[calc(100vh-2.5rem)] min-h-0 flex-col overflow-hidden bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50">
<header className="grid h-14 shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 border-b border-slate-200/80 bg-white/70 px-4 backdrop-blur dark:border-white/10 dark:bg-slate-950/80">
<div className="flex min-w-0 items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => navigate('/app-center')}
data-testid="product-center-back"
className="shrink-0"
>
<ArrowLeft className="h-4 w-4" />
{t('productCenter.back')}
</Button>
<Button
type="button"
variant="outline"
size="icon"
onClick={reloadApp}
aria-label={t('productCenter.reload')}
title={t('productCenter.reload')}
className="h-9 w-9 shrink-0"
>
<RefreshCcw className="h-4 w-4" />
</Button>
<div className="min-w-0">
<h1 className="truncate text-base font-semibold tracking-normal">
{t('productCenter.title')}
</h1>
</div>
</div>
<div className="flex min-w-0 items-center justify-self-center rounded-lg border border-slate-200/70 bg-white/60 px-3 py-1.5 text-xs font-medium text-slate-600 dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
<ShoppingBag className="mr-1.5 h-3.5 w-3.5 text-[#075985] dark:text-blue-200" />
<span className="truncate">{t('productCenter.badge')}</span>
</div>
<div className="flex min-w-0 justify-end">
<Button
type="button"
variant="outline"
size="sm"
onClick={openExternal}
data-testid="product-center-open-external"
className="shrink-0"
>
<ExternalLink className="h-4 w-4" />
{t('productCenter.openExternal')}
</Button>
</div>
</header>
<section className="relative min-h-0 flex-1 overflow-hidden bg-white dark:bg-slate-950">
<webview
key={reloadKey}
ref={webviewRef}
data-testid="product-center-frame"
src={embeddedSrc}
partition={PRODUCT_CENTER_PARTITION}
title={t('productCenter.frameTitle')}
className="absolute inset-0 h-full w-full border-0 bg-white dark:bg-slate-950"
allowpopups={false}
webpreferences="contextIsolation=yes,nodeIntegration=no"
/>
{isLoading && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-white/92 p-6 text-center dark:bg-slate-950/92">
<div className="max-w-md">
<div className="mx-auto flex h-11 w-11 items-center justify-center rounded-lg bg-[#EAF5FA] text-[#075985] dark:bg-[#1E3A8A]/30 dark:text-blue-100">
<Loader2 className="h-5 w-5 animate-spin" />
</div>
<div className="mt-4 text-base font-semibold text-slate-950 dark:text-slate-50">
{t('productCenter.loading')}
</div>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{t('productCenter.loadingDesc')}
</p>
</div>
</div>
)}
</section>
</div>
);
}
export default ProductCenter;