Skip to content

Commit 5ed3425

Browse files
Merge pull request #326 from nocodb/blog-corrections
2 parents 6296002 + 80cbc38 commit 5ed3425

File tree

8 files changed

+134
-37
lines changed

8 files changed

+134
-37
lines changed

.github/workflows/claude.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
id: claude
3535
uses: anthropics/claude-code-action@v1
3636
with:
37-
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
37+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
3838

3939
# Optional: Customize the trigger phrase (default: @claude)
4040
# trigger_phrase: "/claude"

app/blog/layout.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {baseOptions} from '@/app/layout.config';
44
import {Footer} from "@/components/blog/home/Footer";
55
import {Navbar} from "@/components/blog/home/Navbar";
66
import { ReCaptchaProvider } from "next-recaptcha-v3";
7+
import { NuqsAdapter } from 'nuqs/adapters/next/app';
78

89
export const metadata = {
910
icons: {
@@ -16,9 +17,11 @@ export default function Layout({children,}: { children: ReactNode; }): React.Rea
1617
<HomeLayout {...baseOptions} nav={{
1718
component: Navbar()
1819
}}>
19-
<ReCaptchaProvider reCaptchaKey="6LcdnI0oAAAAAHYW3hwztfZw9qAjX4TiviE4fWip">
20-
{children}
21-
</ReCaptchaProvider>
20+
<NuqsAdapter>
21+
<ReCaptchaProvider reCaptchaKey="6LcdnI0oAAAAAHYW3hwztfZw9qAjX4TiviE4fWip">
22+
{children}
23+
</ReCaptchaProvider>
24+
</NuqsAdapter>
2225
<Footer/>
2326
</HomeLayout>
2427
)

app/blog/page.tsx

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,100 @@
11
import {blogSource} from "@/lib/source";
2-
import {FeaturedPost} from "@/components/blog/FeaturedPost";
32
import BlogCard from "@/components/blog/BlogCard";
43
import {Separator} from "@/components/ui/separator";
54
import Link from "next/link";
65
import {CategoryTabs} from "@/components/blog/Category";
76
import Subscribe from "@/components/blog/Subscribe";
7+
import {SearchInput} from "@/components/blog/SearchInput";
8+
import Image from "next/image";
89

910
export const metadata = {
1011
title: "Blog | NocoDB",
1112
description: "Insights, tutorials, and updates from the team building the future of no-code databases.",
1213
}
1314

1415
export default async function BlogPage({searchParams}: {
15-
searchParams?: Promise<{ category?: string; page?: string }>
16+
searchParams?: Promise<{ category?: string; page?: string; search?: string }>
1617
}) {
1718
const posts = blogSource.getPages().sort(
1819
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
1920
);
2021

21-
const featuredPost = posts?.length ? posts[0] : null;
22-
23-
const latestPosts = posts.filter(post => post.data?.title !== featuredPost?.data?.title);
24-
2522
const categories = new Set<string>();
26-
latestPosts.forEach((post) => {
23+
posts.forEach((post) => {
2724
categories.add(post.data.category ?? "Uncategorized");
2825
});
2926

3027
const selectedCategory = (await searchParams)?.category;
28+
const searchQuery = (await searchParams)?.search?.toLowerCase() || '';
3129
const currentPage = parseInt((await searchParams)?.page || "1", 10);
3230
const postsPerPage = 15;
3331

3432
const categoryFilteredPosts = selectedCategory
35-
? latestPosts.filter((post) => (post.data.category ?? "Uncategorized") === selectedCategory)
36-
: latestPosts;
33+
? posts.filter((post) => (post.data.category ?? "Uncategorized") === selectedCategory)
34+
: posts;
35+
36+
const searchFilteredPosts = searchQuery
37+
? categoryFilteredPosts.filter((post) => {
38+
const titleMatch = post.data.title?.toLowerCase().includes(searchQuery);
39+
const descriptionMatch = post.data.description?.toLowerCase().includes(searchQuery);
40+
const authorMatch = post.data.author?.toLowerCase().includes(searchQuery);
41+
return titleMatch || descriptionMatch || authorMatch;
42+
})
43+
: categoryFilteredPosts;
3744

3845
const startIndex = (currentPage - 1) * postsPerPage;
3946
const endIndex = startIndex + postsPerPage;
40-
const displayedPosts = categoryFilteredPosts.slice(0, endIndex);
47+
const displayedPosts = searchFilteredPosts.slice(0, endIndex);
4148

42-
const hasMorePosts = endIndex < categoryFilteredPosts.length;
49+
const hasMorePosts = endIndex < searchFilteredPosts.length;
4350
const nextPage = currentPage + 1;
4451
return (
4552
<main className="py-8 w-full md:py-12">
46-
<div className="container py-20 lg:py-16">
47-
<h1 className="text-center text-nc-content-grey-emphasis text-[40px] font-semibold leading-15.5">
53+
<div className="container py-10 lg:pt-16 lg:pb-8">
54+
<h1 className="text-nc-content-grey-emphasis text-[40px] font-semibold leading-15.5">
4855
Blog
4956
</h1>
50-
<h5 className="text-nc-content-grey-subtle text-center mt-10 lg:mt-6 text-base leading-6 font-medium">
51-
Insights, tutorials, and updates <br/>
52-
from the team building the future of no-code databases.
57+
<h5 className="text-nc-content-grey-subtle mt-6 lg:mt-2 text-base leading-6 font-medium">
58+
Insights, tutorials, and updates from the team building the future of no-code databases.
5359
</h5>
60+
<div className="mt-6">
61+
<SearchInput />
62+
</div>
5463
</div>
55-
<div className="container mx-auto">
56-
{featuredPost && (
57-
<FeaturedPost post={featuredPost}/>
58-
)}
59-
</div>
60-
61-
<Subscribe/>
6264

63-
<div className="container mt-5 lg:mt-20">
64-
<CategoryTabs categories={Array.from(categories)} selectedCategory={selectedCategory}/>
65+
66+
<div className="container mt-5">
67+
68+
<CategoryTabs categories={Array.from(categories)} selectedCategory={selectedCategory} searchQuery={searchQuery}/>
6569
<Separator className="border-nc-border-grey-medium"/>
6670
</div>
6771

6872
<div className="container py-8 lg:pt-15 lg:pb-20 gap-8 lg:gap-16 grid grid-cols-1 lg:grid-cols-2">
69-
{displayedPosts.map((post) => (
70-
<BlogCard post={post} key={post.url}/>
71-
))}
73+
{displayedPosts.length === 0 ? (
74+
<div className="col-span-full flex flex-col items-center justify-center py-16">
75+
<Image
76+
src="/img/no-search-result-found.png"
77+
alt="No results found"
78+
width={400}
79+
height={300}
80+
className="mb-6"
81+
/>
82+
<h2 className="text-2xl font-semibold text-nc-content-grey-emphasis mb-2">
83+
No posts found
84+
</h2>
85+
<p className="text-nc-content-grey-subtle">
86+
{searchQuery
87+
? `No blog posts match "${searchQuery}". Try a different search term.`
88+
: selectedCategory
89+
? `No blog posts found in the "${selectedCategory}" category.`
90+
: "No blog posts available."}
91+
</p>
92+
</div>
93+
) : (
94+
displayedPosts.map((post) => (
95+
<BlogCard post={post} key={post.url}/>
96+
))
97+
)}
7298
</div>
7399

74100
{hasMorePosts && (
@@ -79,6 +105,7 @@ export default async function BlogPage({searchParams}: {
79105
pathname: "/blog",
80106
query: {
81107
...(selectedCategory ? {category: selectedCategory} : {}),
108+
...(searchQuery ? {search: searchQuery} : {}),
82109
page: nextPage.toString(),
83110
},
84111
}}
@@ -89,6 +116,7 @@ export default async function BlogPage({searchParams}: {
89116
</div>
90117
)}
91118

119+
<Subscribe/>
92120
<Separator/>
93121
</main>
94122
);

components/blog/Category.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@ import {cn} from "@/lib/utils";
55
interface TabProps {
66
categories: string[]
77
selectedCategory?: string
8+
searchQuery?: string
89
}
910

10-
export function CategoryTabs({categories, selectedCategory}: TabProps) {
11+
export function CategoryTabs({categories, selectedCategory, searchQuery}: TabProps) {
1112
return (
1213
<div className="border-b border-nc-border-grey-medium">
1314
<div className="flex flex-wrap space-x-6">
1415
<div className="relative mb-4">
1516
<Link
1617
scroll={false}
17-
href="/blog"
18+
href={{
19+
pathname: "/blog",
20+
query: searchQuery ? { search: searchQuery } : undefined,
21+
}}
1822
className={`text-nc-content-brand-default mx-2 relative px-3 py-1.5 rounded-[6px] text-sm h-6 bg-nc-background-brand ${!selectedCategory ? "font-semibold" : ""}`}
1923
>
2024
<span>All</span>
@@ -28,7 +32,13 @@ export function CategoryTabs({categories, selectedCategory}: TabProps) {
2832
>
2933
<Link
3034
scroll={false}
31-
href={`/blog?category=${encodeURIComponent(category)}`}
35+
href={{
36+
pathname: "/blog",
37+
query: {
38+
category: encodeURIComponent(category),
39+
...(searchQuery ? { search: searchQuery } : {}),
40+
},
41+
}}
3242
style={{backgroundColor: getCategoryColor(category)}}
3343
className={cn(`text-nc-content-grey-subtle-2 mb-2 relative px-3 py-1.5 rounded-[6px] text-sm h-6 bg-nc-background-brand`)}
3444
>

components/blog/SearchInput.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client';
2+
3+
import { useQueryState } from 'nuqs';
4+
import { Search } from 'lucide-react';
5+
6+
export function SearchInput() {
7+
const [search, setSearch] = useQueryState('search', {
8+
defaultValue: '',
9+
shallow: false,
10+
});
11+
12+
return (
13+
<div className="relative w-full max-w-md">
14+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-nc-content-grey-subtle" />
15+
<input
16+
type="text"
17+
placeholder="Search blog posts..."
18+
value={search}
19+
onChange={(e) => setSearch(e.target.value || null)}
20+
className="w-full pl-10 pr-4 py-2 border border-nc-border-grey-medium rounded-md bg-transparent text-nc-content-grey-emphasis placeholder:text-nc-content-grey-subtle focus:outline-none focus:ring-2 focus:ring-nc-content-brand-default focus:border-transparent"
21+
/>
22+
</div>
23+
);
24+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"next-sitemap": "^4.2.3",
3737
"next-themes": "^0.4.6",
3838
"next-validate-link": "^1.5.2",
39+
"nuqs": "^2.7.1",
3940
"react": "^19.1.0",
4041
"react-dom": "^19.1.0",
4142
"remark": "^15.0.1",

pnpm-lock.yaml

Lines changed: 33 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
49.2 KB
Loading

0 commit comments

Comments
 (0)