144 lines
5.5 KiB
TypeScript
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;
|