some fixes

This commit is contained in:
Alexander Medvedev 2026-07-29 20:04:50 +02:00
parent ef5092ee4b
commit 93511af260
7 changed files with 168 additions and 29 deletions

View File

@ -7,6 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<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 rel="search" type="application/opensearchdescription+xml" title="Pumpkin Market" href="/opensearch.xml" />
<title>Pumpkin Market</title>
</head>
<body>

8
public/opensearch.xml Normal file
View 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>

View File

@ -391,7 +391,7 @@ h3 {
.sidebar-widget {
position: static;
padding: 1.5rem;
padding: 1.25rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
@ -404,28 +404,45 @@ h3 {
}
.plugin-title {
font-size: 2.25rem;
font-size: 1.75rem;
line-height: 1.2;
word-break: break-word;
overflow-wrap: break-word;
}
.plugin-detail-header {
flex-direction: row;
align-items: flex-start;
gap: 1.25rem;
margin-bottom: 2rem;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.plugin-icon.large {
width: 80px;
height: 80px;
min-width: 80px;
font-size: 2.5rem;
border-radius: 16px;
width: 64px;
height: 64px;
min-width: 64px;
font-size: 2rem;
border-radius: 12px;
}
.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 {
@ -435,21 +452,34 @@ h3 {
}
.container {
padding: 1.5rem 1rem;
padding: 1.25rem 1rem;
}
.price-hero-section {
margin: 0.5rem 0 1rem 0;
}
.thumbnail {
width: 100px;
height: 66px;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.markdown-content {
font-size: 1rem;
font-size: 0.95rem;
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 {
@ -460,29 +490,33 @@ h3 {
}
.hero {
padding: 4rem 1rem;
padding: 3rem 1rem;
}
.hero h1 {
font-size: 2.5rem;
font-size: 2.2rem;
}
.hero-stats {
flex-direction: column;
gap: 1.5rem;
margin-top: 3rem;
margin-top: 2rem;
}
.review-summary-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
gap: 1.25rem;
}
.review-avg-block {
border-right: none;
border-bottom: 1px solid var(--border);
padding-right: 0;
padding-bottom: 1.5rem;
padding-bottom: 1.25rem;
}
.review-item-premium {
padding: 1rem;
}
.profile-layout {

View File

@ -24,6 +24,7 @@ export type PluginData = {
price_cents: number;
type: 'free' | 'paid' | 'adwall';
screenshots: { id: number; path: string }[];
preview_path?: string;
version?: string;
status?: 'published' | 'draft' | 'review';
sale_active?: boolean;
@ -87,8 +88,8 @@ const ManagePlugin = () => {
{/* ── Sidebar ───────────────────────────────────────────── */}
<aside className="mp-sidebar">
<div className="mp-plugin-identity">
<div className="mp-plugin-avatar">
{plugin.name.charAt(0).toUpperCase()}
<div className="mp-plugin-avatar" style={plugin.preview_path ? { backgroundImage: `url(${plugin.preview_path})`, backgroundSize: 'cover', backgroundPosition: 'center', color: 'transparent' } : {}}>
{!plugin.preview_path && plugin.name.charAt(0).toUpperCase()}
</div>
<div>
<p className="mp-plugin-name">{plugin.name}</p>

View File

@ -5,7 +5,7 @@ import {
} from 'recharts';
import {
TrendingUp, TrendingDown, Minus, DollarSign,
Download, MousePointer2, Star, BarChart3
Download, MousePointer2, Star, BarChart3, Globe
} from 'lucide-react';
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 }) => {
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'];
if (loading) {
@ -206,8 +206,8 @@ export const PluginAnalyticsTab: React.FC<Props> = ({ pluginId, pluginName }) =>
</div>
</div>
{/* Right Sidebar: Rating Histogram */}
<div>
{/* Right Sidebar: Rating Histogram & Traffic Sources */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{ratingSummary && (
<div className="mp-card">
<div className="mp-card-title">
@ -240,6 +240,45 @@ export const PluginAnalyticsTab: React.FC<Props> = ({ pluginId, pluginName }) =>
</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>

View File

@ -174,6 +174,17 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
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) => {
e.preventDefault();
setSaving(true);
@ -185,6 +196,9 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
fd.append('youtube_video_url', youtubeVideoUrl);
fd.append('keywords', keywords);
fd.append('is_early_access', String(isEarlyAccess));
if (iconFile) {
fd.append('preview_image', iconFile);
}
try {
await api.put(`/plugins/${plugin.id}`, fd);
onSaved();
@ -230,6 +244,44 @@ const StoreListing = ({ plugin, onSaved }: Props) => {
<div className="mp-card">
<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">
<label className="mp-label">Plugin Name</label>
<input className="mp-input" type="text" value={name}

View File

@ -4,6 +4,7 @@ import api from '../../api';
export const useAnalytics = (pluginId?: number) => {
const [timeSeries, setTimeSeries] = useState<any[]>([]);
const [ratingSummary, setRatingSummary] = useState<any>(null);
const [referrers, setReferrers] = useState<any[]>([]);
const [mapData, setMapData] = useState<any[]>([]);
const [breakdowns, setBreakdowns] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
@ -20,12 +21,14 @@ export const useAnalytics = (pluginId?: number) => {
setTimeSeries(Array.isArray(res.data.timeSeries) ? res.data.timeSeries : []);
setRatingSummary(res.data.ratingSummary || null);
setReferrers(Array.isArray(res.data.referrers) ? res.data.referrers : []);
setMapData(Array.isArray(res.data.mapData) ? res.data.mapData : []);
setBreakdowns(Array.isArray(res.data.breakdowns) ? res.data.breakdowns : []);
} catch (error) {
console.error("Analytics Fetch Error:", error);
setTimeSeries([]);
setRatingSummary(null);
setReferrers([]);
setMapData([]);
setBreakdowns([]);
} finally {
@ -83,6 +86,7 @@ export const useAnalytics = (pluginId?: number) => {
return {
timeSeries,
ratingSummary,
referrers,
mapData,
breakdowns,
processedData,