'use client'; import { useEffect, useState, useCallback } from 'react'; import { Search, Download, Check, Loader2, Store, Puzzle, SwatchBook, Star, Filter } from 'lucide-react'; import { apiFetch } from '@/lib/browser-navigation'; interface Extension { slug: string; name: string; type: 'plugin' | 'theme'; pluginType: string | null; description: string; permissions: string[]; tags: string[]; totalDownloads: number; featured: boolean; minAppVersion: string | null; latestVersion: string | null; installed: boolean; author: { displayName: string; githubLogin: string; avatarUrl: string | null; } | null; } interface SearchResult { data: Extension[]; meta: { page: number; perPage: number; total: number; }; } type TypeFilter = 'all' | 'plugin' | 'theme'; export default function AdminMarketplacePage() { const [extensions, setExtensions] = useState([]); const [loading, setLoading] = useState(true); const [query, setQuery] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [perPage] = useState(12); const [installing, setInstalling] = useState(null); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [error, setError] = useState(null); const fetchExtensions = useCallback(async () => { setLoading(true); setError(null); try { const params = new URLSearchParams(); if (query) params.set('q', query); if (typeFilter !== 'all') params.set('type', typeFilter); params.set('page', String(page)); params.set('perPage', String(perPage)); params.set('sort', 'newest'); const res = await apiFetch(`/api/admin/marketplace?${params}`); if (!res.ok) { const data = await res.json().catch(() => ({})); setError(data.error || 'Failed to connect to extension directory'); setExtensions([]); return; } const data: SearchResult = await res.json(); setExtensions(data.data || []); setTotal(data.meta?.total || 0); } catch { setError('Failed to connect to extension directory. Make sure it is running.'); setExtensions([]); } finally { setLoading(false); } }, [query, typeFilter, page, perPage]); useEffect(() => { fetchExtensions(); }, [fetchExtensions]); // Debounced search const [searchInput, setSearchInput] = useState(''); useEffect(() => { const t = setTimeout(() => { setQuery(searchInput); setPage(1); }, 300); return () => clearTimeout(t); }, [searchInput]); async function handleInstall(ext: Extension) { setInstalling(ext.slug); setMessage(null); try { const res = await apiFetch('/api/admin/marketplace', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: ext.slug, version: ext.latestVersion || '1.0.0', type: ext.type, }), }); const data = await res.json(); if (res.ok) { const warnings = data.warnings?.length ? ` (${data.warnings.length} warning(s))` : ''; setMessage({ type: 'success', text: `"${ext.name}" installed successfully${warnings}` }); // Mark as installed in the UI setExtensions(prev => prev.map(e => e.slug === ext.slug ? { ...e, installed: true } : e)); } else { setMessage({ type: 'error', text: data.error || 'Installation failed' }); } } catch { setMessage({ type: 'error', text: 'Installation failed - network error' }); } finally { setInstalling(null); } } const totalPages = Math.ceil(total / perPage); return (

Marketplace

Browse and install plugins and themes from the BulwarkMail extension directory

{message && (
{message.text}
)} {/* Search & Filters */}
setSearchInput(e.target.value)} className="w-full h-9 pl-9 pr-3 rounded-md border border-input bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring/20 focus:border-ring" />
{(['all', 'plugin', 'theme'] as const).map((t) => ( ))}
{/* Error State */} {error && (

{error}

Start the extension directory server on the configured port

)} {/* Loading State */} {loading && !error && (
Searching extensions...
)} {/* Empty State */} {!loading && !error && extensions.length === 0 && (

No extensions found

{query && (

Try a different search term

)}
)} {/* Extension Grid */} {!loading && !error && extensions.length > 0 && ( <>
{total} extension{total !== 1 ? 's' : ''} found
{extensions.map((ext) => ( handleInstall(ext)} /> ))}
{/* Pagination */} {totalPages > 1 && (
Page {page} of {totalPages}
)} )}
); } function ExtensionCard({ extension, installing, onInstall, }: { extension: Extension; installing: boolean; onInstall: () => void; }) { const isPlugin = extension.type === 'plugin'; return (
{/* Header */}
{isPlugin ? ( ) : ( )}
{extension.name} {extension.featured && ( )}
{isPlugin ? (extension.pluginType || 'plugin') : 'theme'} {extension.author && ( by {extension.author.displayName} )}
{/* Description */}

{extension.description}

{/* Tags */} {extension.tags && extension.tags.length > 0 && (
{extension.tags.slice(0, 3).map(tag => ( {tag} ))}
)} {/* Footer */}
{extension.totalDownloads.toLocaleString()} {extension.permissions && extension.permissions.length > 0 && ( {extension.permissions.length} permission{extension.permissions.length !== 1 ? 's' : ''} )}
{extension.installed ? ( Installed ) : ( )}
); }