Skip to content

Commit 2896481

Browse files
committed
checkpoint
1 parent 5815ff6 commit 2896481

File tree

8 files changed

+97
-62
lines changed

8 files changed

+97
-62
lines changed

src/components/ShowcaseGallery.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function ShowcaseGallery() {
2828
pageSize: 24,
2929
},
3030
filters: {
31-
libraryId: search.libraryId,
31+
libraryIds: search.libraryIds,
3232
useCases: search.useCases as ShowcaseUseCase[],
3333
q: search.q,
3434
},
@@ -67,7 +67,7 @@ export function ShowcaseGallery() {
6767
getApprovedShowcasesQueryOptions({
6868
pagination: { page: search.page, pageSize: 24 },
6969
filters: {
70-
libraryId: search.libraryId,
70+
libraryIds: search.libraryIds,
7171
useCases: search.useCases as ShowcaseUseCase[],
7272
q: search.q,
7373
},
@@ -111,7 +111,7 @@ export function ShowcaseGallery() {
111111
getApprovedShowcasesQueryOptions({
112112
pagination: { page: search.page, pageSize: 24 },
113113
filters: {
114-
libraryId: search.libraryId,
114+
libraryIds: search.libraryIds,
115115
useCases: search.useCases as ShowcaseUseCase[],
116116
q: search.q,
117117
},
@@ -152,7 +152,7 @@ export function ShowcaseGallery() {
152152
getApprovedShowcasesQueryOptions({
153153
pagination: { page: search.page, pageSize: 24 },
154154
filters: {
155-
libraryId: search.libraryId,
155+
libraryIds: search.libraryIds,
156156
useCases: search.useCases as ShowcaseUseCase[],
157157
q: search.q,
158158
},
@@ -182,11 +182,25 @@ export function ShowcaseGallery() {
182182
voteMutation.mutate({ showcaseId, value })
183183
}
184184

185-
const handleLibraryFilter = (libraryId: string | undefined) => {
185+
const handleLibraryToggle = (libraryId: string) => {
186+
const current = search.libraryIds || []
187+
const updated = current.includes(libraryId)
188+
? current.filter((id: string) => id !== libraryId)
189+
: [...current, libraryId]
186190
navigate({
187191
search: (prev: typeof search) => ({
188192
...prev,
189-
libraryId,
193+
libraryIds: updated.length > 0 ? updated : undefined,
194+
page: 1,
195+
}),
196+
})
197+
}
198+
199+
const clearLibraries = () => {
200+
navigate({
201+
search: (prev: typeof search) => ({
202+
...prev,
203+
libraryIds: undefined,
190204
page: 1,
191205
}),
192206
})
@@ -236,15 +250,15 @@ export function ShowcaseGallery() {
236250
navigate({
237251
search: {
238252
page: 1,
239-
libraryId: undefined,
253+
libraryIds: undefined,
240254
useCases: undefined,
241255
q: undefined,
242256
},
243257
})
244258
}
245259

246260
const hasFilters =
247-
search.libraryId ||
261+
(search.libraryIds && search.libraryIds.length > 0) ||
248262
(search.useCases && search.useCases.length > 0) ||
249263
search.q
250264

@@ -279,11 +293,12 @@ export function ShowcaseGallery() {
279293
<div className="max-w-7xl mx-auto px-4 py-3">
280294
<ShowcaseTopBarFilters
281295
filters={{
282-
libraryId: search.libraryId,
296+
libraryIds: search.libraryIds,
283297
useCases: search.useCases as ShowcaseUseCase[],
284298
q: search.q,
285299
}}
286-
onLibraryChange={handleLibraryFilter}
300+
onLibraryToggle={handleLibraryToggle}
301+
onClearLibraries={clearLibraries}
287302
onUseCaseToggle={handleUseCaseFilter}
288303
onClearUseCases={clearUseCases}
289304
onClearFilters={clearFilters}

src/components/ShowcaseTopBarFilters.tsx

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { SHOWCASE_USE_CASES, type ShowcaseUseCase } from '~/db/types'
88
import { USE_CASE_LABELS } from '~/utils/showcase.client'
99

1010
interface ShowcaseFilters {
11-
libraryId?: string
11+
libraryIds?: string[]
1212
useCases?: ShowcaseUseCase[]
1313
q?: string
1414
}
1515

1616
interface ShowcaseTopBarFiltersProps {
1717
filters: ShowcaseFilters
18-
onLibraryChange: (libraryId: string | undefined) => void
18+
onLibraryToggle: (libraryId: string) => void
19+
onClearLibraries: () => void
1920
onUseCaseToggle: (useCase: ShowcaseUseCase) => void
2021
onClearUseCases: () => void
2122
onClearFilters: () => void
@@ -32,21 +33,18 @@ const LIBRARY_OPTIONS = libraries
3233

3334
export function ShowcaseTopBarFilters({
3435
filters,
35-
onLibraryChange,
36+
onLibraryToggle,
37+
onClearLibraries,
3638
onUseCaseToggle,
3739
onClearUseCases,
3840
onClearFilters,
3941
onSearchChange,
4042
}: ShowcaseTopBarFiltersProps) {
4143
const hasActiveFilters =
42-
!!filters.libraryId ||
44+
(filters.libraryIds && filters.libraryIds.length > 0) ||
4345
(filters.useCases && filters.useCases.length > 0) ||
4446
!!filters.q
4547

46-
const selectedLibrary = LIBRARY_OPTIONS.find(
47-
(lib) => lib.id === filters.libraryId,
48-
)
49-
5048
return (
5149
<TopBarFilter
5250
onClearAll={onClearFilters}
@@ -59,23 +57,18 @@ export function ShowcaseTopBarFilters({
5957
>
6058
{/* Library Facet */}
6159
<FacetFilterButton
62-
label={
63-
selectedLibrary ? `Library: ${selectedLibrary.label}` : 'Library'
64-
}
65-
hasSelection={!!filters.libraryId}
66-
onClear={() => onLibraryChange(undefined)}
60+
label="Library"
61+
hasSelection={filters.libraryIds && filters.libraryIds.length > 0}
62+
selectionCount={filters.libraryIds?.length}
63+
onClear={onClearLibraries}
6764
>
6865
<div className="space-y-0.5">
6966
{LIBRARY_OPTIONS.map((lib) => (
7067
<FilterCheckbox
7168
key={lib.id}
7269
label={lib.label}
73-
checked={filters.libraryId === lib.id}
74-
onChange={() =>
75-
onLibraryChange(
76-
filters.libraryId === lib.id ? undefined : lib.id,
77-
)
78-
}
70+
checked={filters.libraryIds?.includes(lib.id) ?? false}
71+
onChange={() => onLibraryToggle(lib.id)}
7972
/>
8073
))}
8174
</div>

src/queries/showcases.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const getMyShowcasesQueryOptions = (params: {
3535
export const getApprovedShowcasesQueryOptions = (params: {
3636
pagination: ShowcasePagination
3737
filters?: {
38-
libraryId?: string
38+
libraryIds?: string[]
3939
useCases?: ShowcaseUseCase[]
4040
featured?: boolean
4141
q?: string

src/routes/admin/npm-stats.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,14 +273,20 @@ function NpmStatsAdmin() {
273273
getCoreRowModel: getCoreRowModel(),
274274
})
275275

276+
const packages = useMemo(
277+
() =>
278+
[...(packagesData?.packages ?? [])].sort(
279+
(a, b) => (b.downloads ?? 0) - (a.downloads ?? 0),
280+
),
281+
[packagesData?.packages],
282+
)
283+
276284
const packagesTable = useReactTable({
277-
data: packagesData?.packages ?? [],
285+
data: packages,
278286
columns: packageColumns,
279287
getCoreRowModel: getCoreRowModel(),
280288
})
281289

282-
const packages = packagesData?.packages ?? []
283-
284290
return (
285291
<div className="w-full p-4">
286292
<div className="max-w-7xl mx-auto">

src/routes/showcase/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import * as v from 'valibot'
33
import { seo } from '~/utils/seo'
44
import { ShowcaseGallery } from '~/components/ShowcaseGallery'
55
import { getApprovedShowcasesQueryOptions } from '~/queries/showcases'
6-
import { showcaseUseCaseSchema } from '~/utils/schemas'
6+
import { libraryIdSchema, showcaseUseCaseSchema } from '~/utils/schemas'
77

88
export const Route = createFileRoute('/showcase/')({
99
validateSearch: (search) => {
1010
const parsed = v.parse(
1111
v.object({
1212
page: v.optional(v.number(), 1),
13-
libraryId: v.optional(v.string()),
13+
libraryIds: v.optional(v.array(libraryIdSchema)),
1414
useCases: v.optional(v.array(showcaseUseCaseSchema)),
1515
q: v.optional(v.string()),
1616
}),
@@ -21,7 +21,7 @@ export const Route = createFileRoute('/showcase/')({
2121
},
2222
loaderDeps: ({ search }) => ({
2323
page: search.page,
24-
libraryId: search.libraryId,
24+
libraryIds: search.libraryIds,
2525
useCases: search.useCases,
2626
q: search.q,
2727
}),
@@ -33,8 +33,8 @@ export const Route = createFileRoute('/showcase/')({
3333
pageSize: 24,
3434
},
3535
filters: {
36-
libraryId: deps.libraryId,
37-
useCases: deps.useCases as any,
36+
libraryIds: deps.libraryIds,
37+
useCases: deps.useCases,
3838
q: deps.q,
3939
},
4040
}),

src/utils/auth.server-helpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import { getRequest } from '@tanstack/react-start/server'
1111
import { getAuthService, getSessionService } from '~/auth/index.server'
12+
import { recordDailyActivity } from './activity.server'
1213

1314
/**
1415
* Get current user from request
@@ -30,6 +31,11 @@ export async function getAuthenticatedUser() {
3031
throw new Error('Not authenticated')
3132
}
3233

34+
// Record daily activity for streak tracking (fire and forget)
35+
recordDailyActivity(user.userId).catch(() => {
36+
// Silently ignore errors to not break auth flow
37+
})
38+
3339
return user
3440
}
3541

src/utils/showcase.functions.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export const getApprovedShowcases = createServerFn({ method: 'POST' })
383383
}),
384384
filters: v.optional(
385385
v.object({
386-
libraryId: v.optional(v.string()),
386+
libraryIds: v.optional(v.array(v.string())),
387387
useCases: v.optional(v.array(showcaseUseCaseSchema)),
388388
featured: v.optional(v.boolean()),
389389
q: v.optional(v.string()),
@@ -398,8 +398,15 @@ export const getApprovedShowcases = createServerFn({ method: 'POST' })
398398
// Build where conditions
399399
const conditions = [eq(showcases.status, 'approved' as ShowcaseStatus)]
400400

401-
if (filters.libraryId) {
402-
conditions.push(arrayContains(showcases.libraries, [filters.libraryId]))
401+
if (filters.libraryIds && filters.libraryIds.length > 0) {
402+
// Match showcases that have ANY of the selected libraries
403+
conditions.push(
404+
or(
405+
...filters.libraryIds.map((libId) =>
406+
arrayContains(showcases.libraries, [libId]),
407+
),
408+
)!,
409+
)
403410
}
404411

405412
if (filters.useCases && filters.useCases.length > 0) {

src/utils/stats.functions.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -341,42 +341,50 @@ async function fetchNpmPackageCreationDate(
341341
}
342342

343343
/**
344-
* Normalize date ranges into consistent chunk boundaries
345-
* Uses fixed 500-day chunks aligned to calendar dates for cache consistency
344+
* Normalize date ranges into consistent year-based chunk boundaries
345+
* Uses calendar years for stable cache keys that don't shift daily
346346
*
347347
* This ensures the same date ranges are used across all fetches, preventing
348-
* duplicate cache entries with different keys (e.g., 2025-12-06 vs 2025-12-07)
348+
* duplicate cache entries. Historical years are immutable (Jan 1 - Dec 31),
349+
* current year ends at today's date.
349350
*/
350351
function generateNormalizedChunks(
351352
startDate: string,
352353
endDate: string,
353354
): Array<{ from: string; to: string }> {
354-
const CHUNK_DAYS = 500 // Stay well under 18-month (549 day) limit
355+
const NPM_STATS_START_DATE = '2015-01-10'
355356
const chunks: Array<{ from: string; to: string }> = []
356357

357-
let currentFrom = new Date(startDate)
358-
const finalDate = new Date(endDate)
358+
const startYear = new Date(startDate).getFullYear()
359+
const endYear = new Date(endDate).getFullYear()
360+
const currentYear = new Date().getFullYear()
361+
const today = new Date().toISOString().substring(0, 10)
359362

360-
while (currentFrom <= finalDate) {
361-
const from = currentFrom.toISOString().substring(0, 10)
363+
for (let year = startYear; year <= endYear; year++) {
364+
let from = `${year}-01-01`
365+
let to = `${year}-12-31`
362366

363-
// Calculate chunk end: either CHUNK_DAYS later or finalDate, whichever is earlier
364-
const potentialTo = new Date(currentFrom)
365-
potentialTo.setDate(potentialTo.getDate() + CHUNK_DAYS - 1) // -1 because inclusive
367+
// Adjust start date if before npm stats started
368+
if (from < NPM_STATS_START_DATE) {
369+
from = NPM_STATS_START_DATE
370+
}
366371

367-
const to =
368-
potentialTo > finalDate
369-
? finalDate.toISOString().substring(0, 10)
370-
: potentialTo.toISOString().substring(0, 10)
372+
// Current year ends at today
373+
if (year === currentYear) {
374+
to = today
375+
}
371376

372-
chunks.push({ from, to })
377+
// Skip if the entire chunk is before npm stats started
378+
if (to < NPM_STATS_START_DATE) {
379+
continue
380+
}
373381

374-
// Move to next chunk (day after current chunk ends)
375-
currentFrom = new Date(to)
376-
currentFrom.setDate(currentFrom.getDate() + 1)
382+
// Skip future years
383+
if (year > currentYear) {
384+
continue
385+
}
377386

378-
// Prevent infinite loop if we've reached the end
379-
if (to === endDate) break
387+
chunks.push({ from, to })
380388
}
381389

382390
return chunks

0 commit comments

Comments
 (0)