some fixes
This commit is contained in:
parent
ef5092ee4b
commit
93511af260
@ -7,6 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800;900&family=JetBrains+Mono:wght@700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800;900&family=JetBrains+Mono:wght@700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="search" type="application/opensearchdescription+xml" title="Pumpkin Market" href="/opensearch.xml" />
|
||||||
<title>Pumpkin Market</title>
|
<title>Pumpkin Market</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
8
public/opensearch.xml
Normal file
8
public/opensearch.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
|
<ShortName>Pumpkin Market</ShortName>
|
||||||
|
<Description>Search plugins on Pumpkin Market</Description>
|
||||||
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
|
<Image height="16" width="16" type="image/png">/icon.png</Image>
|
||||||
|
<Url type="text/html" template="/search?q={searchTerms}"/>
|
||||||
|
</OpenSearchDescription>
|
||||||
80
src/App.css
80
src/App.css
@ -391,7 +391,7 @@ h3 {
|
|||||||
|
|
||||||
.sidebar-widget {
|
.sidebar-widget {
|
||||||
position: static;
|
position: static;
|
||||||
padding: 1.5rem;
|
padding: 1.25rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -404,28 +404,45 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plugin-title {
|
.plugin-title {
|
||||||
font-size: 2.25rem;
|
font-size: 1.75rem;
|
||||||
|
line-height: 1.2;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-detail-header {
|
.plugin-detail-header {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 1.25rem;
|
gap: 1rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-icon.large {
|
.plugin-icon.large {
|
||||||
width: 80px;
|
width: 64px;
|
||||||
height: 80px;
|
height: 64px;
|
||||||
min-width: 80px;
|
min-width: 64px;
|
||||||
font-size: 2.5rem;
|
font-size: 2rem;
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-screenshot {
|
.main-screenshot {
|
||||||
max-height: 300px;
|
max-height: 240px;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-strip {
|
||||||
|
gap: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 72px;
|
||||||
|
height: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@ -435,21 +452,34 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 1.5rem 1rem;
|
padding: 1.25rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-hero-section {
|
.price-hero-section {
|
||||||
margin: 0.5rem 0 1rem 0;
|
margin: 0.5rem 0 1rem 0;
|
||||||
}
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
.thumbnail {
|
justify-content: space-between;
|
||||||
width: 100px;
|
|
||||||
height: 66px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-content {
|
.markdown-content {
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightbox-close {
|
.lightbox-close {
|
||||||
@ -460,29 +490,33 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
padding: 4rem 1rem;
|
padding: 3rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-stats {
|
.hero-stats {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
margin-top: 3rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-summary-grid {
|
.review-summary-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-avg-block {
|
.review-avg-block {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
padding-bottom: 1.5rem;
|
padding-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-item-premium {
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-layout {
|
.profile-layout {
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export type PluginData = {
|
|||||||
price_cents: number;
|
price_cents: number;
|
||||||
type: 'free' | 'paid' | 'adwall';
|
type: 'free' | 'paid' | 'adwall';
|
||||||
screenshots: { id: number; path: string }[];
|
screenshots: { id: number; path: string }[];
|
||||||
|
preview_path?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
status?: 'published' | 'draft' | 'review';
|
status?: 'published' | 'draft' | 'review';
|
||||||
sale_active?: boolean;
|
sale_active?: boolean;
|
||||||
@ -87,8 +88,8 @@ const ManagePlugin = () => {
|
|||||||
{/* ── Sidebar ───────────────────────────────────────────── */}
|
{/* ── Sidebar ───────────────────────────────────────────── */}
|
||||||
<aside className="mp-sidebar">
|
<aside className="mp-sidebar">
|
||||||
<div className="mp-plugin-identity">
|
<div className="mp-plugin-identity">
|
||||||
<div className="mp-plugin-avatar">
|
<div className="mp-plugin-avatar" style={plugin.preview_path ? { backgroundImage: `url(${plugin.preview_path})`, backgroundSize: 'cover', backgroundPosition: 'center', color: 'transparent' } : {}}>
|
||||||
{plugin.name.charAt(0).toUpperCase()}
|
{!plugin.preview_path && plugin.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="mp-plugin-name">{plugin.name}</p>
|
<p className="mp-plugin-name">{plugin.name}</p>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
import {
|
import {
|
||||||
TrendingUp, TrendingDown, Minus, DollarSign,
|
TrendingUp, TrendingDown, Minus, DollarSign,
|
||||||
Download, MousePointer2, Star, BarChart3
|
Download, MousePointer2, Star, BarChart3, Globe
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAnalytics } from '../useAnalytics';
|
import { useAnalytics } from '../useAnalytics';
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ const KpiCard = ({ title, value, comparison, icon: Icon, chartData, dataKey }: a
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PluginAnalyticsTab: React.FC<Props> = ({ pluginId, pluginName }) => {
|
export const PluginAnalyticsTab: React.FC<Props> = ({ pluginId, pluginName }) => {
|
||||||
const { processedData, totals, ratingSummary, timeframe, setTimeframe, loading } = useAnalytics(pluginId);
|
const { processedData, totals, ratingSummary, referrers, timeframe, setTimeframe, loading } = useAnalytics(pluginId);
|
||||||
const timeframes = ['Today', '7 Days', '1 Month', '1 Year', 'Lifetime'];
|
const timeframes = ['Today', '7 Days', '1 Month', '1 Year', 'Lifetime'];
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -206,8 +206,8 @@ export const PluginAnalyticsTab: React.FC<Props> = ({ pluginId, pluginName }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Sidebar: Rating Histogram */}
|
{/* Right Sidebar: Rating Histogram & Traffic Sources */}
|
||||||
<div>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||||
{ratingSummary && (
|
{ratingSummary && (
|
||||||
<div className="mp-card">
|
<div className="mp-card">
|
||||||
<div className="mp-card-title">
|
<div className="mp-card-title">
|
||||||
@ -240,6 +240,45 @@ export const PluginAnalyticsTab: React.FC<Props> = ({ pluginId, pluginName }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Traffic Referrers & CTR */}
|
||||||
|
<div className="mp-card">
|
||||||
|
<div className="mp-card-title">
|
||||||
|
<Globe size={16} />
|
||||||
|
Traffic Sources & CTR
|
||||||
|
</div>
|
||||||
|
{referrers && referrers.length > 0 ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginTop: '0.75rem' }}>
|
||||||
|
{referrers.map((r: any, idx: number) => (
|
||||||
|
<div key={idx} style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
padding: '8px 10px', background: 'rgba(255,255,255,0.03)', borderRadius: '6px',
|
||||||
|
fontSize: '0.82rem'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', marginRight: '8px' }}>
|
||||||
|
<span style={{ fontWeight: 600, color: 'var(--mp-text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
|
{r.referrer}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: '0.72rem', color: 'var(--mp-text-3)' }}>
|
||||||
|
{r.views} views • {r.downloads} downloads
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(62, 207, 142, 0.12)', color: '#3ecf8e',
|
||||||
|
padding: '2px 8px', borderRadius: '12px', fontWeight: 700, fontSize: '0.75rem',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}>
|
||||||
|
{r.ctr}% CTR
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '1.5rem', color: 'var(--mp-text-3)', fontSize: '0.83rem' }}>
|
||||||
|
No referrer traffic recorded yet.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -174,6 +174,17 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
|
|||||||
try { return DISPLAY_NAMES.of(code) ?? code; } catch { return code; }
|
try { return DISPLAY_NAMES.of(code) ?? code; } catch { return code; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [iconFile, setIconFile] = useState<File | null>(null);
|
||||||
|
const [iconPreview, setIconPreview] = useState<string | null>(plugin.preview_path || null);
|
||||||
|
|
||||||
|
const handleIconChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
setIconFile(file);
|
||||||
|
setIconPreview(URL.createObjectURL(file));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async (e: React.FormEvent) => {
|
const handleSave = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@ -185,6 +196,9 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
|
|||||||
fd.append('youtube_video_url', youtubeVideoUrl);
|
fd.append('youtube_video_url', youtubeVideoUrl);
|
||||||
fd.append('keywords', keywords);
|
fd.append('keywords', keywords);
|
||||||
fd.append('is_early_access', String(isEarlyAccess));
|
fd.append('is_early_access', String(isEarlyAccess));
|
||||||
|
if (iconFile) {
|
||||||
|
fd.append('preview_image', iconFile);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await api.put(`/plugins/${plugin.id}`, fd);
|
await api.put(`/plugins/${plugin.id}`, fd);
|
||||||
onSaved();
|
onSaved();
|
||||||
@ -230,6 +244,44 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
|
|||||||
<div className="mp-card">
|
<div className="mp-card">
|
||||||
<div className="mp-card-title"><Globe size={14} />General Details</div>
|
<div className="mp-card-title"><Globe size={14} />General Details</div>
|
||||||
|
|
||||||
|
<div className="mp-form-group">
|
||||||
|
<label className="mp-label">Plugin Icon</label>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1.25rem', marginTop: '0.25rem' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '72px',
|
||||||
|
height: '72px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1px solid var(--mp-border)',
|
||||||
|
background: iconPreview ? `url(${iconPreview}) center/cover no-repeat` : 'var(--mp-surface-2)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 800,
|
||||||
|
color: 'var(--mp-text-2)',
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
|
{!iconPreview && name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="icon-upload-input" className="mp-btn mp-btn-secondary" style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 1rem', fontSize: '0.82rem' }}>
|
||||||
|
<ImageIcon size={14} />
|
||||||
|
Change Icon
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="icon-upload-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg,image/webp"
|
||||||
|
onChange={handleIconChange}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<p style={{ margin: '0.35rem 0 0 0', fontSize: '0.74rem', color: 'var(--mp-text-3)' }}>
|
||||||
|
Square image recommended (PNG, JPEG, WebP). Max 2MB.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mp-form-group">
|
<div className="mp-form-group">
|
||||||
<label className="mp-label">Plugin Name</label>
|
<label className="mp-label">Plugin Name</label>
|
||||||
<input className="mp-input" type="text" value={name}
|
<input className="mp-input" type="text" value={name}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import api from '../../api';
|
|||||||
export const useAnalytics = (pluginId?: number) => {
|
export const useAnalytics = (pluginId?: number) => {
|
||||||
const [timeSeries, setTimeSeries] = useState<any[]>([]);
|
const [timeSeries, setTimeSeries] = useState<any[]>([]);
|
||||||
const [ratingSummary, setRatingSummary] = useState<any>(null);
|
const [ratingSummary, setRatingSummary] = useState<any>(null);
|
||||||
|
const [referrers, setReferrers] = useState<any[]>([]);
|
||||||
const [mapData, setMapData] = useState<any[]>([]);
|
const [mapData, setMapData] = useState<any[]>([]);
|
||||||
const [breakdowns, setBreakdowns] = useState<any[]>([]);
|
const [breakdowns, setBreakdowns] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -20,12 +21,14 @@ export const useAnalytics = (pluginId?: number) => {
|
|||||||
|
|
||||||
setTimeSeries(Array.isArray(res.data.timeSeries) ? res.data.timeSeries : []);
|
setTimeSeries(Array.isArray(res.data.timeSeries) ? res.data.timeSeries : []);
|
||||||
setRatingSummary(res.data.ratingSummary || null);
|
setRatingSummary(res.data.ratingSummary || null);
|
||||||
|
setReferrers(Array.isArray(res.data.referrers) ? res.data.referrers : []);
|
||||||
setMapData(Array.isArray(res.data.mapData) ? res.data.mapData : []);
|
setMapData(Array.isArray(res.data.mapData) ? res.data.mapData : []);
|
||||||
setBreakdowns(Array.isArray(res.data.breakdowns) ? res.data.breakdowns : []);
|
setBreakdowns(Array.isArray(res.data.breakdowns) ? res.data.breakdowns : []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Analytics Fetch Error:", error);
|
console.error("Analytics Fetch Error:", error);
|
||||||
setTimeSeries([]);
|
setTimeSeries([]);
|
||||||
setRatingSummary(null);
|
setRatingSummary(null);
|
||||||
|
setReferrers([]);
|
||||||
setMapData([]);
|
setMapData([]);
|
||||||
setBreakdowns([]);
|
setBreakdowns([]);
|
||||||
} finally {
|
} finally {
|
||||||
@ -83,6 +86,7 @@ export const useAnalytics = (pluginId?: number) => {
|
|||||||
return {
|
return {
|
||||||
timeSeries,
|
timeSeries,
|
||||||
ratingSummary,
|
ratingSummary,
|
||||||
|
referrers,
|
||||||
mapData,
|
mapData,
|
||||||
breakdowns,
|
breakdowns,
|
||||||
processedData,
|
processedData,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user