'use client'; import { useEffect, useState, useCallback } from 'react'; import Link from 'next/link'; import { Search, Download, Check, Loader2, Store, Puzzle, SwatchBook, Star, Eye, AlertTriangle, ArrowUpCircle } from 'lucide-react'; import { apiFetch } from '@/lib/browser-navigation'; import { compareVersions, isVersionSatisfied } from '@/lib/version-compare'; const CURRENT_APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0'; 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; installedVersion: string | null; iconUrl: string | null; bannerUrl: string | null; 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 function MarketplaceTab() { 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]); const [searchInput, setSearchInput] = useState(''); useEffect(() => { const t = setTimeout(() => { setQuery(searchInput); setPage(1); }, 300); return () => clearTimeout(t); }, [searchInput]); async function handleInstall(ext: Extension) { if (ext.minAppVersion && !isVersionSatisfied(CURRENT_APP_VERSION, ext.minAppVersion)) { setMessage({ type: 'error', text: `"${ext.name}" requires app v${ext.minAppVersion}+. You are running v${CURRENT_APP_VERSION}.`, }); return; } const isUpdate = ext.installed; const targetVersion = ext.latestVersion || '1.0.0'; 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: targetVersion, 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: isUpdate ? `"${ext.name}" updated to v${targetVersion}${warnings}` : `"${ext.name}" installed successfully${warnings}`, }); setExtensions(prev => prev.map(e => e.slug === ext.slug ? { ...e, installed: true, installedVersion: targetVersion } : e, )); } else { setMessage({ type: 'error', text: data.error || (isUpdate ? 'Update failed' : 'Installation failed') }); } } catch { setMessage({ type: 'error', text: isUpdate ? 'Update failed - network error' : '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}
)}
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 && (

{error}

Start the extension directory server on the configured port

)} {loading && !error && (
Searching extensions...
)} {!loading && !error && extensions.length === 0 && (

No extensions found

{query && (

Try a different search term

)}
)} {!loading && !error && extensions.length > 0 && ( <>
{total} extension{total !== 1 ? 's' : ''} found
{extensions.map((ext) => ( handleInstall(ext)} /> ))}
{totalPages > 1 && (
Page {page} of {totalPages}
)} )}
); } function ExtensionCard({ extension, installing, onInstall, }: { extension: Extension; installing: boolean; onInstall: () => void; }) { const isPlugin = extension.type === 'plugin'; const previewHref = `/admin/marketplace/${encodeURIComponent(extension.slug)}`; const versionMismatch = !!extension.minAppVersion && !isVersionSatisfied(CURRENT_APP_VERSION, extension.minAppVersion); const updateAvailable = extension.installed && !!extension.installedVersion && !!extension.latestVersion && compareVersions(extension.latestVersion, extension.installedVersion) > 0 && !versionMismatch; return (
{extension.bannerUrl && ( )}
{extension.iconUrl ? ( ) : isPlugin ? ( ) : ( )}
{extension.name} {extension.featured && ( )}
{isPlugin ? (extension.pluginType || 'plugin') : 'theme'} {extension.author && ( by {extension.author.displayName} )}

{extension.description}

{extension.tags && extension.tags.length > 0 && (
{extension.tags.slice(0, 3).map(tag => ( {tag} ))}
)}
{extension.totalDownloads.toLocaleString()} {extension.permissions && extension.permissions.length > 0 && ( {extension.permissions.length} permission{extension.permissions.length !== 1 ? 's' : ''} )}
Preview
{extension.installed && updateAvailable ? ( ) : extension.installed ? ( Installed ) : versionMismatch ? ( Requires v{extension.minAppVersion}+ ) : ( )}
); }