Files
Portfolio/CONTRIBUTING.md
T
damjan_savicandClaude Sonnet 4.5 65f3a28435 auto-claude: subtask-1-4 - Document content workflows (portfolio projects and blog posts)
- Added comprehensive Content Workflows section to CONTRIBUTING.md
- Documented how to add portfolio projects with TypeScript data structure example
- Documented how to add blog posts using MDX format
- Documented i18n workflow for translations with JSON structure
- Added file location conventions for all locales (de, en, sr)
- Included step-by-step instructions and best practices

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-25 06:36:41 +01:00

27 KiB

Contributing to Portfolio

Thank you for your interest in contributing to this project! This guide will help you get started with development.

Table of Contents

Getting Started

Prerequisites

  • Node.js 18.x or higher
  • npm 9.x or higher
  • Git for version control

Installation

  1. Clone the repository

    git clone <repository-url>
    cd portfolio
    
  2. Install dependencies

    npm install
    
  3. Set up environment variables

    Create a .env.local file in the root directory with the required environment variables:

    # Supabase Configuration
    NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
    NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
    
    # Optional: OpenAI (for image generation scripts)
    OPENAI_API_KEY=your_openai_api_key
    
  4. Start the development server

    npm run dev
    

    The application will be available at http://localhost:3000

Available Scripts

Command Description
npm run dev Start development server with hot reload
npm run build Build production bundle
npm start Start production server (after build)
npm run lint Run ESLint with strict mode
npm test Run unit tests with Vitest
npm run test:coverage Run tests with coverage report
npm run build:images Optimize images using Sharp
npm run generate:images Generate blog images using OpenAI
npm run generate:images:dry Preview image generation without creating files

Project Overview

This is a personal portfolio website for Damjan Savić, showcasing work as an AI & Automation Specialist. The site features:

  • Multi-language support - German, English, and Serbian with automatic language detection
  • Progressive Web App - Installable and offline-capable
  • SEO optimized - Structured data (Schema.org), sitemap generation
  • Responsive Design - Mobile-first approach with Tailwind CSS
  • Blog System - MDX-based content with frontmatter support
  • Portfolio Gallery - Dynamic project showcase with filtering
  • Contact Form - Integrated with Supabase backend

Tech Stack

Core Framework

  • Next.js 15 - React framework with App Router
  • React 19 - UI library with latest features
  • TypeScript 5.5 - Type-safe development

Styling & UI

  • Tailwind CSS 3.4 - Utility-first CSS framework
  • Framer Motion 11 - Animation library
  • Lucide React - Icon library
  • Tailwind Merge - Utility for merging Tailwind classes
  • CVA (Class Variance Authority) - Component variants management

Content & Internationalization

  • MDX - Markdown + JSX for content pages
    • @mdx-js/loader, @mdx-js/mdx, @mdx-js/react
    • @next/mdx, next-mdx-remote
  • next-intl 3.26 - Internationalization (i18n)
  • gray-matter - Frontmatter parsing

Backend & Services

  • Supabase - Database, authentication, and backend services
    • @supabase/supabase-js
    • @supabase/ssr
  • OpenAI 4.77 - AI integration for content generation

Development Tools

  • Vitest - Unit testing framework
  • ESLint 9 - Code linting with Next.js config
  • PostCSS - CSS processing
  • Autoprefixer - CSS vendor prefixes
  • Sharp - Image optimization

Performance & Analytics

  • web-vitals - Performance metrics
  • react-intersection-observer - Lazy loading and scroll animations

Project Structure

portfolio/
├── public/                    # Static assets (images, fonts, etc.)
├── scripts/                   # Build and utility scripts
│   ├── optimize-images.js     # Image optimization
│   └── generate-blog-images.mjs # AI-powered image generation
├── src/
│   ├── app/                   # Next.js App Router
│   │   ├── [locale]/          # Internationalized routes
│   │   │   ├── about/         # About page
│   │   │   ├── blog/          # Blog listing & posts
│   │   │   ├── contact/       # Contact form
│   │   │   ├── dashboard/     # Admin dashboard
│   │   │   ├── imprint/       # Legal imprint
│   │   │   ├── leistungen/    # Services page
│   │   │   ├── login/         # Authentication
│   │   │   ├── portfolio/     # Portfolio gallery & projects
│   │   │   ├── privacy/       # Privacy policy
│   │   │   ├── terms/         # Terms of service
│   │   │   ├── layout.tsx     # Locale-specific layout
│   │   │   └── page.tsx       # Homepage
│   │   ├── globals.css        # Global styles
│   │   ├── layout.tsx         # Root layout
│   │   ├── not-found.tsx      # 404 page
│   │   ├── robots.ts          # Robots.txt generation
│   │   └── sitemap.ts         # Sitemap generation
│   ├── components/            # Reusable React components
│   ├── components-vite/       # Legacy Vite components
│   ├── data/                  # Static data and content
│   ├── hooks/                 # Custom React hooks
│   ├── i18n/                  # Internationalization config
│   ├── lib/                   # Utility libraries and helpers
│   ├── messages/              # Translation files (de, en, sr)
│   ├── middleware.ts          # Next.js middleware (i18n routing)
│   ├── pages-vite/            # Legacy Vite pages
│   ├── services/              # API and service integrations
│   ├── styles/                # Additional styles
│   ├── types/                 # TypeScript type definitions
│   └── utils/                 # Helper functions
├── .env.local                 # Environment variables (not in git)
├── .eslintrc.json             # ESLint configuration
├── docker-compose.yml         # Docker Compose setup
├── Dockerfile                 # Docker container config
├── next.config.ts             # Next.js configuration
├── package.json               # Dependencies and scripts
├── postcss.config.js          # PostCSS configuration
├── tailwind.config.js         # Tailwind CSS configuration
└── tsconfig.json              # TypeScript configuration

Key Directories Explained

  • src/app/ - Next.js App Router pages and layouts using the new file-based routing system
  • src/app/[locale]/ - All pages are nested under locale for multi-language support (de, en, sr)
  • src/components/ - Reusable UI components (buttons, cards, forms, navigation, etc.)
  • src/hooks/ - Custom React hooks for shared logic
  • src/lib/ - Utility libraries (Supabase client, helpers, etc.)
  • src/messages/ - Translation JSON files for each language
  • src/services/ - API integrations and external service wrappers
  • src/types/ - TypeScript interfaces and type definitions
  • src/utils/ - Helper functions and utilities
  • public/ - Static files served directly (images, fonts, favicons)
  • scripts/ - Build automation and utility scripts

Development Workflow

1. Create a New Feature Branch

git checkout -b feature/your-feature-name

2. Make Your Changes

  • Follow the existing code patterns and structure
  • Use TypeScript for type safety
  • Write clean, readable code with appropriate comments
  • Test your changes locally

3. Test Your Changes

# Run development server
npm run dev

# Run linter
npm run lint

# Run tests
npm test

4. Build for Production

npm run build

Ensure the build completes without errors before submitting.

Content Workflows

This section covers how to add and manage content for portfolio projects, blog posts, and translations. The project supports multi-language content in German (de), English (en), and Serbian (sr).

Adding Portfolio Projects

Portfolio projects are defined as TypeScript files with structured metadata and content. Each project must be created for all supported locales.

File Location Conventions

Portfolio project files follow this structure:

src/i18n/locales-old/
├── de/portfolio/projects/       # German portfolio projects
│   └── my-project.ts
├── en/portfolio/projects/       # English portfolio projects
│   └── my-project.ts
└── sr/portfolio/projects/       # Serbian portfolio projects
    └── my-project.ts

Important: Use the same filename (slug) across all locales to maintain consistency.

TypeScript Data Structure

Each portfolio project file must export an object with the following structure:

// src/i18n/locales-old/de/portfolio/projects/my-project.ts
export const myProject = {
  meta: {
    slug: 'my-project',                    // Unique identifier (same across locales)
    title: 'Project Title | Damjan Savić', // SEO-optimized title
    description: 'Brief project description for meta tags',
    excerpt: 'Short excerpt for project cards',
    date: '2025-01',                       // Publication date (YYYY-MM format)
    category: 'Category Name',             // e.g., "Web Development", "Data Processing"
    client: 'Client Name',                 // Client or organization
    duration: '2 Monate',                  // Project duration
    url: 'https://project-url.com',        // Live project URL (optional)
    repository: '',                        // GitHub repository URL (optional)
    documentation: '',                     // Documentation URL (optional)
    published: true,                       // Visibility flag
    featured: true,                        // Featured on homepage
    technologies: [                        // Technology stack
      'TypeScript',
      'React',
      'Next.js'
    ],
    tags: [                                // Project tags for filtering
      'Web Development',
      'Full Stack',
      'SEO'
    ]
  },
  content: {
    intro: 'Introduction paragraph describing the project overview.',

    challenge: {
      title: 'The Challenge',
      description: 'Brief description of the problem to solve.',
      points: [
        'Challenge point 1',
        'Challenge point 2',
        'Challenge point 3'
      ]
    },

    solution: {
      title: 'The Solution',
      description: 'How the problem was approached.',
      content: 'Detailed explanation of the solution.',
      points: [
        'Solution feature 1',
        'Solution feature 2',
        'Solution feature 3'
      ]
    },

    technical: {
      title: 'Technical Implementation',
      description: 'Technical approach and architecture.',
      points: [
        'Technical detail 1',
        'Technical detail 2',
        'Technical detail 3'
      ],
      code: `// Optional code snippet to showcase implementation
export function exampleFunction() {
  return 'Example code';
}`
    },

    implementation: {
      title: 'Implementation Process',
      description: 'How the project was built.',
      points: [
        'Implementation step 1',
        'Implementation step 2',
        'Implementation step 3'
      ]
    },

    results: {
      title: 'Results and Impact',
      description: 'Measurable outcomes and business impact.',
      points: [
        'Result metric 1',
        'Result metric 2',
        'Result metric 3'
      ]
    },

    conclusion: 'Final thoughts and project summary.'
  }
};

// Re-export for compatibility with the project import system
export default myProject;

Step-by-Step: Adding a New Portfolio Project

  1. Create the TypeScript file for each locale:

    # German version
    touch src/i18n/locales-old/de/portfolio/projects/my-project.ts
    
    # English version
    touch src/i18n/locales-old/en/portfolio/projects/my-project.ts
    
    # Serbian version
    touch src/i18n/locales-old/sr/portfolio/projects/my-project.ts
    
  2. Copy the template structure above into each file

  3. Translate the content for each locale while keeping the same structure

  4. Add project images (if any) to public/images/portfolio/my-project/

  5. Test the project by navigating to:

    • German: http://localhost:3000/de/portfolio/my-project
    • English: http://localhost:3000/en/portfolio/my-project
    • Serbian: http://localhost:3000/sr/portfolio/my-project
  6. Verify SEO metadata using browser DevTools to check meta tags

Adding Blog Posts

Blog posts use MDX (Markdown + JSX) format with frontmatter metadata. MDX allows you to embed React components directly in your content.

File Location Conventions

Blog posts are organized by locale:

src/content/blog/
├── de/                          # German blog posts
│   └── my-blog-post.mdx
├── en/                          # English blog posts
│   └── my-blog-post.mdx
└── sr/                          # Serbian blog posts
    └── my-blog-post.mdx

MDX File Structure

Each blog post must include frontmatter metadata at the top:

---
title: "Blog Post Title"
description: "Brief description for SEO and post previews"
date: "2025-01-20"
author: "Damjan Savić"
category: "Technology"
tags: ["React", "Next.js", "TypeScript"]
image: "/images/blog/my-blog-post/hero.jpg"
published: true
featured: false
excerpt: "A short excerpt that appears in post listings"
---

# Blog Post Title

Your content starts here. You can use standard Markdown syntax:

## Headings

Regular **bold** and *italic* text.

### Code Blocks

```typescript
// Code examples with syntax highlighting
const greeting = "Hello, World!";
console.log(greeting);

Lists

  • Bullet point 1
  • Bullet point 2
  • Bullet point 3

Images

Alt text

Custom Components (MDX Feature)

You can embed React components:

This is a custom component in MDX! ```

Step-by-Step: Adding a New Blog Post

  1. Create the MDX file for each locale:

    # German version
    touch src/content/blog/de/my-blog-post.mdx
    
    # English version
    touch src/content/blog/en/my-blog-post.mdx
    
    # Serbian version
    touch src/content/blog/sr/my-blog-post.mdx
    
  2. Add frontmatter metadata with all required fields

  3. Write the content using Markdown/MDX syntax

  4. Add images to public/images/blog/my-blog-post/

  5. Generate blog images (optional) using the AI-powered script:

    # Preview image generation
    npm run generate:images:dry
    
    # Generate actual images
    npm run generate:images
    
  6. Test the blog post by navigating to:

    • German: http://localhost:3000/de/blog/my-blog-post
    • English: http://localhost:3000/en/blog/my-blog-post
    • Serbian: http://localhost:3000/sr/blog/my-blog-post

Internationalization (i18n) Workflow

The project uses next-intl for internationalization with JSON-based translation files.

Translation File Structure

Translation files are located in src/messages/ and organized by locale:

src/messages/
├── de.json          # German translations
├── en.json          # English translations
└── sr.json          # Serbian translations

Translation File Format

Each translation file is a nested JSON object organized by page or section:

{
  "meta": {
    "site": {
      "name": "Damjan Savić",
      "title": "Damjan Savić | Fullstack Developer",
      "description": "Portfolio website description"
    }
  },
  "common": {
    "nav": {
      "home": "Home",
      "about": "About",
      "portfolio": "Portfolio",
      "blog": "Blog",
      "contact": "Contact"
    },
    "actions": {
      "readMore": "Read More",
      "viewMore": "View More",
      "back": "Back",
      "close": "Close"
    }
  },
  "pages": {
    "home": {
      "hero": {
        "title": "FULLSTACK DEVELOPER",
        "subtitle": "AI Agents | Voice AI | Process Automation"
      }
    }
  }
}

Step-by-Step: Adding Translations

  1. Add the translation key to src/messages/de.json (German - primary language):

    {
      "pages": {
        "contact": {
          "form": {
            "title": "Kontakt aufnehmen",
            "name": "Name",
            "email": "E-Mail",
            "message": "Nachricht",
            "submit": "Absenden"
          }
        }
      }
    }
    
  2. Add the same structure to src/messages/en.json (English):

    {
      "pages": {
        "contact": {
          "form": {
            "title": "Get in Touch",
            "name": "Name",
            "email": "Email",
            "message": "Message",
            "submit": "Submit"
          }
        }
      }
    }
    
  3. Add the same structure to src/messages/sr.json (Serbian):

    {
      "pages": {
        "contact": {
          "form": {
            "title": "Kontaktirajte nas",
            "name": "Ime",
            "email": "Email",
            "message": "Poruka",
            "submit": "Pošalji"
          }
        }
      }
    }
    
  4. Use the translation in your component:

    import { useTranslations } from 'next-intl';
    
    export function ContactForm() {
      const t = useTranslations('pages.contact.form');
    
      return (
        <form>
          <h2>{t('title')}</h2>
          <input placeholder={t('name')} />
          <input placeholder={t('email')} />
          <textarea placeholder={t('message')} />
          <button>{t('submit')}</button>
        </form>
      );
    }
    

Translation Best Practices

  1. Maintain structure consistency - Use the same JSON structure across all locale files
  2. Use semantic keys - Keys should describe the content, not the translation (e.g., actions.submit not actions.submitButton)
  3. Organize by feature - Group related translations under feature namespaces
  4. Test all locales - Always verify translations in all three languages (de, en, sr)
  5. Avoid hardcoded text - All user-facing text should come from translation files
  6. Keep translations concise - Match the tone and length of the original language when possible

Locale-Specific File Naming

When creating locale-specific content files, follow these conventions:

Content Type File Location Naming Pattern Example
Portfolio Projects src/i18n/locales-old/[locale]/portfolio/projects/ project-slug.ts ai-data-reader.ts
Blog Posts src/content/blog/[locale]/ post-slug.mdx react-best-practices.mdx
Translations src/messages/ [locale].json de.json
Page Content src/app/[locale]/[page]/ Standard Next.js routing page.tsx

Switching Between Locales

The application automatically detects the user's locale based on:

  1. URL path segment (e.g., /de/, /en/, /sr/)
  2. Browser language settings
  3. User preference stored in cookies

Users can manually switch languages using the language selector in the navigation menu.

Code Style & Standards

ESLint & Code Linting

This project uses ESLint to enforce code quality and consistency. The configuration extends Next.js recommended presets:

ESLint Configuration (.eslintrc.json):

{
  "extends": [
    "next/core-web-vitals",
    "next/typescript"
  ]
}

Key ESLint Rules:

  • next/core-web-vitals - Enforces Next.js best practices and Core Web Vitals optimizations
  • next/typescript - TypeScript-specific rules for Next.js applications

Running the Linter:

# Run ESLint in strict mode (recommended before committing)
npm run lint

# The linter will check for:
# - Code quality issues
# - Next.js best practices violations
# - TypeScript type errors
# - Unused variables and imports
# - Accessibility issues

Important:

  • Always run npm run lint before committing changes
  • Fix all linting errors - the project uses --strict mode
  • The linter runs automatically during the build process
  • ESLint errors will prevent successful production builds

Code Formatting

General Formatting Guidelines:

  • Indentation: 2 spaces (no tabs)
  • Line Length: Aim for 80-100 characters, hard limit at 120
  • Semicolons: Required at the end of statements
  • Quotes: Single quotes for strings (except in JSX/TSX where double quotes are preferred)
  • Trailing Commas: Use trailing commas in multi-line objects and arrays

Example:

// ✅ Good
const user = {
  name: 'John Doe',
  email: 'john@example.com',
  role: 'admin',
};

// ❌ Bad
const user = {
  name: "John Doe",
  email: "john@example.com",
  role: "admin"
}

TypeScript

  • Use TypeScript for all new files
  • Define proper types and interfaces
  • Avoid using any - use proper typing or unknown
  • Export types from a central types/ directory when shared

React Components

  • Use functional components with hooks
  • Follow the component structure:
    1. Imports
    2. Type definitions
    3. Component definition
    4. Helper functions (if needed)
    5. Exports

Example:

import { useState } from 'react';
import { Button } from '@/components/ui/Button';

interface MyComponentProps {
  title: string;
  onSubmit: () => void;
}

export function MyComponent({ title, onSubmit }: MyComponentProps) {
  const [isLoading, setIsLoading] = useState(false);

  const handleClick = async () => {
    setIsLoading(true);
    await onSubmit();
    setIsLoading(false);
  };

  return (
    <div>
      <h2>{title}</h2>
      <Button onClick={handleClick} disabled={isLoading}>
        Submit
      </Button>
    </div>
  );
}

Styling

  • Use Tailwind CSS utility classes
  • Use cn() utility from tailwind-merge to merge classes
  • Follow the mobile-first responsive design approach
  • Use CSS variables for theme colors (defined in globals.css)

File Naming

  • Components: PascalCase (e.g., MyComponent.tsx)
  • Utilities: camelCase (e.g., formatDate.ts)
  • Pages: lowercase (e.g., page.tsx, layout.tsx)
  • Types: PascalCase with .types.ts suffix (e.g., User.types.ts)

Import Organization

// 1. External dependencies
import { useState } from 'react';
import { useTranslations } from 'next-intl';

// 2. Internal components and utilities
import { Button } from '@/components/ui/Button';
import { formatDate } from '@/utils/date';

// 3. Types
import type { User } from '@/types/User.types';

// 4. Styles (if any)
import styles from './Component.module.css';

Internationalization

  • All user-facing text must be internationalized
  • Use useTranslations() hook from next-intl
  • Add translation keys to all language files in src/messages/

Example:

import { useTranslations } from 'next-intl';

export function WelcomeMessage() {
  const t = useTranslations('home');

  return <h1>{t('welcome')}</h1>;
}

Testing

This project uses Vitest as its testing framework. Vitest is a fast, modern test runner built for Vite-based projects, offering:

  • Lightning-fast execution with native ESM support
  • Jest-compatible API for easy migration and familiarity
  • First-class TypeScript support
  • Built-in coverage reporting with c8

Current State: The project is fully configured with Vitest, but no tests currently exist. We encourage contributors to add tests for new features and gradually improve test coverage.

Test Setup

Vitest is already configured in package.json:

{
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest run --coverage"
  },
  "devDependencies": {
    "vitest": "^1.3.1"
  }
}

Running Tests

# Run tests in watch mode (recommended during development)
npm test

# Run tests once (useful for CI/CD)
npm run test:coverage

# The test runner will:
# - Automatically detect .test.ts, .test.tsx, .spec.ts, .spec.tsx files
# - Re-run tests when files change (in watch mode)
# - Display coverage reports (with --coverage flag)

Writing Tests

When adding tests to this project, follow these conventions:

1. File Naming & Location

  • Place test files next to the code they test
  • Use .test.ts or .test.tsx extension for test files
  • Match the filename of the file being tested

Example:

src/utils/
├── formatDate.ts          # Source file
└── formatDate.test.ts     # Test file

2. Test Structure

  • Use describe blocks to group related tests
  • Use it or test for individual test cases
  • Write descriptive test names that explain the expected behavior

Example:

// src/utils/formatDate.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate } from './formatDate';

describe('formatDate', () => {
  it('formats date in DD.MM.YYYY format', () => {
    const date = new Date('2024-01-15');
    expect(formatDate(date)).toBe('15.01.2024');
  });

  it('handles invalid dates gracefully', () => {
    const invalidDate = new Date('invalid');
    expect(formatDate(invalidDate)).toBe('Invalid Date');
  });
});

3. Component Testing

For React components, use Vitest with React Testing Library patterns:

// src/components/Button.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });
});

4. What to Test

  • Utility functions - Pure functions and helpers
  • Business logic - Complex calculations and transformations
  • Hooks - Custom React hooks with multiple states
  • Components - User interactions and rendering logic
  • API integrations - Service layer functions (use mocks)

5. Testing Conventions

  • Arrange-Act-Assert pattern for test structure
  • One assertion per test when possible (for clarity)
  • Mock external dependencies (API calls, database, etc.)
  • Test edge cases and error conditions
  • Use meaningful test data that reflects real usage

Example:

describe('calculateDiscount', () => {
  it('applies 10% discount for orders over $100', () => {
    // Arrange
    const orderTotal = 150;
    const discountThreshold = 100;

    // Act
    const result = calculateDiscount(orderTotal, discountThreshold);

    // Assert
    expect(result).toBe(135); // 150 - 15 (10%)
  });
});

Test Coverage Goals

While we don't enforce strict coverage requirements, aim for:

  • 80%+ coverage for utility functions and business logic
  • 60%+ coverage for components
  • 100% coverage for critical paths (authentication, payments, data validation)

Run npm run test:coverage to see current coverage reports.

Commit Guidelines

Commit Message Format

<type>: <subject>

<body>

Types

  • feat - New feature
  • fix - Bug fix
  • docs - Documentation changes
  • style - Code style changes (formatting, etc.)
  • refactor - Code refactoring
  • test - Adding or updating tests
  • chore - Maintenance tasks

Examples

git commit -m "feat: add contact form validation"
git commit -m "fix: resolve mobile navigation menu bug"
git commit -m "docs: update README with deployment instructions"

Questions?

If you have any questions or need help, please:

  • Check existing documentation
  • Review similar code in the codebase
  • Open an issue for discussion

Built with Next.js + TypeScript + Tailwind CSS