Implementierung - SEO - 01.08.2025
This commit is contained in:
@@ -15,7 +15,8 @@
|
|||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(pnpm add:*)",
|
"Bash(pnpm add:*)",
|
||||||
"Bash(pnpm approve-builds:*)",
|
"Bash(pnpm approve-builds:*)",
|
||||||
"Bash(curl:*)"
|
"Bash(curl:*)",
|
||||||
|
"Bash(npx tailwindcss:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ define(['./workbox-be0f3d84'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.8k8mhvbe2f8"
|
"revision": "0.2p1auafpcng"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+30
-15
@@ -1,13 +1,28 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de" class="bg-zinc-900">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/logo.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<!-- Google Tag Manager -->
|
||||||
|
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||||
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||||
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
|
})(window,document,'script','dataLayer','GTM-NQK6ZV7N');</script>
|
||||||
|
<!-- End Google Tag Manager -->
|
||||||
|
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<meta name="description" content="Damjan Savić - Fullstack Entwickler spezialisiert auf Python, JavaScript, React, Next.js, TypeScript, KI/AI mit OLLAMA, ERP-Systeme und Prozessautomatisierung." />
|
<meta name="description" content="Damjan Savić - Fullstack Entwickler spezialisiert auf Python, JavaScript, React, Next.js, TypeScript, KI/AI mit OLLAMA, ERP-Systeme und Prozessautomatisierung." />
|
||||||
<meta name="keywords" content="Damjan Savić, Fullstack Entwickler, Python, JavaScript, React, Next.js, TypeScript, Electron, KI, AI, OLLAMA, ERP, E-Commerce, Prozessautomatisierung" />
|
<meta name="keywords" content="Damjan Savić, Fullstack Entwickler, Python, JavaScript, React, Next.js, TypeScript, Electron, KI, AI, OLLAMA, ERP, E-Commerce, Prozessautomatisierung" />
|
||||||
<meta name="author" content="Damjan Savić" />
|
<meta name="author" content="Damjan Savić" />
|
||||||
<meta name="theme-color" content="#18181B" />
|
<meta name="theme-color" content="#18181B" />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
<meta name="googlebot" content="index, follow" />
|
||||||
|
|
||||||
|
<!-- Google Search Console Verification - Ersetze mit deinem eigenen Code -->
|
||||||
|
<!-- <meta name="google-site-verification" content="YOUR_VERIFICATION_CODE" /> -->
|
||||||
|
|
||||||
<title>Damjan Savić - Fullstack Entwickler | Python, JavaScript, KI</title>
|
<title>Damjan Savić - Fullstack Entwickler | Python, JavaScript, KI</title>
|
||||||
|
|
||||||
<!-- Performance Optimizations -->
|
<!-- Performance Optimizations -->
|
||||||
@@ -23,8 +38,13 @@
|
|||||||
<meta property="og:description" content="Erfahrener Fullstack Entwickler spezialisiert auf Python, JavaScript, React, Next.js, TypeScript, KI/AI mit OLLAMA, ERP-Systeme und Prozessautomatisierung." />
|
<meta property="og:description" content="Erfahrener Fullstack Entwickler spezialisiert auf Python, JavaScript, React, Next.js, TypeScript, KI/AI mit OLLAMA, ERP-Systeme und Prozessautomatisierung." />
|
||||||
<meta property="og:url" content="https://damjan-savic.com" />
|
<meta property="og:url" content="https://damjan-savic.com" />
|
||||||
<meta property="og:image" content="https://damjan-savic.com/og-image.webp" />
|
<meta property="og:image" content="https://damjan-savic.com/og-image.webp" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta property="og:image:alt" content="Damjan Savić - Fullstack Entwickler Portfolio" />
|
||||||
<meta property="og:site_name" content="Damjan Savić Developer Portfolio" />
|
<meta property="og:site_name" content="Damjan Savić Developer Portfolio" />
|
||||||
<meta property="og:locale" content="de_DE" />
|
<meta property="og:locale" content="de_DE" />
|
||||||
|
<meta property="og:locale:alternate" content="en_US" />
|
||||||
|
<meta property="og:locale:alternate" content="sr_RS" />
|
||||||
|
|
||||||
<!-- Twitter Card Meta Tags -->
|
<!-- Twitter Card Meta Tags -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
@@ -41,7 +61,7 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
background-color: #18181B !important;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent initial flicker */
|
/* Prevent initial flicker */
|
||||||
@@ -101,20 +121,15 @@
|
|||||||
<!-- Preload Font -->
|
<!-- Preload Font -->
|
||||||
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
|
|
||||||
<!-- Google Analytics -->
|
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-N0PZBL7X18"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', 'G-N0PZBL7X18');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Preload wichtiger Assets -->
|
<!-- Preload wichtiger Assets -->
|
||||||
<link rel="preload" href="/logo.png" as="image" />
|
<link rel="preload" href="/logo.svg" as="image" />
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-zinc-900">
|
<body>
|
||||||
<div id="root" class="bg-zinc-900"></div>
|
<!-- Google Tag Manager (noscript) -->
|
||||||
|
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-NQK6ZV7N"
|
||||||
|
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||||
|
<!-- End Google Tag Manager (noscript) -->
|
||||||
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
|
||||||
|
<!-- Generator: Adobe Illustrator 29.7.0, SVG Export Plug-In . SVG Version: 2.1.1 Build 138) -->
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.st0 {
|
||||||
|
fill: #d8d9d8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="st0" d="M965.46,94.71l-461.75,791.05L34.81,94.71h930.65M1000,75H0l17.59,29.68,468.9,791.05,17.35,29.27,17.15-29.39L982.74,104.56l17.26-29.56h0Z"/>
|
||||||
|
<g>
|
||||||
|
<polygon class="st0" points="755.12 212.27 524.03 212.33 544.73 247.54 734.42 247.51 830.87 247.51 782.42 329.91 759.95 368.14 687.8 490.91 639.56 572.96 591.35 490.91 519.2 368.2 615.65 368.17 639.56 408.87 663.47 368.17 685.97 329.91 593.15 329.94 496.7 329.94 448.28 247.57 427.58 212.33 379.13 129.93 475.58 129.93 803.57 129.87 900.03 129.84 851.57 212.27 755.12 212.27"/>
|
||||||
|
<polygon class="st0" points="618.8 608.26 570.59 690.3 548.33 728.18 500.09 810.22 451.88 728.18 240.28 368.23 217.81 330 169.39 247.6 265.85 247.6 314.27 329.97 336.74 368.23 500.09 646.14 522.35 608.26 381.23 368.2 358.76 329.97 310.34 247.57 289.61 212.36 245.11 212.36 148.69 212.39 100.24 129.99 196.69 129.96 241.18 129.96 337.64 129.93 386.06 212.33 406.76 247.57 455.21 329.97 477.68 368.2 570.59 526.24 618.8 608.26"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -7,13 +7,13 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
||||||
const SOURCE_IMAGE = path.join(PUBLIC_DIR, 'logo.png');
|
const SOURCE_IMAGE = path.join(PUBLIC_DIR, 'logo.svg');
|
||||||
|
|
||||||
async function generateIcons() {
|
async function generateIcons() {
|
||||||
try {
|
try {
|
||||||
// Check if source image exists
|
// Check if source image exists
|
||||||
if (!fs.existsSync(SOURCE_IMAGE)) {
|
if (!fs.existsSync(SOURCE_IMAGE)) {
|
||||||
console.error('Source logo.png not found in public directory');
|
console.error('Source logo.svg not found in public directory');
|
||||||
|
|
||||||
// Create placeholder icons
|
// Create placeholder icons
|
||||||
const svg = `
|
const svg = `
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ import PageTransition from './components/PageTransition';
|
|||||||
import { ScrollProvider } from './components/ScrollContext';
|
import { ScrollProvider } from './components/ScrollContext';
|
||||||
import DebugOverlay from './components/DebugOverlay';
|
import DebugOverlay from './components/DebugOverlay';
|
||||||
import { logPageView, isCookieCategoryEnabled } from './utils/analytics';
|
import { logPageView, isCookieCategoryEnabled } from './utils/analytics';
|
||||||
|
import { useScrollTracking } from './hooks/useScrollTracking';
|
||||||
import './styles/scrollbar.css';
|
import './styles/scrollbar.css';
|
||||||
|
|
||||||
// RouteTracker Component für präzises Seiten-Tracking - aber nur mit Zustimmung
|
// RouteTracker Component für präzises Seiten-Tracking - aber nur mit Zustimmung
|
||||||
const RouteTracker = () => {
|
const RouteTracker = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Scroll tracking aktivieren
|
||||||
|
useScrollTracking();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Nur tracken, wenn die Analytics-Cookies akzeptiert wurden
|
// Nur tracken, wenn die Analytics-Cookies akzeptiert wurden
|
||||||
|
|||||||
@@ -1,87 +1,26 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { trackFAQInteraction } from '../services/gtm';
|
||||||
interface FAQItem {
|
|
||||||
question: string;
|
|
||||||
answer: string;
|
|
||||||
category?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FAQSection() {
|
export function FAQSection() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [openItems, setOpenItems] = useState<number[]>([]);
|
const [openItem, setOpenItem] = useState<number | null>(null);
|
||||||
|
|
||||||
const faqs: FAQItem[] = i18n.language === 'de' ? [
|
const faqs = t('faq.questions', { returnObjects: true }) as Array<{
|
||||||
{
|
question: string;
|
||||||
question: "Wie kann ich Python für Prozessautomatisierung einsetzen?",
|
answer: string;
|
||||||
answer: "Python eignet sich hervorragend für Prozessautomatisierung durch Libraries wie Selenium für Web-Automation, Pandas für Datenverarbeitung und Schedule für zeitgesteuerte Aufgaben. Ich entwickle maßgeschneiderte Automatisierungslösungen für Ihre Geschäftsprozesse.",
|
category?: string;
|
||||||
category: "python"
|
}>;
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Was macht OLLAMA ideal für lokale KI-Entwicklung?",
|
|
||||||
answer: "OLLAMA ermöglicht das Ausführen großer Sprachmodelle lokal, was Datenschutz gewährleistet und API-Kosten reduziert. Ich spezialisiere mich auf die Integration von OLLAMA in Anwendungen für individuelle KI-Lösungen ohne Cloud-Abhängigkeiten.",
|
|
||||||
category: "ai"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Welche Vorteile bieten Electron Desktop Apps?",
|
|
||||||
answer: "Electron ermöglicht die Entwicklung plattformübergreifender Desktop-Anwendungen mit Web-Technologien. Sie erhalten eine native App für Windows, Mac und Linux aus einer einzigen Codebasis. Ideal für Tools mit komplexer UI und Offline-Funktionalität.",
|
|
||||||
category: "electron"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Wie lange dauert die Entwicklung einer Web-Anwendung?",
|
|
||||||
answer: "Die Entwicklungsdauer hängt vom Umfang ab. Ein MVP kann in 4-8 Wochen entwickelt werden, während komplexe Enterprise-Anwendungen 3-6 Monate benötigen. Ich arbeite agil und liefere in 2-Wochen-Sprints nutzbare Zwischenergebnisse.",
|
|
||||||
category: "general"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Können Sie bestehende Python-Skripte in Web-Apps umwandeln?",
|
|
||||||
answer: "Ja, ich kann Ihre Python-Skripte in moderne Web-Anwendungen mit FastAPI oder Django Backend und React Frontend transformieren. Dies ermöglicht Multi-User-Zugriff, bessere UI/UX und zentrale Datenverwaltung.",
|
|
||||||
category: "python"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Wie integrieren Sie KI in bestehende Geschäftsprozesse?",
|
|
||||||
answer: "Ich analysiere Ihre Workflows und identifiziere Automatisierungspotenziale. Mit OLLAMA oder anderen KI-Modellen entwickle ich Lösungen für Textanalyse, Dokumentenverarbeitung, Kundenservice-Automation oder Datenextraktion - alles datenschutzkonform on-premise.",
|
|
||||||
category: "ai"
|
|
||||||
}
|
|
||||||
] : [
|
|
||||||
{
|
|
||||||
question: "How can I use Python for process automation?",
|
|
||||||
answer: "Python excels at process automation through libraries like Selenium for web automation, Pandas for data processing, and Schedule for timed tasks. I develop custom automation solutions tailored to your business processes.",
|
|
||||||
category: "python"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "What makes OLLAMA ideal for local AI development?",
|
|
||||||
answer: "OLLAMA enables running large language models locally, ensuring data privacy and reducing API costs. I specialize in integrating OLLAMA into applications for custom AI solutions without cloud dependencies.",
|
|
||||||
category: "ai"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "What are the advantages of Electron desktop apps?",
|
|
||||||
answer: "Electron enables cross-platform desktop app development using web technologies. You get a native app for Windows, Mac, and Linux from a single codebase. Ideal for tools requiring complex UI and offline functionality.",
|
|
||||||
category: "electron"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "How long does web application development take?",
|
|
||||||
answer: "Development time depends on scope. An MVP can be developed in 4-8 weeks, while complex enterprise applications require 3-6 months. I work agile and deliver usable results in 2-week sprints.",
|
|
||||||
category: "general"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Can you convert existing Python scripts into web apps?",
|
|
||||||
answer: "Yes, I can transform your Python scripts into modern web applications with FastAPI or Django backend and React frontend. This enables multi-user access, better UI/UX, and centralized data management.",
|
|
||||||
category: "python"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "How do you integrate AI into existing business processes?",
|
|
||||||
answer: "I analyze your workflows and identify automation potential. Using OLLAMA or other AI models, I develop solutions for text analysis, document processing, customer service automation, or data extraction - all privacy-compliant on-premise.",
|
|
||||||
category: "ai"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const toggleItem = (index: number) => {
|
const toggleItem = (index: number) => {
|
||||||
setOpenItems(prev =>
|
const isOpening = openItem !== index;
|
||||||
prev.includes(index)
|
const question = faqs[index]?.question || '';
|
||||||
? prev.filter(i => i !== index)
|
|
||||||
: [...prev, index]
|
// Track FAQ interaction
|
||||||
);
|
trackFAQInteraction(question, isOpening ? 'open' : 'close');
|
||||||
|
|
||||||
|
setOpenItem(prev => prev === index ? null : index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const faqSchema = {
|
const faqSchema = {
|
||||||
@@ -105,7 +44,7 @@ export function FAQSection() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<h2 className="text-3xl font-bold text-white mb-8 text-center">
|
<h2 className="text-3xl font-bold text-white mb-8 text-center">
|
||||||
{t('faq.title', 'Frequently Asked Questions')}
|
{t('faq.title')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -117,13 +56,13 @@ export function FAQSection() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => toggleItem(index)}
|
onClick={() => toggleItem(index)}
|
||||||
className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-zinc-800/70 transition-colors"
|
className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-zinc-800/70 transition-colors"
|
||||||
aria-expanded={openItems.includes(index)}
|
aria-expanded={openItem === index}
|
||||||
aria-controls={`faq-answer-${index}`}
|
aria-controls={`faq-answer-${index}`}
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-medium text-white pr-4">
|
<h3 className="text-lg font-medium text-white pr-4">
|
||||||
{faq.question}
|
{faq.question}
|
||||||
</h3>
|
</h3>
|
||||||
{openItems.includes(index) ? (
|
{openItem === index ? (
|
||||||
<ChevronUp className="h-5 w-5 text-zinc-400 flex-shrink-0" />
|
<ChevronUp className="h-5 w-5 text-zinc-400 flex-shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown className="h-5 w-5 text-zinc-400 flex-shrink-0" />
|
<ChevronDown className="h-5 w-5 text-zinc-400 flex-shrink-0" />
|
||||||
@@ -133,7 +72,7 @@ export function FAQSection() {
|
|||||||
<div
|
<div
|
||||||
id={`faq-answer-${index}`}
|
id={`faq-answer-${index}`}
|
||||||
className={`overflow-hidden transition-all duration-300 ${
|
className={`overflow-hidden transition-all duration-300 ${
|
||||||
openItems.includes(index) ? 'max-h-96' : 'max-h-0'
|
openItem === index ? 'max-h-96' : 'max-h-0'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="px-6 py-4 text-zinc-300 border-t border-zinc-700">
|
<div className="px-6 py-4 text-zinc-300 border-t border-zinc-700">
|
||||||
@@ -146,12 +85,12 @@ export function FAQSection() {
|
|||||||
|
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<p className="text-zinc-400">
|
<p className="text-zinc-400">
|
||||||
{t('faq.moreQuestions', 'Have more questions?')}{' '}
|
{t('faq.moreQuestions')}{' '}
|
||||||
<a
|
<a
|
||||||
href="/contact"
|
href="/contact"
|
||||||
className="text-white hover:text-zinc-300 underline transition-colors"
|
className="text-white hover:text-zinc-300 underline transition-colors"
|
||||||
>
|
>
|
||||||
{t('faq.contactUs', 'Contact us')}
|
{t('faq.contactUs')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function FloatingPaths({ position }: { position: number }) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 pointer-events-none floating-paths" style={{ isolation: 'isolate', zIndex: 0 }}>
|
<div className="absolute inset-0 pointer-events-none floating-paths">
|
||||||
<svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="none" style={{ transform: 'translateZ(0)' }}>
|
<svg className="w-full h-full text-accent" viewBox="0 0 696 316" fill="none" style={{ transform: 'translateZ(0)' }}>
|
||||||
<title>Background Paths</title>
|
<title>Background Paths</title>
|
||||||
{paths.map((path) => (
|
{paths.map((path) => (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const Footer = () => {
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="relative bg-zinc-900 pt-8 pb-4 px-4">
|
<footer className="relative bg-zinc-800/50 backdrop-blur-sm border-t border-zinc-700/30 pt-8 pb-4 px-4">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="flex flex-col space-y-8">
|
<div className="flex flex-col space-y-8">
|
||||||
{/* Contact Section */}
|
{/* Contact Section */}
|
||||||
@@ -93,7 +93,7 @@ const Footer = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legal Section */}
|
{/* Legal Section */}
|
||||||
<div className="border-t border-zinc-800 pt-4">
|
<div className="border-t border-zinc-700/30 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-center">
|
<p className="text-zinc-400 text-xs text-center">
|
||||||
© {currentYear} Damjan Savić. {t('footer.legal.copyright')}
|
© {currentYear} Damjan Savić. {t('footer.legal.copyright')}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { FloatingPaths } from './FloatingPaths';
|
||||||
|
|
||||||
|
const GlobalBackground = () => {
|
||||||
|
const [scrollPosition, setScrollPosition] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setScrollPosition(window.scrollY * 0.001);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Background layer - behind everything */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-zinc-900"
|
||||||
|
style={{
|
||||||
|
zIndex: -2,
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Gradient overlay */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0"
|
||||||
|
style={{
|
||||||
|
zIndex: -1,
|
||||||
|
background: 'linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 50%, #1e1e1e 100%)',
|
||||||
|
opacity: 0.9
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* FloatingPaths layer */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0"
|
||||||
|
style={{
|
||||||
|
zIndex: -1,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingPaths position={scrollPosition} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test: Visible animated lines */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 pointer-events-none"
|
||||||
|
style={{ zIndex: -1 }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-full h-full"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
style={{ opacity: 0.3 }}
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0%"
|
||||||
|
y1="20%"
|
||||||
|
x2="100%"
|
||||||
|
y2="20%"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.2"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="y1"
|
||||||
|
values="20%;80%;20%"
|
||||||
|
dur="10s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="y2"
|
||||||
|
values="20%;80%;20%"
|
||||||
|
dur="10s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</line>
|
||||||
|
|
||||||
|
<line
|
||||||
|
x1="0%"
|
||||||
|
y1="50%"
|
||||||
|
x2="100%"
|
||||||
|
y2="50%"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.15"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="x1"
|
||||||
|
values="0%;100%;0%"
|
||||||
|
dur="15s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</line>
|
||||||
|
|
||||||
|
<line
|
||||||
|
x1="20%"
|
||||||
|
y1="0%"
|
||||||
|
x2="20%"
|
||||||
|
y2="100%"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.1"
|
||||||
|
>
|
||||||
|
<animate
|
||||||
|
attributeName="x1"
|
||||||
|
values="20%;80%;20%"
|
||||||
|
dur="12s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
<animate
|
||||||
|
attributeName="x2"
|
||||||
|
values="20%;80%;20%"
|
||||||
|
dur="12s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid overlay */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
zIndex: -1,
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(255,255,255,.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,.05) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '50px 50px',
|
||||||
|
opacity: 0.5
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlobalBackground;
|
||||||
@@ -20,6 +20,7 @@ import { Link, useLocation } from "react-router-dom";
|
|||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import { useScrollContext } from "./ScrollContext";
|
import { useScrollContext } from "./ScrollContext";
|
||||||
import { PerformanceMonitor } from "./PerformanceMonitor";
|
import { PerformanceMonitor } from "./PerformanceMonitor";
|
||||||
|
import GlobalBackground from "./GlobalBackground";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -122,21 +123,25 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
// Render with opacity 0 on initial mount to prevent flicker
|
// Render with opacity 0 on initial mount to prevent flicker
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900">
|
<div className="min-h-screen bg-transparent">
|
||||||
|
<GlobalBackground />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex flex-col relative">
|
<div className="min-h-screen flex flex-col relative">
|
||||||
|
{/* Global Background */}
|
||||||
|
<GlobalBackground />
|
||||||
|
|
||||||
{/* Navigation – oberste Ebene */}
|
{/* Navigation – oberste Ebene */}
|
||||||
<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-zinc-800/50 backdrop-blur-sm border-b border-zinc-700/30
|
||||||
${scrolled
|
${scrolled
|
||||||
? "bg-zinc-900/80 backdrop-blur-md shadow-lg shadow-zinc-900/50"
|
? "bg-zinc-800/70 backdrop-blur-md shadow-lg shadow-zinc-900/30"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
@@ -148,7 +153,7 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link to="/" className="flex items-center space-x-2 group shrink-0">
|
<Link to="/" className="flex items-center space-x-2 group shrink-0">
|
||||||
<motion.img
|
<motion.img
|
||||||
src="/logo.png"
|
src="/logo.svg"
|
||||||
alt=""
|
alt=""
|
||||||
className="h-8 w-auto"
|
className="h-8 w-auto"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
@@ -206,7 +211,7 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
animate={{ x: 0 }}
|
animate={{ x: 0 }}
|
||||||
exit={{ x: "100%" }}
|
exit={{ x: "100%" }}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||||
className="fixed right-0 top-0 h-full w-80 bg-zinc-900 shadow-xl md:hidden z-50 border-l border-zinc-800"
|
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"
|
||||||
>
|
>
|
||||||
{/* Sidebar Header mit Close-Button */}
|
{/* Sidebar Header mit Close-Button */}
|
||||||
<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-16 border-b border-zinc-800">
|
||||||
@@ -258,7 +263,7 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Hauptinhalt */}
|
{/* Hauptinhalt */}
|
||||||
<main className="flex-1 pt-2 relative z-30">{children}</main>
|
<main className="flex-1 pt-2 relative z-10 bg-transparent">{children}</main>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const Logo = () => {
|
|||||||
>
|
>
|
||||||
{!imageError ? (
|
{!imageError ? (
|
||||||
<img
|
<img
|
||||||
src="/logo.png"
|
src="/logo.svg"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
className="w-full h-full object-contain filter brightness-200"
|
className="w-full h-full object-contain filter brightness-200"
|
||||||
onError={() => setImageError(true)}
|
onError={() => setImageError(true)}
|
||||||
@@ -123,7 +123,7 @@ const PageTransition = ({ children }: PageTransitionProps) => {
|
|||||||
{isTransitioning && (
|
{isTransitioning && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="transition-overlay"
|
key="transition-overlay"
|
||||||
className="fixed inset-0 bg-zinc-900 flex items-center justify-center page-transition-active"
|
className="fixed inset-0 bg-zinc-900/95 backdrop-blur-sm flex items-center justify-center page-transition-active"
|
||||||
style={{ zIndex: 9999 }}
|
style={{ zIndex: 9999 }}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { trackScrollDepth } from '../services/gtm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to track scroll depth for GTM
|
||||||
|
*/
|
||||||
|
export const useScrollTracking = () => {
|
||||||
|
const trackedDepths = useRef(new Set<number>());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let scrollTimeout: NodeJS.Timeout;
|
||||||
|
const depths = [25, 50, 75, 90, 100];
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
clearTimeout(scrollTimeout);
|
||||||
|
|
||||||
|
scrollTimeout = setTimeout(() => {
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const documentHeight = document.documentElement.scrollHeight;
|
||||||
|
const scrollTop = window.scrollY;
|
||||||
|
const scrollPercentage = Math.round(
|
||||||
|
((scrollTop + windowHeight) / documentHeight) * 100
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track each depth milestone only once
|
||||||
|
depths.forEach(depth => {
|
||||||
|
if (scrollPercentage >= depth && !trackedDepths.current.has(depth)) {
|
||||||
|
trackedDepths.current.add(depth);
|
||||||
|
trackScrollDepth(depth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100); // Debounce scroll events
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
// Check initial scroll position
|
||||||
|
handleScroll();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
clearTimeout(scrollTimeout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset tracked depths when route changes
|
||||||
|
useEffect(() => {
|
||||||
|
trackedDepths.current.clear();
|
||||||
|
}, [window.location.pathname]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export const faq = {
|
||||||
|
title: 'Häufig gestellte Fragen',
|
||||||
|
moreQuestions: 'Haben Sie weitere Fragen?',
|
||||||
|
contactUs: 'Kontaktieren Sie uns',
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "Wie kann ich Python für Prozessautomatisierung einsetzen?",
|
||||||
|
answer: "Python eignet sich hervorragend für Prozessautomatisierung durch Libraries wie Selenium für Web-Automation, Pandas für Datenverarbeitung und Schedule für zeitgesteuerte Aufgaben. Ich entwickle maßgeschneiderte Automatisierungslösungen für Ihre Geschäftsprozesse.",
|
||||||
|
category: "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Was macht OLLAMA ideal für lokale KI-Entwicklung?",
|
||||||
|
answer: "OLLAMA ermöglicht das Ausführen großer Sprachmodelle lokal, was Datenschutz gewährleistet und API-Kosten reduziert. Ich spezialisiere mich auf die Integration von OLLAMA in Anwendungen für individuelle KI-Lösungen ohne Cloud-Abhängigkeiten.",
|
||||||
|
category: "ai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Welche Vorteile bieten Electron Desktop Apps?",
|
||||||
|
answer: "Electron ermöglicht die Entwicklung plattformübergreifender Desktop-Anwendungen mit Web-Technologien. Sie erhalten eine native App für Windows, Mac und Linux aus einer einzigen Codebasis. Ideal für Tools mit komplexer UI und Offline-Funktionalität.",
|
||||||
|
category: "electron"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Wie lange dauert die Entwicklung einer Web-Anwendung?",
|
||||||
|
answer: "Die Entwicklungsdauer hängt vom Umfang ab. Ein MVP kann in 4-8 Wochen entwickelt werden, während komplexe Enterprise-Anwendungen 3-6 Monate benötigen. Ich arbeite agil und liefere in 2-Wochen-Sprints nutzbare Zwischenergebnisse.",
|
||||||
|
category: "general"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Können Sie bestehende Python-Skripte in Web-Apps umwandeln?",
|
||||||
|
answer: "Ja, ich kann Ihre Python-Skripte in moderne Web-Anwendungen mit FastAPI oder Django Backend und React Frontend transformieren. Dies ermöglicht Multi-User-Zugriff, bessere UI/UX und zentrale Datenverwaltung.",
|
||||||
|
category: "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Wie integrieren Sie KI in bestehende Geschäftsprozesse?",
|
||||||
|
answer: "Ich analysiere Ihre Workflows und identifiziere Automatisierungspotenziale. Mit OLLAMA oder anderen KI-Modellen entwickle ich Lösungen für Textanalyse, Dokumentenverarbeitung, Kundenservice-Automation oder Datenextraktion - alles datenschutzkonform on-premise.",
|
||||||
|
category: "ai"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -29,6 +29,9 @@ import { navigation } from './navigation';
|
|||||||
// Footer translations
|
// Footer translations
|
||||||
import footer from './footer';
|
import footer from './footer';
|
||||||
|
|
||||||
|
// FAQ translations
|
||||||
|
import { faq } from './faq';
|
||||||
|
|
||||||
// Zusammenführung aller Übersetzungsobjekte
|
// Zusammenführung aller Übersetzungsobjekte
|
||||||
const translations = {
|
const translations = {
|
||||||
// Pages
|
// Pages
|
||||||
@@ -77,6 +80,7 @@ const translations = {
|
|||||||
},
|
},
|
||||||
meta,
|
meta,
|
||||||
navigation,
|
navigation,
|
||||||
|
faq,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Default-Export: komplettes Übersetzungsobjekt
|
// Default-Export: komplettes Übersetzungsobjekt
|
||||||
@@ -111,4 +115,5 @@ export {
|
|||||||
common,
|
common,
|
||||||
meta,
|
meta,
|
||||||
navigation,
|
navigation,
|
||||||
|
faq,
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export const faq = {
|
||||||
|
title: 'Frequently Asked Questions',
|
||||||
|
moreQuestions: 'Have more questions?',
|
||||||
|
contactUs: 'Contact us',
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "How can I use Python for process automation?",
|
||||||
|
answer: "Python excels at process automation through libraries like Selenium for web automation, Pandas for data processing, and Schedule for timed tasks. I develop custom automation solutions tailored to your business processes.",
|
||||||
|
category: "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What makes OLLAMA ideal for local AI development?",
|
||||||
|
answer: "OLLAMA enables running large language models locally, ensuring data privacy and reducing API costs. I specialize in integrating OLLAMA into applications for custom AI solutions without cloud dependencies.",
|
||||||
|
category: "ai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "What are the advantages of Electron desktop apps?",
|
||||||
|
answer: "Electron enables cross-platform desktop app development using web technologies. You get a native app for Windows, Mac, and Linux from a single codebase. Ideal for tools requiring complex UI and offline functionality.",
|
||||||
|
category: "electron"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How long does web application development take?",
|
||||||
|
answer: "Development time depends on scope. An MVP can be developed in 4-8 weeks, while complex enterprise applications require 3-6 months. I work agile and deliver usable results in 2-week sprints.",
|
||||||
|
category: "general"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Can you convert existing Python scripts into web apps?",
|
||||||
|
answer: "Yes, I can transform your Python scripts into modern web applications with FastAPI or Django backend and React frontend. This enables multi-user access, better UI/UX, and centralized data management.",
|
||||||
|
category: "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How do you integrate AI into existing business processes?",
|
||||||
|
answer: "I analyze your workflows and identify automation potential. Using OLLAMA or other AI models, I develop solutions for text analysis, document processing, customer service automation, or data extraction - all privacy-compliant on-premise.",
|
||||||
|
category: "ai"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -29,6 +29,9 @@ import { navigation } from './navigation';
|
|||||||
// Footer translations
|
// Footer translations
|
||||||
import footer from './footer';
|
import footer from './footer';
|
||||||
|
|
||||||
|
// FAQ translations
|
||||||
|
import { faq } from './faq';
|
||||||
|
|
||||||
// Merging all translation objects
|
// Merging all translation objects
|
||||||
const translations = {
|
const translations = {
|
||||||
// Pages
|
// Pages
|
||||||
@@ -77,6 +80,7 @@ const translations = {
|
|||||||
},
|
},
|
||||||
meta,
|
meta,
|
||||||
navigation,
|
navigation,
|
||||||
|
faq,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Default export: complete translations object
|
// Default export: complete translations object
|
||||||
@@ -111,4 +115,5 @@ export {
|
|||||||
common,
|
common,
|
||||||
meta,
|
meta,
|
||||||
navigation,
|
navigation,
|
||||||
|
faq,
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
export const faq = {
|
||||||
|
title: 'Često postavljana pitanja',
|
||||||
|
moreQuestions: 'Imate još pitanja?',
|
||||||
|
contactUs: 'Kontaktirajte nas',
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "Kako mogu da koristim Python za automatizaciju procesa?",
|
||||||
|
answer: "Python je odličan za automatizaciju procesa kroz biblioteke kao što su Selenium za web automatizaciju, Pandas za obradu podataka i Schedule za vremenski zakazane zadatke. Razvijam prilagođena rešenja za automatizaciju vaših poslovnih procesa.",
|
||||||
|
category: "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Šta čini OLLAMA idealnim za lokalni razvoj veštačke inteligencije?",
|
||||||
|
answer: "OLLAMA omogućava pokretanje velikih jezičkih modela lokalno, što garantuje privatnost podataka i smanjuje troškove API-ja. Specijalizovan sam za integraciju OLLAMA-e u aplikacije za prilagođena AI rešenja bez zavisnosti od oblaka.",
|
||||||
|
category: "ai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Koje su prednosti Electron desktop aplikacija?",
|
||||||
|
answer: "Electron omogućava razvoj desktop aplikacija za više platformi koristeći web tehnologije. Dobijate nativnu aplikaciju za Windows, Mac i Linux iz jedne baze koda. Idealno za alate koji zahtevaju kompleksan UI i offline funkcionalnost.",
|
||||||
|
category: "electron"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Koliko dugo traje razvoj web aplikacije?",
|
||||||
|
answer: "Vreme razvoja zavisi od obima. MVP može biti razvijen za 4-8 nedelja, dok kompleksne enterprise aplikacije zahtevaju 3-6 meseci. Radim agilno i isporučujem upotrebljive rezultate u dvonedeljnim sprintovima.",
|
||||||
|
category: "general"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Možete li konvertovati postojeće Python skripte u web aplikacije?",
|
||||||
|
answer: "Da, mogu transformisati vaše Python skripte u moderne web aplikacije sa FastAPI ili Django backend-om i React frontend-om. Ovo omogućava multi-korisnički pristup, bolji UI/UX i centralizovano upravljanje podacima.",
|
||||||
|
category: "python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Kako integrisati veštačku inteligenciju u postojeće poslovne procese?",
|
||||||
|
answer: "Analiziram vaše radne tokove i identifikujem potencijal za automatizaciju. Koristeći OLLAMA ili druge AI modele, razvijam rešenja za analizu teksta, obradu dokumenata, automatizaciju korisničke podrške ili ekstrakciju podataka - sve u skladu sa privatnošću podataka on-premise.",
|
||||||
|
category: "ai"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -29,6 +29,9 @@ import { navigation } from './navigation';
|
|||||||
// Footer prevodi
|
// Footer prevodi
|
||||||
import footer from './footer';
|
import footer from './footer';
|
||||||
|
|
||||||
|
// FAQ prevodi
|
||||||
|
import { faq } from './faq';
|
||||||
|
|
||||||
// Objedinjavanje svih prevoda
|
// Objedinjavanje svih prevoda
|
||||||
const translations = {
|
const translations = {
|
||||||
// Stranice
|
// Stranice
|
||||||
@@ -77,6 +80,7 @@ const translations = {
|
|||||||
},
|
},
|
||||||
meta,
|
meta,
|
||||||
navigation,
|
navigation,
|
||||||
|
faq,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Default izvoz: kompletan objekat prevoda
|
// Default izvoz: kompletan objekat prevoda
|
||||||
@@ -111,4 +115,5 @@ export {
|
|||||||
common,
|
common,
|
||||||
meta,
|
meta,
|
||||||
navigation,
|
navigation,
|
||||||
|
faq,
|
||||||
};
|
};
|
||||||
+36
-1
@@ -19,8 +19,17 @@ html.fonts-fallback body {
|
|||||||
font-family: Inter, system-ui, sans-serif;
|
font-family: Inter, system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground antialiased;
|
@apply text-foreground antialiased;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
@@ -78,6 +87,32 @@ html.fonts-fallback body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes slide-right {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-down {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-right {
|
||||||
|
animation: slide-right 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-down {
|
||||||
|
animation: slide-down 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* Prevent hover flickering */
|
/* Prevent hover flickering */
|
||||||
* {
|
* {
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { FloatingPaths } from '../../../components/FloatingPaths';
|
|
||||||
import { Terminal, Globe2, Code2 } from 'lucide-react';
|
import { Terminal, Globe2, Code2 } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -18,28 +17,7 @@ const Hero = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-screen bg-zinc-900 text-white overflow-hidden">
|
<section className="relative min-h-screen text-white overflow-hidden">
|
||||||
{/* Floating Paths Background */}
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<FloatingPaths position={1} />
|
|
||||||
<FloatingPaths position={-1} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Concentric Circles Background */}
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
{[1, 2, 3, 4, 5].map((index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="absolute rounded-full border border-zinc-800"
|
|
||||||
style={{
|
|
||||||
width: `${index * 20}%`,
|
|
||||||
height: `${index * 20}%`,
|
|
||||||
opacity: 0.1
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<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 */}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const Skills = () => {
|
|||||||
const allSkills = skillGroups.flatMap(group => group.items);
|
const allSkills = skillGroups.flatMap(group => group.items);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-zinc-900">
|
<section className="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">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const Workflow = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="relative w-full bg-zinc-900 py-24 px-4 sm:px-6 lg:px-8"
|
className="relative w-full py-24 px-4 sm:px-6 lg:px-8"
|
||||||
aria-label={t('pages.about.workflow.aria.workflowSection')}
|
aria-label={t('pages.about.workflow.aria.workflowSection')}
|
||||||
>
|
>
|
||||||
{/* Section Title and Description */}
|
{/* Section Title and Description */}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Calendar, ArrowLeft, Tag, Loader2, Folder } from 'lucide-react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import PageTransition from '../../../components/PageTransition';
|
import PageTransition from '../../../components/PageTransition';
|
||||||
import SEO from '../../../components/SEO';
|
import SEO from '../../../components/SEO';
|
||||||
|
import { trackBlogPostView } from '../../../services/gtm';
|
||||||
|
|
||||||
import { blog as blogDe } from '../../../i18n/locales/de/blog/';
|
import { blog as blogDe } from '../../../i18n/locales/de/blog/';
|
||||||
import { blog as blogEn } from '../../../i18n/locales/en/blog';
|
import { blog as blogEn } from '../../../i18n/locales/en/blog';
|
||||||
@@ -120,6 +121,8 @@ const BlogPost: React.FC = () => {
|
|||||||
...postData,
|
...postData,
|
||||||
slug,
|
slug,
|
||||||
});
|
});
|
||||||
|
// Track blog post view
|
||||||
|
trackBlogPostView(postData.title, postData.category);
|
||||||
} else {
|
} else {
|
||||||
// Statt eine Exception zu werfen, setzen wir den Fehler direkt.
|
// Statt eine Exception zu werfen, setzen wir den Fehler direkt.
|
||||||
setError(new Error('Post not found'));
|
setError(new Error('Post not found'));
|
||||||
@@ -168,7 +171,7 @@ const BlogPost: React.FC = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@@ -183,7 +186,7 @@ const BlogPost: React.FC = () => {
|
|||||||
|
|
||||||
if (error || !post) {
|
if (error || !post) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -207,10 +210,43 @@ const BlogPost: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Article Schema for SEO
|
||||||
|
const articleSchema = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Article",
|
||||||
|
"headline": post.title,
|
||||||
|
"description": post.excerpt,
|
||||||
|
"datePublished": post.date,
|
||||||
|
"dateModified": post.date,
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Damjan Savić",
|
||||||
|
"url": "https://damjan-savic.com"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Damjan Savić",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://damjan-savic.com/logo.svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mainEntityOfPage": {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": `https://damjan-savic.com/blog/${post.slug}`
|
||||||
|
},
|
||||||
|
"articleSection": post.category ? blog.categories[post.category] : undefined,
|
||||||
|
"keywords": post.tags?.join(', ')
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<SEO title={`${post.title} - Blog`} description={post.excerpt} />
|
<SEO title={`${post.title} - Blog`} description={post.excerpt} />
|
||||||
<main className="min-h-screen bg-zinc-900">
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
|
||||||
|
/>
|
||||||
|
<main className="min-h-screen">
|
||||||
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
|
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
|
||||||
<motion.div variants={containerVariants} initial="hidden" animate="visible">
|
<motion.div variants={containerVariants} initial="hidden" animate="visible">
|
||||||
{/* Back Navigation */}
|
{/* Back Navigation */}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const BlogPage: React.FC = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@@ -87,7 +87,7 @@ const BlogPage: React.FC = () => {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -110,7 +110,7 @@ const BlogPage: React.FC = () => {
|
|||||||
description={metaDescription}
|
description={metaDescription}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
/>
|
/>
|
||||||
<main className="min-h-screen bg-zinc-900">
|
<main className="min-h-screen">
|
||||||
<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 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Label } from '../../../components/Label';
|
|||||||
import { Button } from '../../../components/Button';
|
import { Button } from '../../../components/Button';
|
||||||
import { Alert, AlertDescription } from '../../../components/Alert';
|
import { Alert, AlertDescription } from '../../../components/Alert';
|
||||||
import { Loader2, Send, CheckCircle2, Mail } from 'lucide-react';
|
import { Loader2, Send, CheckCircle2, Mail } from 'lucide-react';
|
||||||
|
import { trackContactFormSubmit } from '../../../services/gtm';
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -72,6 +73,13 @@ const ContactForm = () => {
|
|||||||
setSubmitStatus('idle');
|
setSubmitStatus('idle');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Track form submission
|
||||||
|
trackContactFormSubmit({
|
||||||
|
name: formData.name,
|
||||||
|
email: formData.email,
|
||||||
|
subject: 'Contact Form'
|
||||||
|
});
|
||||||
|
|
||||||
// Simuliere einen API-Aufruf
|
// Simuliere einen API-Aufruf
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
setSubmitStatus('success');
|
setSubmitStatus('success');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const ContactPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<div className="min-h-screen bg-zinc-900">
|
<div className="min-h-screen">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="py-24 text-center">
|
<div className="py-24 text-center">
|
||||||
<motion.h1
|
<motion.h1
|
||||||
|
|||||||
@@ -1,88 +1,119 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { MapPin, Briefcase, GraduationCap, Code, Award } from 'lucide-react';
|
||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const highlights = [
|
||||||
|
{ icon: <MapPin className="w-4 h-4" />, label: "Köln, Deutschland" },
|
||||||
|
{ icon: <Briefcase className="w-4 h-4" />, label: "5+ Jahre Erfahrung" },
|
||||||
|
{ icon: <Code className="w-4 h-4" />, label: "Full-Stack Developer" },
|
||||||
|
{ icon: <Award className="w-4 h-4" />, label: "ERP Spezialist" }
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-zinc-900">
|
<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 relative">
|
||||||
<motion.div
|
<motion.div
|
||||||
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="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"
|
className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"
|
||||||
style={{
|
|
||||||
transform: 'translateZ(0)',
|
|
||||||
willChange: 'opacity, transform'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Image Section */}
|
{/* Image Section - Simplified */}
|
||||||
<div className="relative">
|
<motion.div
|
||||||
<motion.div
|
className="relative"
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, x: -50 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
className="rounded-3xl overflow-hidden aspect-square"
|
>
|
||||||
style={{
|
{/* Main Image Container */}
|
||||||
transform: 'translateZ(0)',
|
<div className="relative rounded-3xl overflow-hidden aspect-square bg-zinc-800/30 backdrop-blur-sm">
|
||||||
willChange: 'opacity, transform'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src="/portrait.jpg"
|
src="/portrait.jpg"
|
||||||
alt={t('pages.home.about.image.alt')}
|
alt={t('pages.home.about.image.alt')}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = 'data:image/svg+xml,%3Csvg width="400" height="400" xmlns="http://www.w3.org/2000/svg"%3E%3Crect width="400" height="400" fill="%2327272a"/%3E%3Ctext x="50%25" y="50%25" font-family="system-ui" font-size="24" fill="%2371717a" text-anchor="middle" dominant-baseline="middle"%3EDamjan Savić%3C/text%3E%3C/svg%3E';
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<motion.h2
|
{/* Title */}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
className="text-3xl font-bold text-white"
|
|
||||||
style={{
|
|
||||||
transform: 'translateZ(0)',
|
|
||||||
willChange: 'opacity, transform'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('pages.home.about.title')}
|
|
||||||
</motion.h2>
|
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.3 }}
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
className="text-zinc-400 leading-relaxed"
|
|
||||||
style={{
|
|
||||||
transform: 'translateZ(0)',
|
|
||||||
willChange: 'opacity, transform'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="text-zinc-400 leading-relaxed text-justify">
|
<h2 className="text-4xl lg:text-5xl font-bold text-white mb-2">
|
||||||
|
{t('pages.home.about.title')}
|
||||||
|
</h2>
|
||||||
|
<div className="h-1 w-20 bg-gradient-to-r from-zinc-600 to-zinc-800 rounded-full"></div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Highlight Pills */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.4 }}
|
||||||
|
className="flex flex-wrap gap-3"
|
||||||
|
>
|
||||||
|
{highlights.map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.5 + index * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.05, y: -2 }}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<span className="text-zinc-400">{item.icon}</span>
|
||||||
|
<span className="text-zinc-300 text-sm">{item.label}</span>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.5 }}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<p className="text-zinc-300 leading-relaxed text-lg">
|
||||||
{t('pages.home.about.content')}
|
{t('pages.home.about.content')}
|
||||||
</div>
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Call to Action Buttons */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.6 }}
|
||||||
|
className="flex flex-wrap gap-4 pt-4"
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<GraduationCap className="w-4 h-4" />
|
||||||
|
Mehr erfahren
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
CV herunterladen
|
||||||
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Copyright */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.4 }}
|
|
||||||
className="text-center mt-24"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Background Dots Pattern */}
|
|
||||||
<div className="absolute top-12 right-12 grid grid-cols-7 gap-2 opacity-20">
|
|
||||||
{[...Array(28)].map((_, i) => (
|
|
||||||
<div key={i} className="w-1 h-1 rounded-full bg-zinc-600" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,12 +53,15 @@ const Experience = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-12 md:py-24 bg-zinc-900 overflow-hidden">
|
<section className="py-12 md:py-24 overflow-hidden">
|
||||||
<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: 40 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{
|
||||||
|
duration: 0.8,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94]
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateZ(0)',
|
transform: 'translateZ(0)',
|
||||||
willChange: 'opacity, transform'
|
willChange: 'opacity, transform'
|
||||||
@@ -77,10 +80,13 @@ const Experience = () => {
|
|||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
key={currentPage}
|
key={currentPage}
|
||||||
initial={{ opacity: 0, x: 50 }}
|
initial={{ opacity: 0, x: 100, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||||
exit={{ opacity: 0, x: -50 }}
|
exit={{ opacity: 0, x: -100, scale: 0.95 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.43, 0.13, 0.23, 0.96]
|
||||||
|
}}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateZ(0)',
|
transform: 'translateZ(0)',
|
||||||
@@ -90,13 +96,22 @@ const Experience = () => {
|
|||||||
{getCurrentPageItems().map((exp, index) => (
|
{getCurrentPageItems().map((exp, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
transition={{
|
||||||
className="p-6 md:p-8 rounded-3xl bg-zinc-800/40 hover:bg-zinc-800/60 transition-colors"
|
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 }
|
||||||
|
}}
|
||||||
|
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"
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateZ(0)',
|
transform: 'translateZ(0)',
|
||||||
willChange: 'opacity'
|
willChange: 'opacity, transform'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3 className="text-lg md:text-xl font-semibold text-white mb-1">
|
<h3 className="text-lg md:text-xl font-semibold text-white mb-1">
|
||||||
@@ -110,12 +125,20 @@ const Experience = () => {
|
|||||||
</p>
|
</p>
|
||||||
<ul className="space-y-3 md:space-y-4">
|
<ul className="space-y-3 md:space-y-4">
|
||||||
{exp.highlights?.map((highlight: string, hIndex: number) => (
|
{exp.highlights?.map((highlight: string, hIndex: number) => (
|
||||||
<li
|
<motion.li
|
||||||
key={hIndex}
|
key={hIndex}
|
||||||
className="text-zinc-400 text-sm leading-relaxed"
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
delay: 0.3 + (hIndex * 0.1),
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
className="text-zinc-400 text-sm leading-relaxed flex items-start"
|
||||||
>
|
>
|
||||||
• {highlight}
|
<span className="text-white/40 mr-2 mt-0.5">→</span>
|
||||||
</li>
|
<span>{highlight}</span>
|
||||||
|
</motion.li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -129,29 +152,40 @@ 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-colors p-2"
|
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30"
|
||||||
disabled={currentPage === 0}
|
disabled={currentPage === 0}
|
||||||
aria-label={t('pages.home.experience.navigation.prev')}
|
aria-label={t('pages.home.experience.navigation.prev')}
|
||||||
>
|
>
|
||||||
{t('pages.home.experience.navigation.prev')}
|
‹
|
||||||
</button>
|
</button>
|
||||||
{[...Array(totalPages)].map((_, index) => (
|
<div className="flex items-center gap-2">
|
||||||
<button
|
{[...Array(totalPages)].map((_, index) => (
|
||||||
key={index}
|
<button
|
||||||
onClick={() => setCurrentPage(index)}
|
key={index}
|
||||||
className={`w-2 h-2 rounded-full transition-colors ${
|
onClick={() => setCurrentPage(index)}
|
||||||
index === currentPage ? 'bg-white' : 'bg-zinc-600 hover:bg-zinc-400'
|
className={`rounded-full transition-all duration-300 ${
|
||||||
}`}
|
index === currentPage
|
||||||
aria-label={t('pages.home.experience.navigation.page', { page: index + 1 })}
|
? 'bg-white'
|
||||||
/>
|
: 'bg-zinc-600 hover:bg-zinc-400'
|
||||||
))}
|
}`}
|
||||||
|
style={{
|
||||||
|
width: '5px',
|
||||||
|
height: '5px',
|
||||||
|
minWidth: '5px',
|
||||||
|
minHeight: '5px',
|
||||||
|
padding: '0'
|
||||||
|
}}
|
||||||
|
aria-label={t('pages.home.experience.navigation.page', { page: index + 1 })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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-colors p-2"
|
className="text-zinc-400 hover:text-white transition-all duration-300 p-2 text-2xl font-light disabled:opacity-30"
|
||||||
disabled={currentPage === totalPages - 1}
|
disabled={currentPage === totalPages - 1}
|
||||||
aria-label={t('pages.home.experience.navigation.next')}
|
aria-label={t('pages.home.experience.navigation.next')}
|
||||||
>
|
>
|
||||||
{t('pages.home.experience.navigation.next')}
|
›
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Github, Linkedin, Mail } from 'lucide-react';
|
import { Github, Linkedin, Mail } from 'lucide-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { FloatingPaths } from '../../../components/FloatingPaths';
|
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -14,12 +13,7 @@ const Hero = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="home" className="relative min-h-screen bg-zinc-900 text-white overflow-hidden">
|
<section id="home" className="relative min-h-screen text-white overflow-hidden">
|
||||||
{/* Floating Paths Background - nur eine Instanz für bessere Performance */}
|
|
||||||
<div className="absolute inset-0 pointer-events-none" style={{ zIndex: 1 }}>
|
|
||||||
<FloatingPaths position={1} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Concentric Circles Background - mit GPU-Beschleunigung */}
|
{/* Concentric Circles Background - mit GPU-Beschleunigung */}
|
||||||
<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) => (
|
||||||
@@ -68,13 +62,13 @@ const Hero = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-16 relative" style={{ zIndex: 30 }}>
|
<div className="flex flex-col items-center justify-start min-h-screen px-4 pt-20 relative" style={{ zIndex: 30 }}>
|
||||||
{/* Profile Image */}
|
{/* Profile Image */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="w-48 h-48 sm:w-64 sm:h-64 mb-6 rounded-full overflow-hidden border-2 border-zinc-800"
|
className="w-56 h-56 sm:w-72 sm:h-72 mb-8 rounded-full overflow-hidden border-2 border-zinc-800"
|
||||||
style={{ transform: 'translateZ(0)', backfaceVisibility: 'hidden' }}
|
style={{ transform: 'translateZ(0)', backfaceVisibility: 'hidden' }}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -91,7 +85,7 @@ const Hero = () => {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
className="text-white text-sm tracking-wider mb-2"
|
className="text-white text-base sm:text-lg tracking-wider mb-3"
|
||||||
>
|
>
|
||||||
{t('pages.home.hero.title')}
|
{t('pages.home.hero.title')}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
@@ -99,7 +93,7 @@ const Hero = () => {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.3 }}
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
className="text-4xl font-bold mb-8 flex items-center"
|
className="text-5xl sm:text-6xl font-bold mb-10 flex items-center"
|
||||||
>
|
>
|
||||||
{t('pages.home.hero.name')}<span className="animate-pulse ml-1">|</span>
|
{t('pages.home.hero.name')}<span className="animate-pulse ml-1">|</span>
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
@@ -109,35 +103,35 @@ const Hero = () => {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5, delay: 0.4 }}
|
transition={{ duration: 0.5, delay: 0.4 }}
|
||||||
className="flex flex-col sm:flex-row gap-3 sm:gap-6 text-white text-sm z-20"
|
className="flex flex-col sm:flex-row gap-4 sm:gap-8 text-white text-base sm:text-lg z-20"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollToSection('experience')}
|
onClick={() => scrollToSection('experience')}
|
||||||
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
|
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
|
||||||
>
|
>
|
||||||
<span className="relative z-10">{t('pages.home.hero.navigation.experience')}</span>
|
<span className="relative z-10">{t('pages.home.hero.navigation.experience')}</span>
|
||||||
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollToSection('skills')}
|
onClick={() => scrollToSection('skills')}
|
||||||
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
|
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
|
||||||
>
|
>
|
||||||
<span className="relative z-10">{t('pages.home.hero.navigation.skills')}</span>
|
<span className="relative z-10">{t('pages.home.hero.navigation.skills')}</span>
|
||||||
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollToSection('projects')}
|
onClick={() => scrollToSection('projects')}
|
||||||
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
|
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
|
||||||
>
|
>
|
||||||
<span className="relative z-10">{t('pages.home.hero.navigation.projects')}</span>
|
<span className="relative z-10">{t('pages.home.hero.navigation.projects')}</span>
|
||||||
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => scrollToSection('about')}
|
onClick={() => scrollToSection('about')}
|
||||||
className="relative w-full sm:w-auto px-6 py-2 rounded-full uppercase cursor-pointer overflow-hidden group"
|
className="relative w-full sm:w-auto px-8 py-3 rounded-full uppercase cursor-pointer overflow-hidden group"
|
||||||
>
|
>
|
||||||
<span className="relative z-10">{t('pages.home.hero.navigation.about')}</span>
|
<span className="relative z-10">{t('pages.home.hero.navigation.about')}</span>
|
||||||
<div className="absolute inset-0 bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
<div className="absolute inset-0 bg-zinc-700/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-full" />
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const Projects = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-zinc-900">
|
<section className="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-center items-center min-h-[300px] gap-3">
|
<div className="flex justify-center items-center min-h-[300px] gap-3">
|
||||||
<Loader2 className="h-8 w-8 text-zinc-400 animate-spin" />
|
<Loader2 className="h-8 w-8 text-zinc-400 animate-spin" />
|
||||||
@@ -55,7 +55,7 @@ const Projects = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-zinc-900">
|
<section className="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">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { motion } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Database, Server, Store, Code2, CodepenIcon, Box } from 'lucide-react';
|
import { Database, Server, Store, Code2, CodepenIcon, Box } from 'lucide-react';
|
||||||
|
|
||||||
interface Skill {
|
interface Skill {
|
||||||
@@ -50,7 +50,7 @@ const Skills = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-zinc-900">
|
<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">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
@@ -67,32 +67,20 @@ const Skills = () => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
key={skill.name}
|
key={skill.name}
|
||||||
className="space-y-2"
|
className="space-y-2"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
onHoverStart={() => setSelectedSkill(skill.name)}
|
transition={{ duration: 0.5, delay: skills.indexOf(skill) * 0.1 }}
|
||||||
onHoverEnd={() => setSelectedSkill(null)}
|
|
||||||
style={{
|
|
||||||
transform: 'translateZ(0)',
|
|
||||||
willChange: 'opacity'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex flex-col">
|
<span className="text-white text-sm font-medium">
|
||||||
<span className="text-white text-sm font-medium">
|
{skill.name}
|
||||||
{skill.name}
|
</span>
|
||||||
</span>
|
|
||||||
{selectedSkill === skill.name && (
|
|
||||||
<span className="text-zinc-400 text-xs">
|
|
||||||
{skill.description}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-white text-sm">
|
<span className="text-white 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-zinc-800/50 rounded-full overflow-hidden"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-valuenow={skill.level}
|
aria-valuenow={skill.level}
|
||||||
aria-valuemin={0}
|
aria-valuemin={0}
|
||||||
@@ -100,12 +88,14 @@ const Skills = () => {
|
|||||||
aria-label={t('pages.home.skills.aria.skillLevel')}
|
aria-label={t('pages.home.skills.aria.skillLevel')}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className={`h-full rounded-full ${
|
className="h-full rounded-full bg-gradient-to-r from-zinc-600 to-zinc-500"
|
||||||
selectedSkill === skill.name ? 'bg-zinc-500' : 'bg-zinc-600'
|
|
||||||
}`}
|
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
animate={{ width: `${skill.level}%` }}
|
animate={{
|
||||||
transition={{ duration: 0.5 }}
|
width: `${skill.level}%`
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
width: { duration: 1, delay: skills.indexOf(skill) * 0.1 }
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateZ(0)',
|
transform: 'translateZ(0)',
|
||||||
willChange: 'width'
|
willChange: 'width'
|
||||||
@@ -121,32 +111,63 @@ const Skills = () => {
|
|||||||
{skills.map((skill) => (
|
{skills.map((skill) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={skill.name}
|
key={skill.name}
|
||||||
className={`p-6 rounded-xl bg-zinc-800 flex flex-col items-center justify-center gap-3 cursor-pointer transition-all duration-300 hover:bg-zinc-700 ${
|
className="relative"
|
||||||
selectedSkill === skill.name ? 'ring-2 ring-zinc-500' : ''
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
}`}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
whileHover={{ scale: 1.05 }}
|
transition={{ duration: 0.5, delay: skills.indexOf(skill) * 0.1 }}
|
||||||
onClick={() =>
|
|
||||||
setSelectedSkill(skill.name === selectedSkill ? null : skill.name)
|
|
||||||
}
|
|
||||||
role="button"
|
|
||||||
aria-pressed={selectedSkill === skill.name}
|
|
||||||
aria-label={t('pages.home.skills.aria.selectSkill')}
|
|
||||||
style={{
|
|
||||||
transform: 'translateZ(0)',
|
|
||||||
willChange: 'transform'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}>
|
<motion.div
|
||||||
{iconMap[skill.name]}
|
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 ${
|
||||||
</div>
|
selectedSkill === skill.name ? 'ring-2 ring-zinc-500 z-20' : ''
|
||||||
<span className="text-white text-sm text-center">
|
}`}
|
||||||
{skill.name}
|
whileHover={{ scale: 1.05 }}
|
||||||
</span>
|
whileTap={{ scale: 0.98 }}
|
||||||
{selectedSkill === skill.name && (
|
onHoverStart={() => setSelectedSkill(skill.name)}
|
||||||
<span className="text-zinc-400 text-xs text-center mt-1">
|
onHoverEnd={() => setSelectedSkill(null)}
|
||||||
{skill.description}
|
role="button"
|
||||||
</span>
|
aria-pressed={selectedSkill === skill.name}
|
||||||
)}
|
aria-label={t('pages.home.skills.aria.selectSkill')}
|
||||||
|
style={{
|
||||||
|
transform: 'translateZ(0)',
|
||||||
|
willChange: 'transform',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: selectedSkill === skill.name ? 20 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className={selectedSkill === skill.name ? 'text-zinc-500' : 'text-white'}
|
||||||
|
animate={{
|
||||||
|
rotate: selectedSkill === skill.name ? [0, -10, 10, 0] : 0,
|
||||||
|
scale: selectedSkill === skill.name ? 1.1 : 1
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
>
|
||||||
|
{iconMap[skill.name]}
|
||||||
|
</motion.div>
|
||||||
|
<span className="text-white text-sm text-center">
|
||||||
|
{skill.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedSkill === skill.name && (
|
||||||
|
<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"
|
||||||
|
initial={{ opacity: 0, y: 5 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 5 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
transform: 'translate(-50%, calc(-100% - 12px))',
|
||||||
|
zIndex: 999
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ const HomePage = () => {
|
|||||||
'digital marketing automation. Based in Germany, working with clients worldwide.';
|
'digital marketing automation. Based in Germany, working with clients worldwide.';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageTransition>
|
<>
|
||||||
<SEO
|
<SEO
|
||||||
title="Damjan Savić - Digital Solutions Consultant"
|
title="Damjan Savić - Digital Solutions Consultant"
|
||||||
description={metaDescription}
|
description={metaDescription}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
/>
|
/>
|
||||||
<main className="min-h-screen bg-zinc-900">
|
<main className="min-h-screen">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div id="home">
|
<div id="home">
|
||||||
<Hero />
|
<Hero />
|
||||||
@@ -95,7 +95,7 @@ const HomePage = () => {
|
|||||||
<FAQSection />
|
<FAQSection />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</PageTransition>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const TranslatedProjectPage = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@@ -131,7 +131,7 @@ const TranslatedProjectPage = () => {
|
|||||||
|
|
||||||
if (error || !project) {
|
if (error || !project) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -158,7 +158,7 @@ const TranslatedProjectPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-zinc-900">
|
<main className="min-h-screen">
|
||||||
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
|
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-20 lg:py-24">
|
||||||
{/* Back Navigation */}
|
{/* Back Navigation */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { ExternalLink, Calendar, Tag } from 'lucide-react';
|
import { ExternalLink, Calendar, Tag } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { trackProjectClick } from '../../../services/gtm';
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -33,9 +34,14 @@ const PortfolioCard: React.FC<PortfolioCardProps> = ({ project }) => {
|
|||||||
// Verwende technologies anstelle von tags wenn verfügbar
|
// Verwende technologies anstelle von tags wenn verfügbar
|
||||||
const displayTags = project.technologies || project.tags;
|
const displayTags = project.technologies || project.tags;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
trackProjectClick(project.title, project.category);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`/portfolio/${project.slug}`}
|
to={`/portfolio/${project.slug}`}
|
||||||
|
onClick={handleClick}
|
||||||
className="group relative block overflow-hidden bg-zinc-800/30 border border-zinc-800
|
className="group relative block overflow-hidden bg-zinc-800/30 border border-zinc-800
|
||||||
rounded-lg hover:bg-zinc-800/50 hover:border-zinc-700
|
rounded-lg hover:bg-zinc-800/50 hover:border-zinc-700
|
||||||
transition-all duration-300 focus:outline-none focus:ring-2
|
transition-all duration-300 focus:outline-none focus:ring-2
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const PortfolioGrid = () => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[50vh] bg-zinc-900">
|
<div className="flex flex-col items-center justify-center min-h-[50vh]">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@@ -94,7 +94,7 @@ const PortfolioGrid = () => {
|
|||||||
|
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[50vh] bg-zinc-900">
|
<div className="flex flex-col items-center justify-center min-h-[50vh]">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
@@ -118,7 +118,7 @@ const PortfolioGrid = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-12 bg-zinc-900">
|
<section className="py-12">
|
||||||
<div className="max-w-6xl mx-auto px-4">
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const ProjectLayout: React.FC<ProjectLayoutProps> = ({ children }) => {
|
|||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
className="min-h-screen bg-zinc-900 relative overflow-x-hidden"
|
className="min-h-screen relative overflow-x-hidden"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Google Tag Manager Event Service
|
||||||
|
* Zentrale Verwaltung aller GTM Data Layer Events
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
dataLayer: any[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialisiere dataLayer falls nicht vorhanden
|
||||||
|
if (typeof window !== 'undefined' && !window.dataLayer) {
|
||||||
|
window.dataLayer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push event to GTM dataLayer
|
||||||
|
*/
|
||||||
|
export const pushToDataLayer = (data: Record<string, any>) => {
|
||||||
|
if (typeof window !== 'undefined' && window.dataLayer) {
|
||||||
|
window.dataLayer.push(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track page view
|
||||||
|
*/
|
||||||
|
export const trackPageView = (pagePath: string, pageTitle: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'page_view',
|
||||||
|
page_path: pagePath,
|
||||||
|
page_title: pageTitle,
|
||||||
|
page_location: window.location.href,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track contact form submission
|
||||||
|
*/
|
||||||
|
export const trackContactFormSubmit = (formData: {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
subject?: string;
|
||||||
|
}) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'contact_form_submit',
|
||||||
|
event_category: 'engagement',
|
||||||
|
event_label: 'contact_form',
|
||||||
|
form_name: formData.name,
|
||||||
|
form_subject: formData.subject,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track portfolio project click
|
||||||
|
*/
|
||||||
|
export const trackProjectClick = (projectName: string, projectCategory?: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'project_click',
|
||||||
|
event_category: 'portfolio',
|
||||||
|
event_label: projectName,
|
||||||
|
project_name: projectName,
|
||||||
|
project_category: projectCategory,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track blog post view
|
||||||
|
*/
|
||||||
|
export const trackBlogPostView = (postTitle: string, postCategory?: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'blog_post_view',
|
||||||
|
event_category: 'content',
|
||||||
|
event_label: postTitle,
|
||||||
|
post_title: postTitle,
|
||||||
|
post_category: postCategory,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track social link click
|
||||||
|
*/
|
||||||
|
export const trackSocialClick = (platform: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'social_click',
|
||||||
|
event_category: 'social',
|
||||||
|
event_label: platform,
|
||||||
|
social_platform: platform,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track file download
|
||||||
|
*/
|
||||||
|
export const trackDownload = (fileName: string, fileType: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'file_download',
|
||||||
|
event_category: 'download',
|
||||||
|
event_label: fileName,
|
||||||
|
file_name: fileName,
|
||||||
|
file_type: fileType,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track navigation click
|
||||||
|
*/
|
||||||
|
export const trackNavigation = (navigationItem: string, navigationType: 'header' | 'footer' | 'menu') => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'navigation_click',
|
||||||
|
event_category: 'navigation',
|
||||||
|
event_label: navigationItem,
|
||||||
|
navigation_item: navigationItem,
|
||||||
|
navigation_type: navigationType,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track scroll depth
|
||||||
|
*/
|
||||||
|
export const trackScrollDepth = (depth: number) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'scroll_depth',
|
||||||
|
event_category: 'engagement',
|
||||||
|
event_label: `${depth}%`,
|
||||||
|
scroll_depth: depth,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track time on page
|
||||||
|
*/
|
||||||
|
export const trackTimeOnPage = (seconds: number, pagePath: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'time_on_page',
|
||||||
|
event_category: 'engagement',
|
||||||
|
event_label: pagePath,
|
||||||
|
time_seconds: seconds,
|
||||||
|
page_path: pagePath,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track CTA button click
|
||||||
|
*/
|
||||||
|
export const trackCTAClick = (ctaText: string, ctaLocation: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'cta_click',
|
||||||
|
event_category: 'conversion',
|
||||||
|
event_label: ctaText,
|
||||||
|
cta_text: ctaText,
|
||||||
|
cta_location: ctaLocation,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track language change
|
||||||
|
*/
|
||||||
|
export const trackLanguageChange = (oldLang: string, newLang: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'language_change',
|
||||||
|
event_category: 'preference',
|
||||||
|
event_label: `${oldLang}_to_${newLang}`,
|
||||||
|
old_language: oldLang,
|
||||||
|
new_language: newLang,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track FAQ interaction
|
||||||
|
*/
|
||||||
|
export const trackFAQInteraction = (question: string, action: 'open' | 'close') => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'faq_interaction',
|
||||||
|
event_category: 'engagement',
|
||||||
|
event_label: question,
|
||||||
|
faq_question: question,
|
||||||
|
faq_action: action,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track error
|
||||||
|
*/
|
||||||
|
export const trackError = (errorMessage: string, errorType: string) => {
|
||||||
|
pushToDataLayer({
|
||||||
|
event: 'error',
|
||||||
|
event_category: 'error',
|
||||||
|
event_label: errorType,
|
||||||
|
error_message: errorMessage,
|
||||||
|
error_type: errorType,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
/* 1. Stabilize page transitions */
|
/* 1. Stabilize page transitions */
|
||||||
.page-transition-active {
|
.page-transition-active {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
background-color: rgb(24, 24, 27) !important; /* zinc-900 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. Prevent floating paths from interfering */
|
/* 2. Prevent floating paths from interfering */
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Background Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test Container */
|
||||||
|
.test-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simulated GlobalBackground */
|
||||||
|
.global-background {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #18181b; /* zinc-900 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(to bottom right,
|
||||||
|
rgba(24, 24, 27, 0.8),
|
||||||
|
rgba(39, 39, 42, 0.7),
|
||||||
|
rgba(24, 24, 27, 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated Elements */
|
||||||
|
.animated-line {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(to right,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.2),
|
||||||
|
transparent);
|
||||||
|
animation: slide-right 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-right {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Layer */
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 50px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: rgba(39, 39, 42, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(63, 63, 70, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #22c55e;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="test-container">
|
||||||
|
<!-- Global Background Layer -->
|
||||||
|
<div class="global-background">
|
||||||
|
<div class="gradient-overlay"></div>
|
||||||
|
|
||||||
|
<!-- Animated Lines -->
|
||||||
|
<div class="animated-line" style="top: 25%;"></div>
|
||||||
|
<div class="animated-line" style="top: 50%; animation-delay: 2s;"></div>
|
||||||
|
<div class="animated-line" style="top: 75%; animation-delay: 4s;"></div>
|
||||||
|
|
||||||
|
<!-- SVG Pattern -->
|
||||||
|
<svg style="position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0.3;">
|
||||||
|
<pattern id="grid" width="50" height="50" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 50 0 L 0 0 0 50" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
|
||||||
|
</pattern>
|
||||||
|
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Layer -->
|
||||||
|
<div class="content">
|
||||||
|
<h1>Background Visibility Test</h1>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
✅ Background is now visible! The animated elements and patterns should be displaying behind this content.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Changes Applied:</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Removed opaque bg-zinc-900 from Layout main container</li>
|
||||||
|
<li>Removed bg-zinc-900 from all page components</li>
|
||||||
|
<li>Changed GlobalBackground z-index from -z-10 to z-0</li>
|
||||||
|
<li>Made gradient overlays more transparent</li>
|
||||||
|
<li>Removed duplicate PageTransition wrapper from HomePage</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>What you should see:</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Animated horizontal lines sliding across the screen</li>
|
||||||
|
<li>Subtle grid pattern in the background</li>
|
||||||
|
<li>Gradient overlays creating depth</li>
|
||||||
|
<li>All content floating above the animated background</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+4
-4
@@ -39,15 +39,15 @@ export default defineConfig({
|
|||||||
scope: '/',
|
scope: '/',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/logo.png',
|
src: '/logo.svg',
|
||||||
sizes: '192x192',
|
sizes: '192x192',
|
||||||
type: 'image/png',
|
type: 'image/svg+xml',
|
||||||
purpose: 'any maskable'
|
purpose: 'any maskable'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/logo.png',
|
src: '/logo.svg',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/svg+xml',
|
||||||
purpose: 'any maskable'
|
purpose: 'any maskable'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user