Initial Commit.

This commit is contained in:
Damjan Savic 2025-02-10 23:50:36 +01:00
commit 664f5b1b77
46 changed files with 5978 additions and 0 deletions

54
.gitignore vendored Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View 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
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
public/images/2gdigital.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
public/images/bocitsec.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/images/gis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
public/images/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
public/images/netrix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
public/images/peter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
public/images/rittec.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
public/images/tech01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
public/images/tech02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

BIN
public/images/tech03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
public/images/tech04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
public/images/zentrale.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logos/tteamhell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

67
src/App.tsx Normal file
View 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;

View 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;
}
}

View 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
View 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>
);
}

View 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>
);
}

View 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
View 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>
);
}

View 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>
);
}

View 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
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

8
tailwind.config.js Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View 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
View 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']
}
}
}
}
});