Merge pull request #25 from damjan1996/auto-claude/006-add-portfolio-category-filter

auto-claude: 006-add-portfolio-category-filter
This commit is contained in:
Damjan Savic
2026-01-25 19:46:40 +01:00
committed by GitHub
6 changed files with 104 additions and 15 deletions
+14 -3
View File
@@ -1,10 +1,11 @@
import { setRequestLocale } from 'next-intl/server'; import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { PortfolioGrid } from '@/components/portfolio'; import { PortfolioGrid, CategoryFilter } from '@/components/portfolio';
type Props = { type Props = {
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
searchParams: Promise<{ category?: string }>;
}; };
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com'; const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
@@ -159,12 +160,20 @@ const projects = [
}, },
]; ];
export default async function PortfolioPage({ params }: Props) { export default async function PortfolioPage({ params, searchParams }: Props) {
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); setRequestLocale(locale);
const t = await getTranslations('portfolio'); const t = await getTranslations('portfolio');
// Get category filter from URL params
const { category } = await searchParams;
// Filter projects by category if specified
const filteredProjects = category
? projects.filter((project) => project.category === category)
: projects;
return ( return (
<div className="relative min-h-screen"> <div className="relative min-h-screen">
<section className="py-24"> <section className="py-24">
@@ -178,7 +187,9 @@ export default async function PortfolioPage({ params }: Props) {
</p> </p>
</div> </div>
<PortfolioGrid projects={projects} /> <CategoryFilter />
<PortfolioGrid projects={filteredProjects} />
</div> </div>
</section> </section>
</div> </div>
@@ -0,0 +1,74 @@
'use client';
import { useTranslations } from 'next-intl';
import { useRouter, useSearchParams } from 'next/navigation';
const categories = [
'AI Development',
'IoT',
'Full-Stack',
'Enterprise Software',
'E-Commerce',
'Developer Tools',
] as const;
export function CategoryFilter() {
const t = useTranslations('portfolio.filters');
const router = useRouter();
const searchParams = useSearchParams();
const selectedCategory = searchParams.get('category');
const handleCategoryClick = (category: string | null) => {
if (category === null) {
// Remove category param to show all
router.push(window.location.pathname);
} else {
// Set category param
const params = new URLSearchParams(searchParams.toString());
params.set('category', category);
router.push(`${window.location.pathname}?${params.toString()}`);
}
};
return (
<div className="mb-12">
<div className="flex items-center gap-3 overflow-x-auto pb-4 scrollbar-hide">
{/* All button */}
<button
onClick={() => handleCategoryClick(null)}
className={`
relative whitespace-nowrap rounded-full py-2 px-4
transition-all duration-200 ease-in-out text-sm tracking-wide
${!selectedCategory
? 'text-white bg-zinc-700/60'
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/30'
}
`}
>
{t('all')}
</button>
{/* Category buttons */}
{categories.map((category) => {
const isActive = selectedCategory === category;
return (
<button
key={category}
onClick={() => handleCategoryClick(category)}
className={`
relative whitespace-nowrap rounded-full py-2 px-4
transition-all duration-200 ease-in-out text-sm tracking-wide
${isActive
? 'text-white bg-zinc-700/60'
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/30'
}
`}
>
{t(`categories.${category}`)}
</button>
);
})}
</div>
</div>
);
}
+1
View File
@@ -1,2 +1,3 @@
export { CategoryFilter } from './CategoryFilter';
export { default as PortfolioCard } from './PortfolioCard'; export { default as PortfolioCard } from './PortfolioCard';
export { default as PortfolioGrid } from './PortfolioGrid'; export { default as PortfolioGrid } from './PortfolioGrid';
+5 -4
View File
@@ -450,11 +450,12 @@
"filters": { "filters": {
"all": "Alle", "all": "Alle",
"categories": { "categories": {
"Data Science": "Data Science",
"AI Development": "KI-Entwicklung", "AI Development": "KI-Entwicklung",
"Integration": "Integration", "IoT": "IoT",
"Full-Stack Development": "Full-Stack Entwicklung", "Full-Stack": "Full-Stack",
"Data Processing": "Datenverarbeitung" "Enterprise Software": "Unternehmenssoftware",
"E-Commerce": "E-Commerce",
"Developer Tools": "Entwickler-Tools"
} }
}, },
"ui": { "ui": {
+5 -4
View File
@@ -465,11 +465,12 @@
"filters": { "filters": {
"all": "All", "all": "All",
"categories": { "categories": {
"Data Science": "Data Science",
"AI Development": "AI Development", "AI Development": "AI Development",
"Integration": "Integration", "IoT": "IoT",
"Full-Stack Development": "Full-Stack Development", "Full-Stack": "Full-Stack",
"Data Processing": "Data Processing" "Enterprise Software": "Enterprise Software",
"E-Commerce": "E-Commerce",
"Developer Tools": "Developer Tools"
} }
}, },
"ui": { "ui": {
+5 -4
View File
@@ -472,11 +472,12 @@
"filters": { "filters": {
"all": "Sve", "all": "Sve",
"categories": { "categories": {
"Data Science": "Data Science",
"AI Development": "AI Razvoj", "AI Development": "AI Razvoj",
"Integration": "Integracija", "IoT": "IoT",
"Full-Stack Development": "Full-Stack Razvoj", "Full-Stack": "Full-Stack",
"Data Processing": "Obrada podataka" "Enterprise Software": "Korporativni Softver",
"E-Commerce": "E-trgovina",
"Developer Tools": "Alati za Razvoj"
} }
}, },
"ui": { "ui": {