diff --git a/app/blog/page.tsx b/app/blog/page.tsx
index 3c48bc3..6728d80 100644
--- a/app/blog/page.tsx
+++ b/app/blog/page.tsx
@@ -2,7 +2,7 @@ import { BlogContent } from "@/components/blog-content";
import { getBlogPageData } from "@/data/blog";
type BlogPageProps = {
- searchParams?: Promise<{ page?: string | string[]; category?: string | string[] }>;
+ searchParams?: Promise<{ page?: string | string[]; category?: string | string[]; search?: string | string[] }>;
};
export default async function BlogPage({ searchParams }: BlogPageProps) {
@@ -11,9 +11,10 @@ export default async function BlogPage({ searchParams }: BlogPageProps) {
const categoryParam = Array.isArray(resolved?.category)
? resolved?.category[0]
: resolved?.category;
+ const searchParam = Array.isArray(resolved?.search) ? resolved?.search[0] : resolved?.search;
const page = Number(pageParam ?? "1");
const currentPage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
- const data = await getBlogPageData(currentPage, 12, categoryParam);
+ const data = await getBlogPageData(currentPage, 12, categoryParam, searchParam);
return ;
}
diff --git a/components/app-shell.tsx b/components/app-shell.tsx
index 60c1ba5..6dd8117 100644
--- a/components/app-shell.tsx
+++ b/components/app-shell.tsx
@@ -1,4 +1,4 @@
-"use client";
+"use client";
import { Icon } from "@iconify/react";
import Link from "next/link";
@@ -24,7 +24,7 @@ export function AppShell({ children }: AppShellProps) {
return (
<>
-
+
{announcement ? (
) : null}
- {children}
+ {children}
>
);
diff --git a/components/blog-content.tsx b/components/blog-content.tsx
index 942f2a7..cf5de22 100644
--- a/components/blog-content.tsx
+++ b/components/blog-content.tsx
@@ -1,10 +1,12 @@
"use client";
+import { useState, useCallback } from "react";
import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { Icon } from "@iconify/react";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
import {
ArticleCard,
- NewsCard,
Pagination,
PaginationContent,
PaginationItem,
@@ -19,72 +21,116 @@ type BlogContentProps = {
data: BlogPageData;
};
-function pageHref(page: number, category: string) {
- const params = new URLSearchParams();
+function buildHref(params: { page?: number; category?: string; search?: string }) {
+ const qs = new URLSearchParams();
- if (page > 1) {
- params.set("page", String(page));
- }
+ if (params.page && params.page > 1) qs.set("page", String(params.page));
+ if (params.category && params.category !== "All") qs.set("category", params.category);
+ if (params.search) qs.set("search", params.search);
- if (category !== "All") {
- params.set("category", category);
- }
-
- const query = params.toString();
+ const query = qs.toString();
return query ? `/blog?${query}` : "/blog";
}
-function categoryHref(category: string) {
- if (category === "All") {
- return "/blog";
- }
-
- const params = new URLSearchParams();
- params.set("category", category);
-
- return `/blog?${params.toString()}`;
-}
-
export function BlogContent({ data }: BlogContentProps) {
- const pageNumbers = Array.from({ length: data.totalPages }, (_, index) => index + 1);
+ const router = useRouter();
+ const [searchInput, setSearchInput] = useState(data.searchQuery);
+ const pageNumbers = Array.from({ length: data.totalPages }, (_, i) => i + 1);
const hasArticles = data.articles.length > 0;
+ const submitSearch = useCallback(
+ (value: string) => {
+ const trimmed = value.trim();
+ router.push(buildHref({ category: data.selectedCategory, search: trimmed }));
+ },
+ [router, data.selectedCategory],
+ );
+
+ const clearFilters = useCallback(() => {
+ setSearchInput("");
+ router.push("/blog");
+ }, [router]);
+
return (
-
-
-
- {data.news.map((post) => (
-
+ {/* Filtre Çubuğu */}
+
+
+ {/* Kategoriler */}
+
+
+ Kategori:
+
+ {data.categories.map((category) => (
+
+
+ {category}
+
+
+ ))}
+
+
+ {/* Arama */}
+
+
- ))}
+ setSearchInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") submitSearch(searchInput);
+ }}
+ placeholder="Başlık veya içerikte ara..."
+ className="w-full rounded-sm border border-border bg-background py-2 pr-10 pl-9 text-sm text-foreground placeholder:text-muted-foreground focus:border-red-600 focus:ring-1 focus:ring-red-600/30 focus:outline-none"
+ />
+ {searchInput && (
+
+ )}
+
+
-
-
- Kategoriler
-
- {data.categories.map((category) => (
-
-
- {category}
-
-
- ))}
-
-
+ {/* Aktif filtre göstergesi */}
+ {(data.searchQuery || data.selectedCategory !== "All") && (
+
+
+ {data.articles.length} sonuç
+ {data.searchQuery ? ` · "${data.searchQuery}"` : ""}
+ {data.selectedCategory !== "All" ? ` · ${data.selectedCategory}` : ""}
+
+
-
+ )}
+ {/* Yazı Kartları */}
{hasArticles ? (
@@ -98,50 +144,83 @@ export function BlogContent({ data }: BlogContentProps) {
date={post.date}
readTime={post.readTime}
href={post.href}
- className="rounded-sm border-border [&_h3]:line-clamp-2 [&_h3]:min-h-[2.5rem]"
+ className="rounded-sm border-border [&_h3]:line-clamp-2 [&_h3]:min-h-[2.5rem] [&_p]:line-clamp-3"
author={{ name: post.author, avatar: "/logo/logo.jpeg" }}
/>
))}
) : (
-
+
+
- {data.selectedCategory === "All"
- ? "Henüz blog yazısı bulunmuyor."
- : `"${data.selectedCategory}" kategorisinde henüz blog yazısı bulunmuyor.`}
+ {data.searchQuery
+ ? `"${data.searchQuery}" aramasına uygun sonuç bulunamadı.`
+ : data.selectedCategory === "All"
+ ? "Henüz blog yazısı bulunmuyor."
+ : `"${data.selectedCategory}" kategorisinde henüz blog yazısı bulunmuyor.`}
+ {(data.searchQuery || data.selectedCategory !== "All") && (
+
+ )}
)}
-
-
-
-
-
-
- {pageNumbers.map((page) => (
-
-
- {page}
-
+ {/* Sayfalama */}
+ {data.totalPages > 1 && (
+
+
+
+
- ))}
-
- = data.totalPages}
- />
-
-
-
+ {pageNumbers.map((page) => (
+
+
+ {page}
+
+
+ ))}
+
+
+ = data.totalPages}
+ />
+
+
+
+ )}
);
diff --git a/components/blog-detail-content.tsx b/components/blog-detail-content.tsx
index 9b0fa57..7fe7613 100644
--- a/components/blog-detail-content.tsx
+++ b/components/blog-detail-content.tsx
@@ -3,7 +3,8 @@
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { Icon } from "@iconify/react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Badge, Card, Typography } from "poyraz-ui/atoms";
@@ -99,38 +100,37 @@ const GISCUS_CATEGORY = process.env.NEXT_PUBLIC_GISCUS_CATEGORY || "Announcement
const GISCUS_CATEGORY_ID = process.env.NEXT_PUBLIC_GISCUS_CATEGORY_ID || "";
export function BlogDetailContent({ post }: BlogDetailContentProps) {
- const scrollerRef = useRef(null);
- const [progress, setProgress] = useState(0);
- const progressWidth = useMemo(() => `${Math.min(100, Math.max(0, progress))}%`, [progress]);
+ const progressBarRef = useRef(null);
+ const [tocOpen, setTocOpen] = useState(false);
- const handleScroll = () => {
- const element = scrollerRef.current;
- if (!element) return;
+ const closeToc = useCallback(() => setTocOpen(false), []);
- const scrollable = element.scrollHeight - element.clientHeight;
- if (scrollable <= 0) {
- setProgress(100);
- return;
- }
+ useEffect(() => {
+ const update = () => {
+ const bar = progressBarRef.current;
+ if (!bar) return;
+ const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
+ const pct = docHeight <= 0 ? 100 : (window.scrollY / docHeight) * 100;
+ bar.style.width = `${Math.min(100, Math.max(0, pct))}%`;
+ };
- setProgress((element.scrollTop / scrollable) * 100);
- };
+ window.addEventListener("scroll", update, { passive: true });
+ update();
+
+ return () => window.removeEventListener("scroll", update);
+ }, []);
const showGiscus = GISCUS_REPO && GISCUS_REPO_ID && GISCUS_CATEGORY_ID;
return (
<>
-
-
-
+
+
+