setIsMenuOpen(false)}>{t('home')}
- {user && (user.is_developer || user.plugin_count > 0 || user.role === 'admin' || user.role === 'moderator') && (
+ {user && (user.plugin_count > 0 || user.role === 'admin' || user.role === 'moderator') && (
setIsMenuOpen(false)}>{t('dashboard')}
)}
{user &&
setIsMenuOpen(false)}>Publish}
@@ -330,15 +316,4 @@ const Footer = () => (
);
-const NotFoundPage = () => (
-
-
404
-
Page Not Found
-
- The page you're looking for doesn't exist or has been moved.
-
-
Go Home
-
-);
-
export default App;
\ No newline at end of file
diff --git a/src/context/CurrencyContext.tsx b/src/context/CurrencyContext.tsx
deleted file mode 100644
index fe61fbb..0000000
--- a/src/context/CurrencyContext.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-/**
- * CurrencyContext
- *
- * Provides `currency`, `setCurrency`, and `formatPrice(cents)` throughout the app.
- * The preferred currency is read from the JWT claims (via AuthContext) so it's available
- * immediately without an extra API call. When the user saves a new currency in settings,
- * we call `login()` with the refreshed token — the context automatically picks up the change.
- */
-
-import { createContext, useContext, useMemo } from 'react';
-import { useAuth } from '../../App';
-
-// ISO 4217 currency metadata used for the settings select and price formatting.
-export interface CurrencyMeta {
- code: string;
- label: string;
- symbol: string;
- locale: string;
-}
-
-export const SUPPORTED_CURRENCIES: CurrencyMeta[] = [
- { code: 'EUR', label: 'Euro', symbol: '€', locale: 'de-DE' },
- { code: 'USD', label: 'US Dollar', symbol: '$', locale: 'en-US' },
- { code: 'GBP', label: 'British Pound', symbol: '£', locale: 'en-GB' },
- { code: 'JPY', label: 'Japanese Yen', symbol: '¥', locale: 'ja-JP' },
- { code: 'CAD', label: 'Canadian Dollar', symbol: 'CA$',locale: 'en-CA' },
- { code: 'AUD', label: 'Australian Dollar', symbol: 'A$', locale: 'en-AU' },
- { code: 'CHF', label: 'Swiss Franc', symbol: 'Fr', locale: 'de-CH' },
- { code: 'CNY', label: 'Chinese Yuan', symbol: '¥', locale: 'zh-CN' },
- { code: 'SEK', label: 'Swedish Krona', symbol: 'kr', locale: 'sv-SE' },
- { code: 'NOK', label: 'Norwegian Krone', symbol: 'kr', locale: 'nb-NO' },
- { code: 'DKK', label: 'Danish Krone', symbol: 'kr', locale: 'da-DK' },
- { code: 'NZD', label: 'New Zealand Dollar', symbol: 'NZ$',locale: 'en-NZ' },
- { code: 'SGD', label: 'Singapore Dollar', symbol: 'S$', locale: 'en-SG' },
- { code: 'HKD', label: 'Hong Kong Dollar', symbol: 'HK$',locale: 'zh-HK' },
- { code: 'KRW', label: 'South Korean Won', symbol: '₩', locale: 'ko-KR' },
- { code: 'BRL', label: 'Brazilian Real', symbol: 'R$', locale: 'pt-BR' },
- { code: 'INR', label: 'Indian Rupee', symbol: '₹', locale: 'en-IN' },
- { code: 'MXN', label: 'Mexican Peso', symbol: 'MX$',locale: 'es-MX' },
- { code: 'PLN', label: 'Polish Złoty', symbol: 'zł', locale: 'pl-PL' },
- { code: 'CZK', label: 'Czech Koruna', symbol: 'Kč', locale: 'cs-CZ' },
- { code: 'HUF', label: 'Hungarian Forint', symbol: 'Ft', locale: 'hu-HU' },
- { code: 'RON', label: 'Romanian Leu', symbol: 'lei',locale: 'ro-RO' },
- { code: 'BGN', label: 'Bulgarian Lev', symbol: 'лв', locale: 'bg-BG' },
- { code: 'TRY', label: 'Turkish Lira', symbol: '₺', locale: 'tr-TR' },
- { code: 'ZAR', label: 'South African Rand', symbol: 'R', locale: 'en-ZA' },
- { code: 'ILS', label: 'Israeli Shekel', symbol: '₪', locale: 'he-IL' },
- { code: 'AED', label: 'UAE Dirham', symbol: 'د.إ',locale: 'ar-AE' },
- { code: 'SAR', label: 'Saudi Riyal', symbol: '﷼', locale: 'ar-SA' },
- { code: 'THB', label: 'Thai Baht', symbol: '฿', locale: 'th-TH' },
- { code: 'MYR', label: 'Malaysian Ringgit', symbol: 'RM', locale: 'ms-MY' },
- { code: 'IDR', label: 'Indonesian Rupiah', symbol: 'Rp', locale: 'id-ID' },
- { code: 'PHP', label: 'Philippine Peso', symbol: '₱', locale: 'fil-PH'},
- { code: 'VND', label: 'Vietnamese Dong', symbol: '₫', locale: 'vi-VN' },
- { code: 'NGN', label: 'Nigerian Naira', symbol: '₦', locale: 'en-NG' },
- { code: 'EGP', label: 'Egyptian Pound', symbol: '£', locale: 'ar-EG' },
- { code: 'UAH', label: 'Ukrainian Hryvnia', symbol: '₴', locale: 'uk-UA' },
-];
-
-/**
- * Approximate EUR → other currency exchange rates.
- * These are static approximations for display purposes only — actual charges are
- * always processed in EUR. A production app would fetch live rates from an API.
- */
-const EUR_RATES: Record
= {
- EUR: 1, USD: 1.08, GBP: 0.86, JPY: 163, CAD: 1.47,
- AUD: 1.65, CHF: 0.96, CNY: 7.82, SEK: 11.5, NOK: 11.4,
- DKK: 7.46, NZD: 1.79, SGD: 1.45, HKD: 8.44, KRW: 1455,
- BRL: 5.4, INR: 89.8, MXN: 18.5, PLN: 4.3, CZK: 25.3,
- HUF: 390, RON: 4.97, BGN: 1.96, TRY: 34.6, ZAR: 20.2,
- ILS: 4.01, AED: 3.97, SAR: 4.05, THB: 37.7, MYR: 5.05,
- IDR: 17200, PHP: 62.1, VND: 26900, NGN: 1780, EGP: 52.7,
- UAH: 44.7,
-};
-
-export interface CurrencyContextValue {
- currency: string;
- currencyMeta: CurrencyMeta;
- /** Format EUR cents into the user's preferred display currency */
- formatPrice: (eurCents: number) => string;
-}
-
-export const CurrencyContext = createContext(null);
-
-/** Hook to access the currency context. Falls back to EUR if used outside the provider. */
-export const useCurrency = (): CurrencyContextValue => {
- const ctx = useContext(CurrencyContext);
- if (ctx) return ctx;
- // Fallback so hooks in non-provider-wrapped code don't crash during init.
- return {
- currency: 'EUR',
- currencyMeta: SUPPORTED_CURRENCIES[0],
- formatPrice: (cents) =>
- new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(cents / 100),
- };
-};
-
-/** Drop this provider at the root of your app (inside AuthContext) */
-export const CurrencyProvider = ({ children }: { children: React.ReactNode }) => {
- const { user } = useAuth();
-
- const value = useMemo(() => {
- const code = (user as any)?.preferred_currency || 'EUR';
- const meta = SUPPORTED_CURRENCIES.find(c => c.code === code) ?? SUPPORTED_CURRENCIES[0];
- const rate = EUR_RATES[code] ?? 1;
-
- const formatPrice = (eurCents: number): string => {
- if (eurCents === 0) return 'Free';
- const amount = (eurCents / 100) * rate;
- try {
- return new Intl.NumberFormat(meta.locale, {
- style: 'currency',
- currency: meta.code,
- maximumFractionDigits: meta.code === 'JPY' || meta.code === 'KRW' || meta.code === 'IDR' || meta.code === 'VND' ? 0 : 2,
- }).format(amount);
- } catch {
- return `${meta.symbol}${amount.toFixed(2)}`;
- }
- };
-
- return { currency: code, currencyMeta: meta, formatPrice };
- }, [user]);
-
- return {children};
-};
diff --git a/src/pages/AuthorProfilePage.tsx b/src/pages/AuthorProfilePage.tsx
index e2ee06e..5bd0781 100644
--- a/src/pages/AuthorProfilePage.tsx
+++ b/src/pages/AuthorProfilePage.tsx
@@ -15,15 +15,16 @@ const AuthorProfilePage = () => {
useEffect(() => {
setLoading(true);
setError(null);
-
- // Fetch author info and plugins in parallel — saves one full round-trip.
- Promise.all([
- api.get(`/user/public/${username}`),
- api.get('/plugins', { params: { dev_name: username, sort: 'newest' } }),
- ])
- .then(([authorRes, pluginsRes]) => {
- setAuthor(authorRes.data);
- setPlugins(Array.isArray(pluginsRes.data) ? pluginsRes.data : []);
+
+ // Fetch author public info
+ api.get(`/user/public/${username}`)
+ .then(res => {
+ setAuthor(res.data);
+ // Fetch author plugins
+ return api.get('/plugins', { params: { dev_name: username, sort: 'newest' } });
+ })
+ .then(res => {
+ setPlugins(Array.isArray(res.data) ? res.data : []);
})
.catch(err => {
console.error('Fetch error:', err);
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 067a207..bf575dd 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -6,13 +6,10 @@ import SEO from '../components/SEO';
import PluginCard from '../components/PluginCard';
// Global cache to persist data across component unmounts (navigation)
-const PAGE_SIZE = 24;
-
let homeCache: {
popular: any[];
newest: any[];
all: any[];
- allHasMore: boolean;
} | null = null;
const Home = () => {
@@ -20,16 +17,13 @@ const Home = () => {
const [popular, setPopular] = useState(homeCache?.popular || []);
const [newest, setNewest] = useState(homeCache?.newest || []);
const [allPlugins, setAllPlugins] = useState(homeCache?.all || []);
- const [allHasMore, setAllHasMore] = useState(homeCache?.allHasMore ?? true);
- const [allOffset, setAllOffset] = useState(homeCache?.all.length || 0);
- const [loadingMore, setLoadingMore] = useState(false);
const [loading, setLoading] = useState(false);
const [heroIndex, setHeroIndex] = useState(0);
const [heroSubImageIndex, setHeroSubImageIndex] = useState(0);
const popularRef = useRef(null);
const newestRef = useRef(null);
- const fetched = useRef({ popular: !!homeCache?.popular.length, newest: !!homeCache?.newest.length, all: !!homeCache?.all.length });
+ const fetched = useRef({ popular: !!homeCache?.popular, newest: !!homeCache?.newest, all: !!homeCache?.all });
const saveSection = (key: 'popular' | 'newest' | 'all', data: any[]) => {
if (key === 'popular') setPopular(data);
@@ -52,23 +46,13 @@ const Home = () => {
setLoading(true);
try {
const [popRes, newRes, allRes] = await Promise.all([
- api.get('/plugins', { params: { sort: 'downloads', limit: 12 } }),
- api.get('/plugins', { params: { sort: 'newest', limit: 12 } }),
- api.get('/plugins', { params: { limit: PAGE_SIZE, offset: 0 } })
+ api.get('/plugins', { params: { sort: 'downloads' } }),
+ api.get('/plugins', { params: { sort: 'newest' } }),
+ api.get('/plugins')
]);
- const popData = Array.isArray(popRes.data) ? popRes.data : [];
- const newData = Array.isArray(newRes.data) ? newRes.data : [];
- const allData = Array.isArray(allRes.data) ? allRes.data : [];
- const hasMore = allData.length >= PAGE_SIZE;
-
- setPopular(popData);
- setNewest(newData);
- setAllPlugins(allData);
- setAllHasMore(hasMore);
- setAllOffset(allData.length);
-
- homeCache = { popular: popData, newest: newData, all: allData, allHasMore: hasMore };
- fetched.current = { popular: true, newest: true, all: true };
+ saveSection('popular', Array.isArray(popRes.data) ? popRes.data : []);
+ saveSection('newest', Array.isArray(newRes.data) ? newRes.data : []);
+ saveSection('all', Array.isArray(allRes.data) ? allRes.data : []);
} catch (err) {
console.error('Fetch error for home sections:', err);
} finally {
@@ -79,25 +63,6 @@ const Home = () => {
loadSections();
}, []);
- const loadMoreAll = async () => {
- if (loadingMore || !allHasMore) return;
- setLoadingMore(true);
- try {
- const res = await api.get('/plugins', { params: { limit: PAGE_SIZE, offset: allOffset } });
- const newData = Array.isArray(res.data) ? res.data : [];
- const merged = [...allPlugins, ...newData];
- const hasMore = newData.length >= PAGE_SIZE;
- setAllPlugins(merged);
- setAllHasMore(hasMore);
- setAllOffset(merged.length);
- if (homeCache) homeCache = { ...homeCache, all: merged, allHasMore: hasMore };
- } catch (err) {
- console.error('Load more error:', err);
- } finally {
- setLoadingMore(false);
- }
- };
-
// Featured plugins for top Steam-like carousel
const featuredPlugins = useMemo(() => popular.slice(0, 5), [popular]);
@@ -274,7 +239,7 @@ const Home = () => {
- All Plugins ({allPlugins.length}{allHasMore ? '+' : ''})
+ All Plugins ({allPlugins.length})
{allPlugins.length > 0 ? (
allPlugins.map((plugin) => (
@@ -284,18 +249,6 @@ const Home = () => {
{loading ? 'Loading all plugins...' : 'No plugins available'}
)}
- {allHasMore && (
-
-
-
- )}
{(fetched.current?.popular && fetched.current?.newest && popular.length === 0 && newest.length === 0 && allPlugins.length === 0) && (
diff --git a/src/pages/PluginDetail.tsx b/src/pages/PluginDetail.tsx
index b16454c..3d2ebd4 100644
--- a/src/pages/PluginDetail.tsx
+++ b/src/pages/PluginDetail.tsx
@@ -187,7 +187,7 @@ const PluginDetail = () => {
setCouponMessage(null);
};
- // Ownership — resolved from plugin.owned (injected by server) so no extra HTTP call needed
+ // Ownership
const [ownsPlugin, setOwnsPlugin] = useState(false);
const [ownershipChecked, setOwnershipChecked] = useState(false);
@@ -201,7 +201,6 @@ const PluginDetail = () => {
const [reportSubmitting, setReportSubmitting] = useState(false);
const [reportDone, setReportDone] = useState(false);
const [changelogOpen, setChangelogOpen] = useState(false);
- const [moreByDev, setMoreByDev] = useState([]);
// Review Report State
const [reviewReportOpen, setReviewReportOpen] = useState(null);
@@ -234,41 +233,19 @@ const PluginDetail = () => {
} else if (res.data.screenshots?.length > 0) {
setMainScreenshot(res.data.screenshots[0].path);
}
- // Resolve ownership from the plugin response — server includes it when a JWT is sent.
- if (res.data.owned !== undefined && res.data.owned !== null) {
- setOwnsPlugin(res.data.owned);
- setOwnershipChecked(true);
- }
- // Save to recently viewed
- try {
- const key = 'recentlyViewed';
- const existing: any[] = JSON.parse(localStorage.getItem(key) || '[]');
- const filtered = existing.filter((p: any) => String(p.id) !== String(res.data.id));
- const entry = { id: res.data.id, name: res.data.name, preview_path: res.data.preview_path, dev_name: res.data.dev_name };
- localStorage.setItem(key, JSON.stringify([entry, ...filtered].slice(0, 10)));
- } catch { /* ignore */ }
});
};
useEffect(() => { fetchPlugin(); }, [id]);
- // ── Fetch more plugins by same developer ─────────────────────────────────
+ // ── Check ownership ──────────────────────────────────────────────────────
useEffect(() => {
- if (!plugin?.dev_name || !plugin?.id) return;
- api.get('/plugins', { params: { dev_name: plugin.dev_name, limit: 4 } })
- .then(res => {
- const data = Array.isArray(res.data) ? res.data : [];
- setMoreByDev(data.filter((p: any) => String(p.id) !== String(plugin.id)).slice(0, 3));
- })
- .catch(() => {});
- }, [plugin?.dev_name, plugin?.id]);
-
- // ── Ownership is now provided by the plugin detail response.
- // Fallback: if the server didn't include it (unauthenticated request that later
- // becomes authenticated via token refresh), mark it checked immediately.
- useEffect(() => {
- if (!user) { setOwnershipChecked(true); }
- }, [user]);
+ if (!user || !id) { setOwnershipChecked(true); return; }
+ api.get(`/plugins/${id}/owned`)
+ .then(res => setOwnsPlugin(res.data.owned === true))
+ .catch(() => setOwnsPlugin(false))
+ .finally(() => setOwnershipChecked(true));
+ }, [user, id]);
// ── Translations ─────────────────────────────────────────────────────────
useEffect(() => {
@@ -1127,32 +1104,6 @@ const PluginDetail = () => {
)}