chore: change home sorting

This commit is contained in:
Alexander Medvedev 2026-05-15 08:50:18 +02:00
parent 1de42e60bf
commit 56bcb62222
4 changed files with 110 additions and 22 deletions

View File

@ -553,6 +553,14 @@ h3 {
} }
/* --- Plugin Grid --- */ /* --- Plugin Grid --- */
.home-section {
margin-bottom: 4rem;
}
.home-section:last-child {
margin-bottom: 0;
}
.plugin-grid, .plugin-grid,
.home-plugin-grid { .home-plugin-grid {
display: grid; display: grid;

View File

@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, NavLink, useNavigate, Outlet, Navigate } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Link, NavLink, useNavigate, Outlet, Navigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Home from './pages/Home'; import Home from './pages/Home';
import PluginDetail from './pages/PluginDetail'; import PluginDetail from './pages/PluginDetail';
@ -25,6 +25,15 @@ import './App.css';
import api from './api'; import api from './api';
// --- Scroll To Top Helper ---
const ScrollToTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
};
// --- Auth Context --- // --- Auth Context ---
const AuthContext = createContext<any>(null); const AuthContext = createContext<any>(null);
export const useAuth = () => useContext(AuthContext); export const useAuth = () => useContext(AuthContext);
@ -112,6 +121,7 @@ const AdminRoute = ({ children }: { children: React.ReactNode }) => {
const App = () => ( const App = () => (
<Router> <Router>
<AuthProvider> <AuthProvider>
<ScrollToTop />
<Routes> <Routes>
{/* All routes share MainLayout (navbar + footer) */} {/* All routes share MainLayout (navbar + footer) */}
<Route path="/" element={<MainLayout />}> <Route path="/" element={<MainLayout />}>
@ -211,7 +221,10 @@ const Navbar = ({ user }: any) => {
<div className={`nav-content ${isMenuOpen ? 'open' : ''}`}> <div className={`nav-content ${isMenuOpen ? 'open' : ''}`}>
<div className="nav-links"> <div className="nav-links">
<NavLink to="/" onClick={() => setIsMenuOpen(false)}>{t('home')}</NavLink> <NavLink to="/" onClick={() => setIsMenuOpen(false)}>{t('home')}</NavLink>
{user && <NavLink to="/dashboard" onClick={() => setIsMenuOpen(false)}>{t('dashboard')}</NavLink>} {user && (user.plugin_count > 0 || user.role === 'admin') && (
<NavLink to="/dashboard" onClick={() => setIsMenuOpen(false)}>{t('dashboard')}</NavLink>
)}
{user && <NavLink to="/dashboard/add-plugin" onClick={() => setIsMenuOpen(false)}>Publish</NavLink>}
{user && user.role === 'admin' && <NavLink to="/admin" onClick={() => setIsMenuOpen(false)}>Admin</NavLink>} {user && user.role === 'admin' && <NavLink to="/admin" onClick={() => setIsMenuOpen(false)}>Admin</NavLink>}
</div> </div>

View File

@ -4,24 +4,64 @@ import api from '../api';
import SEO from '../components/SEO'; import SEO from '../components/SEO';
import PluginCard from '../components/PluginCard'; import PluginCard from '../components/PluginCard';
// Global cache to persist data across component unmounts (navigation)
// This allows the page to render immediately when going "back",
// which is required for the browser to restore scroll position.
let homeCache: {
topPaid: any[];
popular: any[];
newest: any[];
} | null = null;
const Home = () => { const Home = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [plugins, setPlugins] = useState<any[]>([]); const [topPaid, setTopPaid] = useState<any[]>(homeCache?.topPaid || []);
const hasFetched = useRef(false); const [popular, setPopular] = useState<any[]>(homeCache?.popular || []);
const [newest, setNewest] = useState<any[]>(homeCache?.newest || []);
const [loading, setLoading] = useState(!homeCache);
const hasFetched = useRef(!!homeCache);
useEffect(() => { useEffect(() => {
if (hasFetched.current) return; if (hasFetched.current) return;
api.get('/plugins', { params: { q: '', sort: 'newest' } })
.then(res => { const fetchSections = async () => {
setPlugins(Array.isArray(res.data) ? res.data : []); try {
const [topPaidRes, popularRes, newestRes] = await Promise.all([
api.get('/plugins', { params: { type: 'paid', sort: 'downloads', limit: 4 } }),
api.get('/plugins', { params: { sort: 'downloads', limit: 8 } }),
api.get('/plugins', { params: { sort: 'newest', limit: 12 } })
]);
const data = {
topPaid: Array.isArray(topPaidRes.data) ? topPaidRes.data : [],
popular: Array.isArray(popularRes.data) ? popularRes.data : [],
newest: Array.isArray(newestRes.data) ? newestRes.data : []
};
setTopPaid(data.topPaid);
setPopular(data.popular);
setNewest(data.newest);
homeCache = data;
hasFetched.current = true; hasFetched.current = true;
}) } catch (err) {
.catch(err => {
console.error('Fetch error:', err); console.error('Fetch error:', err);
setPlugins([]); } finally {
}); setLoading(false);
}
};
fetchSections();
}, []); }, []);
if (loading) {
return (
<div className="loading-state">
<div className="spinner"></div>
<p>Discovering best plugins...</p>
</div>
);
}
return ( return (
<> <>
<SEO <SEO
@ -35,18 +75,41 @@ const Home = () => {
</section> </section>
<div className="container" id="browse"> <div className="container" id="browse">
{topPaid.length > 0 && (
<section className="home-section">
<h2 className="section-title"><span>Top</span> Premium</h2>
<div className="home-plugin-grid"> <div className="home-plugin-grid">
{plugins.map((plugin) => ( {topPaid.map((plugin) => (
<PluginCard key={plugin.id} plugin={plugin} /> <PluginCard key={`top-${plugin.id}`} plugin={plugin} />
))} ))}
</div>
</section>
)}
{plugins.length === 0 && ( <section className="home-section">
<h2 className="section-title"><span>Most</span> Popular</h2>
<div className="home-plugin-grid">
{popular.map((plugin) => (
<PluginCard key={`pop-${plugin.id}`} plugin={plugin} />
))}
</div>
</section>
<section className="home-section">
<h2 className="section-title"><span>New</span> & Trending</h2>
<div className="home-plugin-grid">
{newest.map((plugin) => (
<PluginCard key={`new-${plugin.id}`} plugin={plugin} />
))}
</div>
</section>
{topPaid.length === 0 && popular.length === 0 && newest.length === 0 && (
<div className="empty-state"> <div className="empty-state">
<p>No plugins found. Check back later!</p> <p>No plugins found. Check back later!</p>
</div> </div>
)} )}
</div> </div>
</div>
</> </>
); );
}; };

View File

@ -191,7 +191,7 @@ const LangPicker = ({ usedCodes, onAdd, onClose }: { usedCodes: string[]; onAdd:
// ── Main ────────────────────────────────────────────────────────────────────── // ── Main ──────────────────────────────────────────────────────────────────────
const AddPlugin = () => { const AddPlugin = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user, refreshUser } = useAuth();
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [done, setDone] = useState(false); const [done, setDone] = useState(false);
@ -362,6 +362,10 @@ const AddPlugin = () => {
if (wasmFile) fd.append('wasm', wasmFile); if (wasmFile) fd.append('wasm', wasmFile);
screenshots.forEach(f => fd.append('screenshots', f)); screenshots.forEach(f => fd.append('screenshots', f));
await api.post('/plugins', fd); await api.post('/plugins', fd);
// Refresh user to update plugin_count in JWT for Navbar
await refreshUser();
setDone(true); setDone(true);
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err);