From b162833243da2f30dcee4e6dd379b8754a516829 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Thu, 27 Aug 2026 21:54:06 +0200 Subject: [PATCH] fix: some bugs --- src/App.css | 22 +- src/App.tsx | 53 ++- src/components/DeveloperOnboardingModal.tsx | 50 ++- src/pages/PluginDetail.tsx | 5 + src/pages/ProfilePage.tsx | 237 +++++++++---- src/pages/SearchResultsPage.tsx | 17 +- src/pages/admin/AdminPanel.tsx | 6 +- src/pages/auth/ForgotPasswordPage.tsx | 2 + src/pages/auth/LoginPage.tsx | 10 +- src/pages/auth/RegisterPage.tsx | 4 +- src/pages/auth/ResetPasswordPage.tsx | 4 + src/pages/dashboard/Dashboard.css | 342 ++++++++++++++++++ src/pages/dashboard/DashboardPlugins.tsx | 355 +++++++++++++++---- src/pages/dashboard/plugin/AddPlugin.tsx | 60 +++- src/pages/dashboard/plugin/Coupons.tsx | 24 +- src/pages/dashboard/plugin/DangerZone.tsx | 4 + src/pages/dashboard/plugin/Licenses.tsx | 6 +- src/pages/dashboard/plugin/Pricing.tsx | 25 +- src/pages/dashboard/plugin/PublishUpdate.tsx | 6 +- src/pages/dashboard/plugin/StoreListing.tsx | 59 ++- 20 files changed, 1080 insertions(+), 211 deletions(-) diff --git a/src/App.css b/src/App.css index 32a649a..89b758e 100644 --- a/src/App.css +++ b/src/App.css @@ -216,7 +216,7 @@ h3 { .nav-search-input-wrap input { width: 100%; - padding: 8px 16px 8px 40px; + padding: 8px 34px 8px 40px; border-radius: 20px; border: 1px solid var(--border); background: rgba(255, 255, 255, 0.05); @@ -226,6 +226,26 @@ h3 { transition: all 0.2s ease; } +.nav-search-clear { + position: absolute; + right: 12px; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 3px; + border-radius: 50%; + transition: color 0.15s, background-color 0.15s; +} + +.nav-search-clear:hover { + color: var(--text); + background: rgba(255, 255, 255, 0.1); +} + .nav-search-input-wrap input::placeholder { color: var(--text-muted); } diff --git a/src/App.tsx b/src/App.tsx index 12e9745..002609c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route, Link, NavLink, useNavigate, Outlet, Navigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Search, Store } from 'lucide-react'; +import { Search, Store, X } from 'lucide-react'; import Home from './pages/Home'; import PluginDetail from './pages/PluginDetail'; import LoginPage from './pages/auth/LoginPage'; @@ -272,18 +272,45 @@ const DashboardLayout = () => { // --- Components --- const Navbar = ({ user }: any) => { const { t } = useTranslation(); - const [searchQuery, setSearchQuery] = useState(''); + const location = useLocation(); + const [searchQuery, setSearchQuery] = useState(() => { + if (typeof window !== 'undefined' && window.location.pathname === '/search') { + return new URLSearchParams(window.location.search).get('q') || ''; + } + return ''; + }); const [isMenuOpen, setIsMenuOpen] = useState(false); const navigate = useNavigate(); - const handleSearch = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' && searchQuery.trim()) { - navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`); + // Sync searchQuery with URL 'q' parameter when navigating + useEffect(() => { + if (location.pathname === '/search') { + const urlQuery = new URLSearchParams(location.search).get('q') || ''; + setSearchQuery(urlQuery); + } else { setSearchQuery(''); + } + }, [location.pathname, location.search]); + + const handleSearch = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + const query = searchQuery.trim(); + if (query) { + navigate(`/search?q=${encodeURIComponent(query)}`); + } else { + navigate('/search'); + } setIsMenuOpen(false); } }; + const handleClear = () => { + setSearchQuery(''); + if (location.pathname === '/search') { + navigate('/search'); + } + }; + const toggleMenu = () => setIsMenuOpen(!isMenuOpen); return ( @@ -319,12 +346,26 @@ const Navbar = ({ user }: any) => {
setSearchQuery(e.target.value)} onKeyDown={handleSearch} + autoComplete="off" + autoCorrect="off" + spellCheck={false} /> + {searchQuery && ( + + )}
diff --git a/src/components/DeveloperOnboardingModal.tsx b/src/components/DeveloperOnboardingModal.tsx index 5b95976..30dc1a7 100644 --- a/src/components/DeveloperOnboardingModal.tsx +++ b/src/components/DeveloperOnboardingModal.tsx @@ -177,13 +177,16 @@ export const DeveloperOnboardingModal: React.FC =
- + setDisplayName(e.target.value)} + autoComplete="nickname" maxLength={60} autoFocus /> @@ -234,51 +237,63 @@ export const DeveloperOnboardingModal: React.FC = {isSellingPaid && step === 2 && (
-
- + setStreetAddress(e.target.value)} placeholder="123 Main St, Suite 400" + autoComplete="street-address" required />
- + setCity(e.target.value)} placeholder="City" + autoComplete="address-level2" required />
- + setPostalCode(e.target.value)} placeholder="10001" + autoComplete="postal-code" required />
@@ -286,11 +301,14 @@ export const DeveloperOnboardingModal: React.FC =
- + setVatId(e.target.value)} placeholder="EU123456789 or Tax ID" + autoComplete="off" />
@@ -329,36 +350,45 @@ export const DeveloperOnboardingModal: React.FC = {((!isSellingPaid && step === 2) || (isSellingPaid && step === 3)) && (
- + setSupportEmail(e.target.value)} placeholder="support@yourdomain.com" + autoComplete="email" required />
- + setWebsiteUrl(e.target.value)} placeholder="https://yourwebsite.com" + autoComplete="url" />
- + setGithubUrl(e.target.value)} placeholder="https://github.com/yourname" + autoComplete="url" />
diff --git a/src/pages/PluginDetail.tsx b/src/pages/PluginDetail.tsx index c41333d..012bdf2 100644 --- a/src/pages/PluginDetail.tsx +++ b/src/pages/PluginDetail.tsx @@ -1157,11 +1157,16 @@ const PluginDetail = () => {
setCouponCode(e.target.value.toUpperCase())} placeholder="PROMOCODE" disabled={validatingCoupon || appliedCoupon !== null} + autoComplete="off" + autoCapitalize="characters" + spellCheck={false} style={{ flex: 1, padding: '8px 12px', diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index b955959..c93f57e 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -38,6 +38,110 @@ const NAV: { key: Tab; label: string; icon: React.FC<{ size?: number }> ; danger { key: 'danger', label: 'Danger Zone', icon: AlertTriangle, danger: true }, ]; +// ── Settings Subcomponents (defined at module level to prevent remounting on state changes) ── + +interface SettingsCardProps { + title?: string; + description?: string; + icon?: React.FC<{ size?: number }>; + children: React.ReactNode; +} + +const SettingsCard: React.FC = ({ title, description, icon: Icon, children }) => ( +
+ {(title || Icon) && ( +
+ {Icon &&
} +
+ {title &&

{title}

} + {description &&

{description}

} +
+
+ )} +
{children}
+
+); + +interface FieldProps { + label: string; + hint?: string; + children: React.ReactNode; +} + +const Field: React.FC = ({ label, hint, children }) => ( +
+ + {hint &&

{hint}

} + {children} +
+); + +interface SaveBtnProps { + isSaving?: boolean; + label?: string; +} + +const SaveBtn: React.FC = ({ isSaving = false, label = 'Save Changes' }) => ( + +); + +interface ToggleProps { + checked: boolean; + onChange: (v: boolean) => void; + label: string; + desc?: string; +} + +const Toggle: React.FC = ({ checked, onChange, label, desc }) => ( +
+
+

{label}

+ {desc &&

{desc}

} +
+ +
+); + +interface PwInputProps { + placeholder: string; + value: string; + onChange: (v: string) => void; + show: boolean; + onToggle: () => void; + autoComplete?: string; + id?: string; + name?: string; +} + +const PwInput: React.FC = ({ placeholder, value, onChange, show, onToggle, autoComplete, id, name }) => ( +
+ onChange(e.target.value)} + autoComplete={autoComplete} + required + /> + +
+); + const ProfilePage = () => { const { user, login, logout, refreshUser } = useAuth(); const navigate = useNavigate(); @@ -316,75 +420,6 @@ const ProfilePage = () => { } }; - // ── helpers ────────────────────────────────────────────────────────────────── - - const SettingsCard = ({ title, description, icon: Icon, children }: { - title: string; description?: string; icon?: React.FC<{ size?: number }>; children: React.ReactNode; - }) => ( -
- {(title || Icon) && ( -
- {Icon &&
} -
-

{title}

- {description &&

{description}

} -
-
- )} -
{children}
-
- ); - - const Field = ({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) => ( -
- - {hint &&

{hint}

} - {children} -
- ); - - const SaveBtn = ({ id, label = 'Save Changes' }: { id: string; label?: string }) => ( - - ); - - const Toggle = ({ checked, onChange, label, desc }: { checked: boolean; onChange: (v: boolean) => void; label: string; desc?: string }) => ( -
-
-

{label}

- {desc &&

{desc}

} -
- -
- ); - - const PwInput = ({ placeholder, value, onChange, show, onToggle }: { - placeholder: string; value: string; onChange: (v: string) => void; show: boolean; onToggle: () => void; - }) => ( -
- onChange(e.target.value)} - required - /> - -
- ); - // ── render ──────────────────────────────────────────────────────────────────── return ( @@ -452,22 +487,28 @@ const ProfilePage = () => {
setFormData({ ...formData, username: e.target.value })} placeholder="Your username" + autoComplete="username" required /> setFormData({ ...formData, email: e.target.value })} placeholder="your@email.com" + autoComplete="email" required /> @@ -475,9 +516,12 @@ const ProfilePage = () => { setVerify2faCode(e.target.value.replace(/\D/g, ''))} + onChange={e => setVerify2faCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + autoComplete="one-time-code" + inputMode="numeric" + pattern="[0-9]*" required + autoFocus /> @@ -663,93 +725,120 @@ const ProfilePage = () => { setDevProfileForm({ ...devProfileForm, displayName: e.target.value })} + autoComplete="nickname" required /> setDevProfileForm({ ...devProfileForm, legalName: e.target.value })} + autoComplete="name" /> setDevProfileForm({ ...devProfileForm, streetAddress: e.target.value })} placeholder="123 Main St" + autoComplete="street-address" />
setDevProfileForm({ ...devProfileForm, city: e.target.value })} + autoComplete="address-level2" /> setDevProfileForm({ ...devProfileForm, postalCode: e.target.value })} + autoComplete="postal-code" />
setDevProfileForm({ ...devProfileForm, vatId: e.target.value })} placeholder="EU123456789" + autoComplete="off" /> setDevProfileForm({ ...devProfileForm, supportEmail: e.target.value })} placeholder="support@domain.com" + autoComplete="email" /> setDevProfileForm({ ...devProfileForm, websiteUrl: e.target.value })} placeholder="https://website.com" + autoComplete="url" /> setDevProfileForm({ ...devProfileForm, githubUrl: e.target.value })} placeholder="https://github.com/username" + autoComplete="url" /> - + )} diff --git a/src/pages/SearchResultsPage.tsx b/src/pages/SearchResultsPage.tsx index d0f9f32..2717f93 100644 --- a/src/pages/SearchResultsPage.tsx +++ b/src/pages/SearchResultsPage.tsx @@ -65,7 +65,7 @@ const SearchResultsPage = () => { return (
@@ -103,14 +103,22 @@ const SearchResultsPage = () => {
setMinPrice(e.target.value)} /> - setMaxPrice(e.target.value)} @@ -122,10 +130,7 @@ const SearchResultsPage = () => {

- {searchQuery - ? <>Results for "{searchQuery}" - : 'All Plugins' - } + All Plugins ({plugins.length} found)

@@ -159,7 +164,7 @@ const SearchResultsPage = () => {

{searchQuery - ? `No plugins found for "${searchQuery}" with these filters.` + ? `No plugins found matching your search and filters.` : 'No plugins available with these filters.' }

diff --git a/src/pages/admin/AdminPanel.tsx b/src/pages/admin/AdminPanel.tsx index d9ffd7c..6c30e24 100644 --- a/src/pages/admin/AdminPanel.tsx +++ b/src/pages/admin/AdminPanel.tsx @@ -143,10 +143,14 @@ const AdminPanel = () => {
setSearchQuery(e.target.value)} + autoComplete="off" + spellCheck={false} />
{activeTab === 'logs' && ( diff --git a/src/pages/auth/ForgotPasswordPage.tsx b/src/pages/auth/ForgotPasswordPage.tsx index 3cebaa4..8efc195 100644 --- a/src/pages/auth/ForgotPasswordPage.tsx +++ b/src/pages/auth/ForgotPasswordPage.tsx @@ -273,12 +273,14 @@ const ForgotPasswordPage = () => {
diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index 02fe85d..c8a1711 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -373,12 +373,16 @@ const LoginPage = () => {
setTotpCode(e.target.value.replace(/\D/g, ''))} + onChange={e => setTotpCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + autoComplete="one-time-code" + inputMode="numeric" + pattern="[0-9]*" required disabled={isSubmitting} autoFocus @@ -393,6 +397,7 @@ const LoginPage = () => {
{
{
setForm({ ...form, username: e.target.value })} required disabled={isSubmitting} autoComplete="username" autoCapitalize="none" spellCheck={false} @@ -306,6 +306,7 @@ const RegisterPage = () => {
{
{ setPassword(e.target.value)} + autoComplete="new-password" required disabled={isSubmitting} /> @@ -204,11 +206,13 @@ const ResetPasswordPage = () => { setConfirmPassword(e.target.value)} + autoComplete="new-password" required disabled={isSubmitting} /> diff --git a/src/pages/dashboard/Dashboard.css b/src/pages/dashboard/Dashboard.css index 59cb8d2..1cdcc02 100644 --- a/src/pages/dashboard/Dashboard.css +++ b/src/pages/dashboard/Dashboard.css @@ -207,6 +207,348 @@ border: 1px solid var(--dash-border); } +/* --- Developer Plugins List --- */ +.dev-plugins-container { + width: 100%; + max-width: 100%; +} + +.dev-plugins-card { + background: var(--dash-surface, #141414); + border: 1px solid var(--dash-border, #2a2a2a); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); +} + +.dev-plugins-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem 1.75rem; + border-bottom: 1px solid var(--dash-border, #2a2a2a); + flex-wrap: wrap; + gap: 1rem; +} + +.dev-plugins-title-area { + display: flex; + align-items: baseline; + gap: 0.75rem; +} + +.dev-plugins-title { + font-size: 1.35rem; + font-weight: 700; + color: var(--dash-text, #eaeaea); + margin: 0; +} + +.dev-plugins-count { + font-size: 0.85rem; + font-weight: 600; + color: var(--dash-text-muted, #888); + font-family: var(--font-mono, monospace); + background: rgba(255, 255, 255, 0.06); + padding: 2px 8px; + border-radius: 12px; +} + +.dev-plugins-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.85rem 1.75rem; + background: rgba(0, 0, 0, 0.15); + border-bottom: 1px solid var(--dash-border, #2a2a2a); + gap: 1rem; + flex-wrap: wrap; +} + +.dev-plugins-filter-box { + display: flex; + align-items: center; + gap: 8px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--dash-border, #2a2a2a); + border-radius: 8px; + padding: 6px 12px; + width: 100%; + max-width: 320px; + transition: all 0.2s ease; +} + +.dev-plugins-filter-box:focus-within { + border-color: var(--dash-primary, #f97316); + background: rgba(0, 0, 0, 0.4); + box-shadow: 0 0 10px rgba(249, 115, 22, 0.2); +} + +.dev-plugins-filter-box input { + background: transparent; + border: none; + color: var(--dash-text, #eaeaea); + font-size: 0.85rem; + outline: none; + width: 100%; +} + +.dev-plugins-filter-tabs { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.dev-plugins-filter-tab { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--dash-border, #2a2a2a); + color: var(--dash-text-muted, #888); + padding: 4px 12px; + border-radius: 16px; + font-size: 0.78rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.dev-plugins-filter-tab:hover { + color: var(--dash-text, #eaeaea); + border-color: rgba(255, 255, 255, 0.2); +} + +.dev-plugins-filter-tab.active { + background: var(--dash-primary-light, rgba(249, 115, 22, 0.15)); + color: var(--dash-primary, #f97316); + border-color: rgba(249, 115, 22, 0.4); + font-weight: 600; +} + +.dev-plugins-table { + width: 100%; + border-collapse: collapse; + text-align: left; +} + +.dev-plugins-table th { + padding: 12px 18px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--dash-text-muted, #888); + border-bottom: 1px solid var(--dash-border, #2a2a2a); + background: rgba(0, 0, 0, 0.2); +} + +.dev-plugins-table td { + padding: 14px 18px; + border-bottom: 1px solid var(--dash-border, #2a2a2a); + vertical-align: middle; + transition: background 0.15s ease; +} + +.dev-plugins-row:hover td { + background: rgba(255, 255, 255, 0.025); +} + +.dev-plugins-row:last-child td { + border-bottom: none; +} + +/* Plugin Column - Wide & Prominent */ +.dev-plugins-col-main { + width: 44%; + min-width: 320px; +} + +.dev-plugins-cell { + display: flex; + align-items: center; + gap: 16px; +} + +.dev-plugins-thumb { + width: 116px; + height: 65px; + aspect-ratio: 16 / 9; + border-radius: 8px; + background: #181818; + border: 1px solid rgba(255, 255, 255, 0.1); + overflow: hidden; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + position: relative; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + cursor: pointer; +} + +.dev-plugins-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.dev-plugins-row:hover .dev-plugins-thumb img { + transform: scale(1.05); +} + +.dev-plugins-thumb-fallback { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(15, 23, 42, 0.9)); + color: var(--dash-primary, #f97316); + font-weight: 700; + font-size: 1.25rem; +} + +.dev-plugins-info { + display: flex; + flex-direction: column; + gap: 5px; + min-width: 0; + flex: 1; +} + +.dev-plugins-title-row { + display: flex; + align-items: center; + gap: 8px; +} + +.dev-plugins-item-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--dash-text, #eaeaea); + text-decoration: none; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.15s ease; +} + +.dev-plugins-item-title:hover { + color: var(--dash-primary, #f97316); +} + +.dev-plugins-meta-row { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.dev-plugins-badge { + font-size: 0.7rem; + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.07); + color: var(--dash-text-muted, #94a3b8); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.dev-plugins-badge-mono { + font-family: var(--font-mono, monospace); +} + +.dev-plugins-badge-early { + background: rgba(245, 158, 11, 0.15); + color: #f59e0b; + border-color: rgba(245, 158, 11, 0.3); +} + +.dev-plugins-badge-paid { + background: rgba(16, 185, 129, 0.12); + color: #10b981; + border-color: rgba(16, 185, 129, 0.25); +} + +.dev-plugins-hover-actions { + display: flex; + align-items: center; + gap: 6px; + margin-top: 2px; + opacity: 0; + transform: translateY(2px); + transition: all 0.15s ease; +} + +.dev-plugins-row:hover .dev-plugins-hover-actions { + opacity: 1; + transform: translateY(0); +} + +.dev-plugins-action-icon-btn { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--dash-text-secondary, #a3a3a3); + border-radius: 4px; + padding: 4px 6px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.dev-plugins-action-icon-btn:hover { + background: rgba(249, 115, 22, 0.15); + color: var(--dash-primary, #f97316); + border-color: rgba(249, 115, 22, 0.35); +} + +/* Visibility dot & badge */ +.dev-plugins-visibility-cell { + display: flex; + flex-direction: column; + gap: 4px; +} + +.dev-plugins-visibility-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.85rem; + font-weight: 500; + color: var(--dash-text, #eaeaea); +} + +.dev-plugins-status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #10b981; + box-shadow: 0 0 6px rgba(16, 185, 129, 0.5); +} + +.dev-plugins-type-pill { + font-size: 0.72rem; + color: var(--dash-text-muted, #888); + font-weight: 500; +} + +.dev-plugins-date-cell { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 0.8rem; + color: var(--dash-text-muted, #888); +} + +.dev-plugins-date-primary { + color: var(--dash-text, #eaeaea); + font-weight: 500; +} + @media (max-width: 768px) { .dashboard-nav { display: flex; diff --git a/src/pages/dashboard/DashboardPlugins.tsx b/src/pages/dashboard/DashboardPlugins.tsx index d1df72d..17ba6e2 100644 --- a/src/pages/dashboard/DashboardPlugins.tsx +++ b/src/pages/dashboard/DashboardPlugins.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAnalytics } from './useAnalytics'; import { useNavigate } from 'react-router-dom'; -import { Settings, Plus, ExternalLink, Sparkles } from 'lucide-react'; +import { Settings, Plus, ExternalLink, Sparkles, Search, BarChart2, Package } from 'lucide-react'; import { useAuth } from '../../App'; import DeveloperOnboardingModal from '../../components/DeveloperOnboardingModal'; import { getPluginUrl } from '../../utils/url'; @@ -14,6 +14,8 @@ const DashboardPlugins = () => { const { timeSeries } = useAnalytics(); const navigate = useNavigate(); const [isDevModalOpen, setIsDevModalOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [filterType, setFilterType] = useState('all'); // Grouping analytics data by plugin ID const plugins = useMemo(() => { @@ -22,19 +24,56 @@ const DashboardPlugins = () => { if (!acc[id]) { acc[id] = { id: id, - name: curr.name, + name: curr.name, + version: curr.version, + pluginType: curr.pluginType || curr.type, + category: curr.category, + previewPath: curr.previewPath || curr.preview_path, + priceCents: curr.priceCents || curr.price_cents || 0, + isEarlyAccess: curr.isEarlyAccess || curr.is_early_access || false, downloads: 0, earnings: 0, + views: 0, + avgRating: curr.avgRating || 0, created_at: curr.created_at, updated_at: curr.updated_at }; } acc[id].downloads += (curr.downloads || 0); acc[id].earnings += (curr.earnings || 0); + acc[id].views += (curr.views || 0); + if (curr.avgRating > 0) acc[id].avgRating = curr.avgRating; return acc; }, {} as Record)); }, [timeSeries]); + // Check if creator has any paid plugins or earnings + const hasPaidPlugins = useMemo(() => { + return plugins.some((p: any) => + (p.pluginType && p.pluginType.toLowerCase() === 'paid') || + (p.priceCents && p.priceCents > 0) || + (p.earnings && p.earnings > 0) + ); + }, [plugins]); + + const filteredPlugins = useMemo(() => { + return plugins.filter((p: any) => { + const term = searchTerm.trim().toLowerCase(); + const matchesSearch = !term || + (p.name && p.name.toLowerCase().includes(term)) || + (p.category && p.category.toLowerCase().includes(term)) || + String(p.id).includes(term); + + const type = (p.pluginType || 'free').toLowerCase(); + const matchesType = filterType === 'all' || + (filterType === 'paid' && type === 'paid') || + (filterType === 'free' && type === 'free') || + (filterType === 'early_access' && p.isEarlyAccess); + + return matchesSearch && matchesType; + }); + }, [plugins, searchTerm, filterType]); + const formatDate = (dateStr: string) => { if (!dateStr || typeof dateStr !== 'string') return 'N/A'; const normalized = dateStr.toLowerCase().trim(); @@ -47,8 +86,8 @@ const DashboardPlugins = () => { if (!user?.is_developer) { return ( -
-
+
+
{ } return ( -
-
-
-

{t('developer.dashboard.plugins')}

+
+
+ {/* Header */} +
+
+

{t('developer.dashboard.plugins')}

+ {plugins.length} +
- - - - - - - - - - - - - {plugins.map((p: any) => ( - - - - - - - - ))} - - {plugins.length === 0 && ( - - - + + {/* Toolbar */} +
+
+ + setSearchTerm(e.target.value)} + autoComplete="off" + spellCheck={false} + /> +
+ +
+ + {hasPaidPlugins && ( + )} -
-
{t('plugin.version')}{t('developer.dashboard.total_downloads')}{t('developer.dashboard.total_revenue')}DatesActions
-
- {p.name} - ID: {p.id} -
-
{p.downloads.toLocaleString()} - +${p.earnings.toFixed(2)} - -
-
Created: {formatDate(p.created_at)}
-
Updated: {formatDate(p.updated_at)}
-
-
-
- {/* View on Marketplace */} - - - {/* Manage/Edit Plugin */} - -
-
-
-

No plugins published yet.

- Click "Add New Plugin" to get started -
-
+ + +
+
+ + {/* Table */} +
+ + + + + + + {hasPaidPlugins && } + + + + + + {filteredPlugins.map((p: any) => { + const pluginType = (p.pluginType || 'free').toLowerCase(); + const isPaid = pluginType === 'paid' || (p.priceCents && p.priceCents > 0); + const priceFormatted = isPaid ? `€${((p.priceCents || 0) / 100).toFixed(2)}` : 'Free'; + const initial = (p.name || '?').charAt(0).toUpperCase(); + + return ( + + {/* Plugin Details (Wide Column) */} + + + {/* Visibility & Type */} + + + {/* Total Downloads */} + + + {/* Total Revenue (Only shown if hasPaidPlugins is true) */} + {hasPaidPlugins && ( + + )} + + {/* Dates */} + + + {/* Quick Actions */} + + + ); + })} + + {filteredPlugins.length === 0 && ( + + + + )} + +
PluginVisibility{t('developer.dashboard.total_downloads')}{t('developer.dashboard.total_revenue')}DateActions
+
+ {/* 16:9 Thumbnail */} +
navigate(`/dashboard/manage-plugin/${p.id}`)} + title="Manage Plugin" + > + {p.previewPath ? ( + {p.name} + ) : ( +
+ {initial} +
+ )} +
+ + {/* Info & Meta */} +
+
+ navigate(`/dashboard/manage-plugin/${p.id}`)} + title={p.name} + > + {p.name} + +
+ +
+ + {p.version ? `v${p.version}` : `ID: ${p.id}`} + + {p.category && ( + + {p.category} + + )} + {p.isEarlyAccess && ( + + Early Access + + )} +
+ + {/* Hover Quick Actions */} +
+ + + +
+
+
+
+
+
+ + Public +
+ + {isPaid ? priceFormatted : 'Free'} + +
+
+ {p.downloads.toLocaleString()} + 0 ? 'var(--success)' : 'var(--dash-text-muted)' }}> + {isPaid || p.earnings > 0 ? ( + p.earnings > 0 ? `+€${p.earnings.toFixed(2)}` : '€0.00' + ) : ( + '—' + )} + +
+
{formatDate(p.created_at)}
+
Uploaded
+
+
+
+ + + +
+
+
+ +

+ {searchTerm ? 'No plugins matched your filter.' : 'No plugins published yet.'} +

+ + {searchTerm ? 'Try adjusting your search or category filter.' : 'Click "Add New Plugin" to publish your first creation.'} + +
+
+
); diff --git a/src/pages/dashboard/plugin/AddPlugin.tsx b/src/pages/dashboard/plugin/AddPlugin.tsx index 4c0f81a..d559901 100644 --- a/src/pages/dashboard/plugin/AddPlugin.tsx +++ b/src/pages/dashboard/plugin/AddPlugin.tsx @@ -567,16 +567,19 @@ const AddPlugin = () => { {/* Name */}
-