Skip to content

Commit ed407f5

Browse files
committed
checkpoint
1 parent 9c5e5b5 commit ed407f5

File tree

19 files changed

+850
-443
lines changed

19 files changed

+850
-443
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Config } from '@netlify/functions'
2+
import { syncBlogPosts } from '~/server/feed/blog'
3+
4+
/**
5+
* Netlify Scheduled Function - Sync blog posts from content collections
6+
*
7+
* This function syncs blog posts from content collections into the feed database:
8+
* - Reads all posts from content-collections (available in deployed server)
9+
* - Creates/updates feed entries with excerpt and link to full post
10+
* - Marks entries as auto-synced
11+
*
12+
* Scheduled: Runs automatically every 5 minutes to pick up new blog posts after deployment
13+
* Since the sync is idempotent, running frequently is safe.
14+
*/
15+
const handler = async (req: Request) => {
16+
const { next_run } = await req.json()
17+
18+
console.log('[sync-blog-posts-background] Starting blog posts sync...')
19+
20+
const startTime = Date.now()
21+
22+
try {
23+
const result = await syncBlogPosts()
24+
25+
const duration = Date.now() - startTime
26+
console.log(
27+
`[sync-blog-posts-background] ✓ Completed in ${duration}ms - Created: ${result.created}, Updated: ${result.updated}, Total: ${result.syncedCount}`,
28+
)
29+
if (result.errors.length > 0) {
30+
console.error(
31+
`[sync-blog-posts-background] Errors: ${result.errors.length}`,
32+
result.errors,
33+
)
34+
}
35+
console.log('[sync-blog-posts-background] Next invocation at:', next_run)
36+
} catch (error) {
37+
const duration = Date.now() - startTime
38+
const errorMessage = error instanceof Error ? error.message : String(error)
39+
const errorStack = error instanceof Error ? error.stack : undefined
40+
41+
console.error(
42+
`[sync-blog-posts-background] ✗ Failed after ${duration}ms:`,
43+
errorMessage,
44+
)
45+
if (errorStack) {
46+
console.error('[sync-blog-posts-background] Stack:', errorStack)
47+
}
48+
}
49+
}
50+
51+
export default handler
52+
53+
export const config: Config = {
54+
schedule: '*/5 * * * *', // Every 5 minutes
55+
}

src/components/FeedFilters.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,6 @@ export function FeedFilters({
186186
onFiltersChange({ featured: value })
187187
}
188188

189-
// Check if release levels differ from default (all except patch)
190-
const defaultReleaseLevels: ReleaseLevel[] = ['major', 'minor']
191-
const releaseLevelsDiffer =
192-
!selectedReleaseLevels ||
193-
selectedReleaseLevels.length !== defaultReleaseLevels.length ||
194-
!defaultReleaseLevels.every((level) =>
195-
selectedReleaseLevels.includes(level),
196-
)
197-
198189
const hasActiveFilters =
199190
(selectedSources && selectedSources.length > 0) ||
200191
(selectedLibraries && selectedLibraries.length > 0) ||
@@ -203,7 +194,7 @@ export function FeedFilters({
203194
(selectedTags && selectedTags.length > 0) ||
204195
featured !== undefined ||
205196
search ||
206-
releaseLevelsDiffer ||
197+
(selectedReleaseLevels && selectedReleaseLevels.length > 0) ||
207198
(includePrerelease !== undefined && includePrerelease !== true)
208199

209200
// Render filter content (shared between mobile and desktop)

src/components/FeedPage.tsx

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { useState, useEffect } from 'react'
2+
import { useMounted } from '~/hooks/useMounted'
3+
import { Footer } from '~/components/Footer'
4+
import {
5+
FeedPageLayout,
6+
type FeedFiltersState,
7+
} from '~/components/FeedPageLayout'
8+
import { useFeedQuery } from '~/hooks/useFeedQuery'
9+
import { FeedEntry } from '~/components/FeedEntry'
10+
import { FEED_DEFAULTS } from '~/utils/feedDefaults'
11+
12+
interface FeedPageProps {
13+
search: FeedFiltersState & {
14+
page?: number
15+
pageSize?: number
16+
viewMode?: 'table' | 'timeline'
17+
expanded?: string[]
18+
}
19+
onNavigate: (updates: {
20+
search?: Partial<
21+
FeedFiltersState & {
22+
page?: number
23+
pageSize?: number
24+
viewMode?: 'table' | 'timeline'
25+
expanded?: string[]
26+
}
27+
>
28+
replace?: boolean
29+
resetScroll?: boolean
30+
}) => void
31+
includeHidden?: boolean
32+
adminActions?: {
33+
onEdit?: (entry: FeedEntry) => void
34+
onToggleVisibility?: (entry: FeedEntry, isVisible: boolean) => void
35+
onToggleFeatured?: (entry: FeedEntry, featured: boolean) => void
36+
onDelete?: (entry: FeedEntry) => void
37+
}
38+
headerTitle?: React.ReactNode
39+
headerActions?: React.ReactNode
40+
headerExtra?: React.ReactNode
41+
showFooter?: boolean
42+
}
43+
44+
export function FeedPage({
45+
search,
46+
onNavigate,
47+
includeHidden = false,
48+
adminActions,
49+
headerTitle,
50+
headerActions,
51+
headerExtra,
52+
showFooter = true,
53+
}: FeedPageProps) {
54+
const mounted = useMounted()
55+
56+
// Load saved filter preferences from localStorage (only on client)
57+
const [savedFilters, setSavedFilters] = useState<typeof search | null>(null)
58+
const [savedViewMode, setSavedViewMode] = useState<string | null>(null)
59+
60+
useEffect(() => {
61+
if (mounted) {
62+
const saved = localStorage.getItem('feedFilters')
63+
if (saved) {
64+
try {
65+
setSavedFilters(JSON.parse(saved))
66+
} catch {
67+
// Ignore parse errors
68+
}
69+
}
70+
const viewMode = localStorage.getItem('feedViewMode')
71+
if (viewMode) {
72+
setSavedViewMode(viewMode)
73+
}
74+
}
75+
}, [mounted])
76+
77+
// Merge saved filters with URL params (URL params take precedence)
78+
const effectiveFilters = {
79+
...savedFilters,
80+
...search,
81+
viewMode:
82+
search.viewMode ??
83+
(savedViewMode === 'table' || savedViewMode === 'timeline'
84+
? savedViewMode
85+
: undefined) ??
86+
'table',
87+
}
88+
89+
// Normalize empty arrays to undefined so they don't filter anything
90+
const normalizeFilter = <T,>(value: T[] | undefined): T[] | undefined => {
91+
return value && value.length > 0 ? value : undefined
92+
}
93+
94+
const feedQuery = useFeedQuery({
95+
page: effectiveFilters.page ?? 1,
96+
pageSize: effectiveFilters.pageSize ?? 50,
97+
filters: {
98+
sources: normalizeFilter(effectiveFilters.sources),
99+
libraries: normalizeFilter(effectiveFilters.libraries),
100+
categories: normalizeFilter(effectiveFilters.categories),
101+
partners: normalizeFilter(effectiveFilters.partners),
102+
tags: normalizeFilter(effectiveFilters.tags),
103+
releaseLevels: normalizeFilter(effectiveFilters.releaseLevels),
104+
includePrerelease: effectiveFilters.includePrerelease,
105+
featured: effectiveFilters.featured,
106+
search: effectiveFilters.search,
107+
includeHidden,
108+
},
109+
})
110+
111+
const handleFiltersChange = (newFilters: Partial<FeedFiltersState>): void => {
112+
onNavigate({
113+
search: {
114+
...search,
115+
...newFilters,
116+
page: 1,
117+
},
118+
replace: true,
119+
})
120+
121+
// Save to localStorage
122+
if (typeof window !== 'undefined') {
123+
const updatedFilters = { ...search, ...newFilters, page: 1 }
124+
localStorage.setItem('feedFilters', JSON.stringify(updatedFilters))
125+
setSavedFilters(updatedFilters)
126+
}
127+
}
128+
129+
const handleClearFilters = () => {
130+
onNavigate({
131+
search: {
132+
page: FEED_DEFAULTS.page,
133+
pageSize: effectiveFilters.pageSize ?? FEED_DEFAULTS.pageSize,
134+
viewMode: effectiveFilters.viewMode ?? FEED_DEFAULTS.viewMode,
135+
},
136+
replace: true,
137+
})
138+
139+
if (typeof window !== 'undefined') {
140+
localStorage.removeItem('feedFilters')
141+
setSavedFilters(null)
142+
}
143+
}
144+
145+
const handlePageSizeChange = (newPageSize: number) => {
146+
onNavigate({
147+
search: {
148+
...search,
149+
pageSize: newPageSize,
150+
page: 1,
151+
},
152+
replace: true,
153+
resetScroll: false,
154+
})
155+
}
156+
157+
const handleViewModeChange = (viewMode: 'table' | 'timeline') => {
158+
onNavigate({
159+
search: {
160+
...search,
161+
viewMode,
162+
},
163+
replace: true,
164+
})
165+
if (typeof window !== 'undefined') {
166+
localStorage.setItem('feedViewMode', viewMode)
167+
}
168+
}
169+
170+
const handleExpandedChange = (expandedIds: string[]) => {
171+
onNavigate({
172+
search: {
173+
...search,
174+
expanded: expandedIds.length > 0 ? expandedIds : undefined,
175+
},
176+
replace: true,
177+
resetScroll: false,
178+
})
179+
}
180+
181+
return (
182+
<FeedPageLayout.Root
183+
feedQuery={feedQuery}
184+
currentPage={effectiveFilters.page ?? 1}
185+
pageSize={effectiveFilters.pageSize ?? 50}
186+
filters={effectiveFilters}
187+
onFiltersChange={handleFiltersChange}
188+
onClearFilters={handleClearFilters}
189+
onPageChange={(page) => {
190+
onNavigate({
191+
search: {
192+
...search,
193+
page,
194+
},
195+
replace: true,
196+
resetScroll: false,
197+
})
198+
}}
199+
onPageSizeChange={handlePageSizeChange}
200+
viewMode={effectiveFilters.viewMode ?? 'table'}
201+
onViewModeChange={handleViewModeChange}
202+
expandedIds={search.expanded}
203+
onExpandedChange={handleExpandedChange}
204+
adminActions={adminActions}
205+
>
206+
{headerTitle && (
207+
<FeedPageLayout.Header
208+
title={headerTitle}
209+
actions={headerActions}
210+
extra={headerExtra}
211+
/>
212+
)}
213+
<div className="flex flex-col lg:flex-row gap-2">
214+
<FeedPageLayout.Filters />
215+
<FeedPageLayout.Content />
216+
</div>
217+
{showFooter && (
218+
<FeedPageLayout.Footer>
219+
<Footer />
220+
</FeedPageLayout.Footer>
221+
)}
222+
</FeedPageLayout.Root>
223+
)
224+
}

src/components/FeedPageLayout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ function FeedPageLayoutRoot({
154154
adminActions,
155155
}}
156156
>
157-
<div className="flex-1 flex flex-col max-w-full min-h-screen gap-2 sm:gap-4 p-2 sm:p-4 pb-0">
157+
<div className="flex-1 flex flex-col max-w-full gap-2 sm:gap-4 relative">
158158
<div className="flex-1 space-y-2 sm:space-y-4 w-full max-w-7xl mx-auto">
159159
{children}
160160
</div>
@@ -208,7 +208,7 @@ function FeedPageLayoutFilters() {
208208
} = useFeedPageLayout()
209209

210210
return (
211-
<aside className="lg:w-64 flex-shrink-0">
211+
<aside className="lg:w-64 flex-shrink-0 lg:self-start">
212212
<FeedFilters
213213
libraries={libraries}
214214
partners={partners}
@@ -245,7 +245,7 @@ function FeedPageLayoutContent({ children }: { children?: ReactNode }) {
245245
} = useFeedPageLayout()
246246

247247
return (
248-
<main className="flex-1 min-w-0">
248+
<main className="flex-1 min-w-0 relative">
249249
<FeedList
250250
query={feedQuery}
251251
currentPage={currentPage}

src/components/PaginationControls.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export function PaginationControls({
107107
if (sticky) {
108108
return (
109109
<div className="sticky bottom-4 mt-4">
110-
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg px-3 py-2">
110+
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl px-3 py-2">
111111
{content}
112112
</div>
113113
</div>

src/components/TableComponents.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export function TableHeaderCell({
7373
align === 'right'
7474
? 'text-right'
7575
: align === 'center'
76-
? 'text-center'
77-
: 'text-left'
76+
? 'text-center'
77+
: 'text-left'
7878
const paddingClass = compact ? 'px-2 py-1.5' : 'px-4 py-2'
7979
const textSizeClass = compact ? 'text-[10px]' : 'text-xs'
8080
return (
@@ -139,8 +139,8 @@ export function TableCell({
139139
align === 'right'
140140
? 'text-right'
141141
: align === 'center'
142-
? 'text-center'
143-
: 'text-left'
142+
? 'text-center'
143+
: 'text-left'
144144
const paddingClass = compact ? 'px-2 py-2' : 'px-4 py-3'
145145
return (
146146
<td

0 commit comments

Comments
 (0)