Modernize frontend and centralize branding
This commit is contained in:
1 parent
bc4542d0fb
commit
2d61d80550
55 files changed
+1901
-14446
No files matched your search
@@ -0,0 +1,402 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Search, Download, RefreshCw, Package, AlertTriangle, ArrowLeft, Filter
|
||||
} from "lucide-react";
|
||||
import { colors, Dropdown, SectionCard, minecraftVersions } from '../../components/DashboardComponents';
|
||||
|
||||
import { API_BASE_URL } from '../../config/api';
|
||||
import branding from '../../branding';
|
||||
|
||||
// Embedded Plugin Store Component for Dashboard
|
||||
const PluginStoreEmbedded = ({ serverId }) => {
|
||||
const [plugins, setPlugins] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedGameVersion, setSelectedGameVersion] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
const [selectedPlugin, setSelectedPlugin] = useState(null);
|
||||
const [pluginDetails, setPluginDetails] = useState(null);
|
||||
const [pluginVersions, setPluginVersions] = useState([]);
|
||||
const [selectedVersion, setSelectedVersion] = useState(null);
|
||||
const [downloadingToServer, setDownloadingToServer] = useState(false);
|
||||
const [downloadStatus, setDownloadStatus] = useState('');
|
||||
const [loadingDetails, setLoadingDetails] = useState(false);
|
||||
const [totalResults, setTotalResults] = useState(0);
|
||||
|
||||
const handlePluginClick = async (pluginId) => {
|
||||
setSelectedPlugin(pluginId);
|
||||
setError('');
|
||||
setLoadingDetails(true);
|
||||
|
||||
try {
|
||||
// Fetch plugin details
|
||||
const projectResponse = await fetch(
|
||||
`${API_BASE_URL}/api/modrinth/project/${pluginId}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': `${branding.name}/1.0`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!projectResponse.ok) {
|
||||
throw new Error('Failed to fetch plugin details');
|
||||
}
|
||||
|
||||
const projectData = await projectResponse.json();
|
||||
setPluginDetails(projectData);
|
||||
|
||||
// Fetch versions
|
||||
const versionsResponse = await fetch(
|
||||
`${API_BASE_URL}/api/modrinth/project/${pluginId}/versions`
|
||||
);
|
||||
|
||||
if (versionsResponse.ok) {
|
||||
const versionsData = await versionsResponse.json();
|
||||
setPluginVersions(versionsData);
|
||||
if (versionsData.length > 0) {
|
||||
setSelectedVersion(versionsData[0]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error('Error fetching plugin details:', err);
|
||||
} finally {
|
||||
setLoadingDetails(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackToList = () => {
|
||||
setSelectedPlugin(null);
|
||||
setPluginDetails(null);
|
||||
setPluginVersions([]);
|
||||
setSelectedVersion(null);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleDownloadToServer = async () => {
|
||||
if (!serverId || !selectedVersion) {
|
||||
setError('Please select a version first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedVersion.files || selectedVersion.files.length === 0) {
|
||||
setError('No file available for this version');
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryFile = selectedVersion.files.find(f => f.primary) || selectedVersion.files[0];
|
||||
setDownloadingToServer(true);
|
||||
setDownloadStatus('Downloading plugin...');
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('anchorhost_token');
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/plugins/download-to-server`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
server_id: serverId,
|
||||
plugin_url: primaryFile.url,
|
||||
plugin_name: pluginDetails?.title || selectedPlugin,
|
||||
file_name: primaryFile.filename || `${selectedPlugin}.jar`
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || 'Failed to download plugin to server');
|
||||
}
|
||||
|
||||
setDownloadStatus('Plugin downloaded successfully!');
|
||||
setTimeout(() => {
|
||||
setDownloadStatus('');
|
||||
setDownloadingToServer(false);
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setDownloadingToServer(false);
|
||||
setDownloadStatus('');
|
||||
}
|
||||
};
|
||||
|
||||
const gameVersions = [
|
||||
{ value: '', label: 'All Versions' },
|
||||
...minecraftVersions.flatMap(series =>
|
||||
series.versions.map(v => ({ value: v, label: v }))
|
||||
)
|
||||
];
|
||||
|
||||
const searchPlugins = useCallback(async (query = '', page = 0) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
query: query || '',
|
||||
limit: '20',
|
||||
offset: (page * 20).toString(),
|
||||
project_type: 'plugin'
|
||||
});
|
||||
|
||||
if (selectedGameVersion) {
|
||||
params.append('game_versions', selectedGameVersion);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/modrinth/search?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to search plugins');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setPlugins(data.hits || []);
|
||||
setTotalResults(data.total_hits || 0);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error('Error searching plugins:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [selectedGameVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
searchPlugins(searchQuery, currentPage);
|
||||
}, [searchPlugins, searchQuery, currentPage]);
|
||||
|
||||
const handleSearch = (e) => {
|
||||
e.preventDefault();
|
||||
setCurrentPage(0);
|
||||
searchPlugins(searchQuery, 0);
|
||||
};
|
||||
|
||||
if (loadingDetails) {
|
||||
return (
|
||||
<div className="h-full flex justify-center items-center rounded-2xl animate-pulse" style={{ backgroundColor: 'rgba(0,0,0,0.1)' }}>
|
||||
<RefreshCw className="w-8 h-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedPlugin && pluginDetails) {
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-8 animate-in fade-in slide-in-from-right-4 duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={handleBackToList}
|
||||
className="flex items-center space-x-2 text-xs font-bold uppercase tracking-wider px-5 py-2.5 rounded-xl transition-all duration-200 hover:scale-105 border hover:bg-white/10"
|
||||
style={{ color: colors.foreground, backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Back to Store</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SectionCard title={pluginDetails.title} icon={Package} color={colors.blue}>
|
||||
<div className="flex flex-col md:flex-row items-center md:items-start gap-8">
|
||||
<img
|
||||
src={pluginDetails.icon_url || 'https://via.placeholder.com/150'}
|
||||
alt={pluginDetails.title}
|
||||
className="w-32 h-32 rounded-3xl shadow-2xl border-4 border-white/5"
|
||||
/>
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<p className="text-base mb-6 leading-relaxed opacity-70" style={{ color: colors.foreground }}>{pluginDetails.description}</p>
|
||||
<div className="flex flex-wrap justify-center md:justify-start items-center gap-4 text-xs">
|
||||
<div className="px-4 py-2.5 rounded-xl border flex flex-col items-center justify-center min-w-[100px]" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
|
||||
<span className="text-[10px] uppercase opacity-50 mb-1">Downloads</span>
|
||||
<span className="font-bold text-sm" style={{ color: colors.blue }}>{pluginDetails.downloads.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="px-4 py-2.5 rounded-xl border flex flex-col items-center justify-center min-w-[100px]" style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}>
|
||||
<span className="text-[10px] uppercase opacity-50 mb-1">Last Update</span>
|
||||
<span className="font-bold text-sm" style={{ color: colors.green }}>{new Date(pluginDetails.updated).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Available Versions" icon={Download} color={colors.green}>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{pluginVersions.map((version) => (
|
||||
<div
|
||||
key={version.id}
|
||||
onClick={() => setSelectedVersion(version)}
|
||||
className={`p-5 rounded-2xl border transition-all duration-300 group cursor-pointer ${
|
||||
selectedVersion?.id === version.id
|
||||
? 'bg-blue-500/10'
|
||||
: 'bg-black/20 hover:bg-white/5'
|
||||
}`}
|
||||
style={{ borderColor: selectedVersion?.id === version.id ? colors.blue : colors.brightBlack }}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="font-bold text-white text-base">{version.name}</span>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-white/10 text-white/60">
|
||||
{version.version_number}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{version.game_versions.slice(0, 5).map(gv => (
|
||||
<span key={gv} className="text-[10px] px-2 py-0.5 rounded bg-black/40 text-blue-400 border border-blue-500/10">
|
||||
{gv}
|
||||
</span>
|
||||
))}
|
||||
{version.game_versions.length > 5 && <span className="text-[10px] opacity-40">+{version.game_versions.length - 5} more</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedVersion(version);
|
||||
handleDownloadToServer();
|
||||
}}
|
||||
disabled={downloadingToServer}
|
||||
className={`ml-4 px-6 py-3 rounded-xl font-bold transition-all text-xs uppercase tracking-wider border flex items-center gap-2 ${
|
||||
selectedVersion?.id === version.id
|
||||
? 'bg-blue-500/20 text-blue-400 border-blue-500/30 hover:bg-blue-500/30'
|
||||
: 'bg-white/5 text-white/60 border-transparent hover:bg-white/10 hover:text-white'
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
{downloadingToServer && selectedVersion?.id === version.id ? (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
<span>Installing...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Install</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{downloadStatus && (
|
||||
<div className="p-4 rounded-2xl bg-green-500/10 border border-green-500/20 text-green-400 text-sm font-bold flex items-center justify-center gap-3 animate-in fade-in slide-in-from-bottom-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||
{downloadStatus}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||
<SectionCard title="Search Plugins" icon={Search} color={colors.blue}>
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
<div className="flex-1 relative group">
|
||||
<Search className="w-4 h-4 absolute left-4 top-1/2 transform -translate-y-1/2 text-white/50 group-focus-within:text-blue-400 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search thousands of plugins..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch(e)}
|
||||
className="w-full pl-11 pr-4 py-3 rounded-xl bg-black/40 border text-white placeholder:text-white/50 focus:border-primary/50 focus:bg-black/60 focus:outline-none transition-all font-medium text-sm"
|
||||
style={{ borderColor: colors.brightBlack }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="w-48">
|
||||
<Dropdown
|
||||
value={selectedGameVersion}
|
||||
options={gameVersions}
|
||||
onChange={setSelectedGameVersion}
|
||||
placeholder="Version"
|
||||
icon={Filter}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar">
|
||||
{loading ? (
|
||||
<div className="flex flex-col justify-center items-center h-64 space-y-4 opacity-50">
|
||||
<RefreshCw className="w-10 h-10 animate-spin text-blue-500" />
|
||||
<span className="text-sm font-bold tracking-widest text-blue-400 animate-pulse uppercase">Searching Modrinth...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center p-12 bg-red-500/5 border border-red-500/10 rounded-3xl">
|
||||
<AlertTriangle className="w-16 h-16 mx-auto mb-4 text-red-500 opacity-50" />
|
||||
<h4 className="text-xl font-bold text-white mb-2">Search Error</h4>
|
||||
<p className="text-red-400/70">{error}</p>
|
||||
</div>
|
||||
) : plugins.length === 0 ? (
|
||||
<div className="text-center p-12 bg-white/5 border border-dashed border-white/10 rounded-3xl">
|
||||
<Package className="w-16 h-16 mx-auto mb-4 opacity-20" />
|
||||
<h4 className="text-xl font-bold text-white/40 mb-2">No Plugins Found</h4>
|
||||
<p className="text-white/20 text-sm">Try broadening your search or changing the version filter.</p>
|
||||
</div>
|
||||
) : (
|
||||
<SectionCard title={`Results (${totalResults.toLocaleString()})`} icon={Package} color={colors.magenta}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{plugins.map((plugin) => (
|
||||
<div
|
||||
key={plugin.slug}
|
||||
onClick={() => handlePluginClick(plugin.slug)}
|
||||
className="p-5 rounded-xl border transition-all cursor-pointer group flex flex-col h-full"
|
||||
style={{ backgroundColor: colors.brightBlack, borderColor: colors.brightBlack }}
|
||||
>
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={plugin.icon_url || 'https://via.placeholder.com/64'}
|
||||
alt={plugin.title}
|
||||
className="w-16 h-16 rounded-xl shadow-lg group-hover:scale-110 transition-transform duration-500 relative z-10 bg-gray-900 border border-white/5"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-blue-500/20 blur-xl rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold text-white text-base leading-tight group-hover:text-blue-400 transition-colors truncate">
|
||||
{plugin.title}
|
||||
</h4>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-[10px] font-bold text-white/40 uppercase tracking-tighter">
|
||||
{plugin.author}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-white/70 mb-6 line-clamp-2 leading-relaxed flex-1">
|
||||
{plugin.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t border-white/5 mt-auto">
|
||||
<div className="flex items-center gap-3 text-[10px] font-bold text-white/30 uppercase tracking-wider">
|
||||
<span className="flex items-center gap-1.5 px-2 py-1 rounded bg-black/40">
|
||||
<Download className="w-3 h-3 text-blue-500" />
|
||||
{plugin.downloads.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-1.5 rounded-lg bg-white/5 group-hover:bg-blue-500/20 group-hover:text-blue-400 transition-all">
|
||||
<ArrowLeft className="w-3.5 h-3.5 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginStoreEmbedded;
|
||||
Reference in new issue
Block a user