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.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
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 {
|
||||
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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user