255 lines
11 KiB
TypeScript
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>
|
|
);
|
|
};
|