feat: Docker部署与CI/CD集成, 搜索栏修复, 上传目录改为data
This commit is contained in:
93
components/MaterialCard.tsx
Normal file
93
components/MaterialCard.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { MaterialDTO, MaterialType, UserDTO } from '../types';
|
||||
import { Code, Video, Package, Heart, Download, Eye, Terminal } from 'lucide-react';
|
||||
|
||||
interface MaterialCardProps {
|
||||
material: MaterialDTO;
|
||||
onClick: (id: string) => void;
|
||||
currentUser?: UserDTO | null;
|
||||
}
|
||||
|
||||
export const MaterialCard: React.FC<MaterialCardProps> = ({ material, onClick, currentUser }) => {
|
||||
const Icon = material.type === MaterialType.CODE ? Code : material.type === MaterialType.VIDEO ? Video : Package;
|
||||
const typeColor = material.type === MaterialType.CODE ? 'text-cyber-blue' : material.type === MaterialType.VIDEO ? 'text-cyber-pink' : 'text-cyber-neon';
|
||||
const borderColor = material.type === MaterialType.CODE ? 'hover:border-cyber-blue/50' : material.type === MaterialType.VIDEO ? 'hover:border-cyber-pink/50' : 'hover:border-cyber-neon/50';
|
||||
|
||||
// Check if current user has favorited this material
|
||||
const isFavorited = currentUser && material.favorites?.some(f => f.userId === currentUser.id);
|
||||
const siteIconSvg = encodeURIComponent(
|
||||
`<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'>`+
|
||||
`<rect width='100%' height='100%' fill='#0b0b0b'/>`+
|
||||
`<circle cx='16' cy='16' r='14' fill='#111' stroke='#39ff14'/>`+
|
||||
`<text x='50%' y='56%' dominant-baseline='middle' text-anchor='middle' fill='#39ff14' font-size='12' font-family='monospace'>NM</text>`+
|
||||
`</svg>`
|
||||
);
|
||||
const authorAvatar = material.author.avatarUrl && material.author.avatarUrl.trim()
|
||||
? material.author.avatarUrl
|
||||
: `data:image/svg+xml;utf8,${siteIconSvg}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onClick(material.id)}
|
||||
className={`group relative bg-cyber-panel/40 backdrop-blur-sm border border-white/5 ${borderColor} overflow-hidden cursor-pointer transition-all duration-500 hover:-translate-y-1 hover:shadow-2xl hover:shadow-cyber-neon/5`}
|
||||
>
|
||||
{/* Decoration Top Right */}
|
||||
<div className="absolute top-0 right-0 p-2">
|
||||
<Icon className={`w-5 h-5 ${typeColor} opacity-50 group-hover:opacity-100 transition-opacity`} />
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="p-6 h-full flex flex-col">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="text-[10px] font-mono text-gray-500 uppercase tracking-widest border border-gray-800 px-1 rounded">
|
||||
{material.type}
|
||||
</div>
|
||||
{material.language && (
|
||||
<div className="text-[10px] font-mono text-cyber-blue uppercase tracking-widest px-1">
|
||||
.{material.language}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold text-gray-100 mb-2 font-mono group-hover:text-cyber-neon transition-colors line-clamp-1">
|
||||
{material.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-400 mb-6 line-clamp-2 flex-grow font-sans">
|
||||
{material.description}
|
||||
</p>
|
||||
|
||||
{/* Preview (Code or Image placeholder) */}
|
||||
<div className="w-full h-24 bg-black/50 mb-4 rounded border border-white/5 p-2 font-mono text-xs text-gray-600 overflow-hidden relative">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-black/80 pointer-events-none"></div>
|
||||
{material.type === MaterialType.CODE ? (
|
||||
<pre>{material.codeSnippet}</pre>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-cyber-pink opacity-50">
|
||||
<Terminal size={24} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Stats */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/5 text-xs font-mono text-gray-500">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex items-center gap-1 hover:text-white"><Eye size={12} /> {material.stats.views}</span>
|
||||
<span className={`flex items-center gap-1 ${isFavorited ? 'text-cyber-pink' : 'hover:text-cyber-pink'}`}>
|
||||
<Heart size={12} className={isFavorited ? 'fill-cyber-pink' : ''} />
|
||||
{material.stats.favorites}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Image src={authorAvatar} alt="author" width={20} height={20} className="w-5 h-5 rounded-full border border-gray-700" />
|
||||
<span className="opacity-75">{material.author.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hover Line Animation */}
|
||||
<div className="absolute bottom-0 left-0 w-0 h-[2px] bg-cyber-neon group-hover:w-full transition-all duration-500 ease-out"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user