sync: Redis caching for browse/skill API, progress bar on search, default sort by stars
- Add Redis caching to browse search results and skill detail API - Show progress bar on homepage search - Default sort to stars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { BrowseFilters, SearchBar, Pagination, ActiveFilters, EmptyState } from
|
||||
import { SkillCard } from '@/components/SkillCard';
|
||||
import { toPersianNumber } from '@/lib/format-number';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL, hashSearchParams } from '@/lib/cache';
|
||||
|
||||
|
||||
// Force dynamic rendering to fetch fresh data from database
|
||||
@@ -25,6 +25,7 @@ interface BrowsePageProps {
|
||||
}
|
||||
|
||||
// Get skills directly from database with filters - all filtering at database level
|
||||
// Results are cached in Redis for 30 minutes, keyed by a hash of all filter parameters
|
||||
async function getSkills(params: {
|
||||
q?: string;
|
||||
platform?: string;
|
||||
@@ -34,48 +35,62 @@ async function getSkills(params: {
|
||||
category?: string;
|
||||
}) {
|
||||
try {
|
||||
const db = createDb();
|
||||
const limit = 20;
|
||||
const page = parseInt(params.page || '1');
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const sortMap: Record<string, 'stars' | 'downloads' | 'rating' | 'updated' | 'lastDownloaded'> = {
|
||||
'stars': 'stars',
|
||||
'downloads': 'downloads',
|
||||
'recent': 'updated',
|
||||
'rating': 'rating',
|
||||
'lastDownloaded': 'lastDownloaded',
|
||||
};
|
||||
|
||||
// Build filter options - push ALL filters to database level
|
||||
const filterOptions = {
|
||||
query: params.q,
|
||||
const hash = hashSearchParams({
|
||||
q: params.q,
|
||||
category: params.category,
|
||||
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
|
||||
sourceFormat: params.format || 'skill.md',
|
||||
sortBy: sortMap[params.sort || 'lastDownloaded'] || 'lastDownloaded',
|
||||
sortOrder: 'desc' as const,
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
|
||||
// Fetch paginated results directly from database
|
||||
const skills = await skillQueries.search(db, filterOptions);
|
||||
|
||||
// Get accurate total count for pagination
|
||||
const total = await skillQueries.count(db, {
|
||||
query: params.q,
|
||||
category: params.category,
|
||||
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
|
||||
sourceFormat: params.format || 'skill.md',
|
||||
format: params.format || 'skill.md',
|
||||
sort: params.sort || 'lastDownloaded',
|
||||
page,
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
return await getOrSetCache(
|
||||
cacheKeys.searchSkills(hash),
|
||||
cacheTTL.search,
|
||||
async () => {
|
||||
const db = createDb();
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
return {
|
||||
skills,
|
||||
pagination: { total, page, totalPages },
|
||||
};
|
||||
const sortMap: Record<string, 'stars' | 'downloads' | 'rating' | 'updated' | 'lastDownloaded'> = {
|
||||
'stars': 'stars',
|
||||
'downloads': 'downloads',
|
||||
'recent': 'updated',
|
||||
'rating': 'rating',
|
||||
'lastDownloaded': 'lastDownloaded',
|
||||
};
|
||||
|
||||
const filterOptions = {
|
||||
query: params.q,
|
||||
category: params.category,
|
||||
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
|
||||
sourceFormat: params.format || 'skill.md',
|
||||
sortBy: sortMap[params.sort || 'lastDownloaded'] || 'lastDownloaded',
|
||||
sortOrder: 'desc' as const,
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
|
||||
const [skills, total] = await Promise.all([
|
||||
skillQueries.search(db, filterOptions),
|
||||
skillQueries.count(db, {
|
||||
query: params.q,
|
||||
category: params.category,
|
||||
platform: params.platform && params.platform !== 'all' ? params.platform : undefined,
|
||||
sourceFormat: params.format || 'skill.md',
|
||||
}),
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
skills,
|
||||
pagination: { total, page, totalPages },
|
||||
};
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching skills:', error);
|
||||
return { skills: [], pagination: { total: 0, page: 1, totalPages: 1 } };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
import { createDb, skillQueries } from '@skillhub/db';
|
||||
import { shouldCountView } from '@/lib/cache';
|
||||
import { shouldCountView, getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
|
||||
// Create database connection
|
||||
const db = createDb();
|
||||
@@ -35,8 +35,12 @@ export async function GET(
|
||||
const { id } = await params;
|
||||
const skillId = id.join('/');
|
||||
|
||||
// Get skill from database
|
||||
const skill = await skillQueries.getById(db, skillId);
|
||||
// Get skill from database (cached 1h)
|
||||
const skill = await getOrSetCache(
|
||||
cacheKeys.skill(skillId),
|
||||
cacheTTL.skill,
|
||||
() => skillQueries.getById(db, skillId)
|
||||
);
|
||||
|
||||
if (!skill) {
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
import { createDb, skillQueries } from '@skillhub/db';
|
||||
import { invalidateCache, cacheKeys, shouldCountDownload } from '@/lib/cache';
|
||||
import { invalidateCache, invalidateCachePattern, cacheKeys, shouldCountDownload } from '@/lib/cache';
|
||||
|
||||
// Create database connection
|
||||
const db = createDb();
|
||||
@@ -69,12 +69,13 @@ export async function POST(request: NextRequest) {
|
||||
await skillQueries.incrementDownloads(db, skillId);
|
||||
}
|
||||
|
||||
// Invalidate relevant caches so featured/recent lists reflect the new download
|
||||
// Invalidate relevant caches so featured/recent/browse lists reflect the new download
|
||||
await Promise.all([
|
||||
invalidateCache(cacheKeys.featuredSkills()),
|
||||
invalidateCache(cacheKeys.recentSkills()),
|
||||
invalidateCache(cacheKeys.stats()),
|
||||
invalidateCache(cacheKeys.skill(skillId)),
|
||||
invalidateCachePattern('skills:search:*'),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -272,6 +272,10 @@ export function SearchBar({ placeholder, defaultValue = '' }: SearchBarProps) {
|
||||
|
||||
if (searchQuery) {
|
||||
params.set('q', searchQuery);
|
||||
// Default to sorting by stars when searching
|
||||
if (!params.get('sort')) {
|
||||
params.set('sort', 'stars');
|
||||
}
|
||||
} else {
|
||||
params.delete('q');
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ export function HeroSearch({ placeholder, locale }: HeroSearchProps) {
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
window.dispatchEvent(new Event('progressbar:start'));
|
||||
if (query.trim()) {
|
||||
router.push(`/${locale}/browse?q=${encodeURIComponent(query.trim())}`);
|
||||
router.push(`/${locale}/browse?q=${encodeURIComponent(query.trim())}&sort=stars`);
|
||||
} else {
|
||||
router.push(`/${locale}/browse`);
|
||||
}
|
||||
|
||||
@@ -121,12 +121,19 @@ export function ProgressBar() {
|
||||
const handlePopState = () => onUrlChange();
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
|
||||
// 4. Custom event for programmatic navigation (e.g. form submits with router.push)
|
||||
const handleManualStart = () => {
|
||||
if (!isRunningRef.current) start();
|
||||
};
|
||||
window.addEventListener('progressbar:start', handleManualStart);
|
||||
|
||||
return () => {
|
||||
clearTimers();
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
history.pushState = origPushState;
|
||||
history.replaceState = origReplaceState;
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
window.removeEventListener('progressbar:start', handleManualStart);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user