diff --git a/src/pages/dashboard/plugin/ManagePlugin.tsx b/src/pages/dashboard/plugin/ManagePlugin.tsx
index a9a39b1..c83970b 100644
--- a/src/pages/dashboard/plugin/ManagePlugin.tsx
+++ b/src/pages/dashboard/plugin/ManagePlugin.tsx
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import api from '../../../api';
import {
LayoutGrid, Tag, Upload, DollarSign, Trash2,
- Circle
+ Circle, BarChart3
} from 'lucide-react';
import StoreListing from './StoreListing';
@@ -11,6 +11,7 @@ import PublishUpdate from './PublishUpdate';
import Pricing from './Pricing';
import Coupons from './Coupons';
import DangerZone from './DangerZone';
+import PluginAnalyticsTab from './PluginAnalyticsTab';
import './ManagePlugin.css';
export type PluginData = {
@@ -32,11 +33,12 @@ export type PluginData = {
};
const NAV_ITEMS = [
- { key: 'listing', label: 'Store Listing', icon: LayoutGrid },
- { key: 'update', label: 'Publish Update', icon: Upload },
- { key: 'pricing', label: 'Pricing', icon: DollarSign },
- { key: 'coupons', label: 'Coupons', icon: Tag },
- { key: 'danger', label: 'Danger Zone', icon: Trash2 },
+ { key: 'listing', label: 'Store Listing', icon: LayoutGrid },
+ { key: 'update', label: 'Publish Update', icon: Upload },
+ { key: 'analytics', label: 'Analytics', icon: BarChart3 },
+ { key: 'pricing', label: 'Pricing', icon: DollarSign },
+ { key: 'coupons', label: 'Coupons', icon: Tag },
+ { key: 'danger', label: 'Danger Zone', icon: Trash2 },
];
const ManagePlugin = () => {
@@ -130,6 +132,9 @@ const ManagePlugin = () => {
{activeTab === 'update' && (
)}
+ {activeTab === 'analytics' && (
+
+ )}
{activeTab === 'pricing' && (
)}
diff --git a/src/pages/dashboard/plugin/PluginAnalyticsTab.tsx b/src/pages/dashboard/plugin/PluginAnalyticsTab.tsx
new file mode 100644
index 0000000..83b4821
--- /dev/null
+++ b/src/pages/dashboard/plugin/PluginAnalyticsTab.tsx
@@ -0,0 +1,249 @@
+import React from 'react';
+import {
+ AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer,
+ LineChart, Line
+} from 'recharts';
+import {
+ TrendingUp, TrendingDown, Minus, DollarSign,
+ Download, MousePointer2, Star, BarChart3
+} from 'lucide-react';
+import { useAnalytics } from '../useAnalytics';
+
+type Props = {
+ pluginId: number;
+ pluginName: string;
+};
+
+const formatValue = (val: number) => {
+ if (val >= 1000) return `${(val / 1000).toFixed(1)}k`;
+ return val.toString();
+};
+
+const CustomTooltip = ({ active, payload, label }: any) => {
+ if (active && payload && payload.length) {
+ return (
+
+
{label}
+ {payload.map((entry: any, index: number) => (
+
+ {entry.name}: {entry.dataKey === 'earnings' ? `$${entry.value.toLocaleString()}` : entry.value}
+
+ ))}
+
+ );
+ }
+ return null;
+};
+
+const MiniChart = ({ data, color, dataKey }: any) => (
+
+
+
+
+
+
+
+);
+
+const KpiCard = ({ title, value, comparison, icon: Icon, chartData, dataKey }: any) => {
+ const trendValue = parseFloat(comparison) || 0;
+ const isPositive = trendValue > 0;
+ const isNeutral = trendValue === 0;
+ const trendColor = isNeutral ? '#94a3b8' : (isPositive ? '#10b981' : '#ef4444');
+
+ const TrendIcon = () => {
+ if (isNeutral) return
;
+ return isPositive ?
:
;
+ };
+
+ return (
+
+
+
+
+
{value}
+
+
+ {isNeutral ? '0%' : `${trendValue}%`}
+
+
+
+
+
+ );
+};
+
+export const PluginAnalyticsTab: React.FC
= ({ pluginId, pluginName }) => {
+ const { processedData, totals, ratingSummary, timeframe, setTimeframe, loading } = useAnalytics(pluginId);
+ const timeframes = ['Today', '7 Days', '1 Month', '1 Year', 'Lifetime'];
+
+ if (loading) {
+ return (
+
+
+
Loading analytics for {pluginName}…
+
+ );
+ }
+
+ return (
+
+
+
+
Analytics & Performance
+
Track unique downloads, revenue, conversion rates, and review ratings specifically for {pluginName} .
+
+
+ {timeframes.map((tf) => (
+ setTimeframe(tf)}
+ >
+ {tf}
+
+ ))}
+
+
+
+
+
+ {/* Grid: Charts & Rating Breakdown */}
+
+
+ {/* Revenue & Download Growth */}
+
+
+
+ Downloads & Revenue Over Time
+
+
+
+
+
+
+
+
+
+
+
+ } />
+
+
+
+
+
+ {/* Rating History */}
+
+
+
+ Rating Trend Over Time
+
+
+
+
+
+ } />
+
+
+
+
+
+
+ {/* Right Sidebar: Rating Histogram */}
+
+ {ratingSummary && (
+
+
+
+ Rating Breakdown
+
+
+
+
+ {ratingSummary.averageRating.toFixed(1)}
+
+
+ {ratingSummary.totalReviews} customer reviews
+
+
+
+
+ {ratingSummary.breakdown?.map((b: any) => {
+ const pct = ratingSummary.totalReviews > 0 ? (b.count / ratingSummary.totalReviews) * 100 : 0;
+ return (
+
+
{b.stars} ★
+
+
{b.count}
+
+ );
+ })}
+
+
+ )}
+
+
+
+ );
+};
+
+export default PluginAnalyticsTab;
diff --git a/src/pages/dashboard/plugin/Pricing.tsx b/src/pages/dashboard/plugin/Pricing.tsx
index 576ce0c..cffc3b8 100644
--- a/src/pages/dashboard/plugin/Pricing.tsx
+++ b/src/pages/dashboard/plugin/Pricing.tsx
@@ -74,9 +74,7 @@ const Pricing = ({ plugin, onSaved }: Props) => {
}
try {
- await api.put(`/plugins/${plugin.id}`, fd, {
- headers: { 'Content-Type': 'multipart/form-data' },
- });
+ await api.put(`/plugins/${plugin.id}`, fd);
onSaved();
} catch {
alert('Failed to save pricing.');
diff --git a/src/pages/dashboard/plugin/PublishUpdate.tsx b/src/pages/dashboard/plugin/PublishUpdate.tsx
index 1030bfd..01d4b34 100644
--- a/src/pages/dashboard/plugin/PublishUpdate.tsx
+++ b/src/pages/dashboard/plugin/PublishUpdate.tsx
@@ -1,15 +1,20 @@
import React, { useState } from 'react';
-import { Upload, FileCode, Send, CheckCircle} from 'lucide-react';
+import { Upload, FileCode, Send, CheckCircle, AlertTriangle, Clock } from 'lucide-react';
import api from '../../../api';
import type { PluginData } from './ManagePlugin';
-type ReleaseTrack = 'stable' | 'beta' | 'alpha';
type Props = { plugin: PluginData; onSaved: () => void };
+const TRACKS = [
+ { key: 'stable', label: 'Production (Stable)', desc: 'Available to all users across the marketplace.' },
+ { key: 'beta', label: 'Beta', desc: 'Public beta release for community testing.' },
+ { key: 'alpha', label: 'Alpha / Testing', desc: 'Early access build for testers.' },
+] as const;
+
const PublishUpdate = ({ plugin, onSaved }: Props) => {
const [wasmFile, setWasmFile] = useState(null);
- const [track] = useState('stable');
const [version, setVersion] = useState('');
+ const [track, setTrack] = useState<'stable' | 'beta' | 'alpha'>('stable');
const [releaseNotes, setReleaseNotes] = useState('');
const [dragOver, setDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
@@ -28,14 +33,12 @@ const PublishUpdate = ({ plugin, onSaved }: Props) => {
setUploading(true);
const fd = new FormData();
fd.append('wasm', wasmFile);
- fd.append('track', track);
if (version) fd.append('version', version);
+ fd.append('track', track);
if (releaseNotes) fd.append('release_notes', releaseNotes);
try {
- await api.put(`/plugins/${plugin.id}`, fd, {
- headers: { 'Content-Type': 'multipart/form-data' },
- });
+ await api.put(`/plugins/${plugin.id}`, fd);
setPublished(true);
onSaved();
setTimeout(() => {
@@ -71,7 +74,7 @@ const PublishUpdate = ({ plugin, onSaved }: Props) => {
Update Published!
- Your new version is being rolled out on the {track} track.
+ Your new version has been published successfully to the {track} track.
@@ -86,7 +89,7 @@ const PublishUpdate = ({ plugin, onSaved }: Props) => {
Ship a new version of your plugin. Choose your release track, upload the binary, and add release notes — like Google Play, but for your plugin marketplace.