diff --git a/.auto-claude-status b/.auto-claude-status index 825997e..5e2a149 100644 --- a/.auto-claude-status +++ b/.auto-claude-status @@ -1,25 +1,25 @@ { "active": true, - "spec": "019-fix-readme-inaccuracies-and-add-missing-setup-docu", - "state": "complete", + "spec": "030-add-unit-tests-vitest-configured-but-no-tests-exis", + "state": "building", "subtasks": { - "completed": 5, - "total": 5, - "in_progress": 0, + "completed": 2, + "total": 8, + "in_progress": 1, "failed": 0 }, "phase": { - "current": "Update Documentation", + "current": "Utility Functions Tests", "id": null, - "total": 5 + "total": 2 }, "workers": { "active": 0, "max": 1 }, "session": { - "number": 6, - "started_at": "2026-01-25T06:23:50.684173" + "number": 4, + "started_at": "2026-01-25T06:18:19.433683" }, - "last_update": "2026-01-25T06:35:44.803685" + "last_update": "2026-01-25T06:32:23.146357" } \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 870f3db..f35b6c6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -50,7 +50,24 @@ "Bash(npm init -y)", "Bash(npx create-next-app@latest:*)", "Bash(echo \"npm install failed with exit code $?\")", - "Bash(echo:*)" + "Bash(echo:*)", + "Bash(npm run lint)", + "Bash(npx --yes next build:*)", + "Bash(pnpm next build:*)", + "Bash(timeout /t 2 /nobreak)", + "Bash(pnpm list:*)", + "WebSearch", + "Bash(npx eslint:*)", + "WebFetch(domain:platform.openai.com)", + "WebFetch(domain:cookbook.openai.com)", + "Bash(findstr:*)", + "Bash(npm i:*)", + "Bash(npm view:*)", + "WebFetch(domain:ai.google.dev)", + "Bash(copy \"C:\\\\Users\\\\damja\\\\WebstormProjects\\\\Portfolio\\\\Portfolio.png\" \"C:\\\\Users\\\\damja\\\\WebstormProjects\\\\Portfolio\\\\public\\\\hero-portrait.png\")", + "Bash(npx playwright:*)", + "Bash(npx:*)", + "Bash(git reset:*)" ], "deny": [] } diff --git a/.claude_settings.json b/.claude_settings.json index 87200c3..f2da165 100644 --- a/.claude_settings.json +++ b/.claude_settings.json @@ -11,14 +11,14 @@ "Edit(./**)", "Glob(./**)", "Grep(./**)", - "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", - "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", - "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", - "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", - "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", - "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu\\.auto-claude\\specs\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", - "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu\\.auto-claude\\specs\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", - "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\019-fix-readme-inaccuracies-and-add-missing-setup-docu\\.auto-claude\\specs\\019-fix-readme-inaccuracies-and-add-missing-setup-docu/**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", + "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)", "Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", "Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", "Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)", diff --git a/.env.example b/.env.example index 0611672..c1b851a 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ # Supabase Configuration +<<<<<<< HEAD # Get these from your Supabase project settings: https://app.supabase.com NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key-here @@ -10,3 +11,16 @@ NEXT_PUBLIC_GA_TRACKING_ID=G-XXXXXXXXXX # Site Configuration # The public URL where your site is hosted (e.g., https://damjan-savic.com) NEXT_PUBLIC_SITE_URL=https://your-domain.com +======= +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key + +# Google Analytics (optional) +NEXT_PUBLIC_GA_TRACKING_ID=G-XXXXXXXXXX + +# Site URL +NEXT_PUBLIC_SITE_URL=https://damjan-savic.com + +# OpenAI API Key (for image generation) +OPENAI_API_KEY=sk-your-api-key-here +>>>>>>> origin/master 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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0cf30a7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,1221 @@ +# 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](#getting-started) +- [Project Overview](#project-overview) +- [Tech Stack](#tech-stack) +- [Project Structure](#project-structure) +- [Development Workflow](#development-workflow) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Content Workflows](#content-workflows) +- [Code Style & Standards](#code-style--standards) +- [Testing](#testing) +- [Legacy Code & Migration](#legacy-code--migration) +- [Commit Guidelines](#commit-guidelines) + +## Getting Started + +### Prerequisites + +- **Node.js** 18.x or higher +- **npm** 9.x or higher +- **Git** for version control + +### Installation + +1. **Clone the repository** + ```bash + git clone + cd portfolio + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **Set up environment variables** + + Create a `.env.local` file in the root directory with the required environment variables: + ```bash + # 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** + ```bash + 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 + +### Branch Naming Conventions + +Use descriptive branch names that follow these patterns: + +| Branch Type | Pattern | Example | Description | +|-------------|---------|---------|-------------| +| **Feature** | `feature/` | `feature/add-dark-mode` | New features or enhancements | +| **Bug Fix** | `fix/` | `fix/mobile-nav-bug` | Bug fixes and error corrections | +| **Documentation** | `docs/` | `docs/update-readme` | Documentation updates | +| **Refactoring** | `refactor/` | `refactor/contact-form` | Code restructuring without behavior changes | +| **Testing** | `test/` | `test/add-unit-tests` | Adding or updating tests | +| **Chore** | `chore/` | `chore/update-dependencies` | Maintenance tasks, dependency updates | + +**Guidelines:** +- Use lowercase with hyphens (kebab-case) +- Keep branch names concise but descriptive (3-5 words max) +- Avoid special characters except hyphens +- Delete branches after they've been merged + +**Examples:** +```bash +# ✅ Good +git checkout -b feature/cookie-consent-banner +git checkout -b fix/portfolio-filter-crash +git checkout -b docs/api-integration-guide + +# ❌ Bad +git checkout -b myFeature +git checkout -b fix_bug +git checkout -b john-working-branch +``` + +### 1. Create a New Feature Branch +```bash +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 +```bash +# Run development server +npm run dev + +# Run linter +npm run lint + +# Run tests +npm test +``` + +### 4. Build for Production +```bash +npm run build +``` + +Ensure the build completes without errors before submitting. + +### 5. Submit a Pull Request + +See the [Pull Request Guidelines](#pull-request-guidelines) section below for detailed instructions. + +## Pull Request Guidelines + +When you're ready to submit your changes, create a pull request (PR) following these guidelines to ensure a smooth review process. + +### Before Creating a PR + +**Pre-submission Checklist:** +- [ ] All tests pass (`npm test`) +- [ ] Linter runs without errors (`npm run lint`) +- [ ] Production build succeeds (`npm run build`) +- [ ] Changes have been tested locally +- [ ] Code follows project style guidelines +- [ ] No debugging statements left in code (console.log, debugger, etc.) +- [ ] Translation files updated for all locales (de, en, sr) if UI text changed +- [ ] Documentation updated if necessary + +### PR Title Format + +Use the same format as commit messages: + +``` +: +``` + +**Examples:** +``` +feat: add cookie consent banner +fix: resolve mobile navigation menu bug +docs: update API integration guide +refactor: simplify contact form validation +``` + +### PR Description Template + +Provide a clear description of your changes: + +```markdown +## Summary +Brief overview of what this PR does and why. + +## Changes Made +- Added new component for cookie consent +- Updated privacy policy page +- Added tests for consent logic + +## Testing +- [ ] Tested on desktop browsers (Chrome, Firefox, Safari) +- [ ] Tested on mobile devices +- [ ] Verified all three languages (de, en, sr) +- [ ] Checked accessibility with screen reader + +## Screenshots (if applicable) +[Add screenshots for UI changes] + +## Related Issues +Closes #123 +Related to #456 +``` + +### PR Best Practices + +1. **Keep PRs Focused** + - One feature or fix per PR + - Avoid mixing unrelated changes + - Break large features into smaller, reviewable PRs + +2. **Write Descriptive Commits** + - Follow the [Commit Guidelines](#commit-guidelines) + - Each commit should be a logical unit of work + - Avoid "WIP" or "Fix" commit messages + +3. **Update Documentation** + - Update README.md if adding new features + - Update CONTRIBUTING.md if changing development workflow + - Add JSDoc comments for new public functions + +4. **Request Review** + - Tag relevant reviewers + - Respond to review comments promptly + - Make requested changes in new commits (don't force push) + +5. **CI/CD Checks** + - Ensure all automated checks pass + - Fix any linting or build errors + - Address test failures before requesting review + +### PR Review Process + +1. **Automated Checks** - All CI/CD checks must pass +2. **Code Review** - At least one approval required +3. **Testing** - Reviewer may test changes locally +4. **Merge** - Squash and merge into main branch +5. **Cleanup** - Delete branch after merge + +### Handling Review Feedback + +- **Be responsive** - Address feedback within 1-2 days +- **Ask questions** - If feedback is unclear, ask for clarification +- **Make changes** - Commit review suggestions as new commits +- **Re-request review** - After addressing all comments +- **Be respectful** - Code reviews are about the code, not the person + +### What Gets Rejected + +PRs will be rejected or require changes if: +- ❌ Tests are failing +- ❌ Linter errors exist +- ❌ Build fails +- ❌ Breaking changes without discussion +- ❌ Missing translations for new UI text +- ❌ No description or context provided +- ❌ Touches legacy code without coordination (see [Legacy Code](#legacy-code--migration)) + +## 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: + +```typescript +// 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: + ```bash + # 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: + +```mdx +--- +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](/images/blog/my-blog-post/diagram.png) + +### 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: + ```bash + # 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: + ```bash + # 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: + +```json +{ + "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): + ```json + { + "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): + ```json + { + "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): + ```json + { + "pages": { + "contact": { + "form": { + "title": "Kontaktirajte nas", + "name": "Ime", + "email": "Email", + "message": "Poruka", + "submit": "Pošalji" + } + } + } + } + ``` + +4. **Use the translation** in your component: + ```typescript + import { useTranslations } from 'next-intl'; + + export function ContactForm() { + const t = useTranslations('pages.contact.form'); + + return ( +
+

{t('title')}

+ + + - - - -
- ); -}; - -export default ContactForm; \ No newline at end of file diff --git a/src/components-vite/ContactInfo.tsx b/src/components-vite/ContactInfo.tsx deleted file mode 100644 index 284a9ee..0000000 --- a/src/components-vite/ContactInfo.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Phone, Mail, ArrowRight } from "lucide-react" -import { Link } from "react-router-dom" - -export function ContactInfo() { - return ( -
-
-

- Kontakt -

- - - - +1 234 567 890 - - - - - info@damjan-savic.com - - - - Kontakt aufnehmen - - -
-
- ) -} diff --git a/src/components-vite/CookieBanner.tsx b/src/components-vite/CookieBanner.tsx deleted file mode 100644 index 6750142..0000000 --- a/src/components-vite/CookieBanner.tsx +++ /dev/null @@ -1,490 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { initGA, logPageView, initFBPixel, initFunctionalCookies } from '../utils/analytics'; -import { loadGoogleTagManager } from '../services/gtm'; - -// Feste Übersetzungen direkt in der Komponente -const translations = { - de: { - title: "Cookie-Einstellungen", - message: "Wir nutzen Cookies zur Verbesserung der Website. Ohne Ihre Zustimmung keine Datenweitergabe an Dritte. ", - acceptAll: "Alle akzeptieren", - save: "Einstellungen speichern", - decline: "Ablehnen", - necessary: "Notwendige Cookies", - necessaryDesc: "Für grundlegende Funktionen der Website", - analytics: "Analyse-Cookies", - analyticsDesc: "Für Analysen zur Verbesserung der Website", - marketing: "Marketing-Cookies", - marketingDesc: "Für personalisierte Werbung und Inhalte", - functional: "Funktionale Cookies", - functionalDesc: "Für erweiterte Funktionen und Personalisierung", - learnMore: "Mehr erfahren", - alwaysActive: "Immer aktiv" - }, - en: { - title: "Cookie Settings", - message: "This website uses cookies to enhance your browsing experience. Data will not be shared with third parties without your consent.", - acceptAll: "Accept All", - save: "Save Settings", - decline: "Decline All", - necessary: "Necessary Cookies", - necessaryDesc: "For basic website functions", - analytics: "Analytics Cookies", - analyticsDesc: "For analyzing website usage", - marketing: "Marketing Cookies", - marketingDesc: "For personalized ads and content", - functional: "Functional Cookies", - functionalDesc: "For enhanced functionality and personalization", - learnMore: "Learn more", - alwaysActive: "Always active" - }, - sr: { - title: "Podešavanja kolačića", - message: "Ovaj sajt koristi kolačiće za bolje iskustvo pretraživanja. Podaci neće biti deljeni sa trećim licima bez vaše saglasnosti.", - acceptAll: "Prihvati sve", - save: "Sačuvaj podešavanja", - decline: "Odbij sve", - necessary: "Neophodni kolačići", - necessaryDesc: "Za osnovne funkcije sajta", - analytics: "Analitički kolačići", - analyticsDesc: "Za analizu korišćenja sajta", - marketing: "Marketing kolačići", - marketingDesc: "Za personalizovane reklame i sadržaj", - functional: "Funkcionalni kolačići", - functionalDesc: "Za napredne funkcije i personalizaciju", - learnMore: "Saznajte više", - alwaysActive: "Uvek aktivno" - } -}; - -// Definition der verschiedenen Services -const cookieServices = { - essentiell: { - category: 'necessary', - services: [ - { - name: 'Session Cookies', - provider: 'Eigentümer der Website', - purpose: 'Speichert Ihre Sitzungsinformationen' - } - ] - }, - funktional: { - category: 'functional', - services: [ - { - name: 'Spracheinstellungen', - provider: 'Eigentümer der Website', - purpose: 'Speichert Ihre bevorzugte Sprache' - }, - { - name: 'Google Fonts', - provider: 'Google LLC', - purpose: 'Anzeige von Webschriften' - } - ] - }, - analyse: { - category: 'analytics', - services: [ - { - name: 'Google Analytics', - provider: 'Google LLC', - purpose: 'Analysiert Webseitennutzung und Nutzerverhalten' - } - ] - }, - marketing: { - category: 'marketing', - services: [ - { - name: 'Google Ads', - provider: 'Google LLC', - purpose: 'Personalisierte Werbung' - }, - { - name: 'Facebook Pixel', - provider: 'Meta Platforms Inc.', - purpose: 'Tracking von Werbekonversionen' - } - ] - } -}; - -interface CookieSettings { - necessary: boolean; - analytics: boolean; - marketing: boolean; - functional: boolean; -} - -// Entfernt externe Google Fonts, falls keine Zustimmung vorliegt -const removeExternalGoogleFonts = () => { - // Suche nach allen Google Fonts Link-Elementen und entferne sie - const linkElements = document.querySelectorAll('link[href*="fonts.googleapis.com"]'); - linkElements.forEach(link => { - link.parentNode?.removeChild(link); - }); - - // Suche nach allen Google Fonts Script-Elementen und entferne sie - const scriptElements = document.querySelectorAll('script[src*="fonts.googleapis.com"]'); - scriptElements.forEach(script => { - script.parentNode?.removeChild(script); - }); -}; - -// Funktion zum Blockieren von Google Analytics falls noch keine Zustimmung vorliegt -const blockGoogleAnalytics = () => { - // Entferne alle bestehenden GA Cookies - document.cookie.split(';').forEach(function(c) { - if (c.trim().indexOf('_ga') === 0) { - document.cookie = c.trim().split('=')[0] + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;'; - } - }); -}; - -const CookieBanner = () => { - const { i18n } = useTranslation(); - const [currentLang, setCurrentLang] = useState<'de' | 'en' | 'sr'>('de'); - const [showBanner, setShowBanner] = useState(false); - const [showSettings, setShowSettings] = useState(false); - const [cookieSettings, setCookieSettings] = useState({ - necessary: true, // Always true and disabled - analytics: false, - marketing: false, - functional: false - }); - - // Initialisiere Dienste basierend auf den Cookie-Einstellungen - const initializeServices = useCallback((settings: CookieSettings) => { - // Blockieren von Diensten, wenn nicht zugestimmt wurde - if (!settings.functional) { - removeExternalGoogleFonts(); - } - - if (!settings.analytics) { - blockGoogleAnalytics(); - } else { - // Initialisiere Google Analytics nur wenn ausdrücklich zugestimmt wurde - initGA(); - logPageView(); - } - - // Marketing-Dienste nur mit Zustimmung - if (settings.marketing) { - initFBPixel(); - loadGoogleTagManager(); - } - - // Funktionale Dienste nur mit Zustimmung - if (settings.functional) { - initFunctionalCookies(); - } - }, []); - - // Aktuelle Sprache erkennen - useEffect(() => { - const lang = i18n.language.substring(0, 2); - if (lang === 'de' || lang === 'en' || lang === 'sr') { - setCurrentLang(lang); - } else { - setCurrentLang('en'); // Fallback to English - } - }, [i18n.language]); - - // Get text based on current language - const getText = (key: keyof typeof translations.en) => { - return translations[currentLang][key]; - }; - - // DSGVO: Unmittelbar beim ersten Laden blockiere alle nicht-essentiellen Dienste - useEffect(() => { - // Blockiere externe Dienste bis Zustimmung vorliegt - removeExternalGoogleFonts(); - blockGoogleAnalytics(); - }, []); - - // Prüfe beim Laden, ob bereits Zustimmung vorhanden ist - useEffect(() => { - try { - const storedSettings = localStorage.getItem('cookieSettings'); - const hasConsent = localStorage.getItem('cookieConsent'); - - if (storedSettings && hasConsent) { - const parsedSettings = JSON.parse(storedSettings) as CookieSettings; - setCookieSettings(parsedSettings); - - // Lade die entsprechenden Dienste basierend auf den Einstellungen - initializeServices(parsedSettings); - setShowBanner(false); - } else { - // Zeige Banner wenn keine Zustimmung vorhanden ist - setShowBanner(true); - } - } catch (error) { - console.error('Error loading cookie settings:', error); - setShowBanner(true); - } - }, [initializeServices]); - - const handleSaveSettings = () => { - localStorage.setItem('cookieSettings', JSON.stringify(cookieSettings)); - localStorage.setItem('cookieConsent', 'true'); - localStorage.setItem('cookieConsentDate', new Date().toISOString()); - - // Initialisiere Dienste basierend auf den ausgewählten Einstellungen - initializeServices(cookieSettings); - - setShowSettings(false); - setShowBanner(false); - }; - - const handleAcceptAll = () => { - const allSettings: CookieSettings = { - necessary: true, - analytics: true, - marketing: true, - functional: true - }; - - localStorage.setItem('cookieSettings', JSON.stringify(allSettings)); - localStorage.setItem('cookieConsent', 'true'); - localStorage.setItem('cookieConsentDate', new Date().toISOString()); - setCookieSettings(allSettings); - - // Initialisiere alle Dienste - initializeServices(allSettings); - - setShowSettings(false); - setShowBanner(false); - }; - - const handleDeclineAll = () => { - const minimalSettings: CookieSettings = { - necessary: true, - analytics: false, - marketing: false, - functional: false - }; - - localStorage.setItem('cookieSettings', JSON.stringify(minimalSettings)); - localStorage.setItem('cookieConsent', 'true'); - localStorage.setItem('cookieConsentDate', new Date().toISOString()); - setCookieSettings(minimalSettings); - - // Block all non-essential services - initializeServices(minimalSettings); - - setShowSettings(false); - setShowBanner(false); - }; - - const toggleSetting = (setting: keyof Omit) => { - setCookieSettings({ - ...cookieSettings, - [setting]: !cookieSettings[setting] - }); - }; - - // If banner should not be shown, return null - if (!showBanner) { - return null; - } - - return ( -
-
-
- {!showSettings ? ( -
-
-

{getText('message')}

-
-
- - - -
-
- ) : ( -
-
-

{getText('title')}

- -
- -
-

{getText('message')}

- - {/* Cookie categories */} -
- {/* Necessary cookies - always enabled */} -
-
-
{getText('necessary')}
-
{getText('alwaysActive')}
-
-

{getText('necessaryDesc')}

- - {/* Services List */} -
- {cookieServices.essentiell.services.map((service, index) => ( -
-
{service.name}
-
Anbieter: {service.provider}
-
Zweck: {service.purpose}
-
- ))} -
-
- - {/* Analytics cookies */} -
-
-
{getText('analytics')}
- -
-

{getText('analyticsDesc')}

- - {/* Services List */} -
- {cookieServices.analyse.services.map((service, index) => ( -
-
{service.name}
-
Anbieter: {service.provider}
-
Zweck: {service.purpose}
-
- ))} -
-
- - {/* Marketing cookies */} -
-
-
{getText('marketing')}
- -
-

{getText('marketingDesc')}

- - {/* Services List */} -
- {cookieServices.marketing.services.map((service, index) => ( -
-
{service.name}
-
Anbieter: {service.provider}
-
Zweck: {service.purpose}
-
- ))} -
-
- - {/* Functional cookies */} -
-
-
{getText('functional')}
- -
-

{getText('functionalDesc')}

- - {/* Services List */} -
- {cookieServices.funktional.services.map((service, index) => ( -
-
{service.name}
-
Anbieter: {service.provider}
-
Zweck: {service.purpose}
-
- ))} -
-
-
-
- -
- - -
- - -
- )} -
-
-
- ); -}; - -export default CookieBanner; \ No newline at end of file diff --git a/src/components-vite/DebugOverlay.tsx b/src/components-vite/DebugOverlay.tsx deleted file mode 100644 index 8c73789..0000000 --- a/src/components-vite/DebugOverlay.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useScrollContext } from './ScrollContext'; - -interface KeyboardEvent { - key: string; -} - -const DebugOverlay: React.FC = () => { - const [showDebug, setShowDebug] = useState(false); - const { isTransitioning } = useScrollContext(); - - useEffect(() => { - const handleKeyPress = (e: KeyboardEvent) => { - if (e.key === 'd') { - setShowDebug((prev: boolean) => !prev); - } - }; - - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, []); - - if (!showDebug) return null; - - return ( -
-
Transitioning: {isTransitioning ? 'Yes' : 'No'}
-
Body Overflow: {document.body.style.overflow}
-
Scroll Position: {window.scrollY}
-
View Height: {window.innerHeight}
- -
- ); -}; - -export default DebugOverlay; \ No newline at end of file diff --git a/src/components-vite/ErrorBoundary.tsx b/src/components-vite/ErrorBoundary.tsx deleted file mode 100644 index f8a7900..0000000 --- a/src/components-vite/ErrorBoundary.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, ErrorInfo, ReactNode } from 'react'; -import { AlertTriangle, RefreshCw } from 'lucide-react'; - -interface Props { - children: ReactNode; - fallback?: ReactNode; -} - -interface State { - hasError: boolean; - error?: Error; -} - -class ErrorBoundary extends Component { - public state: State = { - hasError: false - }; - - public static getDerivedStateFromError(error: Error): State { - return { hasError: true, error }; - } - - public componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('Uncaught error:', error, errorInfo); - } - - private handleRetry = () => { - this.setState({ hasError: false, error: undefined }); - }; - - public render() { - if (this.state.hasError) { - if (this.props.fallback) { - return this.props.fallback; - } - - return ( -
-
- -

Something went wrong

-

- {this.state.error?.message || 'An unexpected error occurred'} -

- -
-
- ); - } - - return this.props.children; - } -} - -export default ErrorBoundary; \ No newline at end of file diff --git a/src/components-vite/FAQSection.tsx b/src/components-vite/FAQSection.tsx deleted file mode 100644 index 83ceeac..0000000 --- a/src/components-vite/FAQSection.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useState } from 'react'; -import { ChevronDown, ChevronUp } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { trackFAQInteraction } from '../services/gtm'; - -export function FAQSection() { - const { t } = useTranslation(); - const [openItem, setOpenItem] = useState(null); - - const faqs = t('faq.questions', { returnObjects: true }) as Array<{ - question: string; - answer: string; - category?: string; - }>; - - const toggleItem = (index: number) => { - const isOpening = openItem !== index; - const question = faqs[index]?.question || ''; - - // Track FAQ interaction - trackFAQInteraction(question, isOpening ? 'open' : 'close'); - - setOpenItem(prev => prev === index ? null : index); - }; - - const faqSchema = { - "@context": "https://schema.org", - "@type": "FAQPage", - "mainEntity": faqs.map(faq => ({ - "@type": "Question", - "name": faq.question, - "acceptedAnswer": { - "@type": "Answer", - "text": faq.answer - } - })) - }; - - return ( -
- - - - ); -}; - -export default SEO; diff --git a/src/components-vite/ScrollAnimation.tsx b/src/components-vite/ScrollAnimation.tsx deleted file mode 100644 index 12c98f3..0000000 --- a/src/components-vite/ScrollAnimation.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { motion, useAnimation } from "framer-motion" -import { useEffect, useRef } from "react" - -interface ScrollAnimationProps { - children: React.ReactNode - delay?: number -} - -const ScrollAnimation: React.FC = ({ children, delay = 0 }) => { - const controls = useAnimation() - const ref = useRef(null) - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - controls.start({ opacity: 1, y: 0, transition: { duration: 0.5, delay: delay } }) - } - }, - { - threshold: 0.5, - }, - ) - - if (ref.current) { - observer.observe(ref.current) - } - - return () => { - if (ref.current) { - observer.unobserve(ref.current) - } - } - }, [controls, delay]) - - return ( - - {children} - - ) -} - -export default ScrollAnimation - diff --git a/src/components-vite/ScrollContext.tsx b/src/components-vite/ScrollContext.tsx deleted file mode 100644 index ba77b07..0000000 --- a/src/components-vite/ScrollContext.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// ScrollContext.tsx -import React, { createContext, useContext, useState, useCallback } from 'react'; - -interface ScrollContextType { - isTransitioning: boolean; - setIsTransitioning: (value: boolean) => void; - isMenuOpen: boolean; - setIsMenuOpen: (value: boolean) => void; - lockScroll: () => void; - unlockScroll: () => void; -} - -const ScrollContext = createContext(undefined); - -export const ScrollProvider = ({ children }: { children: React.ReactNode }) => { - const [isTransitioning, setIsTransitioning] = useState(false); - const [isMenuOpen, setIsMenuOpen] = useState(false); - - const lockScroll = useCallback(() => { - const scrollY = window.scrollY; - document.body.style.position = 'fixed'; - document.body.style.top = `-${scrollY}px`; - document.body.style.width = '100%'; - }, []); - - const unlockScroll = useCallback(() => { - const scrollY = document.body.style.top; - document.body.style.position = ''; - document.body.style.top = ''; - document.body.style.width = ''; - window.scrollTo(0, parseInt(scrollY || '0') * -1); - }, []); - - return ( - - {children} - - ); -}; - -export const useScrollContext = () => { - const context = useContext(ScrollContext); - if (context === undefined) { - throw new Error('useScrollContext must be used within a ScrollProvider'); - } - return context; -}; \ No newline at end of file diff --git a/src/components-vite/ShareMenu.tsx b/src/components-vite/ShareMenu.tsx deleted file mode 100644 index 2e6ee15..0000000 --- a/src/components-vite/ShareMenu.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { Share2, X, Twitter, Facebook, Linkedin, Link as LinkIcon } from 'lucide-react'; -import { useOnClickOutside } from '../hooks/useOnClickOutside'; - -interface ShareMenuProps { - title: string; - url: string; - description: string; -} - -const ShareMenu: React.FC = ({ title, url, description }) => { - const [isOpen, setIsOpen] = useState(false); - const [copyStatus, setCopyStatus] = useState<'idle' | 'copied'>('idle'); - const menuRef = useRef(null); - - useOnClickOutside(menuRef, () => setIsOpen(false)); - - const shareLinks = [ - { - name: 'Twitter', - icon: Twitter, - href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(url)}`, - label: 'Share on Twitter' - }, - { - name: 'Facebook', - icon: Facebook, - href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, - label: 'Share on Facebook' - }, - { - name: 'LinkedIn', - icon: Linkedin, - href: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}&summary=${encodeURIComponent(description)}`, - label: 'Share on LinkedIn' - }, - ]; - - const copyToClipboard = async () => { - try { - await navigator.clipboard.writeText(url); - setCopyStatus('copied'); - setTimeout(() => setCopyStatus('idle'), 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - setIsOpen(false); - } - }; - - return ( -
- - - {isOpen && ( -
-
- {shareLinks.map((link) => ( - - - ))} - -
-
- )} -
- ); -}; - -export default ShareMenu \ No newline at end of file diff --git a/src/components-vite/SkeletonLoader.tsx b/src/components-vite/SkeletonLoader.tsx deleted file mode 100644 index 556597e..0000000 --- a/src/components-vite/SkeletonLoader.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; - -interface SkeletonProps { - className?: string; -} - -const Skeleton: React.FC = ({ className }) => ( -
-); - -export const BlogPostSkeleton = () => ( -
- - - -
- - - -
-
-); - -export const ProjectSkeleton = () => ( -
- -
- - - -
- -
- - - -
-
-); - -export const PortfolioSkeleton = () => ( -
- -
- {[1, 2, 3].map((i) => ( -
- -
- ))} -
-
-); \ No newline at end of file diff --git a/src/components-vite/SkillsMatrix.tsx b/src/components-vite/SkillsMatrix.tsx deleted file mode 100644 index 3db7543..0000000 --- a/src/components-vite/SkillsMatrix.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import { motion } from 'framer-motion'; -import { useInView } from 'react-intersection-observer'; - -interface Skill { - name: string; - level: number; - category: string; - description: string; -} - -interface SkillsMatrixProps { - skills: Skill[]; -} - -const SkillsMatrix: React.FC = ({ skills }) => { - const [ref, inView] = useInView({ - triggerOnce: true, - threshold: 0.1, - }); - - const categories = Array.from(new Set(skills.map(skill => skill.category))); - - return ( -
- {categories.map((category, categoryIndex) => ( -
-

{category}

-
- {skills - .filter(skill => skill.category === category) - .map((skill, skillIndex) => ( -
-
-
- {skill.name} -
-

{skill.description}

-
-
- {skill.level}% -
-
- -
-
- ))} -
-
- ))} -
- ); -}; - -export default SkillsMatrix; \ No newline at end of file diff --git a/src/components-vite/SkipToContent.tsx b/src/components-vite/SkipToContent.tsx deleted file mode 100644 index e4c8cae..0000000 --- a/src/components-vite/SkipToContent.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { useState } from 'react'; - -const SkipToContent: React.FC = () => { - const [isFocused, setIsFocused] = useState(false); - - return ( - setIsFocused(true)} - onBlur={() => setIsFocused(false)} - > - Skip to main content - - ); -}; - -export default SkipToContent \ No newline at end of file diff --git a/src/components-vite/TestimonialCard.tsx b/src/components-vite/TestimonialCard.tsx deleted file mode 100644 index 2e72507..0000000 --- a/src/components-vite/TestimonialCard.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { Star, Quote } from 'lucide-react'; -import ImageWithFallback from './ImageWithFallback'; - -interface TestimonialProps { - name: string; - role: string; - company: string; - image: string; - content: string; - rating: number; -} - -const TestimonialCard: React.FC = ({ - name, - role, - company, - image, - content, - rating -}) => { - return ( -
- -
- -
-

{name}

-

{role} at {company}

-
-
-
- {[...Array(5)].map((_, i) => ( - - ))} -
-

{content}

-
- ); -}; - -export default TestimonialCard; \ No newline at end of file diff --git a/src/components-vite/TextArea.tsx b/src/components-vite/TextArea.tsx deleted file mode 100644 index 35cddcf..0000000 --- a/src/components-vite/TextArea.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; - -interface TextareaProps extends React.TextareaHTMLAttributes { - error?: string; -} - -export const Textarea = React.forwardRef( - ({ className = '', error, ...props }, ref) => { - return ( -
-