import React, { useState, useEffect, useMemo } from 'react'; import { useParams, useLocation, Link } from 'react-router-dom'; import api from '../api'; import { useAuth } from '../App'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { useNavigate } from 'react-router-dom'; import SEO from '../components/SEO'; const REPORT_REASONS = [ 'Malware or malicious code', 'Stolen or plagiarised content', 'Misleading description', 'Broken / non-functional', 'Inappropriate content', 'Spam or fake listing', 'Other', ]; const StarRating = ({ rating, max = 5, onRate }: { rating: number; max?: number; onRate?: (n: number) => void }) => (
{[...Array(max)].map((_, i) => ( onRate && onRate(i + 1)} > {i < rating ? '★' : '☆'} ))}
); const PriceDisplay = ({ cents }: { cents: number }) => { if (!cents && cents !== 0) return null; const euros = Math.floor(cents / 100); const remainder = (cents % 100).toString().padStart(2, '0'); return (
€ {euros} . {remainder}
); }; const getAvatarClass = (username: string) => { const classes = ['av-a', 'av-b', 'av-c', 'av-d', 'av-e']; let hash = 0; for (let i = 0; i < username.length; i++) { hash = username.charCodeAt(i) + ((hash << 5) - hash); } return classes[Math.abs(hash) % classes.length]; }; const getInitials = (username: string) => { const parts = username.trim().split(/[\s_\-\.]+/); if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); return username.slice(0, 2).toUpperCase(); }; const PluginDetail = () => { const { id } = useParams(); const { user } = useAuth(); const navigate = useNavigate(); const location = useLocation(); const [plugin, setPlugin] = useState(null); const [reviewForm, setReviewForm] = useState({ rating: 5, comment: '' }); const [adShown, setAdShown] = useState(false); const [mainScreenshot, setMainScreenshot] = useState(null); const [selectedLanguage] = useState('en'); const [currentDescription, setCurrentDescription] = useState(''); const [setLoading] = useState(true); // Ownership const [ownsPlugin, setOwnsPlugin] = useState(false); const [ownershipChecked, setOwnershipChecked] = useState(false); // Payment success banner const [showPaymentSuccess, setShowPaymentSuccess] = useState(false); // Report const [reportOpen, setReportOpen] = useState(false); const [reportSelected, setReportSelected] = useState(''); const [reportCustom, setReportCustom] = useState(''); const [reportSubmitting, setReportSubmitting] = useState(false); const [reportDone, setReportDone] = useState(false); // ── Detect ?payment=success and strip from URL ─────────────────────────── useEffect(() => { const params = new URLSearchParams(location.search); if (params.get('payment') === 'success') { setShowPaymentSuccess(true); window.history.replaceState({}, '', location.pathname); } }, []); // ── Fetch plugin ───────────────────────────────────────────────────────── const fetchPlugin = () => { api.get(`/plugins/${id}`).then(res => { setPlugin(res.data); setLoading(false); if (res.data.screenshots?.length > 0) { setMainScreenshot(res.data.screenshots[0].path); } }); }; useEffect(() => { fetchPlugin(); }, [id]); // ── Check ownership ────────────────────────────────────────────────────── useEffect(() => { if (!user || !id) { setOwnershipChecked(true); return; } api.get(`/plugins/${id}/owned`) .then(res => setOwnsPlugin(res.data.owned === true)) .catch(() => setOwnsPlugin(false)) .finally(() => setOwnershipChecked(true)); }, [user, id]); // ── Translations ───────────────────────────────────────────────────────── useEffect(() => { if (plugin?.translated_descriptions) { try { const translations = JSON.parse(plugin.translated_descriptions); setCurrentDescription( translations[selectedLanguage] || translations['en'] || Object.values(translations)[0] as string || '' ); } catch { setCurrentDescription('Description not available.'); } } else { setCurrentDescription('Description not available.'); } }, [plugin, selectedLanguage]); const hasReviewed = useMemo(() => { if (!user || !plugin?.reviews || !Array.isArray(plugin.reviews)) return false; return plugin.reviews.some((r: any) => r.username === user.username); }, [plugin?.reviews, user]); const stats = useMemo(() => { if (!plugin?.reviews || !Array.isArray(plugin.reviews) || plugin.reviews.length === 0) return null; const total = plugin.reviews.length; const distribution = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 }; let sum = 0; plugin.reviews.forEach((r: any) => { distribution[r.rating as keyof typeof distribution]++; sum += r.rating; }); return { avg: (sum / total).toFixed(1), total, distribution }; }, [plugin?.reviews]); // ── Trigger a blob download ────────────────────────────────────────────── const triggerDownload = async (endpoint: string) => { const res = await api.get(endpoint, { responseType: 'blob' }); const url = URL.createObjectURL(res.data); const a = document.createElement('a'); a.href = url; a.download = `${plugin.name.replace(/\s+/g, '_')}.wasm`; a.click(); URL.revokeObjectURL(url); }; // ── Download / purchase handler ────────────────────────────────────────── const handleDownload = async () => { if (plugin.type === 'paid') { if (!user) return navigate('/login', { state: { from: `/plugins/${id}` } }); // Already owns it — download directly, no checkout if (ownsPlugin) { try { await triggerDownload(`/plugins/${id}/download/premium`); } catch { alert('Download failed. Please try again.'); } return; } // Does not own — go to Stripe checkout try { setLoading(true); const res = await api.post(`/plugins/${id}/checkout`); if (res.data.url) window.location.href = res.data.url; } catch (err: any) { if (err?.response?.status === 409) { // Webhook already recorded purchase before redirect resolved setOwnsPlugin(true); alert('You already own this plugin — downloading now.'); await triggerDownload(`/plugins/${id}/download/premium`); } else { alert('Checkout failed. Please try again.'); } } finally { setLoading(false); } return; } // Adwall if (plugin.type === 'adwall' && !adShown) { alert('Wait 5 seconds to support the developer...'); setTimeout(() => setAdShown(true), 5000); return; } // Free / adwall (after ad) try { await triggerDownload(`/plugins/${id}/download`); } catch { alert('Download failed. Please try again.'); } }; const submitReview = async (e: React.FormEvent) => { e.preventDefault(); if (!reviewForm.comment.trim()) return; await api.post(`/plugins/${id}/reviews`, reviewForm); fetchPlugin(); setReviewForm({ rating: 5, comment: '' }); }; const submitReport = async (e: React.FormEvent) => { e.preventDefault(); const reason = reportSelected === 'Other' ? reportCustom.trim() : reportSelected; if (!reason) return; setReportSubmitting(true); try { await api.post(`/plugins/${id}/report`, { reason }); setReportDone(true); setReportSelected(''); setReportCustom(''); } catch { alert('Failed to submit report. Please try again.'); } finally { setReportSubmitting(false); } }; const resetReport = () => { setReportOpen(false); setReportSelected(''); setReportCustom(''); }; if (!plugin) return

LOADING...

; const formatSize = (bytes: number) => { if (!bytes) return ''; if (bytes >= 1048576) return `(${(bytes / 1048576).toFixed(2)} MB)`; if (bytes >= 1024) return `(${(bytes / 1024).toFixed(1)} KB)`; return `(${bytes} B)`; }; const downloadLabel = (() => { const sizeStr = plugin.file_size ? ` ${formatSize(plugin.file_size)}` : ''; if (plugin.type === 'paid') { if (!ownershipChecked) return 'Loading…'; return ownsPlugin ? `Download ${sizeStr}` : 'Purchase Plugin'; } if (plugin.type === 'adwall' && !adShown) return 'Watch Ad to Download'; return `Download ${sizeStr}`; })(); const productSchema = { "@context": "https://schema.org/", "@type": "SoftwareApplication", "name": plugin.name, "operatingSystem": "Minecraft", "applicationCategory": "GameApplication", "offers": { "@type": "Offer", "price": plugin.type === 'paid' ? (plugin.price_cents / 100).toFixed(2) : "0.00", "priceCurrency": "EUR" }, "aggregateRating": stats ? { "@type": "AggregateRating", "ratingValue": stats.avg, "reviewCount": stats.total } : undefined, "author": { "@type": "Person", "name": plugin.dev_name } }; return (
{/* ── Payment success banner ── */} {showPaymentSuccess && (
🎉
Purchase successful!
You now own {plugin.name}. Click the download button to get your WASM file.
)} {/* HEADER */}
{!plugin.preview_path && plugin.name.charAt(0)}

{plugin.name}

by {plugin.dev_name}

{/* SCREENSHOTS */} {Array.isArray(plugin.screenshots) && plugin.screenshots.length > 0 && (
Main
{plugin.screenshots.map((shot: any) => ( Thumbnail setMainScreenshot(shot.path)} /> ))}
)} {/* DESCRIPTION */}
{currentDescription}

{/* REVIEWS */}
Ratings & Reviews {stats ? (
{stats.avg}
{[1, 2, 3, 4, 5].map(n => ( ★ ))}
{stats.total.toLocaleString()} Ratings
{[5, 4, 3, 2, 1].map(num => (
{num}
{stats.distribution[num as keyof typeof stats.distribution]}
))}
) : (
No reviews yet. Be the first to leave one!
)}
{Array.isArray(plugin.reviews) && plugin.reviews.map((r: any) => (
{getInitials(r.username)}
{r.username} Verified User
{[1, 2, 3, 4, 5].map(n => ( ★ ))}

{r.comment}

))}
{user ? ( hasReviewed ? (
✓
Review posted
You've already reviewed this plugin. Thanks for the feedback!
) : (

Leave a Review

setReviewForm({ ...reviewForm, rating: val })} />