Compare commits

..
6 Commits
Author SHA1 Message Date
damjan_savic ebd14169fe chore: remove unused next-mdx-remote dependency (vulnerable 5.0.0, blocked Vercel build) 2026-06-11 00:08:14 +02:00
damjan_savic b6ad1e0b2f chore: capture live server state before migration (2026-06-10) 2026-06-10 23:56:36 +02:00
damjan_savic 97bf6da106 ci: add Gitea Actions CI/CD pipeline 2026-02-10 18:25:24 +01:00
damjan_savicandClaude Opus 4.6 62ae5140b9 Tailwind v4 migration, massive SEO optimization, and fixes
- Migrate to Tailwind CSS v4 with @theme, @layer, @plugin syntax
- Upgrade to Next.js 16.1, React 19.2, TypeScript 5.9.3
- Add 8 technology landing pages (Next.js, React, TypeScript, Tailwind CSS, Claude AI, Python, n8n, KI-Agenten)
- Expand SEO keywords across all locales (de/en/sr) for home, layout, cities, services
- Add tech-specific keywords to all 22 city pages
- Add FAQ schema + seoKeywords to service detail pages
- Add CollectionPage JSON-LD to blog listing
- Fix city page: dynamic region, BASE_URL canonicals, x-default hreflang
- Rewrite terms page with full German AGB, English and Serbian translations
- Remove dead crypto import and unused CSRF middleware (Edge Runtime fix)
- Remove crawlDelay from robots.ts
- Update sitemap with technology pages (~198 indexed URLs)
- Remove deprecated tailwind.config.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 20:40:19 +01:00
damjan_savicandClaude Opus 4.5 03d6228a68 Add reference person images for AI generation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 20:14:35 +01:00
damjan_savicandClaude Opus 4.5 2854a02afb Update config, dependencies and add icon
- Update next.config.ts and tsconfig.json configuration
- Update ContactInfo component
- Add app icon (SVG)
- Add blog image prompts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 19:33:48 +01:00
73 changed files with 4541 additions and 2290 deletions
+47
View File
@@ -0,0 +1,47 @@
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build
env:
NODE_ENV: production
- name: Test
run: pnpm run test -- --passWithNoTests
env:
NODE_ENV: test
deploy:
needs: ci
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Deploy
run: |
echo "Deployment handled via webhook"
+1
View File
@@ -0,0 +1 @@
manage-package-manager-versions=false
+1 -1
View File
@@ -6,7 +6,7 @@ Personal portfolio website showcasing my work as an **AI & Automation Specialist
## Current Role ## Current Role
**Process Automation Specialist** @ Everlast Consulting GmbH (since Dec 2024) **Process Automation Specialist** @ Everlast Consulting GmbH (since Dec 2025)
- Developing AI agents with n8n and Zapier - Developing AI agents with n8n and Zapier
- Building web scraping solutions - Building web scraping solutions
- Migration from Power Automate to n8n - Migration from Power Automate to n8n
+457
View File
@@ -0,0 +1,457 @@
================================================================================
BLOG IMAGE PROMPTS - PORTFOLIO
================================================================================
Format: 8K Resolution, 16:9 Aspect Ratio
CI: Dark moss (#181C14), Sage green (#697565/#465B50), Cream (#ECDFCC)
Style: Modern, minimalist, tech-forward with earthy tones
================================================================================
================================================================================
AI & LLM (01-20)
================================================================================
01 - Agentic AI 2026
--------------------
Futuristic dark moss green control room with multiple holographic AI agent interfaces, interconnected neural network visualization, enterprise server infrastructure in background, soft sage green ambient lighting, cream-colored data streams, 8K resolution, 16:9 aspect ratio, cinematic lighting, minimalist tech aesthetic
02 - DeepSeek R1 vs OpenAI o1
-----------------------------
Split-screen comparison of two abstract AI brain architectures, left side with blue reasoning pathways, right side with orange logic networks, dark moss background (#181C14), floating mathematical equations in cream color, sage green connection lines, 8K resolution, 16:9 aspect ratio, clean minimalist design
03 - Multi-Agent Systeme TypeScript
-----------------------------------
Abstract visualization of multiple interconnected AI agents as geometric nodes, TypeScript code fragments floating in space, dark sage green (#465B50) connecting lines, dark moss background, cream-colored data packets flowing between agents, 8K resolution, 16:9 aspect ratio, modern tech aesthetic
04 - AI Cost Optimization
-------------------------
Minimalist visualization of descending cost graph transforming into efficiency curve, golden coins morphing into data bits, dark moss environment, sage green accent lighting on graph elements, cream-colored financial metrics floating, 8K resolution, 16:9 aspect ratio, clean corporate aesthetic
05 - Human-in-the-Loop
----------------------
Elegant human hand reaching toward AI holographic interface, feedback loop visualization connecting human and machine, dark moss background, sage green neural pathways, cream-colored approval checkmarks, 8K resolution, 16:9 aspect ratio, sophisticated tech-human harmony
06 - Claude Haiku vs Sonnet vs Opus
-----------------------------------
Three abstract AI model representations as ascending geometric structures, smallest to largest, dark moss environment, sage green light gradients distinguishing each model, cream-colored performance metrics floating, 8K resolution, 16:9 aspect ratio, elegant comparative visualization
07 - Tool Use Patterns
----------------------
AI agent hand reaching toward floating API tool icons, gear mechanisms, database symbols, webhook connections, dark moss background, sage green tool outlines, cream-colored data flows, 8K resolution, 16:9 aspect ratio, clean technical illustration
08 - AI Governance Frameworks
-----------------------------
Balanced scales with AI brain on one side and ethical guidelines document on other, governance framework structure surrounding, dark moss background, sage green framework lines, cream-colored policy text elements, 8K resolution, 16:9 aspect ratio, professional corporate aesthetic
09 - Physical AI Robotics
-------------------------
Sleek robotic arm with integrated LLM visualization, holographic decision-making interface, industrial environment with dark moss tones, sage green sensor lights, cream-colored data streams connecting brain to mechanics, 8K resolution, 16:9 aspect ratio, futuristic industrial design
10 - Open Source AI Demokratisierung
------------------------------------
Open hands releasing multiple AI models into the world, community collaboration visualization, dark moss background, sage green open-source symbols, cream-colored accessibility icons, diverse representation, 8K resolution, 16:9 aspect ratio, empowering community aesthetic
11 - DeepSeek R1 Training Paradigm
----------------------------------
Abstract visualization of reinforcement learning training loop, reward signals as golden particles, neural network layers expanding, dark moss background, sage green gradient learning curves, cream-colored milestone markers, 8K resolution, 16:9 aspect ratio, scientific research aesthetic
12 - Telegram Bots AI Enterprise
--------------------------------
Telegram logo morphing into enterprise AI interface, chat bubbles with intelligent responses, corporate environment silhouette, dark moss background, sage green Telegram accent, cream-colored message threads, 8K resolution, 16:9 aspect ratio, professional communication aesthetic
13 - AI Agent Testing
---------------------
Quality assurance visualization with AI agent under examination, test case checkboxes, debugging magnifying glass, dark moss background, sage green test success indicators, cream-colored test metrics, 8K resolution, 16:9 aspect ratio, quality-focused technical design
14 - Prompt Engineering 2026
----------------------------
Artistic representation of prompt crafting as sculpting light, human hands shaping text into AI responses, dark moss environment, sage green text highlighting, cream-colored output manifestation, 8K resolution, 16:9 aspect ratio, creative technical artistry
15 - Human-AI Teams
-------------------
Human and AI avatar working at shared holographic workspace, collaborative task completion visualization, dark moss office environment, sage green connection threads, cream-colored shared documents, 8K resolution, 16:9 aspect ratio, harmonious teamwork aesthetic
16 - Voice AI Infrastructure
----------------------------
Sound wave transforming into structured data pipeline, server infrastructure with audio processing nodes, dark moss data center, sage green audio waveforms, cream-colored transcription text, 8K resolution, 16:9 aspect ratio, technical infrastructure visualization
17 - Conversational AI Turn-Taking
----------------------------------
Two speech bubbles in elegant dance, timing visualization with conversation rhythm, dark moss background, sage green speaker indicators, cream-colored dialogue text, 8K resolution, 16:9 aspect ratio, fluid communication design
18 - Multilingual Voice Bots
----------------------------
Global languages floating around central voice AI core, translation pathways connecting cultures, dark moss globe, sage green language bridges, cream-colored text in multiple scripts, 8K resolution, 16:9 aspect ratio, international connectivity aesthetic
19 - Deepgram Nova German
-------------------------
German language waveform being processed by Nova neural network, Umlauts and German text elements, dark moss background, sage green processing nodes, cream-colored German transcription, 8K resolution, 16:9 aspect ratio, precision speech recognition visualization
20 - ElevenLabs Turbo Performance
---------------------------------
Speed-optimized voice synthesis visualization, sound wave racing through optimized pipeline, dark moss environment, sage green performance metrics, cream-colored latency indicators showing improvement, 8K resolution, 16:9 aspect ratio, high-performance audio aesthetic
================================================================================
VOICE & REAL-TIME (21-27)
================================================================================
21 - WebSockets Voice AI
------------------------
Bidirectional data streams carrying voice data between client and server, persistent connection visualization, dark moss network environment, sage green WebSocket channels, cream-colored audio packets, 8K resolution, 16:9 aspect ratio, real-time communication design
22 - N8N AI Agents Workflow
---------------------------
Visual workflow canvas with AI agent nodes connected by automation paths, n8n interface elements, dark moss background, sage green workflow connections, cream-colored trigger events, 8K resolution, 16:9 aspect ratio, workflow automation aesthetic
23 - Zapier vs N8N vs Make
--------------------------
Three workflow platforms represented as distinct geometric islands connected by bridges, comparison visualization, dark moss ocean, sage green platform accents, cream-colored feature labels, 8K resolution, 16:9 aspect ratio, clean platform comparison design
24 - Event-Driven AI Architecture
---------------------------------
Event bus with AI processors responding to streaming events, pub-sub pattern visualization, dark moss infrastructure, sage green event streams, cream-colored message payloads, 8K resolution, 16:9 aspect ratio, architectural diagram aesthetic
25 - Socket.IO AI Chat
----------------------
Real-time chat interface with AI responses appearing instantly, Socket.IO room visualization, dark moss chat environment, sage green user indicators, cream-colored message bubbles, 8K resolution, 16:9 aspect ratio, modern chat application design
26 - Realtime Dashboards WebSocket
----------------------------------
Live updating dashboard with streaming metrics and charts, WebSocket data feeds, dark moss dashboard background, sage green chart lines, cream-colored metric numbers updating, 8K resolution, 16:9 aspect ratio, data visualization aesthetic
27 - SSE vs WebSocket
---------------------
Split comparison showing one-way SSE stream vs bidirectional WebSocket, protocol visualization, dark moss background, sage green data direction arrows, cream-colored protocol labels, 8K resolution, 16:9 aspect ratio, technical comparison design
================================================================================
FRONTEND & FRAMEWORK (28-40)
================================================================================
28 - Next.js 15 Deep Dive
-------------------------
Next.js logo descending into deep architectural layers, RSC, routing, caching visualization, dark moss code environment, sage green framework structure, cream-colored component hierarchy, 8K resolution, 16:9 aspect ratio, framework deep-dive aesthetic
29 - React Server Components
----------------------------
Server and client components separated by elegant boundary line, data flowing from server to client, dark moss background, sage green server zone, cream-colored client hydration, 8K resolution, 16:9 aspect ratio, React architecture visualization
30 - Streaming UI Patterns
--------------------------
UI skeleton progressively filling with content, streaming data visualization, dark moss interface, sage green loading states, cream-colored content appearing, 8K resolution, 16:9 aspect ratio, progressive enhancement design
31 - TypeScript 6 Advanced Patterns
-----------------------------------
Abstract type system visualization with generics, conditional types, mapped types floating, dark moss IDE environment, sage green type annotations, cream-colored TypeScript syntax, 8K resolution, 16:9 aspect ratio, type-safe code aesthetic
32 - Tailwind CSS 4
-------------------
Utility classes transforming into beautiful UI components, CSS-in-JS visualization, dark moss design environment, sage green utility badges, cream-colored styled components, 8K resolution, 16:9 aspect ratio, modern CSS framework design
33 - Zod Schema Validation
--------------------------
Data passing through validation gates, schema structure protecting application, dark moss security environment, sage green validation checkmarks, cream-colored schema definitions, 8K resolution, 16:9 aspect ratio, data validation aesthetic
34 - Prisma 7 ORM
-----------------
Database schema transforming into TypeScript types, Prisma client connecting to database, dark moss database environment, sage green model relationships, cream-colored query results, 8K resolution, 16:9 aspect ratio, ORM visualization design
35 - tRPC Type-Safe APIs
------------------------
End-to-end type safety visualization from client to server, type inference flowing, dark moss API environment, sage green type connections, cream-colored procedure calls, 8K resolution, 16:9 aspect ratio, type-safe API aesthetic
36 - Zustand vs Jotai
---------------------
Two state management approaches visualized as different architectural styles, comparison view, dark moss application state, sage green store structures, cream-colored state atoms, 8K resolution, 16:9 aspect ratio, state management comparison design
37 - TanStack Query
-------------------
Data fetching lifecycle visualization with caching layers, stale-while-revalidate pattern, dark moss data layer, sage green cache indicators, cream-colored fresh data, 8K resolution, 16:9 aspect ratio, query management aesthetic
38 - React Hook Form
--------------------
Form fields with elegant validation states, hook-based form management visualization, dark moss form interface, sage green valid indicators, cream-colored form labels, 8K resolution, 16:9 aspect ratio, form handling design
39 - Vitest Testing
-------------------
Test suite running with fast execution visualization, green checkmarks appearing rapidly, dark moss terminal environment, sage green passing tests, cream-colored test descriptions, 8K resolution, 16:9 aspect ratio, testing framework aesthetic
40 - Radix UI Components
------------------------
Accessible UI primitives assembling into polished components, headless design system, dark moss component library, sage green accessibility indicators, cream-colored component labels, 8K resolution, 16:9 aspect ratio, design system visualization
================================================================================
BACKEND & DATABASES (41-50)
================================================================================
41 - Supabase Fullstack Platform
--------------------------------
Supabase ecosystem with auth, database, storage, functions orbiting central platform, dark moss cloud environment, sage green service connections, cream-colored data flows, 8K resolution, 16:9 aspect ratio, platform architecture design
42 - MongoDB Atlas Vector Search
--------------------------------
Vector embeddings in high-dimensional space with semantic search visualization, dark moss database cloud, sage green vector arrows, cream-colored similar document clusters, 8K resolution, 16:9 aspect ratio, vector database aesthetic
43 - Redis Stack Vector Database
--------------------------------
Redis data structures with vector similarity search, in-memory speed visualization, dark moss cache layer, sage green vector indices, cream-colored fast query results, 8K resolution, 16:9 aspect ratio, high-performance database design
44 - Drizzle ORM Lightweight
----------------------------
Lightweight ORM as elegant minimal structure, SQL-like syntax with TypeScript types, dark moss database connection, sage green query builder, cream-colored lean code, 8K resolution, 16:9 aspect ratio, minimalist ORM aesthetic
45 - Turso LibSQL Edge
----------------------
Edge database nodes distributed globally, LibSQL replication visualization, dark moss edge network, sage green sync connections, cream-colored regional data, 8K resolution, 16:9 aspect ratio, edge computing database design
46 - PlanetScale MySQL Serverless
---------------------------------
Serverless MySQL scaling visualization, branching database schema, dark moss cloud infrastructure, sage green branch connections, cream-colored schema migrations, 8K resolution, 16:9 aspect ratio, serverless database aesthetic
47 - Hono Edge Framework
------------------------
Ultrafast edge runtime with minimal footprint, Hono bird silhouette, dark moss edge environment, sage green request routing, cream-colored response paths, 8K resolution, 16:9 aspect ratio, lightweight framework design
48 - API Design Patterns
------------------------
RESTful and GraphQL patterns visualized as architectural blueprints, dark moss API layer, sage green endpoint structures, cream-colored request/response flows, 8K resolution, 16:9 aspect ratio, API architecture aesthetic
49 - Authentication Patterns
----------------------------
Security layers with JWT tokens, session management, OAuth flows, dark moss security environment, sage green authentication shields, cream-colored user credentials protected, 8K resolution, 16:9 aspect ratio, auth security design
50 - Background Jobs Queues
---------------------------
Job queue with workers processing tasks, async execution visualization, dark moss worker pool, sage green job progress, cream-colored task payloads, 8K resolution, 16:9 aspect ratio, background processing aesthetic
================================================================================
WEB SCRAPING & AUTOMATION (51-60)
================================================================================
51 - Playwright Web Scraping
----------------------------
Automated browser navigating through web pages, data extraction visualization, dark moss browser environment, sage green selector highlights, cream-colored extracted data, 8K resolution, 16:9 aspect ratio, web automation design
52 - Puppeteer Browser Automation
---------------------------------
Puppet strings controlling browser actions, headless Chrome visualization, dark moss automation environment, sage green action paths, cream-colored DOM elements, 8K resolution, 16:9 aspect ratio, browser puppetry aesthetic
53 - Cheerio HTML Parsing
-------------------------
HTML document being parsed into structured data tree, jQuery-like selection, dark moss code environment, sage green parsed elements, cream-colored data extraction, 8K resolution, 16:9 aspect ratio, HTML parsing design
54 - Anti-Bot Detection
-----------------------
Shield protecting scraper from detection, fingerprinting evasion visualization, dark moss security layer, sage green protection barriers, cream-colored stealth indicators, 8K resolution, 16:9 aspect ratio, anti-detection aesthetic
55 - Data Extraction Pipelines
------------------------------
Data flowing through extraction, transformation, loading stages, ETL pipeline visualization, dark moss data flow, sage green processing stages, cream-colored clean output data, 8K resolution, 16:9 aspect ratio, pipeline architecture design
56 - Cron Job Scheduling
------------------------
Clock with scheduled tasks triggering at specific times, calendar automation visualization, dark moss scheduling environment, sage green time indicators, cream-colored task executions, 8K resolution, 16:9 aspect ratio, scheduling aesthetic
57 - Browser Extensions Automation
----------------------------------
Browser extension icon with automation gears inside, content script injection visualization, dark moss browser chrome, sage green extension actions, cream-colored automated workflows, 8K resolution, 16:9 aspect ratio, extension development design
58 - API Integration Patterns
-----------------------------
Multiple APIs connecting through integration hub, adapter pattern visualization, dark moss integration layer, sage green API connectors, cream-colored data transformations, 8K resolution, 16:9 aspect ratio, integration architecture aesthetic
59 - Email Automation
---------------------
Automated email sequences flowing to recipients, campaign visualization, dark moss email infrastructure, sage green trigger points, cream-colored email templates, 8K resolution, 16:9 aspect ratio, email marketing automation design
60 - Workflow Orchestration
---------------------------
Complex workflow with conditional branches, parallel execution paths, orchestration visualization, dark moss workflow canvas, sage green decision nodes, cream-colored execution paths, 8K resolution, 16:9 aspect ratio, orchestration aesthetic
================================================================================
IOT & SMART HOME (61-70)
================================================================================
61 - Home Assistant Integration
-------------------------------
Smart home central hub connecting all devices, Home Assistant dashboard visualization, dark moss home environment, sage green device connections, cream-colored automation flows, 8K resolution, 16:9 aspect ratio, smart home aesthetic
62 - MQTT Protocol IoT
----------------------
Publish-subscribe messaging pattern with IoT devices, broker connecting publishers and subscribers, dark moss IoT network, sage green topic channels, cream-colored message payloads, 8K resolution, 16:9 aspect ratio, MQTT protocol design
63 - ESP32 Development
----------------------
ESP32 microcontroller with connected sensors and actuators, embedded development visualization, dark moss electronics environment, sage green circuit traces, cream-colored data readings, 8K resolution, 16:9 aspect ratio, embedded systems aesthetic
64 - Raspberry Pi IoT
---------------------
Raspberry Pi as IoT gateway connecting edge devices to cloud, dark moss computing environment, sage green GPIO connections, cream-colored sensor data streams, 8K resolution, 16:9 aspect ratio, single-board computer design
65 - Matter Thread Protocol
---------------------------
Matter-enabled devices communicating over Thread mesh network, interoperability visualization, dark moss smart home, sage green mesh connections, cream-colored device icons, 8K resolution, 16:9 aspect ratio, next-gen smart home protocol aesthetic
66 - Zigbee Zigbee2MQTT
-----------------------
Zigbee mesh network with MQTT bridge, device communication visualization, dark moss IoT environment, sage green Zigbee links, cream-colored MQTT messages, 8K resolution, 16:9 aspect ratio, Zigbee network design
67 - InfluxDB Sensor Data
-------------------------
Time-series data streaming into InfluxDB, sensor metrics visualization, dark moss database environment, sage green time buckets, cream-colored metric graphs, 8K resolution, 16:9 aspect ratio, time-series database aesthetic
68 - IoT Security
-----------------
Security layers protecting IoT devices, encryption and authentication visualization, dark moss security perimeter, sage green security shields, cream-colored protected devices, 8K resolution, 16:9 aspect ratio, IoT security design
69 - Smart Home Dashboard
-------------------------
Beautiful dashboard displaying all smart home controls and metrics, dark moss interface, sage green active device indicators, cream-colored temperature and status readings, 8K resolution, 16:9 aspect ratio, dashboard UI aesthetic
70 - Edge Computing IoT
-----------------------
Processing happening at the edge before cloud, reduced latency visualization, dark moss edge nodes, sage green local processing, cream-colored optimized data to cloud, 8K resolution, 16:9 aspect ratio, edge computing design
================================================================================
3D & GRAPHICS (71-73)
================================================================================
71 - Three.js React Three Fiber
-------------------------------
3D scene rendered with React components, Three.js geometry with declarative syntax, dark moss 3D space, sage green wireframe meshes, cream-colored lighting effects, 8K resolution, 16:9 aspect ratio, 3D web development aesthetic
72 - WebGPU 3D Graphics
-----------------------
GPU-accelerated 3D rendering with WebGPU, shader pipelines visualization, dark moss graphics environment, sage green compute shaders, cream-colored rendered output, 8K resolution, 16:9 aspect ratio, next-gen web graphics design
73 - glTF Model Optimization
----------------------------
3D model being optimized and compressed, polygon reduction visualization, dark moss 3D workspace, sage green optimized mesh, cream-colored file size reduction metrics, 8K resolution, 16:9 aspect ratio, 3D optimization aesthetic
================================================================================
SEO & PERFORMANCE (74-80)
================================================================================
74 - Next.js SEO Metadata
-------------------------
Search engine crawling Next.js pages with rich metadata, structured data visualization, dark moss search environment, sage green metadata tags, cream-colored search results preview, 8K resolution, 16:9 aspect ratio, SEO optimization design
75 - Core Web Vitals Optimization
---------------------------------
LCP, FID, CLS metrics being optimized, performance gauges improving, dark moss performance dashboard, sage green optimal scores, cream-colored metric improvements, 8K resolution, 16:9 aspect ratio, web vitals aesthetic
76 - Structured Data JSON-LD
----------------------------
JSON-LD schema enriching search results, rich snippets visualization, dark moss semantic web, sage green structured connections, cream-colored enhanced search listings, 8K resolution, 16:9 aspect ratio, structured data design
77 - Image Optimization Web
---------------------------
Images being compressed and optimized for web delivery, format conversion visualization, dark moss media pipeline, sage green optimization process, cream-colored faster loading indicators, 8K resolution, 16:9 aspect ratio, image optimization aesthetic
78 - Technical SEO Checklist
----------------------------
SEO audit checklist with items being completed, technical optimization visualization, dark moss audit environment, sage green completed checkmarks, cream-colored improvement items, 8K resolution, 16:9 aspect ratio, SEO checklist design
79 - International SEO i18n
---------------------------
Global map with localized content versions, hreflang connections, dark moss world view, sage green regional markers, cream-colored translated content, 8K resolution, 16:9 aspect ratio, international SEO aesthetic
80 - Performance Monitoring Analytics
-------------------------------------
Real-time performance metrics dashboard, analytics visualization, dark moss monitoring center, sage green healthy metrics, cream-colored trend graphs, 8K resolution, 16:9 aspect ratio, performance analytics design
================================================================================
SAAS & GROWTH (81-100)
================================================================================
81 - A/B Testing Implementation
-------------------------------
Split test visualization with variant A and B, statistical significance indicators, dark moss testing environment, sage green winning variant, cream-colored conversion data, 8K resolution, 16:9 aspect ratio, A/B testing aesthetic
82 - Conversion Rate Optimization
---------------------------------
Funnel optimization with improved conversion at each stage, dark moss marketing funnel, sage green optimization points, cream-colored conversion percentages, 8K resolution, 16:9 aspect ratio, CRO design
83 - SaaS Pricing Strategies
----------------------------
Pricing tiers as elegant ascending platforms, value proposition visualization, dark moss pricing environment, sage green tier highlights, cream-colored feature lists, 8K resolution, 16:9 aspect ratio, SaaS pricing aesthetic
84 - Subscription Billing Stripe
--------------------------------
Recurring payment cycle with Stripe integration, subscription management visualization, dark moss billing environment, sage green successful payments, cream-colored invoice flows, 8K resolution, 16:9 aspect ratio, subscription billing design
85 - Multi-Tenancy Architecture
-------------------------------
Isolated tenant environments within shared infrastructure, data separation visualization, dark moss cloud architecture, sage green tenant boundaries, cream-colored shared services, 8K resolution, 16:9 aspect ratio, multi-tenant aesthetic
86 - Feature Flags Progressive Rollout
--------------------------------------
Feature being gradually enabled for user segments, rollout percentage visualization, dark moss deployment environment, sage green enabled features, cream-colored user segments, 8K resolution, 16:9 aspect ratio, feature flag design
87 - User Onboarding Flows
--------------------------
New user journey through onboarding steps, progress visualization, dark moss onboarding interface, sage green completed steps, cream-colored welcome screens, 8K resolution, 16:9 aspect ratio, onboarding experience aesthetic
88 - SaaS Analytics Metrics
---------------------------
Key SaaS metrics dashboard showing MRR, churn, LTV, dark moss analytics environment, sage green positive trends, cream-colored metric values, 8K resolution, 16:9 aspect ratio, SaaS metrics design
89 - Customer Success Automation
--------------------------------
Automated customer success workflows, health score monitoring visualization, dark moss CS environment, sage green healthy accounts, cream-colored intervention triggers, 8K resolution, 16:9 aspect ratio, customer success aesthetic
90 - SaaS Security Compliance
-----------------------------
Security compliance badges SOC2, GDPR, with audit trail visualization, dark moss compliance environment, sage green certified checkmarks, cream-colored compliance documentation, 8K resolution, 16:9 aspect ratio, security compliance design
91 - API Design Documentation
-----------------------------
Beautiful API documentation with interactive examples, developer portal visualization, dark moss documentation environment, sage green endpoint categories, cream-colored code samples, 8K resolution, 16:9 aspect ratio, API docs aesthetic
92 - Webhook Implementation
---------------------------
Event-driven webhook notifications flowing to external systems, payload delivery visualization, dark moss integration environment, sage green successful deliveries, cream-colored event payloads, 8K resolution, 16:9 aspect ratio, webhook design
93 - Rate Limiting Throttling
-----------------------------
Traffic being controlled through rate limiting gates, protection visualization, dark moss API gateway, sage green allowed requests, cream-colored throttled indicators, 8K resolution, 16:9 aspect ratio, rate limiting aesthetic
94 - Developer Experience DX
----------------------------
Developer workflow with excellent tooling, productivity visualization, dark moss development environment, sage green efficient workflows, cream-colored documentation and CLI tools, 8K resolution, 16:9 aspect ratio, DX design
95 - API Versioning
-------------------
Multiple API versions coexisting, deprecation and migration visualization, dark moss API timeline, sage green current version, cream-colored legacy support, 8K resolution, 16:9 aspect ratio, API versioning aesthetic
96 - Marketplace Platform
-------------------------
Two-sided marketplace connecting sellers and buyers, platform architecture visualization, dark moss marketplace environment, sage green transaction flows, cream-colored product listings, 8K resolution, 16:9 aspect ratio, marketplace design
97 - Payment Integration Providers
----------------------------------
Multiple payment providers unified through single integration, payment method icons, dark moss checkout environment, sage green successful transactions, cream-colored payment options, 8K resolution, 16:9 aspect ratio, payment integration aesthetic
98 - Digital Product Delivery
-----------------------------
Digital products being delivered instantly after purchase, download and access visualization, dark moss delivery system, sage green completed deliveries, cream-colored product files, 8K resolution, 16:9 aspect ratio, digital delivery design
99 - Affiliate Referral Systems
-------------------------------
Referral network with tracked conversions and commissions, viral growth visualization, dark moss affiliate environment, sage green successful referrals, cream-colored commission payouts, 8K resolution, 16:9 aspect ratio, referral system aesthetic
100 - Scaling SaaS Infrastructure
---------------------------------
Infrastructure scaling from small to massive, auto-scaling visualization with growing servers, dark moss cloud environment, sage green healthy instances, cream-colored traffic growth graph, 8K resolution, 16:9 aspect ratio, infrastructure scaling design
================================================================================
END OF PROMPTS
================================================================================
+1
View File
@@ -6,6 +6,7 @@ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone', output: 'standalone',
outputFileTracingRoot: process.cwd(),
// Enable React strict mode for better error detection // Enable React strict mode for better error detection
reactStrictMode: true, reactStrictMode: true,
+44 -33
View File
@@ -19,49 +19,60 @@
"test:coverage": "vitest run --coverage" "test:coverage": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"@mdx-js/loader": "^3.1.1", "@mdx-js/loader": "^3.1.0",
"@mdx-js/mdx": "^3.1.1", "@mdx-js/mdx": "^3.1.0",
"@mdx-js/react": "^3.1.1", "@mdx-js/react": "^3.1.0",
"@next-safe/middleware": "^0.10.0", "@next-safe/middleware": "^0.10.0",
"@next/mdx": "^16.1.6", "@next/mdx": "^16.1.0",
"@supabase/ssr": "^0.8.0", "@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3", "@supabase/supabase-js": "^2.39.7",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.29.2", "framer-motion": "^11.18.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lucide-react": "^0.563.0", "lucide-react": "^0.475.0",
"next": "^16.1.6", "next": "^16.1.0",
"next-intl": "^4.8.1", "next-intl": "^4.7.2",
"next-mdx-remote": "^5.0.0", "openai": "^4.77.0",
"openai": "^6.17.0", "pg": "^8.13.1",
"pg": "^8.18.0", "react": "^19.2.0",
"react": "^19.2.4", "react-dom": "^19.2.0",
"react-dom": "^19.2.4",
"react-ga4": "^2.1.0", "react-ga4": "^2.1.0",
"react-intersection-observer": "^10.0.2", "react-intersection-observer": "^9.8.1",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"web-vitals": "^5.1.0" "web-vitals": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.58.1", "@playwright/test": "^1.58.0",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/forms": "^0.5.11",
"@testing-library/jest-dom": "^6.9.1", "@tailwindcss/postcss": "^4.1.0",
"@testing-library/react": "^16.3.2", "@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.1.0",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"@types/node": "^25.1.0", "@types/node": "^22.0.0",
"@types/pg": "^8.16.0", "@types/pg": "^8.11.10",
"@types/react": "^19.2.10", "@types/react": "^19.2.0",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.0",
"@vitest/coverage-v8": "4.0.18", "@vitest/coverage-v8": "1.6.1",
"autoprefixer": "^10.4.24", "eslint": "^9.9.1",
"eslint": "^9.39.2", "eslint-config-next": "^16.1.0",
"eslint-config-next": "^16.1.6", "jsdom": "^23.2.0",
"jsdom": "^27.4.0",
"playwright-lighthouse": "^4.0.0", "playwright-lighthouse": "^4.0.0",
"postcss": "^8.5.6", "postcss": "^8.4.35",
"sharp": "^0.34.5", "sharp": "^0.34.3",
"typescript": "^5.9.3", "tailwindcss": "^4.1.0",
"vitest": "^4.0.18" "typescript": "5.9.3",
"vitest": "^1.3.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@swc/core",
"esbuild",
"sharp",
"unrs-resolver"
],
"ignoredBuiltDependencies": []
} }
} }
+14 -22
View File
@@ -24,8 +24,8 @@ export default defineConfig({
// Retry on CI only // Retry on CI only
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
// Opt out of parallel tests on CI // Limit workers to avoid overwhelming the dev server
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : 4,
// Reporter to use // Reporter to use
reporter: [ reporter: [
@@ -35,8 +35,8 @@ export default defineConfig({
// Shared settings for all projects // Shared settings for all projects
use: { use: {
// Base URL to use in actions like `await page.goto('/')` // Base URL - test against running Docker container
baseURL: 'http://localhost:3000', baseURL: process.env.BASE_URL || 'http://localhost:3001',
// Collect trace when retrying the failed test // Collect trace when retrying the failed test
trace: 'on-first-retry', trace: 'on-first-retry',
@@ -45,16 +45,14 @@ export default defineConfig({
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
}, },
// Configure projects - Chromium only for Lighthouse compatibility // Configure projects for E2E testing
// Note: --remote-debugging-port removed to allow parallel test execution.
// For Lighthouse performance tests, use a separate config with a single worker.
projects: [ projects: [
{ {
name: 'chromium', name: 'chromium',
use: { use: {
...devices['Desktop Chrome'], ...devices['Desktop Chrome'],
// Launch options for Lighthouse
launchOptions: {
args: ['--remote-debugging-port=9222'],
},
}, },
}, },
// Mobile Chrome for responsive testing // Mobile Chrome for responsive testing
@@ -62,9 +60,6 @@ export default defineConfig({
name: 'mobile-chrome', name: 'mobile-chrome',
use: { use: {
...devices['Pixel 5'], ...devices['Pixel 5'],
launchOptions: {
args: ['--remote-debugging-port=9223'],
},
}, },
}, },
// Tablet viewport for responsive testing // Tablet viewport for responsive testing
@@ -72,20 +67,17 @@ export default defineConfig({
name: 'tablet', name: 'tablet',
use: { use: {
viewport: { width: 768, height: 1024 }, viewport: { width: 768, height: 1024 },
launchOptions: {
args: ['--remote-debugging-port=9224'],
},
}, },
}, },
], ],
// Configure web server to start before tests // Use running Docker container - no webServer needed
webServer: { // webServer: {
command: 'npm run dev', // command: 'pnpm dev --port 3100',
url: 'http://localhost:3000', // url: 'http://localhost:3100',
reuseExistingServer: !process.env.CI, // reuseExistingServer: false,
timeout: 120000, // timeout: 120000,
}, // },
// Global timeout for each test // Global timeout for each test
timeout: 60000, timeout: 60000,
+1846 -1189
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
ignoredBuiltDependencies:
- '@parcel/watcher'
- '@swc/core'
- esbuild
- sharp
- unrs-resolver
-1
View File
@@ -1,6 +1,5 @@
module.exports = { module.exports = {
plugins: { plugins: {
'@tailwindcss/postcss': {}, '@tailwindcss/postcss': {},
autoprefixer: {},
}, },
}; };
+6 -14
View File
@@ -1,16 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000"> <rect width="32" height="32" rx="6" fill="#181C14"/>
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) --> <g transform="translate(2.5, 4.5) scale(0.027)">
<defs> <path fill="#ECDFCC" d="M965.47,97.09l-461.75,786.4L34.81,97.09h930.65M1000,77.5H0l17.59,29.5,468.89,786.4,17.35,29.1,17.15-29.22L982.73,106.88l17.27-29.38h0Z"/>
<style> <polygon fill="#ECDFCC" points="755.12 213.96 524.03 214.02 544.73 249.03 734.41 249 830.87 249 782.41 330.91 759.95 368.91 687.8 490.96 639.56 572.54 591.35 490.96 519.2 368.98 615.65 368.95 639.56 409.41 663.47 368.95 685.97 330.91 593.15 330.95 496.71 330.95 448.28 249.05 427.57 214.02 379.13 132.11 475.57 132.11 803.57 132.05 900.03 132.02 851.57 213.96 755.12 213.96"/>
.st0 { <polygon fill="#ECDFCC" points="618.8 607.63 570.59 689.19 548.33 726.83 500.09 808.39 451.88 726.83 240.28 369.01 217.81 331 169.39 249.08 265.85 249.08 314.27 330.97 336.73 369.01 500.09 645.29 522.35 607.63 381.23 368.98 358.76 330.97 310.33 249.05 289.61 214.06 245.11 214.06 148.69 214.08 100.24 132.17 196.69 132.14 241.17 132.14 337.64 132.11 386.05 214.02 406.76 249.05 455.21 330.97 477.68 368.98 570.59 526.08 618.8 607.63"/>
fill: #d6d7d8;
}
</style>
</defs>
<path class="st0" d="M965.47,97.09l-461.75,786.4L34.81,97.09h930.65M1000,77.5H0l17.59,29.5,468.89,786.4,17.35,29.1,17.15-29.22L982.73,106.88l17.27-29.38h0Z"/>
<g>
<polygon class="st0" points="755.12 213.96 524.03 214.02 544.73 249.03 734.41 249 830.87 249 782.41 330.91 759.95 368.91 687.8 490.96 639.56 572.54 591.35 490.96 519.2 368.98 615.65 368.95 639.56 409.41 663.47 368.95 685.97 330.91 593.15 330.95 496.71 330.95 448.28 249.05 427.57 214.02 379.13 132.11 475.57 132.11 803.57 132.05 900.03 132.02 851.57 213.96 755.12 213.96"/>
<polygon class="st0" points="618.8 607.63 570.59 689.19 548.33 726.83 500.09 808.39 451.88 726.83 240.28 369.01 217.81 331 169.39 249.08 265.85 249.08 314.27 330.97 336.73 369.01 500.09 645.29 522.35 607.63 381.23 368.98 358.76 330.97 310.33 249.05 289.61 214.06 245.11 214.06 148.69 214.08 100.24 132.17 196.69 132.14 241.17 132.14 337.64 132.11 386.05 214.02 406.76 249.05 455.21 330.97 477.68 368.98 570.59 526.08 618.8 607.63"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB

+4 -4
View File
@@ -121,10 +121,10 @@ export default async function AboutPage({ params }: Props) {
<section id="skills" className="py-24 relative"> <section id="skills" className="py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold text-white mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
{t('skills.title')} {t('skills.title')}
</h2> </h2>
<p className="text-zinc-400 max-w-2xl mx-auto"> <p className="text-muted-foreground max-w-2xl mx-auto">
{t('skills.description')} {t('skills.description')}
</p> </p>
</div> </div>
@@ -136,10 +136,10 @@ export default async function AboutPage({ params }: Props) {
<section className="py-24 relative"> <section className="py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold text-white mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
{t('workflow.title')} {t('workflow.title')}
</h2> </h2>
<p className="text-zinc-400 max-w-2xl mx-auto"> <p className="text-muted-foreground max-w-2xl mx-auto">
{t('workflow.description')} {t('workflow.description')}
</p> </p>
</div> </div>
+7 -7
View File
@@ -1335,7 +1335,7 @@ export default async function BlogPostPage({ params }: Props) {
{/* Back Link */} {/* Back Link */}
<Link <Link
href={`/${locale}/blog`} href={`/${locale}/blog`}
className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors mb-8" className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-8"
> >
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
@@ -1345,22 +1345,22 @@ export default async function BlogPostPage({ params }: Props) {
{/* Header */} {/* Header */}
<header className="mb-12"> <header className="mb-12">
<div className="flex items-center gap-4 mb-6 text-zinc-500"> <div className="flex items-center gap-4 mb-6 text-muted-foreground">
<time dateTime={post.date}>{getFormattedDate(post.date)}</time> <time dateTime={post.date}>{getFormattedDate(post.date)}</time>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
<span>{readingTime} min read</span> <span>{readingTime} min read</span>
</div> </div>
<span className="px-3 py-1 bg-zinc-800 rounded-full text-sm"> <span className="px-3 py-1 bg-card rounded-full text-sm">
{post.category} {post.category}
</span> </span>
</div> </div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6"> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground mb-6">
{post.title} {post.title}
</h1> </h1>
<p className="text-xl text-zinc-400"> <p className="text-xl text-muted-foreground">
{post.excerpt} {post.excerpt}
</p> </p>
@@ -1369,7 +1369,7 @@ export default async function BlogPostPage({ params }: Props) {
{post.tags?.map((tag, index) => ( {post.tags?.map((tag, index) => (
<span <span
key={index} key={index}
className="px-3 py-1 bg-zinc-800/50 text-sm text-zinc-400 rounded-full ring-1 ring-zinc-700/50" className="px-3 py-1 bg-card/50 text-sm text-muted-foreground rounded-full ring-1 ring-border/50"
> >
{tag} {tag}
</span> </span>
@@ -1378,7 +1378,7 @@ export default async function BlogPostPage({ params }: Props) {
</header> </header>
{/* Cover Image */} {/* Cover Image */}
<div className="aspect-[16/9] w-full mb-12 rounded-xl overflow-hidden bg-zinc-800 relative"> <div className="aspect-[16/9] w-full mb-12 rounded-xl overflow-hidden bg-card relative">
<Image <Image
src={post.coverImage} src={post.coverImage}
alt={post.title} alt={post.title}
+47 -36
View File
@@ -6,6 +6,7 @@ import Image from 'next/image';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { getAllBlogPosts, BlogPost } from '@/lib/blog'; import { getAllBlogPosts, BlogPost } from '@/lib/blog';
import { legacyBlogPosts } from '@/data/legacyBlogPosts'; import { legacyBlogPosts } from '@/data/legacyBlogPosts';
import { CollectionPageJsonLd } from '@/components/seo';
const POSTS_PER_PAGE = 12; const POSTS_PER_PAGE = 12;
@@ -128,26 +129,26 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
return ( return (
<Link <Link
href={`/${locale}/blog/${post.slug}`} href={`/${locale}/blog/${post.slug}`}
className="group relative block bg-zinc-900/50 backdrop-blur-sm className="group relative block bg-background/50 backdrop-blur-sm
rounded-xl shadow-lg hover:shadow-2xl rounded-xl shadow-lg hover:shadow-2xl
ring-1 ring-zinc-800 hover:ring-zinc-700 ring-1 ring-border hover:ring-accent
transition-all duration-500 focus:outline-none focus:ring-2 transition-all duration-500 focus:outline-none focus:ring-2
focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background
transform hover:-translate-y-1" transform hover:-translate-y-1"
> >
<div className="relative overflow-hidden rounded-xl"> <div className="relative overflow-hidden rounded-xl">
{/* Image Container */} {/* Image Container */}
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900"> <div className="aspect-[16/9] w-full relative overflow-hidden bg-background">
<BlogImage <BlogImage
src={getImagePath(post.coverImage)} src={getImagePath(post.coverImage)}
alt={post.title} alt={post.title}
/> />
<div className="absolute inset-x-0 -bottom-20 top-0 bg-gradient-to-t from-zinc-900 via-zinc-900/70 to-transparent opacity-95" /> <div className="absolute inset-x-0 -bottom-20 top-0 bg-gradient-to-t from-background via-background/70 to-transparent opacity-95" />
</div> </div>
{/* Content */} {/* Content */}
<div className="relative -mt-2 p-6 bg-zinc-900 z-10"> <div className="relative -mt-2 p-6 bg-background z-10">
<div className="flex items-center gap-4 mb-4 text-zinc-500"> <div className="flex items-center gap-4 mb-4 text-muted-foreground">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span>{getFormattedDate(post.date, locale)}</span> <span>{getFormattedDate(post.date, locale)}</span>
@@ -166,12 +167,12 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
)} )}
</div> </div>
<h3 className="text-xl font-semibold text-white mb-3 <h3 className="text-xl font-semibold text-foreground mb-3
group-hover:text-zinc-100 transition-colors duration-300"> group-hover:text-foreground transition-colors duration-300">
{post.title} {post.title}
</h3> </h3>
<p className="text-zinc-400 text-sm line-clamp-2 mb-4 <p className="text-muted-foreground text-sm line-clamp-2 mb-4
group-hover:text-zinc-300 transition-colors duration-300"> group-hover:text-foreground/80 transition-colors duration-300">
{post.excerpt} {post.excerpt}
</p> </p>
@@ -180,17 +181,17 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
{post.tags?.slice(0, 3).map((tag, index) => ( {post.tags?.slice(0, 3).map((tag, index) => (
<span <span
key={index} key={index}
className="px-3 py-1 bg-zinc-800/50 className="px-3 py-1 bg-card/50
text-sm text-zinc-400 rounded-full text-sm text-muted-foreground rounded-full
ring-1 ring-zinc-700/50 group-hover:ring-zinc-600/50 ring-1 ring-border/50 group-hover:ring-ring/50
group-hover:bg-zinc-800/70 group-hover:text-zinc-300 group-hover:bg-card/70 group-hover:text-foreground/80
transition-all duration-300" transition-all duration-300"
> >
{tag} {tag}
</span> </span>
))} ))}
{post.tags && post.tags.length > 3 && ( {post.tags && post.tags.length > 3 && (
<span className="text-sm text-zinc-600"> <span className="text-sm text-muted-foreground/60">
+{post.tags.length - 3} more +{post.tags.length - 3} more
</span> </span>
)} )}
@@ -198,11 +199,11 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
{/* Link Icon */} {/* Link Icon */}
<div className="absolute top-6 right-6 p-2.5 rounded-full <div className="absolute top-6 right-6 p-2.5 rounded-full
bg-zinc-800/60 backdrop-blur-sm ring-1 ring-zinc-700/50 bg-card/60 backdrop-blur-sm ring-1 ring-border/50
opacity-0 group-hover:opacity-100 opacity-0 group-hover:opacity-100
transform translate-y-2 group-hover:translate-y-0 transform translate-y-2 group-hover:translate-y-0
transition-all duration-300"> transition-all duration-300">
<ExternalLink className="h-4 w-4 text-zinc-400" /> <ExternalLink className="h-4 w-4 text-muted-foreground" />
</div> </div>
</div> </div>
</div> </div>
@@ -265,9 +266,9 @@ function Pagination({
{currentPage > 1 ? ( {currentPage > 1 ? (
<Link <Link
href={`/${locale}/blog${currentPage > 2 ? `?page=${currentPage - 1}` : ''}`} href={`/${locale}/blog${currentPage > 2 ? `?page=${currentPage - 1}` : ''}`}
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground hover:text-foreground
bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50 bg-card/50 hover:bg-card rounded-lg ring-1 ring-border/50
hover:ring-zinc-600 transition-all duration-200" hover:ring-accent transition-all duration-200"
aria-label={t('ui.pagination.previous')} aria-label={t('ui.pagination.previous')}
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
@@ -275,8 +276,8 @@ function Pagination({
</Link> </Link>
) : ( ) : (
<span <span
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-600 className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground/60
bg-zinc-800/30 rounded-lg ring-1 ring-zinc-800/50 cursor-not-allowed" bg-card/30 rounded-lg ring-1 ring-border/50 cursor-not-allowed"
aria-disabled="true" aria-disabled="true"
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
@@ -289,7 +290,7 @@ function Pagination({
{getPageNumbers().map((page, index) => { {getPageNumbers().map((page, index) => {
if (page === 'ellipsis') { if (page === 'ellipsis') {
return ( return (
<span key={`ellipsis-${index}`} className="px-2 text-zinc-600"> <span key={`ellipsis-${index}`} className="px-2 text-muted-foreground/60">
... ...
</span> </span>
); );
@@ -305,7 +306,7 @@ function Pagination({
ring-1 transition-all duration-200 ring-1 transition-all duration-200
${isCurrentPage ${isCurrentPage
? 'bg-[#697565] text-white ring-[#697565] font-medium' ? 'bg-[#697565] text-white ring-[#697565] font-medium'
: 'text-zinc-400 hover:text-white bg-zinc-800/50 hover:bg-zinc-800 ring-zinc-700/50 hover:ring-zinc-600' : 'text-muted-foreground hover:text-foreground bg-card/50 hover:bg-card ring-border/50 hover:ring-accent'
}`} }`}
aria-current={isCurrentPage ? 'page' : undefined} aria-current={isCurrentPage ? 'page' : undefined}
> >
@@ -319,9 +320,9 @@ function Pagination({
{currentPage < totalPages ? ( {currentPage < totalPages ? (
<Link <Link
href={`/${locale}/blog?page=${currentPage + 1}`} href={`/${locale}/blog?page=${currentPage + 1}`}
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground hover:text-foreground
bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50 bg-card/50 hover:bg-card rounded-lg ring-1 ring-border/50
hover:ring-zinc-600 transition-all duration-200" hover:ring-accent transition-all duration-200"
aria-label={t('ui.pagination.next')} aria-label={t('ui.pagination.next')}
> >
<span className="hidden sm:inline">{t('ui.pagination.next')}</span> <span className="hidden sm:inline">{t('ui.pagination.next')}</span>
@@ -329,8 +330,8 @@ function Pagination({
</Link> </Link>
) : ( ) : (
<span <span
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-600 className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground/60
bg-zinc-800/30 rounded-lg ring-1 ring-zinc-800/50 cursor-not-allowed" bg-card/30 rounded-lg ring-1 ring-border/50 cursor-not-allowed"
aria-disabled="true" aria-disabled="true"
> >
<span className="hidden sm:inline">{t('ui.pagination.next')}</span> <span className="hidden sm:inline">{t('ui.pagination.next')}</span>
@@ -370,23 +371,33 @@ export default async function BlogPage({ params, searchParams }: Props) {
const endIndex = startIndex + POSTS_PER_PAGE; const endIndex = startIndex + POSTS_PER_PAGE;
const posts = allPosts.slice(startIndex, endIndex); const posts = allPosts.slice(startIndex, endIndex);
// Map posts for CollectionPage schema
const collectionItems = allPosts.slice(0, 20).map((post) => ({
name: post.title,
description: post.excerpt,
url: `/${locale}/blog/${post.slug}`,
dateCreated: post.date,
technologies: post.tags,
}));
return ( return (
<main className="min-h-screen"> <main className="min-h-screen">
<CollectionPageJsonLd locale={locale as 'de' | 'en' | 'sr'} projects={collectionItems} />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
{/* Header Section */} {/* Header Section */}
<div className="mb-12 sm:mb-16 lg:mb-20"> <div className="mb-12 sm:mb-16 lg:mb-20">
<div className="flex items-center gap-4 mb-8"> <div className="flex items-center gap-4 mb-8">
<ArrowRight className="h-5 w-5 text-zinc-500" /> <ArrowRight className="h-5 w-5 text-muted-foreground" />
<h2 className="text-white text-sm tracking-wider">BLOG</h2> <h2 className="text-foreground text-sm tracking-wider">BLOG</h2>
</div> </div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6"> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground mb-6">
{t('meta.header.title')} {t('meta.header.title')}
</h1> </h1>
<p className="text-lg sm:text-xl text-zinc-400 max-w-2xl"> <p className="text-lg sm:text-xl text-muted-foreground max-w-2xl">
{t('meta.header.subtitle')} {t('meta.header.subtitle')}
</p> </p>
{/* Post count */} {/* Post count */}
<p className="text-sm text-zinc-500 mt-4"> <p className="text-sm text-muted-foreground mt-4">
{t('ui.pagination.showing', { {t('ui.pagination.showing', {
start: startIndex + 1, start: startIndex + 1,
end: Math.min(endIndex, allPosts.length), end: Math.min(endIndex, allPosts.length),
@@ -413,7 +424,7 @@ export default async function BlogPage({ params, searchParams }: Props) {
{/* Empty State */} {/* Empty State */}
{posts.length === 0 && ( {posts.length === 0 && (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-zinc-400">{t('ui.errors.posts')}</p> <p className="text-muted-foreground">{t('ui.errors.posts')}</p>
</div> </div>
)} )}
</div> </div>
+21 -21
View File
@@ -221,21 +221,21 @@ export default async function ImprintPage({ params }: Props) {
<nav className="mb-8"> <nav className="mb-8">
<Link <Link
href={`/${locale}`} href={`/${locale}`}
className="text-zinc-400 hover:text-white transition-colors" className="text-muted-foreground hover:text-foreground transition-colors"
> >
Home Home
</Link> </Link>
<span className="mx-2 text-zinc-600">/</span> <span className="mx-2 text-muted-foreground/60">/</span>
<span className="text-white">{c.title}</span> <span className="text-foreground">{c.title}</span>
</nav> </nav>
<h1 className="text-4xl font-bold text-white mb-8">{c.title}</h1> <h1 className="text-4xl font-bold text-foreground mb-8">{c.title}</h1>
<div className="prose prose-invert max-w-none space-y-8"> <div className="prose prose-invert max-w-none space-y-8">
{/* Contact */} {/* Contact */}
<section className="bg-zinc-900/50 p-6 rounded-xl ring-1 ring-zinc-800"> <section className="bg-background/50 p-6 rounded-xl ring-1 ring-border">
<h2 className="text-2xl font-semibold text-white mb-4">{c.contact.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.contact.title}</h2>
<div className="text-zinc-300 space-y-1"> <div className="text-foreground/80 space-y-1">
{c.contact.content.map((line, index) => ( {c.contact.content.map((line, index) => (
<p key={index}>{line}</p> <p key={index}>{line}</p>
))} ))}
@@ -244,25 +244,25 @@ export default async function ImprintPage({ params }: Props) {
{/* Liability */} {/* Liability */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.liability.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.liability.title}</h2>
{c.liability.sections.map((section, index) => ( {c.liability.sections.map((section, index) => (
<div key={index} className="mb-4"> <div key={index} className="mb-4">
<h3 className="text-xl font-medium text-white mb-2">{section.title}</h3> <h3 className="text-xl font-medium text-foreground mb-2">{section.title}</h3>
<p className="text-zinc-300">{section.content}</p> <p className="text-foreground/80">{section.content}</p>
</div> </div>
))} ))}
</section> </section>
{/* Copyright */} {/* Copyright */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.copyright.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.copyright.title}</h2>
<p className="text-zinc-300">{c.copyright.content}</p> <p className="text-foreground/80">{c.copyright.content}</p>
</section> </section>
{/* Images */} {/* Images */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.images.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.images.title}</h2>
<ul className="list-disc list-inside text-zinc-300 space-y-1"> <ul className="list-disc list-inside text-foreground/80 space-y-1">
{c.images.content.map((item, index) => ( {c.images.content.map((item, index) => (
<li key={index}>{item}</li> <li key={index}>{item}</li>
))} ))}
@@ -271,8 +271,8 @@ export default async function ImprintPage({ params }: Props) {
{/* Privacy */} {/* Privacy */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.privacy.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.privacy.title}</h2>
<p className="text-zinc-300"> <p className="text-foreground/80">
{c.privacy.content}{' '} {c.privacy.content}{' '}
<Link href={`/${locale}/privacy`} className="text-orange-500 hover:text-orange-400 underline"> <Link href={`/${locale}/privacy`} className="text-orange-500 hover:text-orange-400 underline">
{c.privacy.link} {c.privacy.link}
@@ -282,15 +282,15 @@ export default async function ImprintPage({ params }: Props) {
</section> </section>
{/* Technical */} {/* Technical */}
<section className="bg-zinc-900/50 p-6 rounded-xl ring-1 ring-zinc-800"> <section className="bg-background/50 p-6 rounded-xl ring-1 ring-border">
<h2 className="text-2xl font-semibold text-white mb-4">{c.technical.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.technical.title}</h2>
<p className="text-zinc-300">{c.technical.design}</p> <p className="text-foreground/80">{c.technical.design}</p>
<p className="text-zinc-300">{c.technical.tech}</p> <p className="text-foreground/80">{c.technical.tech}</p>
</section> </section>
{/* Copyright Notice */} {/* Copyright Notice */}
<section> <section>
<p className="text-zinc-400"> <p className="text-muted-foreground">
&copy; {new Date().getFullYear()} Damjan Savić. {c.copyrightNotice} &copy; {new Date().getFullYear()} Damjan Savić. {c.copyrightNotice}
</p> </p>
</section> </section>
+12 -10
View File
@@ -3,17 +3,13 @@ import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server'; import { getMessages, setRequestLocale } from 'next-intl/server';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import { locales, localeMetadata, seoKeywords, type Locale } from '@/i18n/config'; import { locales, localeMetadata, seoKeywords, type Locale } from '@/i18n/config';
import type { Metadata } from 'next'; import type { Metadata, Viewport } from 'next';
import { Header, Footer, GlobalBackground } from '@/components/layout'; import { Header, Footer, GlobalBackground } from '@/components/layout';
import { PersonJsonLd, ProfessionalServiceJsonLd, OrganizationJsonLd, WebSiteJsonLd } from '@/components/seo'; import { PersonJsonLd, ProfessionalServiceJsonLd, OrganizationJsonLd, WebSiteJsonLd } from '@/components/seo';
import { WebVitals } from '@/components/analytics'; import { WebVitals } from '@/components/analytics';
import { ChatbotLoader } from '@/components/chat/ChatbotLoader'; import { ChatbotLoader } from '@/components/chat/ChatbotLoader';
import '../globals.css'; import '../globals.css';
// Enable SSR - render pages on each request
export const dynamic = 'force-dynamic';
export const revalidate = 0;
const inter = Inter({ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
display: 'swap', display: 'swap',
@@ -25,6 +21,12 @@ type Props = {
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
}; };
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
viewportFit: 'cover',
};
export function generateStaticParams() { export function generateStaticParams() {
return locales.map((locale) => ({ locale })); return locales.map((locale) => ({ locale }));
} }
@@ -43,9 +45,9 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
}; };
const descriptions: Record<Locale, string> = { const descriptions: Record<Locale, string> = {
de: 'Fullstack Entwicklung für Websites, Apps und SaaS mit Next.js, React und TypeScript.', de: 'Fullstack Developer & KI Entwickler Websites, Apps und SaaS mit Next.js, React, TypeScript und Claude AI. Künstliche Intelligenz & AI Agents aus Köln.',
en: 'Fullstack development for websites, apps and SaaS with Next.js, React and TypeScript.', en: 'Fullstack Developer & AI Expert Websites, apps and SaaS with Next.js, React, TypeScript and Claude AI. Artificial intelligence & AI agent development.',
sr: 'Fullstack razvoj za web stranice, aplikacije i SaaS sa Next.js, React i TypeScript.', sr: 'Fullstack Developer & AI Expert Web stranice, aplikacije i SaaS sa Next.js, React, TypeScript i Claude AI. Veštačka inteligencija i AI agenti.',
}; };
// Generate alternates using localeMetadata // Generate alternates using localeMetadata
@@ -120,12 +122,12 @@ export default async function LocaleLayout({ children, params }: Props) {
<OrganizationJsonLd locale={locale as Locale} /> <OrganizationJsonLd locale={locale as Locale} />
<WebSiteJsonLd locale={locale as Locale} /> <WebSiteJsonLd locale={locale as Locale} />
</head> </head>
<body className="min-h-screen bg-zinc-900 text-zinc-50 antialiased"> <body className="min-h-screen bg-background text-foreground antialiased">
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
<div className="min-h-screen flex flex-col relative"> <div className="min-h-screen flex flex-col relative">
<GlobalBackground /> <GlobalBackground />
<Header /> <Header />
<main className="flex-1 pt-16 relative z-10">{children}</main> <main className="flex-1 pt-14 sm:pt-16 relative z-10">{children}</main>
<Footer /> <Footer />
</div> </div>
{/* AI Chatbot - lazy loaded for performance */} {/* AI Chatbot - lazy loaded for performance */}
+109 -30
View File
@@ -4,7 +4,7 @@ import Link from 'next/link';
import { ArrowLeft, ArrowRight, CheckCircle, Brain, Mic, Workflow, Cloud, Code, Bot, Building, ShoppingCart, Factory } from 'lucide-react'; import { ArrowLeft, ArrowRight, CheckCircle, Brain, Mic, Workflow, Cloud, Code, Bot, Building, ShoppingCart, Factory } from 'lucide-react';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { allServices, getServiceBySlug, getAllServiceSlugs } from '@/data/services'; import { allServices, getServiceBySlug, getAllServiceSlugs } from '@/data/services';
import { BreadcrumbJsonLd, ServiceJsonLd } from '@/components/seo'; import { BreadcrumbJsonLd, ServiceJsonLd, FAQJsonLd } from '@/components/seo';
import { type Locale, locales } from '@/i18n/config'; import { type Locale, locales } from '@/i18n/config';
type Props = { type Props = {
@@ -51,15 +51,17 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const localePath = locale === 'de' ? `/leistungen/${slug}` : `/${locale}/leistungen/${slug}`; const localePath = locale === 'de' ? `/leistungen/${slug}` : `/${locale}/leistungen/${slug}`;
// Enhanced keywords for English // Use seoKeywords if available, otherwise fall back to technologies
const enhancedKeywords = locale === 'en' const seoKw = service.seoKeywords?.[locale as Locale] || [];
? [...service.technologies, 'Remote AI Developer', 'Hire AI Developer', 'AI Consultant USA', 'AI Developer UK'].join(', ') const allKeywords = [...service.technologies, ...seoKw];
: service.technologies.join(', '); if (locale === 'en') {
allKeywords.push('Remote AI Developer', 'Hire AI Developer', 'AI Consultant USA', 'AI Developer UK');
}
return { return {
title, title,
description: service.description[locale as Locale] || service.description.de, description: service.description[locale as Locale] || service.description.de,
keywords: enhancedKeywords, keywords: allKeywords.join(', '),
alternates: { alternates: {
canonical: `${BASE_URL}${localePath}`, canonical: `${BASE_URL}${localePath}`,
languages: { languages: {
@@ -76,6 +78,14 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
type: 'website', type: 'website',
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US', locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')), alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')),
images: [
{
url: `${BASE_URL}/images/og-image.avif`,
width: 1200,
height: 630,
alt: `${service.name[locale as Locale]} - Damjan Savić`,
},
],
}, },
}; };
} }
@@ -101,6 +111,48 @@ export default async function ServicePage({ params }: Props) {
// Get related services (excluding current) // Get related services (excluding current)
const relatedServices = allServices.filter((s) => s.slug !== slug).slice(0, 3); const relatedServices = allServices.filter((s) => s.slug !== slug).slice(0, 3);
// FAQ items for this service
const serviceFaqItems = locale === 'de' ? [
{
question: `Was kostet ${service.name.de}?`,
answer: `Die Kosten für ${service.name.de} variieren je nach Projektumfang und Komplexität. Ich erstelle gerne ein individuelles Angebot nach einem kostenlosen Erstgespräch.`,
},
{
question: `Welche Technologien nutzen Sie für ${service.name.de}?`,
answer: `Für ${service.name.de} setze ich auf bewährte Technologien: ${service.technologies.join(', ')}. Die genaue Auswahl hängt von Ihren Anforderungen ab.`,
},
{
question: `Wie lange dauert ein typisches ${service.name.de}-Projekt?`,
answer: `Die Dauer eines ${service.name.de}-Projekts hängt vom Umfang ab. Einfache Projekte können in 2-4 Wochen abgeschlossen werden, komplexere Lösungen in 6-12 Wochen.`,
},
] : locale === 'en' ? [
{
question: `How much does ${service.name.en} cost?`,
answer: `The cost of ${service.name.en} varies depending on project scope and complexity. I'm happy to provide a custom quote after a free initial consultation.`,
},
{
question: `What technologies do you use for ${service.name.en}?`,
answer: `For ${service.name.en}, I use proven technologies: ${service.technologies.join(', ')}. The exact selection depends on your requirements.`,
},
{
question: `How long does a typical ${service.name.en} project take?`,
answer: `The duration of a ${service.name.en} project depends on scope. Simple projects can be completed in 2-4 weeks, more complex solutions in 6-12 weeks.`,
},
] : [
{
question: `Koliko košta ${service.name.sr}?`,
answer: `Troškovi za ${service.name.sr} variraju u zavisnosti od obima i kompleksnosti projekta. Rado ću vam dati individualizovanu ponudu nakon besplatnog inicijalnog razgovora.`,
},
{
question: `Koje tehnologije koristite za ${service.name.sr}?`,
answer: `Za ${service.name.sr} koristim proverene tehnologije: ${service.technologies.join(', ')}. Tačan izbor zavisi od vaših zahteva.`,
},
{
question: `Koliko traje tipičan ${service.name.sr} projekat?`,
answer: `Trajanje ${service.name.sr} projekta zavisi od obima. Jednostavni projekti mogu se završiti za 2-4 nedelje, kompleksnija rešenja za 6-12 nedelja.`,
},
];
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} /> <BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
@@ -110,6 +162,7 @@ export default async function ServicePage({ params }: Props) {
url={`/${locale}/leistungen/${slug}`} url={`/${locale}/leistungen/${slug}`}
locale={locale as Locale} locale={locale as Locale}
/> />
<FAQJsonLd items={serviceFaqItems} locale={locale as Locale} />
{/* Hero Section */} {/* Hero Section */}
<section className="py-24"> <section className="py-24">
@@ -117,23 +170,23 @@ export default async function ServicePage({ params }: Props) {
{/* Back Link */} {/* Back Link */}
<Link <Link
href={`/${locale}/leistungen`} href={`/${locale}/leistungen`}
className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors mb-8" className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-8"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
{locale === 'de' ? 'Alle Leistungen' : locale === 'sr' ? 'Sve usluge' : 'All Services'} {locale === 'de' ? 'Alle Leistungen' : locale === 'sr' ? 'Sve usluge' : 'All Services'}
</Link> </Link>
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-4 mb-6">
<div className="p-4 bg-zinc-800/50 rounded-xl"> <div className="p-4 bg-card/50 rounded-xl">
<IconComponent className="h-8 w-8 text-white" /> <IconComponent className="h-8 w-8 text-foreground" />
</div> </div>
</div> </div>
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6"> <h1 className="text-4xl md:text-5xl font-bold text-foreground mb-6">
{service.name[locale as Locale]} {service.name[locale as Locale]}
</h1> </h1>
<p className="text-xl text-zinc-400 mb-8"> <p className="text-xl text-muted-foreground mb-8">
{service.description[locale as Locale]} {service.description[locale as Locale]}
</p> </p>
@@ -142,7 +195,7 @@ export default async function ServicePage({ params }: Props) {
{service.technologies.map((tech) => ( {service.technologies.map((tech) => (
<span <span
key={tech} key={tech}
className="px-3 py-1 bg-zinc-800/50 border border-zinc-700 text-sm text-zinc-300 rounded-full" className="px-3 py-1 bg-card/50 border border-border text-sm text-foreground/80 rounded-full"
> >
{tech} {tech}
</span> </span>
@@ -152,19 +205,19 @@ export default async function ServicePage({ params }: Props) {
</section> </section>
{/* Features Section */} {/* Features Section */}
<section className="py-16 bg-zinc-800/20"> <section className="py-16 bg-card/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8"> <h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Was ich anbiete' : locale === 'sr' ? 'Šta nudim' : 'What I offer'} {locale === 'de' ? 'Was ich anbiete' : locale === 'sr' ? 'Šta nudim' : 'What I offer'}
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{service.features[locale as Locale].map((feature, index) => ( {service.features[locale as Locale].map((feature, index) => (
<div <div
key={index} key={index}
className="flex items-start gap-3 p-4 bg-zinc-800/30 rounded-lg" className="flex items-start gap-3 p-4 bg-card/30 rounded-lg"
> >
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-zinc-300">{feature}</span> <span className="text-foreground/80">{feature}</span>
</div> </div>
))} ))}
</div> </div>
@@ -174,20 +227,20 @@ export default async function ServicePage({ params }: Props) {
{/* Use Cases Section */} {/* Use Cases Section */}
<section className="py-16"> <section className="py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8"> <h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Anwendungsbeispiele' : locale === 'sr' ? 'Primeri primene' : 'Use Cases'} {locale === 'de' ? 'Anwendungsbeispiele' : locale === 'sr' ? 'Primeri primene' : 'Use Cases'}
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{service.useCases[locale as Locale].map((useCase, index) => ( {service.useCases[locale as Locale].map((useCase, index) => (
<div <div
key={index} key={index}
className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-xl" className="p-6 bg-card/30 border border-border rounded-xl"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-zinc-700 rounded-full flex items-center justify-center text-sm font-semibold text-white"> <div className="w-8 h-8 bg-accent rounded-full flex items-center justify-center text-sm font-semibold text-foreground">
{index + 1} {index + 1}
</div> </div>
<span className="text-zinc-200">{useCase}</span> <span className="text-foreground/90">{useCase}</span>
</div> </div>
</div> </div>
))} ))}
@@ -196,9 +249,9 @@ export default async function ServicePage({ params }: Props) {
</section> </section>
{/* Related Services */} {/* Related Services */}
<section className="py-16 bg-zinc-800/20"> <section className="py-16 bg-card/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8"> <h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Weitere Leistungen' : locale === 'sr' ? 'Ostale usluge' : 'Other Services'} {locale === 'de' ? 'Weitere Leistungen' : locale === 'sr' ? 'Ostale usluge' : 'Other Services'}
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
@@ -208,15 +261,15 @@ export default async function ServicePage({ params }: Props) {
<Link <Link
key={relatedService.slug} key={relatedService.slug}
href={`/${locale}/leistungen/${relatedService.slug}`} href={`/${locale}/leistungen/${relatedService.slug}`}
className="group p-6 bg-zinc-800/30 border border-zinc-800 rounded-xl hover:border-zinc-700 transition-all duration-300" className="group p-6 bg-card/30 border border-border rounded-xl hover:border-accent transition-all duration-300"
> >
<div className="p-2 bg-zinc-800/50 rounded-lg w-fit mb-3"> <div className="p-2 bg-card/50 rounded-lg w-fit mb-3">
<RelatedIcon className="h-5 w-5 text-zinc-400 group-hover:text-white transition-colors" /> <RelatedIcon className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
</div> </div>
<h3 className="text-lg font-semibold text-white mb-2"> <h3 className="text-lg font-semibold text-foreground mb-2">
{relatedService.name[locale as Locale]} {relatedService.name[locale as Locale]}
</h3> </h3>
<p className="text-sm text-zinc-400"> <p className="text-sm text-muted-foreground">
{relatedService.shortDescription[locale as Locale]} {relatedService.shortDescription[locale as Locale]}
</p> </p>
</Link> </Link>
@@ -226,17 +279,43 @@ export default async function ServicePage({ params }: Props) {
</div> </div>
</section> </section>
{/* FAQ Section */}
<section className="py-16">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground text-center mb-8">
{locale === 'de'
? `Häufige Fragen zu ${service.name.de}`
: locale === 'sr'
? `Česta pitanja o ${service.name.sr}`
: `FAQ about ${service.name.en}`}
</h2>
<div className="space-y-4">
{serviceFaqItems.map((item, index) => (
<div
key={index}
className="bg-card/30 border border-border rounded-lg p-6"
>
<h3 className="text-lg font-semibold text-foreground mb-2">
{item.question}
</h3>
<p className="text-muted-foreground">{item.answer}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */} {/* CTA Section */}
<section className="py-24"> <section className="py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold text-white mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
{locale === 'de' {locale === 'de'
? `${service.name.de} für Ihr Unternehmen?` ? `${service.name.de} für Ihr Unternehmen?`
: locale === 'sr' : locale === 'sr'
? `${service.name.sr} za vašu kompaniju?` ? `${service.name.sr} za vašu kompaniju?`
: `${service.name.en} for your business?`} : `${service.name.en} for your business?`}
</h2> </h2>
<p className="text-zinc-400 mb-8"> <p className="text-muted-foreground mb-8">
{locale === 'de' {locale === 'de'
? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.' ? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.'
: locale === 'sr' : locale === 'sr'
@@ -245,7 +324,7 @@ export default async function ServicePage({ params }: Props) {
</p> </p>
<Link <Link
href={`/${locale}/contact`} href={`/${locale}/contact`}
className="inline-flex items-center px-8 py-4 bg-white text-zinc-900 font-semibold rounded-lg hover:bg-zinc-100 transition-colors" className="inline-flex items-center px-8 py-4 bg-primary text-background font-semibold rounded-lg hover:bg-accent transition-colors"
> >
{locale === 'de' ? 'Projekt besprechen' : locale === 'sr' ? 'Razgovarajte o projektu' : 'Discuss project'} {locale === 'de' ? 'Projekt besprechen' : locale === 'sr' ? 'Razgovarajte o projektu' : 'Discuss project'}
<ArrowRight className="h-5 w-5 ml-2" /> <ArrowRight className="h-5 w-5 ml-2" />
+63 -39
View File
@@ -6,6 +6,7 @@ import Image from 'next/image';
import { Brain, Mic, Workflow, Cloud, Code, ArrowRight, MapPin, Bot, Building, ShoppingCart, Factory } from 'lucide-react'; import { Brain, Mic, Workflow, Cloud, Code, ArrowRight, MapPin, Bot, Building, ShoppingCart, Factory } from 'lucide-react';
import { services, techServices, industryServices } from '@/data/services'; import { services, techServices, industryServices } from '@/data/services';
import { cities } from '@/data/cities'; import { cities } from '@/data/cities';
import { technologies } from '@/data/technologies';
import { BreadcrumbJsonLd, FAQJsonLd } from '@/components/seo'; import { BreadcrumbJsonLd, FAQJsonLd } from '@/components/seo';
import { type Locale } from '@/i18n/config'; import { type Locale } from '@/i18n/config';
@@ -238,10 +239,10 @@ export default async function ServicesPage({ params }: Props) {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="text-center lg:text-left"> <div className="text-center lg:text-left">
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6"> <h1 className="text-4xl md:text-5xl font-bold text-foreground mb-6">
{pageTitle} {pageTitle}
</h1> </h1>
<p className="text-xl text-zinc-400 max-w-2xl"> <p className="text-xl text-muted-foreground max-w-2xl">
{pageSubtitle} {pageSubtitle}
</p> </p>
</div> </div>
@@ -261,7 +262,7 @@ export default async function ServicesPage({ params }: Props) {
{/* Core Services Grid */} {/* Core Services Grid */}
<section className="pb-16"> <section className="pb-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">{coreServicesTitle}</h2> <h2 className="text-2xl font-bold text-foreground mb-8">{coreServicesTitle}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{services.map((service) => { {services.map((service) => {
const IconComponent = iconMap[service.icon] || Brain; const IconComponent = iconMap[service.icon] || Brain;
@@ -269,28 +270,28 @@ export default async function ServicesPage({ params }: Props) {
<Link <Link
key={service.slug} key={service.slug}
href={`/${locale}/leistungen/${service.slug}`} href={`/${locale}/leistungen/${service.slug}`}
className="group bg-zinc-800/30 border border-zinc-800 rounded-xl p-6 hover:border-zinc-700 transition-all duration-300" className="group bg-card/30 border border-border rounded-xl p-6 hover:border-accent transition-all duration-300"
> >
<div className="p-3 bg-zinc-800/50 rounded-lg w-fit mb-4"> <div className="p-3 bg-card/50 rounded-lg w-fit mb-4">
<IconComponent className="h-6 w-6 text-zinc-400 group-hover:text-white transition-colors" /> <IconComponent className="h-6 w-6 text-muted-foreground group-hover:text-foreground transition-colors" />
</div> </div>
<h3 className="text-xl font-semibold text-white mb-2"> <h3 className="text-xl font-semibold text-foreground mb-2">
{service.name[locale as Locale]} {service.name[locale as Locale]}
</h3> </h3>
<p className="text-zinc-400 mb-4"> <p className="text-muted-foreground mb-4">
{service.shortDescription[locale as Locale]} {service.shortDescription[locale as Locale]}
</p> </p>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{service.technologies.slice(0, 4).map((tech) => ( {service.technologies.slice(0, 4).map((tech) => (
<span <span
key={tech} key={tech}
className="px-2 py-1 bg-zinc-800/50 text-xs text-zinc-400 rounded" className="px-2 py-1 bg-card/50 text-xs text-muted-foreground rounded"
> >
{tech} {tech}
</span> </span>
))} ))}
</div> </div>
<div className="flex items-center text-zinc-400 group-hover:text-white transition-colors"> <div className="flex items-center text-muted-foreground group-hover:text-foreground transition-colors">
<span className="text-sm"> <span className="text-sm">
{locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'} {locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'}
</span> </span>
@@ -306,7 +307,7 @@ export default async function ServicesPage({ params }: Props) {
{/* Technology Services Grid */} {/* Technology Services Grid */}
<section className="pb-16"> <section className="pb-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">{techServicesTitle}</h2> <h2 className="text-2xl font-bold text-foreground mb-8">{techServicesTitle}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{techServices.map((service) => { {techServices.map((service) => {
const IconComponent = iconMap[service.icon] || Brain; const IconComponent = iconMap[service.icon] || Brain;
@@ -314,28 +315,28 @@ export default async function ServicesPage({ params }: Props) {
<Link <Link
key={service.slug} key={service.slug}
href={`/${locale}/leistungen/${service.slug}`} href={`/${locale}/leistungen/${service.slug}`}
className="group bg-gradient-to-br from-zinc-800/40 to-zinc-800/20 border border-zinc-700/50 rounded-xl p-6 hover:border-zinc-600 transition-all duration-300" className="group bg-gradient-to-br from-card/40 to-card/20 border border-border/50 rounded-xl p-6 hover:border-accent transition-all duration-300"
> >
<div className="p-3 bg-zinc-700/50 rounded-lg w-fit mb-4"> <div className="p-3 bg-accent/50 rounded-lg w-fit mb-4">
<IconComponent className="h-6 w-6 text-zinc-300 group-hover:text-white transition-colors" /> <IconComponent className="h-6 w-6 text-foreground/80 group-hover:text-foreground transition-colors" />
</div> </div>
<h3 className="text-xl font-semibold text-white mb-2"> <h3 className="text-xl font-semibold text-foreground mb-2">
{service.name[locale as Locale]} {service.name[locale as Locale]}
</h3> </h3>
<p className="text-zinc-400 mb-4"> <p className="text-muted-foreground mb-4">
{service.shortDescription[locale as Locale]} {service.shortDescription[locale as Locale]}
</p> </p>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{service.technologies.slice(0, 4).map((tech) => ( {service.technologies.slice(0, 4).map((tech) => (
<span <span
key={tech} key={tech}
className="px-2 py-1 bg-zinc-700/50 text-xs text-zinc-300 rounded" className="px-2 py-1 bg-accent/50 text-xs text-foreground/80 rounded"
> >
{tech} {tech}
</span> </span>
))} ))}
</div> </div>
<div className="flex items-center text-zinc-400 group-hover:text-white transition-colors"> <div className="flex items-center text-muted-foreground group-hover:text-foreground transition-colors">
<span className="text-sm"> <span className="text-sm">
{locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'} {locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'}
</span> </span>
@@ -351,7 +352,7 @@ export default async function ServicesPage({ params }: Props) {
{/* Industry Services Grid */} {/* Industry Services Grid */}
<section className="pb-24"> <section className="pb-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8">{industryServicesTitle}</h2> <h2 className="text-2xl font-bold text-foreground mb-8">{industryServicesTitle}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{industryServices.map((service) => { {industryServices.map((service) => {
const IconComponent = iconMap[service.icon] || Brain; const IconComponent = iconMap[service.icon] || Brain;
@@ -359,28 +360,28 @@ export default async function ServicesPage({ params }: Props) {
<Link <Link
key={service.slug} key={service.slug}
href={`/${locale}/leistungen/${service.slug}`} href={`/${locale}/leistungen/${service.slug}`}
className="group bg-gradient-to-br from-zinc-800/30 to-zinc-900/30 border border-zinc-800 rounded-xl p-6 hover:border-zinc-600 transition-all duration-300" className="group bg-gradient-to-br from-card/30 to-background/30 border border-border rounded-xl p-6 hover:border-accent transition-all duration-300"
> >
<div className="p-3 bg-zinc-800/70 rounded-lg w-fit mb-4"> <div className="p-3 bg-card/70 rounded-lg w-fit mb-4">
<IconComponent className="h-6 w-6 text-zinc-400 group-hover:text-white transition-colors" /> <IconComponent className="h-6 w-6 text-muted-foreground group-hover:text-foreground transition-colors" />
</div> </div>
<h3 className="text-xl font-semibold text-white mb-2"> <h3 className="text-xl font-semibold text-foreground mb-2">
{service.name[locale as Locale]} {service.name[locale as Locale]}
</h3> </h3>
<p className="text-zinc-400 mb-4"> <p className="text-muted-foreground mb-4">
{service.shortDescription[locale as Locale]} {service.shortDescription[locale as Locale]}
</p> </p>
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{service.technologies.slice(0, 4).map((tech) => ( {service.technologies.slice(0, 4).map((tech) => (
<span <span
key={tech} key={tech}
className="px-2 py-1 bg-zinc-800/50 text-xs text-zinc-400 rounded" className="px-2 py-1 bg-card/50 text-xs text-muted-foreground rounded"
> >
{tech} {tech}
</span> </span>
))} ))}
</div> </div>
<div className="flex items-center text-zinc-400 group-hover:text-white transition-colors"> <div className="flex items-center text-muted-foreground group-hover:text-foreground transition-colors">
<span className="text-sm"> <span className="text-sm">
{locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'} {locale === 'de' ? 'Mehr erfahren' : locale === 'sr' ? 'Saznaj više' : 'Learn more'}
</span> </span>
@@ -393,22 +394,45 @@ export default async function ServicesPage({ params }: Props) {
</div> </div>
</section> </section>
{/* Technologies Section */}
<section className="pb-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Technologien' : locale === 'sr' ? 'Tehnologije' : 'Technologies'}
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{technologies.map((tech) => (
<Link
key={tech.slug}
href={`/${locale}/leistungen/technologie/${tech.slug}`}
className="group flex items-center gap-3 p-4 bg-card/30 border border-border rounded-lg hover:border-accent transition-all duration-300"
>
<Code className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
<span className="text-foreground/80 group-hover:text-foreground transition-colors font-medium">
{tech.name}
</span>
</Link>
))}
</div>
</div>
</section>
{/* Cities Section */} {/* Cities Section */}
<section className="py-24 bg-zinc-800/20"> <section className="py-24 bg-card/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl font-bold text-white mb-4">{citiesTitle}</h2> <h2 className="text-3xl font-bold text-foreground mb-4">{citiesTitle}</h2>
<p className="text-zinc-400">{citiesSubtitle}</p> <p className="text-muted-foreground">{citiesSubtitle}</p>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{cities.map((city) => ( {cities.map((city) => (
<Link <Link
key={city.slug} key={city.slug}
href={`/${locale}/leistungen/standort/${city.slug}`} href={`/${locale}/leistungen/standort/${city.slug}`}
className="group flex items-center gap-2 p-4 bg-zinc-800/30 border border-zinc-800 rounded-lg hover:border-zinc-700 transition-all duration-300" className="group flex items-center gap-2 p-4 bg-card/30 border border-border rounded-lg hover:border-accent transition-all duration-300"
> >
<MapPin className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors" /> <MapPin className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
<span className="text-zinc-300 group-hover:text-white transition-colors"> <span className="text-foreground/80 group-hover:text-foreground transition-colors">
{city.name[locale as Locale]} {city.name[locale as Locale]}
</span> </span>
</Link> </Link>
@@ -427,12 +451,12 @@ export default async function ServicesPage({ params }: Props) {
{faqItems.map((item, index) => ( {faqItems.map((item, index) => (
<div <div
key={index} key={index}
className="bg-zinc-800/30 border border-zinc-800 rounded-lg p-6" className="bg-card/30 border border-border rounded-lg p-6"
> >
<h3 className="text-lg font-semibold text-white mb-2"> <h3 className="text-lg font-semibold text-foreground mb-2">
{item.question} {item.question}
</h3> </h3>
<p className="text-zinc-400">{item.answer}</p> <p className="text-muted-foreground">{item.answer}</p>
</div> </div>
))} ))}
</div> </div>
@@ -440,7 +464,7 @@ export default async function ServicesPage({ params }: Props) {
</section> </section>
{/* CTA Section */} {/* CTA Section */}
<section className="py-24 bg-zinc-800/20"> <section className="py-24 bg-card/20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
<div className="relative aspect-video rounded-2xl overflow-hidden order-2 lg:order-1"> <div className="relative aspect-video rounded-2xl overflow-hidden order-2 lg:order-1">
@@ -453,10 +477,10 @@ export default async function ServicesPage({ params }: Props) {
/> />
</div> </div>
<div className="text-center lg:text-left order-1 lg:order-2"> <div className="text-center lg:text-left order-1 lg:order-2">
<h2 className="text-3xl font-bold text-white mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
{locale === 'de' ? 'Bereit für Ihr Projekt?' : locale === 'sr' ? 'Spremni za vaš projekat?' : 'Ready for your project?'} {locale === 'de' ? 'Bereit für Ihr Projekt?' : locale === 'sr' ? 'Spremni za vaš projekat?' : 'Ready for your project?'}
</h2> </h2>
<p className="text-zinc-400 mb-8"> <p className="text-muted-foreground mb-8">
{locale === 'de' {locale === 'de'
? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.' ? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.'
: locale === 'sr' : locale === 'sr'
@@ -465,7 +489,7 @@ export default async function ServicesPage({ params }: Props) {
</p> </p>
<Link <Link
href={`/${locale}/contact`} href={`/${locale}/contact`}
className="inline-flex items-center px-8 py-4 bg-white text-zinc-900 font-semibold rounded-lg hover:bg-zinc-100 transition-colors" className="inline-flex items-center px-8 py-4 bg-primary text-background font-semibold rounded-lg hover:bg-accent transition-colors"
> >
{locale === 'de' ? 'Kontakt aufnehmen' : locale === 'sr' ? 'Kontaktirajte me' : 'Get in touch'} {locale === 'de' ? 'Kontakt aufnehmen' : locale === 'sr' ? 'Kontaktirajte me' : 'Get in touch'}
<ArrowRight className="h-5 w-5 ml-2" /> <ArrowRight className="h-5 w-5 ml-2" />
@@ -12,6 +12,8 @@ type Props = {
params: Promise<{ locale: string; city: string }>; params: Promise<{ locale: string; city: string }>;
}; };
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = { const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
Brain, Brain,
Mic, Mic,
@@ -39,15 +41,15 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
} }
const titles: Record<string, string> = { const titles: Record<string, string> = {
de: `KI Entwickler ${city.name.de} - AI & Automatisierung | Damjan Savić`, de: `Fullstack Developer & KI Entwickler ${city.name.de} | Claude AI, Next.js, React | Damjan Savić`,
en: `AI Developer ${city.name.en} - AI & Automation | Damjan Savić`, en: `AI & Fullstack Developer ${city.name.en} | Claude AI, Next.js, React | Damjan Savić`,
sr: `AI Developer ${city.name.sr} - AI & Automatizacija | Damjan Savić`, sr: `AI & Fullstack Developer ${city.name.sr} | Claude AI, Next.js, React | Damjan Savić`,
}; };
const descriptions: Record<string, string> = { const descriptions: Record<string, string> = {
de: `Professionelle KI-Entwicklung und Automatisierung in ${city.name.de}. Voice AI, Prozessautomatisierung, SaaS-Entwicklung. Ihr AI & Automation Specialist für ${city.name.de} und ${city.region}.`, de: `Fullstack Developer & KI Entwickler in ${city.name.de}. Claude AI, Next.js, React, TypeScript, Tailwind CSS. Voice AI, Prozessautomatisierung, SaaS-Entwicklung. Ihr AI & Automation Specialist für ${city.name.de} und ${city.region}.`,
en: `Professional AI development and automation in ${city.name.en}. Voice AI, process automation, SaaS development. Your AI & Automation Specialist for ${city.name.en} and ${city.region}.`, en: `Fullstack Developer & AI Expert in ${city.name.en}. Claude AI, Next.js, React, TypeScript, Tailwind CSS. Voice AI, process automation, SaaS development. Your AI & Automation Specialist for ${city.name.en} and ${city.region}.`,
sr: `Profesionalni AI razvoj i automatizacija u ${city.name.sr}. Voice AI, automatizacija procesa, SaaS razvoj. Vaš AI & Automation Specialist za ${city.name.sr} i ${city.region}.`, sr: `Fullstack Developer & AI Expert u ${city.name.sr}. Claude AI, Next.js, React, TypeScript, Tailwind CSS. Voice AI, automatizacija procesa, SaaS razvoj. Vaš AI & Automation Specialist za ${city.name.sr} i ${city.region}.`,
}; };
return { return {
@@ -55,17 +57,27 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
description: descriptions[locale] || descriptions.de, description: descriptions[locale] || descriptions.de,
keywords: city.keywords[locale as Locale]?.join(', ') || city.keywords.de.join(', '), keywords: city.keywords[locale as Locale]?.join(', ') || city.keywords.de.join(', '),
alternates: { alternates: {
canonical: `/${locale}/leistungen/standort/${citySlug}`, canonical: `${BASE_URL}/${locale}/leistungen/standort/${citySlug}`,
languages: { languages: {
de: `/de/leistungen/standort/${citySlug}`, 'x-default': `${BASE_URL}/de/leistungen/standort/${citySlug}`,
en: `/en/leistungen/standort/${citySlug}`, de: `${BASE_URL}/de/leistungen/standort/${citySlug}`,
sr: `/sr/leistungen/standort/${citySlug}`, en: `${BASE_URL}/en/leistungen/standort/${citySlug}`,
sr: `${BASE_URL}/sr/leistungen/standort/${citySlug}`,
}, },
}, },
openGraph: { openGraph: {
title: titles[locale] || titles.de, title: titles[locale] || titles.de,
description: descriptions[locale] || descriptions.de, description: descriptions[locale] || descriptions.de,
type: 'website', type: 'website',
url: `${BASE_URL}/${locale}/leistungen/standort/${citySlug}`,
images: [
{
url: `${BASE_URL}/images/og-image.avif`,
width: 1200,
height: 630,
alt: `Damjan Savić - AI Developer ${city.name.en}`,
},
],
}, },
}; };
} }
@@ -91,6 +103,14 @@ export default async function CityPage({ params }: Props) {
question: `Bieten Sie Ihre Dienstleistungen auch vor Ort in ${city.name.de} an?`, question: `Bieten Sie Ihre Dienstleistungen auch vor Ort in ${city.name.de} an?`,
answer: `Ja, ich arbeite mit Unternehmen in ${city.name.de} und Umgebung. Die meisten Projekte wickle ich remote ab, aber persönliche Meetings in ${city.name.de} sind bei Bedarf möglich.`, answer: `Ja, ich arbeite mit Unternehmen in ${city.name.de} und Umgebung. Die meisten Projekte wickle ich remote ab, aber persönliche Meetings in ${city.name.de} sind bei Bedarf möglich.`,
}, },
{
question: `Entwickeln Sie mit Next.js und React in ${city.name.de}?`,
answer: `Ja, ich bin auf moderne Webentwicklung mit Next.js, React, TypeScript und Tailwind CSS spezialisiert. Für Unternehmen in ${city.name.de} entwickle ich performante, SEO-optimierte Web-Anwendungen und SaaS-Produkte mit diesem Tech-Stack.`,
},
{
question: `Setzen Sie Claude AI für Projekte in ${city.name.de} ein?`,
answer: `Ja, ich integriere Claude AI von Anthropic sowie andere KI-Modelle wie GPT-4 in Geschäftsprozesse. Ob KI-Agenten, Chatbots oder automatisierte Workflows ich setze modernste Künstliche Intelligenz für Unternehmen in ${city.name.de} ein.`,
},
{ {
question: `Welche Branchen bedienen Sie in ${city.name.de}?`, question: `Welche Branchen bedienen Sie in ${city.name.de}?`,
answer: `Ich arbeite branchenübergreifend mit Startups, mittelständischen Unternehmen und Konzernen in ${city.name.de}. Besonders häufig sind Projekte in den Bereichen E-Commerce, Fintech, Healthcare und B2B-Services.`, answer: `Ich arbeite branchenübergreifend mit Startups, mittelständischen Unternehmen und Konzernen in ${city.name.de}. Besonders häufig sind Projekte in den Bereichen E-Commerce, Fintech, Healthcare und B2B-Services.`,
@@ -104,6 +124,14 @@ export default async function CityPage({ params }: Props) {
question: `Do you also offer your services on-site in ${city.name.en}?`, question: `Do you also offer your services on-site in ${city.name.en}?`,
answer: `Yes, I work with companies in ${city.name.en} and the surrounding area. Most projects are handled remotely, but in-person meetings in ${city.name.en} are possible if needed.`, answer: `Yes, I work with companies in ${city.name.en} and the surrounding area. Most projects are handled remotely, but in-person meetings in ${city.name.en} are possible if needed.`,
}, },
{
question: `Do you develop with Next.js and React in ${city.name.en}?`,
answer: `Yes, I specialize in modern web development with Next.js, React, TypeScript, and Tailwind CSS. For companies in ${city.name.en}, I build performant, SEO-optimized web applications and SaaS products using this tech stack.`,
},
{
question: `Do you use Claude AI for projects in ${city.name.en}?`,
answer: `Yes, I integrate Claude AI by Anthropic as well as other AI models like GPT-4 into business processes. Whether AI agents, chatbots, or automated workflows I leverage cutting-edge artificial intelligence for companies in ${city.name.en}.`,
},
{ {
question: `Which industries do you serve in ${city.name.en}?`, question: `Which industries do you serve in ${city.name.en}?`,
answer: `I work across industries with startups, medium-sized companies and corporations in ${city.name.en}. Projects are particularly common in e-commerce, fintech, healthcare and B2B services.`, answer: `I work across industries with startups, medium-sized companies and corporations in ${city.name.en}. Projects are particularly common in e-commerce, fintech, healthcare and B2B services.`,
@@ -117,6 +145,14 @@ export default async function CityPage({ params }: Props) {
question: `Da li nudite usluge i na licu mesta u ${city.name.sr}?`, question: `Da li nudite usluge i na licu mesta u ${city.name.sr}?`,
answer: `Da, radim sa kompanijama u ${city.name.sr} i okolini. Većina projekata se obrađuje remote, ali lični sastanci u ${city.name.sr} su mogući po potrebi.`, answer: `Da, radim sa kompanijama u ${city.name.sr} i okolini. Većina projekata se obrađuje remote, ali lični sastanci u ${city.name.sr} su mogući po potrebi.`,
}, },
{
question: `Da li razvijate sa Next.js i React u ${city.name.sr}?`,
answer: `Da, specijalizovan sam za moderni web razvoj sa Next.js, React, TypeScript i Tailwind CSS. Za kompanije u ${city.name.sr} pravim performantne, SEO-optimizovane web aplikacije i SaaS proizvode sa ovim tech stack-om.`,
},
{
question: `Da li koristite Claude AI za projekte u ${city.name.sr}?`,
answer: `Da, integrisem Claude AI od Anthropic-a kao i druge AI modele poput GPT-4 u poslovne procese. Bilo da su u pitanju AI agenti, chatbotovi ili automatizovani radni tokovi koristim najsavremeniju veštačku inteligenciju za kompanije u ${city.name.sr}.`,
},
{ {
question: `Koje industrije opslužujete u ${city.name.sr}?`, question: `Koje industrije opslužujete u ${city.name.sr}?`,
answer: `Radim u svim industrijama sa startapima, srednjim preduzećima i korporacijama u ${city.name.sr}. Projekti su posebno česti u e-commerce, fintech, zdravstvu i B2B uslugama.`, answer: `Radim u svim industrijama sa startapima, srednjim preduzećima i korporacijama u ${city.name.sr}. Projekti su posebno česti u e-commerce, fintech, zdravstvu i B2B uslugama.`,
@@ -154,31 +190,31 @@ export default async function CityPage({ params }: Props) {
{/* Back Link */} {/* Back Link */}
<Link <Link
href={`/${locale}/leistungen`} href={`/${locale}/leistungen`}
className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors mb-8" className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-8"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
{locale === 'de' ? 'Alle Leistungen' : locale === 'sr' ? 'Sve usluge' : 'All Services'} {locale === 'de' ? 'Alle Leistungen' : locale === 'sr' ? 'Sve usluge' : 'All Services'}
</Link> </Link>
<div className="flex items-center gap-2 text-zinc-400 mb-4"> <div className="flex items-center gap-2 text-muted-foreground mb-4">
<MapPin className="h-5 w-5" /> <MapPin className="h-5 w-5" />
<span>{city.region}, Deutschland</span> <span>{city.region}</span>
</div> </div>
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6"> <h1 className="text-4xl md:text-5xl font-bold text-foreground mb-6">
{pageTitle} {pageTitle}
</h1> </h1>
<p className="text-xl text-zinc-400 mb-8"> <p className="text-xl text-muted-foreground mb-8">
{city.description[locale as Locale]} {city.description[locale as Locale]}
</p> </p>
{/* Keywords as tags */} {/* Keywords as tags */}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{city.keywords[locale as Locale]?.slice(0, 5).map((keyword) => ( {city.keywords[locale as Locale]?.map((keyword) => (
<span <span
key={keyword} key={keyword}
className="px-3 py-1 bg-zinc-800/50 border border-zinc-700 text-sm text-zinc-300 rounded-full" className="px-3 py-1 bg-card/50 border border-border text-sm text-foreground/80 rounded-full"
> >
{keyword} {keyword}
</span> </span>
@@ -188,9 +224,9 @@ export default async function CityPage({ params }: Props) {
</section> </section>
{/* Services Section */} {/* Services Section */}
<section className="py-16 bg-zinc-800/20"> <section className="py-16 bg-card/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8"> <h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' {locale === 'de'
? `Meine Leistungen in ${city.name.de}` ? `Meine Leistungen in ${city.name.de}`
: locale === 'sr' : locale === 'sr'
@@ -204,16 +240,16 @@ export default async function CityPage({ params }: Props) {
<Link <Link
key={service.slug} key={service.slug}
href={`/${locale}/leistungen/${service.slug}`} href={`/${locale}/leistungen/${service.slug}`}
className="group flex items-start gap-4 p-6 bg-zinc-800/30 border border-zinc-800 rounded-xl hover:border-zinc-700 transition-all duration-300" className="group flex items-start gap-4 p-6 bg-card/30 border border-border rounded-xl hover:border-accent transition-all duration-300"
> >
<div className="p-2 bg-zinc-800/50 rounded-lg"> <div className="p-2 bg-card/50 rounded-lg">
<IconComponent className="h-5 w-5 text-zinc-400 group-hover:text-white transition-colors" /> <IconComponent className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold text-white mb-1"> <h3 className="text-lg font-semibold text-foreground mb-1">
{service.name[locale as Locale]} {service.name[locale as Locale]}
</h3> </h3>
<p className="text-sm text-zinc-400"> <p className="text-sm text-muted-foreground">
{service.shortDescription[locale as Locale]} {service.shortDescription[locale as Locale]}
</p> </p>
</div> </div>
@@ -227,7 +263,7 @@ export default async function CityPage({ params }: Props) {
{/* Why Choose Me Section */} {/* Why Choose Me Section */}
<section className="py-16"> <section className="py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8"> <h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' {locale === 'de'
? `Warum mit mir arbeiten in ${city.name.de}?` ? `Warum mit mir arbeiten in ${city.name.de}?`
: locale === 'sr' : locale === 'sr'
@@ -264,10 +300,10 @@ export default async function CityPage({ params }: Props) {
).map((point, index) => ( ).map((point, index) => (
<div <div
key={index} key={index}
className="flex items-start gap-3 p-4 bg-zinc-800/30 rounded-lg" className="flex items-start gap-3 p-4 bg-card/30 rounded-lg"
> >
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" /> <CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-zinc-300">{point}</span> <span className="text-foreground/80">{point}</span>
</div> </div>
))} ))}
</div> </div>
@@ -275,16 +311,16 @@ export default async function CityPage({ params }: Props) {
</section> </section>
{/* Nearby Areas */} {/* Nearby Areas */}
<section className="py-16 bg-zinc-800/20"> <section className="py-16 bg-card/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-4"> <h2 className="text-2xl font-bold text-foreground mb-4">
{locale === 'de' {locale === 'de'
? 'Auch verfügbar in der Region' ? 'Auch verfügbar in der Region'
: locale === 'sr' : locale === 'sr'
? 'Takođe dostupno u regionu' ? 'Takođe dostupno u regionu'
: 'Also available in the region'} : 'Also available in the region'}
</h2> </h2>
<p className="text-zinc-400 mb-6"> <p className="text-muted-foreground mb-6">
{locale === 'de' {locale === 'de'
? `Neben ${city.name.de} arbeite ich auch mit Unternehmen in:` ? `Neben ${city.name.de} arbeite ich auch mit Unternehmen in:`
: locale === 'sr' : locale === 'sr'
@@ -295,7 +331,7 @@ export default async function CityPage({ params }: Props) {
{city.nearbyAreas.map((area) => ( {city.nearbyAreas.map((area) => (
<span <span
key={area} key={area}
className="px-4 py-2 bg-zinc-800/50 border border-zinc-700 text-zinc-300 rounded-lg" className="px-4 py-2 bg-card/50 border border-border text-foreground/80 rounded-lg"
> >
{area} {area}
</span> </span>
@@ -307,7 +343,7 @@ export default async function CityPage({ params }: Props) {
{/* Other Cities */} {/* Other Cities */}
<section className="py-16"> <section className="py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white mb-8"> <h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Weitere Standorte' : locale === 'sr' ? 'Ostale lokacije' : 'Other Locations'} {locale === 'de' ? 'Weitere Standorte' : locale === 'sr' ? 'Ostale lokacije' : 'Other Locations'}
</h2> </h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
@@ -315,10 +351,10 @@ export default async function CityPage({ params }: Props) {
<Link <Link
key={otherCity.slug} key={otherCity.slug}
href={`/${locale}/leistungen/standort/${otherCity.slug}`} href={`/${locale}/leistungen/standort/${otherCity.slug}`}
className="group flex items-center gap-2 p-4 bg-zinc-800/30 border border-zinc-800 rounded-lg hover:border-zinc-700 transition-all duration-300" className="group flex items-center gap-2 p-4 bg-card/30 border border-border rounded-lg hover:border-accent transition-all duration-300"
> >
<MapPin className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors" /> <MapPin className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
<span className="text-zinc-300 group-hover:text-white transition-colors"> <span className="text-foreground/80 group-hover:text-foreground transition-colors">
{otherCity.name[locale as Locale]} {otherCity.name[locale as Locale]}
</span> </span>
</Link> </Link>
@@ -328,9 +364,9 @@ export default async function CityPage({ params }: Props) {
</section> </section>
{/* FAQ Section */} {/* FAQ Section */}
<section className="py-16 bg-zinc-800/20"> <section className="py-16 bg-card/20">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-white text-center mb-8"> <h2 className="text-2xl font-bold text-foreground text-center mb-8">
{locale === 'de' {locale === 'de'
? `Häufige Fragen für ${city.name.de}` ? `Häufige Fragen für ${city.name.de}`
: locale === 'sr' : locale === 'sr'
@@ -341,12 +377,12 @@ export default async function CityPage({ params }: Props) {
{faqItems.map((item, index) => ( {faqItems.map((item, index) => (
<div <div
key={index} key={index}
className="bg-zinc-800/30 border border-zinc-800 rounded-lg p-6" className="bg-card/30 border border-border rounded-lg p-6"
> >
<h3 className="text-lg font-semibold text-white mb-2"> <h3 className="text-lg font-semibold text-foreground mb-2">
{item.question} {item.question}
</h3> </h3>
<p className="text-zinc-400">{item.answer}</p> <p className="text-muted-foreground">{item.answer}</p>
</div> </div>
))} ))}
</div> </div>
@@ -356,14 +392,14 @@ export default async function CityPage({ params }: Props) {
{/* CTA Section */} {/* CTA Section */}
<section className="py-24"> <section className="py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold text-white mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
{locale === 'de' {locale === 'de'
? `Projekt in ${city.name.de} starten?` ? `Projekt in ${city.name.de} starten?`
: locale === 'sr' : locale === 'sr'
? `Započnite projekat u ${city.name.sr}?` ? `Započnite projekat u ${city.name.sr}?`
: `Start a project in ${city.name.en}?`} : `Start a project in ${city.name.en}?`}
</h2> </h2>
<p className="text-zinc-400 mb-8"> <p className="text-muted-foreground mb-8">
{locale === 'de' {locale === 'de'
? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.' ? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.'
: locale === 'sr' : locale === 'sr'
@@ -372,7 +408,7 @@ export default async function CityPage({ params }: Props) {
</p> </p>
<Link <Link
href={`/${locale}/contact`} href={`/${locale}/contact`}
className="inline-flex items-center px-8 py-4 bg-white text-zinc-900 font-semibold rounded-lg hover:bg-zinc-100 transition-colors" className="inline-flex items-center px-8 py-4 bg-primary text-background font-semibold rounded-lg hover:bg-accent transition-colors"
> >
{locale === 'de' ? 'Kontakt aufnehmen' : locale === 'sr' ? 'Kontaktirajte me' : 'Get in touch'} {locale === 'de' ? 'Kontakt aufnehmen' : locale === 'sr' ? 'Kontaktirajte me' : 'Get in touch'}
<ArrowRight className="h-5 w-5 ml-2" /> <ArrowRight className="h-5 w-5 ml-2" />
@@ -0,0 +1,362 @@
import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next';
import Link from 'next/link';
import { ArrowLeft, ArrowRight, CheckCircle, Code, Zap, MapPin } from 'lucide-react';
import { notFound } from 'next/navigation';
import { technologies, getTechnologyBySlug, getAllTechnologySlugs } from '@/data/technologies';
import { allServices, getServiceBySlug } from '@/data/services';
import { cities } from '@/data/cities';
import { BreadcrumbJsonLd, ServiceJsonLd, FAQJsonLd } from '@/components/seo';
import { type Locale, locales } from '@/i18n/config';
type Props = {
params: Promise<{ locale: string; tech: string }>;
};
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
export async function generateStaticParams() {
const slugs = getAllTechnologySlugs();
return locales.flatMap((locale) =>
slugs.map((tech) => ({
locale,
tech,
}))
);
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale, tech: techSlug } = await params;
const tech = getTechnologyBySlug(techSlug);
if (!tech) {
return { title: 'Technology Not Found' };
}
const title = `${tech.title[locale as Locale]} | Damjan Savić`;
return {
title,
description: tech.description[locale as Locale],
keywords: tech.keywords[locale as Locale].join(', '),
alternates: {
canonical: `${BASE_URL}/${locale}/leistungen/technologie/${techSlug}`,
languages: {
'x-default': `${BASE_URL}/de/leistungen/technologie/${techSlug}`,
de: `${BASE_URL}/de/leistungen/technologie/${techSlug}`,
en: `${BASE_URL}/en/leistungen/technologie/${techSlug}`,
sr: `${BASE_URL}/sr/leistungen/technologie/${techSlug}`,
},
},
openGraph: {
title,
description: tech.description[locale as Locale],
url: `${BASE_URL}/${locale}/leistungen/technologie/${techSlug}`,
type: 'website',
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')),
images: [
{
url: `${BASE_URL}/images/og-image.avif`,
width: 1200,
height: 630,
alt: `${tech.name} Developer - Damjan Savić`,
},
],
},
};
}
export default async function TechnologyPage({ params }: Props) {
const { locale, tech: techSlug } = await params;
setRequestLocale(locale);
const tech = getTechnologyBySlug(techSlug);
if (!tech) {
notFound();
}
const breadcrumbItems = [
{ name: locale === 'de' ? 'Start' : locale === 'sr' ? 'Početna' : 'Home', url: `/${locale}` },
{ name: locale === 'de' ? 'Leistungen' : locale === 'sr' ? 'Usluge' : 'Services', url: `/${locale}/leistungen` },
{ name: tech.name, url: `/${locale}/leistungen/technologie/${techSlug}` },
];
const faqItems = locale === 'de' ? [
{
question: `Warum ${tech.name} für mein Projekt?`,
answer: `${tech.name} bietet hervorragende Performance, Skalierbarkeit und Entwicklerproduktivität. Als erfahrener ${tech.name} Entwickler kann ich Ihnen die beste Lösung für Ihre Anforderungen empfehlen.`,
},
{
question: `Was kostet ein ${tech.name}-Projekt?`,
answer: `Die Kosten hängen vom Projektumfang ab. Ich erstelle gerne ein individuelles Angebot nach einem kostenlosen Erstgespräch. Einfache Projekte starten ab ca. 2.000€.`,
},
{
question: `Wie lange dauert ein ${tech.name}-Projekt?`,
answer: `Je nach Komplexität zwischen 2 und 12 Wochen. Ein MVP kann in 4-6 Wochen realisiert werden. Ich arbeite agil mit wöchentlichen Updates.`,
},
] : locale === 'en' ? [
{
question: `Why choose ${tech.name} for my project?`,
answer: `${tech.name} offers excellent performance, scalability and developer productivity. As an experienced ${tech.name} developer, I can recommend the best solution for your requirements.`,
},
{
question: `How much does a ${tech.name} project cost?`,
answer: `The cost depends on the project scope. I'm happy to provide a custom quote after a free initial consultation. Simple projects start from around $2,200.`,
},
{
question: `How long does a ${tech.name} project take?`,
answer: `Depending on complexity, between 2 and 12 weeks. An MVP can be realized in 4-6 weeks. I work agile with weekly updates.`,
},
] : [
{
question: `Zašto odabrati ${tech.name} za moj projekat?`,
answer: `${tech.name} nudi odlične performanse, skalabilnost i produktivnost. Kao iskusni ${tech.name} developer, mogu vam preporučiti najbolje rešenje za vaše zahteve.`,
},
{
question: `Koliko košta ${tech.name} projekat?`,
answer: `Troškovi zavise od obima projekta. Rado ću vam dati individualizovanu ponudu nakon besplatnog inicijalnog razgovora. Jednostavni projekti počinju od oko 2.000€.`,
},
{
question: `Koliko traje ${tech.name} projekat?`,
answer: `U zavisnosti od kompleksnosti, između 2 i 12 nedelja. MVP se može realizovati za 4-6 nedelja. Radim agilno sa nedeljnim ažuriranjima.`,
},
];
// Related services
const relatedServiceData = tech.relatedServices
.map((slug) => getServiceBySlug(slug))
.filter(Boolean);
// Other technologies for cross-linking
const otherTechnologies = technologies.filter((t) => t.slug !== techSlug).slice(0, 4);
// Select cities for cross-linking (mix of regions)
const featuredCities = cities.filter((c) =>
['koeln', 'berlin', 'muenchen', 'london', 'new-york', 'beograd'].includes(c.slug)
);
return (
<div className="min-h-screen">
<BreadcrumbJsonLd items={breadcrumbItems} locale={locale as Locale} />
<ServiceJsonLd
name={`${tech.name} ${locale === 'de' ? 'Entwicklung' : locale === 'sr' ? 'Razvoj' : 'Development'}`}
description={tech.description[locale as Locale]}
url={`/${locale}/leistungen/technologie/${techSlug}`}
locale={locale as Locale}
/>
<FAQJsonLd items={faqItems} locale={locale as Locale} />
{/* Hero Section */}
<section className="py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<Link
href={`/${locale}/leistungen`}
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-8"
>
<ArrowLeft className="h-4 w-4" />
{locale === 'de' ? 'Alle Leistungen' : locale === 'sr' ? 'Sve usluge' : 'All Services'}
</Link>
<div className="flex items-center gap-4 mb-6">
<div className="p-4 bg-card/50 rounded-xl">
<Code className="h-8 w-8 text-foreground" />
</div>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-foreground mb-6">
{tech.title[locale as Locale]}
</h1>
<p className="text-xl text-muted-foreground mb-8">
{tech.description[locale as Locale]}
</p>
{/* Keywords as tags */}
<div className="flex flex-wrap gap-2">
{tech.keywords[locale as Locale].map((keyword) => (
<span
key={keyword}
className="px-3 py-1 bg-card/50 border border-border text-sm text-foreground/80 rounded-full"
>
{keyword}
</span>
))}
</div>
</div>
</section>
{/* Features Section */}
<section className="py-16 bg-card/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? `${tech.name} Was ich anbiete` : locale === 'sr' ? `${tech.name} Šta nudim` : `${tech.name} What I offer`}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tech.features[locale as Locale].map((feature, index) => (
<div
key={index}
className="flex items-start gap-3 p-4 bg-card/30 rounded-lg"
>
<CheckCircle className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
<span className="text-foreground/80">{feature}</span>
</div>
))}
</div>
</div>
</section>
{/* Use Cases Section */}
<section className="py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Anwendungsbeispiele' : locale === 'sr' ? 'Primeri primene' : 'Use Cases'}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{tech.useCases[locale as Locale].map((useCase, index) => (
<div
key={index}
className="p-6 bg-card/30 border border-border rounded-xl"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-accent rounded-full flex items-center justify-center text-sm font-semibold text-foreground">
{index + 1}
</div>
<span className="text-foreground/90">{useCase}</span>
</div>
</div>
))}
</div>
</div>
</section>
{/* Related Services */}
{relatedServiceData.length > 0 && (
<section className="py-16 bg-card/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Passende Leistungen' : locale === 'sr' ? 'Odgovarajuće usluge' : 'Related Services'}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{relatedServiceData.map((service) => service && (
<Link
key={service.slug}
href={`/${locale}/leistungen/${service.slug}`}
className="group flex items-start gap-4 p-6 bg-card/30 border border-border rounded-xl hover:border-accent transition-all duration-300"
>
<div className="p-2 bg-card/50 rounded-lg">
<Zap className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
<div>
<h3 className="text-lg font-semibold text-foreground mb-1">
{service.name[locale as Locale]}
</h3>
<p className="text-sm text-muted-foreground">
{service.shortDescription[locale as Locale]}
</p>
</div>
</Link>
))}
</div>
</div>
</section>
)}
{/* Other Technologies */}
<section className="py-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? 'Weitere Technologien' : locale === 'sr' ? 'Ostale Tehnologije' : 'Other Technologies'}
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{otherTechnologies.map((otherTech) => (
<Link
key={otherTech.slug}
href={`/${locale}/leistungen/technologie/${otherTech.slug}`}
className="group flex items-center gap-2 p-4 bg-card/30 border border-border rounded-lg hover:border-accent transition-all duration-300"
>
<Code className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
<span className="text-foreground/80 group-hover:text-foreground transition-colors">
{otherTech.name}
</span>
</Link>
))}
</div>
</div>
</section>
{/* Cities Cross-Link */}
<section className="py-16 bg-card/20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground mb-8">
{locale === 'de' ? `${tech.name} Entwickler in Ihrer Stadt` : locale === 'sr' ? `${tech.name} Developer u vašem gradu` : `${tech.name} Developer in your city`}
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{featuredCities.map((city) => (
<Link
key={city.slug}
href={`/${locale}/leistungen/standort/${city.slug}`}
className="group flex items-center gap-2 p-4 bg-card/30 border border-border rounded-lg hover:border-accent transition-all duration-300"
>
<MapPin className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
<span className="text-foreground/80 group-hover:text-foreground transition-colors">
{city.name[locale as Locale]}
</span>
</Link>
))}
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-16">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-bold text-foreground text-center mb-8">
{locale === 'de' ? `Häufige Fragen zu ${tech.name}` : locale === 'sr' ? `Česta pitanja o ${tech.name}` : `FAQ about ${tech.name}`}
</h2>
<div className="space-y-4">
{faqItems.map((item, index) => (
<div
key={index}
className="bg-card/30 border border-border rounded-lg p-6"
>
<h3 className="text-lg font-semibold text-foreground mb-2">
{item.question}
</h3>
<p className="text-muted-foreground">{item.answer}</p>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-24">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold text-foreground mb-4">
{locale === 'de'
? `${tech.name}-Projekt starten?`
: locale === 'sr'
? `Započnite ${tech.name} projekat?`
: `Start a ${tech.name} project?`}
</h2>
<p className="text-muted-foreground mb-8">
{locale === 'de'
? 'Lassen Sie uns besprechen, wie ich Ihnen helfen kann.'
: locale === 'sr'
? 'Razgovarajmo o tome kako vam mogu pomoći.'
: "Let's discuss how I can help you."}
</p>
<Link
href={`/${locale}/contact`}
className="inline-flex items-center px-8 py-4 bg-primary text-background font-semibold rounded-lg hover:bg-accent transition-colors"
>
{locale === 'de' ? 'Kontakt aufnehmen' : locale === 'sr' ? 'Kontaktirajte me' : 'Get in touch'}
<ArrowRight className="h-5 w-5 ml-2" />
</Link>
</div>
</section>
</div>
);
}
+33 -18
View File
@@ -12,38 +12,53 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const titles: Record<string, string> = { const titles: Record<string, string> = {
de: 'KI Entwickler Köln | AI Agents & Automatisierung | Damjan Savić', de: 'Fullstack Developer & KI Entwickler Köln | Claude AI, Next.js, React | Damjan Savić',
en: 'AI Developer Cologne | AI Agents & Automation | Damjan Savić', en: 'AI & Fullstack Developer | Claude AI, Next.js, React, TypeScript | Damjan Savić',
sr: 'AI Developer Keln | AI Agenti & Automatizacija | Damjan Savić', sr: 'AI & Fullstack Developer | Claude AI, Next.js, React | Damjan Savić',
}; };
const descriptions: Record<string, string> = { const descriptions: Record<string, string> = {
de: 'Freelance KI Entwickler aus Köln. Spezialisiert auf KI-Agenten, Voice AI, Prozessautomatisierung mit n8n und SaaS-Entwicklung. Über 7 Jahre Erfahrung.', de: 'Freelance Fullstack Developer & KI Entwickler aus Köln. Spezialisiert auf Claude AI, KI-Agenten, Next.js, React, TypeScript und Tailwind CSS. Prozessautomatisierung mit n8n, Voice AI und SaaS-Entwicklung. Über 7 Jahre Erfahrung in Künstlicher Intelligenz und Webentwicklung.',
en: 'Remote AI Developer & Automation Expert based in Germany. Building AI agents, Voice AI systems, and n8n automations for clients in USA, UK, and Europe. 7+ years experience. Available for international projects.', en: 'Remote Fullstack Developer & AI Expert based in Germany. Building Claude AI integrations, AI agents, and modern web apps with Next.js, React, TypeScript and Tailwind CSS. Voice AI, n8n automation, and SaaS development for clients in USA, UK, and Europe. 7+ years experience.',
sr: 'AI Developer i Automation Specialist iz Nemačke, poreklo iz Srbije. Specijalizovan za AI agente, Voice AI, n8n automatizaciju i SaaS razvoj. Radim sa klijentima iz Srbije, Nemačke i dijaspore. 7+ godina iskustva.', sr: 'AI & Fullstack Developer iz Nemačke, poreklo iz Srbije. Specijalizovan za Claude AI, AI agente, Next.js, React, TypeScript i Tailwind CSS. Voice AI, n8n automatizacija i SaaS razvoj za klijente iz Srbije, Nemačke i dijaspore. 7+ godina iskustva u veštačkoj inteligenciji.',
}; };
const keywords: Record<string, string[]> = { const keywords: Record<string, string[]> = {
de: [ de: [
'Fullstack Developer Köln',
'KI Entwickler Köln', 'KI Entwickler Köln',
'Claude AI Entwickler',
'Next.js Entwickler',
'React Entwickler',
'TypeScript Entwickler',
'Tailwind CSS Entwickler',
'Künstliche Intelligenz',
'AI Developer Deutschland', 'AI Developer Deutschland',
'KI Entwickler DACH', 'KI Entwickler DACH',
'AI Agentur Köln', 'AI Agentur Köln',
'KI-Agenten Entwicklung',
'Prozessautomatisierung', 'Prozessautomatisierung',
'n8n Entwickler', 'n8n Entwickler',
'Voice AI', 'Voice AI',
'KI-Agenten',
'Fullstack Entwickler',
'SaaS Entwicklung', 'SaaS Entwicklung',
'Automatisierung',
'GPT-4 Integration', 'GPT-4 Integration',
'Claude AI Integration',
'KI Entwickler Wien', 'KI Entwickler Wien',
'AI Developer Zürich', 'AI Developer Zürich',
'Python Entwickler',
'Freelancer Deutschland', 'Freelancer Deutschland',
'Claude AI Integration',
], ],
en: [ en: [
'Fullstack Developer',
'AI Developer', 'AI Developer',
'Claude AI Developer',
'Claude AI Integration',
'Next.js Developer',
'React Developer',
'TypeScript Developer',
'Tailwind CSS Developer',
'Artificial Intelligence',
'AI Agent Developer',
'Remote AI Developer', 'Remote AI Developer',
'AI Developer USA', 'AI Developer USA',
'AI Developer UK', 'AI Developer UK',
@@ -51,32 +66,32 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
'Process Automation Expert', 'Process Automation Expert',
'n8n Developer', 'n8n Developer',
'Voice AI Developer', 'Voice AI Developer',
'AI Agents Developer',
'Fullstack Developer',
'SaaS Development', 'SaaS Development',
'GPT-4 Integration', 'GPT-4 Integration',
'ChatGPT Integration', 'Python Developer',
'Remote Developer', 'Remote Developer',
'Freelance AI Developer', 'Freelance AI Developer',
'AI Consultant', 'AI Consultant',
'Automation Specialist',
], ],
sr: [ sr: [
'AI Developer Srbija', 'AI Developer Srbija',
'Fullstack Programer Srbija',
'Claude AI Developer',
'Next.js Programer',
'React Programer',
'TypeScript Programer',
'Veštačka Inteligencija',
'AI Programer Beograd', 'AI Programer Beograd',
'AI Developer Novi Sad', 'AI Developer Novi Sad',
'Fullstack Programer Srbija', 'AI Agenti',
'Automatizacija procesa', 'Automatizacija procesa',
'n8n Automatizacija', 'n8n Automatizacija',
'Voice AI Srbija', 'Voice AI Srbija',
'AI Agenti',
'SaaS Razvoj', 'SaaS Razvoj',
'GPT-4 Integracija',
'Python Programer Srbija', 'Python Programer Srbija',
'React Programer Beograd', 'React Programer Beograd',
'Freelance Developer Srbija', 'Freelance Developer Srbija',
'Remote Programer Nemačka', 'Remote Programer Nemačka',
'Srpski Developer Dijaspora',
'Web Razvoj Beograd', 'Web Razvoj Beograd',
], ],
}; };
+34 -34
View File
@@ -467,7 +467,7 @@ export default async function ProjectPage({ params }: Props) {
<div className="mb-8"> <div className="mb-8">
<Link <Link
href={`/${locale}/portfolio`} href={`/${locale}/portfolio`}
className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors duration-300" className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors duration-300"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
<span>{t('backToPortfolio')}</span> <span>{t('backToPortfolio')}</span>
@@ -476,7 +476,7 @@ export default async function ProjectPage({ params }: Props) {
{/* Project Header */} {/* Project Header */}
<div className="mb-12"> <div className="mb-12">
<div className="flex flex-wrap items-center gap-4 mb-6 text-zinc-400"> <div className="flex flex-wrap items-center gap-4 mb-6 text-muted-foreground">
{project.meta.date && ( {project.meta.date && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
@@ -497,18 +497,18 @@ export default async function ProjectPage({ params }: Props) {
)} )}
</div> </div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-8"> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground mb-8">
{project.meta.title} {project.meta.title}
</h1> </h1>
<p className="text-xl text-zinc-400"> <p className="text-xl text-muted-foreground">
{project.meta.description} {project.meta.description}
</p> </p>
</div> </div>
{/* Cover Image */} {/* Cover Image */}
<div className="mb-12"> <div className="mb-12">
<div className="relative aspect-video rounded-lg overflow-hidden bg-zinc-800"> <div className="relative aspect-video rounded-lg overflow-hidden bg-card">
<Image <Image
src={`/images/projects/${slug}/cover.avif`} src={`/images/projects/${slug}/cover.avif`}
alt={project.meta.title} alt={project.meta.title}
@@ -517,28 +517,28 @@ export default async function ProjectPage({ params }: Props) {
className="object-cover" className="object-cover"
priority priority
/> />
<div className="absolute inset-0 bg-gradient-to-t from-zinc-900/50 to-transparent opacity-50" /> <div className="absolute inset-0 bg-gradient-to-t from-background/50 to-transparent opacity-50" />
</div> </div>
</div> </div>
{/* Project Content */} {/* Project Content */}
<div className="prose prose-invert max-w-none"> <div className="prose prose-invert max-w-none">
{/* Intro */} {/* Intro */}
<div className="mb-12 p-6 bg-zinc-800/30 rounded-lg border border-zinc-800"> <div className="mb-12 p-6 bg-card/30 rounded-lg border border-border">
<p className="text-lg text-zinc-300 m-0"> <p className="text-lg text-foreground/80 m-0">
{project.content.intro} {project.content.intro}
</p> </p>
</div> </div>
{/* Challenge Section */} {/* Challenge Section */}
<section className="mb-12"> <section className="mb-12">
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.challenge.title}</h2> <h2 className="text-2xl font-bold mb-4 text-foreground">{project.content.challenge.title}</h2>
<p className="text-zinc-300 mb-4">{project.content.challenge.description}</p> <p className="text-foreground/80 mb-4">{project.content.challenge.description}</p>
{project.content.challenge.points && ( {project.content.challenge.points && (
<ul className="space-y-2"> <ul className="space-y-2">
{project.content.challenge.points.map((point, index) => ( {project.content.challenge.points.map((point, index) => (
<li key={index} className="text-zinc-300 flex items-start gap-2"> <li key={index} className="text-foreground/80 flex items-start gap-2">
<span className="text-zinc-500"></span> <span className="text-muted-foreground"></span>
{point} {point}
</li> </li>
))} ))}
@@ -548,13 +548,13 @@ export default async function ProjectPage({ params }: Props) {
{/* Solution Section */} {/* Solution Section */}
<section className="mb-12"> <section className="mb-12">
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.solution.title}</h2> <h2 className="text-2xl font-bold mb-4 text-foreground">{project.content.solution.title}</h2>
<p className="text-zinc-300 mb-4">{project.content.solution.description}</p> <p className="text-foreground/80 mb-4">{project.content.solution.description}</p>
{project.content.solution.points && ( {project.content.solution.points && (
<ul className="space-y-2"> <ul className="space-y-2">
{project.content.solution.points.map((point, index) => ( {project.content.solution.points.map((point, index) => (
<li key={index} className="text-zinc-300 flex items-start gap-2"> <li key={index} className="text-foreground/80 flex items-start gap-2">
<span className="text-zinc-500"></span> <span className="text-muted-foreground"></span>
{point} {point}
</li> </li>
))} ))}
@@ -565,20 +565,20 @@ export default async function ProjectPage({ params }: Props) {
{/* Technical Section */} {/* Technical Section */}
{project.content.technical && ( {project.content.technical && (
<section className="mb-12"> <section className="mb-12">
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.technical.title}</h2> <h2 className="text-2xl font-bold mb-4 text-foreground">{project.content.technical.title}</h2>
<p className="text-zinc-300 mb-4">{project.content.technical.description}</p> <p className="text-foreground/80 mb-4">{project.content.technical.description}</p>
{project.content.technical.points && ( {project.content.technical.points && (
<ul className="space-y-2"> <ul className="space-y-2">
{project.content.technical.points.map((point, index) => ( {project.content.technical.points.map((point, index) => (
<li key={index} className="text-zinc-300 flex items-start gap-2"> <li key={index} className="text-foreground/80 flex items-start gap-2">
<span className="text-zinc-500"></span> <span className="text-muted-foreground"></span>
{point} {point}
</li> </li>
))} ))}
</ul> </ul>
)} )}
{project.content.technical.code && ( {project.content.technical.code && (
<pre className="bg-zinc-800/50 p-4 rounded-lg overflow-x-auto mt-4"> <pre className="bg-card/50 p-4 rounded-lg overflow-x-auto mt-4">
<code className="text-sm">{project.content.technical.code}</code> <code className="text-sm">{project.content.technical.code}</code>
</pre> </pre>
)} )}
@@ -588,13 +588,13 @@ export default async function ProjectPage({ params }: Props) {
{/* Implementation Section */} {/* Implementation Section */}
{project.content.implementation && ( {project.content.implementation && (
<section className="mb-12"> <section className="mb-12">
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.implementation.title}</h2> <h2 className="text-2xl font-bold mb-4 text-foreground">{project.content.implementation.title}</h2>
<p className="text-zinc-300 mb-4">{project.content.implementation.description}</p> <p className="text-foreground/80 mb-4">{project.content.implementation.description}</p>
{project.content.implementation.points && ( {project.content.implementation.points && (
<ul className="space-y-2"> <ul className="space-y-2">
{project.content.implementation.points.map((point, index) => ( {project.content.implementation.points.map((point, index) => (
<li key={index} className="text-zinc-300 flex items-start gap-2"> <li key={index} className="text-foreground/80 flex items-start gap-2">
<span className="text-zinc-500"></span> <span className="text-muted-foreground"></span>
{point} {point}
</li> </li>
))} ))}
@@ -605,13 +605,13 @@ export default async function ProjectPage({ params }: Props) {
{/* Results Section */} {/* Results Section */}
<section className="mb-12"> <section className="mb-12">
<h2 className="text-2xl font-bold mb-4 text-white">{project.content.results.title}</h2> <h2 className="text-2xl font-bold mb-4 text-foreground">{project.content.results.title}</h2>
<p className="text-zinc-300 mb-4">{project.content.results.description}</p> <p className="text-foreground/80 mb-4">{project.content.results.description}</p>
{project.content.results.points && ( {project.content.results.points && (
<ul className="space-y-2"> <ul className="space-y-2">
{project.content.results.points.map((point, index) => ( {project.content.results.points.map((point, index) => (
<li key={index} className="text-zinc-300 flex items-start gap-2"> <li key={index} className="text-foreground/80 flex items-start gap-2">
<span className="text-zinc-500"></span> <span className="text-muted-foreground"></span>
{point} {point}
</li> </li>
))} ))}
@@ -621,8 +621,8 @@ export default async function ProjectPage({ params }: Props) {
{/* Conclusion */} {/* Conclusion */}
{project.content.conclusion && ( {project.content.conclusion && (
<section className="mb-12 p-6 bg-zinc-800/30 rounded-lg border border-zinc-800"> <section className="mb-12 p-6 bg-card/30 rounded-lg border border-border">
<p className="text-lg text-zinc-300 m-0"> <p className="text-lg text-foreground/80 m-0">
{project.content.conclusion} {project.content.conclusion}
</p> </p>
</section> </section>
@@ -631,14 +631,14 @@ export default async function ProjectPage({ params }: Props) {
{/* Technologies */} {/* Technologies */}
{project.meta.technologies && ( {project.meta.technologies && (
<div className="mt-12 pt-6 border-t border-zinc-800"> <div className="mt-12 pt-6 border-t border-border">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Tag className="h-4 w-4 text-zinc-400" /> <Tag className="h-4 w-4 text-muted-foreground" />
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{project.meta.technologies.map((tech, index) => ( {project.meta.technologies.map((tech, index) => (
<span <span
key={index} key={index}
className="px-3 py-1 bg-zinc-800/50 border border-zinc-800 rounded-full text-sm text-zinc-300" className="px-3 py-1 bg-card/50 border border-border rounded-full text-sm text-foreground/80"
> >
{tech} {tech}
</span> </span>
+1 -1
View File
@@ -216,7 +216,7 @@ export default async function PortfolioPage({ params, searchParams }: Props) {
<h1 className="text-3xl font-bold text-white mb-4"> <h1 className="text-3xl font-bold text-white mb-4">
{t('sections.title')} {t('sections.title')}
</h1> </h1>
<p className="text-zinc-400 max-w-2xl mx-auto"> <p className="text-muted-foreground max-w-2xl mx-auto">
{t('sections.subtitle')} {t('sections.subtitle')}
</p> </p>
</div> </div>
+54 -54
View File
@@ -481,28 +481,28 @@ export default async function PrivacyPage({ params }: Props) {
<nav className="mb-8"> <nav className="mb-8">
<Link <Link
href={`/${locale}`} href={`/${locale}`}
className="text-zinc-400 hover:text-white transition-colors" className="text-muted-foreground hover:text-foreground transition-colors"
> >
Home Home
</Link> </Link>
<span className="mx-2 text-zinc-600">/</span> <span className="mx-2 text-muted-foreground/60">/</span>
<span className="text-white">{c.title}</span> <span className="text-foreground">{c.title}</span>
</nav> </nav>
<h1 className="text-4xl font-bold text-white mb-2">{c.title}</h1> <h1 className="text-4xl font-bold text-foreground mb-2">{c.title}</h1>
<p className="text-zinc-400 mb-8">{c.lastUpdated}</p> <p className="text-muted-foreground mb-8">{c.lastUpdated}</p>
<div className="prose prose-invert max-w-none space-y-8"> <div className="prose prose-invert max-w-none space-y-8">
{/* Introduction */} {/* Introduction */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.intro.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.intro.title}</h2>
<p className="text-zinc-300">{c.intro.content}</p> <p className="text-foreground/80">{c.intro.content}</p>
</section> </section>
{/* Data Controller */} {/* Data Controller */}
<section className="bg-zinc-900/50 p-6 rounded-xl ring-1 ring-zinc-800"> <section className="bg-background/50 p-6 rounded-xl ring-1 ring-border">
<h2 className="text-2xl font-semibold text-white mb-4">{c.controller.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.controller.title}</h2>
<div className="text-zinc-300 space-y-1"> <div className="text-foreground/80 space-y-1">
{c.controller.content.map((line, index) => ( {c.controller.content.map((line, index) => (
<p key={index}>{line}</p> <p key={index}>{line}</p>
))} ))}
@@ -511,13 +511,13 @@ export default async function PrivacyPage({ params }: Props) {
{/* Data Collection */} {/* Data Collection */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.dataCollection.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.dataCollection.title}</h2>
{c.dataCollection.sections.map((section, index) => ( {c.dataCollection.sections.map((section, index) => (
<div key={index} className="mb-6"> <div key={index} className="mb-6">
<h3 className="text-xl font-medium text-white mb-2">{section.title}</h3> <h3 className="text-xl font-medium text-foreground mb-2">{section.title}</h3>
<p className="text-zinc-300 mb-2">{section.content}</p> <p className="text-foreground/80 mb-2">{section.content}</p>
{section.items && ( {section.items && (
<ul className="list-disc list-inside text-zinc-300 space-y-1 ml-4"> <ul className="list-disc list-inside text-foreground/80 space-y-1 ml-4">
{section.items.map((item, itemIndex) => ( {section.items.map((item, itemIndex) => (
<li key={itemIndex}>{item}</li> <li key={itemIndex}>{item}</li>
))} ))}
@@ -529,46 +529,46 @@ export default async function PrivacyPage({ params }: Props) {
{/* Cookies */} {/* Cookies */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.cookies.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.cookies.title}</h2>
<p className="text-zinc-300 mb-4">{c.cookies.intro}</p> <p className="text-foreground/80 mb-4">{c.cookies.intro}</p>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead> <thead>
<tr className="border-b border-zinc-700"> <tr className="border-b border-border">
<th className="py-2 pr-4 text-white font-semibold">Cookie</th> <th className="py-2 pr-4 text-foreground font-semibold">Cookie</th>
<th className="py-2 pr-4 text-white font-semibold">{locale === 'de' ? 'Zweck' : locale === 'sr' ? 'Svrha' : 'Purpose'}</th> <th className="py-2 pr-4 text-foreground font-semibold">{locale === 'de' ? 'Zweck' : locale === 'sr' ? 'Svrha' : 'Purpose'}</th>
<th className="py-2 text-white font-semibold">{locale === 'de' ? 'Dauer' : locale === 'sr' ? 'Trajanje' : 'Duration'}</th> <th className="py-2 text-foreground font-semibold">{locale === 'de' ? 'Dauer' : locale === 'sr' ? 'Trajanje' : 'Duration'}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{c.cookies.types.map((cookie, index) => ( {c.cookies.types.map((cookie, index) => (
<tr key={index} className="border-b border-zinc-800"> <tr key={index} className="border-b border-border">
<td className="py-3 pr-4 text-zinc-300">{cookie.name}</td> <td className="py-3 pr-4 text-foreground/80">{cookie.name}</td>
<td className="py-3 pr-4 text-zinc-400">{cookie.purpose}</td> <td className="py-3 pr-4 text-muted-foreground">{cookie.purpose}</td>
<td className="py-3 text-zinc-400">{cookie.duration}</td> <td className="py-3 text-muted-foreground">{cookie.duration}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
<p className="text-zinc-400 mt-4 text-sm">{c.cookies.management}</p> <p className="text-muted-foreground mt-4 text-sm">{c.cookies.management}</p>
</section> </section>
{/* Third-Party Services */} {/* Third-Party Services */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.thirdParty.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.thirdParty.title}</h2>
<div className="space-y-6"> <div className="space-y-6">
{c.thirdParty.services.map((service, index) => ( {c.thirdParty.services.map((service, index) => (
<div key={index} className="bg-zinc-900/30 p-5 rounded-lg ring-1 ring-zinc-800"> <div key={index} className="bg-background/30 p-5 rounded-lg ring-1 ring-border">
<h3 className="text-xl font-medium text-white mb-2">{service.name}</h3> <h3 className="text-xl font-medium text-foreground mb-2">{service.name}</h3>
<p className="text-zinc-300 mb-2"> <p className="text-foreground/80 mb-2">
<strong className="text-zinc-200">{locale === 'de' ? 'Zweck:' : locale === 'sr' ? 'Svrha:' : 'Purpose:'}</strong> {service.purpose} <strong className="text-foreground/90">{locale === 'de' ? 'Zweck:' : locale === 'sr' ? 'Svrha:' : 'Purpose:'}</strong> {service.purpose}
</p> </p>
<p className="text-zinc-300 mb-2"> <p className="text-foreground/80 mb-2">
<strong className="text-zinc-200">{locale === 'de' ? 'Verarbeitete Daten:' : locale === 'sr' ? 'Obrađeni podaci:' : 'Data processed:'}</strong> {service.dataProcessed} <strong className="text-foreground/90">{locale === 'de' ? 'Verarbeitete Daten:' : locale === 'sr' ? 'Obrađeni podaci:' : 'Data processed:'}</strong> {service.dataProcessed}
</p> </p>
<p className="text-zinc-400 text-sm"> <p className="text-muted-foreground text-sm">
<strong className="text-zinc-300">{locale === 'de' ? 'Datenschutz:' : locale === 'sr' ? 'Privatnost:' : 'Privacy:'}</strong>{' '} <strong className="text-foreground/80">{locale === 'de' ? 'Datenschutz:' : locale === 'sr' ? 'Privatnost:' : 'Privacy:'}</strong>{' '}
<a <a
href={service.privacy} href={service.privacy}
target="_blank" target="_blank"
@@ -585,27 +585,27 @@ export default async function PrivacyPage({ params }: Props) {
{/* Legal Basis */} {/* Legal Basis */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.legalBasis.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.legalBasis.title}</h2>
<p className="text-zinc-300 mb-4">{c.legalBasis.content}</p> <p className="text-foreground/80 mb-4">{c.legalBasis.content}</p>
<ul className="space-y-2"> <ul className="space-y-2">
{c.legalBasis.bases.map((item, index) => ( {c.legalBasis.bases.map((item, index) => (
<li key={index} className="text-zinc-300"> <li key={index} className="text-foreground/80">
<strong className="text-white">{item.basis}:</strong> {item.description} <strong className="text-foreground">{item.basis}:</strong> {item.description}
</li> </li>
))} ))}
</ul> </ul>
</section> </section>
{/* User Rights */} {/* User Rights */}
<section className="bg-zinc-900/50 p-6 rounded-xl ring-1 ring-zinc-800"> <section className="bg-background/50 p-6 rounded-xl ring-1 ring-border">
<h2 className="text-2xl font-semibold text-white mb-4">{c.rights.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.rights.title}</h2>
<p className="text-zinc-300 mb-4">{c.rights.intro}</p> <p className="text-foreground/80 mb-4">{c.rights.intro}</p>
<ul className="space-y-3"> <ul className="space-y-3">
{c.rights.items.map((item, index) => ( {c.rights.items.map((item, index) => (
<li key={index} className="text-zinc-300"> <li key={index} className="text-foreground/80">
<strong className="text-white">{item.right}</strong> <strong className="text-foreground">{item.right}</strong>
<br /> <br />
<span className="text-zinc-400">{item.description}</span> <span className="text-muted-foreground">{item.description}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -614,26 +614,26 @@ export default async function PrivacyPage({ params }: Props) {
{/* Data Retention */} {/* Data Retention */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.dataRetention.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.dataRetention.title}</h2>
<p className="text-zinc-300">{c.dataRetention.content}</p> <p className="text-foreground/80">{c.dataRetention.content}</p>
</section> </section>
{/* Data Security */} {/* Data Security */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.dataSecurity.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.dataSecurity.title}</h2>
<p className="text-zinc-300">{c.dataSecurity.content}</p> <p className="text-foreground/80">{c.dataSecurity.content}</p>
</section> </section>
{/* Changes */} {/* Changes */}
<section> <section>
<h2 className="text-2xl font-semibold text-white mb-4">{c.changes.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.changes.title}</h2>
<p className="text-zinc-300">{c.changes.content}</p> <p className="text-foreground/80">{c.changes.content}</p>
</section> </section>
{/* Contact */} {/* Contact */}
<section className="bg-zinc-900/50 p-6 rounded-xl ring-1 ring-zinc-800"> <section className="bg-background/50 p-6 rounded-xl ring-1 ring-border">
<h2 className="text-2xl font-semibold text-white mb-4">{c.contact.title}</h2> <h2 className="text-2xl font-semibold text-foreground mb-4">{c.contact.title}</h2>
<div className="text-zinc-300 space-y-1"> <div className="text-foreground/80 space-y-1">
{c.contact.content.map((line, index) => ( {c.contact.content.map((line, index) => (
<p key={index}>{line}</p> <p key={index}>{line}</p>
))} ))}
@@ -642,7 +642,7 @@ export default async function PrivacyPage({ params }: Props) {
{/* Link to Imprint */} {/* Link to Imprint */}
<section> <section>
<p className="text-zinc-400"> <p className="text-muted-foreground">
{locale === 'de' {locale === 'de'
? 'Weitere rechtliche Informationen finden Sie im' ? 'Weitere rechtliche Informationen finden Sie im'
: locale === 'sr' : locale === 'sr'
+277 -48
View File
@@ -1,6 +1,5 @@
import { setRequestLocale } from 'next-intl/server'; import { setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import Link from 'next/link'; import Link from 'next/link';
type Props = { type Props = {
@@ -11,13 +10,24 @@ const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
export async function generateMetadata({ params }: Props): Promise<Metadata> { export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'pages.terms.seo' });
const titles: Record<string, string> = {
de: 'Allgemeine Geschäftsbedingungen | Damjan Savić - KI Entwickler Köln',
en: 'Terms and Conditions | Damjan Savić - AI Developer',
sr: 'Uslovi korišćenja | Damjan Savić - AI Developer',
};
const descriptions: Record<string, string> = {
de: 'Allgemeine Geschäftsbedingungen (AGB) für die Dienstleistungen und Portfolio-Website von Damjan Savić, Fullstack Developer & KI Entwickler aus Köln.',
en: 'Terms and Conditions for the services and portfolio website of Damjan Savić, Fullstack Developer & AI Developer.',
sr: 'Uslovi korišćenja za usluge i portfolio veb sajt Damjana Savića, Fullstack Developer & AI Developer.',
};
const localePath = locale === 'de' ? '/terms' : `/${locale}/terms`; const localePath = locale === 'de' ? '/terms' : `/${locale}/terms`;
return { return {
title: t('title'), title: titles[locale] || titles.de,
description: t('description'), description: descriptions[locale] || descriptions.de,
alternates: { alternates: {
canonical: `${BASE_URL}${localePath}`, canonical: `${BASE_URL}${localePath}`,
languages: { languages: {
@@ -28,8 +38,8 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
}, },
}, },
openGraph: { openGraph: {
title: t('title'), title: titles[locale] || titles.de,
description: t('description'), description: descriptions[locale] || descriptions.de,
url: `${BASE_URL}${localePath}`, url: `${BASE_URL}${localePath}`,
type: 'website', type: 'website',
locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US', locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US',
@@ -42,64 +52,283 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
}; };
} }
type TermsContent = {
title: string;
lastUpdated: string;
sections: { title: string; content: string; items?: string[] }[];
contact: { title: string; content: string[] };
privacyNote: { text: string; link: string };
};
export default async function TermsPage({ params }: Props) { export default async function TermsPage({ params }: Props) {
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); setRequestLocale(locale);
const t = await getTranslations('pages.terms'); const content: Record<string, TermsContent> = {
de: {
title: 'Allgemeine Geschäftsbedingungen',
lastUpdated: 'Zuletzt aktualisiert: Februar 2026',
sections: [
{
title: '1. Geltungsbereich',
content: 'Diese Allgemeinen Geschäftsbedingungen (AGB) gelten für alle Dienstleistungen und Angebote von Damjan Savić im Bereich Fullstack-Entwicklung, KI-Entwicklung, Prozessautomatisierung und IT-Beratung sowie für die Nutzung der Website damjan-savic.com.',
},
{
title: '2. Leistungsumfang',
content: 'Der Umfang der zu erbringenden Leistungen ergibt sich aus der jeweiligen Projektvereinbarung oder dem Angebot. Leistungen umfassen unter anderem:',
items: [
'Entwicklung von Webanwendungen und SaaS-Lösungen (Next.js, React, TypeScript)',
'KI-Entwicklung und Integration (Claude AI, GPT, Voice AI)',
'Prozessautomatisierung (n8n, Make, individuelle Lösungen)',
'IT-Beratung und technische Konzeptentwicklung',
'Wartung und Support bestehender Anwendungen',
],
},
{
title: '3. Angebote und Vertragsschluss',
content: 'Angebote sind freibleibend und unverbindlich. Ein Vertrag kommt erst durch schriftliche Bestätigung oder durch Aufnahme der Leistungserbringung zustande. Mündliche Nebenabreden bedürfen der schriftlichen Bestätigung.',
},
{
title: '4. Vergütung und Zahlungsbedingungen',
content: 'Die Vergütung richtet sich nach der jeweiligen Vereinbarung. Rechnungen sind innerhalb von 14 Tagen nach Rechnungsstellung ohne Abzug zahlbar. Bei Zahlungsverzug werden Verzugszinsen in gesetzlicher Höhe berechnet. Bei größeren Projekten können Abschlagszahlungen vereinbart werden.',
},
{
title: '5. Mitwirkungspflichten des Auftraggebers',
content: 'Der Auftraggeber stellt alle für die Leistungserbringung erforderlichen Informationen, Zugänge und Materialien rechtzeitig zur Verfügung. Verzögerungen, die auf fehlender Mitwirkung beruhen, gehen nicht zu Lasten des Auftragnehmers und können zu Terminverschiebungen führen.',
},
{
title: '6. Urheberrecht und Nutzungsrechte',
content: 'Mit vollständiger Bezahlung gehen die vereinbarten Nutzungsrechte an den erstellten Werken auf den Auftraggeber über. Der Auftragnehmer behält das Recht, die erstellten Arbeiten als Referenz im Portfolio zu verwenden, sofern nicht ausdrücklich anders vereinbart. Quellcode und Dokumentation werden nach Abschluss und Bezahlung vollständig übergeben.',
},
{
title: '7. Gewährleistung und Haftung',
content: 'Die Gewährleistung richtet sich nach den gesetzlichen Bestimmungen. Mängel sind unverzüglich nach Entdeckung schriftlich anzuzeigen. Die Haftung für leichte Fahrlässigkeit ist auf den vorhersehbaren, vertragstypischen Schaden begrenzt, soweit es sich nicht um die Verletzung wesentlicher Vertragspflichten, Schäden aus der Verletzung des Lebens, des Körpers oder der Gesundheit handelt.',
},
{
title: '8. Vertraulichkeit und Datenschutz',
content: 'Beide Parteien verpflichten sich, alle im Rahmen der Zusammenarbeit erhaltenen vertraulichen Informationen geheim zu halten. Die Verarbeitung personenbezogener Daten erfolgt im Einklang mit der DSGVO. Weitere Informationen finden Sie in der Datenschutzerklärung.',
},
{
title: '9. Kündigung',
content: 'Projektverträge können von beiden Seiten mit einer Frist von 14 Tagen zum Monatsende schriftlich gekündigt werden, sofern nicht anders vereinbart. Bereits erbrachte Leistungen sind in jedem Fall zu vergüten. Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt.',
},
{
title: '10. Schlussbestimmungen',
content: 'Es gilt das Recht der Bundesrepublik Deutschland. Erfüllungsort und Gerichtsstand ist Köln, soweit gesetzlich zulässig. Sollten einzelne Bestimmungen dieser AGB unwirksam sein oder werden, bleibt die Wirksamkeit der übrigen Bestimmungen unberührt.',
},
],
contact: {
title: 'Kontakt',
content: [
'Bei Fragen zu diesen AGB erreichen Sie mich unter:',
'Damjan Savić',
'E-Mail: info@damjan-savic.com',
],
},
privacyNote: {
text: 'Informationen zum Datenschutz finden Sie in unserer',
link: 'Datenschutzerklärung',
},
},
en: {
title: 'Terms and Conditions',
lastUpdated: 'Last updated: February 2026',
sections: [
{
title: '1. Scope of Application',
content: 'These Terms and Conditions apply to all services and offerings by Damjan Savić in the fields of fullstack development, AI development, process automation and IT consulting, as well as the use of the website damjan-savic.com.',
},
{
title: '2. Scope of Services',
content: 'The scope of services to be provided is determined by the respective project agreement or offer. Services include but are not limited to:',
items: [
'Development of web applications and SaaS solutions (Next.js, React, TypeScript)',
'AI development and integration (Claude AI, GPT, Voice AI)',
'Process automation (n8n, Make, custom solutions)',
'IT consulting and technical concept development',
'Maintenance and support of existing applications',
],
},
{
title: '3. Offers and Contract Formation',
content: 'Offers are non-binding and subject to change. A contract is only concluded upon written confirmation or commencement of service delivery. Oral side agreements require written confirmation.',
},
{
title: '4. Remuneration and Payment Terms',
content: 'Remuneration is based on the respective agreement. Invoices are payable within 14 days of invoicing without deductions. In case of late payment, default interest at the statutory rate will be charged. For larger projects, progress payments may be agreed upon.',
},
{
title: '5. Client Cooperation Obligations',
content: 'The client shall provide all information, access, and materials required for service delivery in a timely manner. Delays resulting from lack of cooperation are not attributable to the contractor and may lead to schedule adjustments.',
},
{
title: '6. Copyright and Usage Rights',
content: 'Upon full payment, the agreed usage rights to the created works are transferred to the client. The contractor retains the right to use the created works as portfolio references, unless expressly agreed otherwise. Source code and documentation are fully handed over upon completion and payment.',
},
{
title: '7. Warranty and Liability',
content: 'Warranty is governed by statutory provisions. Defects must be reported in writing immediately upon discovery. Liability for slight negligence is limited to foreseeable, contract-typical damage, unless it concerns the breach of essential contractual obligations or damages resulting from injury to life, body, or health.',
},
{
title: '8. Confidentiality and Data Protection',
content: 'Both parties commit to keeping all confidential information received in the course of cooperation secret. The processing of personal data is carried out in accordance with the GDPR. Further information can be found in the Privacy Policy.',
},
{
title: '9. Termination',
content: 'Project contracts may be terminated by either party with 14 days\' notice to the end of the month in writing, unless otherwise agreed. Services already rendered must be compensated in any case. The right to extraordinary termination for good cause remains unaffected.',
},
{
title: '10. Final Provisions',
content: 'The laws of the Federal Republic of Germany apply. The place of performance and jurisdiction is Cologne (Köln), to the extent permitted by law. Should individual provisions of these Terms and Conditions be or become invalid, the validity of the remaining provisions shall remain unaffected.',
},
],
contact: {
title: 'Contact',
content: [
'For questions about these Terms and Conditions, please contact:',
'Damjan Savić',
'Email: info@damjan-savic.com',
],
},
privacyNote: {
text: 'For information on data protection, please see our',
link: 'Privacy Policy',
},
},
sr: {
title: 'Uslovi korišćenja',
lastUpdated: 'Poslednje ažuriranje: Februar 2026',
sections: [
{
title: '1. Oblast primene',
content: 'Ovi Uslovi korišćenja se primenjuju na sve usluge i ponude Damjana Savića u oblastima fullstack razvoja, AI razvoja, automatizacije procesa i IT konsaltinga, kao i na korišćenje web stranice damjan-savic.com.',
},
{
title: '2. Obim usluga',
content: 'Obim usluga koje treba pružiti određuje se odgovarajućim projektnim ugovorom ili ponudom. Usluge uključuju, ali nisu ograničene na:',
items: [
'Razvoj veb aplikacija i SaaS rešenja (Next.js, React, TypeScript)',
'AI razvoj i integracija (Claude AI, GPT, Voice AI)',
'Automatizacija procesa (n8n, Make, prilagođena rešenja)',
'IT konsalting i razvoj tehničkih koncepata',
'Održavanje i podrška postojećih aplikacija',
],
},
{
title: '3. Ponude i zaključenje ugovora',
content: 'Ponude su neobavezujuće i podložne promenama. Ugovor se zaključuje tek pisanom potvrdom ili početkom pružanja usluga. Usmeni sporedni dogovori zahtevaju pisanu potvrdu.',
},
{
title: '4. Naknada i uslovi plaćanja',
content: 'Naknada se zasniva na odgovarajućem dogovoru. Fakture se plaćaju u roku od 14 dana od izdavanja bez odbitaka. U slučaju kašnjenja s plaćanjem, zaračunavaju se zakonske zatezne kamate. Za veće projekte mogu se dogovoriti avansna plaćanja.',
},
{
title: '5. Obaveze saradnje klijenta',
content: 'Klijent je dužan da blagovremeno obezbedi sve informacije, pristupe i materijale potrebne za pružanje usluga. Kašnjenja nastala usled nedostatka saradnje ne pripisuju se izvršiocu i mogu dovesti do prilagođavanja rokova.',
},
{
title: '6. Autorska prava i prava korišćenja',
content: 'Po izvršenoj uplati, dogovorena prava korišćenja kreiranih dela prenose se na klijenta. Izvršilac zadržava pravo da koristi kreirana dela kao referentne projekte u portfoliju, osim ako je izričito drugačije dogovoreno. Izvorni kod i dokumentacija se u potpunosti predaju po završetku i plaćanju.',
},
{
title: '7. Garancija i odgovornost',
content: 'Garancija se reguliše zakonskim odredbama. Nedostaci se moraju prijaviti u pisanoj formi odmah po otkrivanju. Odgovornost za blagi nemar ograničena je na predvidivu, ugovorom tipičnu štetu, osim ako se radi o kršenju suštinskih ugovornih obaveza ili štetama nastalim povredom života, tela ili zdravlja.',
},
{
title: '8. Poverljivost i zaštita podataka',
content: 'Obe strane se obavezuju da će čuvati sve poverljive informacije primljene tokom saradnje. Obrada ličnih podataka vrši se u skladu sa GDPR-om. Više informacija možete pronaći u Politici privatnosti.',
},
{
title: '9. Raskid ugovora',
content: 'Projektni ugovori mogu se raskinuti od strane bilo koje strane sa rokom od 14 dana do kraja meseca u pisanoj formi, osim ako nije drugačije dogovoreno. Već pružene usluge moraju se u svakom slučaju nadoknaditi. Pravo na vanredan raskid iz opravdanog razloga ostaje nepromenjeno.',
},
{
title: '10. Završne odredbe',
content: 'Primenjuju se zakoni Savezne Republike Nemačke. Mesto ispunjenja i nadležnost je Keln (Köln), u meri dozvoljenoj zakonom. Ukoliko pojedine odredbe ovih Uslova korišćenja budu ili postanu nevažeće, važnost ostalih odredbi ostaje nepromenjena.',
},
],
contact: {
title: 'Kontakt',
content: [
'Za pitanja o ovim Uslovima korišćenja, kontaktirajte:',
'Damjan Savić',
'Email: info@damjan-savic.com',
],
},
privacyNote: {
text: 'Za informacije o zaštiti podataka, pogledajte našu',
link: 'Politiku privatnosti',
},
},
};
const c = content[locale] || content.de;
return ( return (
<main className="min-h-screen py-20 px-4"> <main className="min-h-screen py-16 sm:py-20 lg:py-24">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="mb-8"> <nav className="mb-8">
<Link href={`/${locale}`} className="text-zinc-400 hover:text-white transition-colors"> <Link
href={`/${locale}`}
className="text-muted-foreground hover:text-foreground transition-colors"
>
Home Home
</Link> </Link>
<span className="text-zinc-600 mx-2">/</span> <span className="mx-2 text-muted-foreground/60">/</span>
<span className="text-white">{t('content.title')}</span> <span className="text-foreground">{c.title}</span>
</nav> </nav>
<article className="prose prose-invert prose-zinc max-w-none"> <h1 className="text-4xl font-bold text-foreground mb-2">{c.title}</h1>
<h1>{t('content.title')}</h1> <p className="text-muted-foreground mb-8">{c.lastUpdated}</p>
<p className="text-zinc-400">{t('content.lastUpdated')}</p>
<h2>1. Agreement to Terms</h2> <div className="prose prose-invert max-w-none space-y-8">
<p> {c.sections.map((section, index) => (
By accessing our website, you agree to be bound by these Terms and Conditions <section key={index}>
and to comply with all applicable laws and regulations. <h2 className="text-2xl font-semibold text-foreground mb-4">{section.title}</h2>
</p> <p className="text-foreground/80">{section.content}</p>
{section.items && (
<ul className="list-disc list-inside text-foreground/80 space-y-1 ml-4 mt-3">
{section.items.map((item, itemIndex) => (
<li key={itemIndex}>{item}</li>
))}
</ul>
)}
</section>
))}
<h2>2. Intellectual Property</h2> {/* Contact */}
<p> <section className="bg-background/50 p-6 rounded-xl ring-1 ring-border">
All content on this website, including text, graphics, logos, images, and software, <h2 className="text-2xl font-semibold text-foreground mb-4">{c.contact.title}</h2>
is the property of Damjan Savić and is protected by intellectual property laws. <div className="text-foreground/80 space-y-1">
</p> {c.contact.content.map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
</section>
<h2>3. User Responsibilities</h2> {/* Privacy link */}
<p> <section>
When using our website, you agree to provide accurate information, <p className="text-muted-foreground">
use the website legally and ethically, and not attempt unauthorized access. {c.privacyNote.text}{' '}
</p> <Link
href={`/${locale}/privacy`}
className="text-orange-500 hover:text-orange-400 underline"
>
{c.privacyNote.link}
</Link>
.
</p>
</section>
<h2>4. Limitation of Liability</h2> {/* Copyright */}
<p> <section>
We are not liable for any indirect, incidental, special, or consequential damages <p className="text-muted-foreground">
arising from your use of or inability to use the website. &copy; {new Date().getFullYear()} Damjan Savić.{' '}
</p> {locale === 'de' ? 'Alle Rechte vorbehalten.' : locale === 'sr' ? 'Sva prava zadržana.' : 'All rights reserved.'}
</p>
<h2>5. Governing Law</h2> </section>
<p> </div>
These terms are governed by and construed in accordance with the laws of Germany.
</p>
<h2>6. Contact</h2>
<p>
For questions about these terms, please contact us at:
<br />
Email: legal@damjan-savic.com
</p>
</article>
</div> </div>
</main> </main>
); );
+155 -149
View File
@@ -1,41 +1,50 @@
@import "tailwindcss"; @import "tailwindcss";
@source "../../src";
@plugin "tailwindcss-animate";
@plugin "@tailwindcss/forms" {
strategy: class;
}
/* Tailwind CSS v4 Theme Configuration */
@theme { @theme {
/* Font Family */ --font-sans: 'Inter', system-ui, sans-serif;
--font-sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Colors - Zinc-based dark theme */ /* Primary Colors */
--color-background: #18181b; --color-primary: #697565;
--color-foreground: #fafafa; --color-primary-dark: #181C14;
--color-card: #27272a; --color-primary-foreground: #ECDFCC;
--color-card-foreground: #fafafa;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
--color-secondary: #3f3f46;
--color-secondary-foreground: #fafafa;
--color-muted: #3f3f46;
--color-muted-foreground: #a1a1aa;
--color-accent: #3f3f46;
--color-accent-foreground: #fafafa;
--color-destructive: #ef4444;
--color-destructive-foreground: #fafafa;
--color-border: #3f3f46;
--color-input: #3f3f46;
--color-ring: #3b82f6;
/* Zinc palette for direct use */ /* Secondary Colors */
--color-zinc-50: #fafafa; --color-secondary: #3C3D37;
--color-zinc-100: #f4f4f5; --color-secondary-gray: #3C3D37;
--color-zinc-200: #e4e4e7; --color-secondary-fuzzy: #465B50;
--color-zinc-300: #d4d4d8; --color-secondary-foreground: #ECDFCC;
--color-zinc-400: #a1a1aa;
--color-zinc-500: #71717a; /* Text Colors */
--color-zinc-600: #52525b; --color-text: #ECDFCC;
--color-zinc-700: #3f3f46; --color-text-light: #697565;
--color-zinc-800: #27272a;
--color-zinc-900: #18181b; /* System Colors */
--color-zinc-950: #09090b; --color-background: #181C14;
--color-foreground: #ECDFCC;
--color-card: #1E221A;
--color-card-foreground: #ECDFCC;
--color-popover: #1E221A;
--color-popover-foreground: #ECDFCC;
--color-muted: #3C3D37;
--color-muted-foreground: #697565;
--color-accent: #465B50;
--color-accent-foreground: #ECDFCC;
--color-destructive: #B43C3C;
--color-destructive-foreground: #ECDFCC;
--color-border: #3C3D37;
--color-input: #3C3D37;
--color-ring: #465B50;
/* Border Radius */ /* Border Radius */
--radius-lg: 0.5rem; --radius-lg: 0.5rem;
@@ -45,15 +54,11 @@
/* Animations */ /* Animations */
--animate-slide-up: slideUp 0.5s ease-out forwards; --animate-slide-up: slideUp 0.5s ease-out forwards;
--animate-fade-in: fadeIn 0.3s ease-out forwards; --animate-fade-in: fadeIn 0.3s ease-out forwards;
--animate-spin: spin 1s linear infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite;
} }
/* Keyframes for animations */
@keyframes slideUp { @keyframes slideUp {
0% { transform: translateY(10px); opacity: 0; } 0% { transform: translateY(10px); }
100% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(0); }
} }
@keyframes fadeIn { @keyframes fadeIn {
@@ -61,128 +66,129 @@
100% { opacity: 1; } 100% { opacity: 1; }
} }
@keyframes spin { /*
from { transform: rotate(0deg); } All custom styles MUST be inside @layer blocks.
to { transform: rotate(360deg); } In Tailwind v4, unlayered styles override ALL layered utilities.
} */
@keyframes pulse { @layer base {
0%, 100% { opacity: 1; } :root {
50% { opacity: 0.5; } --font-inter: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
@keyframes bounce { /* Animation Design Tokens */
0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); } --transition-fast: 150ms;
50% { transform: translateY(0); animation-timing-function: cubic-bezier(0, 0, 0.2, 1); } --transition-base: 200ms;
} --transition-slow: 300ms;
--transition-slower: 500ms;
/* CSS Variables for legacy support */ /* Navigation Colors */
:root { --nav-bg: #181C14;
--font-inter: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --nav-bg-transparent: rgb(24 28 20 / 0.95);
--nav-text: #ECDFCC;
--nav-text-hover: #465B50;
--nav-text-active: #465B50;
--nav-border: #3C3D37;
--nav-shadow: rgb(0 0 0 / 0.1);
/* Animation Design Tokens */ --radius: 0.5rem;
--transition-fast: 150ms; }
--transition-base: 200ms;
--transition-slow: 300ms;
--transition-slower: 500ms;
}
/* Prefers-Reduced-Motion Support */ html {
@media (prefers-reduced-motion: reduce) { scroll-behavior: smooth;
*, *::before, *::after { }
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important; body {
animation-iteration-count: 1 !important; font-family: var(--font-inter);
scroll-behavior: auto !important; background-color: var(--color-background);
color: var(--color-foreground);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-background);
}
::-webkit-scrollbar-thumb {
background: var(--color-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-muted-foreground);
}
/* Selection */
::selection {
background-color: rgb(105 117 101 / 0.3);
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
/* Smooth transitions */
a,
button {
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
} }
} }
* { /* Prefers-Reduced-Motion Support - must be outside layers for !important to work correctly */
box-sizing: border-box; @media (prefers-reduced-motion: reduce) {
padding: 0; @layer base {
margin: 0; *, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
}
} }
html { @layer components {
scroll-behavior: smooth; /* Utility classes */
.text-gradient {
background: linear-gradient(135deg, var(--color-primary) 0%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glass {
background: rgb(30 34 26 / 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgb(60 61 55 / 0.5);
}
/* Safe Area Insets for notches */
.mobile-safe {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
.mobile-safe-top {
padding-top: max(1rem, env(safe-area-inset-top));
}
} }
body { @layer utilities {
font-family: var(--font-inter); /* Will-Change for Performance */
background-color: var(--color-background); .animate-spin {
color: var(--color-foreground); will-change: transform;
min-height: 100vh; }
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Custom scrollbar */ .animate-pulse {
::-webkit-scrollbar { will-change: opacity;
width: 8px; }
}
::-webkit-scrollbar-track { .animate-bounce {
background: var(--color-background); will-change: transform;
} }
::-webkit-scrollbar-thumb {
background: var(--color-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-muted-foreground);
}
/* Selection */
::selection {
background-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
/* Smooth transitions */
a,
button {
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
}
/* Utility classes */
.text-gradient {
background: linear-gradient(135deg, var(--color-primary) 0%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glass {
background: color-mix(in srgb, var(--color-card) 80%, transparent);
backdrop-filter: blur(12px);
border: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
}
/* Safe Area Insets for notches */
.mobile-safe {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
.mobile-safe-top {
padding-top: max(1rem, env(safe-area-inset-top));
}
/* Will-Change for Performance */
.animate-spin {
will-change: transform;
}
.animate-pulse {
will-change: opacity;
}
.animate-bounce {
will-change: transform;
} }
+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#181C14"/>
<g transform="translate(2.5, 4.5) scale(0.027)">
<path fill="#ECDFCC" d="M965.47,97.09l-461.75,786.4L34.81,97.09h930.65M1000,77.5H0l17.59,29.5,468.89,786.4,17.35,29.1,17.15-29.22L982.73,106.88l17.27-29.38h0Z"/>
<polygon fill="#ECDFCC" points="755.12 213.96 524.03 214.02 544.73 249.03 734.41 249 830.87 249 782.41 330.91 759.95 368.91 687.8 490.96 639.56 572.54 591.35 490.96 519.2 368.98 615.65 368.95 639.56 409.41 663.47 368.95 685.97 330.91 593.15 330.95 496.71 330.95 448.28 249.05 427.57 214.02 379.13 132.11 475.57 132.11 803.57 132.05 900.03 132.02 851.57 213.96 755.12 213.96"/>
<polygon fill="#ECDFCC" points="618.8 607.63 570.59 689.19 548.33 726.83 500.09 808.39 451.88 726.83 240.28 369.01 217.81 331 169.39 249.08 265.85 249.08 314.27 330.97 336.73 369.01 500.09 645.29 522.35 607.63 381.23 368.98 358.76 330.97 310.33 249.05 289.61 214.06 245.11 214.06 148.69 214.08 100.24 132.17 196.69 132.14 241.17 132.14 337.64 132.11 386.05 214.02 406.76 249.05 455.21 330.97 477.68 368.98 570.59 526.08 618.8 607.63"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+3 -3
View File
@@ -6,16 +6,16 @@ export default async function NotFound() {
const t = await getTranslations('pages.notfound'); const t = await getTranslations('pages.notfound');
return ( return (
<div className="bg-zinc-900 text-zinc-50 min-h-screen flex items-center justify-center"> <div className="bg-background text-foreground min-h-screen flex items-center justify-center">
<div className="text-center px-4"> <div className="text-center px-4">
<h1 className="text-6xl font-bold mb-4">404</h1> <h1 className="text-6xl font-bold mb-4">404</h1>
<h2 className="text-2xl font-semibold mb-4">{t('title')}</h2> <h2 className="text-2xl font-semibold mb-4">{t('title')}</h2>
<p className="text-zinc-400 mb-8"> <p className="text-muted-foreground mb-8">
{t('description')} {t('description')}
</p> </p>
<Link <Link
href={`/${defaultLocale}`} href={`/${defaultLocale}`}
className="px-6 py-3 bg-white text-zinc-900 hover:bg-zinc-200 rounded-lg transition-colors inline-block font-medium" className="px-6 py-3 bg-primary text-background hover:bg-accent rounded-lg transition-colors inline-block font-medium"
> >
{t('backHome')} {t('backHome')}
</Link> </Link>
-1
View File
@@ -9,7 +9,6 @@ export default function robots(): MetadataRoute.Robots {
userAgent: '*', userAgent: '*',
allow: '/', allow: '/',
disallow: ['/login', '/dashboard', '/api/', '/admin', '/.env'], disallow: ['/login', '/dashboard', '/api/', '/admin', '/.env'],
crawlDelay: 1,
}, },
], ],
sitemap: `${baseUrl}/sitemap.xml`, sitemap: `${baseUrl}/sitemap.xml`,
+18
View File
@@ -2,6 +2,7 @@ import { MetadataRoute } from 'next';
import { locales } from '@/i18n/config'; import { locales } from '@/i18n/config';
import { getAllCitySlugs } from '@/data/cities'; import { getAllCitySlugs } from '@/data/cities';
import { getAllServiceSlugs } from '@/data/services'; import { getAllServiceSlugs } from '@/data/services';
import { getAllTechnologySlugs } from '@/data/technologies';
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com'; const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
@@ -109,6 +110,23 @@ export default function sitemap(): MetadataRoute.Sitemap {
}); });
}); });
// Add technology pages
const techSlugs = getAllTechnologySlugs();
techSlugs.forEach((slug) => {
locales.forEach((locale) => {
const localePath = locale === 'de' ? `/leistungen/technologie/${slug}` : `/${locale}/leistungen/technologie/${slug}`;
sitemap.push({
url: `${BASE_URL}${localePath}`,
lastModified: today,
changeFrequency: 'monthly',
priority: 0.8,
alternates: {
languages: generateAlternates(`/leistungen/technologie/${slug}`),
},
});
});
});
// Add blog posts // Add blog posts
blogSlugs.forEach((slug) => { blogSlugs.forEach((slug) => {
locales.forEach((locale) => { locales.forEach((locale) => {
+33 -33
View File
@@ -1,55 +1,55 @@
@layer base { @layer base {
:root { :root {
/* Primary Colors */ /* Primary Colors */
--color-primary: 105 117 101; /* #697565 - Sage green */ --color-primary: #697565; /* Sage green */
--color-primary-dark: 24 28 20; /* #181C14 - Dark moss */ --color-primary-dark: #181C14; /* Dark moss */
/* Secondary Colors */ /* Secondary Colors */
--color-secondary: 24 28 20; /* #181C14 - Dark moss */ --color-secondary: #181C14; /* Dark moss */
--color-secondary-gray: 60 61 55; /* #3C3D37 - Charcoal */ --color-secondary-gray: #3C3D37; /* Charcoal */
--color-secondary-fuzzy: 70 91 80; /* #465B50 - Dark sage */ --color-secondary-fuzzy: #465B50; /* Dark sage */
/* Text Colors */ /* Text Colors */
--color-text: 236 223 204; /* #ECDFCC - Cream */ --color-text: #ECDFCC; /* Cream */
--color-text-light: 105 117 101; /* #697565 - Sage */ --color-text-light: #697565; /* Sage */
/* Navigation Colors */ /* Navigation Colors */
--nav-bg: 24 28 20; /* Dark moss */ --nav-bg: #181C14; /* Dark moss */
--nav-bg-transparent: 24 28 20 / 0.95; /* Dark moss with transparency */ --nav-bg-transparent: rgb(24 28 20 / 0.95); /* Dark moss with transparency */
--nav-text: 236 223 204; /* Cream */ --nav-text: #ECDFCC; /* Cream */
--nav-text-hover: 70 91 80; /* Dark sage */ --nav-text-hover: #465B50; /* Dark sage */
--nav-text-active: 70 91 80; /* Dark sage */ --nav-text-active: #465B50; /* Dark sage */
--nav-border: 60 61 55; /* Charcoal */ --nav-border: #3C3D37; /* Charcoal */
--nav-shadow: 0 0 0 / 0.1; --nav-shadow: rgb(0 0 0 / 0.1);
/* System Colors */ /* System Colors */
--background: 24 28 20; /* Dark moss */ --background: #181C14; /* Dark moss */
--foreground: 236 223 204; /* Cream */ --foreground: #ECDFCC; /* Cream */
--card: 30 34 26; /* Slightly lighter dark moss */ --card: #1E221A; /* Slightly lighter dark moss */
--card-foreground: 236 223 204; /* Cream */ --card-foreground: #ECDFCC; /* Cream */
--popover: 30 34 26; /* Slightly lighter dark moss */ --popover: #1E221A; /* Slightly lighter dark moss */
--popover-foreground: 236 223 204; /* Cream */ --popover-foreground: #ECDFCC; /* Cream */
--primary: 105 117 101; /* Sage */ --primary: #697565; /* Sage */
--primary-foreground: 236 223 204; /* Cream */ --primary-foreground: #ECDFCC; /* Cream */
--secondary: 60 61 55; /* Charcoal */ --secondary: #3C3D37; /* Charcoal */
--secondary-foreground: 236 223 204; /* Cream */ --secondary-foreground: #ECDFCC; /* Cream */
--muted: 60 61 55; /* Charcoal */ --muted: #3C3D37; /* Charcoal */
--muted-foreground: 105 117 101; /* Sage */ --muted-foreground: #697565; /* Sage */
--accent: 70 91 80; /* Dark sage - neuer dunkelgrüner Akzent */ --accent: #465B50; /* Dark sage */
--accent-foreground: 236 223 204; /* Cream */ --accent-foreground: #ECDFCC; /* Cream */
--destructive: 180 60 60; /* Muted red for destructive actions */ --destructive: #B43C3C; /* Muted red for destructive actions */
--destructive-foreground: 236 223 204; /* Cream */ --destructive-foreground: #ECDFCC; /* Cream */
--border: 60 61 55; /* Charcoal */ --border: #3C3D37; /* Charcoal */
--input: 60 61 55; /* Charcoal */ --input: #3C3D37; /* Charcoal */
--ring: 70 91 80; /* Dark sage */ --ring: #465B50; /* Dark sage */
--radius: 0.5rem; --radius: 0.5rem;
} }
+15 -15
View File
@@ -24,14 +24,14 @@ const Hero = () => {
}; };
return ( return (
<section className="relative min-h-screen text-white overflow-hidden"> <section className="relative min-h-screen text-foreground overflow-hidden">
<div className="flex flex-col items-center justify-center min-h-screen px-4 relative z-10"> <div className="flex flex-col items-center justify-center min-h-screen px-4 relative z-10">
{/* Title */} {/* Title */}
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="text-white text-3xl font-bold tracking-wider mb-4" className="text-foreground text-3xl font-bold tracking-wider mb-4"
> >
{t('title')} {t('title')}
</motion.h1> </motion.h1>
@@ -43,7 +43,7 @@ const Hero = () => {
transition={{ duration: 0.5, delay: 0.2 }} transition={{ duration: 0.5, delay: 0.2 }}
className="max-w-3xl text-center mb-12" className="max-w-3xl text-center mb-12"
> >
<p className="text-zinc-400 text-lg mb-8"> <p className="text-muted-foreground text-lg mb-8">
{t('description')} {t('description')}
</p> </p>
</motion.div> </motion.div>
@@ -55,30 +55,30 @@ const Hero = () => {
transition={{ duration: 0.5, delay: 0.3 }} transition={{ duration: 0.5, delay: 0.3 }}
className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12" className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12"
> >
<div className="flex flex-col items-center p-6 border border-zinc-800 rounded-lg bg-zinc-900/50"> <div className="flex flex-col items-center p-6 border border-border rounded-lg bg-background/50">
<Terminal className="h-8 w-8 mb-4 text-white" /> <Terminal className="h-8 w-8 mb-4 text-foreground" />
<h3 className="text-lg font-semibold mb-2"> <h3 className="text-lg font-semibold mb-2">
{t('expertise.processAutomation.title')} {t('expertise.processAutomation.title')}
</h3> </h3>
<p className="text-zinc-400 text-center text-sm"> <p className="text-muted-foreground text-center text-sm">
{t('expertise.processAutomation.description')} {t('expertise.processAutomation.description')}
</p> </p>
</div> </div>
<div className="flex flex-col items-center p-6 border border-zinc-800 rounded-lg bg-zinc-900/50"> <div className="flex flex-col items-center p-6 border border-border rounded-lg bg-background/50">
<Globe2 className="h-8 w-8 mb-4 text-white" /> <Globe2 className="h-8 w-8 mb-4 text-foreground" />
<h3 className="text-lg font-semibold mb-2"> <h3 className="text-lg font-semibold mb-2">
{t('expertise.systemIntegration.title')} {t('expertise.systemIntegration.title')}
</h3> </h3>
<p className="text-zinc-400 text-center text-sm"> <p className="text-muted-foreground text-center text-sm">
{t('expertise.systemIntegration.description')} {t('expertise.systemIntegration.description')}
</p> </p>
</div> </div>
<div className="flex flex-col items-center p-6 border border-zinc-800 rounded-lg bg-zinc-900/50"> <div className="flex flex-col items-center p-6 border border-border rounded-lg bg-background/50">
<Code2 className="h-8 w-8 mb-4 text-white" /> <Code2 className="h-8 w-8 mb-4 text-foreground" />
<h3 className="text-lg font-semibold mb-2"> <h3 className="text-lg font-semibold mb-2">
{t('expertise.customDevelopment.title')} {t('expertise.customDevelopment.title')}
</h3> </h3>
<p className="text-zinc-400 text-center text-sm"> <p className="text-muted-foreground text-center text-sm">
{t('expertise.customDevelopment.description')} {t('expertise.customDevelopment.description')}
</p> </p>
</div> </div>
@@ -94,9 +94,9 @@ const Hero = () => {
{getLanguages().map(([key, value]) => ( {getLanguages().map(([key, value]) => (
<div <div
key={key} key={key}
className="flex flex-col items-center p-3 border border-zinc-800 rounded-lg bg-zinc-900/50 hover:bg-zinc-800/50 transition-colors duration-200" className="flex flex-col items-center p-3 border border-border rounded-lg bg-background/50 hover:bg-card/50 transition-colors duration-200"
> >
<span className="text-white font-medium">{value as string}</span> <span className="text-foreground font-medium">{value as string}</span>
</div> </div>
))} ))}
</motion.div> </motion.div>
@@ -107,7 +107,7 @@ const Hero = () => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }} transition={{ duration: 0.5, delay: 0.5 }}
onClick={scrollToSkills} onClick={scrollToSkills}
className="mt-8 px-6 py-3 bg-white text-zinc-900 rounded-lg font-medium hover:bg-zinc-100 transition-colors duration-200" className="mt-8 px-6 py-3 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-accent transition-colors duration-200"
> >
{t('aboutMe')} {t('aboutMe')}
</motion.button> </motion.button>
+24 -24
View File
@@ -43,10 +43,10 @@ const Journey = async () => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */} {/* Section Header */}
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold text-white mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
{t('title')} {t('title')}
</h2> </h2>
<p className="text-zinc-400 max-w-2xl mx-auto"> <p className="text-muted-foreground max-w-2xl mx-auto">
{t('subtitle')} {t('subtitle')}
</p> </p>
</div> </div>
@@ -54,33 +54,33 @@ const Journey = async () => {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Education Column */} {/* Education Column */}
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-lg"> <div className="p-6 bg-card/30 border border-border rounded-lg">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<GraduationCap className="h-5 w-5 text-zinc-400" /> <GraduationCap className="h-5 w-5 text-muted-foreground" />
<h3 className="text-lg font-medium text-white"> <h3 className="text-lg font-medium text-foreground">
{t('education.title')} {t('education.title')}
</h3> </h3>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h4 className="text-white font-medium"> <h4 className="text-foreground font-medium">
{t('education.degrees.masters.degree')} {t('education.degrees.masters.degree')}
</h4> </h4>
<p className="text-zinc-400 text-sm"> <p className="text-muted-foreground text-sm">
{t('education.degrees.masters.university')} {t('education.degrees.masters.university')}
</p> </p>
<p className="text-zinc-500 text-sm"> <p className="text-muted-foreground/70 text-sm">
{t('education.degrees.masters.period')} {t('education.degrees.masters.period')}
</p> </p>
</div> </div>
<div> <div>
<h4 className="text-white font-medium"> <h4 className="text-foreground font-medium">
{t('education.degrees.bachelors.degree')} {t('education.degrees.bachelors.degree')}
</h4> </h4>
<p className="text-zinc-400 text-sm"> <p className="text-muted-foreground text-sm">
{t('education.degrees.bachelors.university')} {t('education.degrees.bachelors.university')}
</p> </p>
<p className="text-zinc-500 text-sm"> <p className="text-muted-foreground/70 text-sm">
{t('education.degrees.bachelors.period')} {t('education.degrees.bachelors.period')}
</p> </p>
</div> </div>
@@ -88,18 +88,18 @@ const Journey = async () => {
</div> </div>
{/* Languages */} {/* Languages */}
<div className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-lg"> <div className="p-6 bg-card/30 border border-border rounded-lg">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<Globe className="h-5 w-5 text-zinc-400" /> <Globe className="h-5 w-5 text-muted-foreground" />
<h3 className="text-lg font-medium text-white"> <h3 className="text-lg font-medium text-foreground">
{t('languages.title')} {t('languages.title')}
</h3> </h3>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{languageItems.map(([key, item]) => ( {languageItems.map(([key, item]) => (
<div key={key}> <div key={key}>
<div className="text-zinc-300">{item.name}</div> <div className="text-foreground/80">{item.name}</div>
<div className="text-zinc-500 text-sm">{item.level}</div> <div className="text-muted-foreground/70 text-sm">{item.level}</div>
</div> </div>
))} ))}
</div> </div>
@@ -111,21 +111,21 @@ const Journey = async () => {
{workPositions.map((job, index) => ( {workPositions.map((job, index) => (
<div <div
key={index} key={index}
className="p-6 bg-zinc-800/30 border border-zinc-800 rounded-lg" className="p-6 bg-card/30 border border-border rounded-lg"
> >
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-white">{job.title}</h3> <h3 className="text-lg font-medium text-foreground">{job.title}</h3>
<span className="text-zinc-500 text-sm">{job.period}</span> <span className="text-muted-foreground/70 text-sm">{job.period}</span>
</div> </div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Briefcase className="h-4 w-4 text-zinc-400" /> <Briefcase className="h-4 w-4 text-muted-foreground" />
<p className="text-zinc-300">{job.company}</p> <p className="text-foreground/80">{job.company}</p>
<span className="text-zinc-500"> {job.location}</span> <span className="text-muted-foreground/70"> {job.location}</span>
</div> </div>
<ul className="space-y-2"> <ul className="space-y-2">
{job.highlights.map((highlight, idx) => ( {job.highlights.map((highlight, idx) => (
<li key={idx} className="text-zinc-400 text-sm flex items-start gap-2"> <li key={idx} className="text-muted-foreground text-sm flex items-start gap-2">
<span className="text-zinc-500"></span> <span className="text-muted-foreground/70"></span>
{highlight} {highlight}
</li> </li>
))} ))}
+10 -10
View File
@@ -77,21 +77,21 @@ const Skills = () => {
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-white text-sm font-medium"> <span className="text-foreground text-sm font-medium">
{skill.name} {skill.name}
</span> </span>
{selectedSkill === skill.name && ( {selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs"> <span className="text-muted-foreground text-xs">
{skill.level}% Proficiency {skill.level}% Proficiency
</span> </span>
)} )}
</div> </div>
<span className="text-white text-sm"> <span className="text-foreground text-sm">
{skill.level}% {skill.level}%
</span> </span>
</div> </div>
<div <div
className="h-2 bg-zinc-800 rounded-full overflow-hidden" className="h-2 bg-card rounded-full overflow-hidden"
role="progressbar" role="progressbar"
aria-valuenow={skill.level} aria-valuenow={skill.level}
aria-valuemin={0} aria-valuemin={0}
@@ -99,7 +99,7 @@ const Skills = () => {
> >
<motion.div <motion.div
className={`h-full rounded-full ${ className={`h-full rounded-full ${
selectedSkill === skill.name ? 'bg-zinc-500' : 'bg-zinc-600' selectedSkill === skill.name ? 'bg-accent' : 'bg-primary'
}`} }`}
initial={{ width: 0 }} initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }} whileInView={{ width: `${skill.level}%` }}
@@ -116,22 +116,22 @@ const Skills = () => {
{allSkills.map((skill) => ( {allSkills.map((skill) => (
<motion.div <motion.div
key={skill.name} key={skill.name}
className={`p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-all duration-300 hover:bg-zinc-700/50 ${ className={`p-6 rounded-xl bg-card/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-all duration-300 hover:bg-accent/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500' : '' selectedSkill === skill.name ? 'ring-2 ring-ring' : ''
}`} }`}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
onClick={() => handleClick(skill.name)} onClick={() => handleClick(skill.name)}
role="button" role="button"
aria-pressed={selectedSkill === skill.name} aria-pressed={selectedSkill === skill.name}
> >
<div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}> <div className={selectedSkill === skill.name ? 'text-accent' : 'text-foreground'}>
{iconMap[skill.name] || <Box className="w-8 h-8" />} {iconMap[skill.name] || <Box className="w-8 h-8" />}
</div> </div>
<span className="text-white text-sm text-center"> <span className="text-foreground text-sm text-center">
{skill.name} {skill.name}
</span> </span>
{selectedSkill === skill.name && ( {selectedSkill === skill.name && (
<span className="text-zinc-400 text-xs text-center mt-1"> <span className="text-muted-foreground text-xs text-center mt-1">
{skill.level}% {skill.level}%
</span> </span>
)} )}
+4 -4
View File
@@ -33,18 +33,18 @@ const Workflow = async () => {
role="listitem" role="listitem"
aria-label={`Step ${step.number} of ${totalSteps}: ${step.title}`} aria-label={`Step ${step.number} of ${totalSteps}: ${step.title}`}
> >
<div className="flex items-center py-6 border-b border-zinc-800 group-hover:border-zinc-700 transition-colors duration-300"> <div className="flex items-center py-6 border-b border-border group-hover:border-accent transition-colors duration-300">
<div className="w-12 sm:w-20 text-sm text-zinc-600 font-light"> <div className="w-12 sm:w-20 text-sm text-muted-foreground/60 font-light">
{step.number} {step.number}
</div> </div>
<h3 className="text-sm sm:text-base text-zinc-400 group-hover:text-white transition-colors duration-300"> <h3 className="text-sm sm:text-base text-muted-foreground group-hover:text-foreground transition-colors duration-300">
{step.title} {step.title}
</h3> </h3>
</div> </div>
{/* Line Indicator */} {/* Line Indicator */}
<div <div
className="absolute left-0 bottom-0 w-0 h-px bg-white group-hover:w-full transition-all duration-500 ease-out" className="absolute left-0 bottom-0 w-0 h-px bg-foreground group-hover:w-full transition-all duration-500 ease-out"
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>
+15 -15
View File
@@ -73,14 +73,14 @@ export default function DashboardContent() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-12"> <div className="flex items-center justify-between mb-12">
<div> <div>
<h1 className="text-3xl font-bold text-white">{t('header.title')}</h1> <h1 className="text-3xl font-bold text-foreground">{t('header.title')}</h1>
<p className="text-zinc-400 mt-2"> <p className="text-muted-foreground mt-2">
127 {t('header.activeUsers')} 127 {t('header.activeUsers')}
</p> </p>
</div> </div>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-zinc-300 transition-colors" className="flex items-center gap-2 px-4 py-2 bg-card hover:bg-accent rounded-lg text-foreground/80 transition-colors"
> >
<LogOut className="h-4 w-4" /> <LogOut className="h-4 w-4" />
Logout Logout
@@ -92,10 +92,10 @@ export default function DashboardContent() {
{metrics.map((metric, index) => ( {metrics.map((metric, index) => (
<div <div
key={index} key={index}
className="bg-zinc-900/50 backdrop-blur-sm rounded-xl p-6 ring-1 ring-zinc-800" className="bg-background/50 backdrop-blur-sm rounded-xl p-6 ring-1 ring-border"
> >
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="p-2 bg-zinc-800 rounded-lg text-zinc-400"> <div className="p-2 bg-card rounded-lg text-muted-foreground">
{metric.icon} {metric.icon}
</div> </div>
{metric.trend && ( {metric.trend && (
@@ -111,22 +111,22 @@ export default function DashboardContent() {
</div> </div>
)} )}
</div> </div>
<h3 className="text-sm text-zinc-400 mb-1">{metric.title}</h3> <h3 className="text-sm text-muted-foreground mb-1">{metric.title}</h3>
<p className="text-2xl font-bold text-white">{metric.value}</p> <p className="text-2xl font-bold text-foreground">{metric.value}</p>
<p className="text-sm text-zinc-500 mt-1">{metric.subtitle}</p> <p className="text-sm text-muted-foreground mt-1">{metric.subtitle}</p>
</div> </div>
))} ))}
</div> </div>
{/* Top Pages */} {/* Top Pages */}
<div className="bg-zinc-900/50 backdrop-blur-sm rounded-xl ring-1 ring-zinc-800"> <div className="bg-background/50 backdrop-blur-sm rounded-xl ring-1 ring-border">
<div className="p-6 border-b border-zinc-800"> <div className="p-6 border-b border-border">
<h2 className="text-xl font-semibold text-white">{t('topPages.title')}</h2> <h2 className="text-xl font-semibold text-foreground">{t('topPages.title')}</h2>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="text-left text-sm text-zinc-500 border-b border-zinc-800"> <tr className="text-left text-sm text-muted-foreground border-b border-border">
<th className="px-6 py-4 font-medium">{t('topPages.columns.path')}</th> <th className="px-6 py-4 font-medium">{t('topPages.columns.path')}</th>
<th className="px-6 py-4 font-medium">{t('topPages.columns.views')}</th> <th className="px-6 py-4 font-medium">{t('topPages.columns.views')}</th>
<th className="px-6 py-4 font-medium">{t('topPages.columns.change')}</th> <th className="px-6 py-4 font-medium">{t('topPages.columns.change')}</th>
@@ -134,9 +134,9 @@ export default function DashboardContent() {
</thead> </thead>
<tbody> <tbody>
{topPages.map((page, index) => ( {topPages.map((page, index) => (
<tr key={index} className="border-b border-zinc-800/50 last:border-0"> <tr key={index} className="border-b border-border/50 last:border-0">
<td className="px-6 py-4 text-white font-mono text-sm">{page.path}</td> <td className="px-6 py-4 text-foreground font-mono text-sm">{page.path}</td>
<td className="px-6 py-4 text-zinc-300">{page.views.toLocaleString()}</td> <td className="px-6 py-4 text-foreground/80">{page.views.toLocaleString()}</td>
<td className={`px-6 py-4 ${ <td className={`px-6 py-4 ${
page.change.startsWith('+') ? 'text-green-500' : 'text-red-500' page.change.startsWith('+') ? 'text-green-500' : 'text-red-500'
}`}> }`}>
+4 -4
View File
@@ -70,7 +70,7 @@ export default function LoginForm() {
<div className="max-w-md w-full space-y-8"> <div className="max-w-md w-full space-y-8">
<div className="text-center"> <div className="text-center">
<Lock className="mx-auto h-12 w-12 text-orange-500" /> <Lock className="mx-auto h-12 w-12 text-orange-500" />
<h1 className="mt-6 text-3xl font-bold text-white">{t('header.title')}</h1> <h1 className="mt-6 text-3xl font-bold text-foreground">{t('header.title')}</h1>
</div> </div>
<form className="mt-8 space-y-6" onSubmit={handleLogin}> <form className="mt-8 space-y-6" onSubmit={handleLogin}>
@@ -118,7 +118,7 @@ export default function LoginForm() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder={t('form.email.placeholder')} placeholder={t('form.email.placeholder')}
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500" className="w-full bg-background/50 border border-white/10 rounded-lg px-4 py-3 text-foreground placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
/> />
</div> </div>
@@ -133,7 +133,7 @@ export default function LoginForm() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder={t('form.password.placeholder')} placeholder={t('form.password.placeholder')}
className="w-full bg-zinc-900/50 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500" className="w-full bg-background/50 border border-white/10 rounded-lg px-4 py-3 text-foreground placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-orange-500"
/> />
</div> </div>
</div> </div>
@@ -154,7 +154,7 @@ export default function LoginForm() {
</button> </button>
</form> </form>
<div className="text-center text-sm text-zinc-500"> <div className="text-center text-sm text-muted-foreground">
<p className="flex items-center justify-center gap-2"> <p className="flex items-center justify-center gap-2">
<svg className="h-4 w-4 text-green-500" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"> <svg className="h-4 w-4 text-green-500" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" /> <path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
+33 -33
View File
@@ -71,27 +71,27 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
return ( return (
<Link <Link
href={`/${locale}/blog/${post.slug}`} href={`/${locale}/blog/${post.slug}`}
className="group relative block bg-zinc-900/50 backdrop-blur-sm className="group relative block bg-background/50 backdrop-blur-sm
rounded-xl shadow-lg hover:shadow-2xl rounded-xl shadow-lg hover:shadow-2xl
ring-1 ring-zinc-800 hover:ring-zinc-700 ring-1 ring-border hover:ring-border
transition-all duration-500 focus:outline-none focus:ring-2 transition-all duration-500 focus:outline-none focus:ring-2
focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background
transform hover:-translate-y-1" transform hover:-translate-y-1"
> >
<div className="relative overflow-hidden rounded-xl"> <div className="relative overflow-hidden rounded-xl">
{/* Image Container */} {/* Image Container */}
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900"> <div className="aspect-[16/9] w-full relative overflow-hidden bg-background">
<BlogImage <BlogImage
src={getImagePath(post.coverImage)} src={getImagePath(post.coverImage)}
alt={post.title} alt={post.title}
fallback={PLACEHOLDER_IMAGE} fallback={PLACEHOLDER_IMAGE}
/> />
<div className="absolute inset-x-0 -bottom-20 top-0 bg-gradient-to-t from-zinc-900 via-zinc-900/70 to-transparent opacity-95" /> <div className="absolute inset-x-0 -bottom-20 top-0 bg-gradient-to-t from-background via-background/70 to-transparent opacity-95" />
</div> </div>
{/* Content */} {/* Content */}
<div className="relative -mt-2 p-6 bg-zinc-900 z-10"> <div className="relative -mt-2 p-6 bg-background z-10">
<div className="flex items-center gap-4 mb-4 text-zinc-500"> <div className="flex items-center gap-4 mb-4 text-muted-foreground">
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span>{getFormattedDate(post.date, locale)}</span> <span>{getFormattedDate(post.date, locale)}</span>
@@ -104,12 +104,12 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
)} )}
</div> </div>
<h3 className="text-xl font-semibold text-white mb-3 <h3 className="text-xl font-semibold text-foreground mb-3
group-hover:text-zinc-100 transition-colors duration-300"> group-hover:text-foreground/90 transition-colors duration-300">
{post.title} {post.title}
</h3> </h3>
<p className="text-zinc-400 text-sm line-clamp-2 mb-4 <p className="text-muted-foreground text-sm line-clamp-2 mb-4
group-hover:text-zinc-300 transition-colors duration-300"> group-hover:text-foreground/80 transition-colors duration-300">
{post.excerpt} {post.excerpt}
</p> </p>
@@ -118,17 +118,17 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
{post.tags?.slice(0, 3).map((tag, index) => ( {post.tags?.slice(0, 3).map((tag, index) => (
<span <span
key={index} key={index}
className="px-3 py-1 bg-zinc-800/50 className="px-3 py-1 bg-card/50
text-sm text-zinc-400 rounded-full text-sm text-muted-foreground rounded-full
ring-1 ring-zinc-700/50 group-hover:ring-zinc-600/50 ring-1 ring-border/50 group-hover:ring-ring/50
group-hover:bg-zinc-800/70 group-hover:text-zinc-300 group-hover:bg-card/70 group-hover:text-foreground/80
transition-all duration-300" transition-all duration-300"
> >
{tag} {tag}
</span> </span>
))} ))}
{post.tags && post.tags.length > 3 && ( {post.tags && post.tags.length > 3 && (
<span className="text-sm text-zinc-600"> <span className="text-sm text-muted-foreground/60">
+{post.tags.length - 3} more +{post.tags.length - 3} more
</span> </span>
)} )}
@@ -136,11 +136,11 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) {
{/* Link Icon */} {/* Link Icon */}
<div className="absolute top-6 right-6 p-2.5 rounded-full <div className="absolute top-6 right-6 p-2.5 rounded-full
bg-zinc-800/60 backdrop-blur-sm ring-1 ring-zinc-700/50 bg-card/60 backdrop-blur-sm ring-1 ring-border/50
opacity-0 group-hover:opacity-100 opacity-0 group-hover:opacity-100
transform translate-y-2 group-hover:translate-y-0 transform translate-y-2 group-hover:translate-y-0
transition-all duration-300"> transition-all duration-300">
<ExternalLink className="h-4 w-4 text-zinc-400" /> <ExternalLink className="h-4 w-4 text-muted-foreground" />
</div> </div>
</div> </div>
</div> </div>
@@ -203,9 +203,9 @@ function Pagination({
{currentPage > 1 ? ( {currentPage > 1 ? (
<button <button
onClick={() => onPageChange(currentPage - 1)} onClick={() => onPageChange(currentPage - 1)}
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground hover:text-foreground
bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50 bg-card/50 hover:bg-card rounded-lg ring-1 ring-border/50
hover:ring-zinc-600 transition-all duration-200" hover:ring-ring transition-all duration-200"
aria-label={t('ui.pagination.previous')} aria-label={t('ui.pagination.previous')}
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
@@ -213,8 +213,8 @@ function Pagination({
</button> </button>
) : ( ) : (
<span <span
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-600 className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground/60
bg-zinc-800/30 rounded-lg ring-1 ring-zinc-800/50 cursor-not-allowed" bg-card/30 rounded-lg ring-1 ring-border/50 cursor-not-allowed"
aria-disabled="true" aria-disabled="true"
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
@@ -227,7 +227,7 @@ function Pagination({
{getPageNumbers().map((page, index) => { {getPageNumbers().map((page, index) => {
if (page === 'ellipsis') { if (page === 'ellipsis') {
return ( return (
<span key={`ellipsis-${index}`} className="px-2 text-zinc-600"> <span key={`ellipsis-${index}`} className="px-2 text-muted-foreground/60">
... ...
</span> </span>
); );
@@ -242,8 +242,8 @@ function Pagination({
className={`min-w-[44px] h-11 flex items-center justify-center text-sm rounded-lg className={`min-w-[44px] h-11 flex items-center justify-center text-sm rounded-lg
ring-1 transition-all duration-200 ring-1 transition-all duration-200
${isCurrentPage ${isCurrentPage
? 'bg-[#697565] text-white ring-[#697565] font-medium' ? 'bg-[#697565] text-foreground ring-[#697565] font-medium'
: 'text-zinc-400 hover:text-white bg-zinc-800/50 hover:bg-zinc-800 ring-zinc-700/50 hover:ring-zinc-600' : 'text-muted-foreground hover:text-foreground bg-card/50 hover:bg-card ring-border/50 hover:ring-ring'
}`} }`}
aria-current={isCurrentPage ? 'page' : undefined} aria-current={isCurrentPage ? 'page' : undefined}
> >
@@ -257,9 +257,9 @@ function Pagination({
{currentPage < totalPages ? ( {currentPage < totalPages ? (
<button <button
onClick={() => onPageChange(currentPage + 1)} onClick={() => onPageChange(currentPage + 1)}
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground hover:text-foreground
bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50 bg-card/50 hover:bg-card rounded-lg ring-1 ring-border/50
hover:ring-zinc-600 transition-all duration-200" hover:ring-ring transition-all duration-200"
aria-label={t('ui.pagination.next')} aria-label={t('ui.pagination.next')}
> >
<span className="hidden sm:inline">{t('ui.pagination.next')}</span> <span className="hidden sm:inline">{t('ui.pagination.next')}</span>
@@ -267,8 +267,8 @@ function Pagination({
</button> </button>
) : ( ) : (
<span <span
className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-600 className="flex items-center gap-1 px-3 py-2 text-sm text-muted-foreground/60
bg-zinc-800/30 rounded-lg ring-1 ring-zinc-800/50 cursor-not-allowed" bg-card/30 rounded-lg ring-1 ring-border/50 cursor-not-allowed"
aria-disabled="true" aria-disabled="true"
> >
<span className="hidden sm:inline">{t('ui.pagination.next')}</span> <span className="hidden sm:inline">{t('ui.pagination.next')}</span>
@@ -389,7 +389,7 @@ export function BlogList({ posts, locale, initialSearch = '', initialCategory =
</div> </div>
{/* Results count */} {/* Results count */}
<p className="text-sm text-zinc-500 mb-6"> <p className="text-sm text-muted-foreground mb-6">
{translations.showingText( {translations.showingText(
startIndex + 1, startIndex + 1,
Math.min(endIndex, filteredPosts.length), Math.min(endIndex, filteredPosts.length),
@@ -421,7 +421,7 @@ export function BlogList({ posts, locale, initialSearch = '', initialCategory =
) : ( ) : (
/* Empty State */ /* Empty State */
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-zinc-400">{translations.noPostsText}</p> <p className="text-muted-foreground">{translations.noPostsText}</p>
</div> </div>
)} )}
</div> </div>
+6 -6
View File
@@ -26,8 +26,8 @@ export function CategoryFilter({
className={`${className}`} className={`${className}`}
> >
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<Tag className="h-5 w-5 text-zinc-400 flex-shrink-0" /> <Tag className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<h3 className="text-sm font-medium text-zinc-300">Filter by Category</h3> <h3 className="text-sm font-medium text-foreground/80">Filter by Category</h3>
</div> </div>
{/* Mobile: Horizontal scrollable */} {/* Mobile: Horizontal scrollable */}
@@ -44,8 +44,8 @@ export function CategoryFilter({
onClick={() => onCategoryChange(category === 'All' ? null : category)} onClick={() => onCategoryChange(category === 'All' ? null : category)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${ className={`px-4 py-2 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${
isActive isActive
? 'bg-[#697565] text-white' ? 'bg-primary text-primary-foreground'
: 'bg-zinc-900/50 text-zinc-400 border border-zinc-800 hover:bg-zinc-800/50 hover:text-white' : 'bg-background/50 text-muted-foreground border border-border hover:bg-card/50 hover:text-foreground'
}`} }`}
> >
{category} {category}
@@ -68,8 +68,8 @@ export function CategoryFilter({
onClick={() => onCategoryChange(category === 'All' ? null : category)} onClick={() => onCategoryChange(category === 'All' ? null : category)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${ className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
isActive isActive
? 'bg-[#697565] text-white' ? 'bg-primary text-primary-foreground'
: 'bg-zinc-900/50 text-zinc-400 border border-zinc-800 hover:bg-zinc-800/50 hover:text-white' : 'bg-background/50 text-muted-foreground border border-border hover:bg-card/50 hover:text-foreground'
}`} }`}
> >
{category} {category}
+3 -3
View File
@@ -27,18 +27,18 @@ export function SearchBar({
className={`relative ${className}`} className={`relative ${className}`}
> >
<div className="relative"> <div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-zinc-400" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<input <input
type="text" type="text"
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
placeholder={placeholder} placeholder={placeholder}
className="w-full pl-12 pr-12 py-3 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-[#697565] focus:border-transparent transition-all" className="w-full pl-12 pr-12 py-3 bg-background/50 border border-border rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-all"
/> />
{value && ( {value && (
<button <button
onClick={handleClear} onClick={handleClear}
className="absolute right-4 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-white transition-colors" className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
aria-label="Clear search" aria-label="Clear search"
> >
<X className="h-5 w-5" /> <X className="h-5 w-5" />
+16 -16
View File
@@ -61,14 +61,14 @@ export function ChatMessage({ message, index = 0 }: ChatMessageProps) {
<div <div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
isUser isUser
? 'bg-zinc-600/50' ? 'bg-primary/50'
: 'bg-zinc-800/50 border border-zinc-700' : 'bg-card/50 border border-border'
}`} }`}
> >
{isUser ? ( {isUser ? (
<User className="w-4 h-4 text-zinc-300" /> <User className="w-4 h-4 text-foreground/80" />
) : ( ) : (
<Bot className="w-4 h-4 text-zinc-400" /> <Bot className="w-4 h-4 text-muted-foreground" />
)} )}
</div> </div>
@@ -81,21 +81,21 @@ export function ChatMessage({ message, index = 0 }: ChatMessageProps) {
<div <div
className={`px-4 py-3 rounded-2xl ${ className={`px-4 py-3 rounded-2xl ${
isUser isUser
? 'bg-zinc-600/50 text-white rounded-tr-sm' ? 'bg-primary/50 text-foreground rounded-tr-sm'
: 'bg-zinc-800/50 border border-zinc-800 text-zinc-200 rounded-tl-sm' : 'bg-card/50 border border-border text-foreground/90 rounded-tl-sm'
}`} }`}
> >
<p className="text-sm whitespace-pre-wrap break-words leading-relaxed"> <p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
{content} {content}
{isStreaming && ( {isStreaming && (
<span className="inline-block w-1.5 h-4 ml-1 bg-zinc-400 animate-pulse rounded-sm" /> <span className="inline-block w-1.5 h-4 ml-1 bg-muted-foreground animate-pulse rounded-sm" />
)} )}
</p> </p>
</div> </div>
{/* Timestamp */} {/* Timestamp */}
{formattedTime && !isStreaming && ( {formattedTime && !isStreaming && (
<span className="text-xs text-zinc-500 mt-1 px-1"> <span className="text-xs text-muted-foreground mt-1 px-1">
{formattedTime} {formattedTime}
</span> </span>
)} )}
@@ -116,17 +116,17 @@ export function TypingIndicator() {
className="flex gap-3" className="flex gap-3"
> >
{/* Avatar */} {/* Avatar */}
<div className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center bg-zinc-800/50 border border-zinc-700"> <div className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center bg-card/50 border border-border">
<Bot className="w-4 h-4 text-zinc-400" /> <Bot className="w-4 h-4 text-muted-foreground" />
</div> </div>
{/* Typing dots */} {/* Typing dots */}
<div className="px-4 py-3 rounded-2xl rounded-tl-sm bg-zinc-800/50 border border-zinc-800"> <div className="px-4 py-3 rounded-2xl rounded-tl-sm bg-card/50 border border-border">
<div className="flex gap-1.5"> <div className="flex gap-1.5">
{[0, 1, 2].map((i) => ( {[0, 1, 2].map((i) => (
<motion.span <motion.span
key={i} key={i}
className="w-2 h-2 bg-zinc-500 rounded-full" className="w-2 h-2 bg-muted-foreground rounded-full"
animate={{ animate={{
opacity: [0.4, 1, 0.4], opacity: [0.4, 1, 0.4],
scale: [0.8, 1, 0.8], scale: [0.8, 1, 0.8],
@@ -154,13 +154,13 @@ export function WelcomeMessage() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="flex flex-col items-center justify-center py-8 px-4 text-center" className="flex flex-col items-center justify-center py-8 px-4 text-center"
> >
<div className="w-12 h-12 rounded-full bg-zinc-800/50 border border-zinc-700 flex items-center justify-center mb-4"> <div className="w-12 h-12 rounded-full bg-card/50 border border-border flex items-center justify-center mb-4">
<Bot className="w-6 h-6 text-zinc-400" /> <Bot className="w-6 h-6 text-muted-foreground" />
</div> </div>
<h3 className="text-lg font-medium text-white mb-2"> <h3 className="text-lg font-medium text-foreground mb-2">
Hi, I&apos;m Damjan&apos;s AI Assistant Hi, I&apos;m Damjan&apos;s AI Assistant
</h3> </h3>
<p className="text-sm text-zinc-400 max-w-xs"> <p className="text-sm text-muted-foreground max-w-xs">
I can help you learn about Damjan&apos;s skills, services, and experience. I can help you learn about Damjan&apos;s skills, services, and experience.
Feel free to ask me anything! Feel free to ask me anything!
</p> </p>
+15 -15
View File
@@ -259,7 +259,7 @@ export function Chatbot() {
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
onClick={toggleChat} onClick={toggleChat}
className="fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full bg-zinc-100 hover:bg-white text-zinc-900 shadow-lg shadow-black/20 flex items-center justify-center transition-colors" className="fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full bg-primary text-primary-foreground shadow-lg shadow-black/20 flex items-center justify-center hover:bg-accent transition-colors"
aria-label="Open chat" aria-label="Open chat"
> >
<MessageCircle className="w-6 h-6" /> <MessageCircle className="w-6 h-6" />
@@ -275,7 +275,7 @@ export function Chatbot() {
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }} exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className={`fixed z-50 bg-zinc-900 border border-zinc-800 shadow-2xl shadow-black/40 flex flex-col overflow-hidden ${ className={`fixed z-50 bg-background border border-border shadow-2xl shadow-black/40 flex flex-col overflow-hidden ${
isMinimized isMinimized
? 'bottom-6 right-6 w-80 h-14 rounded-full' ? 'bottom-6 right-6 w-80 h-14 rounded-full'
: 'bottom-6 right-6 w-[380px] h-[600px] max-h-[calc(100vh-3rem)] rounded-2xl sm:max-w-[calc(100vw-3rem)]' : 'bottom-6 right-6 w-[380px] h-[600px] max-h-[calc(100vh-3rem)] rounded-2xl sm:max-w-[calc(100vw-3rem)]'
@@ -289,18 +289,18 @@ export function Chatbot() {
> >
{/* Header */} {/* Header */}
<div <div
className={`flex items-center justify-between px-4 bg-zinc-800/50 border-b border-zinc-800 ${ className={`flex items-center justify-between px-4 bg-card/50 border-b border-border ${
isMinimized ? 'h-full rounded-full' : 'h-14' isMinimized ? 'h-full rounded-full' : 'h-14'
}`} }`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-zinc-700/50 flex items-center justify-center"> <div className="w-8 h-8 rounded-full bg-muted/50 flex items-center justify-center">
<MessageCircle className="w-4 h-4 text-zinc-300" /> <MessageCircle className="w-4 h-4 text-foreground/80" />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium text-white">AI Assistant</span> <span className="text-sm font-medium text-foreground">AI Assistant</span>
{!isMinimized && ( {!isMinimized && (
<span className="text-xs text-zinc-400">Ask me anything</span> <span className="text-xs text-muted-foreground">Ask me anything</span>
)} )}
</div> </div>
</div> </div>
@@ -310,7 +310,7 @@ export function Chatbot() {
{!isMinimized && messages.length > 0 && ( {!isMinimized && messages.length > 0 && (
<button <button
onClick={clearChat} onClick={clearChat}
className="p-2 rounded-lg hover:bg-zinc-700/50 text-zinc-400 hover:text-zinc-200 transition-colors" className="p-2 rounded-lg hover:bg-muted/50 text-muted-foreground hover:text-foreground/80 transition-colors"
aria-label="Clear chat" aria-label="Clear chat"
title="Clear chat" title="Clear chat"
> >
@@ -321,7 +321,7 @@ export function Chatbot() {
{/* Close Button */} {/* Close Button */}
<button <button
onClick={toggleChat} onClick={toggleChat}
className="p-2 rounded-lg hover:bg-zinc-700/50 text-zinc-400 hover:text-zinc-200 transition-colors" className="p-2 rounded-lg hover:bg-muted/50 text-muted-foreground hover:text-foreground/80 transition-colors"
aria-label="Close chat" aria-label="Close chat"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
@@ -340,14 +340,14 @@ export function Chatbot() {
{/* Suggested Prompts */} {/* Suggested Prompts */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<p className="text-xs text-zinc-500 text-center mb-2"> <p className="text-xs text-muted-foreground text-center mb-2">
Try asking: Try asking:
</p> </p>
{SUGGESTED_PROMPTS.map((prompt, index) => ( {SUGGESTED_PROMPTS.map((prompt, index) => (
<button <button
key={index} key={index}
onClick={() => handleSuggestedPrompt(prompt)} onClick={() => handleSuggestedPrompt(prompt)}
className="w-full px-4 py-2.5 text-sm text-left bg-zinc-800/50 border border-zinc-800 rounded-xl text-zinc-300 hover:bg-zinc-700/50 hover:border-zinc-700 transition-colors" className="w-full px-4 py-2.5 text-sm text-left bg-card/50 border border-border rounded-xl text-foreground/80 hover:bg-accent/50 hover:border-accent transition-colors"
> >
{prompt} {prompt}
</button> </button>
@@ -397,7 +397,7 @@ export function Chatbot() {
{/* Input Form */} {/* Input Form */}
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="p-4 border-t border-zinc-800 bg-zinc-900" className="p-4 border-t border-border bg-background"
> >
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
@@ -407,13 +407,13 @@ export function Chatbot() {
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
placeholder="Type a message..." placeholder="Type a message..."
disabled={isLoading} disabled={isLoading}
className="flex-1 h-11 px-4 bg-zinc-800/50 border border-zinc-800 rounded-xl text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50" className="flex-1 h-11 px-4 bg-card/50 border border-border rounded-xl text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
aria-label="Chat message input" aria-label="Chat message input"
/> />
<button <button
type="submit" type="submit"
disabled={isLoading || !inputValue.trim()} disabled={isLoading || !inputValue.trim()}
className="h-11 w-11 bg-zinc-100 hover:bg-white text-zinc-900 rounded-xl flex items-center justify-center transition-colors disabled:opacity-50 disabled:cursor-not-allowed" className="h-11 w-11 bg-primary hover:bg-accent text-primary-foreground rounded-xl flex items-center justify-center transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="Send message" aria-label="Send message"
> >
{isLoading ? ( {isLoading ? (
@@ -425,7 +425,7 @@ export function Chatbot() {
</div> </div>
{/* Privacy note */} {/* Privacy note */}
<p className="text-xs text-zinc-500 text-center mt-2"> <p className="text-xs text-muted-foreground text-center mt-2">
Messages may be stored to improve service Messages may be stored to improve service
</p> </p>
</form> </form>
+34 -34
View File
@@ -125,11 +125,11 @@ export function ContactForm() {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
{/* Hero Section */} {/* Hero Section */}
<div className="py-24 text-center"> <div className="py-12 sm:py-16 md:py-24 text-center">
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="text-4xl md:text-5xl font-bold text-white mb-4" className="text-4xl md:text-5xl font-bold text-foreground mb-4"
> >
{t('hero.title')} {t('hero.title')}
</motion.h1> </motion.h1>
@@ -137,7 +137,7 @@ export function ContactForm() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }} transition={{ delay: 0.1 }}
className="text-lg text-zinc-400 max-w-2xl mx-auto px-4" className="text-lg text-muted-foreground max-w-2xl mx-auto px-4"
> >
{t('hero.subtitle')} {t('hero.subtitle')}
</motion.p> </motion.p>
@@ -145,7 +145,7 @@ export function ContactForm() {
{/* Main Content */} {/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-24"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-24">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12">
{/* Contact Info */} {/* Contact Info */}
<motion.div <motion.div
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
@@ -155,20 +155,20 @@ export function ContactForm() {
> >
{/* Headshot */} {/* Headshot */}
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="relative w-24 h-24 rounded-full overflow-hidden border-2 border-zinc-700 flex-shrink-0"> <div className="relative w-16 h-16 sm:w-20 sm:h-20 md:w-24 md:h-24 rounded-full overflow-hidden border-2 border-border flex-shrink-0">
<Image <Image
src="/images/headshot.avif" src="/images/headshot.avif"
alt="Damjan Savić - Full-Stack Web Developer" alt="Damjan Savić - Full-Stack Web Developer"
fill fill
sizes="96px" sizes="(max-width: 640px) 64px, (max-width: 768px) 80px, 96px"
className="object-cover" className="object-cover"
/> />
</div> </div>
<div> <div className="min-w-0">
<h2 className="text-2xl font-bold text-white mb-1"> <h2 className="text-2xl font-bold text-foreground mb-1 break-words">
{t('contactInfo.title')} {t('contactInfo.title')}
</h2> </h2>
<p className="text-zinc-400"> <p className="text-muted-foreground">
{t('contactInfo.description')} {t('contactInfo.description')}
</p> </p>
</div> </div>
@@ -181,14 +181,14 @@ export function ContactForm() {
transition={{ delay: 0.3 }} transition={{ delay: 0.3 }}
className="flex items-start gap-4" className="flex items-start gap-4"
> >
<div className="p-3 bg-zinc-800/50 rounded-lg"> <div className="p-3 bg-card/50 rounded-lg">
<MapPin className="h-6 w-6 text-zinc-400" aria-hidden="true" /> <MapPin className="h-6 w-6 text-muted-foreground" aria-hidden="true" />
</div> </div>
<div> <div>
<h3 className="text-sm font-medium text-zinc-300 mb-1"> <h3 className="text-sm font-medium text-foreground/80 mb-1">
{t('contactInfo.address.label')} {t('contactInfo.address.label')}
</h3> </h3>
<p className="text-zinc-400"> <p className="text-muted-foreground">
{t('contactInfo.address.value')} {t('contactInfo.address.value')}
</p> </p>
</div> </div>
@@ -200,16 +200,16 @@ export function ContactForm() {
transition={{ delay: 0.4 }} transition={{ delay: 0.4 }}
className="flex items-start gap-4" className="flex items-start gap-4"
> >
<div className="p-3 bg-zinc-800/50 rounded-lg"> <div className="p-3 bg-card/50 rounded-lg">
<Phone className="h-6 w-6 text-zinc-400" aria-hidden="true" /> <Phone className="h-6 w-6 text-muted-foreground" aria-hidden="true" />
</div> </div>
<div> <div>
<h3 className="text-sm font-medium text-zinc-300 mb-1"> <h3 className="text-sm font-medium text-foreground/80 mb-1">
{t('contactInfo.phone.label')} {t('contactInfo.phone.label')}
</h3> </h3>
<a <a
href="tel:+491756950979" href="tel:+491756950979"
className="text-zinc-400 hover:text-white transition-colors duration-200" className="text-muted-foreground hover:text-foreground transition-colors duration-200"
> >
{t('contactInfo.phone.value')} {t('contactInfo.phone.value')}
</a> </a>
@@ -222,16 +222,16 @@ export function ContactForm() {
transition={{ delay: 0.5 }} transition={{ delay: 0.5 }}
className="flex items-start gap-4" className="flex items-start gap-4"
> >
<div className="p-3 bg-zinc-800/50 rounded-lg"> <div className="p-3 bg-card/50 rounded-lg">
<Mail className="h-6 w-6 text-zinc-400" aria-hidden="true" /> <Mail className="h-6 w-6 text-muted-foreground" aria-hidden="true" />
</div> </div>
<div> <div>
<h3 className="text-sm font-medium text-zinc-300 mb-1"> <h3 className="text-sm font-medium text-foreground/80 mb-1">
{t('contactInfo.email.label')} {t('contactInfo.email.label')}
</h3> </h3>
<a <a
href="mailto:info@damjan-savic.com" href="mailto:info@damjan-savic.com"
className="text-zinc-400 hover:text-white transition-colors duration-200" className="text-muted-foreground hover:text-foreground transition-colors duration-200"
> >
{t('contactInfo.email.value')} {t('contactInfo.email.value')}
</a> </a>
@@ -243,7 +243,7 @@ export function ContactForm() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: 0.6 }} transition={{ delay: 0.6 }}
className="text-sm text-zinc-500" className="text-sm text-muted-foreground"
> >
{t('contactInfo.availabilityNote')} {t('contactInfo.availabilityNote')}
</motion.p> </motion.p>
@@ -255,21 +255,21 @@ export function ContactForm() {
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }} transition={{ delay: 0.3 }}
> >
<div className="bg-zinc-800/30 border border-zinc-800 rounded-xl p-6 md:p-8"> <div className="bg-card/30 border border-border rounded-xl p-6 md:p-8">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Mail className="h-5 w-5 text-zinc-400" aria-hidden="true" /> <Mail className="h-5 w-5 text-muted-foreground" aria-hidden="true" />
<h2 className="text-xl font-semibold text-white"> <h2 className="text-xl font-semibold text-foreground">
{t('contactForm.title')} {t('contactForm.title')}
</h2> </h2>
</div> </div>
<p className="text-zinc-400 mb-6"> <p className="text-muted-foreground mb-6">
{t('contactForm.description')} {t('contactForm.description')}
</p> </p>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="name" className="block text-sm text-zinc-300"> <label htmlFor="name" className="block text-sm text-foreground/80">
{t('contactForm.name.label')} {t('contactForm.name.label')}
</label> </label>
<input <input
@@ -283,7 +283,7 @@ export function ContactForm() {
aria-required="true" aria-required="true"
aria-invalid={!!errors.name} aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined} aria-describedby={errors.name ? 'name-error' : undefined}
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50" className="w-full h-12 px-4 bg-background/50 border border-border rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
/> />
{errors.name && ( {errors.name && (
<p id="name-error" role="alert" className="text-sm text-red-400">{errors.name}</p> <p id="name-error" role="alert" className="text-sm text-red-400">{errors.name}</p>
@@ -291,7 +291,7 @@ export function ContactForm() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="email" className="block text-sm text-zinc-300"> <label htmlFor="email" className="block text-sm text-foreground/80">
{t('contactForm.email.label')} {t('contactForm.email.label')}
</label> </label>
<input <input
@@ -305,7 +305,7 @@ export function ContactForm() {
aria-required="true" aria-required="true"
aria-invalid={!!errors.email} aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined} aria-describedby={errors.email ? 'email-error' : undefined}
className="w-full h-12 px-4 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50" className="w-full h-12 px-4 bg-background/50 border border-border rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
/> />
{errors.email && ( {errors.email && (
<p id="email-error" role="alert" className="text-sm text-red-400">{errors.email}</p> <p id="email-error" role="alert" className="text-sm text-red-400">{errors.email}</p>
@@ -313,7 +313,7 @@ export function ContactForm() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="message" className="block text-sm text-zinc-300"> <label htmlFor="message" className="block text-sm text-foreground/80">
{t('contactForm.message.label')} {t('contactForm.message.label')}
</label> </label>
<textarea <textarea
@@ -328,7 +328,7 @@ export function ContactForm() {
aria-required="true" aria-required="true"
aria-invalid={!!errors.message} aria-invalid={!!errors.message}
aria-describedby={errors.message ? 'message-error' : undefined} aria-describedby={errors.message ? 'message-error' : undefined}
className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-800 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-zinc-600 disabled:opacity-50 resize-none" className="w-full px-4 py-3 bg-background/50 border border-border rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50 resize-none"
/> />
{errors.message && ( {errors.message && (
<p id="message-error" role="alert" className="text-sm text-red-400">{errors.message}</p> <p id="message-error" role="alert" className="text-sm text-red-400">{errors.message}</p>
@@ -340,7 +340,7 @@ export function ContactForm() {
? 'text-red-500' ? 'text-red-500'
: messageLength >= MAX_MESSAGE_LENGTH * 0.8 : messageLength >= MAX_MESSAGE_LENGTH * 0.8
? 'text-yellow-500' ? 'text-yellow-500'
: 'text-zinc-500' : 'text-muted-foreground'
}`} }`}
> >
{t('contactForm.characterCounter', { current: messageLength, max: MAX_MESSAGE_LENGTH })} {t('contactForm.characterCounter', { current: messageLength, max: MAX_MESSAGE_LENGTH })}
@@ -397,7 +397,7 @@ export function ContactForm() {
disabled={isSubmitting} disabled={isSubmitting}
aria-busy={isSubmitting} aria-busy={isSubmitting}
aria-label={isSubmitting ? t('contactForm.submitting') : undefined} aria-label={isSubmitting ? t('contactForm.submitting') : undefined}
className="w-full h-12 bg-zinc-100 hover:bg-white text-zinc-900 font-medium rounded-lg transition-colors duration-200 duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" className="w-full h-12 bg-primary text-primary-foreground hover:bg-accent font-medium rounded-lg transition-colors duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
> >
{isSubmitting ? ( {isSubmitting ? (
<Loader2 className="h-5 w-5 animate-spin" aria-hidden="true" /> <Loader2 className="h-5 w-5 animate-spin" aria-hidden="true" />
+7 -9
View File
@@ -9,33 +9,31 @@ export function ContactInfo() {
const t = useTranslations('footer.sections.contact'); const t = useTranslations('footer.sections.contact');
return ( return (
<div className="pb-6 px-6 pt-4 bg-zinc-800/30 border border-zinc-800 rounded-lg mx-4 mt-2"> <div className="pb-6 px-6 pt-4 bg-card/30 border border-border rounded-lg mx-4 mt-2">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-sm font-medium text-white tracking-wider uppercase"> <h3 className="text-sm font-medium text-foreground tracking-wider uppercase">
{t('title')} {t('title')}
</h3> </h3>
<a <a
href="tel:+491756950979" href="tel:+491756950979"
className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group" className="flex items-center space-x-3 text-sm text-muted-foreground hover:text-foreground transition-colors duration-200 group"
> >
<Phone className="h-4 w-4 text-zinc-400 group-hover:text-white transition-colors duration-200" /> <Phone className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors duration-200" />
<span>+49 175 695 0979</span> <span>+49 175 695 0979</span>
</a> </a>
<a <a
href="mailto:info@damjan-savic.com" href="mailto:info@damjan-savic.com"
className="flex items-center space-x-3 text-sm text-zinc-400 hover:text-white transition-colors duration-200 group" className="flex items-center space-x-3 text-sm text-muted-foreground hover:text-foreground transition-colors duration-200 group"
> >
<Mail className="h-4 w-4 text-zinc-400 group-hover:text-white transition-colors duration-200" /> <Mail className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors duration-200" />
<span>{t('email')}</span> <span>{t('email')}</span>
</a> </a>
<Link <Link
href={`/${locale}/contact`} href={`/${locale}/contact`}
className="flex items-center justify-between mt-6 px-4 py-2 bg-zinc-800/50 className="flex items-center justify-between mt-6 px-4 py-2 bg-card/50 hover:bg-card rounded-full text-sm text-muted-foreground hover:text-foreground transition-all duration-200 group border border-border hover:border-accent"
hover:bg-zinc-800 rounded-full text-sm text-zinc-400 hover:text-white
transition-all duration-200 group border border-zinc-800 hover:border-zinc-700"
> >
<span className="tracking-wide">Kontakt aufnehmen</span> <span className="tracking-wide">Kontakt aufnehmen</span>
<ArrowRight className="h-4 w-4 transform group-hover:translate-x-1 transition-transform duration-200" /> <ArrowRight className="h-4 w-4 transform group-hover:translate-x-1 transition-transform duration-200" />
+20 -20
View File
@@ -8,24 +8,24 @@ const Footer = async () => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
return ( return (
<footer className="relative bg-zinc-800/50 backdrop-blur-sm border-t border-zinc-700/30 pt-8 pb-4 px-4"> <footer className="relative bg-card/50 backdrop-blur-sm border-t border-border/30 pt-6 sm:pt-8 pb-4 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="flex flex-col md:grid md:grid-cols-2 lg:grid-cols-4 gap-8"> <div className="flex flex-col sm:grid sm:grid-cols-2 lg:grid-cols-4 gap-6 sm:gap-8">
{/* Contact Section */} {/* Contact Section */}
<div> <div>
<h3 className="text-white text-base font-semibold mb-3"> <h3 className="text-foreground text-base font-semibold mb-3">
{t('sections.contact.title')} {t('sections.contact.title')}
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
<a <a
href={`mailto:${t('sections.contact.email')}`} href={`mailto:${t('sections.contact.email')}`}
className="group flex items-center gap-2 text-zinc-400 hover:text-white transition-colors duration-200 text-sm w-fit" className="group flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors duration-200 text-sm w-fit"
> >
<Mail className="h-4 w-4 text-zinc-500 group-hover:text-white transition-colors duration-200 flex-shrink-0" /> <Mail className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors duration-200 flex-shrink-0" />
<span>{t('sections.contact.email')}</span> <span>{t('sections.contact.email')}</span>
</a> </a>
<div className="flex items-center gap-2 text-zinc-400 text-sm w-fit"> <div className="flex items-center gap-2 text-muted-foreground text-sm w-fit">
<MapPin className="h-4 w-4 text-zinc-500 flex-shrink-0" /> <MapPin className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<span>{t('sections.contact.location')}</span> <span>{t('sections.contact.location')}</span>
</div> </div>
</div> </div>
@@ -33,31 +33,31 @@ const Footer = async () => {
{/* Navigation Section */} {/* Navigation Section */}
<div> <div>
<h3 className="text-white text-base font-semibold mb-3 text-left"> <h3 className="text-foreground text-base font-semibold mb-3 text-left">
{t('sections.navigation.title')} {t('sections.navigation.title')}
</h3> </h3>
<nav className="flex flex-col space-y-2"> <nav className="flex flex-col space-y-2">
<Link <Link
href={`/${locale}/portfolio`} href={`/${locale}/portfolio`}
className="text-zinc-400 hover:text-white transition-colors duration-200 text-sm w-fit" className="text-muted-foreground hover:text-foreground transition-colors duration-200 text-sm w-fit"
> >
{t('sections.navigation.links.portfolio')} {t('sections.navigation.links.portfolio')}
</Link> </Link>
<Link <Link
href={`/${locale}/blog`} href={`/${locale}/blog`}
className="text-zinc-400 hover:text-white transition-colors duration-200 text-sm w-fit" className="text-muted-foreground hover:text-foreground transition-colors duration-200 text-sm w-fit"
> >
{t('sections.navigation.links.blog')} {t('sections.navigation.links.blog')}
</Link> </Link>
<Link <Link
href={`/${locale}/about`} href={`/${locale}/about`}
className="text-zinc-400 hover:text-white transition-colors duration-200 text-sm w-fit" className="text-muted-foreground hover:text-foreground transition-colors duration-200 text-sm w-fit"
> >
{t('sections.navigation.links.about')} {t('sections.navigation.links.about')}
</Link> </Link>
<Link <Link
href={`/${locale}/contact`} href={`/${locale}/contact`}
className="text-zinc-400 hover:text-white transition-colors duration-200 text-sm w-fit" className="text-muted-foreground hover:text-foreground transition-colors duration-200 text-sm w-fit"
> >
{t('sections.navigation.links.contact')} {t('sections.navigation.links.contact')}
</Link> </Link>
@@ -66,7 +66,7 @@ const Footer = async () => {
{/* Social Media Section */} {/* Social Media Section */}
<div> <div>
<h3 className="text-white text-base font-semibold mb-3 text-left"> <h3 className="text-foreground text-base font-semibold mb-3 text-left">
{t('sections.social.title')} {t('sections.social.title')}
</h3> </h3>
<div className="flex space-x-4"> <div className="flex space-x-4">
@@ -74,7 +74,7 @@ const Footer = async () => {
href="https://www.linkedin.com/in/damjan-savić-720288127/" href="https://www.linkedin.com/in/damjan-savić-720288127/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group text-zinc-400 hover:text-white transition-colors duration-200" className="group text-muted-foreground hover:text-foreground transition-colors duration-200"
aria-label="LinkedIn" aria-label="LinkedIn"
> >
<Linkedin className="transform transition-transform group-hover:scale-110" size={20} /> <Linkedin className="transform transition-transform group-hover:scale-110" size={20} />
@@ -83,7 +83,7 @@ const Footer = async () => {
href="https://github.com/damjan1996" href="https://github.com/damjan1996"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group text-zinc-400 hover:text-white transition-colors duration-200" className="group text-muted-foreground hover:text-foreground transition-colors duration-200"
aria-label="GitHub" aria-label="GitHub"
> >
<Github className="transform transition-transform group-hover:scale-110" size={20} /> <Github className="transform transition-transform group-hover:scale-110" size={20} />
@@ -92,27 +92,27 @@ const Footer = async () => {
</div> </div>
{/* Legal Section */} {/* Legal Section */}
<div className="border-t border-zinc-700/30 pt-4"> <div className="border-t sm:border-t-0 lg:border-t border-border/30 pt-4 sm:pt-0 lg:pt-4">
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<p className="text-zinc-400 text-xs text-left"> <p className="text-muted-foreground text-xs text-left">
&copy; {currentYear} Damjan Savić. {t('legal.copyright')} &copy; {currentYear} Damjan Savić. {t('legal.copyright')}
</p> </p>
<div className="flex space-x-4"> <div className="flex space-x-4">
<Link <Link
href={`/${locale}/privacy`} href={`/${locale}/privacy`}
className="text-zinc-400 hover:text-white text-xs transition-colors duration-200" className="text-muted-foreground hover:text-foreground text-xs transition-colors duration-200"
> >
{t('legal.links.privacy')} {t('legal.links.privacy')}
</Link> </Link>
<Link <Link
href={`/${locale}/imprint`} href={`/${locale}/imprint`}
className="text-zinc-400 hover:text-white text-xs transition-colors duration-200" className="text-muted-foreground hover:text-foreground text-xs transition-colors duration-200"
> >
{t('legal.links.imprint')} {t('legal.links.imprint')}
</Link> </Link>
<Link <Link
href={`/${locale}/terms`} href={`/${locale}/terms`}
className="text-zinc-400 hover:text-white text-xs transition-colors duration-200" className="text-muted-foreground hover:text-foreground text-xs transition-colors duration-200"
> >
{t('legal.links.terms')} {t('legal.links.terms')}
</Link> </Link>
+7 -7
View File
@@ -17,7 +17,7 @@ const GlobalBackground = () => {
{/* Background layer */} {/* Background layer */}
<div <div
ref={ref} ref={ref}
className="fixed inset-0 bg-zinc-900" className="fixed inset-0 bg-background"
style={{ zIndex: -2 }} style={{ zIndex: -2 }}
/> />
@@ -26,7 +26,7 @@ const GlobalBackground = () => {
className="fixed inset-0" className="fixed inset-0"
style={{ style={{
zIndex: -1, zIndex: -1,
background: 'linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 50%, #1e1e1e 100%)', background: 'linear-gradient(135deg, #181C14 0%, #1E221A 50%, #181C14 100%)',
opacity: 0.9 opacity: 0.9
}} }}
/> />
@@ -46,7 +46,7 @@ const GlobalBackground = () => {
y1="20%" y1="20%"
x2="100%" x2="100%"
y2="20%" y2="20%"
stroke="white" stroke="#ECDFCC"
strokeWidth="1" strokeWidth="1"
opacity="0.2" opacity="0.2"
> >
@@ -73,7 +73,7 @@ const GlobalBackground = () => {
y1="50%" y1="50%"
x2="100%" x2="100%"
y2="50%" y2="50%"
stroke="white" stroke="#ECDFCC"
strokeWidth="1" strokeWidth="1"
opacity="0.15" opacity="0.15"
> >
@@ -92,7 +92,7 @@ const GlobalBackground = () => {
y1="0%" y1="0%"
x2="20%" x2="20%"
y2="100%" y2="100%"
stroke="white" stroke="#ECDFCC"
strokeWidth="1" strokeWidth="1"
opacity="0.1" opacity="0.1"
> >
@@ -122,8 +122,8 @@ const GlobalBackground = () => {
style={{ style={{
zIndex: -1, zIndex: -1,
backgroundImage: ` backgroundImage: `
linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px), linear-gradient(rgba(236,223,204,.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px) linear-gradient(90deg, rgba(236,223,204,.05) 1px, transparent 1px)
`, `,
backgroundSize: '50px 50px', backgroundSize: '50px 50px',
opacity: 0.5 opacity: 0.5
+13 -13
View File
@@ -92,9 +92,9 @@ const Header = () => {
<nav <nav
className={` className={`
fixed w-full z-40 transition-all duration-300 fixed w-full z-40 transition-all duration-300
bg-zinc-800/50 backdrop-blur-sm border-b border-zinc-700/30 bg-card/50 backdrop-blur-sm border-b border-border/30
${scrolled ${scrolled
? 'bg-zinc-800/70 backdrop-blur-md shadow-lg shadow-zinc-900/30' ? 'bg-card/70 backdrop-blur-md shadow-lg shadow-background/30'
: '' : ''
} }
`} `}
@@ -102,7 +102,7 @@ const Header = () => {
aria-label="Main navigation" aria-label="Main navigation"
> >
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-14 sm:h-16">
{/* Logo */} {/* Logo */}
<Link href={`/${locale}`} className="flex items-center space-x-2 group shrink-0"> <Link href={`/${locale}`} className="flex items-center space-x-2 group shrink-0">
<Image <Image
@@ -126,14 +126,14 @@ const Header = () => {
className="text-sm" className="text-sm"
/> />
))} ))}
<div className="ml-4 text-zinc-400 pl-2 border-l border-zinc-700"> <div className="ml-4 text-muted-foreground pl-2 border-l border-border">
<LanguageSwitcher /> <LanguageSwitcher />
</div> </div>
</div> </div>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <button
className="md:hidden relative z-50 p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors active:scale-95" className="md:hidden relative z-50 p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-card/50 transition-colors active:scale-95"
onClick={() => setIsMenuOpen(!isMenuOpen)} onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-expanded={isMenuOpen} aria-expanded={isMenuOpen}
aria-controls="mobile-menu" aria-controls="mobile-menu"
@@ -156,30 +156,30 @@ const Header = () => {
{/* Mobile Sidebar */} {/* Mobile Sidebar */}
<div <div
id="mobile-menu" id="mobile-menu"
className={`fixed right-0 top-0 h-full w-80 bg-zinc-900/95 backdrop-blur-md shadow-xl md:hidden z-50 border-l border-zinc-800 transition-transform duration-300 ease-out ${ className={`fixed right-0 top-0 h-full w-[min(20rem,90vw)] bg-background/95 backdrop-blur-md shadow-xl md:hidden z-50 border-l border-border transition-transform duration-300 ease-out ${
isMenuOpen ? 'translate-x-0' : 'translate-x-full' isMenuOpen ? 'translate-x-0' : 'translate-x-full'
}`} }`}
> >
{/* Sidebar Header */} {/* Sidebar Header */}
<div className="flex items-center justify-between px-4 h-16 border-b border-zinc-800"> <div className="flex items-center justify-between px-4 h-14 sm:h-16 border-b border-border">
<span className="text-white font-semibold">Menu</span> <span className="text-foreground font-semibold">Menu</span>
<button <button
onClick={() => setIsMenuOpen(false)} onClick={() => setIsMenuOpen(false)}
aria-label="Close sidebar" aria-label="Close sidebar"
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800/50 transition-colors active:scale-95" className="p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-card/50 transition-colors active:scale-95"
> >
<X className="h-6 w-6" /> <X className="h-6 w-6" />
</button> </button>
</div> </div>
{/* Scrollable Sidebar Content */} {/* Scrollable Sidebar Content */}
<div className="h-[calc(100vh-4rem)] overflow-y-auto mobile-safe"> <div className="h-[calc(100vh-3.5rem)] sm:h-[calc(100vh-4rem)] overflow-y-auto mobile-safe">
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Contact Info */} {/* Contact Info */}
<ContactInfo /> <ContactInfo />
{/* Navigation Links */} {/* Navigation Links */}
<div className="py-4 px-4"> <div className="py-3 px-3 sm:py-4 sm:px-4">
{navigationLinks.map(({ path, label, icon }) => ( {navigationLinks.map(({ path, label, icon }) => (
<NavLink <NavLink
key={path} key={path}
@@ -193,9 +193,9 @@ const Header = () => {
</div> </div>
{/* Language Switcher */} {/* Language Switcher */}
<div className="px-4 py-3 border-t border-zinc-800"> <div className="px-4 py-3 border-t border-border">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-zinc-400"> <span className="text-sm text-muted-foreground">
Sprache ändern Sprache ändern
</span> </span>
<LanguageSwitcher /> <LanguageSwitcher />
+8 -8
View File
@@ -80,9 +80,9 @@ const LanguageSwitcher = () => {
return ( return (
<div className="relative inline-block" ref={dropdownRef}> <div className="relative inline-block" ref={dropdownRef}>
<button <button
className="flex items-center space-x-2 text-zinc-400 hover:text-white className="flex items-center space-x-2 text-muted-foreground hover:text-foreground
px-4 py-2.5 min-h-[44px] rounded-full transition-all duration-200 px-4 py-2.5 min-h-[44px] rounded-full transition-all duration-200
hover:bg-zinc-800/50 border border-transparent hover:border-zinc-800 hover:bg-card/50 border border-transparent hover:border-border
hover:scale-105 active:scale-95" hover:scale-105 active:scale-95"
onClick={() => { onClick={() => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
@@ -97,8 +97,8 @@ const LanguageSwitcher = () => {
</button> </button>
<div <div
className={`absolute ${isMobile ? 'bottom-full mb-2' : 'top-full mt-2'} right-0 w-48 rounded-lg overflow-hidden className={`absolute ${isMobile ? 'bottom-full mb-2' : 'top-full mt-2'} right-0 w-44 sm:w-48 max-w-[calc(100vw-2rem)] rounded-lg overflow-hidden
border border-zinc-800 bg-zinc-900/95 backdrop-blur-sm shadow-lg border border-border bg-background/95 backdrop-blur-sm shadow-lg
transition-all duration-200 origin-top transition-all duration-200 origin-top
${isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`} ${isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`}
role="listbox" role="listbox"
@@ -112,12 +112,12 @@ const LanguageSwitcher = () => {
key={lang.code} key={lang.code}
id={`lang-option-${lang.code}`} id={`lang-option-${lang.code}`}
className={`w-full text-left px-4 py-2 text-sm transition-colors duration-200 className={`w-full text-left px-4 py-2 text-sm transition-colors duration-200
hover:bg-zinc-800/50 hover:bg-card/50
${locale === lang.code ${locale === lang.code
? 'bg-zinc-800 text-white' ? 'bg-card text-foreground'
: 'text-zinc-400 hover:text-white' : 'text-muted-foreground hover:text-foreground'
} }
${focusedIndex === index ? 'ring-2 ring-orange-500 ring-inset' : ''}`} ${focusedIndex === index ? 'ring-2 ring-ring ring-inset' : ''}`}
onClick={() => handleLanguageChange(lang.code)} onClick={() => handleLanguageChange(lang.code)}
role="option" role="option"
aria-selected={locale === lang.code} aria-selected={locale === lang.code}
+2 -2
View File
@@ -27,8 +27,8 @@ export function NavLink({ href, icon, label, onClick, className }: NavLinkProps)
className={` className={`
relative flex items-center gap-2 rounded-full py-2.5 px-4 min-h-[44px] relative flex items-center gap-2 rounded-full py-2.5 px-4 min-h-[44px]
transition-all duration-200 ease-in-out transition-all duration-200 ease-in-out
focus:outline-none focus:ring-2 focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background
${isActive ? 'text-white bg-zinc-700/60' : 'text-zinc-400 hover:text-white hover:bg-zinc-800/30'} ${isActive ? 'text-foreground bg-accent/60' : 'text-muted-foreground hover:text-foreground hover:bg-card/30'}
${className || ''} ${className || ''}
`} `}
> >
+4 -4
View File
@@ -40,8 +40,8 @@ export function CategoryFilter() {
relative whitespace-nowrap rounded-full py-2 px-4 relative whitespace-nowrap rounded-full py-2 px-4
transition-all duration-200 ease-in-out text-sm tracking-wide transition-all duration-200 ease-in-out text-sm tracking-wide
${!selectedCategory ${!selectedCategory
? 'text-white bg-zinc-700/60' ? 'text-foreground bg-accent/60'
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/30' : 'text-muted-foreground hover:text-foreground hover:bg-card/30'
} }
`} `}
> >
@@ -59,8 +59,8 @@ export function CategoryFilter() {
relative whitespace-nowrap rounded-full py-2 px-4 relative whitespace-nowrap rounded-full py-2 px-4
transition-all duration-200 ease-in-out text-sm tracking-wide transition-all duration-200 ease-in-out text-sm tracking-wide
${isActive ${isActive
? 'text-white bg-zinc-700/60' ? 'text-foreground bg-accent/60'
: 'text-zinc-400 hover:text-white hover:bg-zinc-800/30' : 'text-muted-foreground hover:text-foreground hover:bg-card/30'
} }
`} `}
> >
+18 -18
View File
@@ -30,11 +30,11 @@ const PortfolioCard = ({ project }: PortfolioCardProps) => {
return ( return (
<Link <Link
href={`/${locale}/portfolio/${project.slug}`} href={`/${locale}/portfolio/${project.slug}`}
className="group relative block bg-zinc-900/50 backdrop-blur-sm className="group relative block bg-background/50 backdrop-blur-sm
rounded-xl shadow-lg hover:shadow-2xl rounded-xl shadow-lg hover:shadow-2xl
ring-1 ring-zinc-800 hover:ring-zinc-700 ring-1 ring-border hover:ring-accent
transition-all duration-500 focus:outline-none focus:ring-2 transition-all duration-500 focus:outline-none focus:ring-2
focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background
transform hover:-translate-y-1" transform hover:-translate-y-1"
> >
<motion.div <motion.div
@@ -45,7 +45,7 @@ const PortfolioCard = ({ project }: PortfolioCardProps) => {
className="relative overflow-hidden rounded-xl" className="relative overflow-hidden rounded-xl"
> >
{/* Image Container */} {/* Image Container */}
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900"> <div className="aspect-[16/9] w-full relative overflow-hidden bg-background">
<Image <Image
src={`/images/projects/${project.slug}/cover.avif`} src={`/images/projects/${project.slug}/cover.avif`}
alt={project.title} alt={project.title}
@@ -56,9 +56,9 @@ const PortfolioCard = ({ project }: PortfolioCardProps) => {
</div> </div>
{/* Content */} {/* Content */}
<div className="relative -mt-2 p-6 bg-zinc-900 z-10"> <div className="relative -mt-2 p-6 bg-background z-10">
{/* Project Info */} {/* Project Info */}
<div className="flex items-center gap-4 mb-4 text-zinc-500"> <div className="flex items-center gap-4 mb-4 text-muted-foreground">
{project.date && ( {project.date && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
@@ -74,12 +74,12 @@ const PortfolioCard = ({ project }: PortfolioCardProps) => {
</div> </div>
{/* Title & Description */} {/* Title & Description */}
<h3 className="text-xl font-semibold text-white mb-3 <h3 className="text-xl font-semibold text-foreground mb-3
group-hover:text-zinc-100 transition-colors duration-300"> group-hover:text-foreground/90 transition-colors duration-300">
{project.title} {project.title}
</h3> </h3>
<p className="text-zinc-400 text-sm line-clamp-2 mb-4 <p className="text-muted-foreground text-sm line-clamp-2 mb-4
group-hover:text-zinc-300 transition-colors duration-300"> group-hover:text-foreground/70 transition-colors duration-300">
{project.excerpt || project.description} {project.excerpt || project.description}
</p> </p>
@@ -88,17 +88,17 @@ const PortfolioCard = ({ project }: PortfolioCardProps) => {
{displayTags.slice(0, 3).map((tag, index) => ( {displayTags.slice(0, 3).map((tag, index) => (
<span <span
key={index} key={index}
className="px-3 py-1 bg-zinc-800/50 className="px-3 py-1 bg-card/50
text-sm text-zinc-400 rounded-full text-sm text-muted-foreground rounded-full
ring-1 ring-zinc-700/50 group-hover:ring-zinc-600/50 ring-1 ring-border/50 group-hover:ring-accent/50
group-hover:bg-zinc-800/70 group-hover:text-zinc-300 group-hover:bg-card/70 group-hover:text-foreground/80
transition-all duration-300" transition-all duration-300"
> >
{tag} {tag}
</span> </span>
))} ))}
{displayTags.length > 3 && ( {displayTags.length > 3 && (
<span className="text-sm text-zinc-600"> <span className="text-sm text-muted-foreground/60">
+{displayTags.length - 3} more +{displayTags.length - 3} more
</span> </span>
)} )}
@@ -106,17 +106,17 @@ const PortfolioCard = ({ project }: PortfolioCardProps) => {
{/* Link Icon */} {/* Link Icon */}
<div className="absolute top-6 right-6 p-2.5 rounded-full <div className="absolute top-6 right-6 p-2.5 rounded-full
bg-zinc-800/60 backdrop-blur-sm ring-1 ring-zinc-700/50 bg-card/60 backdrop-blur-sm ring-1 ring-border/50
opacity-0 group-hover:opacity-100 opacity-0 group-hover:opacity-100
transform translate-y-2 group-hover:translate-y-0 transform translate-y-2 group-hover:translate-y-0
transition-all duration-300"> transition-all duration-300">
<ExternalLink className="h-4 w-4 text-zinc-400" /> <ExternalLink className="h-4 w-4 text-muted-foreground" />
</div> </div>
</div> </div>
{/* Subtle Hover Border */} {/* Subtle Hover Border */}
<div className="absolute inset-0 rounded-xl pointer-events-none <div className="absolute inset-0 rounded-xl pointer-events-none
ring-1 ring-zinc-600/0 group-hover:ring-zinc-600/30 ring-1 ring-ring/0 group-hover:ring-ring/30
transition-all duration-500" /> transition-all duration-500" />
</motion.div> </motion.div>
</Link> </Link>
+12 -12
View File
@@ -20,17 +20,17 @@ const About = async () => {
]; ];
return ( return (
<section id="about" className="py-24 relative"> <section id="about" className="py-12 sm:py-16 md:py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 md:gap-12 items-center">
{/* Content Section */} {/* Content Section */}
<div className="space-y-6 order-2 lg:order-1"> <div className="space-y-6 order-2 lg:order-1">
{/* Title */} {/* Title */}
<div> <div>
<h2 className="text-4xl lg:text-5xl font-bold text-white mb-2"> <h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground mb-2">
{t('title')} {t('title')}
</h2> </h2>
<div className="h-1 w-20 bg-gradient-to-r from-zinc-600 to-zinc-800 rounded-full" /> <div className="h-1 w-20 bg-gradient-to-r from-primary to-muted rounded-full" />
</div> </div>
{/* Highlight Pills */} {/* Highlight Pills */}
@@ -38,17 +38,17 @@ const About = async () => {
{highlights.map((item, index) => ( {highlights.map((item, index) => (
<div <div
key={index} key={index}
className="flex items-center gap-2 px-3 py-1.5 bg-zinc-800/30 backdrop-blur-sm rounded-full border border-zinc-700/50 hover:border-zinc-600 transition-colors" className="flex items-center gap-2 px-3 py-1.5 bg-card/30 backdrop-blur-sm rounded-full border border-border/50 hover:border-accent transition-colors"
> >
<span className="text-zinc-400">{item.icon}</span> <span className="text-muted-foreground">{item.icon}</span>
<span className="text-zinc-300 text-sm">{item.label}</span> <span className="text-foreground/80 text-sm">{item.label}</span>
</div> </div>
))} ))}
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="space-y-4"> <div className="space-y-4">
<p className="text-zinc-300 leading-relaxed text-lg"> <p className="text-foreground/80 leading-relaxed text-lg">
{t('content')} {t('content')}
</p> </p>
</div> </div>
@@ -57,7 +57,7 @@ const About = async () => {
<div className="flex flex-wrap gap-4 pt-4"> <div className="flex flex-wrap gap-4 pt-4">
<Link <Link
href={`/${locale}/about`} href={`/${locale}/about`}
className="px-6 py-3 bg-zinc-800/50 backdrop-blur-sm hover:bg-zinc-700/50 text-white rounded-xl font-medium transition-colors duration-300 flex items-center gap-2" className="px-6 py-3 bg-card/50 backdrop-blur-sm hover:bg-accent/50 text-foreground rounded-xl font-medium transition-colors duration-300 flex items-center gap-2"
> >
<GraduationCap className="w-4 h-4" /> <GraduationCap className="w-4 h-4" />
{t('buttons.learnMore')} {t('buttons.learnMore')}
@@ -65,7 +65,7 @@ const About = async () => {
<a <a
href={getCvUrl()} href={getCvUrl()}
download download
className="px-6 py-3 bg-transparent hover:bg-zinc-800/30 text-zinc-300 border border-zinc-700/50 rounded-xl font-medium transition-colors duration-300 flex items-center gap-2" className="px-6 py-3 bg-transparent hover:bg-card/30 text-foreground/80 border border-border/50 rounded-xl font-medium transition-colors duration-300 flex items-center gap-2"
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
{t('buttons.downloadCV')} {t('buttons.downloadCV')}
@@ -75,12 +75,12 @@ const About = async () => {
{/* Image Section */} {/* Image Section */}
<div className="relative order-1 lg:order-2"> <div className="relative order-1 lg:order-2">
<div className="relative rounded-3xl overflow-hidden aspect-square bg-zinc-800/30 backdrop-blur-sm"> <div className="relative rounded-3xl overflow-hidden aspect-square bg-card/30 backdrop-blur-sm">
<Image <Image
src="/images/gallery/frontal-smile.avif" src="/images/gallery/frontal-smile.avif"
alt="Damjan Savić" alt="Damjan Savić"
fill fill
sizes="(max-width: 1024px) 100vw, 50vw" sizes="(max-width: 1023px) 90vw, 50vw"
className="object-cover object-bottom" className="object-cover object-bottom"
/> />
</div> </div>
+11 -13
View File
@@ -73,7 +73,7 @@ const Experience = () => {
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }} transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
> >
<h2 className="text-3xl font-bold text-white text-center mb-12"> <h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-foreground text-center mb-8 sm:mb-12">
{t('title')} {t('title')}
</h2> </h2>
<div <div
@@ -99,15 +99,15 @@ const Experience = () => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.15, ease: [0.25, 0.46, 0.45, 0.94] }} transition={{ duration: 0.6, delay: index * 0.15, ease: [0.25, 0.46, 0.45, 0.94] }}
whileHover={{ scale: 1.02, y: -5, transition: { duration: 0.3 } }} whileHover={{ scale: 1.02, y: -5, transition: { duration: 0.3 } }}
className="p-6 md:p-8 rounded-3xl bg-zinc-800/40 hover:bg-zinc-800/60 transition-all duration-300 hover:shadow-xl hover:shadow-zinc-900/50" className="p-6 md:p-8 rounded-3xl bg-card/40 hover:bg-card/60 transition-all duration-300 hover:shadow-xl hover:shadow-background/50"
> >
<h3 className="text-lg md:text-xl font-semibold text-white mb-1"> <h3 className="text-lg md:text-xl font-semibold text-foreground mb-1">
{exp.role} {exp.role}
</h3> </h3>
<h4 className="text-base md:text-lg text-zinc-300 mb-2"> <h4 className="text-base md:text-lg text-foreground/80 mb-2">
{exp.company} {exp.company}
</h4> </h4>
<p className="text-sm text-zinc-400 mb-6"> <p className="text-sm text-muted-foreground mb-6">
{exp.period} {exp.period}
</p> </p>
<ul className="space-y-3 md:space-y-4"> <ul className="space-y-3 md:space-y-4">
@@ -117,9 +117,9 @@ const Experience = () => {
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: 0.3 + (hIndex * 0.1), ease: 'easeOut' }} transition={{ duration: 0.4, delay: 0.3 + (hIndex * 0.1), ease: 'easeOut' }}
className="text-zinc-400 text-sm leading-relaxed flex items-start" className="text-muted-foreground text-sm leading-relaxed flex items-start"
> >
<span className="text-white/40 mr-2 mt-0.5"></span> <span className="text-foreground/40 mr-2 mt-0.5"></span>
<span>{highlight}</span> <span>{highlight}</span>
</motion.li> </motion.li>
))} ))}
@@ -135,7 +135,7 @@ const Experience = () => {
<div className="flex justify-center items-center gap-4 mt-8"> <div className="flex justify-center items-center gap-4 mt-8">
<button <button
onClick={() => currentPage > 0 && setCurrentPage(prev => prev - 1)} onClick={() => currentPage > 0 && setCurrentPage(prev => prev - 1)}
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30" className="text-muted-foreground hover:text-foreground transition-all duration-300 p-3 sm:p-2 text-2xl font-light disabled:opacity-30"
disabled={currentPage === 0} disabled={currentPage === 0}
aria-label="Previous page" aria-label="Previous page"
> >
@@ -146,22 +146,20 @@ const Experience = () => {
<button <button
key={index} key={index}
onClick={() => setCurrentPage(index)} onClick={() => setCurrentPage(index)}
className="relative flex items-center justify-center transition-all duration-300" className="relative flex items-center justify-center w-10 h-10 sm:w-6 sm:h-6 transition-all duration-300"
style={{ width: '24px', height: '24px' }}
aria-label={`Page ${index + 1}`} aria-label={`Page ${index + 1}`}
> >
<span <span
className={`rounded-full transition-all duration-300 ${ className={`rounded-full transition-all duration-300 ${
index === currentPage ? 'bg-white' : 'bg-zinc-600 hover:bg-zinc-400' index === currentPage ? 'bg-foreground w-2.5 h-2.5' : 'bg-muted hover:bg-muted-foreground w-2 h-2'
}`} }`}
style={{ width: '8px', height: '8px' }}
/> />
</button> </button>
))} ))}
</div> </div>
<button <button
onClick={() => currentPage < totalPages - 1 && setCurrentPage(prev => prev + 1)} onClick={() => currentPage < totalPages - 1 && setCurrentPage(prev => prev + 1)}
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30" className="text-muted-foreground hover:text-foreground transition-all duration-300 p-3 sm:p-2 text-2xl font-light disabled:opacity-30"
disabled={currentPage === totalPages - 1} disabled={currentPage === totalPages - 1}
aria-label="Next page" aria-label="Next page"
> >
+25 -24
View File
@@ -6,13 +6,13 @@ const Hero = async () => {
const t = await getTranslations('pages.home.hero'); const t = await getTranslations('pages.home.hero');
return ( return (
<section id="home" className="relative min-h-screen text-white overflow-hidden"> <section id="home" className="relative min-h-screen text-foreground overflow-hidden">
{/* Concentric Circles Background */} {/* Concentric Circles Background */}
<div className="absolute inset-0 pointer-events-none flex items-center justify-center" style={{ zIndex: 2 }}> <div className="absolute inset-0 pointer-events-none flex items-center justify-center" style={{ zIndex: 2 }}>
{[1, 2, 3].map((index) => ( {[1, 2, 3].map((index) => (
<div <div
key={index} key={index}
className="absolute rounded-full border border-zinc-800/30" className="absolute rounded-full border border-border/30"
style={{ style={{
width: `${index * 30}%`, width: `${index * 30}%`,
height: `${index * 30}%`, height: `${index * 30}%`,
@@ -24,14 +24,14 @@ const Hero = async () => {
</div> </div>
{/* Social Links Bar */} {/* Social Links Bar */}
<div className="absolute top-20 left-0 right-0 px-4 sm:px-8" style={{ zIndex: 40 }}> <div className="absolute top-20 left-0 right-0 px-4 sm:px-6 lg:px-8" style={{ zIndex: 40 }}>
<div className="max-w-7xl mx-auto flex justify-between items-center"> <div className="max-w-7xl mx-auto flex justify-between items-center">
<div className="flex gap-4"> <div className="flex gap-4">
<a <a
href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/" href="https://www.linkedin.com/in/damjan-savi%C4%87-720288127/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group text-zinc-400 hover:text-white transition-colors" className="group text-muted-foreground hover:text-foreground transition-colors"
aria-label="LinkedIn" aria-label="LinkedIn"
> >
<Linkedin className="transform transition-transform group-hover:scale-110" size={20} /> <Linkedin className="transform transition-transform group-hover:scale-110" size={20} />
@@ -40,7 +40,7 @@ const Hero = async () => {
href="https://github.com/damjan1996" href="https://github.com/damjan1996"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group text-zinc-400 hover:text-white transition-colors" className="group text-muted-foreground hover:text-foreground transition-colors"
aria-label="GitHub" aria-label="GitHub"
> >
<Github className="transform transition-transform group-hover:scale-110" size={20} /> <Github className="transform transition-transform group-hover:scale-110" size={20} />
@@ -48,7 +48,7 @@ const Hero = async () => {
</div> </div>
<a <a
href="mailto:info@damjan-savic.com" href="mailto:info@damjan-savic.com"
className="text-sm text-zinc-400 hover:text-white transition-colors uppercase tracking-wider" className="text-sm text-muted-foreground hover:text-foreground transition-colors uppercase tracking-wider"
> >
{t('cta')} {t('cta')}
</a> </a>
@@ -56,10 +56,10 @@ const Hero = async () => {
</div> </div>
{/* Main Content */} {/* Main Content */}
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-32 relative" style={{ zIndex: 30 }}> <div className="flex flex-col items-center justify-start min-h-screen px-4 sm:px-6 pt-24 sm:pt-32 relative" style={{ zIndex: 30 }}>
{/* Profile Image */} {/* Profile Image */}
<div <div
className="w-56 h-56 sm:w-72 sm:h-72 mb-8 rounded-full overflow-hidden border-2 border-zinc-800" className="w-44 h-44 sm:w-56 sm:h-56 md:w-64 md:h-64 lg:w-72 lg:h-72 mb-6 sm:mb-8 rounded-full overflow-hidden border-2 border-border"
style={{ transform: 'translateZ(0)' }} style={{ transform: 'translateZ(0)' }}
> >
<Image <Image
@@ -67,66 +67,67 @@ const Hero = async () => {
alt={t('image.alt')} alt={t('image.alt')}
width={288} width={288}
height={288} height={288}
sizes="(max-width: 640px) 176px, (max-width: 768px) 224px, (max-width: 1024px) 256px, 288px"
className="w-full h-full object-cover" className="w-full h-full object-cover"
priority priority
/> />
</div> </div>
{/* Title and Name */} {/* Title and Name */}
<p className="text-zinc-400 text-sm sm:text-base tracking-wider mb-2"> <p className="text-muted-foreground text-xs sm:text-sm md:text-base tracking-wider mb-2">
{t('title')} {t('title')}
</p> </p>
<h1 className="text-4xl sm:text-5xl font-bold mb-3 flex items-center"> <h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold mb-3 flex items-center">
{t('name')}<span className="animate-pulse ml-1">|</span> {t('name')}<span className="animate-pulse ml-1">|</span>
</h1> </h1>
<p className="text-zinc-300 text-sm sm:text-base max-w-xl text-center mb-2 px-4"> <p className="text-foreground/80 text-sm sm:text-base max-w-xl text-center mb-2 px-2">
{t('description')} {t('description')}
</p> </p>
<p className="text-zinc-400 text-xs sm:text-sm mb-8"> <p className="text-muted-foreground text-xs sm:text-sm mb-6 sm:mb-8">
{t('currentRole')} {t('currentRole')}
</p> </p>
{/* Navigation */} {/* Navigation */}
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 text-white text-base sm:text-lg z-20"> <div className="flex flex-wrap justify-center gap-3 sm:gap-4 md:gap-6 text-foreground text-sm sm:text-base md:text-lg z-20 w-full max-w-2xl">
<a <a
href="#experience" href="#experience"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center" className="relative px-5 sm:px-8 py-2.5 sm:py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center"
> >
<span className="relative z-10">{t('navigation.experience')}</span> <span className="relative z-10">{t('navigation.experience')}</span>
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" /> <div className="absolute inset-0 bg-accent/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</a> </a>
<a <a
href="#skills" href="#skills"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center" className="relative px-5 sm:px-8 py-2.5 sm:py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center"
> >
<span className="relative z-10">{t('navigation.skills')}</span> <span className="relative z-10">{t('navigation.skills')}</span>
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" /> <div className="absolute inset-0 bg-accent/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</a> </a>
<a <a
href="#projects" href="#projects"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center" className="relative px-5 sm:px-8 py-2.5 sm:py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center"
> >
<span className="relative z-10">{t('navigation.projects')}</span> <span className="relative z-10">{t('navigation.projects')}</span>
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" /> <div className="absolute inset-0 bg-accent/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</a> </a>
<a <a
href="#about" href="#about"
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center" className="relative px-5 sm:px-8 py-2.5 sm:py-3 rounded-full uppercase cursor-pointer overflow-hidden group text-center"
> >
<span className="relative z-10">{t('navigation.about')}</span> <span className="relative z-10">{t('navigation.about')}</span>
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" /> <div className="absolute inset-0 bg-accent/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
</a> </a>
</div> </div>
</div> </div>
{/* Contact Button Bottom */} {/* Contact Button Bottom */}
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2" style={{ zIndex: 40 }}> <div className="absolute bottom-6 sm:bottom-8 left-1/2 transform -translate-x-1/2" style={{ zIndex: 40 }}>
<a <a
href="mailto:info@damjan-savic.com" href="mailto:info@damjan-savic.com"
className="w-12 h-12 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition-colors" className="w-12 h-12 flex items-center justify-center rounded-full bg-foreground/10 hover:bg-foreground/20 transition-colors"
aria-label={t('social.getInTouch')} aria-label={t('social.getInTouch')}
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="white"> <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M0 3v18h24v-18h-24zm21.518 2l-9.518 7.713-9.518-7.713h19.036zm-19.518 14v-11.817l10 8.104 10-8.104v11.817h-20z"/> <path d="M0 3v18h24v-18h-24zm21.518 2l-9.518 7.713-9.518-7.713h19.036zm-19.518 14v-11.817l10 8.104 10-8.104v11.817h-20z"/>
</svg> </svg>
</a> </a>
+20 -20
View File
@@ -36,47 +36,47 @@ const Projects = async () => {
const locale = await getLocale(); const locale = await getLocale();
return ( return (
<section id="projects" className="py-24"> <section id="projects" className="py-12 sm:py-16 md:py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center mb-12"> <div className="flex justify-between items-center mb-8 sm:mb-12">
<h2 className="text-3xl font-bold text-zinc-100"> <h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-foreground">
{t('title')} {t('title')}
</h2> </h2>
<Link <Link
href={`/${locale}/portfolio`} href={`/${locale}/portfolio`}
className="text-zinc-400 hover:text-white transition-colors duration-300" className="text-muted-foreground hover:text-foreground transition-colors duration-300"
> >
{t('viewAll')} {t('viewAll')}
</Link> </Link>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{projectsData.map((project) => ( {projectsData.map((project) => (
<Link <Link
key={project.slug} key={project.slug}
href={`/${locale}/portfolio/${project.slug}`} href={`/${locale}/portfolio/${project.slug}`}
className="group relative block bg-zinc-900/50 backdrop-blur-sm className="group relative block bg-background/50 backdrop-blur-sm
rounded-xl shadow-lg hover:shadow-2xl rounded-xl shadow-lg hover:shadow-2xl
ring-1 ring-zinc-800 hover:ring-zinc-700 ring-1 ring-border hover:ring-accent
transition-all duration-500 focus:outline-none focus:ring-2 transition-all duration-500 focus:outline-none focus:ring-2
focus:ring-zinc-600 focus:ring-offset-2 focus:ring-offset-zinc-950 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background
transform hover:-translate-y-1" transform hover:-translate-y-1"
> >
<div className="relative overflow-hidden rounded-xl"> <div className="relative overflow-hidden rounded-xl">
{/* Image Container */} {/* Image Container */}
<div className="aspect-[16/9] w-full relative overflow-hidden bg-zinc-900"> <div className="aspect-[16/9] w-full relative overflow-hidden bg-background">
<Image <Image
src={`/images/projects/${project.slug}/cover.avif`} src={`/images/projects/${project.slug}/cover.avif`}
alt={project.title} alt={project.title}
fill fill
sizes="(max-width: 640px) 350px, (max-width: 768px) 400px, (max-width: 1024px) 550px, 400px" sizes="(max-width: 640px) 100vw, (max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover transition-transform duration-700 group-hover:scale-110" className="object-cover transition-transform duration-700 group-hover:scale-110"
/> />
</div> </div>
{/* Content */} {/* Content */}
<div className="relative -mt-2 p-6 bg-zinc-900 z-10"> <div className="relative -mt-2 p-6 bg-background z-10">
<div className="flex items-center gap-4 mb-4 text-zinc-500"> <div className="flex items-center gap-4 mb-4 text-muted-foreground">
{project.date && ( {project.date && (
<div className="flex items-center gap-2 text-sm"> <div className="flex items-center gap-2 text-sm">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
@@ -91,10 +91,10 @@ const Projects = async () => {
)} )}
</div> </div>
<h3 className="text-xl font-semibold text-white mb-3 group-hover:text-zinc-100 transition-colors duration-300"> <h3 className="text-xl font-semibold text-foreground mb-3 group-hover:text-foreground/90 transition-colors duration-300">
{project.title} {project.title}
</h3> </h3>
<p className="text-zinc-400 text-sm line-clamp-2 mb-4 group-hover:text-zinc-300 transition-colors duration-300"> <p className="text-muted-foreground text-sm line-clamp-2 mb-4 group-hover:text-foreground/70 transition-colors duration-300">
{project.excerpt} {project.excerpt}
</p> </p>
@@ -103,16 +103,16 @@ const Projects = async () => {
{project.technologies.slice(0, 3).map((tag, tagIndex) => ( {project.technologies.slice(0, 3).map((tag, tagIndex) => (
<span <span
key={tagIndex} key={tagIndex}
className="px-3 py-1 bg-zinc-800/50 text-sm text-zinc-400 rounded-full className="px-3 py-1 bg-card/50 text-sm text-muted-foreground rounded-full
ring-1 ring-zinc-700/50 group-hover:ring-zinc-600/50 ring-1 ring-border/50 group-hover:ring-accent/50
group-hover:bg-zinc-800/70 group-hover:text-zinc-300 group-hover:bg-card/70 group-hover:text-foreground/80
transition-all duration-300" transition-all duration-300"
> >
{tag} {tag}
</span> </span>
))} ))}
{project.technologies.length > 3 && ( {project.technologies.length > 3 && (
<span className="text-sm text-zinc-600"> <span className="text-sm text-muted-foreground/60">
+{project.technologies.length - 3} more +{project.technologies.length - 3} more
</span> </span>
)} )}
@@ -120,11 +120,11 @@ const Projects = async () => {
{/* Link Icon */} {/* Link Icon */}
<div className="absolute top-6 right-6 p-2.5 rounded-full <div className="absolute top-6 right-6 p-2.5 rounded-full
bg-zinc-800/60 backdrop-blur-sm ring-1 ring-zinc-700/50 bg-card/60 backdrop-blur-sm ring-1 ring-border/50
opacity-0 group-hover:opacity-100 opacity-0 group-hover:opacity-100
transform translate-y-2 group-hover:translate-y-0 transform translate-y-2 group-hover:translate-y-0
transition-all duration-300"> transition-all duration-300">
<ExternalLink className="h-4 w-4 text-zinc-400" /> <ExternalLink className="h-4 w-4 text-muted-foreground" />
</div> </div>
</div> </div>
</div> </div>
+16 -16
View File
@@ -16,7 +16,7 @@ interface SkeletonProps {
} }
const Skeleton: React.FC<SkeletonProps> = ({ className }) => ( const Skeleton: React.FC<SkeletonProps> = ({ className }) => (
<div className={`animate-pulse bg-zinc-700/50 rounded ${className}`}></div> <div className={`animate-pulse bg-muted/50 rounded ${className}`}></div>
); );
const SkillsSkeleton = () => ( const SkillsSkeleton = () => (
@@ -40,7 +40,7 @@ const SkillsSkeleton = () => (
{/* Skills Icons Grid Skeleton */} {/* Skills Icons Grid Skeleton */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => ( {[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm"> <div key={i} className="p-6 rounded-xl bg-card/30 backdrop-blur-sm">
<Skeleton className="w-8 h-8 mx-auto mb-3" /> <Skeleton className="w-8 h-8 mx-auto mb-3" />
<Skeleton className="h-4 w-16 mx-auto" /> <Skeleton className="h-4 w-16 mx-auto" />
</div> </div>
@@ -98,7 +98,7 @@ const Skills = () => {
} }
return ( return (
<section id="skills" className="py-24 relative"> <section id="skills" className="py-12 sm:py-16 md:py-24 relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
@@ -106,10 +106,10 @@ const Skills = () => {
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }} transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
> >
<h2 className="text-3xl font-bold text-white mb-12"> <h2 className="text-2xl sm:text-3xl md:text-4xl font-bold text-foreground mb-8 sm:mb-12">
{t('title')} {t('title')}
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 md:gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
{/* Skills Progress Bars */} {/* Skills Progress Bars */}
<div className="space-y-6"> <div className="space-y-6">
{skills.map((skill, idx) => ( {skills.map((skill, idx) => (
@@ -122,18 +122,18 @@ const Skills = () => {
transition={{ duration: 0.5, delay: idx * 0.1 }} transition={{ duration: 0.5, delay: idx * 0.1 }}
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-white text-sm font-medium">{skill.name}</span> <span className="text-foreground text-sm font-medium">{skill.name}</span>
<span className="text-white text-sm">{skill.level}%</span> <span className="text-foreground text-sm">{skill.level}%</span>
</div> </div>
<div <div
className="h-2 bg-zinc-800/50 rounded-full overflow-hidden" className="h-2 bg-card/50 rounded-full overflow-hidden"
role="progressbar" role="progressbar"
aria-valuenow={skill.level} aria-valuenow={skill.level}
aria-valuemin={0} aria-valuemin={0}
aria-valuemax={100} aria-valuemax={100}
> >
<motion.div <motion.div
className="h-full rounded-full bg-gradient-to-r from-zinc-600 to-zinc-500" className="h-full rounded-full bg-gradient-to-r from-primary to-accent"
initial={{ width: 0 }} initial={{ width: 0 }}
whileInView={{ width: `${skill.level}%` }} whileInView={{ width: `${skill.level}%` }}
viewport={{ once: true }} viewport={{ once: true }}
@@ -145,7 +145,7 @@ const Skills = () => {
</div> </div>
{/* Skills Icons Grid */} {/* Skills Icons Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-3 gap-4 sm:gap-6"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4 md:gap-6">
{skills.map((skill, idx) => ( {skills.map((skill, idx) => (
<motion.div <motion.div
key={skill.name} key={skill.name}
@@ -153,8 +153,8 @@ const Skills = () => {
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.6, delay: idx * 0.1, ease: [0.25, 0.1, 0.25, 1] }} transition={{ duration: 0.6, delay: idx * 0.1, ease: [0.25, 0.1, 0.25, 1] }}
className={`relative p-6 rounded-xl bg-zinc-800/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors duration-300 hover:bg-zinc-700/50 ${ className={`relative p-6 rounded-xl bg-card/30 backdrop-blur-sm flex flex-col items-center justify-center gap-3 cursor-pointer transition-colors duration-300 hover:bg-accent/50 ${
selectedSkill === skill.name ? 'ring-2 ring-zinc-500 z-20' : '' selectedSkill === skill.name ? 'ring-2 ring-ring z-20' : ''
}`} }`}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
@@ -163,7 +163,7 @@ const Skills = () => {
role="button" role="button"
> >
<motion.div <motion.div
className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'} className={selectedSkill === skill.name ? 'text-accent' : 'text-foreground'}
animate={{ animate={{
rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0, rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0,
scale: selectedSkill === skill.name ? 1.1 : 1 scale: selectedSkill === skill.name ? 1.1 : 1
@@ -172,12 +172,12 @@ const Skills = () => {
> >
{iconMap[skill.name] || <Code2 className="w-8 h-8" />} {iconMap[skill.name] || <Code2 className="w-8 h-8" />}
</motion.div> </motion.div>
<span className="text-white text-sm text-center">{skill.name}</span> <span className="text-foreground text-sm text-center">{skill.name}</span>
<AnimatePresence> <AnimatePresence>
{selectedSkill === skill.name && skill.description && ( {selectedSkill === skill.name && skill.description && (
<motion.div <motion.div
className="absolute left-1/2 top-0 -translate-x-1/2 bg-zinc-800/80 backdrop-blur-sm rounded-lg px-4 py-2 text-zinc-300 text-xs text-center shadow-xl pointer-events-none" className="absolute left-1/2 top-0 -translate-x-1/2 bg-card/80 backdrop-blur-sm rounded-lg px-4 py-2 text-foreground/80 text-xs text-center shadow-xl pointer-events-none"
initial={{ opacity: 0, y: 5 }} initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }} exit={{ opacity: 0, y: 5 }}
@@ -189,7 +189,7 @@ const Skills = () => {
}} }}
> >
{skill.description} {skill.description}
<div className="absolute left-1/2 bottom-0 translate-y-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-zinc-800" /> <div className="absolute left-1/2 bottom-0 translate-y-1/2 -translate-x-1/2 w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-card" />
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
+78 -78
View File
@@ -34,9 +34,9 @@ export const cities: CityData[] = [
sr: 'Kao AI developer i ekspert za automatizaciju iz Kelna, razvijam prilagođena rešenja za kompanije u rajnskoj metropoli. Od Voice AI do automatizacije procesa - vodim vaš posao na sledeći nivo.', sr: 'Kao AI developer i ekspert za automatizaciju iz Kelna, razvijam prilagođena rešenja za kompanije u rajnskoj metropoli. Od Voice AI do automatizacije procesa - vodim vaš posao na sledeći nivo.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Köln', 'AI Agentur Köln', 'Fullstack Entwickler Köln', 'Webentwickler Köln', 'Python Entwickler Köln'], de: ['KI Entwickler Köln', 'AI Agentur Köln', 'Fullstack Entwickler Köln', 'Next.js Entwickler Köln', 'React Entwickler Köln', 'Claude AI Köln', 'TypeScript Entwickler Köln', 'Künstliche Intelligenz Köln', 'Webentwickler Köln', 'Python Entwickler Köln'],
en: ['AI Developer Cologne', 'AI Agency Cologne', 'Fullstack Developer Cologne', 'Web Developer Cologne', 'Python Developer Cologne'], en: ['AI Developer Cologne', 'AI Agency Cologne', 'Fullstack Developer Cologne', 'Next.js Developer Cologne', 'React Developer Cologne', 'Claude AI Developer Cologne', 'TypeScript Developer Cologne', 'Web Developer Cologne', 'Python Developer Cologne'],
sr: ['AI Developer Keln', 'Fullstack Developer Keln', 'Web Developer Keln'], sr: ['AI Developer Keln', 'Fullstack Developer Keln', 'Next.js Programer Keln', 'React Programer Keln', 'Claude AI Keln', 'Web Developer Keln'],
}, },
nearbyAreas: ['Bonn', 'Düsseldorf', 'Leverkusen', 'Bergisch Gladbach'], nearbyAreas: ['Bonn', 'Düsseldorf', 'Leverkusen', 'Bergisch Gladbach'],
}, },
@@ -54,9 +54,9 @@ export const cities: CityData[] = [
sr: 'Za kompanije u Diseldorfu nudim profesionalni AI razvoj i rešenja za automatizaciju. Glavni grad NRW-a poznat je po inovativnosti i ekonomskoj snazi - a ja vam pomažem da to iskoristite kroz pametnu tehnologiju.', sr: 'Za kompanije u Diseldorfu nudim profesionalni AI razvoj i rešenja za automatizaciju. Glavni grad NRW-a poznat je po inovativnosti i ekonomskoj snazi - a ja vam pomažem da to iskoristite kroz pametnu tehnologiju.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Düsseldorf', 'AI Agentur Düsseldorf', 'Fullstack Entwickler Düsseldorf', 'Webentwickler Düsseldorf', 'Automatisierung Düsseldorf'], de: ['KI Entwickler Düsseldorf', 'AI Agentur Düsseldorf', 'Fullstack Entwickler Düsseldorf', 'Next.js Entwickler Düsseldorf', 'React Entwickler Düsseldorf', 'Claude AI Düsseldorf', 'TypeScript Entwickler Düsseldorf', 'Künstliche Intelligenz Düsseldorf', 'Webentwickler Düsseldorf', 'Automatisierung Düsseldorf'],
en: ['AI Developer Düsseldorf', 'AI Agency Düsseldorf', 'Fullstack Developer Düsseldorf', 'Web Developer Düsseldorf', 'Automation Düsseldorf'], en: ['AI Developer Düsseldorf', 'AI Agency Düsseldorf', 'Fullstack Developer Düsseldorf', 'Next.js Developer Düsseldorf', 'React Developer Düsseldorf', 'Claude AI Developer Düsseldorf', 'TypeScript Developer Düsseldorf', 'Automation Düsseldorf'],
sr: ['AI Developer Diseldorf', 'Fullstack Developer Diseldorf'], sr: ['AI Developer Diseldorf', 'Fullstack Developer Diseldorf', 'Next.js Programer Diseldorf', 'React Programer Diseldorf'],
}, },
nearbyAreas: ['Köln', 'Essen', 'Duisburg', 'Wuppertal'], nearbyAreas: ['Köln', 'Essen', 'Duisburg', 'Wuppertal'],
}, },
@@ -74,9 +74,9 @@ export const cities: CityData[] = [
sr: 'Frankfurt kao finansijska metropola Nemačke ima posebne zahteve za tehnologiju i automatizaciju. Razvijam AI rešenja i SaaS aplikacije koje ispunjavaju visoke standarde finansijske industrije.', sr: 'Frankfurt kao finansijska metropola Nemačke ima posebne zahteve za tehnologiju i automatizaciju. Razvijam AI rešenja i SaaS aplikacije koje ispunjavaju visoke standarde finansijske industrije.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Frankfurt', 'SaaS Entwicklung Frankfurt', 'Fullstack Entwickler Frankfurt', 'Fintech Entwickler Frankfurt', 'Automatisierung Frankfurt'], de: ['KI Entwickler Frankfurt', 'Fullstack Entwickler Frankfurt', 'Next.js Entwickler Frankfurt', 'React Entwickler Frankfurt', 'Claude AI Frankfurt', 'TypeScript Entwickler Frankfurt', 'Künstliche Intelligenz Frankfurt', 'SaaS Entwicklung Frankfurt', 'Fintech Entwickler Frankfurt', 'Automatisierung Frankfurt'],
en: ['AI Developer Frankfurt', 'SaaS Development Frankfurt', 'Fullstack Developer Frankfurt', 'Fintech Developer Frankfurt', 'Automation Frankfurt'], en: ['AI Developer Frankfurt', 'Fullstack Developer Frankfurt', 'Next.js Developer Frankfurt', 'React Developer Frankfurt', 'Claude AI Developer Frankfurt', 'TypeScript Developer Frankfurt', 'SaaS Development Frankfurt', 'Fintech Developer Frankfurt', 'Automation Frankfurt'],
sr: ['AI Developer Frankfurt', 'SaaS Razvoj Frankfurt'], sr: ['AI Developer Frankfurt', 'Next.js Programer Frankfurt', 'React Programer Frankfurt', 'Claude AI Frankfurt', 'SaaS Razvoj Frankfurt'],
}, },
nearbyAreas: ['Wiesbaden', 'Mainz', 'Darmstadt', 'Offenbach'], nearbyAreas: ['Wiesbaden', 'Mainz', 'Darmstadt', 'Offenbach'],
}, },
@@ -94,9 +94,9 @@ export const cities: CityData[] = [
sr: 'Minhen je vodeća tehnološka lokacija u Nemačkoj. Kao remote freelancer, podržavam kompanije iz Minhena u razvoju AI agenata, Voice AI i rešenja za automatizaciju.', sr: 'Minhen je vodeća tehnološka lokacija u Nemačkoj. Kao remote freelancer, podržavam kompanije iz Minhena u razvoju AI agenata, Voice AI i rešenja za automatizaciju.',
}, },
keywords: { keywords: {
de: ['KI Entwickler München', 'AI Agentur München', 'Fullstack Entwickler München', 'SaaS Entwicklung München', 'Automatisierung München'], de: ['KI Entwickler München', 'AI Agentur München', 'Fullstack Entwickler München', 'Next.js Entwickler München', 'React Entwickler München', 'Claude AI München', 'TypeScript Entwickler München', 'Künstliche Intelligenz München', 'SaaS Entwicklung München', 'Automatisierung München'],
en: ['AI Developer Munich', 'AI Agency Munich', 'Fullstack Developer Munich', 'SaaS Development Munich', 'Automation Munich'], en: ['AI Developer Munich', 'AI Agency Munich', 'Fullstack Developer Munich', 'Next.js Developer Munich', 'React Developer Munich', 'Claude AI Developer Munich', 'TypeScript Developer Munich', 'SaaS Development Munich', 'Automation Munich'],
sr: ['AI Developer Minhen', 'Fullstack Developer Minhen'], sr: ['AI Developer Minhen', 'Fullstack Developer Minhen', 'Next.js Programer Minhen', 'React Programer Minhen', 'Claude AI Minhen'],
}, },
nearbyAreas: ['Augsburg', 'Ingolstadt', 'Regensburg', 'Nürnberg'], nearbyAreas: ['Augsburg', 'Ingolstadt', 'Regensburg', 'Nürnberg'],
}, },
@@ -114,9 +114,9 @@ export const cities: CityData[] = [
sr: 'Startap prestonica Nemačke treba inovativne tehnološke partnere. Podržavam berlinske startape i kompanije u razvoju AI rešenja, MVP-ova i skalabilnih SaaS proizvoda.', sr: 'Startap prestonica Nemačke treba inovativne tehnološke partnere. Podržavam berlinske startape i kompanije u razvoju AI rešenja, MVP-ova i skalabilnih SaaS proizvoda.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Berlin', 'Startup Entwickler Berlin', 'SaaS Entwicklung Berlin', 'MVP Entwicklung Berlin', 'Automatisierung Berlin'], de: ['KI Entwickler Berlin', 'Fullstack Entwickler Berlin', 'Next.js Entwickler Berlin', 'React Entwickler Berlin', 'Claude AI Berlin', 'TypeScript Entwickler Berlin', 'Künstliche Intelligenz Berlin', 'Startup Entwickler Berlin', 'SaaS Entwicklung Berlin', 'MVP Entwicklung Berlin', 'Automatisierung Berlin'],
en: ['AI Developer Berlin', 'Startup Developer Berlin', 'SaaS Development Berlin', 'MVP Development Berlin', 'Automation Berlin'], en: ['AI Developer Berlin', 'Fullstack Developer Berlin', 'Next.js Developer Berlin', 'React Developer Berlin', 'Claude AI Developer Berlin', 'TypeScript Developer Berlin', 'Startup Developer Berlin', 'SaaS Development Berlin', 'MVP Development Berlin', 'Automation Berlin'],
sr: ['AI Developer Berlin', 'Startup Developer Berlin'], sr: ['AI Developer Berlin', 'Next.js Programer Berlin', 'React Programer Berlin', 'Claude AI Berlin', 'Startup Developer Berlin'],
}, },
nearbyAreas: ['Potsdam', 'Leipzig', 'Dresden', 'Hamburg'], nearbyAreas: ['Potsdam', 'Leipzig', 'Dresden', 'Hamburg'],
}, },
@@ -134,9 +134,9 @@ export const cities: CityData[] = [
sr: 'Hamburg kao trgovinsko i medijsko središte ima raznovrsne zahteve za digitalna rešenja. Razvijam AI agente i automatizacije za e-commerce, logistiku i medijske kompanije.', sr: 'Hamburg kao trgovinsko i medijsko središte ima raznovrsne zahteve za digitalna rešenja. Razvijam AI agente i automatizacije za e-commerce, logistiku i medijske kompanije.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Hamburg', 'E-Commerce Entwickler Hamburg', 'Fullstack Entwickler Hamburg', 'Automatisierung Hamburg', 'SaaS Hamburg'], de: ['KI Entwickler Hamburg', 'E-Commerce Entwickler Hamburg', 'Next.js Entwickler Hamburg', 'React Entwickler Hamburg', 'Claude AI Hamburg', 'TypeScript Entwickler Hamburg', 'Künstliche Intelligenz Hamburg', 'Fullstack Entwickler Hamburg', 'Automatisierung Hamburg', 'SaaS Hamburg'],
en: ['AI Developer Hamburg', 'E-Commerce Developer Hamburg', 'Fullstack Developer Hamburg', 'Automation Hamburg', 'SaaS Hamburg'], en: ['AI Developer Hamburg', 'E-Commerce Developer Hamburg', 'Next.js Developer Hamburg', 'React Developer Hamburg', 'Claude AI Developer Hamburg', 'TypeScript Developer Hamburg', 'Fullstack Developer Hamburg', 'Automation Hamburg', 'SaaS Hamburg'],
sr: ['AI Developer Hamburg', 'E-Commerce Developer Hamburg'], sr: ['AI Developer Hamburg', 'E-Commerce Developer Hamburg', 'Next.js Programer Hamburg', 'React Programer Hamburg', 'Claude AI Hamburg'],
}, },
nearbyAreas: ['Bremen', 'Lübeck', 'Kiel', 'Hannover'], nearbyAreas: ['Bremen', 'Lübeck', 'Kiel', 'Hannover'],
}, },
@@ -154,9 +154,9 @@ export const cities: CityData[] = [
sr: 'Bon kao bivši federalni glavni grad i sedište mnogih međunarodnih organizacija treba profesionalna IT rešenja. Nudim AI razvoj i automatizaciju za kompanije iz Bona.', sr: 'Bon kao bivši federalni glavni grad i sedište mnogih međunarodnih organizacija treba profesionalna IT rešenja. Nudim AI razvoj i automatizaciju za kompanije iz Bona.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Bonn', 'Fullstack Entwickler Bonn', 'Webentwickler Bonn', 'Automatisierung Bonn', 'IT Freelancer Bonn'], de: ['KI Entwickler Bonn', 'Fullstack Entwickler Bonn', 'Next.js Entwickler Bonn', 'React Entwickler Bonn', 'Claude AI Bonn', 'TypeScript Entwickler Bonn', 'Künstliche Intelligenz Bonn', 'Webentwickler Bonn', 'Automatisierung Bonn', 'IT Freelancer Bonn'],
en: ['AI Developer Bonn', 'Fullstack Developer Bonn', 'Web Developer Bonn', 'Automation Bonn', 'IT Freelancer Bonn'], en: ['AI Developer Bonn', 'Fullstack Developer Bonn', 'Next.js Developer Bonn', 'React Developer Bonn', 'Claude AI Developer Bonn', 'TypeScript Developer Bonn', 'Web Developer Bonn', 'Automation Bonn', 'IT Freelancer Bonn'],
sr: ['AI Developer Bon', 'Fullstack Developer Bon'], sr: ['AI Developer Bon', 'Fullstack Developer Bon', 'Next.js Programer Bon', 'React Programer Bon', 'Claude AI Bon'],
}, },
nearbyAreas: ['Köln', 'Koblenz', 'Siegburg', 'Bad Godesberg'], nearbyAreas: ['Köln', 'Koblenz', 'Siegburg', 'Bad Godesberg'],
}, },
@@ -174,9 +174,9 @@ export const cities: CityData[] = [
sr: 'Štutgart je srce nemačke automobilske industrije i Industrije 4.0. Razvijam AI rešenja i automatizacije za mašinstvo, automotive i proizvodne kompanije u regionu.', sr: 'Štutgart je srce nemačke automobilske industrije i Industrije 4.0. Razvijam AI rešenja i automatizacije za mašinstvo, automotive i proizvodne kompanije u regionu.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Stuttgart', 'Industrie 4.0 Stuttgart', 'Automatisierung Stuttgart', 'KI Automotive Stuttgart', 'Machine Learning Stuttgart', 'IoT Entwickler Stuttgart'], de: ['KI Entwickler Stuttgart', 'Industrie 4.0 Stuttgart', 'Next.js Entwickler Stuttgart', 'React Entwickler Stuttgart', 'Claude AI Stuttgart', 'TypeScript Entwickler Stuttgart', 'Künstliche Intelligenz Stuttgart', 'Automatisierung Stuttgart', 'KI Automotive Stuttgart', 'Machine Learning Stuttgart', 'IoT Entwickler Stuttgart'],
en: ['AI Developer Stuttgart', 'Industry 4.0 Stuttgart', 'Automation Stuttgart', 'AI Automotive Stuttgart', 'Machine Learning Stuttgart', 'IoT Developer Stuttgart'], en: ['AI Developer Stuttgart', 'Industry 4.0 Stuttgart', 'Next.js Developer Stuttgart', 'React Developer Stuttgart', 'Claude AI Developer Stuttgart', 'TypeScript Developer Stuttgart', 'Automation Stuttgart', 'AI Automotive Stuttgart', 'Machine Learning Stuttgart', 'IoT Developer Stuttgart'],
sr: ['AI Developer Štutgart', 'Industrie 4.0 Štutgart', 'Automatizacija Štutgart'], sr: ['AI Developer Štutgart', 'Industrie 4.0 Štutgart', 'Next.js Programer Štutgart', 'React Programer Štutgart', 'Claude AI Štutgart', 'Automatizacija Štutgart'],
}, },
nearbyAreas: ['Esslingen', 'Ludwigsburg', 'Böblingen', 'Sindelfingen', 'Karlsruhe'], nearbyAreas: ['Esslingen', 'Ludwigsburg', 'Böblingen', 'Sindelfingen', 'Karlsruhe'],
}, },
@@ -194,9 +194,9 @@ export const cities: CityData[] = [
sr: 'Esen u srcu Rurske oblasti transformiše se od industrijskog u digitalni centar. Podržavam kompanije iz Esena u digitalnoj transformaciji sa AI agentima i automatizacijom procesa.', sr: 'Esen u srcu Rurske oblasti transformiše se od industrijskog u digitalni centar. Podržavam kompanije iz Esena u digitalnoj transformaciji sa AI agentima i automatizacijom procesa.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Essen', 'Automatisierung Essen', 'Digitalisierung Essen', 'Fullstack Entwickler Essen', 'n8n Entwickler Essen', 'Prozessautomatisierung Essen'], de: ['KI Entwickler Essen', 'Automatisierung Essen', 'Next.js Entwickler Essen', 'React Entwickler Essen', 'Claude AI Essen', 'TypeScript Entwickler Essen', 'Künstliche Intelligenz Essen', 'Digitalisierung Essen', 'Fullstack Entwickler Essen', 'n8n Entwickler Essen', 'Prozessautomatisierung Essen'],
en: ['AI Developer Essen', 'Automation Essen', 'Digitalization Essen', 'Fullstack Developer Essen', 'n8n Developer Essen', 'Process Automation Essen'], en: ['AI Developer Essen', 'Automation Essen', 'Next.js Developer Essen', 'React Developer Essen', 'Claude AI Developer Essen', 'TypeScript Developer Essen', 'Digitalization Essen', 'Fullstack Developer Essen', 'n8n Developer Essen', 'Process Automation Essen'],
sr: ['AI Developer Esen', 'Automatizacija Esen', 'Digitalizacija Esen'], sr: ['AI Developer Esen', 'Automatizacija Esen', 'Next.js Programer Esen', 'React Programer Esen', 'Claude AI Esen', 'Digitalizacija Esen'],
}, },
nearbyAreas: ['Dortmund', 'Duisburg', 'Bochum', 'Gelsenkirchen', 'Düsseldorf'], nearbyAreas: ['Dortmund', 'Duisburg', 'Bochum', 'Gelsenkirchen', 'Düsseldorf'],
}, },
@@ -214,9 +214,9 @@ export const cities: CityData[] = [
sr: 'Dortmund se razvija u vodeći tech hub u Rurskoj oblasti. Sa TU Dortmund i brojnim startapima, nudim AI razvoj, SaaS rešenja i automatizaciju za inovativne kompanije.', sr: 'Dortmund se razvija u vodeći tech hub u Rurskoj oblasti. Sa TU Dortmund i brojnim startapima, nudim AI razvoj, SaaS rešenja i automatizaciju za inovativne kompanije.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Dortmund', 'Startup Entwickler Dortmund', 'SaaS Entwicklung Dortmund', 'Fullstack Entwickler Dortmund', 'Automatisierung Dortmund', 'Tech Freelancer Dortmund'], de: ['KI Entwickler Dortmund', 'Startup Entwickler Dortmund', 'Next.js Entwickler Dortmund', 'React Entwickler Dortmund', 'Claude AI Dortmund', 'TypeScript Entwickler Dortmund', 'Künstliche Intelligenz Dortmund', 'SaaS Entwicklung Dortmund', 'Fullstack Entwickler Dortmund', 'Automatisierung Dortmund', 'Tech Freelancer Dortmund'],
en: ['AI Developer Dortmund', 'Startup Developer Dortmund', 'SaaS Development Dortmund', 'Fullstack Developer Dortmund', 'Automation Dortmund', 'Tech Freelancer Dortmund'], en: ['AI Developer Dortmund', 'Startup Developer Dortmund', 'Next.js Developer Dortmund', 'React Developer Dortmund', 'Claude AI Developer Dortmund', 'TypeScript Developer Dortmund', 'SaaS Development Dortmund', 'Fullstack Developer Dortmund', 'Automation Dortmund', 'Tech Freelancer Dortmund'],
sr: ['AI Developer Dortmund', 'Startup Developer Dortmund', 'SaaS Razvoj Dortmund'], sr: ['AI Developer Dortmund', 'Startup Developer Dortmund', 'Next.js Programer Dortmund', 'React Programer Dortmund', 'Claude AI Dortmund', 'SaaS Razvoj Dortmund'],
}, },
nearbyAreas: ['Essen', 'Bochum', 'Hagen', 'Münster', 'Köln'], nearbyAreas: ['Essen', 'Bochum', 'Hagen', 'Münster', 'Köln'],
}, },
@@ -234,9 +234,9 @@ export const cities: CityData[] = [
sr: 'Lajpcig je rastući tech centar istočne Nemačke. Podržavam startape i etablirane kompanije iz Lajpciga sa AI razvojem, Voice AI i modernim rešenjima za automatizaciju.', sr: 'Lajpcig je rastući tech centar istočne Nemačke. Podržavam startape i etablirane kompanije iz Lajpciga sa AI razvojem, Voice AI i modernim rešenjima za automatizaciju.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Leipzig', 'Startup Entwickler Leipzig', 'Voice AI Leipzig', 'Fullstack Entwickler Leipzig', 'Automatisierung Leipzig', 'n8n Entwickler Leipzig'], de: ['KI Entwickler Leipzig', 'Startup Entwickler Leipzig', 'Next.js Entwickler Leipzig', 'React Entwickler Leipzig', 'Claude AI Leipzig', 'TypeScript Entwickler Leipzig', 'Künstliche Intelligenz Leipzig', 'Voice AI Leipzig', 'Fullstack Entwickler Leipzig', 'Automatisierung Leipzig', 'n8n Entwickler Leipzig'],
en: ['AI Developer Leipzig', 'Startup Developer Leipzig', 'Voice AI Leipzig', 'Fullstack Developer Leipzig', 'Automation Leipzig', 'n8n Developer Leipzig'], en: ['AI Developer Leipzig', 'Startup Developer Leipzig', 'Next.js Developer Leipzig', 'React Developer Leipzig', 'Claude AI Developer Leipzig', 'TypeScript Developer Leipzig', 'Voice AI Leipzig', 'Fullstack Developer Leipzig', 'Automation Leipzig', 'n8n Developer Leipzig'],
sr: ['AI Developer Lajpcig', 'Startup Developer Lajpcig', 'Voice AI Lajpcig'], sr: ['AI Developer Lajpcig', 'Startup Developer Lajpcig', 'Next.js Programer Lajpcig', 'React Programer Lajpcig', 'Claude AI Lajpcig', 'Voice AI Lajpcig'],
}, },
nearbyAreas: ['Dresden', 'Halle', 'Berlin', 'Chemnitz', 'Magdeburg'], nearbyAreas: ['Dresden', 'Halle', 'Berlin', 'Chemnitz', 'Magdeburg'],
}, },
@@ -255,9 +255,9 @@ export const cities: CityData[] = [
sr: 'Beč kao glavni grad Austrije je značajan ekonomski i tehnološki centar u DACH regionu. Nudim razvoj AI i rešenja za automatizaciju za bečke kompanije.', sr: 'Beč kao glavni grad Austrije je značajan ekonomski i tehnološki centar u DACH regionu. Nudim razvoj AI i rešenja za automatizaciju za bečke kompanije.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Wien', 'AI Agentur Wien', 'Fullstack Entwickler Wien', 'Automatisierung Wien', 'n8n Entwickler Österreich', 'Voice AI Wien'], de: ['KI Entwickler Wien', 'AI Agentur Wien', 'Next.js Entwickler Wien', 'React Entwickler Wien', 'Claude AI Wien', 'TypeScript Entwickler Wien', 'Künstliche Intelligenz Wien', 'Fullstack Entwickler Wien', 'Automatisierung Wien', 'n8n Entwickler Österreich', 'Voice AI Wien'],
en: ['AI Developer Vienna', 'AI Agency Vienna', 'Fullstack Developer Vienna', 'Automation Vienna', 'n8n Developer Austria'], en: ['AI Developer Vienna', 'AI Agency Vienna', 'Next.js Developer Vienna', 'React Developer Vienna', 'Claude AI Developer Vienna', 'TypeScript Developer Vienna', 'Fullstack Developer Vienna', 'Automation Vienna', 'n8n Developer Austria'],
sr: ['AI Developer Beč', 'Fullstack Developer Austrija'], sr: ['AI Developer Beč', 'Fullstack Developer Austrija', 'Next.js Programer Beč', 'React Programer Beč', 'Claude AI Beč'],
}, },
nearbyAreas: ['Graz', 'Linz', 'Salzburg', 'Bratislava'], nearbyAreas: ['Graz', 'Linz', 'Salzburg', 'Bratislava'],
}, },
@@ -275,9 +275,9 @@ export const cities: CityData[] = [
sr: 'Grac je drugi najveći grad Austrije i važna lokacija za tehnologiju i inovacije. Podržavam kompanije iz Graca sa AI rešenjima i modernom automatizacijom procesa.', sr: 'Grac je drugi najveći grad Austrije i važna lokacija za tehnologiju i inovacije. Podržavam kompanije iz Graca sa AI rešenjima i modernom automatizacijom procesa.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Graz', 'Fullstack Entwickler Graz', 'Automatisierung Graz', 'SaaS Entwicklung Graz', 'Tech Freelancer Graz'], de: ['KI Entwickler Graz', 'Fullstack Entwickler Graz', 'Next.js Entwickler Graz', 'React Entwickler Graz', 'Claude AI Graz', 'TypeScript Entwickler Graz', 'Künstliche Intelligenz Graz', 'Automatisierung Graz', 'SaaS Entwicklung Graz', 'Tech Freelancer Graz'],
en: ['AI Developer Graz', 'Fullstack Developer Graz', 'Automation Graz', 'SaaS Development Graz'], en: ['AI Developer Graz', 'Fullstack Developer Graz', 'Next.js Developer Graz', 'React Developer Graz', 'Claude AI Developer Graz', 'TypeScript Developer Graz', 'Automation Graz', 'SaaS Development Graz'],
sr: ['AI Developer Grac', 'Fullstack Developer Grac'], sr: ['AI Developer Grac', 'Fullstack Developer Grac', 'Next.js Programer Grac', 'React Programer Grac', 'Claude AI Grac'],
}, },
nearbyAreas: ['Wien', 'Klagenfurt', 'Maribor', 'Ljubljana'], nearbyAreas: ['Wien', 'Klagenfurt', 'Maribor', 'Ljubljana'],
}, },
@@ -296,9 +296,9 @@ export const cities: CityData[] = [
sr: 'Cirih je finansijski i ekonomski centar Švajcarske. Nudim razvoj AI i rešenja za automatizaciju za švajcarske kompanije sa najvišim standardima kvaliteta.', sr: 'Cirih je finansijski i ekonomski centar Švajcarske. Nudim razvoj AI i rešenja za automatizaciju za švajcarske kompanije sa najvišim standardima kvaliteta.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Zürich', 'AI Agentur Zürich', 'Fullstack Entwickler Schweiz', 'Automatisierung Zürich', 'Fintech Entwickler Zürich', 'SaaS Entwicklung Schweiz'], de: ['KI Entwickler Zürich', 'AI Agentur Zürich', 'Next.js Entwickler Zürich', 'React Entwickler Zürich', 'Claude AI Zürich', 'TypeScript Entwickler Zürich', 'Künstliche Intelligenz Zürich', 'Fullstack Entwickler Schweiz', 'Automatisierung Zürich', 'Fintech Entwickler Zürich', 'SaaS Entwicklung Schweiz'],
en: ['AI Developer Zurich', 'AI Agency Zurich', 'Fullstack Developer Switzerland', 'Automation Zurich', 'Fintech Developer Zurich'], en: ['AI Developer Zurich', 'AI Agency Zurich', 'Next.js Developer Zurich', 'React Developer Zurich', 'Claude AI Developer Zurich', 'TypeScript Developer Zurich', 'Fullstack Developer Switzerland', 'Automation Zurich', 'Fintech Developer Zurich'],
sr: ['AI Developer Cirih', 'Fullstack Developer Švajcarska'], sr: ['AI Developer Cirih', 'Fullstack Developer Švajcarska', 'Next.js Programer Cirih', 'React Programer Cirih', 'Claude AI Cirih'],
}, },
nearbyAreas: ['Basel', 'Bern', 'Luzern', 'Winterthur'], nearbyAreas: ['Basel', 'Bern', 'Luzern', 'Winterthur'],
}, },
@@ -316,9 +316,9 @@ export const cities: CityData[] = [
sr: 'Bazel je važna farmaceutska i life sciences lokacija u Švajcarskoj. Podržavam kompanije iz Bazela sa AI rešenjima za Industriju 4.0 i automatizaciju procesa.', sr: 'Bazel je važna farmaceutska i life sciences lokacija u Švajcarskoj. Podržavam kompanije iz Bazela sa AI rešenjima za Industriju 4.0 i automatizaciju procesa.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Basel', 'Pharma IT Basel', 'Fullstack Entwickler Basel', 'Automatisierung Basel', 'Life Sciences IT Basel'], de: ['KI Entwickler Basel', 'Pharma IT Basel', 'Next.js Entwickler Basel', 'React Entwickler Basel', 'Claude AI Basel', 'TypeScript Entwickler Basel', 'Künstliche Intelligenz Basel', 'Fullstack Entwickler Basel', 'Automatisierung Basel', 'Life Sciences IT Basel'],
en: ['AI Developer Basel', 'Pharma IT Basel', 'Fullstack Developer Basel', 'Automation Basel', 'Life Sciences IT Basel'], en: ['AI Developer Basel', 'Pharma IT Basel', 'Next.js Developer Basel', 'React Developer Basel', 'Claude AI Developer Basel', 'TypeScript Developer Basel', 'Fullstack Developer Basel', 'Automation Basel', 'Life Sciences IT Basel'],
sr: ['AI Developer Bazel', 'Fullstack Developer Bazel'], sr: ['AI Developer Bazel', 'Fullstack Developer Bazel', 'Next.js Programer Bazel', 'React Programer Bazel', 'Claude AI Bazel'],
}, },
nearbyAreas: ['Zürich', 'Bern', 'Freiburg', 'Strasbourg'], nearbyAreas: ['Zürich', 'Bern', 'Freiburg', 'Strasbourg'],
}, },
@@ -337,9 +337,9 @@ export const cities: CityData[] = [
sr: 'London je jedan od vodećih svetskih finansijskih centara i tech habova. Nudim remote AI razvoj i rešenja za automatizaciju za londonske kompanije sa nemačkim inženjerskim kvalitetom.', sr: 'London je jedan od vodećih svetskih finansijskih centara i tech habova. Nudim remote AI razvoj i rešenja za automatizaciju za londonske kompanije sa nemačkim inženjerskim kvalitetom.',
}, },
keywords: { keywords: {
de: ['KI Entwickler London', 'Remote Developer UK', 'AI Freelancer London', 'Automatisierung London'], de: ['KI Entwickler London', 'Remote Developer UK', 'Next.js Entwickler London', 'React Entwickler London', 'Claude AI London', 'TypeScript Entwickler London', 'Künstliche Intelligenz London', 'AI Freelancer London', 'Automatisierung London'],
en: ['AI Developer London', 'Remote Developer UK', 'AI Freelancer London', 'Process Automation London', 'n8n Developer London', 'Voice AI London', 'Python Developer London', 'React Developer UK'], en: ['AI Developer London', 'Remote Developer UK', 'Next.js Developer London', 'React Developer London', 'Claude AI Developer London', 'TypeScript Developer London', 'AI Freelancer London', 'Process Automation London', 'n8n Developer London', 'Voice AI London', 'Python Developer London', 'React Developer UK'],
sr: ['AI Developer London', 'Remote Programer UK'], sr: ['AI Developer London', 'Remote Programer UK', 'Next.js Programer London', 'React Programer London', 'Claude AI London'],
}, },
nearbyAreas: ['Manchester', 'Birmingham', 'Cambridge', 'Oxford'], nearbyAreas: ['Manchester', 'Birmingham', 'Cambridge', 'Oxford'],
}, },
@@ -358,9 +358,9 @@ export const cities: CityData[] = [
sr: 'Njujork je ekonomsko srce SAD-a. Podržavam njujorške startape i preduzeća na daljinu sa AI agentima, Voice AI i rešenjima za automatizaciju procesa.', sr: 'Njujork je ekonomsko srce SAD-a. Podržavam njujorške startape i preduzeća na daljinu sa AI agentima, Voice AI i rešenjima za automatizaciju procesa.',
}, },
keywords: { keywords: {
de: ['KI Entwickler New York', 'Remote Developer USA', 'AI Freelancer NYC', 'Automatisierung New York'], de: ['KI Entwickler New York', 'Remote Developer USA', 'Next.js Entwickler New York', 'React Entwickler New York', 'Claude AI New York', 'TypeScript Entwickler New York', 'Künstliche Intelligenz New York', 'AI Freelancer NYC', 'Automatisierung New York'],
en: ['AI Developer New York', 'Remote Developer NYC', 'AI Freelancer New York', 'Process Automation NYC', 'n8n Developer New York', 'Voice AI NYC', 'Python Developer New York', 'Fullstack Developer NYC'], en: ['AI Developer New York', 'Remote Developer NYC', 'Next.js Developer New York', 'React Developer New York', 'Claude AI Developer New York', 'TypeScript Developer New York', 'AI Freelancer New York', 'Process Automation NYC', 'n8n Developer New York', 'Voice AI NYC', 'Python Developer New York', 'Fullstack Developer NYC'],
sr: ['AI Developer Njujork', 'Remote Programer SAD'], sr: ['AI Developer Njujork', 'Remote Programer SAD', 'Next.js Programer Njujork', 'React Programer Njujork', 'Claude AI Njujork'],
}, },
nearbyAreas: ['Brooklyn', 'Manhattan', 'New Jersey', 'Connecticut'], nearbyAreas: ['Brooklyn', 'Manhattan', 'New Jersey', 'Connecticut'],
}, },
@@ -378,9 +378,9 @@ export const cities: CityData[] = [
sr: 'Los Anđeles je vodeći centar za tehnologiju, zabavu i inovacije. Nudim remote AI razvoj i rešenja za automatizaciju za kompanije iz LA.', sr: 'Los Anđeles je vodeći centar za tehnologiju, zabavu i inovacije. Nudim remote AI razvoj i rešenja za automatizaciju za kompanije iz LA.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Los Angeles', 'Remote Developer Kalifornien', 'AI Freelancer LA', 'Tech Entwickler LA'], de: ['KI Entwickler Los Angeles', 'Remote Developer Kalifornien', 'Next.js Entwickler Los Angeles', 'React Entwickler Los Angeles', 'Claude AI Los Angeles', 'TypeScript Entwickler Los Angeles', 'Künstliche Intelligenz Los Angeles', 'AI Freelancer LA', 'Tech Entwickler LA'],
en: ['AI Developer Los Angeles', 'Remote Developer California', 'AI Freelancer LA', 'Process Automation Los Angeles', 'Tech Developer LA', 'Python Developer Los Angeles', 'SaaS Developer California'], en: ['AI Developer Los Angeles', 'Remote Developer California', 'Next.js Developer Los Angeles', 'React Developer Los Angeles', 'Claude AI Developer Los Angeles', 'TypeScript Developer Los Angeles', 'AI Freelancer LA', 'Process Automation Los Angeles', 'Tech Developer LA', 'Python Developer Los Angeles', 'SaaS Developer California'],
sr: ['AI Developer Los Anđeles', 'Remote Programer Kalifornija'], sr: ['AI Developer Los Anđeles', 'Remote Programer Kalifornija', 'Next.js Programer Los Anđeles', 'React Programer Los Anđeles', 'Claude AI Los Anđeles'],
}, },
nearbyAreas: ['San Francisco', 'San Diego', 'Silicon Valley', 'Orange County'], nearbyAreas: ['San Francisco', 'San Diego', 'Silicon Valley', 'Orange County'],
}, },
@@ -398,9 +398,9 @@ export const cities: CityData[] = [
sr: 'Majami se razvija kao rastući tech hab u SAD sa fokusom na fintech i kripto. Podržavam kompanije iz Majamija sa AI rešenjima i modernom automatizacijom procesa.', sr: 'Majami se razvija kao rastući tech hab u SAD sa fokusom na fintech i kripto. Podržavam kompanije iz Majamija sa AI rešenjima i modernom automatizacijom procesa.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Miami', 'Remote Developer Florida', 'AI Freelancer Miami', 'Fintech Entwickler Miami'], de: ['KI Entwickler Miami', 'Remote Developer Florida', 'Next.js Entwickler Miami', 'React Entwickler Miami', 'Claude AI Miami', 'TypeScript Entwickler Miami', 'Künstliche Intelligenz Miami', 'AI Freelancer Miami', 'Fintech Entwickler Miami'],
en: ['AI Developer Miami', 'Remote Developer Florida', 'AI Freelancer Miami', 'Process Automation Miami', 'Fintech Developer Miami', 'Python Developer Miami', 'Startup Developer Miami'], en: ['AI Developer Miami', 'Remote Developer Florida', 'Next.js Developer Miami', 'React Developer Miami', 'Claude AI Developer Miami', 'TypeScript Developer Miami', 'AI Freelancer Miami', 'Process Automation Miami', 'Fintech Developer Miami', 'Python Developer Miami', 'Startup Developer Miami'],
sr: ['AI Developer Majami', 'Remote Programer Florida'], sr: ['AI Developer Majami', 'Remote Programer Florida', 'Next.js Programer Majami', 'React Programer Majami', 'Claude AI Majami'],
}, },
nearbyAreas: ['Fort Lauderdale', 'Tampa', 'Orlando', 'West Palm Beach'], nearbyAreas: ['Fort Lauderdale', 'Tampa', 'Orlando', 'West Palm Beach'],
}, },
@@ -418,9 +418,9 @@ export const cities: CityData[] = [
sr: 'San Francisko i Silikonska dolina su globalni centar za tehnologiju i inovacije. Nudim remote razvojne usluge za startape i tech kompanije iz SF.', sr: 'San Francisko i Silikonska dolina su globalni centar za tehnologiju i inovacije. Nudim remote razvojne usluge za startape i tech kompanije iz SF.',
}, },
keywords: { keywords: {
de: ['KI Entwickler San Francisco', 'Remote Developer Silicon Valley', 'AI Freelancer SF', 'Startup Entwickler Bay Area'], de: ['KI Entwickler San Francisco', 'Remote Developer Silicon Valley', 'Next.js Entwickler San Francisco', 'React Entwickler San Francisco', 'Claude AI San Francisco', 'TypeScript Entwickler San Francisco', 'Künstliche Intelligenz San Francisco', 'AI Freelancer SF', 'Startup Entwickler Bay Area'],
en: ['AI Developer San Francisco', 'Remote Developer Silicon Valley', 'AI Freelancer SF', 'Process Automation San Francisco', 'Startup Developer Bay Area', 'Python Developer SF', 'SaaS Developer Silicon Valley'], en: ['AI Developer San Francisco', 'Remote Developer Silicon Valley', 'Next.js Developer San Francisco', 'React Developer San Francisco', 'Claude AI Developer San Francisco', 'TypeScript Developer San Francisco', 'AI Freelancer SF', 'Process Automation San Francisco', 'Startup Developer Bay Area', 'Python Developer SF', 'SaaS Developer Silicon Valley'],
sr: ['AI Developer San Francisko', 'Remote Programer Silikonska Dolina'], sr: ['AI Developer San Francisko', 'Remote Programer Silikonska Dolina', 'Next.js Programer San Francisko', 'React Programer San Francisko', 'Claude AI San Francisko'],
}, },
nearbyAreas: ['Silicon Valley', 'Palo Alto', 'Oakland', 'San Jose'], nearbyAreas: ['Silicon Valley', 'Palo Alto', 'Oakland', 'San Jose'],
}, },
@@ -438,9 +438,9 @@ export const cities: CityData[] = [
sr: 'Čikago je značajan ekonomski centar na Srednjem zapadu SAD-a. Podržavam kompanije iz Čikaga na daljinu sa AI razvojem i rešenjima za automatizaciju.', sr: 'Čikago je značajan ekonomski centar na Srednjem zapadu SAD-a. Podržavam kompanije iz Čikaga na daljinu sa AI razvojem i rešenjima za automatizaciju.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Chicago', 'Remote Developer Midwest', 'AI Freelancer Chicago', 'Automatisierung Chicago'], de: ['KI Entwickler Chicago', 'Remote Developer Midwest', 'Next.js Entwickler Chicago', 'React Entwickler Chicago', 'Claude AI Chicago', 'TypeScript Entwickler Chicago', 'Künstliche Intelligenz Chicago', 'AI Freelancer Chicago', 'Automatisierung Chicago'],
en: ['AI Developer Chicago', 'Remote Developer Midwest', 'AI Freelancer Chicago', 'Process Automation Chicago', 'Python Developer Chicago', 'Fullstack Developer Chicago'], en: ['AI Developer Chicago', 'Remote Developer Midwest', 'Next.js Developer Chicago', 'React Developer Chicago', 'Claude AI Developer Chicago', 'TypeScript Developer Chicago', 'AI Freelancer Chicago', 'Process Automation Chicago', 'Python Developer Chicago', 'Fullstack Developer Chicago'],
sr: ['AI Developer Čikago', 'Remote Programer Midwest'], sr: ['AI Developer Čikago', 'Remote Programer Midwest', 'Next.js Programer Čikago', 'React Programer Čikago', 'Claude AI Čikago'],
}, },
nearbyAreas: ['Milwaukee', 'Indianapolis', 'Detroit', 'Minneapolis'], nearbyAreas: ['Milwaukee', 'Indianapolis', 'Detroit', 'Minneapolis'],
}, },
@@ -459,9 +459,9 @@ export const cities: CityData[] = [
sr: 'Beograd je ekonomski i tehnološki centar Srbije. Nudim razvoj AI i rešenja za automatizaciju za srpske kompanije sa nemačkom preciznošću i kvalitetom.', sr: 'Beograd je ekonomski i tehnološki centar Srbije. Nudim razvoj AI i rešenja za automatizaciju za srpske kompanije sa nemačkom preciznošću i kvalitetom.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Belgrad', 'AI Entwicklung Serbien', 'Fullstack Entwickler Belgrad', 'Automatisierung Serbien'], de: ['KI Entwickler Belgrad', 'AI Entwicklung Serbien', 'Next.js Entwickler Belgrad', 'React Entwickler Belgrad', 'Claude AI Belgrad', 'TypeScript Entwickler Belgrad', 'Künstliche Intelligenz Belgrad', 'Fullstack Entwickler Belgrad', 'Automatisierung Serbien'],
en: ['AI Developer Belgrade', 'AI Development Serbia', 'Fullstack Developer Belgrade', 'Automation Serbia'], en: ['AI Developer Belgrade', 'AI Development Serbia', 'Next.js Developer Belgrade', 'React Developer Belgrade', 'Claude AI Developer Belgrade', 'TypeScript Developer Belgrade', 'Fullstack Developer Belgrade', 'Automation Serbia'],
sr: ['AI Developer Beograd', 'Softverski Razvoj Beograd', 'Fullstack Programer Beograd', 'Automatizacija Procesa Srbija', 'n8n Automatizacija Beograd', 'Voice AI Beograd'], sr: ['AI Developer Beograd', 'Softverski Razvoj Beograd', 'Next.js Programer Beograd', 'React Programer Beograd', 'Claude AI Beograd', 'Fullstack Programer Beograd', 'Automatizacija Procesa Srbija', 'n8n Automatizacija Beograd', 'Voice AI Beograd'],
}, },
nearbyAreas: ['Novi Sad', 'Niš', 'Kragujevac', 'Subotica'], nearbyAreas: ['Novi Sad', 'Niš', 'Kragujevac', 'Subotica'],
}, },
@@ -479,9 +479,9 @@ export const cities: CityData[] = [
sr: 'Novi Sad je rastući IT centar Srbije i Evropska prestonica kulture 2022. Podržavam rastuću tech scenu sa AI rešenjima i modernom automatizacijom procesa.', sr: 'Novi Sad je rastući IT centar Srbije i Evropska prestonica kulture 2022. Podržavam rastuću tech scenu sa AI rešenjima i modernom automatizacijom procesa.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Novi Sad', 'IT Freelancer Serbien', 'Fullstack Entwickler Novi Sad', 'Automatisierung Vojvodina'], de: ['KI Entwickler Novi Sad', 'IT Freelancer Serbien', 'Next.js Entwickler Novi Sad', 'React Entwickler Novi Sad', 'Claude AI Novi Sad', 'TypeScript Entwickler Novi Sad', 'Künstliche Intelligenz Novi Sad', 'Fullstack Entwickler Novi Sad', 'Automatisierung Vojvodina'],
en: ['AI Developer Novi Sad', 'IT Freelancer Serbia', 'Fullstack Developer Novi Sad', 'Automation Vojvodina'], en: ['AI Developer Novi Sad', 'IT Freelancer Serbia', 'Next.js Developer Novi Sad', 'React Developer Novi Sad', 'Claude AI Developer Novi Sad', 'TypeScript Developer Novi Sad', 'Fullstack Developer Novi Sad', 'Automation Vojvodina'],
sr: ['AI Developer Novi Sad', 'Softverski Razvoj Novi Sad', 'Fullstack Programer Novi Sad', 'IT Freelancer Novi Sad', 'Web Razvoj Novi Sad', 'Startup Developer Novi Sad'], sr: ['AI Developer Novi Sad', 'Softverski Razvoj Novi Sad', 'Next.js Programer Novi Sad', 'React Programer Novi Sad', 'Claude AI Novi Sad', 'Fullstack Programer Novi Sad', 'IT Freelancer Novi Sad', 'Web Razvoj Novi Sad', 'Startup Developer Novi Sad'],
}, },
nearbyAreas: ['Beograd', 'Subotica', 'Zrenjanin', 'Sombor'], nearbyAreas: ['Beograd', 'Subotica', 'Zrenjanin', 'Sombor'],
}, },
@@ -499,9 +499,9 @@ export const cities: CityData[] = [
sr: 'Niš je treći najveći grad Srbije i rastući IT centar na jugu zemlje. Nudim AI razvoj i rešenja za automatizaciju za kompanije iz Niša i južne Srbije.', sr: 'Niš je treći najveći grad Srbije i rastući IT centar na jugu zemlje. Nudim AI razvoj i rešenja za automatizaciju za kompanije iz Niša i južne Srbije.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Niš', 'IT Freelancer Südserbien', 'Fullstack Entwickler Niš', 'Automatisierung Niš'], de: ['KI Entwickler Niš', 'IT Freelancer Südserbien', 'Next.js Entwickler Niš', 'React Entwickler Niš', 'Claude AI Niš', 'TypeScript Entwickler Niš', 'Künstliche Intelligenz Niš', 'Fullstack Entwickler Niš', 'Automatisierung Niš'],
en: ['AI Developer Niš', 'IT Freelancer South Serbia', 'Fullstack Developer Niš', 'Automation Niš'], en: ['AI Developer Niš', 'IT Freelancer South Serbia', 'Next.js Developer Niš', 'React Developer Niš', 'Claude AI Developer Niš', 'TypeScript Developer Niš', 'Fullstack Developer Niš', 'Automation Niš'],
sr: ['AI Developer Niš', 'Softverski Razvoj Niš', 'Fullstack Programer Niš', 'IT Freelancer Niš', 'Web Razvoj Niš', 'Python Programer Niš'], sr: ['AI Developer Niš', 'Softverski Razvoj Niš', 'Next.js Programer Niš', 'React Programer Niš', 'Claude AI Niš', 'Fullstack Programer Niš', 'IT Freelancer Niš', 'Web Razvoj Niš', 'Python Programer Niš'],
}, },
nearbyAreas: ['Beograd', 'Kragujevac', 'Leskovac', 'Vranje'], nearbyAreas: ['Beograd', 'Kragujevac', 'Leskovac', 'Vranje'],
}, },
@@ -519,9 +519,9 @@ export const cities: CityData[] = [
sr: 'Kragujevac je industrijski centar centralne Srbije sa jakom automobilskom industrijom. Podržavam kompanije iz Kragujevca sa modernim AI i rešenjima za automatizaciju.', sr: 'Kragujevac je industrijski centar centralne Srbije sa jakom automobilskom industrijom. Podržavam kompanije iz Kragujevca sa modernim AI i rešenjima za automatizaciju.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Kragujevac', 'IT Freelancer Zentralserbien', 'Fullstack Entwickler Kragujevac', 'Automatisierung Kragujevac'], de: ['KI Entwickler Kragujevac', 'IT Freelancer Zentralserbien', 'Next.js Entwickler Kragujevac', 'React Entwickler Kragujevac', 'Claude AI Kragujevac', 'TypeScript Entwickler Kragujevac', 'Künstliche Intelligenz Kragujevac', 'Fullstack Entwickler Kragujevac', 'Automatisierung Kragujevac'],
en: ['AI Developer Kragujevac', 'IT Freelancer Central Serbia', 'Fullstack Developer Kragujevac', 'Automation Kragujevac'], en: ['AI Developer Kragujevac', 'IT Freelancer Central Serbia', 'Next.js Developer Kragujevac', 'React Developer Kragujevac', 'Claude AI Developer Kragujevac', 'TypeScript Developer Kragujevac', 'Fullstack Developer Kragujevac', 'Automation Kragujevac'],
sr: ['AI Developer Kragujevac', 'Softverski Razvoj Kragujevac', 'Fullstack Programer Kragujevac', 'IT Freelancer Kragujevac', 'Web Razvoj Kragujevac', 'Automatizacija Procesa Kragujevac'], sr: ['AI Developer Kragujevac', 'Softverski Razvoj Kragujevac', 'Next.js Programer Kragujevac', 'React Programer Kragujevac', 'Claude AI Kragujevac', 'Fullstack Programer Kragujevac', 'IT Freelancer Kragujevac', 'Web Razvoj Kragujevac', 'Automatizacija Procesa Kragujevac'],
}, },
nearbyAreas: ['Beograd', 'Niš', 'Čačak', 'Kraljevo'], nearbyAreas: ['Beograd', 'Niš', 'Čačak', 'Kraljevo'],
}, },
@@ -539,9 +539,9 @@ export const cities: CityData[] = [
sr: 'Subotica se nalazi na severu Srbije blizu mađarske granice i poznata je po svojoj multikulturalnoj istoriji. Nudim AI razvoj i automatizaciju za rastuću tech scenu.', sr: 'Subotica se nalazi na severu Srbije blizu mađarske granice i poznata je po svojoj multikulturalnoj istoriji. Nudim AI razvoj i automatizaciju za rastuću tech scenu.',
}, },
keywords: { keywords: {
de: ['KI Entwickler Subotica', 'IT Freelancer Nordserbien', 'Fullstack Entwickler Subotica', 'Automatisierung Subotica'], de: ['KI Entwickler Subotica', 'IT Freelancer Nordserbien', 'Next.js Entwickler Subotica', 'React Entwickler Subotica', 'Claude AI Subotica', 'TypeScript Entwickler Subotica', 'Künstliche Intelligenz Subotica', 'Fullstack Entwickler Subotica', 'Automatisierung Subotica'],
en: ['AI Developer Subotica', 'IT Freelancer North Serbia', 'Fullstack Developer Subotica', 'Automation Subotica'], en: ['AI Developer Subotica', 'IT Freelancer North Serbia', 'Next.js Developer Subotica', 'React Developer Subotica', 'Claude AI Developer Subotica', 'TypeScript Developer Subotica', 'Fullstack Developer Subotica', 'Automation Subotica'],
sr: ['AI Developer Subotica', 'Softverski Razvoj Subotica', 'Fullstack Programer Subotica', 'IT Freelancer Subotica', 'Web Razvoj Subotica', 'Remote Programer Subotica'], sr: ['AI Developer Subotica', 'Softverski Razvoj Subotica', 'Next.js Programer Subotica', 'React Programer Subotica', 'Claude AI Subotica', 'Fullstack Programer Subotica', 'IT Freelancer Subotica', 'Web Razvoj Subotica', 'Remote Programer Subotica'],
}, },
nearbyAreas: ['Novi Sad', 'Sombor', 'Szeged', 'Beograd'], nearbyAreas: ['Novi Sad', 'Sombor', 'Szeged', 'Beograd'],
}, },
+30
View File
@@ -22,6 +22,11 @@ export interface ServiceData {
sr: string[]; sr: string[];
}; };
technologies: string[]; technologies: string[];
seoKeywords?: {
de: string[];
en: string[];
sr: string[];
};
useCases: { useCases: {
de: string[]; de: string[];
en: string[]; en: string[];
@@ -72,6 +77,11 @@ export const services: ServiceData[] = [
], ],
}, },
technologies: ['OpenAI API', 'Claude API', 'LangChain', 'LlamaIndex', 'Python', 'FastAPI'], technologies: ['OpenAI API', 'Claude API', 'LangChain', 'LlamaIndex', 'Python', 'FastAPI'],
seoKeywords: {
de: ['KI Entwickler beauftragen', 'Claude AI Integration', 'KI-Agenten Entwicklung', 'LLM Integration Unternehmen', 'Künstliche Intelligenz Freelancer', 'AI Agentur Köln'],
en: ['Hire AI Developer', 'Claude AI Integration Expert', 'AI Agent Development', 'LLM Integration Services', 'AI Consultant', 'AI Developer for Hire'],
sr: ['AI Razvoj Srbija', 'Claude AI Integracija', 'Razvoj AI Agenata', 'LLM Integracija', 'AI Konsultant'],
},
useCases: { useCases: {
de: [ de: [
'Kundenservice-Automatisierung', 'Kundenservice-Automatisierung',
@@ -135,6 +145,11 @@ export const services: ServiceData[] = [
], ],
}, },
technologies: ['Vapi', 'Twilio', 'ElevenLabs', 'Whisper', 'Python', 'WebRTC'], technologies: ['Vapi', 'Twilio', 'ElevenLabs', 'Whisper', 'Python', 'WebRTC'],
seoKeywords: {
de: ['Voice AI Entwickler', 'Sprachassistent entwickeln', 'Telefon-Bot Entwicklung', 'Voice Bot Agentur', 'KI Sprachassistent'],
en: ['Hire Voice AI Developer', 'Voice Bot Development', 'Phone Bot Developer', 'Voice Assistant Expert', 'AI Voice Agent'],
sr: ['Voice AI Razvoj', 'Glasovni Asistent Razvoj', 'Telefonski Bot Srbija'],
},
useCases: { useCases: {
de: [ de: [
'Automatisierte Terminbuchung', 'Automatisierte Terminbuchung',
@@ -198,6 +213,11 @@ export const services: ServiceData[] = [
], ],
}, },
technologies: ['n8n', 'Make', 'Zapier', 'Python', 'Playwright', 'Puppeteer'], technologies: ['n8n', 'Make', 'Zapier', 'Python', 'Playwright', 'Puppeteer'],
seoKeywords: {
de: ['Prozessautomatisierung Freelancer', 'n8n Experte beauftragen', 'Workflow Automatisierung', 'API Integration Experte', 'Automatisierung Agentur'],
en: ['Hire Automation Expert', 'n8n Developer for Hire', 'Workflow Automation Services', 'API Integration Expert', 'Process Automation Freelancer'],
sr: ['Automatizacija Procesa Srbija', 'n8n Ekspert', 'Automatizacija Radnih Tokova'],
},
useCases: { useCases: {
de: [ de: [
'Lead-Erfassung und CRM-Updates', 'Lead-Erfassung und CRM-Updates',
@@ -261,6 +281,11 @@ export const services: ServiceData[] = [
], ],
}, },
technologies: ['Next.js', 'React', 'TypeScript', 'PostgreSQL', 'Prisma', 'Stripe', 'Vercel'], technologies: ['Next.js', 'React', 'TypeScript', 'PostgreSQL', 'Prisma', 'Stripe', 'Vercel'],
seoKeywords: {
de: ['SaaS Entwickler beauftragen', 'MVP Entwicklung Freelancer', 'Next.js SaaS Entwicklung', 'React SaaS Plattform', 'SaaS Agentur Deutschland'],
en: ['Hire SaaS Developer', 'MVP Development Services', 'Next.js SaaS Development', 'React SaaS Platform', 'SaaS Development Agency'],
sr: ['SaaS Razvoj Srbija', 'MVP Razvoj', 'Next.js SaaS Razvoj'],
},
useCases: { useCases: {
de: [ de: [
'Interne Tools & Dashboards', 'Interne Tools & Dashboards',
@@ -324,6 +349,11 @@ export const services: ServiceData[] = [
], ],
}, },
technologies: ['React', 'Next.js', 'TypeScript', 'Python', 'FastAPI', 'Node.js', 'PostgreSQL', 'Docker'], technologies: ['React', 'Next.js', 'TypeScript', 'Python', 'FastAPI', 'Node.js', 'PostgreSQL', 'Docker'],
seoKeywords: {
de: ['Fullstack Entwickler beauftragen', 'Next.js Entwickler Freelancer', 'React Entwickler Deutschland', 'TypeScript Experte', 'Webentwicklung Agentur Köln'],
en: ['Hire Fullstack Developer', 'Next.js Developer for Hire', 'React Developer Freelance', 'TypeScript Expert', 'Fullstack Development Agency'],
sr: ['Fullstack Programer Srbija', 'Next.js Programer', 'React Programer Freelance', 'Web Razvoj Srbija'],
},
useCases: { useCases: {
de: [ de: [
'Web-Anwendungen', 'Web-Anwendungen',
+281
View File
@@ -0,0 +1,281 @@
export interface TechnologyData {
slug: string;
name: string;
title: {
de: string;
en: string;
sr: string;
};
description: {
de: string;
en: string;
sr: string;
};
keywords: {
de: string[];
en: string[];
sr: string[];
};
features: {
de: string[];
en: string[];
sr: string[];
};
useCases: {
de: string[];
en: string[];
sr: string[];
};
relatedServices: string[]; // slugs from services.ts
}
export const technologies: TechnologyData[] = [
{
slug: 'nextjs',
name: 'Next.js',
title: {
de: 'Next.js Entwickler Köln | Next.js Agentur',
en: 'Next.js Developer | Next.js Agency',
sr: 'Next.js Programer | Next.js Agencija',
},
description: {
de: 'Professionelle Next.js Entwicklung für performante, SEO-optimierte Web-Anwendungen und SaaS-Produkte. Als erfahrener Next.js Entwickler aus Köln setze ich auf Server-Side Rendering, App Router und moderne React-Patterns für Ihr Projekt.',
en: 'Professional Next.js development for performant, SEO-optimized web applications and SaaS products. As an experienced Next.js developer, I leverage Server-Side Rendering, App Router and modern React patterns for your project.',
sr: 'Profesionalni Next.js razvoj za performantne, SEO-optimizovane web aplikacije i SaaS proizvode. Kao iskusni Next.js programer, koristim Server-Side Rendering, App Router i moderne React paterne za vaš projekat.',
},
keywords: {
de: ['Next.js Entwickler', 'Next.js Agentur', 'Next.js Freelancer', 'Next.js Köln', 'Next.js Deutschland', 'Next.js App Router', 'Next.js SSR', 'Server-Side Rendering Entwickler'],
en: ['Next.js Developer', 'Next.js Agency', 'Next.js Freelancer', 'Next.js Expert', 'Hire Next.js Developer', 'Next.js App Router', 'Next.js SSR', 'Server-Side Rendering Developer'],
sr: ['Next.js Programer', 'Next.js Agencija', 'Next.js Freelancer', 'Next.js Srbija', 'Next.js App Router', 'Next.js SSR'],
},
features: {
de: ['Server-Side Rendering (SSR) & Static Generation', 'App Router mit React Server Components', 'API Routes & Serverless Functions', 'Internationalisierung (i18n) mit next-intl', 'SEO-Optimierung & Core Web Vitals', 'Deployment auf Vercel & Self-Hosting'],
en: ['Server-Side Rendering (SSR) & Static Generation', 'App Router with React Server Components', 'API Routes & Serverless Functions', 'Internationalization (i18n) with next-intl', 'SEO optimization & Core Web Vitals', 'Deployment on Vercel & self-hosting'],
sr: ['Server-Side Rendering (SSR) & Static Generation', 'App Router sa React Server Components', 'API Routes & Serverless Functions', 'Internacionalizacija (i18n) sa next-intl', 'SEO optimizacija & Core Web Vitals', 'Deployment na Vercel & self-hosting'],
},
useCases: {
de: ['Corporate Websites & Landing Pages', 'SaaS-Plattformen & Dashboards', 'E-Commerce Shops', 'Multi-Language Portale', 'Blog & Content-Plattformen'],
en: ['Corporate Websites & Landing Pages', 'SaaS Platforms & Dashboards', 'E-Commerce Shops', 'Multi-Language Portals', 'Blog & Content Platforms'],
sr: ['Corporate Web Sajtovi & Landing Pages', 'SaaS Platforme & Dashboards', 'E-Commerce Prodavnice', 'Višejezični Portali', 'Blog & Content Platforme'],
},
relatedServices: ['saas-entwicklung', 'fullstack-entwicklung'],
},
{
slug: 'react',
name: 'React',
title: {
de: 'React Entwickler Köln | React Agentur',
en: 'React Developer | React Agency',
sr: 'React Programer | React Agencija',
},
description: {
de: 'Erfahrener React Entwickler für moderne, interaktive Web-Anwendungen. Ich entwickle skalierbare React-Frontends mit TypeScript, Tailwind CSS und modernem State Management für Unternehmen in Köln und deutschlandweit.',
en: 'Experienced React developer for modern, interactive web applications. I build scalable React frontends with TypeScript, Tailwind CSS and modern state management for businesses worldwide.',
sr: 'Iskusni React programer za moderne, interaktivne web aplikacije. Pravim skalabilne React frontend-ove sa TypeScript, Tailwind CSS i modernim state management-om za kompanije širom sveta.',
},
keywords: {
de: ['React Entwickler', 'React Agentur', 'React Freelancer', 'React Köln', 'React Deutschland', 'React TypeScript', 'React Hooks', 'React Komponenten'],
en: ['React Developer', 'React Agency', 'React Freelancer', 'React Expert', 'Hire React Developer', 'React TypeScript', 'React Hooks', 'React Components'],
sr: ['React Programer', 'React Agencija', 'React Freelancer', 'React Srbija', 'React TypeScript', 'React Hooks'],
},
features: {
de: ['React 19 mit Server Components', 'TypeScript-first Entwicklung', 'Custom Hooks & Composable Architecture', 'State Management (Zustand, Redux)', 'Component Libraries & Design Systems', 'Performance-Optimierung & Lazy Loading'],
en: ['React 19 with Server Components', 'TypeScript-first development', 'Custom Hooks & Composable Architecture', 'State Management (Zustand, Redux)', 'Component Libraries & Design Systems', 'Performance optimization & Lazy Loading'],
sr: ['React 19 sa Server Components', 'TypeScript-first razvoj', 'Custom Hooks & Composable Architecture', 'State Management (Zustand, Redux)', 'Component Libraries & Design Systems', 'Performance optimizacija & Lazy Loading'],
},
useCases: {
de: ['Single-Page Applications (SPA)', 'Admin Dashboards & Portale', 'Interaktive Datenvisualisierungen', 'Mobile-First Web-Apps', 'Design System Implementierung'],
en: ['Single-Page Applications (SPA)', 'Admin Dashboards & Portals', 'Interactive Data Visualizations', 'Mobile-First Web Apps', 'Design System Implementation'],
sr: ['Single-Page Applications (SPA)', 'Admin Dashboards & Portali', 'Interaktivne Vizualizacije Podataka', 'Mobile-First Web Aplikacije', 'Design System Implementacija'],
},
relatedServices: ['fullstack-entwicklung', 'saas-entwicklung'],
},
{
slug: 'typescript',
name: 'TypeScript',
title: {
de: 'TypeScript Entwickler Köln | TypeScript Experte',
en: 'TypeScript Developer | TypeScript Expert',
sr: 'TypeScript Programer | TypeScript Ekspert',
},
description: {
de: 'TypeScript-Experte für typsichere, wartbare und skalierbare Anwendungen. Ich entwickle mit TypeScript sowohl Frontend (React, Next.js) als auch Backend (Node.js, FastAPI) und sorge für höchste Code-Qualität.',
en: 'TypeScript expert for type-safe, maintainable and scalable applications. I develop with TypeScript both frontend (React, Next.js) and backend (Node.js, FastAPI), ensuring the highest code quality.',
sr: 'TypeScript ekspert za type-safe, održive i skalabilne aplikacije. Razvijam sa TypeScript-om i frontend (React, Next.js) i backend (Node.js, FastAPI), obezbeđujući najviši kvalitet koda.',
},
keywords: {
de: ['TypeScript Entwickler', 'TypeScript Experte', 'TypeScript Freelancer', 'TypeScript Köln', 'TypeScript Deutschland', 'Typsichere Entwicklung'],
en: ['TypeScript Developer', 'TypeScript Expert', 'TypeScript Freelancer', 'Hire TypeScript Developer', 'Type-safe Development', 'TypeScript Consultant'],
sr: ['TypeScript Programer', 'TypeScript Ekspert', 'TypeScript Freelancer', 'TypeScript Srbija', 'Type-safe Razvoj'],
},
features: {
de: ['Strenge Typisierung für fehlerfreien Code', 'Generics & Advanced Type Patterns', 'Zod Schema-Validierung', 'Fullstack TypeScript (Frontend + Backend)', 'Migration von JavaScript zu TypeScript', 'CI/CD mit TypeScript Checks'],
en: ['Strict typing for error-free code', 'Generics & Advanced Type Patterns', 'Zod Schema Validation', 'Fullstack TypeScript (Frontend + Backend)', 'JavaScript to TypeScript migration', 'CI/CD with TypeScript checks'],
sr: ['Stroga tipizacija za kod bez grešaka', 'Generics & Advanced Type Patterns', 'Zod Schema Validacija', 'Fullstack TypeScript (Frontend + Backend)', 'Migracija sa JavaScript na TypeScript', 'CI/CD sa TypeScript provere'],
},
useCases: {
de: ['Enterprise Web-Anwendungen', 'API-Entwicklung mit typsicheren Schemas', 'Monorepo-Projekte', 'Library & SDK Entwicklung', 'Legacy-Code Migration'],
en: ['Enterprise Web Applications', 'API development with type-safe schemas', 'Monorepo Projects', 'Library & SDK Development', 'Legacy Code Migration'],
sr: ['Enterprise Web Aplikacije', 'API razvoj sa type-safe šemama', 'Monorepo Projekti', 'Library & SDK Razvoj', 'Migracija Legacy Koda'],
},
relatedServices: ['fullstack-entwicklung', 'saas-entwicklung'],
},
{
slug: 'tailwind-css',
name: 'Tailwind CSS',
title: {
de: 'Tailwind CSS Entwickler Köln | Tailwind Experte',
en: 'Tailwind CSS Developer | Tailwind Expert',
sr: 'Tailwind CSS Programer | Tailwind Ekspert',
},
description: {
de: 'Tailwind CSS Experte für modernes, responsives und performantes UI-Design. Ich entwickle maßgeschneiderte Benutzeroberflächen mit Tailwind CSS v4, Custom Design Systems und Animationen.',
en: 'Tailwind CSS expert for modern, responsive and performant UI design. I build custom user interfaces with Tailwind CSS v4, custom design systems and animations.',
sr: 'Tailwind CSS ekspert za moderan, responsivan i performantan UI dizajn. Pravim prilagođene korisničke interfejse sa Tailwind CSS v4, custom design sistemima i animacijama.',
},
keywords: {
de: ['Tailwind CSS Entwickler', 'Tailwind CSS Experte', 'Tailwind CSS Freelancer', 'Tailwind CSS v4', 'CSS Framework Entwickler', 'UI Entwicklung'],
en: ['Tailwind CSS Developer', 'Tailwind CSS Expert', 'Tailwind CSS Freelancer', 'Tailwind CSS v4', 'CSS Framework Developer', 'UI Development'],
sr: ['Tailwind CSS Programer', 'Tailwind CSS Ekspert', 'Tailwind CSS Freelancer', 'Tailwind CSS v4', 'UI Razvoj'],
},
features: {
de: ['Tailwind CSS v4 mit CSS Layers', 'Custom Design Tokens & Themes', 'Responsive & Mobile-First Design', 'Dark Mode Implementierung', 'Framer Motion Animationen', 'Component Library Entwicklung'],
en: ['Tailwind CSS v4 with CSS Layers', 'Custom Design Tokens & Themes', 'Responsive & Mobile-First Design', 'Dark Mode Implementation', 'Framer Motion Animations', 'Component Library Development'],
sr: ['Tailwind CSS v4 sa CSS Layers', 'Custom Design Tokens & Themes', 'Responsive & Mobile-First Design', 'Dark Mode Implementacija', 'Framer Motion Animacije', 'Component Library Razvoj'],
},
useCases: {
de: ['Landing Pages & Marketing-Seiten', 'SaaS Dashboard Interfaces', 'E-Commerce Produktseiten', 'Mobile-First Responsive Design', 'Design System Aufbau'],
en: ['Landing Pages & Marketing Sites', 'SaaS Dashboard Interfaces', 'E-Commerce Product Pages', 'Mobile-First Responsive Design', 'Design System Development'],
sr: ['Landing Pages & Marketing Sajtovi', 'SaaS Dashboard Interfejsi', 'E-Commerce Stranice Proizvoda', 'Mobile-First Responsive Design', 'Design System Razvoj'],
},
relatedServices: ['fullstack-entwicklung', 'saas-entwicklung'],
},
{
slug: 'claude-ai',
name: 'Claude AI',
title: {
de: 'Claude AI Entwickler | Anthropic Claude Integration',
en: 'Claude AI Developer | Anthropic Claude Integration',
sr: 'Claude AI Developer | Anthropic Claude Integracija',
},
description: {
de: 'Spezialisiert auf Claude AI von Anthropic ich integriere Claude in Ihre Geschäftsprozesse für intelligente Automatisierung, Chatbots und KI-Agenten. DSGVO-konform und maßgeschneidert für Ihre Anforderungen.',
en: 'Specialized in Claude AI by Anthropic I integrate Claude into your business processes for intelligent automation, chatbots and AI agents. GDPR-compliant and tailored to your requirements.',
sr: 'Specijalizovan za Claude AI od Anthropic-a integrisem Claude u vaše poslovne procese za inteligentnu automatizaciju, chatbotove i AI agente. GDPR-usklađeno i prilagođeno vašim zahtevima.',
},
keywords: {
de: ['Claude AI Entwickler', 'Claude AI Integration', 'Anthropic Claude Experte', 'Claude API Entwickler', 'Claude AI Agentur', 'Claude AI DSGVO', 'Künstliche Intelligenz Claude'],
en: ['Claude AI Developer', 'Claude AI Integration', 'Anthropic Claude Expert', 'Claude API Developer', 'Claude AI Agency', 'Claude AI GDPR', 'Artificial Intelligence Claude'],
sr: ['Claude AI Developer', 'Claude AI Integracija', 'Anthropic Claude Ekspert', 'Claude API Programer', 'Veštačka Inteligencija Claude'],
},
features: {
de: ['Claude API Integration & Orchestrierung', 'Claude-basierte KI-Agenten', 'RAG-Systeme mit Claude', 'Claude für Dokumentenanalyse', 'Claude Computer Use & Tool Use', 'Multi-Model Orchestrierung (Claude + GPT)'],
en: ['Claude API Integration & Orchestration', 'Claude-based AI Agents', 'RAG Systems with Claude', 'Claude for Document Analysis', 'Claude Computer Use & Tool Use', 'Multi-Model Orchestration (Claude + GPT)'],
sr: ['Claude API Integracija & Orkestracija', 'Claude-bazirani AI Agenti', 'RAG Sistemi sa Claude', 'Claude za Analizu Dokumenata', 'Claude Computer Use & Tool Use', 'Multi-Model Orkestracija (Claude + GPT)'],
},
useCases: {
de: ['Intelligente Kundenservice-Chatbots', 'Automatische Dokumentenverarbeitung', 'Code-Review & Code-Generierung', 'Wissensmanagement mit RAG', 'Autonome KI-Agenten für Workflows'],
en: ['Intelligent Customer Service Chatbots', 'Automatic Document Processing', 'Code Review & Code Generation', 'Knowledge Management with RAG', 'Autonomous AI Agents for Workflows'],
sr: ['Inteligentni Chatbotovi za Korisničku Službu', 'Automatska Obrada Dokumenata', 'Code Review & Generisanje Koda', 'Upravljanje Znanjem sa RAG', 'Autonomni AI Agenti za Radne Tokove'],
},
relatedServices: ['ki-entwicklung', 'ki-agenten', 'gpt-integration'],
},
{
slug: 'python',
name: 'Python',
title: {
de: 'Python Entwickler Köln | Python Freelancer',
en: 'Python Developer | Python Freelancer',
sr: 'Python Programer | Python Freelancer',
},
description: {
de: 'Erfahrener Python Entwickler für Backend-Systeme, KI-Anwendungen und Automatisierung. Ich setze Python für FastAPI, Datenverarbeitung, Machine Learning und Prozessautomatisierung ein.',
en: 'Experienced Python developer for backend systems, AI applications and automation. I use Python for FastAPI, data processing, machine learning and process automation.',
sr: 'Iskusni Python programer za backend sisteme, AI aplikacije i automatizaciju. Koristim Python za FastAPI, obradu podataka, machine learning i automatizaciju procesa.',
},
keywords: {
de: ['Python Entwickler', 'Python Freelancer', 'Python Köln', 'Python Backend', 'FastAPI Entwickler', 'Python Automatisierung', 'Python KI'],
en: ['Python Developer', 'Python Freelancer', 'Python Backend', 'FastAPI Developer', 'Python Automation', 'Python AI', 'Hire Python Developer'],
sr: ['Python Programer', 'Python Freelancer', 'Python Backend', 'FastAPI Programer', 'Python Automatizacija', 'Python AI'],
},
features: {
de: ['FastAPI & Django Backend-Entwicklung', 'Python für KI & Machine Learning', 'Data Processing & Web Scraping', 'REST & GraphQL APIs', 'Automatisierung mit Python Scripts', 'Integration mit n8n & Zapier'],
en: ['FastAPI & Django Backend Development', 'Python for AI & Machine Learning', 'Data Processing & Web Scraping', 'REST & GraphQL APIs', 'Automation with Python Scripts', 'Integration with n8n & Zapier'],
sr: ['FastAPI & Django Backend Razvoj', 'Python za AI & Machine Learning', 'Obrada Podataka & Web Scraping', 'REST & GraphQL API-ji', 'Automatizacija sa Python Skriptama', 'Integracija sa n8n & Zapier'],
},
useCases: {
de: ['KI-Backend & API-Entwicklung', 'Datenverarbeitung & ETL Pipelines', 'Web Scraping & Data Mining', 'Chatbot & LLM Backends', 'Automatisierte Reporting-Systeme'],
en: ['AI Backend & API Development', 'Data Processing & ETL Pipelines', 'Web Scraping & Data Mining', 'Chatbot & LLM Backends', 'Automated Reporting Systems'],
sr: ['AI Backend & API Razvoj', 'Obrada Podataka & ETL Pipeline-ovi', 'Web Scraping & Data Mining', 'Chatbot & LLM Backend-ovi', 'Automatizovani Reporting Sistemi'],
},
relatedServices: ['ki-entwicklung', 'prozessautomatisierung', 'fullstack-entwicklung'],
},
{
slug: 'n8n',
name: 'n8n',
title: {
de: 'n8n Entwickler | n8n Automatisierung Experte',
en: 'n8n Developer | n8n Automation Expert',
sr: 'n8n Developer | n8n Automatizacija Ekspert',
},
description: {
de: 'Zertifizierter n8n Experte für professionelle Workflow-Automatisierung. Ich entwickle komplexe n8n Workflows, integriere KI (Claude, GPT) und migriere bestehende Automatisierungen von Zapier oder Make.',
en: 'Certified n8n expert for professional workflow automation. I develop complex n8n workflows, integrate AI (Claude, GPT) and migrate existing automations from Zapier or Make.',
sr: 'Sertifikovani n8n ekspert za profesionalnu automatizaciju radnih tokova. Razvijam kompleksne n8n radne tokove, integrisem AI (Claude, GPT) i migriam postojeće automatizacije sa Zapier ili Make.',
},
keywords: {
de: ['n8n Entwickler', 'n8n Experte', 'n8n Automatisierung', 'n8n Freelancer', 'n8n Workflow', 'n8n Self-Hosted', 'Zapier Alternative'],
en: ['n8n Developer', 'n8n Expert', 'n8n Automation', 'n8n Freelancer', 'n8n Workflow', 'n8n Self-Hosted', 'Zapier Alternative'],
sr: ['n8n Developer', 'n8n Ekspert', 'n8n Automatizacija', 'n8n Freelancer', 'n8n Radni Tok', 'n8n Self-Hosted'],
},
features: {
de: ['Custom n8n Workflow-Entwicklung', 'KI-Integration in n8n (Claude, GPT)', 'Migration von Zapier/Make zu n8n', 'Self-Hosted n8n Setup & Wartung', 'API-Integrationen & Webhooks', 'n8n Schulung & Support'],
en: ['Custom n8n Workflow Development', 'AI Integration in n8n (Claude, GPT)', 'Migration from Zapier/Make to n8n', 'Self-Hosted n8n Setup & Maintenance', 'API Integrations & Webhooks', 'n8n Training & Support'],
sr: ['Custom n8n Razvoj Radnih Tokova', 'AI Integracija u n8n (Claude, GPT)', 'Migracija sa Zapier/Make na n8n', 'Self-Hosted n8n Setup & Održavanje', 'API Integracije & Webhooks', 'n8n Obuka & Podrška'],
},
useCases: {
de: ['CRM-Automatisierung', 'E-Commerce Workflow-Automatisierung', 'Lead-Generierung & Scoring', 'Daten-Synchronisation', 'Social Media Automatisierung'],
en: ['CRM Automation', 'E-Commerce Workflow Automation', 'Lead Generation & Scoring', 'Data Synchronization', 'Social Media Automation'],
sr: ['CRM Automatizacija', 'E-Commerce Automatizacija Radnih Tokova', 'Generisanje Lead-ova & Scoring', 'Sinhronizacija Podataka', 'Automatizacija Društvenih Mreža'],
},
relatedServices: ['prozessautomatisierung', 'n8n-entwicklung'],
},
{
slug: 'ki-agenten',
name: 'KI-Agenten',
title: {
de: 'KI-Agenten Entwickler | AI Agent Development',
en: 'AI Agent Developer | AI Agent Development',
sr: 'AI Agent Developer | Razvoj AI Agenata',
},
description: {
de: 'Spezialisiert auf die Entwicklung autonomer KI-Agenten mit Claude AI, GPT-4 und Open-Source-Modellen. Ich baue intelligente Agenten, die eigenständig Entscheidungen treffen, Tools nutzen und komplexe Aufgaben erledigen.',
en: 'Specialized in developing autonomous AI agents with Claude AI, GPT-4 and open-source models. I build intelligent agents that independently make decisions, use tools and complete complex tasks.',
sr: 'Specijalizovan za razvoj autonomnih AI agenata sa Claude AI, GPT-4 i open-source modelima. Pravim inteligentne agente koji samostalno donose odluke, koriste alate i obavljaju kompleksne zadatke.',
},
keywords: {
de: ['KI-Agenten Entwicklung', 'AI Agent Developer', 'KI-Agenten Köln', 'Autonome KI', 'Multi-Agent System', 'Künstliche Intelligenz Agenten'],
en: ['AI Agent Development', 'AI Agent Developer', 'Autonomous AI', 'Multi-Agent System', 'Artificial Intelligence Agents', 'Hire AI Agent Developer'],
sr: ['Razvoj AI Agenata', 'AI Agent Developer', 'Autonomni AI', 'Multi-Agent Sistem', 'Veštačka Inteligencija Agenti'],
},
features: {
de: ['Autonome KI-Agenten mit Tool-Use', 'Multi-Agent-Systeme & Orchestrierung', 'Claude AI & GPT-4 basierte Agenten', 'RAG-integrierte Agenten', 'Menschliche Aufsicht & Kontrolle', 'Integration in bestehende Workflows'],
en: ['Autonomous AI Agents with Tool Use', 'Multi-Agent Systems & Orchestration', 'Claude AI & GPT-4 based Agents', 'RAG-integrated Agents', 'Human Oversight & Control', 'Integration into Existing Workflows'],
sr: ['Autonomni AI Agenti sa Tool Use', 'Multi-Agent Sistemi & Orkestracija', 'Claude AI & GPT-4 bazirani Agenti', 'RAG-integrisani Agenti', 'Ljudski Nadzor & Kontrola', 'Integracija u Postojeće Radne Tokove'],
},
useCases: {
de: ['Research-Agenten für Marktanalyse', 'Sales-Agenten für Lead-Qualifizierung', 'Support-Agenten für Ticketing', 'Content-Agenten für Marketing', 'DevOps-Agenten für Monitoring'],
en: ['Research Agents for Market Analysis', 'Sales Agents for Lead Qualification', 'Support Agents for Ticketing', 'Content Agents for Marketing', 'DevOps Agents for Monitoring'],
sr: ['Research Agenti za Analizu Tržišta', 'Sales Agenti za Kvalifikaciju Lead-ova', 'Support Agenti za Ticketing', 'Content Agenti za Marketing', 'DevOps Agenti za Monitoring'],
},
relatedServices: ['ki-agenten', 'ki-entwicklung', 'gpt-integration'],
},
];
export function getTechnologyBySlug(slug: string): TechnologyData | undefined {
return technologies.find((tech) => tech.slug === slug);
}
export function getAllTechnologySlugs(): string[] {
return technologies.map((tech) => tech.slug);
}
+25 -5
View File
@@ -59,26 +59,41 @@ export const seoKeywords: Record<Locale, string[]> = {
de: [ de: [
'Fullstack Developer', 'Fullstack Developer',
'KI Entwickler', 'KI Entwickler',
'Künstliche Intelligenz',
'Claude AI Entwickler',
'Next.js Entwickler',
'React Entwickler',
'TypeScript Entwickler',
'Tailwind CSS',
'AI Agentenentwicklung', 'AI Agentenentwicklung',
'KI-Agenten Entwicklung',
'Prozessautomatisierung', 'Prozessautomatisierung',
'Voice AI', 'Voice AI',
'n8n Automatisierung', 'n8n Automatisierung',
'Python Entwickler',
'Köln', 'Köln',
'Deutschland', 'Deutschland',
], ],
en: [ en: [
'Fullstack Developer', 'Fullstack Developer',
'AI Developer', 'AI Developer',
'Claude AI Developer',
'Claude AI Integration',
'Next.js Developer',
'React Developer',
'TypeScript Developer',
'Tailwind CSS Developer',
'Artificial Intelligence',
'AI Agent Developer',
'AI Agent Development', 'AI Agent Development',
'Process Automation Expert', 'Process Automation Expert',
'Voice AI Developer', 'Voice AI Developer',
'n8n Automation', 'n8n Automation',
'Python Developer',
'SaaS Development',
'Remote Developer Europe', 'Remote Developer Europe',
'Remote Developer USA', 'Remote Developer USA',
'Remote Developer UK', 'Remote Developer UK',
'Python Developer',
'React Developer',
'SaaS Development',
'Freelance AI Developer', 'Freelance AI Developer',
'London', 'London',
'New York', 'New York',
@@ -90,10 +105,17 @@ export const seoKeywords: Record<Locale, string[]> = {
'Fullstack Programer', 'Fullstack Programer',
'AI Programer Srbija', 'AI Programer Srbija',
'AI Developer Beograd', 'AI Developer Beograd',
'Claude AI Developer',
'Next.js Programer',
'React Programer',
'TypeScript Programer',
'Veštačka Inteligencija',
'Razvoj AI Agenata', 'Razvoj AI Agenata',
'Automatizacija Procesa', 'Automatizacija Procesa',
'Voice AI Srbija', 'Voice AI Srbija',
'n8n Automatizacija', 'n8n Automatizacija',
'Python Programer Srbija',
'React Programer Beograd',
'Beograd', 'Beograd',
'Novi Sad', 'Novi Sad',
'Niš', 'Niš',
@@ -101,8 +123,6 @@ export const seoKeywords: Record<Locale, string[]> = {
'Subotica', 'Subotica',
'Srbija', 'Srbija',
'Softverski Razvoj Srbija', 'Softverski Razvoj Srbija',
'Python Programer Srbija',
'React Programer Beograd',
'Remote Developer Dijaspora', 'Remote Developer Dijaspora',
'Freelance Programer Srbija', 'Freelance Programer Srbija',
], ],
+5 -4
View File
@@ -1,7 +1,8 @@
@import './colors.css'; @import "tailwindcss";
@tailwind base; @plugin "tailwindcss-animate";
@tailwind components; @plugin "@tailwindcss/forms" {
@tailwind utilities; strategy: class;
}
/* Font loading states */ /* Font loading states */
html.fonts-loading body { html.fonts-loading body {
+19 -19
View File
@@ -19,7 +19,7 @@ function parseInlineMarkdown(text: string): React.ReactNode {
const codeMatch = remaining.match(/^`([^`]+)`/); const codeMatch = remaining.match(/^`([^`]+)`/);
if (codeMatch) { if (codeMatch) {
parts.push( parts.push(
<code key={keyIndex++} className="bg-zinc-800 px-1.5 py-0.5 rounded text-[#697565] text-sm font-mono"> <code key={keyIndex++} className="bg-card px-1.5 py-0.5 rounded text-[#697565] text-sm font-mono">
{codeMatch[1]} {codeMatch[1]}
</code> </code>
); );
@@ -30,7 +30,7 @@ function parseInlineMarkdown(text: string): React.ReactNode {
// Bold // Bold
const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/); const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
if (boldMatch) { if (boldMatch) {
parts.push(<strong key={keyIndex++} className="font-semibold text-white">{boldMatch[1]}</strong>); parts.push(<strong key={keyIndex++} className="font-semibold text-foreground">{boldMatch[1]}</strong>);
remaining = remaining.slice(boldMatch[0].length); remaining = remaining.slice(boldMatch[0].length);
continue; continue;
} }
@@ -88,13 +88,13 @@ export function renderMarkdown(content: string): React.ReactNode {
if (listItems) { if (listItems) {
if (listItems.type === 'ul') { if (listItems.type === 'ul') {
elements.push( elements.push(
<ul key={`ul-${elements.length}`} className="list-disc list-inside space-y-2 my-4 text-zinc-300"> <ul key={`ul-${elements.length}`} className="list-disc list-inside space-y-2 my-4 text-foreground/80">
{listItems.items} {listItems.items}
</ul> </ul>
); );
} else { } else {
elements.push( elements.push(
<ol key={`ol-${elements.length}`} className="list-decimal list-inside space-y-2 my-4 text-zinc-300"> <ol key={`ol-${elements.length}`} className="list-decimal list-inside space-y-2 my-4 text-foreground/80">
{listItems.items} {listItems.items}
</ol> </ol>
); );
@@ -111,9 +111,9 @@ export function renderMarkdown(content: string): React.ReactNode {
<div key={`table-${elements.length}`} className="overflow-x-auto my-6"> <div key={`table-${elements.length}`} className="overflow-x-auto my-6">
<table className="min-w-full border-collapse"> <table className="min-w-full border-collapse">
<thead> <thead>
<tr className="border-b border-zinc-700"> <tr className="border-b border-border">
{header.map((cell, idx) => ( {header.map((cell, idx) => (
<th key={idx} className="px-4 py-2 text-left text-zinc-200 font-semibold"> <th key={idx} className="px-4 py-2 text-left text-foreground/90 font-semibold">
{cell.trim()} {cell.trim()}
</th> </th>
))} ))}
@@ -121,9 +121,9 @@ export function renderMarkdown(content: string): React.ReactNode {
</thead> </thead>
<tbody> <tbody>
{body.map((row, rowIdx) => ( {body.map((row, rowIdx) => (
<tr key={rowIdx} className="border-b border-zinc-800"> <tr key={rowIdx} className="border-b border-border">
{row.map((cell, cellIdx) => ( {row.map((cell, cellIdx) => (
<td key={cellIdx} className="px-4 py-2 text-zinc-300"> <td key={cellIdx} className="px-4 py-2 text-foreground/80">
{cell.trim()} {cell.trim()}
</td> </td>
))} ))}
@@ -145,8 +145,8 @@ export function renderMarkdown(content: string): React.ReactNode {
if (codeBlock) { if (codeBlock) {
// End code block // End code block
elements.push( elements.push(
<pre key={`code-${elements.length}`} className="bg-zinc-900 border border-zinc-800 rounded-lg p-4 overflow-x-auto my-6"> <pre key={`code-${elements.length}`} className="bg-background border border-border rounded-lg p-4 overflow-x-auto my-6">
<code className="text-sm font-mono text-zinc-300"> <code className="text-sm font-mono text-foreground/80">
{codeBlock.lines.join('\n')} {codeBlock.lines.join('\n')}
</code> </code>
</pre> </pre>
@@ -188,7 +188,7 @@ export function renderMarkdown(content: string): React.ReactNode {
if (line.startsWith('#### ')) { if (line.startsWith('#### ')) {
flushList(); flushList();
elements.push( elements.push(
<h4 key={`h4-${elements.length}`} className="text-lg font-semibold text-white mt-5 mb-2"> <h4 key={`h4-${elements.length}`} className="text-lg font-semibold text-foreground mt-5 mb-2">
{parseInlineMarkdown(line.slice(5))} {parseInlineMarkdown(line.slice(5))}
</h4> </h4>
); );
@@ -199,7 +199,7 @@ export function renderMarkdown(content: string): React.ReactNode {
if (line.startsWith('### ')) { if (line.startsWith('### ')) {
flushList(); flushList();
elements.push( elements.push(
<h3 key={`h3-${elements.length}`} className="text-xl font-semibold text-white mt-6 mb-3"> <h3 key={`h3-${elements.length}`} className="text-xl font-semibold text-foreground mt-6 mb-3">
{parseInlineMarkdown(line.slice(4))} {parseInlineMarkdown(line.slice(4))}
</h3> </h3>
); );
@@ -210,7 +210,7 @@ export function renderMarkdown(content: string): React.ReactNode {
if (line.startsWith('## ')) { if (line.startsWith('## ')) {
flushList(); flushList();
elements.push( elements.push(
<h2 key={`h2-${elements.length}`} className="text-2xl font-bold text-white mt-8 mb-4"> <h2 key={`h2-${elements.length}`} className="text-2xl font-bold text-foreground mt-8 mb-4">
{parseInlineMarkdown(line.slice(3))} {parseInlineMarkdown(line.slice(3))}
</h2> </h2>
); );
@@ -221,7 +221,7 @@ export function renderMarkdown(content: string): React.ReactNode {
if (line.startsWith('# ')) { if (line.startsWith('# ')) {
flushList(); flushList();
elements.push( elements.push(
<h1 key={`h1-${elements.length}`} className="text-3xl font-bold text-white mt-8 mb-4"> <h1 key={`h1-${elements.length}`} className="text-3xl font-bold text-foreground mt-8 mb-4">
{parseInlineMarkdown(line.slice(2))} {parseInlineMarkdown(line.slice(2))}
</h1> </h1>
); );
@@ -269,7 +269,7 @@ export function renderMarkdown(content: string): React.ReactNode {
const itemContent = line.replace(/^[-*]\s\[[ x]\]\s?/i, ''); const itemContent = line.replace(/^[-*]\s\[[ x]\]\s?/i, '');
listItems.items.push( listItems.items.push(
<li key={`li-${listItems.items.length}`} className="flex items-center gap-2"> <li key={`li-${listItems.items.length}`} className="flex items-center gap-2">
<span className={checked ? 'text-green-500' : 'text-zinc-500'}> <span className={checked ? 'text-green-500' : 'text-muted-foreground'}>
{checked ? '✓' : '○'} {checked ? '✓' : '○'}
</span> </span>
{parseInlineMarkdown(itemContent)} {parseInlineMarkdown(itemContent)}
@@ -282,7 +282,7 @@ export function renderMarkdown(content: string): React.ReactNode {
// Horizontal rule // Horizontal rule
if (line.match(/^[-*_]{3,}$/)) { if (line.match(/^[-*_]{3,}$/)) {
flushList(); flushList();
elements.push(<hr key={`hr-${elements.length}`} className="border-zinc-700 my-8" />); elements.push(<hr key={`hr-${elements.length}`} className="border-border my-8" />);
i++; i++;
continue; continue;
} }
@@ -291,7 +291,7 @@ export function renderMarkdown(content: string): React.ReactNode {
if (line.startsWith('> ')) { if (line.startsWith('> ')) {
flushList(); flushList();
elements.push( elements.push(
<blockquote key={`quote-${elements.length}`} className="border-l-4 border-[#697565] pl-4 my-4 text-zinc-400 italic"> <blockquote key={`quote-${elements.length}`} className="border-l-4 border-[#697565] pl-4 my-4 text-muted-foreground italic">
{parseInlineMarkdown(line.slice(2))} {parseInlineMarkdown(line.slice(2))}
</blockquote> </blockquote>
); );
@@ -309,7 +309,7 @@ export function renderMarkdown(content: string): React.ReactNode {
// Regular paragraph // Regular paragraph
flushList(); flushList();
elements.push( elements.push(
<p key={`p-${elements.length}`} className="text-zinc-300 mb-4 leading-relaxed"> <p key={`p-${elements.length}`} className="text-foreground/80 mb-4 leading-relaxed">
{parseInlineMarkdown(line)} {parseInlineMarkdown(line)}
</p> </p>
); );
@@ -320,5 +320,5 @@ export function renderMarkdown(content: string): React.ReactNode {
flushList(); flushList();
flushTable(); flushTable();
return <div className="prose prose-invert prose-zinc max-w-none">{elements}</div>; return <div className="prose prose-invert max-w-none">{elements}</div>;
} }
+4 -4
View File
@@ -151,7 +151,7 @@
{ {
"role": "Process Automation Specialist", "role": "Process Automation Specialist",
"company": "Everlast Consulting GmbH", "company": "Everlast Consulting GmbH",
"period": "12/2024 - HEUTE", "period": "12/2025 - HEUTE",
"highlights": [ "highlights": [
"Entwicklung von KI-Agenten mit n8n und Zapier", "Entwicklung von KI-Agenten mit n8n und Zapier",
"Aufbau von Web-Scraping-Lösungen", "Aufbau von Web-Scraping-Lösungen",
@@ -161,7 +161,7 @@
{ {
"role": "Consultant Digital Solutions", "role": "Consultant Digital Solutions",
"company": "Ritter Digital GmbH", "company": "Ritter Digital GmbH",
"period": "08/2023 - 11/2024", "period": "08/2023 - 11/2025",
"highlights": [ "highlights": [
"Backend-Services auf dedizierten Servern", "Backend-Services auf dedizierten Servern",
"KI-Integration via Power Automate", "KI-Integration via Power Automate",
@@ -327,7 +327,7 @@
"positions": [ "positions": [
{ {
"title": "Process Automation Specialist", "title": "Process Automation Specialist",
"period": "12/2024 - Heute", "period": "12/2025 - Heute",
"company": "Everlast Consulting GmbH", "company": "Everlast Consulting GmbH",
"location": "Köln", "location": "Köln",
"highlights": [ "highlights": [
@@ -338,7 +338,7 @@
}, },
{ {
"title": "Consultant Digital Solutions", "title": "Consultant Digital Solutions",
"period": "08/2023 - 11/2024", "period": "08/2023 - 11/2025",
"company": "Ritter Digital GmbH", "company": "Ritter Digital GmbH",
"location": "Köln", "location": "Köln",
"highlights": [ "highlights": [
+4 -4
View File
@@ -165,7 +165,7 @@
{ {
"role": "Process Automation Specialist", "role": "Process Automation Specialist",
"company": "Everlast Consulting GmbH", "company": "Everlast Consulting GmbH",
"period": "12/2024 - PRESENT", "period": "12/2025 - PRESENT",
"highlights": [ "highlights": [
"Development of AI agents with n8n and Zapier", "Development of AI agents with n8n and Zapier",
"Building web scraping solutions", "Building web scraping solutions",
@@ -175,7 +175,7 @@
{ {
"role": "Consultant Digital Solutions", "role": "Consultant Digital Solutions",
"company": "Ritter Digital GmbH", "company": "Ritter Digital GmbH",
"period": "08/2023 - 11/2024", "period": "08/2023 - 11/2025",
"highlights": [ "highlights": [
"Backend services on dedicated servers", "Backend services on dedicated servers",
"AI integration via Power Automate", "AI integration via Power Automate",
@@ -342,7 +342,7 @@
"positions": [ "positions": [
{ {
"title": "Process Automation Specialist", "title": "Process Automation Specialist",
"period": "12/2024 - Present", "period": "12/2025 - Present",
"company": "Everlast Consulting GmbH", "company": "Everlast Consulting GmbH",
"location": "Cologne", "location": "Cologne",
"highlights": [ "highlights": [
@@ -353,7 +353,7 @@
}, },
{ {
"title": "Consultant Digital Solutions", "title": "Consultant Digital Solutions",
"period": "08/2023 - 11/2024", "period": "08/2023 - 11/2025",
"company": "Ritter Digital GmbH", "company": "Ritter Digital GmbH",
"location": "Cologne", "location": "Cologne",
"highlights": [ "highlights": [
+4 -4
View File
@@ -172,7 +172,7 @@
{ {
"role": "Process Automation Specialist", "role": "Process Automation Specialist",
"company": "Everlast Consulting GmbH", "company": "Everlast Consulting GmbH",
"period": "12/2024 - DANAS", "period": "12/2025 - DANAS",
"highlights": [ "highlights": [
"Razvoj AI agenata sa n8n i Zapier", "Razvoj AI agenata sa n8n i Zapier",
"Izgradnja web scraping rešenja", "Izgradnja web scraping rešenja",
@@ -182,7 +182,7 @@
{ {
"role": "Consultant Digital Solutions", "role": "Consultant Digital Solutions",
"company": "Ritter Digital GmbH", "company": "Ritter Digital GmbH",
"period": "08/2023 - 11/2024", "period": "08/2023 - 11/2025",
"highlights": [ "highlights": [
"Backend servisi na namenskim serverima", "Backend servisi na namenskim serverima",
"AI integracija preko Power Automate", "AI integracija preko Power Automate",
@@ -349,7 +349,7 @@
"positions": [ "positions": [
{ {
"title": "Process Automation Specialist", "title": "Process Automation Specialist",
"period": "12/2024 - Danas", "period": "12/2025 - Danas",
"company": "Everlast Consulting GmbH", "company": "Everlast Consulting GmbH",
"location": "Keln", "location": "Keln",
"highlights": [ "highlights": [
@@ -360,7 +360,7 @@
}, },
{ {
"title": "Consultant Digital Solutions", "title": "Consultant Digital Solutions",
"period": "08/2023 - 11/2024", "period": "08/2023 - 11/2025",
"company": "Ritter Digital GmbH", "company": "Ritter Digital GmbH",
"location": "Keln", "location": "Keln",
"highlights": [ "highlights": [
-30
View File
@@ -1,18 +1,7 @@
import { chain, chainMatch, isPageRequest, csp } from '@next-safe/middleware'; import { chain, chainMatch, isPageRequest, csp } from '@next-safe/middleware';
import { NextRequest, NextResponse } from 'next/server';
import createMiddleware from 'next-intl/middleware'; import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from './i18n/config'; import { locales, defaultLocale } from './i18n/config';
const CSRF_TOKEN_COOKIE_NAME = 'csrf_token';
const CSRF_TOKEN_LENGTH = 32;
// Edge-compatible random bytes generation
function generateToken(length: number): string {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes)).replace(/[+/=]/g, '');
}
const handleI18nRouting = createMiddleware({ const handleI18nRouting = createMiddleware({
locales, locales,
defaultLocale, defaultLocale,
@@ -34,25 +23,6 @@ const securityMiddleware = csp({
}, },
}); });
// Custom middleware to handle CSRF tokens
function csrfMiddleware(request: NextRequest, response: NextResponse): NextResponse {
const existingToken = request.cookies.get(CSRF_TOKEN_COOKIE_NAME);
if (!existingToken) {
const newToken = generateToken(CSRF_TOKEN_LENGTH);
response.cookies.set(CSRF_TOKEN_COOKIE_NAME, newToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24, // 24 hours
path: '/',
});
}
return response;
}
// Use chain to combine i18n middleware with security middleware // Use chain to combine i18n middleware with security middleware
// First run i18n, then apply CSP only on page requests // First run i18n, then apply CSP only on page requests
export default chain( export default chain(
+1 -1
View File
@@ -27,7 +27,7 @@ export const CONTACT = {
export const CURRENT_ROLE = { export const CURRENT_ROLE = {
company: "Everlast Consulting GmbH", company: "Everlast Consulting GmbH",
position: "Process Automation Specialist", position: "Process Automation Specialist",
since: "2024-12" since: "2025-12"
} as const; } as const;
/** /**