From 834ea2aa3962926bae1201d4d9b98d632b96c21a Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 31 Mar 2026 18:46:51 +0200 Subject: [PATCH] chore: improve ui --- public/robots.txt | 6 ++ src/App.css | 82 ++++++++++++++++++-- src/App.tsx | 69 ++++++++++------- src/components/SEO.tsx | 96 ++++++++++++++++++++++++ src/pages/Home.tsx | 12 ++- src/pages/PluginDetail.tsx | 40 +++++++++- src/pages/ProfilePage.tsx | 2 +- src/pages/SearchResultsPage.tsx | 7 +- src/pages/admin/AdminPanel.css | 52 +++++++++++++ src/pages/admin/AdminPanel.tsx | 36 ++++----- src/pages/dashboard/Dashboard.css | 79 ++++++++++++++++++- src/pages/dashboard/DashboardPlugins.tsx | 10 +-- src/pages/dashboard/useAnalytics.ts | 7 +- src/pages/legal/PrivacyPolicyPage.tsx | 4 +- src/pages/legal/TermsOfServicePage.tsx | 6 +- 15 files changed, 436 insertions(+), 72 deletions(-) create mode 100644 public/robots.txt create mode 100644 src/components/SEO.tsx diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..01cb689 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,6 @@ +User-agent: * +Allow: / +Disallow: /admin +Disallow: /dashboard + +Sitemap: https://pumpkinmc.net/sitemap.xml diff --git a/src/App.css b/src/App.css index 7de37cb..cd7bf93 100644 --- a/src/App.css +++ b/src/App.css @@ -108,16 +108,24 @@ h1, h2, h3 { font-weight: 900; letter-spacing: -1px; } .navbar { background: rgba(10, 10, 10, 0.7); backdrop-filter: blur(12px); - padding: 1.5rem 2.5rem; + padding: 1rem 2.5rem; display: flex; align-items: center; + justify-content: space-between; border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 1000; } .navbar a { text-decoration: none !important; } -.logo { font-size: 1.5rem; font-weight: 900; text-transform: uppercase; letter-spacing: -1px; text-decoration: none; color: var(--text); } +.logo { font-size: 1.5rem; font-weight: 900; text-transform: uppercase; letter-spacing: -1px; text-decoration: none; color: var(--text); z-index: 1001; } .logo span { color: var(--primary); } -.nav-links { flex-grow: 1; margin-left: 3rem; } + +.nav-content { + display: flex; + align-items: center; + flex-grow: 1; +} + +.nav-links { margin-left: 3rem; } .nav-links a { margin: 0 1rem; text-decoration: none; color: var(--text-muted); font-weight: 600; font-size: 0.9rem; text-transform: uppercase; transition: color 0.2s; } .nav-links a.active, .nav-links a:hover { color: var(--text); text-decoration: none !important; } .nav-search { @@ -142,10 +150,74 @@ h1, h2, h3 { font-weight: 900; letter-spacing: -1px; } .nav-search input:focus { border-color: var(--primary); } -.nav-actions { display: flex; align-items: center; gap: 1.5rem; } +.nav-actions { display: flex; align-items: center; gap: 1rem; } .nav-user-link { font-weight: 700; text-decoration: none; color: var(--text); } .balance { font-family: var(--font-mono); color: var(--success); font-weight: 700; } -footer { text-align: center; padding: 4rem 2rem; border-top: 1px solid var(--border); color: var(--text-muted); } + +.mobile-menu-toggle { + display: none; + background: none; + border: none; + color: var(--text); + cursor: pointer; + padding: 0.5rem; + z-index: 1001; +} + +@media (max-width: 1024px) { + .navbar { padding: 1rem 1.5rem; } + .nav-links { margin-left: 1.5rem; } + .nav-links a { margin: 0 0.5rem; } + .nav-search { margin: 0 1rem; } +} + +@media (max-width: 768px) { + .mobile-menu-toggle { display: block; } + .nav-content { + position: fixed; + top: 0; + right: -100%; + width: 80%; + max-width: 300px; + height: 100vh; + background: var(--surface); + flex-direction: column; + padding: 80px 2rem 2rem; + transition: right 0.3s ease; + box-shadow: -5px 0 15px rgba(0,0,0,0.5); + z-index: 1000; + align-items: flex-start; + } + .nav-content.open { right: 0; } + .nav-links { margin: 0 0 2rem 0; display: flex; flex-direction: column; gap: 1.5rem; } + .nav-links a { margin: 0; font-size: 1.1rem; } + .nav-search { margin: 0 0 2rem 0; max-width: none; width: 100%; } + .nav-actions { flex-direction: column; width: 100%; gap: 1rem; } + .nav-actions .btn { width: 100%; } + + .plugin-detail-layout { grid-template-columns: 1fr; gap: 2rem; } + .sidebar-widget { position: static; } + .plugin-title { font-size: 2.5rem; } + .plugin-detail-header { flex-direction: column; align-items: flex-start; gap: 1rem; } + + .container { padding: 2rem 1rem; } + .hero { padding: 4rem 1rem; } + .hero h1 { font-size: 2.5rem; } + .hero-stats { flex-direction: column; gap: 1.5rem; margin-top: 3rem; } + + .review-summary-grid { grid-template-columns: 1fr; gap: 1.5rem; } + .review-avg-block { border-right: none; border-bottom: 1px solid var(--border); padding-right: 0; padding-bottom: 1.5rem; } + + .profile-layout { flex-direction: column; } + .profile-nav { position: static; min-width: 0; width: 100%; margin-bottom: 1rem; } +} + +@media (max-width: 480px) { + .plugin-grid { grid-template-columns: 1fr; } + .library-grid { grid-template-columns: 1fr; } + .plugin-title { font-size: 2rem; } + .auth-card, .settings-card { padding: 2rem 1.5rem; } +} /* --- Hero Section --- */ .hero { diff --git a/src/App.tsx b/src/App.tsx index 0b2136c..8d1c5c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -168,48 +168,65 @@ const DashboardLayout = () => { const Navbar = ({ user }: any) => { const { t } = useTranslation(); const [searchQuery, setSearchQuery] = useState(''); + 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())}`); setSearchQuery(''); + setIsMenuOpen(false); } }; + const toggleMenu = () => setIsMenuOpen(!isMenuOpen); + return ( ); diff --git a/src/components/SEO.tsx b/src/components/SEO.tsx new file mode 100644 index 0000000..362f321 --- /dev/null +++ b/src/components/SEO.tsx @@ -0,0 +1,96 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +interface SEOProps { + title?: string; + description?: string; + keywords?: string; + canonical?: string; + ogType?: string; + ogImage?: string; +} + +const SEO = ({ + title, + description = "Explore powerful, performance-optimized Minecraft plugins at Pumpkin Market. The one-stop marketplace for WASM-powered MC plugins.", + keywords = "minecraft, plugins, wasm, marketplace, mc plugins, development", + canonical, + ogType = "website", + ogImage = "/icon.png", +}: SEOProps) => { + const location = useLocation(); + const siteName = "Pumpkin Market"; + const fullTitle = title ? `${title} | ${siteName}` : `${siteName} - WASM Minecraft Plugin Marketplace`; + const url = canonical || `https://pumpkinmc.net${location.pathname}`; + + useEffect(() => { + // Update Title + document.title = fullTitle; + + // Update Meta Description + let metaDescription = document.querySelector('meta[name="description"]'); + if (!metaDescription) { + metaDescription = document.createElement('meta'); + metaDescription.setAttribute('name', 'description'); + document.head.appendChild(metaDescription); + } + metaDescription.setAttribute('content', description); + + // Update Meta Keywords + let metaKeywords = document.querySelector('meta[name="keywords"]'); + if (!metaKeywords) { + metaKeywords = document.createElement('meta'); + metaKeywords.setAttribute('name', 'keywords'); + document.head.appendChild(metaKeywords); + } + metaKeywords.setAttribute('content', keywords); + + // Update Canonical + let linkCanonical = document.querySelector('link[rel="canonical"]'); + if (!linkCanonical) { + linkCanonical = document.createElement('link'); + linkCanonical.setAttribute('rel', 'canonical'); + document.head.appendChild(linkCanonical); + } + linkCanonical.setAttribute('href', url); + + // Update Open Graph tags + const updateOG = (property: string, content: string) => { + let tag = document.querySelector(`meta[property="${property}"]`); + if (!tag) { + tag = document.createElement('meta'); + tag.setAttribute('property', property); + document.head.appendChild(tag); + } + tag.setAttribute('content', content); + }; + + updateOG('og:title', fullTitle); + updateOG('og:description', description); + updateOG('og:url', url); + updateOG('og:type', ogType); + updateOG('og:image', ogImage); + updateOG('og:site_name', siteName); + + // Update Twitter Card tags + const updateTwitter = (name: string, content: string) => { + let tag = document.querySelector(`meta[name="${name}"]`); + if (!tag) { + tag = document.createElement('meta'); + tag.setAttribute('name', name); + document.head.appendChild(tag); + } + tag.setAttribute('content', content); + }; + + updateTwitter('twitter:card', 'summary_large_image'); + updateTwitter('twitter:title', fullTitle); + updateTwitter('twitter:description', description); + updateTwitter('twitter:image', ogImage); + + }, [fullTitle, description, keywords, url, ogType, ogImage]); + + return null; // This component doesn't render anything +}; + +export default SEO; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index cd55949..f33b0a0 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import api from '../api'; +import SEO from '../components/SEO'; const Home = () => { const { t, i18n } = useTranslation(); @@ -12,10 +13,13 @@ const Home = () => { if (hasFetched.current) return; api.get('/plugins', { params: { q: '', sort: 'newest' } }) .then(res => { - setPlugins(res.data); + setPlugins(Array.isArray(res.data) ? res.data : []); hasFetched.current = true; }) - .catch(err => console.error('Fetch error:', err)); + .catch(err => { + console.error('Fetch error:', err); + setPlugins([]); + }); }, []); const getDescription = (translatedStr: string) => { @@ -59,6 +63,10 @@ const Home = () => { return ( <> +