Files
Nexus_Mat/components/MaterialDetail.tsx

255 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { X, Heart, MessageCircle, Download, User, Code, Play, Package, Trash2, ExternalLink, Sparkles, Copy, Share2, Cpu, MessageSquare } from 'lucide-react';
import { MaterialDTO, MaterialType, UserDTO } from '../types';
import { DataService } from '../services/dataService';
import { GeminiService } from '../services/geminiService';
import { useToast } from './ToastProvider';
import Image from 'next/image';
interface Props {
id: string;
onClose: () => void;
currentUser: UserDTO | null;
}
export const MaterialDetail: React.FC<Props> = ({ id, onClose, currentUser }) => {
const [material, setMaterial] = useState<MaterialDTO | null>(null);
const [loading, setLoading] = useState(true);
const [commentText, setCommentText] = useState('');
const [aiAnalysis, setAiAnalysis] = useState('');
const [analyzingCode, setAnalyzingCode] = useState(false);
const [analyzing, setAnalyzing] = useState(false);
const toast = useToast();
const getAvatar = (username: string, url?: string) => {
if (url && url.trim()) return url;
const svg = encodeURIComponent(
`<svg xmlns='http://www.w3.org/2000/svg' width='48' height='48'>`+
`<rect width='100%' height='100%' fill='#0b0b0b'/>`+
`<circle cx='24' cy='24' r='21' fill='#111' stroke='#39ff14'/>`+
`<text x='50%' y='56%' dominant-baseline='middle' text-anchor='middle' fill='#39ff14' font-size='14' font-family='monospace'>NM</text>`+
`</svg>`
);
return `data:image/svg+xml;utf8,${svg}`;
};
const loadData = useCallback(async () => {
try {
setLoading(true);
const data = await DataService.getMaterialById(id);
setMaterial(data);
} catch (error) {
toast.error('Failed to load material details');
console.error('Error loading material:', error);
} finally {
setLoading(false);
}
}, [id, toast]);
useEffect(() => {
loadData();
}, [loadData]);
const handleAiExplain = async () => {
if (!material?.codeSnippet) return;
try {
setAnalyzing(true);
const text = await GeminiService.explainCode(material.codeSnippet);
setAiAnalysis(text);
} catch (error) {
toast.error('AI analysis failed');
console.error('Error getting AI analysis:', error);
} finally {
setAnalyzing(false);
}
};
const handleCopy = () => {
if (material?.codeSnippet) {
navigator.clipboard.writeText(material.codeSnippet);
toast.success('Code copied to clipboard');
}
};
const handleDownload = () => {
toast.info(`Preparing secure download for protocol: ${material?.id}`);
};
const handleComment = async () => {
if (!commentText.trim() || !material) return;
try {
await DataService.addComment(material.id, commentText);
setCommentText('');
toast.success('Comment added');
loadData();
} catch (error) {
toast.error('Please login to comment');
}
};
const handleDelete = async () => {
if (confirm('CONFIRM DELETION PROTOCOL?')) {
try {
await DataService.deleteMaterial(id);
toast.success('Material deleted');
onClose();
} catch (error: any) {
toast.error(error?.message || 'Only author/admin can delete');
}
}
};
if (loading) return <div className="fixed inset-0 z-50 flex items-center justify-center bg-cyber-black/90 text-cyber-neon font-mono">LOADING_ASSET...</div>;
if (!material) return null;
return (
<div className="fixed inset-0 z-40 overflow-y-auto bg-black/60 backdrop-blur-md flex items-center justify-center p-4">
<div className="bg-cyber-dark border border-cyber-panel w-full max-w-5xl rounded-lg shadow-2xl shadow-black relative flex flex-col md:flex-row overflow-hidden max-h-[90vh]">
<button onClick={onClose} className="absolute top-4 right-4 z-50 p-2 bg-black/50 rounded-full hover:bg-cyber-pink hover:text-white transition-colors text-gray-400">
<X size={20} />
</button>
<div className="w-full md:w-2/3 bg-black/40 border-r border-cyber-panel flex flex-col overflow-hidden">
<div className="p-6 border-b border-cyber-panel">
<div className="flex items-center gap-3 mb-2">
<span className="px-2 py-0.5 bg-cyber-neon/10 text-cyber-neon text-xs font-mono rounded border border-cyber-neon/20">
{material.type}
</span>
<h1 className="text-2xl font-bold font-mono text-white">{material.title}</h1>
</div>
<div className="flex flex-wrap gap-2 mt-2">
{material.tags.map(t => (
<span key={t} className="text-xs text-gray-500 font-mono">#{t}</span>
))}
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 bg-cyber-black/50 relative group">
{material.type === MaterialType.CODE && (
<div className="relative">
<pre className="font-mono text-sm text-gray-300 p-4 bg-[#0d0d0d] rounded border border-gray-800 overflow-x-auto whitespace-pre-wrap">
{material.codeSnippet}
</pre>
<button onClick={handleCopy} className="absolute top-2 right-2 p-2 bg-cyber-panel rounded hover:text-cyber-neon transition-colors">
<Copy size={16} />
</button>
</div>
)}
{material.type === MaterialType.VIDEO && (
<div className="aspect-video bg-black flex items-center justify-center border border-gray-800 relative overflow-hidden">
<video src={material.contentUrl} controls className="w-full h-full object-cover opacity-80 hover:opacity-100 transition-opacity" />
<div className="absolute inset-0 pointer-events-none bg-gradient-to-t from-black/50 to-transparent"></div>
</div>
)}
{material.type === MaterialType.ASSET_ZIP && (
<div className="h-64 flex flex-col items-center justify-center border-2 border-dashed border-gray-700 rounded-lg bg-gray-900/20">
<Package size={64} className="text-cyber-neon mb-4 animate-pulse-fast" />
<p className="font-mono text-gray-400">ENCRYPTED_ARCHIVE.ZIP</p>
</div>
)}
{material.type === MaterialType.CODE && (
<div className="mt-6 border-t border-cyber-panel pt-6">
<button
onClick={handleAiExplain}
disabled={analyzing}
className="flex items-center gap-2 text-xs font-mono text-cyber-blue hover:text-white transition-colors disabled:opacity-50 mb-2"
>
<Cpu size={14} />
{analyzing ? 'ANALYZING_NEURAL_NET...' : 'AI_CODE_REVIEW'}
</button>
{aiAnalysis && (
<div className="bg-cyber-blue/5 border-l-2 border-cyber-blue p-4 text-sm text-gray-300 font-sans">
<p className="whitespace-pre-line">{aiAnalysis}</p>
</div>
)}
</div>
)}
</div>
<div className="p-4 border-t border-cyber-panel bg-cyber-panel/30 flex justify-between items-center">
<div className="flex gap-4">
<button onClick={() => DataService.toggleFavorite(material.id).then(() => loadData())} className="flex items-center gap-2 text-gray-400 hover:text-cyber-pink transition-colors">
<Heart size={20} className={material.stats.favorites > 0 ? "fill-cyber-pink text-cyber-pink" : ""} />
<span className="font-mono text-sm">{material.stats.favorites}</span>
</button>
<button className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
<Share2 size={20} />
</button>
</div>
<div className="flex gap-3">
{currentUser?.id === material.author.id && (
<button onClick={handleDelete} className="p-2 text-red-500 hover:bg-red-900/20 rounded">
<Trash2 size={18} />
</button>
)}
<button
onClick={handleDownload}
className="flex items-center gap-2 px-6 py-2 bg-cyber-neon text-black font-bold font-mono text-sm hover:bg-white transition-colors"
>
<Download size={16} /> DOWNLOAD_ASSET
</button>
</div>
</div>
</div>
<div className="w-full md:w-1/3 bg-cyber-dark flex flex-col">
<div className="p-6 border-b border-cyber-panel flex items-center gap-4">
<Image src={getAvatar(material.author.username, material.author.avatarUrl)} alt="author" width={48} height={48} className="w-12 h-12 rounded-full ring-2 ring-cyber-panel" />
<div>
<h3 className="font-mono text-white font-bold">{material.author.username}</h3>
<p className="text-xs text-gray-500">Joined {new Date(material.author.createdAt).toLocaleDateString()}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-px bg-cyber-panel border-b border-cyber-panel">
<div className="bg-cyber-dark p-4 text-center">
<p className="text-xs text-gray-500 font-mono mb-1">VIEWS</p>
<p className="text-xl text-white font-bold">{material.stats.views}</p>
</div>
<div className="bg-cyber-dark p-4 text-center">
<p className="text-xs text-gray-500 font-mono mb-1">DOWNLOADS</p>
<p className="text-xl text-white font-bold">{material.stats.downloads}</p>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-4">
<h4 className="text-xs font-mono text-gray-500 uppercase">Comm_Log ({material.comments.length})</h4>
{material.comments.map(c => (
<div key={c.id} className="bg-cyber-panel/50 p-3 rounded border border-white/5">
<div className="flex justify-between items-start mb-2">
<span className="text-cyber-neon text-xs font-bold">{c.author.username}</span>
<span className="text-[10px] text-gray-600">{new Date(c.createdAt).toLocaleDateString()}</span>
</div>
<p className="text-sm text-gray-300">{c.content}</p>
</div>
))}
{material.comments.length === 0 && (
<div className="text-center py-10 opacity-30 text-xs font-mono">NO_TRANSMISSIONS_YET</div>
)}
</div>
<div className="p-4 border-t border-cyber-panel bg-cyber-panel/20">
<div className="relative">
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
className="w-full bg-black/50 border border-cyber-panel rounded p-3 text-sm text-white focus:border-cyber-neon focus:outline-none resize-none h-20 font-sans"
placeholder="Enter secure message..."
/>
<button
onClick={handleComment}
className="absolute bottom-2 right-2 p-1.5 bg-cyber-neon text-black rounded hover:bg-white transition-colors"
>
<MessageSquare size={14} />
</button>
</div>
</div>
</div>
</div>
</div>
);
};