From 7bf3aa20ce846fabe8d86470971bac1feb08a9f5 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 02:32:33 +0100 Subject: [PATCH 1/3] auto-claude: subtask-1-1 - Add readingTime field to BlogPost interface and im --- .auto-claude-security.json | 227 +++++++++++++++++++++++++++++++++++++ .auto-claude-status | 25 ++++ .claude_settings.json | 39 +++++++ .gitignore | 5 +- src/lib/blog.ts | 25 ++++ 5 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 .auto-claude-security.json create mode 100644 .auto-claude-status create mode 100644 .claude_settings.json 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..b40a373 --- /dev/null +++ b/.auto-claude-status @@ -0,0 +1,25 @@ +{ + "active": true, + "spec": "005-add-reading-time-to-blog-posts", + "state": "planning", + "subtasks": { + "completed": 0, + "total": 0, + "in_progress": 1, + "failed": 0 + }, + "phase": { + "current": "Add Reading Time Feature", + "id": null, + "total": 3 + }, + "workers": { + "active": 0, + "max": 1 + }, + "session": { + "number": 2, + "started_at": "2026-01-25T02:27:41.262893" + }, + "last_update": "2026-01-25T02:31:24.387608" +} \ No newline at end of file diff --git a/.claude_settings.json b/.claude_settings.json new file mode 100644 index 0000000..68b608c --- /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\\005-add-reading-time-to-blog-posts/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\005-add-reading-time-to-blog-posts/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\005-add-reading-time-to-blog-posts/**)", + "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\005-add-reading-time-to-blog-posts/**)", + "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\005-add-reading-time-to-blog-posts/**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\005-add-reading-time-to-blog-posts\\.auto-claude\\specs\\005-add-reading-time-to-blog-posts/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\005-add-reading-time-to-blog-posts\\.auto-claude\\specs\\005-add-reading-time-to-blog-posts/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\005-add-reading-time-to-blog-posts\\.auto-claude\\specs\\005-add-reading-time-to-blog-posts/**)", + "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/lib/blog.ts b/src/lib/blog.ts index 567ae70..3ddf1dc 100644 --- a/src/lib/blog.ts +++ b/src/lib/blog.ts @@ -10,6 +10,7 @@ export interface BlogPost { category?: string; tags?: string[]; content?: string; + readingTime?: number; } const BLOG_POSTS_DIR = path.join(process.cwd(), 'blog-posts'); @@ -70,6 +71,26 @@ function detectCategory(filename: string, content: string): string { return 'Technologie'; } +function calculateReadingTime(content: string): number { + // Remove markdown syntax for more accurate word count + const text = content + .replace(/```[\s\S]*?```/g, '') // Remove code blocks + .replace(/`[^`]*`/g, '') // Remove inline code + .replace(/!\[.*?\]\(.*?\)/g, '') // Remove images + .replace(/\[.*?\]\(.*?\)/g, '') // Remove links + .replace(/[#*_~]/g, '') // Remove markdown formatting + .replace(/\s+/g, ' ') // Normalize whitespace + .trim(); + + // Count words + const words = text.split(/\s+/).filter(word => word.length > 0).length; + + // Calculate reading time (average reading speed: 200 words per minute) + const minutes = Math.ceil(words / 200); + + return Math.max(1, minutes); // Minimum 1 minute +} + function parseMarkdownPost(filename: string, content: string): BlogPost | null { try { // Extract title from first H1 @@ -113,6 +134,9 @@ function parseMarkdownPost(filename: string, content: string): BlogPost | null { // Cover image path const coverImage = `/images/posts/${slug}/cover.jpg`; + // Calculate reading time + const readingTime = calculateReadingTime(content); + return { slug, title, @@ -122,6 +146,7 @@ function parseMarkdownPost(filename: string, content: string): BlogPost | null { category, tags, content, + readingTime, }; } catch (error) { console.error(`Error parsing ${filename}:`, error); From e749dce0a6121359361c72b6119084825a901db0 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 02:35:47 +0100 Subject: [PATCH 2/3] auto-claude: subtask-1-2 - Display reading time on blog listing page (BlogPostCard) Added Clock icon import and reading time display to BlogPostCard component. The reading time now appears between the date and tags with the format "X min read". Co-Authored-By: Claude Sonnet 4.5 --- src/app/[locale]/blog/page.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index 1b33fbc..7c7200f 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -1,6 +1,6 @@ import { setRequestLocale } from 'next-intl/server'; import { getTranslations } from 'next-intl/server'; -import { ArrowRight, Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; +import { ArrowRight, Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight, Clock } from 'lucide-react'; import Link from 'next/link'; import Image from 'next/image'; import type { Metadata } from 'next'; @@ -392,6 +392,12 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) { {getFormattedDate(post.date, locale)} + {post.readingTime && ( +
+ + {post.readingTime} min read +
+ )} {post.tags && post.tags.length > 0 && (
From 7fa92a04b49e449e20daa58afe34ec3f10654ed3 Mon Sep 17 00:00:00 2001 From: Damjan Savic Date: Sun, 25 Jan 2026 02:38:16 +0100 Subject: [PATCH 3/3] auto-claude: subtask-1-3 - Display reading time on blog post detail page - Import Clock icon from lucide-react - Export calculateReadingTime function from blog.ts - Calculate reading time for post content - Display reading time in post header with Clock icon - Format matches listing page: "X min read" Co-Authored-By: Claude Sonnet 4.5 --- src/app/[locale]/blog/[slug]/page.tsx | 10 +++++++++- src/lib/blog.ts | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index 3b3dd93..ae1256d 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -5,8 +5,9 @@ import Link from 'next/link'; import Image from 'next/image'; import type { Metadata } from 'next'; import { BreadcrumbJsonLd, ArticleJsonLd } from '@/components/seo'; -import { getBlogPostBySlug, getBlogPostSlugs } from '@/lib/blog'; +import { getBlogPostBySlug, getBlogPostSlugs, calculateReadingTime } from '@/lib/blog'; import { renderMarkdown } from '@/lib/markdown'; +import { Clock } from 'lucide-react'; type Props = { params: Promise<{ locale: string; slug: string }>; @@ -1315,6 +1316,9 @@ export default async function BlogPostPage({ params }: Props) { { name: post.title, url: `/${locale}/blog/${slug}` }, ]; + // Calculate reading time + const readingTime = post.content ? calculateReadingTime(post.content) : 1; + return (
@@ -1343,6 +1347,10 @@ export default async function BlogPostPage({ params }: Props) {
+
+ + {readingTime} min read +
{post.category} diff --git a/src/lib/blog.ts b/src/lib/blog.ts index 3ddf1dc..733590a 100644 --- a/src/lib/blog.ts +++ b/src/lib/blog.ts @@ -71,7 +71,7 @@ function detectCategory(filename: string, content: string): string { return 'Technologie'; } -function calculateReadingTime(content: string): number { +export function calculateReadingTime(content: string): number { // Remove markdown syntax for more accurate word count const text = content .replace(/```[\s\S]*?```/g, '') // Remove code blocks