Skip to content

Commit 19a4223

Browse files
Review AI suggestions for PRs #443 and #444 (#446)
* fix: security improvements, bug fixes, and i18n consistency Security improvements: - Add URL validation to prevent open redirect vulnerabilities in checkout routes - Use generic error messages for 500 responses to avoid exposing sensitive details - Add JSON parsing validation with proper 400 responses for malformed requests - Validate item existence before recording views to prevent data pollution - Add customer ID validation for Polar checkout (consistent with Stripe) Bug fixes: - Fix pagination issues (restore standard pagination UI, fix page reset on search) - Fix webhook handlers to properly detect sponsor ad renewals (check subscription metadata) - Fix admin stats to show global counts instead of page-scoped counts - Fix modal closing logic to check operation success before closing - Fix collection card spinner to only show on regular left-click navigation - Fix item view recording to validate item existence for all users - Fix limit parameter validation to handle NaN and invalid values properly - Fix isActive consistency in admin collections page i18n improvements: - Replace hardcoded strings with translation keys in collection components - Add missing translations for aria-labels and status messages Code quality: - Remove unused props and imports - Improve error handling consistency across API routes - Add proper existence checks before database operations - Fix cache revalidation to use correct identifiers (slug vs id) Affected areas: - API routes (sponsor ads, collections, items, payments, webhooks) - Admin pages (collections, sponsorships) - UI components (collections, categories, billing) - Webhook handlers (Stripe, Polar) * fix: improve error handling and internationalization - Add missing 404 handling in cancel sponsor ad route - Complete internationalization of assign items modal - Refactor categories-grid to use exported totalPages function * fix: resolve API errors and missing translation - Increase max limit for collections endpoint (100 -> 1000) - Add missing 'EDIT' translation key in common section - Improve pagination parameter validation for collections
1 parent 3f4d9d8 commit 19a4223

File tree

25 files changed

+1474
-1321
lines changed

25 files changed

+1474
-1321
lines changed

app/[locale]/admin/collections/page.tsx

Lines changed: 338 additions & 279 deletions
Large diffs are not rendered by default.

app/[locale]/admin/sponsorships/page.tsx

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default function AdminSponsorshipsPage() {
5454
deleteSponsorAd,
5555
setStatusFilter,
5656
setSearchTerm: setHookSearchTerm,
57-
setCurrentPage,
57+
setCurrentPage
5858
} = useAdminSponsorAds();
5959

6060
// Calculate active filters
@@ -63,20 +63,21 @@ export default function AdminSponsorshipsPage() {
6363
// Sync debounced search term to hook (only when debounced value changes)
6464
useEffect(() => {
6565
setHookSearchTerm(debouncedSearchTerm);
66-
if (debouncedSearchTerm !== '') {
67-
setCurrentPage(1); // Reset to page 1 when search changes
68-
}
66+
setCurrentPage(1); // Reset to page 1 when search changes (including when cleared)
6967
}, [debouncedSearchTerm, setHookSearchTerm, setCurrentPage]);
7068

7169
// Handlers
7270
const handleSearchChange = useCallback((value: string) => {
7371
setSearchTerm(value);
7472
}, []);
7573

76-
const handleStatusChange = useCallback((value: SponsorAdStatus | undefined) => {
77-
setLocalStatusFilter(value);
78-
setStatusFilter(value);
79-
}, [setStatusFilter]);
74+
const handleStatusChange = useCallback(
75+
(value: SponsorAdStatus | undefined) => {
76+
setLocalStatusFilter(value);
77+
setStatusFilter(value);
78+
},
79+
[setStatusFilter]
80+
);
8081

8182
const handleClearFilters = useCallback(() => {
8283
setSearchTerm('');
@@ -86,20 +87,25 @@ export default function AdminSponsorshipsPage() {
8687
setCurrentPage(1);
8788
}, [setHookSearchTerm, setStatusFilter, setCurrentPage]);
8889

89-
const handleApprove = useCallback(async (id: string) => {
90-
const result = await approveSponsorAd(id);
91-
if (result.requiresForceApprove) {
92-
// Show confirmation modal for force approve
93-
setPendingApproveId(id);
94-
setForceApproveModalOpen(true);
95-
}
96-
}, [approveSponsorAd]);
90+
const handleApprove = useCallback(
91+
async (id: string) => {
92+
const result = await approveSponsorAd(id);
93+
if (result.requiresForceApprove) {
94+
// Show confirmation modal for force approve
95+
setPendingApproveId(id);
96+
setForceApproveModalOpen(true);
97+
}
98+
},
99+
[approveSponsorAd]
100+
);
97101

98102
const handleForceApprove = useCallback(async () => {
99103
if (!pendingApproveId) return;
100-
await approveSponsorAd(pendingApproveId, true);
101-
setForceApproveModalOpen(false);
102-
setPendingApproveId(null);
104+
const result = await approveSponsorAd(pendingApproveId, true);
105+
if (result.success) {
106+
setForceApproveModalOpen(false);
107+
setPendingApproveId(null);
108+
}
103109
}, [pendingApproveId, approveSponsorAd]);
104110

105111
const handleCloseForceApproveModal = useCallback(() => {
@@ -127,23 +133,32 @@ export default function AdminSponsorshipsPage() {
127133
}
128134
}, [selectedSponsorAd, rejectionReason, rejectSponsorAd, handleCloseRejectModal]);
129135

130-
const handleCancel = useCallback(async (id: string) => {
131-
if (!confirm(t('CONFIRM_CANCEL'))) return;
132-
await cancelSponsorAd(id);
133-
}, [t, cancelSponsorAd]);
136+
const handleCancel = useCallback(
137+
async (id: string) => {
138+
if (!confirm(t('CONFIRM_CANCEL'))) return;
139+
await cancelSponsorAd(id);
140+
},
141+
[t, cancelSponsorAd]
142+
);
134143

135-
const handleDelete = useCallback(async (id: string) => {
136-
if (confirmDeleteId !== id) {
137-
setConfirmDeleteId(id);
138-
return;
139-
}
140-
await deleteSponsorAd(id);
141-
setConfirmDeleteId(null);
142-
}, [confirmDeleteId, deleteSponsorAd]);
144+
const handleDelete = useCallback(
145+
async (id: string) => {
146+
if (confirmDeleteId !== id) {
147+
setConfirmDeleteId(id);
148+
return;
149+
}
150+
await deleteSponsorAd(id);
151+
setConfirmDeleteId(null);
152+
},
153+
[confirmDeleteId, deleteSponsorAd]
154+
);
143155

144-
const handlePageChange = useCallback((page: number) => {
145-
setCurrentPage(page);
146-
}, [setCurrentPage]);
156+
const handlePageChange = useCallback(
157+
(page: number) => {
158+
setCurrentPage(page);
159+
},
160+
[setCurrentPage]
161+
);
147162

148163
// Loading state
149164
if (isLoading && sponsorAds.length === 0) {
@@ -185,11 +200,7 @@ export default function AdminSponsorshipsPage() {
185200
{/* Pagination */}
186201
{totalPages > 1 && (
187202
<div className="flex flex-col items-center mt-8 space-y-4">
188-
<UniversalPagination
189-
page={currentPage}
190-
totalPages={totalPages}
191-
onPageChange={handlePageChange}
192-
/>
203+
<UniversalPagination page={currentPage} totalPages={totalPages} onPageChange={handlePageChange} />
193204
</div>
194205
)}
195206

@@ -205,33 +216,20 @@ export default function AdminSponsorshipsPage() {
205216
/>
206217

207218
{/* Force Approve Modal */}
208-
<Modal
209-
isOpen={forceApproveModalOpen}
210-
onClose={handleCloseForceApproveModal}
211-
size="md"
212-
>
219+
<Modal isOpen={forceApproveModalOpen} onClose={handleCloseForceApproveModal} size="md">
213220
<ModalContent>
214221
<ModalHeader className="flex items-center gap-2">
215222
<AlertTriangle className="w-5 h-5 text-amber-500" />
216223
{t('FORCE_APPROVE_TITLE')}
217224
</ModalHeader>
218225
<ModalBody>
219-
<p className="text-gray-600 dark:text-gray-400">
220-
{t('FORCE_APPROVE_MESSAGE')}
221-
</p>
226+
<p className="text-gray-600 dark:text-gray-400">{t('FORCE_APPROVE_MESSAGE')}</p>
222227
</ModalBody>
223228
<ModalFooter>
224-
<Button
225-
variant="light"
226-
onPress={handleCloseForceApproveModal}
227-
>
229+
<Button variant="light" onPress={handleCloseForceApproveModal}>
228230
{t('CANCEL')}
229231
</Button>
230-
<Button
231-
color="warning"
232-
onPress={handleForceApprove}
233-
isLoading={isSubmitting}
234-
>
232+
<Button color="warning" onPress={handleForceApprove} isLoading={isSubmitting}>
235233
{t('FORCE_APPROVE')}
236234
</Button>
237235
</ModalFooter>

app/[locale]/categories/listing-categories.tsx

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,15 @@ import { useTranslations } from 'next-intl';
44
import Hero from '@/components/hero';
55
import Link from 'next/link';
66
import CategoriesGrid from '@/components/categories-grid';
7-
import { Category, ItemData, Tag } from '@/lib/content';
7+
import { Category } from '@/lib/content';
88
import { useLayoutTheme, LayoutHome } from '@/components/context';
9-
import { Paginate } from '@/components/filters/components/pagination/paginate';
10-
import { totalPages } from '@/lib/paginate';
119
import { ListingSkeleton } from '@/components/ui/skeleton';
1210
import { Container } from '@/components/ui/container';
1311

1412
interface ListingCategoriesProps {
15-
total: number;
16-
start: number;
1713
page: number;
1814
basePath: string;
1915
categories: Category[];
20-
tags: Tag[];
21-
items: ItemData[];
2216
}
2317

2418
function ListingCategoriesContent(props: ListingCategoriesProps) {
@@ -41,15 +35,6 @@ function ListingCategoriesContent(props: ListingCategoriesProps) {
4135
>
4236
{layoutHome === LayoutHome.HOME_ONE && <HomeOneLayout categories={props.categories} />}
4337
{layoutHome === LayoutHome.HOME_TWO && <HomeTwoLayout categories={props.categories} />}
44-
{paginationType === 'standard' && totalPages(props.categories.length) > 1 && (
45-
<footer className="flex items-center justify-center">
46-
<Paginate
47-
basePath={props.basePath}
48-
initialPage={props.page}
49-
total={totalPages(props.categories.length)}
50-
/>
51-
</footer>
52-
)}
5338
</Hero>
5439
</>
5540
);
@@ -123,11 +108,7 @@ function HomeOneLayout({ categories }: { categories: Category[] }) {
123108
);
124109
}
125110

126-
function HomeTwoLayout({
127-
categories
128-
}: {
129-
categories: Category[];
130-
}) {
111+
function HomeTwoLayout({ categories }: { categories: Category[] }) {
131112
const t = useTranslations();
132113

133114
return (

app/[locale]/categories/page.tsx

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,31 @@
1-
import { getCachedItems } from "@/lib/content";
2-
import ListingCategories from "./listing-categories";
3-
import { notFound } from "next/navigation";
4-
import { getCategoriesEnabled } from "@/lib/utils/settings";
1+
import { getCachedItems } from '@/lib/content';
2+
import ListingCategories from './listing-categories';
3+
import { notFound } from 'next/navigation';
4+
import { getCategoriesEnabled } from '@/lib/utils/settings';
55

66
export const revalidate = 10;
77

88
// Allow non-English locales to be generated on-demand (ISR)
99
export const dynamicParams = true;
1010

1111
export async function generateStaticParams() {
12-
// Only pre-build English locale for optimal build size
13-
return [{ locale: 'en' }];
12+
// Only pre-build English locale for optimal build size
13+
return [{ locale: 'en' }];
1414
}
1515

16-
export default async function CategoriesPage({
17-
params,
18-
}: {
19-
params: Promise<{ locale: string }>;
20-
}) {
21-
// Check if categories are enabled
22-
const categoriesEnabled = getCategoriesEnabled();
23-
if (!categoriesEnabled) {
24-
notFound();
25-
}
16+
export default async function CategoriesPage({ params }: { params: Promise<{ locale: string }> }) {
17+
// Check if categories are enabled
18+
const categoriesEnabled = getCategoriesEnabled();
19+
if (!categoriesEnabled) {
20+
notFound();
21+
}
2622

27-
const { locale } = await params;
28-
const { categories, tags, items } = await getCachedItems({ lang: locale });
23+
const { locale } = await params;
24+
const { categories } = await getCachedItems({ lang: locale });
2925

30-
// Calculate pagination info
31-
const total = items.length;
32-
const page = 1;
33-
const start = 0;
34-
const basePath = "/categories";
26+
// Calculate pagination info
27+
const page = 1;
28+
const basePath = '/categories';
3529

36-
return (
37-
<ListingCategories
38-
categories={categories}
39-
tags={tags}
40-
items={items}
41-
total={total}
42-
start={start}
43-
page={page}
44-
basePath={basePath}
45-
/>
46-
);
30+
return <ListingCategories categories={categories} page={page} basePath={basePath} />;
4731
}

0 commit comments

Comments
 (0)