fix: restore toast auto-close and implement authentication flow
parent
e2a9052e57
commit
d567377835
|
|
@ -103,14 +103,15 @@ function App() {
|
||||||
// Screenshot handler with auth check
|
// Screenshot handler with auth check
|
||||||
const handleScreenshot = useCallback(() => {
|
const handleScreenshot = useCallback(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
toast.warning('请先登录以拍摄宇宙快照');
|
toast.warning('请先登录以拍摄宇宙快照', 3000, () => {
|
||||||
setShowAuthModal(true);
|
setShowAuthModal(true);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Use username or full_name or fallback
|
// Use username or full_name or fallback
|
||||||
const nickname = user.full_name || user.username || 'Explorer';
|
const nickname = user.full_name || user.username || 'Explorer';
|
||||||
takeScreenshot(nickname);
|
takeScreenshot(nickname);
|
||||||
}, [user, takeScreenshot]);
|
}, [user, takeScreenshot, toast]);
|
||||||
|
|
||||||
// Auth handlers
|
// Auth handlers
|
||||||
const handleLoginSuccess = (userData: any) => {
|
const handleLoginSuccess = (userData: any) => {
|
||||||
|
|
@ -164,7 +165,15 @@ function App() {
|
||||||
isSoundOn={isSoundOn}
|
isSoundOn={isSoundOn}
|
||||||
onToggleSound={() => setIsSoundOn(!isSoundOn)}
|
onToggleSound={() => setIsSoundOn(!isSoundOn)}
|
||||||
showMessageBoard={showMessageBoard}
|
showMessageBoard={showMessageBoard}
|
||||||
onToggleMessageBoard={() => setShowMessageBoard(!showMessageBoard)}
|
onToggleMessageBoard={() => {
|
||||||
|
if (!user) {
|
||||||
|
toast.warning('请先登录以访问留言板', 3000, () => {
|
||||||
|
setShowAuthModal(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setShowMessageBoard(!showMessageBoard);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onScreenshot={handleScreenshot}
|
onScreenshot={handleScreenshot}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,35 +41,7 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const terminalStyles = `
|
const styles = `
|
||||||
.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 */
|
|
||||||
}
|
|
||||||
@keyframes spin-slow {
|
@keyframes spin-slow {
|
||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
|
|
@ -82,7 +54,7 @@ export function FocusInfo({ body, onClose }: FocusInfoProps) {
|
||||||
return (
|
return (
|
||||||
// Remove fixed positioning, now handled by parent container (Html component in 3D)
|
// 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">
|
<div className="flex flex-col items-center -translate-y-24 pointer-events-none">
|
||||||
<style>{terminalStyles}</style>
|
<style>{styles}</style>
|
||||||
{/* Main Info Card */}
|
{/* 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">
|
<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 */}
|
{/* 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>
|
<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 */}
|
<TerminalModal
|
||||||
<Modal
|
|
||||||
open={showTerminal}
|
open={showTerminal}
|
||||||
onCancel={() => setShowTerminal(false)}
|
onClose={() => 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'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2 text-[#2ea043] font-mono tracking-wider text-xs">
|
<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>
|
<div className="w-2 h-2 rounded-full bg-[#2ea043] animate-pulse"></div>
|
||||||
JPL/HORIZONS SYSTEM INTERFACE // {body.name.toUpperCase()}
|
JPL/HORIZONS SYSTEM INTERFACE // {body.name.toUpperCase()}
|
||||||
</div>
|
</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">
|
<div className="whitespace-pre-wrap">
|
||||||
{loading ? (
|
{terminalData}
|
||||||
<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>
|
</div>
|
||||||
</Modal>
|
</TerminalModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { createContext, useContext, useState, useCallback, useRef } from 'react';
|
import { createContext, useContext, useState, useCallback, useRef } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react';
|
import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
@ -7,16 +8,17 @@ type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||||
interface Toast {
|
interface Toast {
|
||||||
id: string;
|
id: string;
|
||||||
type: ToastType;
|
type: ToastType;
|
||||||
message: string;
|
message: ReactNode;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToastContextValue {
|
interface ToastContextValue {
|
||||||
showToast: (message: string, type?: ToastType, duration?: number) => string;
|
showToast: (message: ReactNode, type?: ToastType, duration?: number, onClose?: () => void) => string;
|
||||||
success: (message: string, duration?: number) => string;
|
success: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||||
error: (message: string, duration?: number) => string;
|
error: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||||
warning: (message: string, duration?: number) => string;
|
warning: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||||
info: (message: string, duration?: number) => string;
|
info: (message: ReactNode, duration?: number, onClose?: () => void) => string;
|
||||||
removeToast: (id: string) => void;
|
removeToast: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,16 +56,22 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||||
const timersRef = useRef<Map<string, number>>(new Map());
|
const timersRef = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
const removeToast = useCallback((id: string) => {
|
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)) {
|
if (timersRef.current.has(id)) {
|
||||||
clearTimeout(timersRef.current.get(id));
|
clearTimeout(timersRef.current.get(id));
|
||||||
timersRef.current.delete(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 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]);
|
setToasts((prev) => [...prev, newToast]);
|
||||||
|
|
||||||
|
|
@ -78,10 +86,10 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||||
}, [removeToast]);
|
}, [removeToast]);
|
||||||
|
|
||||||
// Convenience methods
|
// Convenience methods
|
||||||
const success = useCallback((msg: string, d?: number) => showToast(msg, 'success', d), [showToast]);
|
const success = useCallback((msg: ReactNode, d?: number, onClose?: () => void) => showToast(msg, 'success', d, onClose), [showToast]);
|
||||||
const error = useCallback((msg: string, d?: number) => showToast(msg, 'error', d), [showToast]);
|
const error = useCallback((msg: ReactNode, d = 3000, onClose?: () => void) => showToast(msg, 'error', d, onClose), [showToast]);
|
||||||
const warning = useCallback((msg: string, d?: number) => showToast(msg, 'warning', d), [showToast]);
|
const warning = useCallback((msg: ReactNode, d = 3000, onClose?: () => void) => showToast(msg, 'warning', d, onClose), [showToast]);
|
||||||
const info = useCallback((msg: string, d?: number) => showToast(msg, 'info', d), [showToast]);
|
const info = useCallback((msg: ReactNode, d?: number, onClose?: () => void) => showToast(msg, 'info', d, onClose), [showToast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContext.Provider value={{ showToast, success, error, warning, info, removeToast }}>
|
<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>
|
<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
|
<button
|
||||||
onClick={() => removeToast(toast.id)}
|
onClick={() => removeToast(toast.id)}
|
||||||
className="text-white/40 hover:text-white transition-colors shrink-0"
|
className="text-white/40 hover:text-white transition-colors shrink-0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue