Skip to content

Commit 29cb2d5

Browse files
authored
Merge pull request #27 from werther41/new-article-discovery
article discover feature
2 parents 7bad8a6 + 378e3b2 commit 29cb2d5

40 files changed

+1693
-142
lines changed

app/admin/import/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default function ImportPage() {
118118
<Card>
119119
<CardHeader>
120120
<CardTitle className="flex items-center gap-2">
121-
<Upload className="h-5 w-5" />
121+
<Upload className="size-5" />
122122
Import Facts
123123
</CardTitle>
124124
</CardHeader>
@@ -192,12 +192,12 @@ export default function ImportPage() {
192192
<CardTitle className="flex items-center gap-2">
193193
{results ? (
194194
results.success ? (
195-
<CheckCircle className="h-5 w-5 text-green-500" />
195+
<CheckCircle className="size-5 text-green-500" />
196196
) : (
197-
<XCircle className="h-5 w-5 text-red-500" />
197+
<XCircle className="size-5 text-red-500" />
198198
)
199199
) : (
200-
<AlertCircle className="h-5 w-5 text-gray-500" />
200+
<AlertCircle className="size-5 text-gray-500" />
201201
)}
202202
Import Results
203203
</CardTitle>
@@ -259,7 +259,7 @@ export default function ImportPage() {
259259
</div>
260260
) : (
261261
<div className="py-8 text-center text-muted-foreground">
262-
<AlertCircle className="mx-auto mb-4 h-12 w-12 opacity-50" />
262+
<AlertCircle className="mx-auto mb-4 size-12 opacity-50" />
263263
<p>Import results will appear here after running an import.</p>
264264
</div>
265265
)}

app/admin/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ export default function AdminLayout({
1717
<div className="flex items-center gap-4">
1818
<Link href="/">
1919
<Button variant="ghost" size="sm">
20-
<ArrowLeft className="mr-2 h-4 w-4" />
20+
<ArrowLeft className="mr-2 size-4" />
2121
Back to App
2222
</Button>
2323
</Link>
2424
<div className="flex items-center gap-2">
25-
<Settings className="h-5 w-5" />
25+
<Settings className="size-5" />
2626
<h1 className="text-xl font-semibold">Admin Panel</h1>
2727
</div>
2828
</div>

app/admin/topics/page.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
1717
const getEntityIcon = (type: string) => {
1818
switch (type) {
1919
case "TECH":
20-
return <Hash className="h-4 w-4" />
20+
return <Hash className="size-4" />
2121
case "ORG":
22-
return <Building className="h-4 w-4" />
22+
return <Building className="size-4" />
2323
case "PERSON":
24-
return <User className="h-4 w-4" />
24+
return <User className="size-4" />
2525
case "LOCATION":
26-
return <MapPin className="h-4 w-4" />
26+
return <MapPin className="size-4" />
2727
case "CONCEPT":
28-
return <Lightbulb className="h-4 w-4" />
28+
return <Lightbulb className="size-4" />
2929
case "EVENT":
30-
return <Calendar className="h-4 w-4" />
30+
return <Calendar className="size-4" />
3131
default:
32-
return <TrendingUp className="h-4 w-4" />
32+
return <TrendingUp className="size-4" />
3333
}
3434
}
3535

@@ -41,7 +41,7 @@ async function TopicStats() {
4141
<Card>
4242
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
4343
<CardTitle className="text-sm font-medium">Total Articles</CardTitle>
44-
<TrendingUp className="h-4 w-4 text-muted-foreground" />
44+
<TrendingUp className="size-4 text-muted-foreground" />
4545
</CardHeader>
4646
<CardContent>
4747
<div className="text-2xl font-bold">{stats.totalArticles}</div>
@@ -53,7 +53,7 @@ async function TopicStats() {
5353
<CardTitle className="text-sm font-medium">
5454
Articles with Topics
5555
</CardTitle>
56-
<Hash className="h-4 w-4 text-muted-foreground" />
56+
<Hash className="size-4 text-muted-foreground" />
5757
</CardHeader>
5858
<CardContent>
5959
<div className="text-2xl font-bold">{stats.articlesWithTopics}</div>
@@ -66,7 +66,7 @@ async function TopicStats() {
6666
<Card>
6767
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
6868
<CardTitle className="text-sm font-medium">Total Topics</CardTitle>
69-
<Lightbulb className="h-4 w-4 text-muted-foreground" />
69+
<Lightbulb className="size-4 text-muted-foreground" />
7070
</CardHeader>
7171
<CardContent>
7272
<div className="text-2xl font-bold">{stats.totalTopics}</div>
@@ -76,7 +76,7 @@ async function TopicStats() {
7676
<Card>
7777
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
7878
<CardTitle className="text-sm font-medium">Trending Topics</CardTitle>
79-
<TrendingUp className="h-4 w-4 text-muted-foreground" />
79+
<TrendingUp className="size-4 text-muted-foreground" />
8080
</CardHeader>
8181
<CardContent>
8282
<div className="text-2xl font-bold">{stats.trendingTopics}</div>
@@ -171,8 +171,8 @@ export default function TopicsAdminPage() {
171171
className="flex items-center justify-between rounded-lg border p-3"
172172
>
173173
<div className="flex items-center gap-3">
174-
<div className="h-4 w-4 animate-pulse rounded bg-muted"></div>
175-
<div className="h-4 w-4 animate-pulse rounded bg-muted"></div>
174+
<div className="size-4 animate-pulse rounded bg-muted"></div>
175+
<div className="size-4 animate-pulse rounded bg-muted"></div>
176176
<div>
177177
<div className="mb-2 h-4 w-32 animate-pulse rounded bg-muted"></div>
178178
<div className="h-3 w-24 animate-pulse rounded bg-muted"></div>

app/api/articles/route.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
3+
import { getArticlesByTopics } from "@/lib/articles"
4+
5+
// Cache for 5 minutes
6+
export const revalidate = 300
7+
export const dynamic = "force-dynamic"
8+
9+
export async function GET(request: NextRequest) {
10+
try {
11+
const { searchParams } = request.nextUrl
12+
13+
// Parse query parameters
14+
const topicsParam = searchParams.get("topics")
15+
const timeFilter = searchParams.get("timeFilter") || "7d"
16+
const topicTypesParam = searchParams.get("topicTypes")
17+
const sortBy = searchParams.get("sortBy") || "score"
18+
19+
// Validate required parameters
20+
if (!topicsParam) {
21+
return NextResponse.json(
22+
{ error: "topics parameter is required" },
23+
{ status: 400 }
24+
)
25+
}
26+
27+
// Parse topics array
28+
const topics = topicsParam
29+
.split(",")
30+
.map((t) => t.trim())
31+
.filter(Boolean)
32+
if (topics.length === 0) {
33+
return NextResponse.json(
34+
{ error: "At least one topic is required" },
35+
{ status: 400 }
36+
)
37+
}
38+
39+
// Parse topic types array
40+
const topicTypes = topicTypesParam
41+
? topicTypesParam
42+
.split(",")
43+
.map((t) => t.trim())
44+
.filter(Boolean)
45+
: []
46+
47+
// Validate timeFilter
48+
const validTimeFilters = ["24h", "7d", "30d", "all"]
49+
if (!validTimeFilters.includes(timeFilter)) {
50+
return NextResponse.json(
51+
{ error: "timeFilter must be one of: 24h, 7d, 30d, all" },
52+
{ status: 400 }
53+
)
54+
}
55+
56+
// Validate sortBy
57+
const validSortBy = ["time", "score"]
58+
if (!validSortBy.includes(sortBy)) {
59+
return NextResponse.json(
60+
{ error: "sortBy must be one of: time, score" },
61+
{ status: 400 }
62+
)
63+
}
64+
65+
// Convert timeFilter to hours
66+
let timeWindow: number | null = null
67+
switch (timeFilter) {
68+
case "24h":
69+
timeWindow = 24
70+
break
71+
case "7d":
72+
timeWindow = 168
73+
break
74+
case "30d":
75+
timeWindow = 720
76+
break
77+
case "all":
78+
timeWindow = null
79+
break
80+
}
81+
82+
// Get articles
83+
const articles = await getArticlesByTopics(topics, {
84+
timeWindow,
85+
topicTypes,
86+
limit: 50,
87+
})
88+
89+
// Sort results
90+
let sortedArticles = [...articles]
91+
if (sortBy === "time") {
92+
sortedArticles.sort(
93+
(a, b) =>
94+
new Date(b.published_at).getTime() -
95+
new Date(a.published_at).getTime()
96+
)
97+
} else {
98+
// Already sorted by relevance score from the query
99+
sortedArticles.sort((a, b) => b.relevanceScore - a.relevanceScore)
100+
}
101+
102+
// Prepare response
103+
const response = {
104+
articles: sortedArticles,
105+
metadata: {
106+
totalResults: sortedArticles.length,
107+
timeFilter,
108+
searchType: "topic" as const,
109+
topics,
110+
topicTypes,
111+
sortBy,
112+
generatedAt: new Date().toISOString(),
113+
},
114+
}
115+
116+
return NextResponse.json(response, {
117+
headers: {
118+
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
119+
"CDN-Cache-Control": "public, s-maxage=300",
120+
"Vercel-CDN-Cache-Control": "public, s-maxage=300",
121+
},
122+
})
123+
} catch (error) {
124+
console.error("Error in GET /api/articles:", error)
125+
return NextResponse.json(
126+
{
127+
error: "Failed to retrieve articles",
128+
details: error instanceof Error ? error.message : "Unknown error",
129+
},
130+
{ status: 500 }
131+
)
132+
}
133+
}

app/api/articles/search/route.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
3+
import { searchArticlesByText } from "@/lib/articles"
4+
5+
// Cache for 5 minutes
6+
export const revalidate = 300
7+
export const dynamic = "force-dynamic"
8+
9+
export async function GET(request: NextRequest) {
10+
try {
11+
const { searchParams } = request.nextUrl
12+
13+
// Parse query parameters
14+
const query = searchParams.get("q")
15+
const timeFilter = searchParams.get("timeFilter") || "7d"
16+
17+
// Validate required parameters
18+
if (!query) {
19+
return NextResponse.json(
20+
{ error: "q (query) parameter is required" },
21+
{ status: 400 }
22+
)
23+
}
24+
25+
// Validate query length
26+
if (query.length < 3) {
27+
return NextResponse.json(
28+
{ error: "Query must be at least 3 characters long" },
29+
{ status: 400 }
30+
)
31+
}
32+
33+
// Validate timeFilter
34+
const validTimeFilters = ["24h", "7d", "30d", "all"]
35+
if (!validTimeFilters.includes(timeFilter)) {
36+
return NextResponse.json(
37+
{ error: "timeFilter must be one of: 24h, 7d, 30d, all" },
38+
{ status: 400 }
39+
)
40+
}
41+
42+
// Convert timeFilter to hours
43+
let timeWindow: number | null = null
44+
switch (timeFilter) {
45+
case "24h":
46+
timeWindow = 24
47+
break
48+
case "7d":
49+
timeWindow = 168
50+
break
51+
case "30d":
52+
timeWindow = 720
53+
break
54+
case "all":
55+
timeWindow = null
56+
break
57+
}
58+
59+
// Search articles
60+
const articles = await searchArticlesByText(query, {
61+
timeWindow,
62+
limit: 20,
63+
})
64+
65+
// Prepare response
66+
const response = {
67+
articles,
68+
metadata: {
69+
totalResults: articles.length,
70+
timeFilter,
71+
searchType: "text" as const,
72+
query,
73+
generatedAt: new Date().toISOString(),
74+
},
75+
}
76+
77+
return NextResponse.json(response, {
78+
headers: {
79+
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
80+
"CDN-Cache-Control": "public, s-maxage=300",
81+
"Vercel-CDN-Cache-Control": "public, s-maxage=300",
82+
},
83+
})
84+
} catch (error) {
85+
console.error("Error in GET /api/articles/search:", error)
86+
return NextResponse.json(
87+
{
88+
error: "Failed to search articles",
89+
details: error instanceof Error ? error.message : "Unknown error",
90+
},
91+
{ status: 500 }
92+
)
93+
}
94+
}

app/api/topics/route.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export async function GET(request: NextRequest) {
1414
const timeWindow = parseInt(searchParams.get("timeWindow") || "48")
1515
const limit = parseInt(searchParams.get("limit") || "10")
1616
const entityType = searchParams.get("entityType") || undefined
17+
const topicTypes =
18+
searchParams.get("topicTypes")?.split(",").filter(Boolean) || undefined
1719
const diverse = searchParams.get("diverse") === "true"
1820
const randomize = searchParams.get("_t") !== null // If timestamp parameter exists, randomize
1921

@@ -35,8 +37,14 @@ export async function GET(request: NextRequest) {
3537

3638
// Get topics (diverse or regular)
3739
const topics = diverse
38-
? await getDiverseTopics({ timeWindow, limit, entityType, randomize })
39-
: await getTrendingTopics({ timeWindow, limit, entityType })
40+
? await getDiverseTopics({
41+
timeWindow,
42+
limit,
43+
entityType,
44+
randomize,
45+
topicTypes,
46+
})
47+
: await getTrendingTopics({ timeWindow, limit, entityType, topicTypes })
4048

4149
// Transform to API response format
4250
const response = {
@@ -63,6 +71,7 @@ export async function GET(request: NextRequest) {
6371
timeWindow,
6472
limit,
6573
entityType,
74+
topicTypes,
6675
diverse,
6776
randomize,
6877
totalTopics: topics.length,

0 commit comments

Comments
 (0)