Initial Commit.
54
.gitignore
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Production
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
|
||||
# Cache
|
||||
.cache
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
# System Files
|
||||
Thumbs.db
|
||||
28
eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
18
index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logos/technologie-team-favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Das Technologie Team ist eine mittelständische Unternehmensgruppe, die sich auf den IT- und Technologiesektor spezialisiert hat." />
|
||||
<meta name="keywords" content="IT, Technologie, Unternehmensgruppe, Deutschland, Oberhausen" />
|
||||
<meta property="og:title" content="Technologie Team - IT-/Technologie-Unternehmensgruppe" />
|
||||
<meta property="og:description" content="Das Technologie Team ist eine mittelständische Unternehmensgruppe, die sich auf den IT- und Technologiesektor spezialisiert hat." />
|
||||
<meta property="og:image" content="/logos/technologie-team.png" />
|
||||
<title>Technologie Team - IT-/Technologie-Unternehmensgruppe</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4501
package-lock.json
generated
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "technologie-team-landing",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.263.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.29.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"postcss": "^8.5.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.14"
|
||||
}
|
||||
}
|
||||
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
BIN
public/images/2gdigital.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/images/bocitsec.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/gis.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/images/hero.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
4
public/images/linkedin.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="60px" height="60px">
|
||||
<path fill="#FFFFFF" d="M24,4H6C4.895,4,4,4.895,4,6v18c0,1.105,0.895,2,2,2h18c1.105,0,2-0.895,2-2V6C26,4.895,25.105,4,24,4z M10.954,22h-2.95 v-9.492h2.95V22z M9.449,11.151c-0.951,0-1.72-0.771-1.72-1.72c0-0.949,0.77-1.719,1.72-1.719c0.948,0,1.719,0.771,1.719,1.719 C11.168,10.38,10.397,11.151,9.449,11.151z M22.004,22h-2.948v-4.616c0-1.101-0.02-2.517-1.533-2.517 c-1.535,0-1.771,1.199-1.771,2.437V22h-2.948v-9.492h2.83v1.297h0.04c0.394-0.746,1.356-1.533,2.791-1.533 c2.987,0,3.539,1.966,3.539,4.522V22z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 625 B |
BIN
public/images/linqit.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
public/images/netrix.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
public/images/peter.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/images/rittec.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/images/ritterdigital.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/images/tech01.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
public/images/tech02.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
public/images/tech03.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
public/images/tech04.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/images/zentrale.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
7
public/logos/technologie-team-favicon.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="4" fill="#C25B3F"/>
|
||||
<path d="M8 8H24V12H8V8Z" fill="white"/>
|
||||
<path d="M8 14H16V26H8V14Z" fill="white"/>
|
||||
<path d="M18 14H24V18H18V14Z" fill="white"/>
|
||||
<path d="M18 20H24V26H18V20Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 339 B |
BIN
public/logos/tteamdunkel.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/logos/tteamhell.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
67
src/App.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import Navbar from './components/Navbar';
|
||||
import Hero from './components/Hero';
|
||||
import Mission from './components/Mission';
|
||||
import Technologies from './components/Technologies';
|
||||
import GroupValue from './components/GroupValue';
|
||||
import GroupCompanies from './components/GroupCompanies';
|
||||
import ExtendedGroup from './components/ExtendedGroup';
|
||||
import Footer from './components/Footer';
|
||||
import ScrollProgress from './components/ScrollProgress';
|
||||
import ScrollToTop from './components/ScrollToTop';
|
||||
|
||||
function App() {
|
||||
const initializeObserver = useCallback(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-fade-in');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px',
|
||||
}
|
||||
);
|
||||
|
||||
const sections = document.querySelectorAll('section');
|
||||
sections.forEach((section) => {
|
||||
observer.observe(section);
|
||||
});
|
||||
|
||||
return () => {
|
||||
sections.forEach((section) => observer.unobserve(section));
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const cleanup = initializeObserver();
|
||||
return cleanup;
|
||||
}, [initializeObserver]);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen bg-white flex flex-col">
|
||||
<ScrollProgress />
|
||||
<Navbar />
|
||||
<main className="flex-grow">
|
||||
<Hero />
|
||||
<Mission />
|
||||
<Technologies />
|
||||
<GroupValue />
|
||||
<GroupCompanies />
|
||||
<ExtendedGroup />
|
||||
</main>
|
||||
<Footer />
|
||||
<ScrollToTop />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
48
src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(): State {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error('Uncaught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center p-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Etwas ist schiefgelaufen
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Bitte laden Sie die Seite neu oder versuchen Sie es später erneut.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="bg-[#C25B3F] hover:bg-[#A34832] text-white px-6 py-2 rounded-md transition-colors"
|
||||
>
|
||||
Seite neu laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
102
src/components/ExtendedGroup.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function ExtendedGroup() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const section = document.getElementById('extended-group');
|
||||
if (section) {
|
||||
observer.observe(section);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (section) {
|
||||
observer.unobserve(section);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const stats = [
|
||||
{ text: 'seit 1984 am Markt', highlight: '1984' },
|
||||
{ text: 'ca. 140 Unternehmen', highlight: '140' },
|
||||
{ text: '6500 Mitarbeiter', highlight: '6500' }
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="extended-group" className="py-12 sm:py-16 lg:py-24 bg-white overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className={`transition-all duration-700 ${
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
|
||||
}`}>
|
||||
<h2 className="font-outfit text-3xl sm:text-4xl lg:text-5xl font-bold text-[#C25B3F]
|
||||
mb-4 sm:mb-6 section-enter section-enter-active">
|
||||
Die erweiterte Unternehmensgruppe
|
||||
</h2>
|
||||
<p className="font-outfit text-base sm:text-lg text-gray-600 mb-8 sm:mb-12 max-w-4xl
|
||||
leading-relaxed section-enter section-enter-active delay-100">
|
||||
Das Technologie Team und deren Beteiligungen werden selbstständig und unabhängig geführt.
|
||||
Gleichzeitig können die Beteiligungen aber auf die Leistungen und Netzwerke der anderen
|
||||
Gesellschafterunternehmen zurückgreifen.
|
||||
</p>
|
||||
|
||||
<div className="relative rounded-2xl overflow-hidden shadow-lg group"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}>
|
||||
{/* Image Container */}
|
||||
<div className="relative">
|
||||
<img
|
||||
src="/images/zentrale.png"
|
||||
alt="Zentrale Oberhausen"
|
||||
className="w-full aspect-[2/1] object-cover transition-transform duration-700
|
||||
group-hover:scale-105 will-change-transform"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Dark Overlay */}
|
||||
<div className={`absolute inset-0 bg-black/20 transition-opacity duration-500
|
||||
${isHovered ? 'opacity-0' : 'opacity-100'}`} />
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90
|
||||
via-black/60 to-transparent p-4 sm:p-6 lg:p-8 transform transition-all
|
||||
duration-500 translate-y-0 group-hover:translate-y-0">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-white">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="flex flex-col items-center sm:items-start
|
||||
transform transition-all duration-500
|
||||
group-hover:translate-y-0 opacity-90
|
||||
group-hover:opacity-100">
|
||||
<p className="font-outfit text-base sm:text-lg text-center sm:text-left">
|
||||
<span className="font-semibold text-orange-400">{stat.highlight}</span>
|
||||
{' ' + stat.text.replace(stat.highlight, '')}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location Badge */}
|
||||
<div className="absolute top-4 left-4 bg-white/90 backdrop-blur-sm px-4 py-2
|
||||
rounded-full shadow-lg transform transition-all duration-300
|
||||
group-hover:scale-105">
|
||||
<p className="font-outfit text-sm font-medium text-gray-800">
|
||||
Zentrale: Oberhausen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
151
src/components/Footer.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
type FooterSection = {
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
};
|
||||
|
||||
const links = [
|
||||
{ href: '#technologies', text: 'Unterstütze Technologien' },
|
||||
{ href: '#group', text: 'Gruppenbeitritt' },
|
||||
{ href: '#impressum', text: 'Impressum' },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [hoveredLink, setHoveredLink] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const footer = document.querySelector('footer');
|
||||
if (footer) {
|
||||
observer.observe(footer);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (footer) {
|
||||
observer.unobserve(footer);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sections: FooterSection[] = [
|
||||
{
|
||||
title: 'Links',
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
{links.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="block transform transition-all duration-300 hover:text-white
|
||||
hover:translate-x-1 focus:outline-none focus:text-white
|
||||
group relative"
|
||||
onMouseEnter={() => setHoveredLink(link.href)}
|
||||
onMouseLeave={() => setHoveredLink(null)}
|
||||
>
|
||||
<span className="relative z-10 inline-block">{link.text}</span>
|
||||
<span className={`absolute left-0 bottom-0 w-0 h-0.5 bg-[#C25B3F]
|
||||
transition-all duration-300 group-hover:w-full
|
||||
${hoveredLink === link.href ? 'w-full' : 'w-0'}`} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Ort',
|
||||
content: (
|
||||
<address className="not-italic space-y-2">
|
||||
<p className="transition-colors duration-300 hover:text-white">
|
||||
Essener Straße 2-24
|
||||
</p>
|
||||
<p className="transition-colors duration-300 hover:text-white">
|
||||
46047 Oberhausen
|
||||
</p>
|
||||
</address>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Mailkontakt',
|
||||
content: (
|
||||
<div>
|
||||
<a href="mailto:kontakt@technologie.team"
|
||||
className="block mb-4 transition-all duration-300 hover:text-white
|
||||
hover:translate-x-1 relative group">
|
||||
<span>kontakt@technologie.team</span>
|
||||
<span className="absolute left-0 bottom-0 w-0 h-0.5 bg-[#C25B3F]
|
||||
transition-all duration-300 group-hover:w-full" />
|
||||
</a>
|
||||
<a
|
||||
href="https://linkedin.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block transform transition-all duration-300
|
||||
hover:scale-110 focus:outline-none focus:scale-110"
|
||||
aria-label="Besuchen Sie uns auf LinkedIn"
|
||||
>
|
||||
<img
|
||||
src="/images/linkedin.svg"
|
||||
alt="LinkedIn"
|
||||
className="h-8 w-8 sm:h-10 sm:w-10"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="bg-gray-900 text-gray-400 py-12 sm:py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className={`flex flex-col space-y-12 sm:space-y-16
|
||||
transition-all duration-700 transform
|
||||
${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||
{/* Logo */}
|
||||
<Link to="/"
|
||||
className="block transition-transform duration-300 hover:scale-105
|
||||
focus:outline-none focus:scale-105">
|
||||
<img
|
||||
src="/logos/tteamhell.png"
|
||||
alt="Technologie Team Logo"
|
||||
className="h-12 sm:h-15"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Navigation Section */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 sm:gap-12">
|
||||
{sections.map((section, index) => (
|
||||
<div key={section.title}
|
||||
className={`transition-all duration-500 delay-${index * 100}
|
||||
${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`}>
|
||||
<h3 className="font-outfit text-white font-semibold mb-4 text-base sm:text-lg
|
||||
transform transition-all duration-300 hover:text-[#C25B3F]">
|
||||
{section.title}
|
||||
</h3>
|
||||
{section.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 mt-12 sm:mt-16 pt-6 sm:pt-8
|
||||
text-center text-sm text-gray-500">
|
||||
<p className="transition-colors duration-300 hover:text-gray-400">
|
||||
Copyright © {new Date().getFullYear()} TechnologieTeam
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
127
src/components/GroupCompanies.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const companies = [
|
||||
{
|
||||
name: 'b.o.c. IT-SECURITY',
|
||||
logo: '/images/bocitsec.png',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
name: 'GIS',
|
||||
logo: '/images/gis.png',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
name: 'RITTER TECHNOLOGIE',
|
||||
logo: '/images/rittec.png',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
name: 'NETRIX',
|
||||
logo: '/images/netrix.png',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
name: 'RITTER digital',
|
||||
logo: '/images/ritterdigital.png',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
name: 'LINQ-IT',
|
||||
logo: '/images/linqit.png',
|
||||
link: '#'
|
||||
},
|
||||
{
|
||||
name: '2G.digital',
|
||||
logo: '/images/2gdigital.png',
|
||||
link: '#'
|
||||
}
|
||||
];
|
||||
|
||||
export default function GroupCompanies() {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const section = document.getElementById('companies');
|
||||
if (section) {
|
||||
observer.observe(section);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (section) {
|
||||
observer.unobserve(section);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="companies" className="py-12 sm:py-16 lg:py-24 bg-white overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h2 className={`font-outfit text-3xl sm:text-4xl lg:text-5xl font-bold text-[#C25B3F]
|
||||
mb-8 sm:mb-12 lg:mb-16 transition-all duration-700
|
||||
${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||
Unsere IT-/Technologie Gruppenunternehmen
|
||||
</h2>
|
||||
|
||||
{/* Angepasstes Grid für 2 Spalten auf Mobile */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-6 lg:gap-8">
|
||||
{companies.map((company, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={company.link}
|
||||
className={`
|
||||
relative bg-white rounded-xl p-4 sm:p-6 lg:p-8
|
||||
transition-all duration-500 ease-out
|
||||
hover:shadow-xl hover:-translate-y-1
|
||||
flex items-center justify-center
|
||||
aspect-[4/3] group
|
||||
border border-gray-100
|
||||
${hoveredIndex === index ? 'shadow-lg scale-[1.02]' : 'shadow-sm'}
|
||||
${isVisible ? 'animate-fade-in' : 'opacity-0'}
|
||||
animate-delay-${(index + 1) * 100}
|
||||
`}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
aria-label={`Besuchen Sie ${company.name}`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<img
|
||||
src={company.logo}
|
||||
alt={company.name}
|
||||
className="w-full max-w-[85%] max-h-[65%] object-contain
|
||||
transition-transform duration-500
|
||||
group-hover:scale-110 will-change-transform"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Company Name Overlay - nur auf größeren Bildschirmen */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-2 sm:p-3 bg-gradient-to-t
|
||||
from-gray-900/10 to-transparent opacity-0
|
||||
group-hover:opacity-100 transition-opacity duration-300
|
||||
hidden sm:block">
|
||||
<p className="text-center text-gray-600 text-sm font-medium">
|
||||
{company.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Hover Effect Border */}
|
||||
<div className="absolute inset-0 rounded-xl border-2 border-[#C25B3F] opacity-0
|
||||
scale-105 group-hover:opacity-100 group-hover:scale-100
|
||||
transition-all duration-300" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
136
src/components/GroupValue.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Users2, Eye, BarChart3, Settings2, DollarSign, Search, Link2, TrendingUp, GraduationCap, Monitor } from 'lucide-react';
|
||||
|
||||
const valueItems = [
|
||||
{
|
||||
icon: Users2,
|
||||
title: 'Management Team',
|
||||
description: 'Ein erfahrenes, kompetentes Managementteam sorgt für strategische Unterstützung.'
|
||||
},
|
||||
{
|
||||
icon: Eye,
|
||||
title: 'Fokus auf Transparenz',
|
||||
description: 'Transparenz in allen Prozessen, was Vertrauen und offene Kommunikation fördert.'
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
title: 'Schlankes Reporting',
|
||||
description: 'Ein effektives Berichtssystem ermöglicht es sich auf seine Aufgaben zu konzentrieren und agil zu reagieren.'
|
||||
},
|
||||
{
|
||||
icon: Settings2,
|
||||
title: 'Operative Hebel',
|
||||
description: 'Gemeinsame Stärken nutzen und durchgängige Experten-Lösungen schaffen.'
|
||||
},
|
||||
{
|
||||
icon: DollarSign,
|
||||
title: 'Finanzielle Hebel',
|
||||
description: 'Finanzielle Stärken bündeln und zum Wohle der Gesamtlösung einsetzen.'
|
||||
},
|
||||
{
|
||||
icon: Search,
|
||||
title: 'Recruiting',
|
||||
description: 'Unterstützung durch Personal-Experten'
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
title: 'Gruppenzugehörigkeit',
|
||||
description: 'Mehr aus den einzelnen Möglichkeiten rausholen und gemeinsam bessere Lösungen und Entwicklungen schaffen.'
|
||||
},
|
||||
{
|
||||
icon: TrendingUp,
|
||||
title: 'Personal Perspektive',
|
||||
description: 'Individuelle Fördermöglichkeiten in einem großen Ökosystem, mit agilen Entscheidungswegen.'
|
||||
},
|
||||
{
|
||||
icon: GraduationCap,
|
||||
title: 'Ausbildung',
|
||||
description: 'Kontinuierliche Schulung und Weiterentwicklung der Mitarbeiterkompetenzen.'
|
||||
},
|
||||
{
|
||||
icon: Monitor,
|
||||
title: 'Digitalisierung',
|
||||
description: 'Nutzung neuester technischer Möglichkeiten zur Verbesserung von Geschäftsprozessen.'
|
||||
}
|
||||
];
|
||||
|
||||
export default function GroupValue() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const section = document.getElementById('group-value');
|
||||
if (section) {
|
||||
observer.observe(section);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (section) {
|
||||
observer.unobserve(section);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="group-value" className="bg-[#C25B3F] py-12 sm:py-16 lg:py-24 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className={`mb-8 sm:mb-12 lg:mb-16 max-w-3xl mx-auto text-center
|
||||
${isVisible ? 'animate-fade-in' : 'opacity-0'}`}>
|
||||
<h2 className="font-outfit text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-4 sm:mb-8
|
||||
section-enter section-enter-active">
|
||||
Unser Gruppenmehrwert
|
||||
</h2>
|
||||
<p className="font-outfit text-base sm:text-lg text-white/90 leading-relaxed
|
||||
section-enter section-enter-active delay-100">
|
||||
IT-/Technologieunternehmen zeichnen sich durch ihre Kompetenz und die exzellente Umsetzung von
|
||||
Kundenanforderungen aus. Unser Ziel ist es, diese Qualitäten zu erhalten, die Unternehmenskultur
|
||||
zu wahren und Raum für weiteres Wachstum und Entwicklung zu schaffen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 sm:gap-6 lg:gap-8">
|
||||
{valueItems.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative bg-white/10 backdrop-blur-sm rounded-2xl p-6 sm:p-8
|
||||
transition-all duration-500 group hover:bg-white/20
|
||||
hover:scale-102 hover:shadow-xl will-change-transform
|
||||
${isVisible ? 'animate-fade-in' : 'opacity-0'}
|
||||
animate-delay-${(index + 1) * 100}`}
|
||||
>
|
||||
<div className="mb-4 sm:mb-6 transform transition-transform duration-300
|
||||
group-hover:scale-110 group-hover:rotate-3">
|
||||
<Icon className="w-8 h-8 sm:w-10 sm:h-10 text-white"
|
||||
strokeWidth={1.5} />
|
||||
</div>
|
||||
<h3 className="font-outfit text-base sm:text-lg font-semibold text-white mb-2 sm:mb-3
|
||||
transform transition-transform duration-300
|
||||
group-hover:translate-x-1">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="font-outfit text-sm text-white/80 leading-relaxed
|
||||
transition-opacity duration-300 group-hover:text-white">
|
||||
{item.description}
|
||||
</p>
|
||||
|
||||
{/* Hover Effect Overlay */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-tr from-white/0 to-white/10
|
||||
opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
87
src/components/Hero.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function Hero() {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoaded(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-gray-900">
|
||||
{/* Hero section with background */}
|
||||
<div className="relative min-h-screen flex items-center">
|
||||
{/* Background image with overlay */}
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
backgroundImage: 'url("/images/hero.png")',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/50 to-black/60" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className={`${isLoaded ? 'animate-fade-in' : ''}`}>
|
||||
<h1 className="font-outfit text-3xl xs:text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl
|
||||
font-bold text-white mb-4 sm:mb-6 md:mb-8 tracking-normal leading-tight
|
||||
section-enter section-enter-active">
|
||||
<span className="block">
|
||||
IHRE IT-/TECHNOLOGIE-
|
||||
</span>
|
||||
<span className="block">
|
||||
UNTERNEHMENSGRUPPE
|
||||
</span>
|
||||
</h1>
|
||||
<p className="font-outfit text-lg xs:text-xl sm:text-2xl md:text-3xl text-gray-200
|
||||
mb-6 sm:mb-8 md:mb-12 tracking-normal leading-relaxed
|
||||
section-enter section-enter-active delay-200">
|
||||
Gemeinsam Stärker | Für Unsere Kunden | Durchgängig In Ihren Prozessen
|
||||
</p>
|
||||
<div className="section-enter section-enter-active delay-300">
|
||||
<Link
|
||||
to="/mehr"
|
||||
className="group relative font-outfit inline-flex items-center justify-center
|
||||
overflow-hidden rounded bg-[#C25B3F] px-6 sm:px-8 py-3 sm:py-4
|
||||
text-base sm:text-lg md:text-xl font-medium uppercase tracking-wide
|
||||
text-white w-full sm:w-auto shadow-lg
|
||||
transition-all duration-300 ease-out
|
||||
hover:bg-[#A34832]"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
Mehr erfahren
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle background effects */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/4 left-0 w-96 h-96 bg-orange-500/10 rounded-full
|
||||
blur-3xl mix-blend-overlay opacity-75" />
|
||||
<div className="absolute bottom-1/4 right-0 w-96 h-96 bg-orange-500/10 rounded-full
|
||||
blur-3xl mix-blend-overlay opacity-75" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
25
src/components/ImageLoader.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ImageLoaderProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ImageLoader({ src, alt, className = '' }: ImageLoaderProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 bg-gray-100 animate-pulse" />
|
||||
)}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={`${className} ${isLoading ? 'opacity-0' : 'opacity-100 transition-opacity duration-300'}`}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
93
src/components/Mission.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import peterImage from '../../public/images/peter.png';
|
||||
|
||||
export default function Mission() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const section = document.getElementById('mission');
|
||||
if (section) {
|
||||
observer.observe(section);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (section) {
|
||||
observer.unobserve(section);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="mission" className="py-16 sm:py-20 lg:py-24 bg-white">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className={`grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-16 items-start
|
||||
${isVisible ? 'animate-fade-in' : 'opacity-0'}`}>
|
||||
{/* CEO Image Card */}
|
||||
<div className="lg:col-span-4 lg:sticky lg:top-24">
|
||||
<div className="group bg-gray-100 rounded-2xl overflow-hidden shadow-lg
|
||||
transition-all duration-300 hover:shadow-2xl transform hover:-translate-y-1">
|
||||
<div className="overflow-hidden">
|
||||
<img
|
||||
src={peterImage}
|
||||
alt="Peter Heim"
|
||||
className="w-full aspect-[90/100] object-cover object-top
|
||||
transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6 sm:p-8 bg-gray-800 transform transition-all duration-300
|
||||
group-hover:bg-gray-900">
|
||||
<h3 className="font-outfit text-xl sm:text-2xl font-semibold text-white mb-1
|
||||
transform transition-all duration-300 group-hover:translate-x-1">
|
||||
Peter Heim
|
||||
</h3>
|
||||
<p className="font-outfit text-base sm:text-lg text-gray-200
|
||||
transform transition-all duration-300 group-hover:translate-x-1">
|
||||
CEO
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mission Text Content */}
|
||||
<div className="lg:col-span-8">
|
||||
<h2 className="font-outfit text-3xl sm:text-4xl lg:text-5xl font-bold text-[#C25B3F]
|
||||
mb-8 sm:mb-12 section-enter section-enter-active">
|
||||
Unser Leitbild
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6 sm:space-y-8">
|
||||
<p className="font-outfit text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed
|
||||
transition-all duration-300 hover:text-gray-900 section-enter section-enter-active">
|
||||
Das Technologie Team ist eine mittelständische Unternehmensgruppe, die sich auf den
|
||||
IT- und Technologiesektor spezialisiert hat. Wir konzentrieren uns auf Beteiligungen in
|
||||
Nachfolgesituationen und die langfristige Weiterentwicklung erfolgreicher
|
||||
Unternehmen. Unser Ziel ist es, eine führende Rolle in der Technologiebranche zu
|
||||
übernehmen, indem wir vertrauensvolle Transaktionen und nachhaltige Strategien für
|
||||
Unternehmensfortführungen anbieten.
|
||||
</p>
|
||||
|
||||
<p className="font-outfit text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed
|
||||
transition-all duration-300 hover:text-gray-900 section-enter section-enter-active
|
||||
delay-100">
|
||||
Wir legen Wert auf langfristige Geschäftsbeziehungen und agieren in agilen
|
||||
Arbeitsstrukturen. Unser Fokus liegt auf der nahtlosen Fortführung des Tagesgeschäfts
|
||||
sowie auf der Unterstützung bei der Erreichung zukünftiger Ziele. So bieten wir
|
||||
umfassende IT- und Technologieleistungen „aus einer Hand" und nutzen die
|
||||
wachsenden Möglichkeiten der IT und Technologien optimal für Ihr Wachstum.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
22
src/components/Navbar.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 bg-white shadow-md z-50 overflow-x-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-start items-center h-20 sm:h-28">
|
||||
{/* Left-aligned Logo section */}
|
||||
<div className="flex items-center">
|
||||
<Link to="/" className="flex items-center">
|
||||
<img
|
||||
src="/logos/tteamdunkel.png"
|
||||
alt="Technologie Team Logo"
|
||||
className="h-16 sm:h-24 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
14
src/components/ScrollProgress.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { useScrollProgress } from '../hooks/useScrollProgress';
|
||||
|
||||
export default function ScrollProgress() {
|
||||
const progress = useScrollProgress();
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 w-full h-1 z-50">
|
||||
<div
|
||||
className="h-full bg-[#C25B3F] transition-all duration-100"
|
||||
style={{ width: `${progress * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/ScrollToTop.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowUp } from 'lucide-react';
|
||||
|
||||
export default function ScrollToTop() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const toggleVisibility = () => {
|
||||
if (window.pageYOffset > 300) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', toggleVisibility);
|
||||
return () => window.removeEventListener('scroll', toggleVisibility);
|
||||
}, []);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`fixed bottom-8 right-8 bg-[#C25B3F] hover:bg-[#A34832] text-white p-3 rounded-full shadow-lg transition-all duration-300 z-50 ${
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'
|
||||
}`}
|
||||
onClick={scrollToTop}
|
||||
aria-label="Zum Seitenanfang scrollen"
|
||||
>
|
||||
<ArrowUp className="w-6 h-6" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
196
src/components/Technologies.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
interface Technology {
|
||||
title: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const technologies: Technology[] = [
|
||||
{
|
||||
title: 'ERP, eCommerce, Warenwirtschaft, digitale Geschäftsprozesse',
|
||||
image: '/images/tech01.png',
|
||||
},
|
||||
{
|
||||
title: 'IT-Infrastruktur, as a Service Modelle, Cloudsysteme',
|
||||
image: '/images/tech02.png',
|
||||
},
|
||||
{
|
||||
title: 'IT-Sicherheitstechnik, Netzwerktechnik',
|
||||
image: '/images/tech03.png',
|
||||
},
|
||||
{
|
||||
title: 'Beratung, Projektunterstützung',
|
||||
image: '/images/tech04.png',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Technologies() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 768);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
setCurrentIndex((prev) => (prev === 0 ? technologies.length - 1 : prev - 1));
|
||||
}, []);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
setCurrentIndex((prev) => (prev === technologies.length - 1 ? 0 : prev + 1));
|
||||
}, []);
|
||||
|
||||
const onTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
setTouchStart(e.touches[0].clientX);
|
||||
};
|
||||
|
||||
const onTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (!touchStart) return;
|
||||
const touchEnd = e.changedTouches[0].clientX;
|
||||
const diff = touchStart - touchEnd;
|
||||
|
||||
if (Math.abs(diff) > 50) {
|
||||
if (diff > 0) handleNext();
|
||||
else handlePrevious();
|
||||
}
|
||||
setTouchStart(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="bg-gray-50 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="font-outfit text-2xl sm:text-3xl font-bold text-[#C25B3F]">
|
||||
Unterstützte Technologien
|
||||
</h2>
|
||||
{!isMobile && (
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
className="p-2 rounded-full bg-white shadow-lg
|
||||
transition-all duration-300 hover:bg-gray-50 active:scale-95
|
||||
focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
aria-label="Vorherige"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="p-2 rounded-full bg-[#C25B3F] shadow-lg
|
||||
transition-all duration-300 hover:bg-[#A34832] active:scale-95
|
||||
focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
aria-label="Nächste"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop View */}
|
||||
{!isMobile && (
|
||||
<div className="relative">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{technologies.map((tech, idx) => {
|
||||
const isVisible = Math.abs(idx - currentIndex) <= 3;
|
||||
const order = (idx - currentIndex + technologies.length) % technologies.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`transition-all duration-500 transform
|
||||
${isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-95'}
|
||||
order-${order}`}
|
||||
>
|
||||
<div className="relative aspect-[4/3] rounded-xl overflow-hidden shadow-lg group">
|
||||
<img
|
||||
src={tech.image}
|
||||
alt={tech.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700
|
||||
group-hover:scale-105"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90
|
||||
via-black/40 to-transparent">
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<h3 className="font-outfit text-white text-lg font-medium leading-snug">
|
||||
{tech.title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile View */}
|
||||
{isMobile && (
|
||||
<div
|
||||
className="relative"
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchEnd={onTouchEnd}
|
||||
>
|
||||
<div className="absolute z-10 inset-y-0 left-0 right-0 flex items-center justify-between pointer-events-none">
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
className="pointer-events-auto p-2 rounded-full bg-white/90 shadow-lg
|
||||
transition-all duration-300 hover:bg-white active:scale-95
|
||||
focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
aria-label="Vorherige"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="pointer-events-auto p-2 rounded-full bg-[#C25B3F] shadow-lg
|
||||
transition-all duration-300 hover:bg-[#A34832] active:scale-95
|
||||
focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
aria-label="Nächste"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative aspect-[4/3] rounded-xl overflow-hidden shadow-lg">
|
||||
<img
|
||||
src={technologies[currentIndex].image}
|
||||
alt={technologies[currentIndex].title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent">
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
||||
<h3 className="font-outfit text-white text-lg font-medium leading-snug">
|
||||
{technologies[currentIndex].title}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-2 mt-4">
|
||||
{technologies.map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setCurrentIndex(idx)}
|
||||
className={`h-1 rounded-full transition-all duration-300
|
||||
${currentIndex === idx ? 'w-8 bg-[#C25B3F]' : 'w-4 bg-gray-300'}`}
|
||||
aria-label={`Gehe zu Slide ${idx + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
22
src/hooks/useScrollProgress.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useScrollProgress() {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const updateScroll = () => {
|
||||
const currentProgress = window.scrollY;
|
||||
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
|
||||
if (scrollHeight) {
|
||||
setProgress(Number((currentProgress / scrollHeight).toFixed(2)));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', updateScroll);
|
||||
|
||||
return () => window.removeEventListener('scroll', updateScroll);
|
||||
}, []);
|
||||
|
||||
return progress;
|
||||
}
|
||||
86
src/index.css
Normal file
@ -0,0 +1,86 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply text-gray-900 antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.section-title {
|
||||
@apply text-3xl md:text-4xl font-bold text-gray-900 mb-8;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 md:py-24;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Section transitions */
|
||||
.section-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.section-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.5s, transform 0.5s;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.section-container {
|
||||
@apply py-12;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.image-loading {
|
||||
@apply relative overflow-hidden bg-gray-100;
|
||||
}
|
||||
|
||||
.image-loading::after {
|
||||
content: '';
|
||||
@apply absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
21
src/main.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import './index.css';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (!rootElement) {
|
||||
throw new Error('Root element not found');
|
||||
}
|
||||
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
);
|
||||
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
8
tailwind.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
24
tsconfig.app.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
22
tsconfig.node.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
30
vite.config.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src',
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5174,
|
||||
host: true,
|
||||
fs: {
|
||||
strict: false
|
||||
}
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vendor': ['react', 'react-dom', 'react-router-dom'],
|
||||
'icons': ['lucide-react']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||