fix: restore toast auto-close and implement authentication flow
parent
e2a9052e57
commit
d567377835
|
|
@ -103,14 +103,15 @@ function App() {
|
|||
// Screenshot handler with auth check
|
||||
const handleScreenshot = useCallback(() => {
|
||||
if (!user) {
|
||||
toast.warning('请先登录以拍摄宇宙快照');
|
||||
setShowAuthModal(true);
|
||||
toast.warning('请先登录以拍摄宇宙快照', 3000, () => {
|
||||
setShowAuthModal(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Use username or full_name or fallback
|
||||
const nickname = user.full_name || user.username || 'Explorer';
|
||||
takeScreenshot(nickname);
|
||||
}, [user, takeScreenshot]);
|
||||
}, [user, takeScreenshot, toast]);
|
||||
|
||||
// Auth handlers
|
||||
const handleLoginSuccess = (userData: any) => {
|
||||
|
|
@ -164,7 +165,15 @@ function App() {
|
|||
isSoundOn={isSoundOn}
|
||||
onToggleSound={() => setIsSoundOn(!isSoundOn)}
|
||||
showMessageBoard={showMessageBoard}
|
||||
onToggleMessageBoard={() => setShowMessageBoard(!showMessageBoard)}
|
||||
onToggleMessageBoard={() => {
|
||||
if (!user) {
|
||||
toast.warning('请先登录以访问留言板', 3000, () => {
|
||||
setShowAuthModal(true);
|
||||
});
|
||||
} else {
|
||||
setShowMessageBoard(!showMessageBoard);
|
||||
}
|
||||
}}
|
||||
onScreenshot={handleScreenshot}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -41,35 +41,7 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const terminalStyles = `
|
||||
.terminal-modal .ant-modal-content {
|
||||
background-color: #0d1117 !important;
|
||||
border: 1px solid #238636 !important;
|
||||
box-shadow: 0 0 30px rgba(35, 134, 54, 0.15) !important;
|
||||
color: #2ea043 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.terminal-modal .ant-modal-body {
|
||||
background-color: #0d1117 !important;
|
||||
}
|
||||
.terminal-modal .ant-modal-header {
|
||||
background-color: #161b22 !important;
|
||||
border-bottom: 1px solid #238636 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.terminal-modal .ant-modal-title {
|
||||
color: #2ea043 !important;
|
||||
}
|
||||
.terminal-modal .ant-modal-close {
|
||||
color: #2ea043 !important;
|
||||
}
|
||||
.terminal-modal .ant-modal-close:hover {
|
||||
background-color: rgba(35, 134, 54, 0.2) !important;
|
||||
}
|
||||
.terminal-modal .ant-modal-body .animate-in {
|
||||
color: #2ea043 !important; /* Ensure content text is green */
|
||||
}
|
||||
const styles = `
|
||||
@keyframes spin-slow {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
|
|
@ -82,7 +54,7 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
|
|||
return (
|
||||
// Remove fixed positioning, now handled by parent container (Html component in 3D)
|
||||
<div className="flex flex-col items-center -translate-y-24 pointer-events-none">
|
||||
<style>{terminalStyles}</style>
|
||||
<style>{styles}</style>
|
||||
{/* Main Info Card */}
|
||||
<div className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-2xl p-5 min-w-[340px] max-w-md shadow-2xl pointer-events-auto relative group mb-2">
|
||||
|
||||
|
|
@ -165,54 +137,22 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
|
|||
{/* Connecting Line/Triangle pointing down to the body */}
|
||||
<div className="w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[8px] border-t-black/80 backdrop-blur-xl mt-[-1px]"></div>
|
||||
|
||||
{/* Terminal Modal */}
|
||||
<Modal
|
||||
<TerminalModal
|
||||
open={showTerminal}
|
||||
onCancel={() => setShowTerminal(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
centered
|
||||
className="terminal-modal"
|
||||
styles={{
|
||||
header: {
|
||||
backgroundColor: '#161b22',
|
||||
borderBottom: '1px solid #238636',
|
||||
padding: '12px 20px',
|
||||
marginBottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
mask: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
||||
backdropFilter: 'blur(4px)'
|
||||
},
|
||||
body: {
|
||||
padding: '20px',
|
||||
backgroundColor: '#0d1117'
|
||||
}
|
||||
}}
|
||||
onClose={() => setShowTerminal(false)}
|
||||
title={
|
||||
<div className="flex items-center gap-2 text-[#2ea043] font-mono tracking-wider text-xs">
|
||||
<div className="w-2 h-2 rounded-full bg-[#2ea043] animate-pulse"></div>
|
||||
JPL/HORIZONS SYSTEM INTERFACE // {body.name.toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
closeIcon={<X size={18} style={{ color: '#2ea043' }} />}
|
||||
loading={loading}
|
||||
loadingText="ESTABLISHING SECURE UPLINK..."
|
||||
>
|
||||
<div className="h-[60vh] overflow-auto font-mono text-xs whitespace-pre-wrap scrollbar-none">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full flex-col gap-4 text-[#2ea043]">
|
||||
<Spin indicator={<Radar className="animate-spin text-[#2ea043]" size={48} />} />
|
||||
<div className="animate-pulse tracking-widest">ESTABLISHING SECURE UPLINK...</div>
|
||||
<div className="text-[10px] opacity-50">Connecting to ssd.jpl.nasa.gov...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="animate-in fade-in duration-500">
|
||||
{terminalData}
|
||||
</div>
|
||||
)}
|
||||
<div className="whitespace-pre-wrap">
|
||||
{terminalData}
|
||||
</div>
|
||||
</Modal>
|
||||
</TerminalModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createContext, useContext, useState, useCallback, useRef } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react';
|
||||
|
||||
// Types
|
||||
|
|
@ -7,16 +8,17 @@ type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|||
interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
message: ReactNode;
|
||||
duration?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
showToast: (message: string, type?: ToastType, duration?: number) => string;
|
||||
success: (message: string, duration?: number) => string;
|
||||
error: (message: string, duration?: number) => string;
|
||||
warning: (message: string, duration?: number) => string;
|
||||
info: (message: string, duration?: number) => string;
|
||||
showToast: (message: ReactNode, type?: ToastType, duration?: number, onClose?: () => void) => string;
|
||||
success: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||
error: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||
warning: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||
info: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||
removeToast: (id: string) => void;
|
||||
}
|
||||
|
||||
|
|
@ -54,16 +56,22 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|||
const timersRef = useRef<Map<string, number>>(new Map());
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
setToasts((prev) => {
|
||||
const toast = prev.find(t => t.id === id);
|
||||
if (toast && toast.onClose) {
|
||||
toast.onClose();
|
||||
}
|
||||
return prev.filter((t) => t.id !== id);
|
||||
});
|
||||
if (timersRef.current.has(id)) {
|
||||
clearTimeout(timersRef.current.get(id));
|
||||
timersRef.current.delete(id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback((message: string, type: ToastType = 'info', duration = 3000) => {
|
||||
const showToast = useCallback((message: ReactNode, type: ToastType = 'info', duration = 3000, onClose?: () => void) => {
|
||||
const id = Math.random().toString(36).substring(2, 9);
|
||||
const newToast: Toast = { id, type, message, duration };
|
||||
const newToast: Toast = { id, type, message, duration, onClose };
|
||||
|
||||
setToasts((prev) => [...prev, newToast]);
|
||||
|
||||
|
|
@ -78,10 +86,10 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|||
}, [removeToast]);
|
||||
|
||||
// Convenience methods
|
||||
const success = useCallback((msg: string, d?: number) => showToast(msg, 'success', d), [showToast]);
|
||||
const error = useCallback((msg: string, d?: number) => showToast(msg, 'error', d), [showToast]);
|
||||
const warning = useCallback((msg: string, d?: number) => showToast(msg, 'warning', d), [showToast]);
|
||||
const info = useCallback((msg: string, d?: number) => showToast(msg, 'info', d), [showToast]);
|
||||
const success = useCallback((msg: ReactNode, d?: number, onClose?: () => void) => showToast(msg, 'success', d, onClose), [showToast]);
|
||||
const error = useCallback((msg: ReactNode, d = 3000, onClose?: () => void) => showToast(msg, 'error', d, onClose), [showToast]);
|
||||
const warning = useCallback((msg: ReactNode, d = 3000, onClose?: () => void) => showToast(msg, 'warning', d, onClose), [showToast]);
|
||||
const info = useCallback((msg: ReactNode, d?: number, onClose?: () => void) => showToast(msg, 'info', d, onClose), [showToast]);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast, success, error, warning, info, removeToast }}>
|
||||
|
|
@ -101,7 +109,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|||
`}
|
||||
>
|
||||
<div className="mt-0.5 shrink-0">{icons[toast.type]}</div>
|
||||
<p className="flex-1 text-sm font-medium leading-tight pt-0.5">{toast.message}</p>
|
||||
<div className="flex-1 text-sm font-medium leading-tight pt-0.5">{toast.message}</div>
|
||||
<button
|
||||
onClick={() => removeToast(toast.id)}
|
||||
className="text-white/40 hover:text-white transition-colors shrink-0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue