feat: Replace floating danmaku with a terminal-style Message Board component
parent
4b214980cf
commit
33ac56aec1
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { message, Modal, Input } from 'antd';
|
import { message } from 'antd';
|
||||||
import { useSpaceData } from './hooks/useSpaceData';
|
import { useSpaceData } from './hooks/useSpaceData';
|
||||||
import { useHistoricalData } from './hooks/useHistoricalData';
|
import { useHistoricalData } from './hooks/useHistoricalData';
|
||||||
import { useTrajectory } from './hooks/useTrajectory';
|
import { useTrajectory } from './hooks/useTrajectory';
|
||||||
|
|
@ -13,9 +13,8 @@ import { Loading } from './components/Loading';
|
||||||
import { InterstellarTicker } from './components/InterstellarTicker';
|
import { InterstellarTicker } from './components/InterstellarTicker';
|
||||||
import { ControlPanel } from './components/ControlPanel';
|
import { ControlPanel } from './components/ControlPanel';
|
||||||
import { AuthModal } from './components/AuthModal';
|
import { AuthModal } from './components/AuthModal';
|
||||||
import { DanmakuLayer } from './components/DanmakuLayer';
|
import { MessageBoard } from './components/MessageBoard';
|
||||||
import { auth } from './utils/auth';
|
import { auth } from './utils/auth';
|
||||||
import { request } from './utils/request';
|
|
||||||
import type { CelestialBody } from './types';
|
import type { CelestialBody } from './types';
|
||||||
|
|
||||||
// Timeline configuration - will be fetched from backend later
|
// Timeline configuration - will be fetched from backend later
|
||||||
|
|
@ -30,7 +29,7 @@ function App() {
|
||||||
const [isTimelineMode, setIsTimelineMode] = useState(false); // Usually not persisted
|
const [isTimelineMode, setIsTimelineMode] = useState(false); // Usually not persisted
|
||||||
const [showOrbits, setShowOrbits] = useState(true);
|
const [showOrbits, setShowOrbits] = useState(true);
|
||||||
const [isSoundOn, setIsSoundOn] = useState(false);
|
const [isSoundOn, setIsSoundOn] = useState(false);
|
||||||
const [showDanmaku, setShowDanmaku] = useState(true);
|
const [showMessageBoard, setShowMessageBoard] = useState(false);
|
||||||
|
|
||||||
// Initialize state from localStorage
|
// Initialize state from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -40,7 +39,6 @@ function App() {
|
||||||
const prefs = JSON.parse(savedPrefs);
|
const prefs = JSON.parse(savedPrefs);
|
||||||
if (prefs.showOrbits !== undefined) setShowOrbits(prefs.showOrbits);
|
if (prefs.showOrbits !== undefined) setShowOrbits(prefs.showOrbits);
|
||||||
if (prefs.isSoundOn !== undefined) setIsSoundOn(prefs.isSoundOn);
|
if (prefs.isSoundOn !== undefined) setIsSoundOn(prefs.isSoundOn);
|
||||||
if (prefs.showDanmaku !== undefined) setShowDanmaku(prefs.showDanmaku);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to parse preferences:', e);
|
console.error('Failed to parse preferences:', e);
|
||||||
}
|
}
|
||||||
|
|
@ -52,10 +50,9 @@ function App() {
|
||||||
const prefs = {
|
const prefs = {
|
||||||
showOrbits,
|
showOrbits,
|
||||||
isSoundOn,
|
isSoundOn,
|
||||||
showDanmaku
|
|
||||||
};
|
};
|
||||||
localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
|
localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
|
||||||
}, [showOrbits, isSoundOn, showDanmaku]);
|
}, [showOrbits, isSoundOn]);
|
||||||
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||||
const { takeScreenshot } = useScreenshot();
|
const { takeScreenshot } = useScreenshot();
|
||||||
|
|
@ -65,10 +62,6 @@ function App() {
|
||||||
const [user, setUser] = useState<any>(auth.getUser());
|
const [user, setUser] = useState<any>(auth.getUser());
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||||
|
|
||||||
// Danmaku state
|
|
||||||
const [isDanmakuInputVisible, setIsDanmakuInputVisible] = useState(false);
|
|
||||||
const [danmakuText, setDanmakuText] = useState('');
|
|
||||||
|
|
||||||
// Use real-time data or historical data based on mode
|
// Use real-time data or historical data based on mode
|
||||||
const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData();
|
const { bodies: realTimeBodies, loading: realTimeLoading, error: realTimeError } = useSpaceData();
|
||||||
const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate);
|
const { bodies: historicalBodies, loading: historicalLoading, error: historicalError } = useHistoricalData(selectedDate);
|
||||||
|
|
@ -118,29 +111,6 @@ function App() {
|
||||||
takeScreenshot(nickname);
|
takeScreenshot(nickname);
|
||||||
}, [user, takeScreenshot]);
|
}, [user, takeScreenshot]);
|
||||||
|
|
||||||
// Danmaku send handler
|
|
||||||
const handleSendDanmaku = async () => {
|
|
||||||
if (!danmakuText.trim()) return;
|
|
||||||
if (danmakuText.length > 20) {
|
|
||||||
message.warning("弹幕内容不能超过20字");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!user) {
|
|
||||||
message.warning("请先登录");
|
|
||||||
setShowAuthModal(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await request.post('/danmaku/send', { text: danmakuText });
|
|
||||||
message.success("发送成功");
|
|
||||||
setDanmakuText('');
|
|
||||||
setIsDanmakuInputVisible(false);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
message.error("发送失败");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auth handlers
|
// Auth handlers
|
||||||
const handleLoginSuccess = (userData: any) => {
|
const handleLoginSuccess = (userData: any) => {
|
||||||
setUser(userData);
|
setUser(userData);
|
||||||
|
|
@ -184,9 +154,6 @@ function App() {
|
||||||
onNavigateToAdmin={() => navigate('/admin')}
|
onNavigateToAdmin={() => navigate('/admin')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Danmaku Layer */}
|
|
||||||
<DanmakuLayer enabled={showDanmaku} />
|
|
||||||
|
|
||||||
{/* Right Control Panel */}
|
{/* Right Control Panel */}
|
||||||
<ControlPanel
|
<ControlPanel
|
||||||
isTimelineMode={isTimelineMode}
|
isTimelineMode={isTimelineMode}
|
||||||
|
|
@ -195,16 +162,8 @@ function App() {
|
||||||
onToggleOrbits={() => setShowOrbits(!showOrbits)}
|
onToggleOrbits={() => setShowOrbits(!showOrbits)}
|
||||||
isSoundOn={isSoundOn}
|
isSoundOn={isSoundOn}
|
||||||
onToggleSound={() => setIsSoundOn(!isSoundOn)}
|
onToggleSound={() => setIsSoundOn(!isSoundOn)}
|
||||||
showDanmaku={showDanmaku}
|
showMessageBoard={showMessageBoard}
|
||||||
onToggleDanmaku={() => setShowDanmaku(!showDanmaku)}
|
onToggleMessageBoard={() => setShowMessageBoard(!showMessageBoard)}
|
||||||
onOpenDanmakuInput={() => {
|
|
||||||
if (!user) {
|
|
||||||
message.warning("请先登录");
|
|
||||||
setShowAuthModal(true);
|
|
||||||
} else {
|
|
||||||
setIsDanmakuInputVisible(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onScreenshot={handleScreenshot}
|
onScreenshot={handleScreenshot}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -215,26 +174,11 @@ function App() {
|
||||||
onLoginSuccess={handleLoginSuccess}
|
onLoginSuccess={handleLoginSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Danmaku Input Modal */}
|
{/* Message Board */}
|
||||||
<Modal
|
<MessageBoard
|
||||||
title="发送星际弹幕"
|
open={showMessageBoard}
|
||||||
open={isDanmakuInputVisible}
|
onClose={() => setShowMessageBoard(false)}
|
||||||
onOk={handleSendDanmaku}
|
/>
|
||||||
onCancel={() => setIsDanmakuInputVisible(false)}
|
|
||||||
okText="发射"
|
|
||||||
cancelText="取消"
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder="输入想说的话 (20字以内)"
|
|
||||||
maxLength={20}
|
|
||||||
showCount
|
|
||||||
value={danmakuText}
|
|
||||||
onChange={e => setDanmakuText(e.target.value)}
|
|
||||||
onPressEnter={handleSendDanmaku}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Probe List Sidebar */}
|
{/* Probe List Sidebar */}
|
||||||
<ProbeList
|
<ProbeList
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,7 @@ import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Camera,
|
Camera
|
||||||
Send
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface ControlPanelProps {
|
interface ControlPanelProps {
|
||||||
|
|
@ -17,9 +16,8 @@ interface ControlPanelProps {
|
||||||
onToggleOrbits: () => void;
|
onToggleOrbits: () => void;
|
||||||
isSoundOn: boolean;
|
isSoundOn: boolean;
|
||||||
onToggleSound: () => void;
|
onToggleSound: () => void;
|
||||||
showDanmaku: boolean;
|
showMessageBoard: boolean;
|
||||||
onToggleDanmaku: () => void;
|
onToggleMessageBoard: () => void;
|
||||||
onOpenDanmakuInput: () => void;
|
|
||||||
onScreenshot: () => void;
|
onScreenshot: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,9 +28,8 @@ export function ControlPanel({
|
||||||
onToggleOrbits,
|
onToggleOrbits,
|
||||||
isSoundOn,
|
isSoundOn,
|
||||||
onToggleSound,
|
onToggleSound,
|
||||||
showDanmaku,
|
showMessageBoard,
|
||||||
onToggleDanmaku,
|
onToggleMessageBoard,
|
||||||
onOpenDanmakuInput,
|
|
||||||
onScreenshot,
|
onScreenshot,
|
||||||
}: ControlPanelProps) {
|
}: ControlPanelProps) {
|
||||||
const buttonClass = (isActive: boolean) => `
|
const buttonClass = (isActive: boolean) => `
|
||||||
|
|
@ -80,29 +77,16 @@ export function ControlPanel({
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Danmaku Toggle */}
|
{/* Message Board Toggle */}
|
||||||
<div className="relative flex items-center">
|
<button
|
||||||
{showDanmaku && (
|
onClick={onToggleMessageBoard}
|
||||||
<button
|
className={buttonClass(showMessageBoard)}
|
||||||
onClick={onOpenDanmakuInput}
|
>
|
||||||
className="mr-2 p-2 rounded-lg bg-white/10 text-gray-300 hover:bg-white/20 border border-white/5 transition-all duration-200 relative group"
|
<MessageSquare size={20} />
|
||||||
>
|
<div className={tooltipClass}>
|
||||||
<Send size={16} />
|
{showMessageBoard ? '关闭留言板' : '打开留言板'}
|
||||||
<div className={tooltipClass}>
|
</div>
|
||||||
发送弹幕
|
</button>
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={onToggleDanmaku}
|
|
||||||
className={buttonClass(showDanmaku)}
|
|
||||||
>
|
|
||||||
<MessageSquare size={20} />
|
|
||||||
<div className={tooltipClass}>
|
|
||||||
{showDanmaku ? '关闭弹幕' : '开启弹幕'}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Screenshot Button */}
|
{/* Screenshot Button */}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Input, Button, message } from 'antd';
|
||||||
|
import { Send, MessageSquare } from 'lucide-react';
|
||||||
|
import { TerminalModal } from './TerminalModal';
|
||||||
|
import { request } from '../utils/request';
|
||||||
|
import { auth } from '../utils/auth';
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string;
|
||||||
|
uid: string;
|
||||||
|
username: string;
|
||||||
|
text: string;
|
||||||
|
ts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageBoardProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessageBoard({ open, onClose }: MessageBoardProps) {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
|
const intervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const fetchMessages = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await request.get('/danmaku/list');
|
||||||
|
setMessages(data.sort((a: Message, b: Message) => a.ts - b.ts));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch messages", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setLoading(true);
|
||||||
|
fetchMessages().finally(() => setLoading(false));
|
||||||
|
intervalRef.current = window.setInterval(fetchMessages, 3000);
|
||||||
|
} else {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
return () => clearInterval(intervalRef.current);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), 100);
|
||||||
|
}
|
||||||
|
}, [messages, open]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
const content = inputValue.trim();
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
const user = auth.getUser();
|
||||||
|
if (!user) {
|
||||||
|
message.warning('请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length > 20) {
|
||||||
|
message.warning('消息不能超过20字');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSending(true);
|
||||||
|
try {
|
||||||
|
await request.post('/danmaku/send', { text: content });
|
||||||
|
setInputValue('');
|
||||||
|
await fetchMessages();
|
||||||
|
} catch (err) {
|
||||||
|
message.error('发送失败');
|
||||||
|
} finally {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TerminalModal
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
title={
|
||||||
|
<div className="flex items-center gap-2 text-[#2ea043] font-mono tracking-wider text-xs">
|
||||||
|
<MessageSquare size={14} />
|
||||||
|
INTERSTELLAR COMM LINK // PUBLIC CHANNEL
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
loading={loading && messages.length === 0}
|
||||||
|
loadingText="SCANNING FREQUENCIES..."
|
||||||
|
footer={
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Transmitting message... (Max 20 chars)"
|
||||||
|
maxLength={20}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onPressEnter={handleSend}
|
||||||
|
className="bg-black border-[#238636] text-[#2ea043] font-mono placeholder:text-[#2ea043]/30 focus:bg-black focus:border-[#2ea043] hover:border-[#2ea043]"
|
||||||
|
style={{ backgroundColor: '#0d1117', color: '#2ea043' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Send size={14} />}
|
||||||
|
loading={sending}
|
||||||
|
onClick={handleSend}
|
||||||
|
className="bg-[#238636] border-[#238636] hover:bg-[#2ea043] hover:border-[#2ea043] text-black font-bold font-mono"
|
||||||
|
>
|
||||||
|
SEND
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<div key={msg.id} className="flex flex-col gap-1 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||||
|
<div className="flex items-center gap-2 text-[10px] opacity-50">
|
||||||
|
<span className="text-[#2ea043]">{new Date(msg.ts * 1000).toLocaleTimeString()}</span>
|
||||||
|
<span className="text-cyan-500">{'<'}{msg.username}{'>'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="pl-2 border-l-2 border-[#238636]/30 text-[#2ea043] break-words font-bold">
|
||||||
|
{msg.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{messages.length === 0 && !loading && (
|
||||||
|
<div className="text-center opacity-30 py-10 italic text-[#2ea043]">
|
||||||
|
NO SIGNALS DETECTED
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={bottomRef} />
|
||||||
|
</div>
|
||||||
|
</TerminalModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { Modal, Spin } from 'antd';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface TerminalModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
title: ReactNode;
|
||||||
|
loading?: boolean;
|
||||||
|
loadingText?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TerminalModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
loading = false,
|
||||||
|
loadingText = "PROCESSING...",
|
||||||
|
children,
|
||||||
|
footer = null,
|
||||||
|
width = 800
|
||||||
|
}: TerminalModalProps) {
|
||||||
|
|
||||||
|
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;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.terminal-modal .ant-modal-body {
|
||||||
|
background-color: #0d1117 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.terminal-modal .ant-modal-body .animate-in {
|
||||||
|
color: #2ea043 !important;
|
||||||
|
}
|
||||||
|
.terminal-modal .ant-modal-header {
|
||||||
|
background-color: #161b22 !important;
|
||||||
|
border-bottom: 1px solid #238636 !important;
|
||||||
|
padding: 12px 20px !important;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.terminal-modal .ant-modal-footer {
|
||||||
|
background-color: #161b22 !important;
|
||||||
|
border-top: 1px solid #238636 !important;
|
||||||
|
padding: 12px 20px !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.terminal-modal .ant-modal-title {
|
||||||
|
color: #2ea043 !important;
|
||||||
|
}
|
||||||
|
.terminal-modal .ant-modal-close {
|
||||||
|
color: #2ea043 !important;
|
||||||
|
top: 12px !important;
|
||||||
|
}
|
||||||
|
.terminal-modal .ant-modal-close:hover {
|
||||||
|
background-color: rgba(35, 134, 54, 0.2) !important;
|
||||||
|
}
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.terminal-scroll::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
.terminal-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: #238636;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.terminal-scroll::-webkit-scrollbar-track {
|
||||||
|
background: #0d1117;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{terminalStyles}</style>
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={footer}
|
||||||
|
width={width}
|
||||||
|
centered
|
||||||
|
className="terminal-modal"
|
||||||
|
title={title}
|
||||||
|
closeIcon={<X size={18} />}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="h-[60vh] flex items-center justify-center flex-col gap-4 text-[#2ea043]">
|
||||||
|
<Spin size="large" />
|
||||||
|
<div className="animate-pulse tracking-widest font-mono">{loadingText}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-[60vh] flex flex-col bg-[#0d1117]">
|
||||||
|
<div className="flex-1 overflow-auto terminal-scroll font-mono text-xs p-5">
|
||||||
|
<div className="animate-in fade-in duration-500">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue