diff --git a/app/admin/dashboard/feed/page.tsx b/app/admin/dashboard/feed/page.tsx index afe2f32..07f090d 100644 --- a/app/admin/dashboard/feed/page.tsx +++ b/app/admin/dashboard/feed/page.tsx @@ -15,7 +15,7 @@ interface FeedPost { excerpt: string; topic: string; postedAt?: string; - source: 'seed' | 'olostep-search-index' | 'community-submission'; + source: 'seed' | 'olostep-search-index' | 'community-submission' | 'admin-import'; status: 'approved' | 'pending' | 'hidden'; createdAt: string; } @@ -30,8 +30,13 @@ const SOURCE_LABELS: Record = { seed: 'Starter list', 'olostep-search-index': 'Found by Olostep', 'community-submission': 'Submitted', + 'admin-import': 'Imported', }; +/** Olostep charges about 5 credits per search */ +const CREDITS_PER_SEARCH = 5; +const DEEP_SEARCHES = 40; + export default function AdminFeedPage() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); @@ -39,6 +44,9 @@ export default function AdminFeedPage() { const [processingId, setProcessingId] = useState(null); const [collecting, setCollecting] = useState(false); const [collectMessage, setCollectMessage] = useState(''); + const [importText, setImportText] = useState(''); + const [importing, setImporting] = useState(false); + const [importMessage, setImportMessage] = useState(''); const fetchPosts = async () => { try { @@ -84,18 +92,28 @@ export default function AdminFeedPage() { } }; - const collect = async () => { + const collect = async (searches?: number) => { + if ( + searches && + !confirm(`Run ${searches} Olostep searches? This uses about ${searches * CREDITS_PER_SEARCH} credits and can take a few minutes.`) + ) { + return; + } setCollecting(true); setCollectMessage(''); try { - const res = await fetch('/api/admin/feed', { method: 'POST' }); + const res = await fetch('/api/admin/feed', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'collect', searches }), + }); const data = await res.json(); if (!res.ok) { setCollectMessage(data.error ?? 'Collection failed.'); return; } setCollectMessage( - `Ran ${data.queries} searches${data.failedQueries ? ` (${data.failedQueries} failed)` : ''}: ${data.added} new posts, ${data.enriched} updated.` + `Ran ${data.queries} of ${data.searchListSize} searches${data.failedQueries ? ` (${data.failedQueries} failed)` : ''}: ${data.added} new posts, ${data.enriched} updated.` ); await fetchPosts(); } catch { @@ -105,6 +123,32 @@ export default function AdminFeedPage() { } }; + const importLinks = async () => { + setImporting(true); + setImportMessage(''); + try { + const res = await fetch('/api/admin/feed', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'import', urls: importText }), + }); + const data = await res.json(); + if (!res.ok) { + setImportMessage(data.error ?? 'Import failed.'); + return; + } + setImportMessage( + `${data.added} added, ${data.alreadyThere} already on the feed${data.invalid ? `, ${data.invalid} not LinkedIn post links` : ''}.` + ); + if (data.added > 0) setImportText(''); + await fetchPosts(); + } catch { + setImportMessage('Import failed.'); + } finally { + setImporting(false); + } + }; + const counts = Object.fromEntries(TABS.map(({ value }) => [value, posts.filter((post) => post.status === value).length])); const shown = posts.filter((post) => post.status === tab); @@ -125,16 +169,52 @@ export default function AdminFeedPage() { LinkedIn posts shown on /feed. Submissions wait here for review.

-
- -

Runs Olostep searches; uses credits.

+
+
+ + +
+

+ Each search uses about {CREDITS_PER_SEARCH} Olostep credits. Runs continue where the last one stopped. +

{collectMessage &&

{collectMessage}

}
+ + + Add posts by link +

+ Paste LinkedIn post links, one per line or separated by spaces, e.g. collected from WhatsApp groups. They go live straight away. +

+
+ + +