0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
177 lines
8.6 KiB
TypeScript
177 lines
8.6 KiB
TypeScript
import React from 'react';
|
|
import { useLanguage } from '../../contexts/LanguageContext';
|
|
import { Search, Plus, MoreHorizontal, Puzzle } from 'lucide-react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
// Mock data for plugins
|
|
interface PluginMock {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
status: 'installed' | 'not-installed' | 'update-available';
|
|
developer: string;
|
|
iconEmoji: string;
|
|
iconBgClass: string;
|
|
}
|
|
|
|
const mockPlugins: PluginMock[] = [
|
|
{
|
|
id: '1',
|
|
name: 'plugin1Name',
|
|
description: 'plugin1Desc',
|
|
status: 'installed',
|
|
developer: 'Official',
|
|
iconEmoji: '🛠️',
|
|
iconBgClass: 'bg-blue-50'
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'plugin2Name',
|
|
description: 'plugin2Desc',
|
|
status: 'update-available',
|
|
developer: 'Official',
|
|
iconEmoji: '📄',
|
|
iconBgClass: 'bg-indigo-50'
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'plugin3Name',
|
|
description: 'plugin3Desc',
|
|
status: 'installed',
|
|
developer: 'Community',
|
|
iconEmoji: '🦊',
|
|
iconBgClass: 'bg-orange-50'
|
|
},
|
|
{
|
|
id: '4',
|
|
name: 'plugin4Name',
|
|
description: 'plugin4Desc',
|
|
status: 'not-installed',
|
|
developer: 'Official',
|
|
iconEmoji: '🌐',
|
|
iconBgClass: 'bg-emerald-50'
|
|
},
|
|
{
|
|
id: '5',
|
|
name: 'plugin5Name',
|
|
description: 'plugin5Desc',
|
|
status: 'not-installed',
|
|
developer: 'Community',
|
|
iconEmoji: '🗄️',
|
|
iconBgClass: 'bg-slate-100'
|
|
},
|
|
{
|
|
id: '6',
|
|
name: 'plugin6Name',
|
|
description: 'plugin6Desc',
|
|
status: 'installed',
|
|
developer: 'Official',
|
|
iconEmoji: '💬',
|
|
iconBgClass: 'bg-sky-50'
|
|
}
|
|
];
|
|
|
|
export const PluginsView: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-[#f4f7fb] overflow-hidden">
|
|
{/* Header Area */}
|
|
<div className="px-8 pt-8 pb-6 flex items-start justify-between shrink-0">
|
|
<div>
|
|
<h1 className="text-[22px] font-bold text-slate-900 leading-tight flex items-center gap-2">
|
|
<Puzzle className="text-blue-600" size={24} />
|
|
{t('pluginTitle')}
|
|
</h1>
|
|
<p className="text-[14px] text-slate-500 mt-1">{t('pluginDesc')}</p>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="relative w-64">
|
|
<Search className="absolute text-slate-400 left-3 top-1/2 -translate-y-1/2" size={16} />
|
|
<input
|
|
type="text"
|
|
placeholder={t('searchPlugin')}
|
|
className="w-full h-10 pl-10 pr-4 bg-white border border-slate-200 rounded-lg focus:bg-white focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all text-sm font-medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<div className="px-8 pb-8 flex-1 overflow-y-auto">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 max-w-[1600px] mx-auto">
|
|
<AnimatePresence>
|
|
{mockPlugins.map((plugin) => (
|
|
<motion.div
|
|
key={plugin.id}
|
|
layout
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="bg-white rounded-2xl p-6 shadow-sm border border-slate-100 hover:shadow-md transition-all group flex flex-col h-[220px]"
|
|
>
|
|
{/* Top layer */}
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className={`w-12 h-12 flex items-center justify-center rounded-xl ${plugin.iconBgClass} text-2xl shadow-sm border border-black/5`}>
|
|
{plugin.iconEmoji}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{/* Status Badge */}
|
|
{plugin.status === 'installed' && (
|
|
<div className="px-2.5 py-1 text-[12px] font-semibold text-emerald-600 bg-emerald-50 rounded-full border border-emerald-100 flex flex-row items-center justify-center">
|
|
{t('installedPlugin')}
|
|
</div>
|
|
)}
|
|
{plugin.status === 'update-available' && (
|
|
<div className="px-2.5 py-1 text-[12px] font-semibold text-orange-600 bg-orange-50 rounded-full border border-orange-100 flex flex-row items-center justify-center">
|
|
{t('updatePlugin')}
|
|
</div>
|
|
)}
|
|
{/* Options button */}
|
|
<button className="text-slate-400 hover:text-slate-600 transition-colors">
|
|
<MoreHorizontal size={20} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Middle layer */}
|
|
<div className="flex-1">
|
|
<h3 className="font-bold text-slate-800 text-[17px] mb-2 leading-tight flex items-center gap-2">
|
|
{t(plugin.name as any)}
|
|
{plugin.developer === 'Official' && (
|
|
<span className="bg-blue-100 text-blue-700 text-[10px] uppercase font-bold px-1.5 py-0.5 rounded-sm">{t('pluginOfficial')}</span>
|
|
)}
|
|
</h3>
|
|
<p className="text-[13px] text-slate-500 leading-relaxed line-clamp-2">
|
|
{t(plugin.description as any)}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Bottom layer */}
|
|
<div className="mt-4 pt-4 border-t border-slate-50 flex items-center justify-between">
|
|
<span className="text-[12px] font-medium text-slate-400">
|
|
{t('pluginBy')}{plugin.developer === 'Official' ? t('pluginOfficial') : t('pluginCommunity')}
|
|
</span>
|
|
{plugin.status === 'not-installed' ? (
|
|
<button className="flex items-center justify-center gap-1.5 px-4 py-1.5 text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors shadow-sm">
|
|
<Plus size={14} className="text-white" />
|
|
<span className="text-[13px] font-bold">{t('installPlugin')}</span>
|
|
</button>
|
|
) : plugin.status === 'update-available' ? (
|
|
<button className="flex items-center justify-center gap-1.5 px-3 py-1.5 text-orange-600 bg-orange-50 hover:bg-orange-100 rounded-lg transition-colors border border-orange-200">
|
|
<span className="text-[13px] font-bold">{t('updatePlugin')}</span>
|
|
</button>
|
|
) : (
|
|
<button className="flex items-center justify-center gap-1.5 px-3 py-1.5 text-slate-600 bg-slate-50 hover:bg-slate-100 rounded-lg transition-colors border border-slate-200">
|
|
<span className="text-[13px] font-bold">{t('pluginConfig')}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|