mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
refactor: move ConnectionQR into standard Layout with proper navigation
- Wrap /connection/qr route in ProtectedRoute with Layout - Remove fullscreen overlay and custom close button - Add AdminBackButton with replace prop to avoid history cycles - Use replace navigation from Connection to QR page - Add replace prop support to AdminBackButton component
This commit is contained in:
@@ -406,9 +406,11 @@ function App() {
|
|||||||
<Route
|
<Route
|
||||||
path="/connection/qr"
|
path="/connection/qr"
|
||||||
element={
|
element={
|
||||||
<LazyPage>
|
<ProtectedRoute>
|
||||||
<ConnectionQR />
|
<LazyPage>
|
||||||
</LazyPage>
|
<ConnectionQR />
|
||||||
|
</LazyPage>
|
||||||
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { BackIcon } from './icons';
|
|||||||
|
|
||||||
interface AdminBackButtonProps {
|
interface AdminBackButtonProps {
|
||||||
to?: string;
|
to?: string;
|
||||||
|
replace?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ interface AdminBackButtonProps {
|
|||||||
* Back button for admin pages.
|
* Back button for admin pages.
|
||||||
* Hidden in Telegram Mini App since native back button is used instead.
|
* Hidden in Telegram Mini App since native back button is used instead.
|
||||||
*/
|
*/
|
||||||
export function AdminBackButton({ to = '/admin', className }: AdminBackButtonProps) {
|
export function AdminBackButton({ to = '/admin', replace, className }: AdminBackButtonProps) {
|
||||||
const { platform } = usePlatform();
|
const { platform } = usePlatform();
|
||||||
|
|
||||||
// In Telegram Mini App, we use native back button
|
// In Telegram Mini App, we use native back button
|
||||||
@@ -22,6 +23,7 @@ export function AdminBackButton({ to = '/admin', className }: AdminBackButtonPro
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
|
replace={replace}
|
||||||
className={
|
className={
|
||||||
className ||
|
className ||
|
||||||
'flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600'
|
'flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600'
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default function Connection() {
|
|||||||
|
|
||||||
const handleOpenQR = useCallback(() => {
|
const handleOpenQR = useCallback(() => {
|
||||||
navigate('/connection/qr', {
|
navigate('/connection/qr', {
|
||||||
|
replace: true,
|
||||||
state: {
|
state: {
|
||||||
url: appConfig?.subscriptionUrl,
|
url: appConfig?.subscriptionUrl,
|
||||||
hideLink: appConfig?.hideLink ?? false,
|
hideLink: appConfig?.hideLink ?? false,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useEffect } from 'react';
|
|||||||
import { useLocation, useNavigate } from 'react-router';
|
import { useLocation, useNavigate } from 'react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { useAuthStore } from '../store/auth';
|
|
||||||
import { useBranding } from '../hooks/useBranding';
|
import { useBranding } from '../hooks/useBranding';
|
||||||
|
import { AdminBackButton } from '@/components/admin';
|
||||||
|
|
||||||
interface ConnectionQRState {
|
interface ConnectionQRState {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -20,96 +20,56 @@ export default function ConnectionQR() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
|
||||||
const isLoading = useAuthStore((state) => state.isLoading);
|
|
||||||
const { appName } = useBranding();
|
const { appName } = useBranding();
|
||||||
|
|
||||||
const state = location.state as unknown;
|
const state = location.state as unknown;
|
||||||
const validState = isValidState(state) ? state : null;
|
const validState = isValidState(state) ? state : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !isAuthenticated) {
|
if (!validState) {
|
||||||
navigate('/login', { replace: true });
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, isLoading, navigate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && isAuthenticated && !validState) {
|
|
||||||
navigate('/connection', { replace: true });
|
navigate('/connection', { replace: true });
|
||||||
}
|
}
|
||||||
}, [isLoading, isAuthenticated, validState, navigate]);
|
}, [validState, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
if (!validState) {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
navigate(-1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
if (isLoading || !validState) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex min-h-dvh flex-col items-center justify-center bg-gradient-to-b from-dark-900 to-dark-950">
|
<div className="animate-fade-in">
|
||||||
{/* Close button */}
|
<div className="mb-6 flex items-center gap-3">
|
||||||
<button
|
<AdminBackButton to="/connection" replace />
|
||||||
type="button"
|
<h1 className="text-2xl font-bold text-dark-100">{t('subscription.connection.qrTitle')}</h1>
|
||||||
onClick={() => navigate(-1)}
|
</div>
|
||||||
className="absolute left-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-dark-800/60 backdrop-blur-sm transition-colors hover:bg-dark-700/80"
|
|
||||||
aria-label={t('common.close')}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="h-5 w-5 text-dark-200"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Content */}
|
<div className="flex flex-col items-center">
|
||||||
<div className="flex w-full max-w-sm animate-scale-in flex-col items-center px-6">
|
<div className="flex w-full max-w-sm flex-col items-center px-6">
|
||||||
{/* Branding name */}
|
{appName && (
|
||||||
{appName && (
|
<p className="mb-3 text-sm font-medium uppercase tracking-wider text-dark-400">
|
||||||
<p className="mb-3 text-sm font-medium uppercase tracking-wider text-dark-400">
|
{appName}
|
||||||
{appName}
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="mb-8 text-center text-sm text-dark-400">
|
||||||
|
{t('subscription.connection.qrScanHint')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Title */}
|
<div className="rounded-3xl bg-white p-6">
|
||||||
<h1 className="mb-2 text-center text-xl font-bold text-dark-100">
|
<QRCodeSVG
|
||||||
{t('subscription.connection.qrTitle')}
|
value={validState.url}
|
||||||
</h1>
|
size={280}
|
||||||
|
level="M"
|
||||||
|
includeMargin={false}
|
||||||
|
className="h-[280px] w-[280px] sm:h-[360px] sm:w-[360px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hint */}
|
{!validState.hideLink && (
|
||||||
<p className="mb-8 text-center text-sm text-dark-400">
|
<p className="mt-6 max-w-full truncate text-center font-mono text-xs text-dark-500">
|
||||||
{t('subscription.connection.qrScanHint')}
|
{validState.url}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
{/* QR code container */}
|
|
||||||
<div className="rounded-3xl bg-white p-6">
|
|
||||||
<QRCodeSVG
|
|
||||||
value={validState.url}
|
|
||||||
size={280}
|
|
||||||
level="M"
|
|
||||||
includeMargin={false}
|
|
||||||
className="h-[280px] w-[280px] sm:h-[360px] sm:w-[360px]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* URL display */}
|
|
||||||
{!validState.hideLink && (
|
|
||||||
<p className="mt-6 max-w-full truncate text-center font-mono text-xs text-dark-500">
|
|
||||||
{validState.url}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user