From 0cc34aa0073e1d28d17f83dc826d092f7f0d30a9 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 02:37:31 +0100 Subject: [PATCH 1/5] auto-claude: subtask-1-1 - Create SearchBar component with search input and r --- src/components/blog/SearchBar.tsx | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/components/blog/SearchBar.tsx diff --git a/src/components/blog/SearchBar.tsx b/src/components/blog/SearchBar.tsx new file mode 100644 index 0000000..4bc3520 --- /dev/null +++ b/src/components/blog/SearchBar.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { Search, X } from 'lucide-react'; +import { motion } from 'framer-motion'; + +interface SearchBarProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; +} + +export function SearchBar({ + value, + onChange, + placeholder = 'Search posts...', + className = '' +}: SearchBarProps) { + const handleClear = () => { + onChange(''); + }; + + return ( + +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full pl-12 pr-12 py-3 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-[#697565] focus:border-transparent transition-all" + /> + {value && ( + + )} +
+
+ ); +} From cfa2bc81e601a7283caebd74ac392c5119082027 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 02:39:30 +0100 Subject: [PATCH 2/5] auto-claude: subtask-1-2 - Create CategoryFilter component with category buttons/dropdown Created CategoryFilter component with the following features: - Client component with 'use client' directive and framer-motion animations - Props: categories, selectedCategory, onCategoryChange, className - Uses lucide-react Tag icon - Responsive design: horizontal scrollable on mobile, grid layout on desktop - Active category highlighted with bg-[#697565] text-white - Includes 'All' option to clear filter - Follows established patterns from SearchBar and ContactForm - Consistent styling with bg-zinc-900/50 and border-zinc-800 - Smooth transitions and hover states Co-Authored-By: Claude Sonnet 4.5 --- .auto-claude-security.json | 227 +++++++++++++++++++++++++ .auto-claude-status | 25 +++ .claude_settings.json | 39 +++++ .gitignore | 5 +- src/components/blog/CategoryFilter.tsx | 82 +++++++++ 5 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 .auto-claude-security.json create mode 100644 .auto-claude-status create mode 100644 .claude_settings.json create mode 100644 src/components/blog/CategoryFilter.tsx diff --git a/.auto-claude-security.json b/.auto-claude-security.json new file mode 100644 index 0000000..eed60a2 --- /dev/null +++ b/.auto-claude-security.json @@ -0,0 +1,227 @@ +{ + "base_commands": [ + ".", + "[", + "[[", + "ag", + "awk", + "basename", + "bash", + "bc", + "break", + "cat", + "cd", + "chmod", + "clear", + "cmp", + "column", + "comm", + "command", + "continue", + "cp", + "curl", + "cut", + "date", + "df", + "diff", + "dig", + "dirname", + "du", + "echo", + "egrep", + "env", + "eval", + "exec", + "exit", + "expand", + "export", + "expr", + "false", + "fd", + "fgrep", + "file", + "find", + "fmt", + "fold", + "gawk", + "gh", + "git", + "grep", + "gunzip", + "gzip", + "head", + "help", + "host", + "iconv", + "id", + "jobs", + "join", + "jq", + "kill", + "killall", + "less", + "let", + "ln", + "ls", + "lsof", + "man", + "mkdir", + "mktemp", + "more", + "mv", + "nl", + "paste", + "pgrep", + "ping", + "pkill", + "popd", + "printenv", + "printf", + "ps", + "pushd", + "pwd", + "read", + "readlink", + "realpath", + "reset", + "return", + "rev", + "rg", + "rm", + "rmdir", + "sed", + "seq", + "set", + "sh", + "shuf", + "sleep", + "sort", + "source", + "split", + "stat", + "tail", + "tar", + "tee", + "test", + "time", + "timeout", + "touch", + "tr", + "tree", + "true", + "type", + "uname", + "unexpand", + "uniq", + "unset", + "unzip", + "watch", + "wc", + "wget", + "whereis", + "which", + "whoami", + "xargs", + "yes", + "yq", + "zip", + "zsh" + ], + "stack_commands": [ + "ar", + "clang", + "clang++", + "cmake", + "composer", + "dive", + "docker", + "docker-buildx", + "docker-compose", + "dockerfile", + "eslint", + "g++", + "gcc", + "ipython", + "jupyter", + "ld", + "make", + "meson", + "next", + "ninja", + "nm", + "node", + "notebook", + "npm", + "npx", + "objdump", + "pdb", + "php", + "pip", + "pip3", + "pipx", + "pnpm", + "pnpx", + "pudb", + "python", + "python3", + "react-scripts", + "strip", + "ts-node", + "tsc", + "tsx", + "vitest" + ], + "script_commands": [ + "bun", + "npm", + "pnpm", + "yarn" + ], + "custom_commands": [], + "detected_stack": { + "languages": [ + "python", + "javascript", + "typescript", + "php", + "c" + ], + "package_managers": [ + "pnpm" + ], + "frameworks": [ + "nextjs", + "react", + "vitest", + "eslint" + ], + "databases": [], + "infrastructure": [ + "docker" + ], + "cloud_providers": [], + "code_quality_tools": [], + "version_managers": [] + }, + "custom_scripts": { + "npm_scripts": [ + "dev", + "build", + "start", + "lint", + "build:images", + "generate:images", + "generate:images:dry", + "test", + "test:coverage" + ], + "make_targets": [], + "poetry_scripts": [], + "cargo_aliases": [], + "shell_scripts": [] + }, + "project_dir": "C:\\Users\\damja\\WebstormProjects\\Portfolio", + "created_at": "2026-01-22T15:28:38.237190", + "project_hash": "c4ad399e16be367eb4e6b076fe1d9ee3", + "inherited_from": "C:\\Users\\damja\\WebstormProjects\\Portfolio" +} \ No newline at end of file diff --git a/.auto-claude-status b/.auto-claude-status new file mode 100644 index 0000000..24bbc83 --- /dev/null +++ b/.auto-claude-status @@ -0,0 +1,25 @@ +{ + "active": true, + "spec": "003-add-blog-search-and-category-filter", + "state": "building", + "subtasks": { + "completed": 1, + "total": 4, + "in_progress": 1, + "failed": 0 + }, + "phase": { + "current": "Blog Search and Filter Implementation", + "id": null, + "total": 4 + }, + "workers": { + "active": 0, + "max": 1 + }, + "session": { + "number": 3, + "started_at": "2026-01-25T02:31:36.044848" + }, + "last_update": "2026-01-25T02:38:09.538183" +} \ No newline at end of file diff --git a/.claude_settings.json b/.claude_settings.json new file mode 100644 index 0000000..bdadd0a --- /dev/null +++ b/.claude_settings.json @@ -0,0 +1,39 @@ +{ + "sandbox": { + "enabled": true, + "autoAllowBashIfSandboxed": true + }, + "permissions": { + "defaultMode": "acceptEdits", + "allow": [ + "Read(./**)", + "Write(./**)", + "Edit(./**)", + "Glob(./**)", + "Grep(./**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", + "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", + "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter\\.auto-claude\\specs\\003-add-blog-search-and-category-filter/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter\\.auto-claude\\specs\\003-add-blog-search-and-category-filter/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter\\.auto-claude\\specs\\003-add-blog-search-and-category-filter/**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", + "Bash(*)", + "WebFetch(*)", + "WebSearch(*)", + "mcp__context7__resolve-library-id(*)", + "mcp__context7__get-library-docs(*)", + "mcp__graphiti-memory__search_nodes(*)", + "mcp__graphiti-memory__search_facts(*)", + "mcp__graphiti-memory__add_episode(*)", + "mcp__graphiti-memory__get_episodes(*)", + "mcp__graphiti-memory__get_entity_edge(*)" + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a484ab2..fa65296 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,7 @@ supabase/.temp/ .history/ # Source images (originals before optimization) -source-images/ \ No newline at end of file +source-images/ + +# Auto Claude data directory +.auto-claude/ diff --git a/src/components/blog/CategoryFilter.tsx b/src/components/blog/CategoryFilter.tsx new file mode 100644 index 0000000..8d479b7 --- /dev/null +++ b/src/components/blog/CategoryFilter.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { Tag } from 'lucide-react'; +import { motion } from 'framer-motion'; + +interface CategoryFilterProps { + categories: string[]; + selectedCategory: string | null; + onCategoryChange: (category: string | null) => void; + className?: string; +} + +export function CategoryFilter({ + categories, + selectedCategory, + onCategoryChange, + className = '' +}: CategoryFilterProps) { + const allCategories = ['All', ...categories]; + + return ( + +
+ +

Filter by Category

+
+ + {/* Mobile: Horizontal scrollable */} +
+
+ {allCategories.map((category) => { + const isActive = category === 'All' + ? selectedCategory === null + : selectedCategory === category; + + return ( + + ); + })} +
+
+ + {/* Desktop: Grid */} +
+ {allCategories.map((category) => { + const isActive = category === 'All' + ? selectedCategory === null + : selectedCategory === category; + + return ( + + ); + })} +
+
+ ); +} From d130972cd789536988b2d32a13113a18de0fa0be Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 02:47:52 +0100 Subject: [PATCH 3/5] auto-claude: subtask-1-3 - Add filtering logic and integrate components into blog page - Created BlogList client component with search and category filtering - Integrated SearchBar and CategoryFilter components - Implemented case-insensitive search by title/excerpt/tags - Added category filtering with AND logic for combined filters - Updated pagination to work with filtered results - Added search placeholder translations for de/en/sr - Moved blog card and pagination logic to client component Co-Authored-By: Claude Sonnet 4.5 --- src/app/[locale]/blog/page.tsx | 325 ++----------------------- src/components/blog/BlogList.tsx | 403 +++++++++++++++++++++++++++++++ src/messages/de.json | 5 +- src/messages/en.json | 5 +- src/messages/sr.json | 5 +- 5 files changed, 429 insertions(+), 314 deletions(-) create mode 100644 src/components/blog/BlogList.tsx diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index 1b33fbc..7bbd402 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -1,16 +1,12 @@ import { setRequestLocale } from 'next-intl/server'; import { getTranslations } from 'next-intl/server'; -import { ArrowRight, Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; -import Link from 'next/link'; -import Image from 'next/image'; +import { ArrowRight } from 'lucide-react'; import type { Metadata } from 'next'; import { getAllBlogPosts, BlogPost } from '@/lib/blog'; - -const POSTS_PER_PAGE = 12; +import { BlogList } from '@/components/blog/BlogList'; type Props = { params: Promise<{ locale: string }>; - searchParams: Promise<{ page?: string }>; }; // Legacy blog posts with translations (these have manual translations) @@ -307,277 +303,9 @@ export async function generateMetadata({ params }: Props): Promise { }; } -function getFormattedDate(date: string, locale: string) { - const languageMap: Record = { - de: 'de-DE', - en: 'en-US', - sr: 'sr-RS', - }; - return new Date(date).toLocaleDateString(languageMap[locale] || 'de-DE', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); -} - -function getImagePath(coverImage: string) { - if (coverImage.startsWith('http')) { - return coverImage; - } - return coverImage.replace('/blog/', '/images/posts/'); -} - -// Placeholder image for posts without cover images -const PLACEHOLDER_IMAGE = 'data:image/svg+xml,' + encodeURIComponent(` - - - - - - - -`); - -// Known existing images (from public/images/posts/) -const EXISTING_IMAGES = new Set([ - '/images/posts/erp-integration-breuninger/cover.jpg', - '/images/posts/fullstack-development-timetracking/cover.jpg', - '/images/posts/rfid-automation/cover.jpg', - '/images/posts/automated-ad-creatives/cover.jpg', -]); - -// Blog image component with fallback -function BlogImage({ src, alt, fallback }: { src: string; alt: string; fallback: string }) { - const imageSrc = EXISTING_IMAGES.has(src) ? src : fallback; - - return ( - {alt} - ); -} - -function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) { - return ( - -
- {/* Image Container */} -
- -
-
- - {/* Content */} -
-
-
- - {getFormattedDate(post.date, locale)} -
- {post.tags && post.tags.length > 0 && ( -
- - {post.tags.length} Tags -
- )} -
- -

- {post.title} -

-

- {post.excerpt} -

- - {/* Tags */} -
- {post.tags?.slice(0, 3).map((tag, index) => ( - - {tag} - - ))} - {post.tags && post.tags.length > 3 && ( - - +{post.tags.length - 3} more - - )} -
- - {/* Link Icon */} -
- -
-
-
- - ); -} - -// Pagination component -function Pagination({ - currentPage, - totalPages, - locale, - t, -}: { - currentPage: number; - totalPages: number; - locale: string; - t: (key: string) => string; -}) { - const getPageNumbers = () => { - const pages: (number | 'ellipsis')[] = []; - - if (totalPages <= 7) { - // Show all pages if 7 or fewer - for (let i = 1; i <= totalPages; i++) { - pages.push(i); - } - } else { - // Always show first page - pages.push(1); - - if (currentPage > 3) { - pages.push('ellipsis'); - } - - // Show pages around current - const start = Math.max(2, currentPage - 1); - const end = Math.min(totalPages - 1, currentPage + 1); - - for (let i = start; i <= end; i++) { - pages.push(i); - } - - if (currentPage < totalPages - 2) { - pages.push('ellipsis'); - } - - // Always show last page - pages.push(totalPages); - } - - return pages; - }; - - if (totalPages <= 1) return null; - - return ( - - ); -} - -export default async function BlogPage({ params, searchParams }: Props) { +export default async function BlogPage({ params }: Props) { const { locale } = await params; - const { page } = await searchParams; setRequestLocale(locale); const t = await getTranslations('blog'); @@ -595,15 +323,6 @@ export default async function BlogPage({ params, searchParams }: Props) { ...legacyPosts, ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - // Pagination - const currentPage = Math.max(1, parseInt(page || '1', 10) || 1); - const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE); - const validPage = Math.min(currentPage, totalPages || 1); - - const startIndex = (validPage - 1) * POSTS_PER_PAGE; - const endIndex = startIndex + POSTS_PER_PAGE; - const posts = allPosts.slice(startIndex, endIndex); - return (
@@ -619,37 +338,21 @@ export default async function BlogPage({ params, searchParams }: Props) {

{t('meta.header.subtitle')}

- {/* Post count */} -

- {t('ui.pagination.showing', { - start: startIndex + 1, - end: Math.min(endIndex, allPosts.length), - total: allPosts.length, - })} -

- {/* Blog Posts Grid */} -
- {posts.map((post) => ( - - ))} -
- - {/* Pagination */} - t(key)} + translations={{ + searchPlaceholder: t('ui.search.placeholder'), + showingText: (start: number, end: number, total: number) => + t('ui.pagination.showing', { start, end, total }), + noPostsText: t('ui.errors.posts'), + paginationPrevious: t('ui.pagination.previous'), + paginationNext: t('ui.pagination.next'), + }} /> - - {/* Empty State */} - {posts.length === 0 && ( -
-

{t('ui.errors.posts')}

-
- )}
); diff --git a/src/components/blog/BlogList.tsx b/src/components/blog/BlogList.tsx new file mode 100644 index 0000000..381b537 --- /dev/null +++ b/src/components/blog/BlogList.tsx @@ -0,0 +1,403 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; +import Link from 'next/link'; +import Image from 'next/image'; +import { BlogPost } from '@/lib/blog'; +import { SearchBar } from './SearchBar'; +import { CategoryFilter } from './CategoryFilter'; + +const POSTS_PER_PAGE = 12; + +// Placeholder image for posts without cover images +const PLACEHOLDER_IMAGE = 'data:image/svg+xml,' + encodeURIComponent(` + + + + + + + +`); + +// Known existing images (from public/images/posts/) +const EXISTING_IMAGES = new Set([ + '/images/posts/erp-integration-breuninger/cover.jpg', + '/images/posts/fullstack-development-timetracking/cover.jpg', + '/images/posts/rfid-automation/cover.jpg', + '/images/posts/automated-ad-creatives/cover.jpg', +]); + +function getFormattedDate(date: string, locale: string) { + const languageMap: Record = { + de: 'de-DE', + en: 'en-US', + sr: 'sr-RS', + }; + + return new Date(date).toLocaleDateString(languageMap[locale] || 'de-DE', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); +} + +function getImagePath(coverImage: string) { + if (coverImage.startsWith('http')) { + return coverImage; + } + return coverImage.replace('/blog/', '/images/posts/'); +} + +// Blog image component with fallback +function BlogImage({ src, alt, fallback }: { src: string; alt: string; fallback: string }) { + const imageSrc = EXISTING_IMAGES.has(src) ? src : fallback; + + return ( + {alt} + ); +} + +function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) { + return ( + +
+ {/* Image Container */} +
+ +
+
+ + {/* Content */} +
+
+
+ + {getFormattedDate(post.date, locale)} +
+ {post.tags && post.tags.length > 0 && ( +
+ + {post.tags.length} Tags +
+ )} +
+ +

+ {post.title} +

+

+ {post.excerpt} +

+ + {/* Tags */} +
+ {post.tags?.slice(0, 3).map((tag, index) => ( + + {tag} + + ))} + {post.tags && post.tags.length > 3 && ( + + +{post.tags.length - 3} more + + )} +
+ + {/* Link Icon */} +
+ +
+
+
+ + ); +} + +// Pagination component +function Pagination({ + currentPage, + totalPages, + onPageChange, + t, +}: { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + t: (key: string) => string; +}) { + const getPageNumbers = () => { + const pages: (number | 'ellipsis')[] = []; + + if (totalPages <= 7) { + // Show all pages if 7 or fewer + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Always show first page + pages.push(1); + + if (currentPage > 3) { + pages.push('ellipsis'); + } + + // Show pages around current + const start = Math.max(2, currentPage - 1); + const end = Math.min(totalPages - 1, currentPage + 1); + + for (let i = start; i <= end; i++) { + pages.push(i); + } + + if (currentPage < totalPages - 2) { + pages.push('ellipsis'); + } + + // Always show last page + pages.push(totalPages); + } + + return pages; + }; + + if (totalPages <= 1) return null; + + return ( + + ); +} + +interface BlogListProps { + posts: BlogPost[]; + locale: string; + translations: { + searchPlaceholder: string; + showingText: (start: number, end: number, total: number) => string; + noPostsText: string; + paginationPrevious: string; + paginationNext: string; + }; +} + +export function BlogList({ posts, locale, translations }: BlogListProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + // Extract unique categories from all posts + const allCategories = useMemo(() => { + const categories = new Set(posts.map(post => post.category).filter(Boolean)); + return Array.from(categories).sort(); + }, [posts]); + + // Filter posts based on search query and category + const filteredPosts = useMemo(() => { + let filtered = posts; + + // Apply search filter (case-insensitive search in title, excerpt, and tags) + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter(post => { + const titleMatch = post.title.toLowerCase().includes(query); + const excerptMatch = post.excerpt.toLowerCase().includes(query); + const tagsMatch = post.tags?.some(tag => tag.toLowerCase().includes(query)) || false; + return titleMatch || excerptMatch || tagsMatch; + }); + } + + // Apply category filter + if (selectedCategory) { + filtered = filtered.filter(post => post.category === selectedCategory); + } + + return filtered; + }, [posts, searchQuery, selectedCategory]); + + // Reset to first page when filters change + useMemo(() => { + setCurrentPage(1); + }, [searchQuery, selectedCategory]); + + // Paginate filtered posts + const totalPages = Math.ceil(filteredPosts.length / POSTS_PER_PAGE); + const validPage = Math.min(Math.max(1, currentPage), totalPages || 1); + + const startIndex = (validPage - 1) * POSTS_PER_PAGE; + const endIndex = startIndex + POSTS_PER_PAGE; + const paginatedPosts = filteredPosts.slice(startIndex, endIndex); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + // Scroll to top of the page + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( +
+ {/* Search and Filter Section */} +
+ {/* Search Bar */} + + + {/* Category Filter */} + +
+ + {/* Results count */} +

+ {translations.showingText( + startIndex + 1, + Math.min(endIndex, filteredPosts.length), + filteredPosts.length + )} +

+ + {/* Blog Posts Grid */} + {paginatedPosts.length > 0 ? ( + <> +
+ {paginatedPosts.map((post) => ( + + ))} +
+ + {/* Pagination */} + { + if (key === 'ui.pagination.previous') return translations.paginationPrevious; + if (key === 'ui.pagination.next') return translations.paginationNext; + return ''; + }} + /> + + ) : ( + /* Empty State */ +
+

{translations.noPostsText}

+
+ )} +
+ ); +} diff --git a/src/messages/de.json b/src/messages/de.json index 0f68cc7..bf4479f 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -463,12 +463,15 @@ }, "ui": { "readMore": "Weiterlesen", + "search": { + "placeholder": "Beiträge durchsuchen..." + }, "loading": { "posts": "Blogbeiträge werden geladen...", "post": "Blogbeitrag wird geladen..." }, "errors": { - "posts": "Fehler beim Laden der Blogbeiträge", + "posts": "Keine Beiträge gefunden", "post": "Der gesuchte Blogbeitrag existiert nicht oder wurde verschoben." }, "backToBlog": "Zurück zum Blog", diff --git a/src/messages/en.json b/src/messages/en.json index 6ffe764..f462997 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -478,12 +478,15 @@ }, "ui": { "readMore": "Read More", + "search": { + "placeholder": "Search posts..." + }, "loading": { "posts": "Loading blog posts...", "post": "Loading blog post..." }, "errors": { - "posts": "Error loading blog posts", + "posts": "No posts found", "post": "The requested blog post does not exist or has been moved." }, "backToBlog": "Back to Blog", diff --git a/src/messages/sr.json b/src/messages/sr.json index ac5b343..9db01e4 100644 --- a/src/messages/sr.json +++ b/src/messages/sr.json @@ -485,12 +485,15 @@ }, "ui": { "readMore": "Procitaj vise", + "search": { + "placeholder": "Pretrazi postove..." + }, "loading": { "posts": "Ucitavanje blog postova...", "post": "Ucitavanje blog posta..." }, "errors": { - "posts": "Greska pri ucitavanju blog postova", + "posts": "Nema pronađenih postova", "post": "Trazeni blog post ne postoji ili je premesten." }, "backToBlog": "Nazad na Blog", From b89031576df7f90be1b62a590d2de20a86d5ac4f Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 04:03:05 +0100 Subject: [PATCH 4/5] auto-claude: subtask-1-4 - Add URL state management using searchParams and router.push --- src/app/[locale]/blog/page.tsx | 10 +++++++++- src/components/blog/BlogList.tsx | 32 ++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index 7bbd402..1adc7ff 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -7,6 +7,7 @@ import { BlogList } from '@/components/blog/BlogList'; type Props = { params: Promise<{ locale: string }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }; // Legacy blog posts with translations (these have manual translations) @@ -304,8 +305,9 @@ export async function generateMetadata({ params }: Props): Promise { } -export default async function BlogPage({ params }: Props) { +export default async function BlogPage({ params, searchParams }: Props) { const { locale } = await params; + const resolvedSearchParams = await searchParams; setRequestLocale(locale); const t = await getTranslations('blog'); @@ -323,6 +325,10 @@ export default async function BlogPage({ params }: Props) { ...legacyPosts, ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + // Extract search and category from URL params + const initialSearch = typeof resolvedSearchParams.search === 'string' ? resolvedSearchParams.search : ''; + const initialCategory = typeof resolvedSearchParams.category === 'string' ? resolvedSearchParams.category : null; + return (
@@ -344,6 +350,8 @@ export default async function BlogPage({ params }: Props) { diff --git a/src/components/blog/BlogList.tsx b/src/components/blog/BlogList.tsx index 381b537..40eedd6 100644 --- a/src/components/blog/BlogList.tsx +++ b/src/components/blog/BlogList.tsx @@ -1,9 +1,10 @@ 'use client'; -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; import Link from 'next/link'; import Image from 'next/image'; +import { useRouter, usePathname } from 'next/navigation'; import { BlogPost } from '@/lib/blog'; import { SearchBar } from './SearchBar'; import { CategoryFilter } from './CategoryFilter'; @@ -281,6 +282,8 @@ function Pagination({ interface BlogListProps { posts: BlogPost[]; locale: string; + initialSearch?: string; + initialCategory?: string | null; translations: { searchPlaceholder: string; showingText: (start: number, end: number, total: number) => string; @@ -290,11 +293,32 @@ interface BlogListProps { }; } -export function BlogList({ posts, locale, translations }: BlogListProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [selectedCategory, setSelectedCategory] = useState(null); +export function BlogList({ posts, locale, initialSearch = '', initialCategory = null, translations }: BlogListProps) { + const router = useRouter(); + const pathname = usePathname(); + + const [searchQuery, setSearchQuery] = useState(initialSearch); + const [selectedCategory, setSelectedCategory] = useState(initialCategory); const [currentPage, setCurrentPage] = useState(1); + // Update URL when filters change + useEffect(() => { + const params = new URLSearchParams(); + + if (searchQuery.trim()) { + params.set('search', searchQuery.trim()); + } + + if (selectedCategory) { + params.set('category', selectedCategory); + } + + const queryString = params.toString(); + const newUrl = queryString ? `${pathname}?${queryString}` : pathname; + + router.push(newUrl, { scroll: false }); + }, [searchQuery, selectedCategory, pathname, router]); + // Extract unique categories from all posts const allCategories = useMemo(() => { const categories = new Set(posts.map(post => post.category).filter(Boolean)); From 51e3be3a630b01d7b7ca3d3cc701510e6804b448 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 04:13:14 +0100 Subject: [PATCH 5/5] fix: remove auto-claude framework files from version control (qa-requested) Removed framework metadata files that should not be version controlled: - .auto-claude-security.json - .auto-claude-status - .claude_settings.json Updated .gitignore to exclude these files pattern-wide. Files removed from git tracking but preserved on disk. QA Fix Session: 1 Co-Authored-By: Claude Sonnet 4.5 --- .auto-claude-security.json | 227 ------------------------------------- .auto-claude-status | 25 ---- .claude_settings.json | 39 ------- .gitignore | 2 + 4 files changed, 2 insertions(+), 291 deletions(-) delete mode 100644 .auto-claude-security.json delete mode 100644 .auto-claude-status delete mode 100644 .claude_settings.json diff --git a/.auto-claude-security.json b/.auto-claude-security.json deleted file mode 100644 index eed60a2..0000000 --- a/.auto-claude-security.json +++ /dev/null @@ -1,227 +0,0 @@ -{ - "base_commands": [ - ".", - "[", - "[[", - "ag", - "awk", - "basename", - "bash", - "bc", - "break", - "cat", - "cd", - "chmod", - "clear", - "cmp", - "column", - "comm", - "command", - "continue", - "cp", - "curl", - "cut", - "date", - "df", - "diff", - "dig", - "dirname", - "du", - "echo", - "egrep", - "env", - "eval", - "exec", - "exit", - "expand", - "export", - "expr", - "false", - "fd", - "fgrep", - "file", - "find", - "fmt", - "fold", - "gawk", - "gh", - "git", - "grep", - "gunzip", - "gzip", - "head", - "help", - "host", - "iconv", - "id", - "jobs", - "join", - "jq", - "kill", - "killall", - "less", - "let", - "ln", - "ls", - "lsof", - "man", - "mkdir", - "mktemp", - "more", - "mv", - "nl", - "paste", - "pgrep", - "ping", - "pkill", - "popd", - "printenv", - "printf", - "ps", - "pushd", - "pwd", - "read", - "readlink", - "realpath", - "reset", - "return", - "rev", - "rg", - "rm", - "rmdir", - "sed", - "seq", - "set", - "sh", - "shuf", - "sleep", - "sort", - "source", - "split", - "stat", - "tail", - "tar", - "tee", - "test", - "time", - "timeout", - "touch", - "tr", - "tree", - "true", - "type", - "uname", - "unexpand", - "uniq", - "unset", - "unzip", - "watch", - "wc", - "wget", - "whereis", - "which", - "whoami", - "xargs", - "yes", - "yq", - "zip", - "zsh" - ], - "stack_commands": [ - "ar", - "clang", - "clang++", - "cmake", - "composer", - "dive", - "docker", - "docker-buildx", - "docker-compose", - "dockerfile", - "eslint", - "g++", - "gcc", - "ipython", - "jupyter", - "ld", - "make", - "meson", - "next", - "ninja", - "nm", - "node", - "notebook", - "npm", - "npx", - "objdump", - "pdb", - "php", - "pip", - "pip3", - "pipx", - "pnpm", - "pnpx", - "pudb", - "python", - "python3", - "react-scripts", - "strip", - "ts-node", - "tsc", - "tsx", - "vitest" - ], - "script_commands": [ - "bun", - "npm", - "pnpm", - "yarn" - ], - "custom_commands": [], - "detected_stack": { - "languages": [ - "python", - "javascript", - "typescript", - "php", - "c" - ], - "package_managers": [ - "pnpm" - ], - "frameworks": [ - "nextjs", - "react", - "vitest", - "eslint" - ], - "databases": [], - "infrastructure": [ - "docker" - ], - "cloud_providers": [], - "code_quality_tools": [], - "version_managers": [] - }, - "custom_scripts": { - "npm_scripts": [ - "dev", - "build", - "start", - "lint", - "build:images", - "generate:images", - "generate:images:dry", - "test", - "test:coverage" - ], - "make_targets": [], - "poetry_scripts": [], - "cargo_aliases": [], - "shell_scripts": [] - }, - "project_dir": "C:\\Users\\damja\\WebstormProjects\\Portfolio", - "created_at": "2026-01-22T15:28:38.237190", - "project_hash": "c4ad399e16be367eb4e6b076fe1d9ee3", - "inherited_from": "C:\\Users\\damja\\WebstormProjects\\Portfolio" -} \ No newline at end of file diff --git a/.auto-claude-status b/.auto-claude-status deleted file mode 100644 index 24bbc83..0000000 --- a/.auto-claude-status +++ /dev/null @@ -1,25 +0,0 @@ -{ - "active": true, - "spec": "003-add-blog-search-and-category-filter", - "state": "building", - "subtasks": { - "completed": 1, - "total": 4, - "in_progress": 1, - "failed": 0 - }, - "phase": { - "current": "Blog Search and Filter Implementation", - "id": null, - "total": 4 - }, - "workers": { - "active": 0, - "max": 1 - }, - "session": { - "number": 3, - "started_at": "2026-01-25T02:31:36.044848" - }, - "last_update": "2026-01-25T02:38:09.538183" -} \ No newline at end of file diff --git a/.claude_settings.json b/.claude_settings.json deleted file mode 100644 index bdadd0a..0000000 --- a/.claude_settings.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "sandbox": { - "enabled": true, - "autoAllowBashIfSandboxed": true - }, - "permissions": { - "defaultMode": "acceptEdits", - "allow": [ - "Read(./**)", - "Write(./**)", - "Edit(./**)", - "Glob(./**)", - "Grep(./**)", - "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", - "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", - "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", - "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", - "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter/**)", - "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter\\.auto-claude\\specs\\003-add-blog-search-and-category-filter/**)", - "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter\\.auto-claude\\specs\\003-add-blog-search-and-category-filter/**)", - "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\003-add-blog-search-and-category-filter\\.auto-claude\\specs\\003-add-blog-search-and-category-filter/**)", - "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", - "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", - "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", - "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", - "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", - "Bash(*)", - "WebFetch(*)", - "WebSearch(*)", - "mcp__context7__resolve-library-id(*)", - "mcp__context7__get-library-docs(*)", - "mcp__graphiti-memory__search_nodes(*)", - "mcp__graphiti-memory__search_facts(*)", - "mcp__graphiti-memory__add_episode(*)", - "mcp__graphiti-memory__get_episodes(*)", - "mcp__graphiti-memory__get_entity_edge(*)" - ] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index fa65296..ea02cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ source-images/ # Auto Claude data directory .auto-claude/ +.auto-claude-* +.claude_settings.json