- 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>
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
- Project Overview
- Tech Stack
- Project Structure
- Development Workflow
- Content Workflows
- Code Style & Standards
- Testing
- Commit Guidelines
Getting Started
Prerequisites
- Node.js 18.x or higher
- npm 9.x or higher
- Git for version control
Installation
-
Clone the repository
git clone <repository-url> cd portfolio -
Install dependencies
npm install -
Set up environment variables
Create a
.env.localfile 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 -
Start the development server
npm run devThe 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 systemsrc/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 logicsrc/lib/- Utility libraries (Supabase client, helpers, etc.)src/messages/- Translation JSON files for each languagesrc/services/- API integrations and external service wrapperssrc/types/- TypeScript interfaces and type definitionssrc/utils/- Helper functions and utilitiespublic/- 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
-
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 -
Copy the template structure above into each file
-
Translate the content for each locale while keeping the same structure
-
Add project images (if any) to
public/images/portfolio/my-project/ -
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
- German:
-
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
Custom Components (MDX Feature)
You can embed React components:
This is a custom component in MDX! ```Step-by-Step: Adding a New Blog Post
-
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 -
Add frontmatter metadata with all required fields
-
Write the content using Markdown/MDX syntax
-
Add images to
public/images/blog/my-blog-post/ -
Generate blog images (optional) using the AI-powered script:
# Preview image generation npm run generate:images:dry # Generate actual images npm run generate:images -
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
- German:
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
-
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" } } } } -
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" } } } } -
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" } } } } -
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
- Maintain structure consistency - Use the same JSON structure across all locale files
- Use semantic keys - Keys should describe the content, not the translation (e.g.,
actions.submitnotactions.submitButton) - Organize by feature - Group related translations under feature namespaces
- Test all locales - Always verify translations in all three languages (de, en, sr)
- Avoid hardcoded text - All user-facing text should come from translation files
- 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:
- URL path segment (e.g.,
/de/,/en/,/sr/) - Browser language settings
- 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 optimizationsnext/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 lintbefore committing changes - Fix all linting errors - the project uses
--strictmode - 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 orunknown - Export types from a central
types/directory when shared
React Components
- Use functional components with hooks
- Follow the component structure:
- Imports
- Type definitions
- Component definition
- Helper functions (if needed)
- 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 fromtailwind-mergeto 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.tssuffix (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 fromnext-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.tsor.test.tsxextension 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
describeblocks to group related tests - Use
itortestfor 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 featurefix- Bug fixdocs- Documentation changesstyle- Code style changes (formatting, etc.)refactor- Code refactoringtest- Adding or updating testschore- 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
