website refactoring #7
45
frontend/app/(root)/Home.tsx
Normal file
45
frontend/app/(root)/Home.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import React, {useEffect} from "react";
|
||||
import HomeServices from "@/app/(root)/sections/HomeServices";
|
||||
import {motion} from "framer-motion";
|
||||
import Hero from "@/app/(root)/sections/Hero";
|
||||
import About from "@/app/(root)/sections/About";
|
||||
import ProcessSection from "@/app/(root)/sections/ProcessSection";
|
||||
import WhyUs from "@/app/(root)/sections/WhyUs";
|
||||
import Faq from "@/app/(root)/sections/Faq";
|
||||
import ReferralSection from "@/app/(root)/sections/ReferralSection";
|
||||
|
||||
const Home = () => {
|
||||
useEffect(() => {
|
||||
const scrollToId = localStorage.getItem('scrollToId')
|
||||
if (scrollToId) {
|
||||
localStorage.removeItem('scrollToId')
|
||||
const el = document.getElementById(scrollToId)
|
||||
if (el) {
|
||||
setTimeout(() => {
|
||||
el.scrollIntoView({behavior: 'smooth', block: 'start'})
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.7, ease: "easeOut"}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Hero/>
|
||||
<HomeServices/>
|
||||
<About/>
|
||||
<ProcessSection/>
|
||||
<WhyUs/>
|
||||
<ReferralSection/>
|
||||
<Faq/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,37 +0,0 @@
|
||||
import type {Metadata} from "next";
|
||||
import "../globals.css";
|
||||
|
||||
import Nav from "@/components/Navbar/Nav";
|
||||
import Footer from "@/components/Footer/Footer";
|
||||
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||
import React from "react";
|
||||
import {cookies} from "next/headers";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Rhein Software",
|
||||
description: "Rhein Software Development",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||
const bgColor = themeColors[theme].primaryBg;
|
||||
|
||||
return (
|
||||
<html lang="de" data-theme={theme}>
|
||||
<head/>
|
||||
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||
<ThemeProvider>
|
||||
<Nav/>
|
||||
{children}
|
||||
<Footer/>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import Home from "@/components/Home/Home";
|
||||
import Home from "@/app/(root)/Home";
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Home />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Home/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
@@ -1,23 +1,21 @@
|
||||
'use client';
|
||||
|
||||
// import Link from 'next/link';
|
||||
// import {FiArrowRight} from 'react-icons/fi';
|
||||
import {motion} from 'framer-motion';
|
||||
import {useThemeColors} from '@/utils/useThemeColors';
|
||||
|
||||
const About = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative w-full py-24 transition-colors duration-700 ease-in-out"
|
||||
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||
>
|
||||
id="about"
|
||||
className="relative w-full py-24 bg-background text-foreground transition-colors duration-700 ease-in-out">
|
||||
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto">
|
||||
<div className="flex flex-col">
|
||||
{/* Title */}
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-1 text-left transition-colors duration-700 ease-in-out"
|
||||
className="text-3xl md:text-4xl font-bold mb-1 text-left"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
Über uns
|
||||
</motion.h2>
|
||||
@@ -33,33 +31,43 @@ const About = () => {
|
||||
{/* Text */}
|
||||
<div className="p-0 max-w-4xl">
|
||||
<motion.p
|
||||
className="text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}
|
||||
className="text-base md:text-lg leading-relaxed text-muted-foreground"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Wir sind Rhein-Software – ein Team, das sich auf individuelle Softwarelösungen und digitale
|
||||
Services spezialisiert hat. Unsere Anwendungen sind technisch solide, skalierbar und
|
||||
durchdacht – gebaut für langfristigen Erfolg.
|
||||
Wir sind Rhein-Software – ein Team, das sich auf individuelle Softwarelösungen spezialisiert
|
||||
hat. Unsere Anwendungen sind technisch solide, skalierbar und durchdacht – gebaut für
|
||||
langfristigen Erfolg.
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
className="mt-6 text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}
|
||||
className="mt-6 text-base md:text-lg leading-relaxed text-muted-foreground"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
Von der ersten Idee bis zum Go-Live begleiten wir Unternehmen und Startups mit einem
|
||||
flexiblen Netzwerk, klarer Kommunikation und einem hohen Anspruch an Qualität.
|
||||
Unsere Lösungen sind intuitiv, effizient – und genau auf deine Anforderungen zugeschnitten.
|
||||
flexiblen Netzwerk, klarer Kommunikation und einem hohen Anspruch an Qualität. Gemeinsam
|
||||
realisieren wir digitale Produkte, die wirklich passen.
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
className="mt-6 text-base md:text-lg leading-relaxed text-muted-foreground"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.4}}
|
||||
>
|
||||
Egal ob App-Entwicklung, interne Tools, Web-Plattformen oder komplexe Schnittstellen – wir
|
||||
entwickeln Softwarelösungen, die intuitiv, effizient und exakt auf deine Anforderungen
|
||||
zugeschnitten sind.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
{/* CTA Placeholder */}
|
||||
<motion.div
|
||||
className="mt-10 flex justify-end"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
@@ -67,13 +75,7 @@ const About = () => {
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.5}}
|
||||
>
|
||||
{/*<Link href="/about">*/}
|
||||
{/* <button*/}
|
||||
{/* className="flex items-center gap-2 bg-blue-700 hover:bg-blue-900 text-white font-semibold px-5 py-2 rounded-full shadow-lg transition-all"*/}
|
||||
{/* >*/}
|
||||
{/* Mehr über uns <FiArrowRight size={18}/>*/}
|
||||
{/* </button>*/}
|
||||
{/*</Link>*/}
|
||||
{/* CTA button can go here */}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
109
frontend/app/(root)/sections/Faq.tsx
Normal file
109
frontend/app/(root)/sections/Faq.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
|
||||
import {Accordion, AccordionContent, AccordionItem, AccordionTrigger,} from "@/components/ui/accordion"
|
||||
import {motion} from "framer-motion"
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
id: "dauer",
|
||||
question: "Wie lange dauert es, bis meine Website oder App online ist?",
|
||||
answers: [
|
||||
"Das hängt vom Umfang des Projekts ab – einfache Websites oder MVPs sind meist innerhalb von 4–6 Wochen realisierbar.",
|
||||
"Komplexere Anwendungen oder individuelle Features benötigen entsprechend mehr Zeit. Wir geben dir zu Beginn eine realistische Einschätzung.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "inhalte",
|
||||
question: "Muss ich Texte und Bilder selbst liefern?",
|
||||
answers: [
|
||||
"Wenn du Inhalte hast, bauen wir diese gerne ein. Falls nicht, unterstützen wir dich mit Textvorschlägen, Icons oder lizenzfreien Bildern.",
|
||||
"Bei Apps helfen wir dir auch bei der Strukturierung und Formulierung von App-Inhalten, z. B. Onboarding-Texte oder UI-Texte.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "technik",
|
||||
question: "Ich habe keine Ahnung von Technik – funktioniert das trotzdem?",
|
||||
answers: [
|
||||
"Auf jeden Fall. Wir begleiten dich Schritt für Schritt und erklären alles verständlich – ganz ohne Fachkenntnisse..",
|
||||
"Du bekommst eine Lösung, die für dich funktioniert – egal ob Website, App oder Backend.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "änderungen",
|
||||
question: "Was ist, wenn ich im Nachhinein etwas ändern möchte?",
|
||||
answers: [
|
||||
"Kein Problem. Du kannst jederzeit neue Inhalte, Features oder Anpassungen beauftragen.",
|
||||
"Auf Wunsch übernehmen wir auch die laufende Wartung oder stellen dir ein CMS bzw. Admin-Interface bereit.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "seo",
|
||||
question: "Wird meine Website oder App auch für Suchmaschinen optimiert?",
|
||||
answers: [
|
||||
"Ja. Jede Website wird suchmaschinenfreundlich aufgebaut – inkl. technischer SEO-Basics wie saubere Struktur, schnelle Ladezeit und mobile Optimierung.",
|
||||
"Bei Apps unterstützen wir dich z.B. auch mit App Store Optimierung (ASO), damit du besser gefunden wirst.",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default function Faq() {
|
||||
return (
|
||||
<section id="faq" className="py-24 px-4 bg-background text-foreground">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-2 text-center"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
Fragen? Antworten.
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-6 bg-amber-500 mx-auto"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<motion.p
|
||||
className="text-sm md:text-base mb-10 text-muted-foreground text-center"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.2}}
|
||||
>
|
||||
Hier beantworten wir häufige Fragen rund um Web- und App-Projekte – klar gegliedert nach Themen.
|
||||
Wenn du darüber hinaus etwas wissen möchtest, melde dich gerne persönlich bei uns.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="text-sm md:text-base mb-10 text-muted-foreground text-center"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.4}}
|
||||
>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full"
|
||||
>
|
||||
{faqItems.map((item, index) => (
|
||||
<AccordionItem key={index} value={`faq-${index}`}>
|
||||
<AccordionTrigger>{item.question}</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-left">
|
||||
{item.answers.map((text, idx) => (
|
||||
<p key={idx}>{text}</p>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
76
frontend/app/(root)/sections/Hero.tsx
Normal file
76
frontend/app/(root)/sections/Hero.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import {motion} from 'framer-motion';
|
||||
import Image from 'next/image';
|
||||
import {Typewriter} from 'react-simple-typewriter';
|
||||
import PulsatingButton from "@/components/PulsatingButton";
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<section id="start" className="relative w-full h-screen overflow-hidden">
|
||||
{/* Background */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/home_hero.jpg"
|
||||
alt="Rhein river aerial view"
|
||||
fill
|
||||
className="object-cover scale-105 blur-sm"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/60"/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className="relative z-10 flex flex-col justify-center items-start h-full w-[90%] sm:w-[80%] max-w-6xl mx-auto text-white">
|
||||
<motion.h1
|
||||
className="text-3xl sm:text-5xl font-bold mb-6 leading-tight"
|
||||
initial={{opacity: 0, y: 40}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.6}}
|
||||
>
|
||||
Digitale Lösungen, <br/> die wirklich passen.
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className="text-lg sm:text-xl text-gray-300 mb-6 max-w-2xl"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.6, delay: 0.2}}
|
||||
>
|
||||
Wir entwickeln individuelle Softwarelösungen für Unternehmen und Startups.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="text-xl font-semibold text-white"
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{delay: 0.6}}
|
||||
>
|
||||
<Typewriter
|
||||
words={['Webdesign', 'App-Entwicklung', 'Interne Tools']}
|
||||
loop={true}
|
||||
cursor
|
||||
cursorStyle="_"
|
||||
typeSpeed={60}
|
||||
deleteSpeed={40}
|
||||
delaySpeed={2000}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div className="mt-10 relative flex items-center justify-center">
|
||||
<PulsatingButton
|
||||
label="Jetzt Kontakt aufnehmen"
|
||||
href="/contact"
|
||||
color="#2563eb" // Tailwind blue-600
|
||||
width={256}
|
||||
pulse
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
115
frontend/app/(root)/sections/HomeServices.tsx
Normal file
115
frontend/app/(root)/sections/HomeServices.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import {motion} from 'framer-motion';
|
||||
import {ChevronRight} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: 'Webdesign',
|
||||
description: 'Moderne Websites, die Vertrauen schaffen und verkaufen.',
|
||||
bullets: [
|
||||
'Maßgeschneidertes Design',
|
||||
'Klare Struktur & überzeugende Inhalte',
|
||||
'Nutzerführung mit System & Strategie',
|
||||
'Für alle Geräte optimiert',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'App-Entwicklung',
|
||||
description: 'Skalierbare Apps für Web und Mobile – von der Idee bis zum Launch.',
|
||||
bullets: [
|
||||
'Plattformübergreifend mit modernen Technologien',
|
||||
'Backend & API-Entwicklung inklusive',
|
||||
'Individuelle Funktionen & Logik',
|
||||
'Stabil, performant & wartbar',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Interne Tools',
|
||||
description: 'Digitale Werkzeuge, die Prozesse vereinfachen und Zeit sparen.',
|
||||
bullets: [
|
||||
'Prozessdigitalisierung & Automatisierung',
|
||||
'Zugeschnitten auf eure Workflows',
|
||||
'Skalierbar & zukunftssicher',
|
||||
'Intuitiv & effizient bedienbar',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const HomeServices = () => {
|
||||
return (
|
||||
<section id="services"
|
||||
className="w-full py-24 bg-background text-foreground">
|
||||
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto">
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-1 text-left"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
Leistungen
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-10 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{services.map((service, index) => (
|
||||
<motion.div
|
||||
key={service.title}
|
||||
className="flex flex-col justify-between h-full p-6 rounded-3xl border bg-muted text-foreground"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: index * 0.1}}
|
||||
whileHover={{
|
||||
scale: 1.03,
|
||||
boxShadow: '0px 12px 30px rgba(0, 0, 0, 0.08)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">{service.title}</h3>
|
||||
<p className="text-muted-foreground mb-4">{service.description}</p>
|
||||
<ul className="space-y-3">
|
||||
{service.bullets.map((point, i) => (
|
||||
<li key={i} className="flex items-start gap-2">
|
||||
<ChevronRight className="w-4 h-4 text-primary mt-1"/>
|
||||
<span className="text-sm text-foreground">{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="mt-12 text-center"
|
||||
initial={{opacity: 0}}
|
||||
whileInView={{opacity: 1}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.3}}
|
||||
>
|
||||
<p className="text-muted-foreground mb-4 text-base md:text-lg">
|
||||
Du möchtest mehr über unsere Leistungen erfahren oder hast ein konkretes Projekt im Kopf?
|
||||
</p>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-sm font-semibold text-primary hover:underline"
|
||||
>
|
||||
Jetzt Kontakt aufnehmen →
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeServices;
|
||||
88
frontend/app/(root)/sections/ProcessSection.tsx
Normal file
88
frontend/app/(root)/sections/ProcessSection.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {VerticalTimeline, VerticalTimelineElement} from 'react-vertical-timeline-component';
|
||||
import {FaRocket, FaLightbulb, FaCode, FaPaperPlane} from 'react-icons/fa';
|
||||
import 'react-vertical-timeline-component/style.min.css';
|
||||
import {motion} from 'framer-motion';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: 'Kick-Off & Strategie',
|
||||
description:
|
||||
'In einem gemeinsamen Auftakt klären wir deine Ziele, Zielgruppen und Herausforderungen. Daraus entsteht ein strukturierter Plan als Basis für alles Weitere.',
|
||||
icon: <FaRocket/>,
|
||||
},
|
||||
{
|
||||
title: 'Konzept & Inhalte',
|
||||
description:
|
||||
'Wir erarbeiten eine klare Struktur und passende Inhalte – abgestimmt auf deine Botschaft und deine Nutzer. So entsteht ein roter Faden für Design und Umsetzung.',
|
||||
icon: <FaLightbulb/>,
|
||||
},
|
||||
{
|
||||
title: 'Design & Entwicklung',
|
||||
description:
|
||||
'Wir gestalten ein modernes Design und setzen es technisch um. Durch regelmäßige Feedback-Schleifen bist du jederzeit im Prozess eingebunden.',
|
||||
icon: <FaCode/>,
|
||||
},
|
||||
{
|
||||
title: 'Go-Live',
|
||||
description:
|
||||
'Nach erfolgreichen Tests geht dein Projekt live. Auch danach begleiten wir dich weiter – für einen reibungslosen Betrieb und mögliche Weiterentwicklungen.',
|
||||
icon: <FaPaperPlane/>,
|
||||
},
|
||||
];
|
||||
|
||||
const ProcessSection = () => {
|
||||
return (
|
||||
<section id="process" className="w-full py-24 bg-background text-foreground">
|
||||
<div className="max-w-6xl px-6 md:px-10 mx-auto">
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-1"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
Unser Prozess
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-12 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<VerticalTimeline
|
||||
lineColor="#eab308"
|
||||
animate={true}
|
||||
|
||||
>
|
||||
{steps.map((step, idx) => (
|
||||
<VerticalTimelineElement
|
||||
key={idx}
|
||||
contentStyle={{
|
||||
background: 'hsl(var(--muted))',
|
||||
color: 'hsl(var(--foreground))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
}}
|
||||
contentArrowStyle={{borderRight: '7px solid hsl(var(--muted))'}}
|
||||
iconStyle={{
|
||||
background: '#eab308',
|
||||
color: '#fff',
|
||||
}}
|
||||
icon={step.icon}
|
||||
>
|
||||
<h3 className="text-xl font-semibold">{step.title}</h3>
|
||||
<p className="text-muted-foreground mt-2">{step.description}</p>
|
||||
</VerticalTimelineElement>
|
||||
))}
|
||||
</VerticalTimeline>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessSection;
|
||||
60
frontend/app/(root)/sections/ReferralSection.tsx
Normal file
60
frontend/app/(root)/sections/ReferralSection.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import {motion} from 'framer-motion'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ReferralSection() {
|
||||
return (
|
||||
<section id="referral" className="py-24 px-4 bg-background/80 text-foreground">
|
||||
<div className="max-w-3xl mx-auto text-center">
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-2"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
Weiterempfehlen lohnt sich
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-6 bg-amber-500 mx-auto"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="text-sm md:text-base mb-8 text-muted-foreground space-y-4"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.2}}
|
||||
>
|
||||
<p>
|
||||
Du empfiehlst uns weiter und dein Kontakt wird Kunde?
|
||||
Als Dank erhältst du <strong>10 % Rabatt</strong> auf dein nächstes Projekt bei uns.
|
||||
</p>
|
||||
<p>
|
||||
Einfach, fair und lohnend – ideal für alle, die mit unserer Arbeit zufrieden sind.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-full font-semibold transition"
|
||||
>
|
||||
Jetzt empfehlen
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
87
frontend/app/(root)/sections/TechStack.tsx
Normal file
87
frontend/app/(root)/sections/TechStack.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import {motion} from 'framer-motion';
|
||||
import {techStack} from "@/constant/TechStack";
|
||||
|
||||
const TechStack = () => {
|
||||
return (
|
||||
<section className="w-full py-20 bg-background text-foreground transition-colors duration-700 ease-in-out">
|
||||
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto">
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-1 text-left"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
Technologien
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-6 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<motion.p
|
||||
className="text-sm md:text-base mb-10 text-muted-foreground"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.3, delay: 0.2}}
|
||||
>
|
||||
Mit diesen Technologien realisieren wir moderne, leistungsstarke Softwarelösungen.
|
||||
</motion.p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{techStack.row1.map((group, index) => (
|
||||
<TechCard key={group.category} group={group} delay={index * 0.2}/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{techStack.row2.map((group, index) => (
|
||||
<TechCard key={group.category} group={group} delay={index * 0.2 + 0.4}/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const TechCard = ({
|
||||
group,
|
||||
delay,
|
||||
}: {
|
||||
group: { category: string; items: { id: string; label: string }[] };
|
||||
delay: number;
|
||||
}) => (
|
||||
<motion.div
|
||||
className="p-4 rounded-lg border bg-muted shadow-md text-foreground"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
whileHover={{scale: 1.03, boxShadow: '0 10px 20px rgba(0,0,0,0.1)'}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay}}
|
||||
>
|
||||
<h3 className="text-base font-semibold mb-4">{group.category}</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{group.items.map(({id, label}) => (
|
||||
<div key={id} className="flex flex-col items-center text-center">
|
||||
<Image
|
||||
src={`/images/svg/${id}.svg`}
|
||||
alt={label}
|
||||
width={32}
|
||||
height={32}
|
||||
className="object-contain"
|
||||
/>
|
||||
<span className="text-[10px] mt-1 text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default TechStack;
|
||||
69
frontend/app/(root)/sections/WhyUs.tsx
Normal file
69
frontend/app/(root)/sections/WhyUs.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import {CheckCircle} from 'lucide-react';
|
||||
import {motion} from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
const points = [
|
||||
'Fertigstellung in 4–8 Wochen',
|
||||
'Fester Ansprechpartner von Anfang bis Ende',
|
||||
'Struktur & Klarheit',
|
||||
'Starkes Alleinstellungsmerkmal',
|
||||
'Zuverlässiges Team mit Weitblick',
|
||||
];
|
||||
|
||||
export default function WhyUs() {
|
||||
return (
|
||||
<section id="whyus" className="py-24 px-4 bg-background text-foreground">
|
||||
<div className="max-w-xl mx-auto">
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-1 text-center"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
Warum wir?
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-10 bg-amber-500 mx-auto"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="rounded-2xl p-8 md:p-10 bg-gradient-to-br from-[#1e3a8a] to-[#2563eb] text-white shadow-xl relative overflow-hidden"
|
||||
initial={{opacity: 0, y: 40}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="mb-6">
|
||||
<Image src="/logo.svg" alt="Rhein Software Logo" width={120} height={32}/>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-4 mb-8">
|
||||
{points.map((point, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<CheckCircle className="text-white w-5 h-5 flex-shrink-0 mt-0.5"/>
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-block bg-white text-blue-700 font-semibold px-6 py-3 rounded-full text-center shadow-md hover:bg-slate-100 transition"
|
||||
>
|
||||
Kostenlose Beratung anfragen
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import {motion} from 'framer-motion';
|
||||
|
||||
const team = [
|
||||
{
|
||||
name: "Thatsaphorn",
|
||||
role: "Gründer & Entwickler",
|
||||
picture: "",
|
||||
name: 'Thatsaphorn',
|
||||
role: 'Gründer & Entwickler',
|
||||
picture: '',
|
||||
},
|
||||
{
|
||||
name: "Anonym",
|
||||
role: "Vertrieb",
|
||||
picture: "",
|
||||
name: 'Anonym',
|
||||
role: 'Vertrieb',
|
||||
picture: '',
|
||||
},
|
||||
];
|
||||
|
||||
const fallbackImage = "/images/team/default-avatar.jpg";
|
||||
const fallbackImage = '/images/team/default-avatar.jpg';
|
||||
|
||||
const TeamSection = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<section className="w-full px-6 sm:px-12 py-16 max-w-6xl mx-auto">
|
||||
<section
|
||||
className="w-full px-6 sm:px-12 py-24 bg-background text-foreground transition-colors duration-700 ease-in-out">
|
||||
<motion.h2
|
||||
className="text-2xl sm:text-3xl font-bold text-left"
|
||||
style={{color: colors.primaryText}}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
@@ -35,6 +32,7 @@ const TeamSection = () => {
|
||||
>
|
||||
Das Team
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-12 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
@@ -45,11 +43,10 @@ const TeamSection = () => {
|
||||
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`grid gap-8
|
||||
grid-cols-1
|
||||
sm:grid-cols-${Math.min(team.length, 2)}
|
||||
md:grid-cols-${Math.min(team.length, 3)}
|
||||
lg:grid-cols-${Math.min(team.length, 4)}`}
|
||||
className={`grid gap-8 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-${Math.min(
|
||||
team.length,
|
||||
4
|
||||
)}`}
|
||||
>
|
||||
{team.map((member, idx) => (
|
||||
<motion.div
|
||||
@@ -59,14 +56,11 @@ const TeamSection = () => {
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: idx * 0.1}}
|
||||
whileHover={{scale: 1.015}}
|
||||
className="flex flex-col items-center text-center
|
||||
rounded-xl border border-gray-200 dark:border-gray-700
|
||||
shadow-md hover:shadow-lg transition-all p-6"
|
||||
style={{backgroundColor: colors.secondaryBg}}
|
||||
className="flex flex-col items-center text-center rounded-xl border border-border shadow-md hover:shadow-lg transition-all p-6 bg-muted"
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{scale: 1.05}}
|
||||
transition={{type: "spring", stiffness: 300, damping: 20}}
|
||||
transition={{type: 'spring', stiffness: 300, damping: 20}}
|
||||
className="w-28 h-28 relative mb-4"
|
||||
>
|
||||
<Image
|
||||
@@ -78,12 +72,10 @@ const TeamSection = () => {
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div className="h-px w-8 bg-gray-300 dark:bg-gray-600 my-4"/>
|
||||
<div className="h-px w-8 bg-border my-4"/>
|
||||
|
||||
<div className="text-lg font-semibold" style={{color: colors.primaryText}}>
|
||||
{member.name}
|
||||
</div>
|
||||
<div className="text-sm mt-1" style={{color: colors.secondaryText}}>
|
||||
<div className="text-lg font-semibold">{member.name}</div>
|
||||
<div className="text-sm mt-1 text-muted-foreground">
|
||||
{member.role}
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -1,37 +0,0 @@
|
||||
import type {Metadata} from "next";
|
||||
import "../globals.css";
|
||||
|
||||
import Nav from "@/components/Navbar/Nav";
|
||||
import Footer from "@/components/Footer/Footer";
|
||||
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||
import React from "react";
|
||||
import {cookies} from "next/headers";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Über Uns | Rhein Software",
|
||||
description: "Rhein Software Development",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||
const bgColor = themeColors[theme].primaryBg;
|
||||
|
||||
return (
|
||||
<html lang="de" data-theme={theme}>
|
||||
<head/>
|
||||
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||
<ThemeProvider>
|
||||
<Nav/>
|
||||
{children}
|
||||
<Footer/>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import AboutContent from "@/components/About/AboutContent";
|
||||
|
||||
const AboutPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<AboutContent/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutPage;
|
||||
@@ -1,37 +0,0 @@
|
||||
import type {Metadata} from "next";
|
||||
import "../globals.css";
|
||||
|
||||
import Nav from "@/components/Navbar/Nav";
|
||||
import Footer from "@/components/Footer/Footer";
|
||||
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||
import React from "react";
|
||||
import {cookies} from "next/headers";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Kontakt | Rhein Software",
|
||||
description: "Rhein Software Development",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||
const bgColor = themeColors[theme].primaryBg;
|
||||
|
||||
return (
|
||||
<html lang="de" data-theme={theme}>
|
||||
<head/>
|
||||
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||
<ThemeProvider>
|
||||
<Nav/>
|
||||
{children}
|
||||
<Footer/>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import React from 'react';
|
||||
import Contact from "@/components/Contact/Contact";
|
||||
import type {Metadata} from "next";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Kontakt | Rhein Software",
|
||||
};
|
||||
}
|
||||
|
||||
const ContactPage = () => {
|
||||
return (
|
||||
|
||||
@@ -25,3 +25,71 @@
|
||||
.animate-float {
|
||||
animation: float 3.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
58
frontend/app/layout.tsx
Normal file
58
frontend/app/layout.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import type {Metadata} from "next";
|
||||
import "./globals.css";
|
||||
|
||||
import Footer from "@/components/Footer/Footer";
|
||||
import {ThemeProvider} from "@/components/theme-provider";
|
||||
import React from "react";
|
||||
import CookieConsentBanner from "@/components/Cookie/CookieConsentBanner";
|
||||
import Navbar from "@/components/Navbar/Navbar";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Rhein Software – Maßgeschneiderte Softwarelösung",
|
||||
description: "Rhein Software bietet individuelle Softwarelösungen für moderne Unternehmen.",
|
||||
keywords: ["Webentwicklung", "Software", "Next.js", "Full Stack", "Rhein Software"],
|
||||
authors: [{name: "Rhein Software"}],
|
||||
creator: "Rhein Software",
|
||||
robots: "index, follow",
|
||||
openGraph: {
|
||||
title: "Rhein Software – Maßgeschneiderte Softwarelösung",
|
||||
description: "Individuelle Softwarelösungen für Unternehmen mit Fokus auf Qualität und Performance.",
|
||||
url: "https://www.rhein-software.dev",
|
||||
siteName: "Rhein Software",
|
||||
locale: "de_DE",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://www.rhein-software.dev/og-image.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Rhein Software – Individuelle Softwarelösung",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="de" suppressHydrationWarning>
|
||||
<body className="antialiased">
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
// disableTransitionOnChange
|
||||
>
|
||||
<Navbar/>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer/>
|
||||
</ThemeProvider>
|
||||
<CookieConsentBanner/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
117
frontend/app/legal/imprint/ImprintComp.tsx
Normal file
117
frontend/app/legal/imprint/ImprintComp.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import {motion} from "framer-motion";
|
||||
|
||||
const fadeInUp = {
|
||||
hidden: {opacity: 0, y: 30},
|
||||
visible: (i: number) => ({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
delay: i * 0.2,
|
||||
ease: "easeOut",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const ImprintComp = () => {
|
||||
const sections = [
|
||||
{
|
||||
title: "Impressum",
|
||||
content: (
|
||||
<>
|
||||
Thatsaphorn Atchariyaphap<br/>
|
||||
Rhein-Software (Einzelunternehmer)<br/>
|
||||
Mühlenstrasse 13<br/>
|
||||
79664 Wehr
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Kontakt",
|
||||
content: (
|
||||
<>
|
||||
Telefon: +49 (0) 151 24003632<br/>
|
||||
E-Mail:{" "}
|
||||
<a
|
||||
href="mailto:contact@rhein-software.dev"
|
||||
className="underline text-primary"
|
||||
>
|
||||
contact@rhein-software.dev
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "EU-Streitschlichtung",
|
||||
content: (
|
||||
<>
|
||||
Die Europäische Kommission stellt eine Plattform zur
|
||||
Online-Streitbeilegung (OS) bereit:{" "}
|
||||
<a
|
||||
href="https://ec.europa.eu/consumers/odr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-primary"
|
||||
>
|
||||
https://ec.europa.eu/consumers/odr/
|
||||
</a>
|
||||
.<br/>
|
||||
Unsere E-Mail-Adresse finden Sie oben im Impressum.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Verbraucherstreitbeilegung / Universalschlichtungsstelle",
|
||||
content: (
|
||||
<>
|
||||
Wir sind nicht bereit oder verpflichtet, an
|
||||
Streitbeilegungsverfahren vor einer
|
||||
Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden bg-background text-foreground">
|
||||
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12 space-y-10 text-base leading-relaxed">
|
||||
{sections.map((section, i) => (
|
||||
<motion.div
|
||||
key={section.title}
|
||||
custom={i}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{once: true, amount: 0.2}}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<h2 className="text-2xl font-bold mb-4">{section.title}</h2>
|
||||
<div className="space-y-4">{section.content}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<motion.p
|
||||
className="text-sm text-muted-foreground"
|
||||
custom={4}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{once: true, amount: 0.2}}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
Quelle:{" "}
|
||||
<a
|
||||
href="https://www.e-recht24.de"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
www.e-recht24.de
|
||||
</a>
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImprintComp;
|
||||
@@ -1,10 +1,17 @@
|
||||
import React from 'react';
|
||||
import ImprintComp from "@/components/Legal/Imprint/ImprintComp";
|
||||
import ImprintComp from "@/app/legal/imprint/ImprintComp";
|
||||
import type {Metadata} from "next";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Impressum | Rhein Software",
|
||||
};
|
||||
}
|
||||
|
||||
const ImprintPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<ImprintComp />
|
||||
<ImprintComp/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import type {Metadata} from "next";
|
||||
import "../globals.css";
|
||||
|
||||
import Nav from "@/components/Navbar/Nav";
|
||||
import Footer from "@/components/Footer/Footer";
|
||||
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||
import React from "react";
|
||||
import {cookies} from "next/headers";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Rechtliches | Rhein Software",
|
||||
description: "Rhein Software Development",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||
const bgColor = themeColors[theme].primaryBg;
|
||||
|
||||
return (
|
||||
<html lang="de" data-theme={theme}>
|
||||
<head/>
|
||||
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||
<ThemeProvider>
|
||||
<Nav/>
|
||||
{children}
|
||||
<Footer/>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,60 @@
|
||||
import React from 'react';
|
||||
'use client'
|
||||
|
||||
const LegalPage = () => {
|
||||
import Link from 'next/link'
|
||||
import {motion} from 'framer-motion'
|
||||
import SmallHero from '@/components/Helper/SmallHero'
|
||||
import {Card, CardContent} from '@/components/ui/card'
|
||||
|
||||
const legalLinks = [
|
||||
{
|
||||
label: 'Impressum',
|
||||
href: '/legal/imprint',
|
||||
},
|
||||
{
|
||||
label: 'Datenschutz',
|
||||
href: '/legal/privacy',
|
||||
},
|
||||
{
|
||||
label: 'Widerrufsrecht',
|
||||
href: '/legal/revocation',
|
||||
},
|
||||
{
|
||||
label: 'Nutzungsbedingungen',
|
||||
href: '/legal/terms-of-use',
|
||||
},
|
||||
]
|
||||
|
||||
export default function LegalOverviewPage() {
|
||||
return (
|
||||
<div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<>
|
||||
<SmallHero
|
||||
title="Rechtliches"
|
||||
subtitle="Alle rechtlich relevanten Informationen auf einen Blick."
|
||||
backgroundImage="/images/contact.png"
|
||||
blurBackground
|
||||
/>
|
||||
|
||||
export default LegalPage;
|
||||
<section className="px-6 sm:px-12 py-16 max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 gap-6"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
{legalLinks.map(({label, href}) => (
|
||||
<Card key={href} className="hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-6">
|
||||
<Link
|
||||
href={href}
|
||||
className="text-primary font-medium text-lg hover:underline"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</motion.div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import React, {useContext} from "react";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
import {motion} from "framer-motion";
|
||||
|
||||
const fadeInUp = {
|
||||
@@ -19,9 +16,6 @@ const fadeInUp = {
|
||||
};
|
||||
|
||||
const PrivacyComp = () => {
|
||||
const {theme} = useContext(ThemeContext);
|
||||
const colors = themeColors[theme];
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: "1. Datenschutz auf einen Blick",
|
||||
@@ -56,16 +50,12 @@ const PrivacyComp = () => {
|
||||
Mühlenstrasse 13
|
||||
<br/>
|
||||
79664 Wehr
|
||||
<br/><br/>
|
||||
<br/>
|
||||
<br/>
|
||||
Telefon: +49 (0) 151 24003632
|
||||
<br/>
|
||||
E-Mail:{" "}
|
||||
<a
|
||||
href="mailto:contact@rhein-software.dev"
|
||||
className="underline text-blue-500"
|
||||
>
|
||||
contact@rhein-software.dev
|
||||
</a>
|
||||
E-Mail: <a href="mailto:contact@rhein-software.dev"
|
||||
className="underline text-primary">contact@rhein-software.dev</a>
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
@@ -102,27 +92,16 @@ const PrivacyComp = () => {
|
||||
{
|
||||
title: "Quelle",
|
||||
content: (
|
||||
<p className="text-sm text-gray-500">
|
||||
Quelle:{" "}
|
||||
<a
|
||||
href="https://www.e-recht24.de"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
www.e-recht24.de
|
||||
</a>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Quelle: <a href="https://www.e-recht24.de" target="_blank" rel="noopener noreferrer"
|
||||
className="underline">www.e-recht24.de</a>
|
||||
</p>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overflow-hidden transition-colors duration-500"
|
||||
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||
>
|
||||
{/* Privacy Content */}
|
||||
<div className="overflow-hidden bg-background text-foreground">
|
||||
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12 space-y-10 text-base leading-relaxed">
|
||||
{sections.map((section, i) => (
|
||||
<motion.div
|
||||
@@ -142,4 +121,4 @@ const PrivacyComp = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyComp;
|
||||
export default PrivacyComp;
|
||||
@@ -1,5 +1,12 @@
|
||||
import React from 'react';
|
||||
import PrivacyComp from "@/components/Legal/Privacy/PrivacyComp";
|
||||
import PrivacyComp from "@/app/legal/privacy/PrivacyComp";
|
||||
import type {Metadata} from "next";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Datenschutz | Rhein Software",
|
||||
};
|
||||
}
|
||||
|
||||
const PrivacyPage = () => {
|
||||
return (
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import RevocationComp from "@/components/Legal/RevocationComp";
|
||||
|
||||
const RevocationPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<RevocationComp />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RevocationPage;
|
||||
98
frontend/app/legal/terms-of-use/TermsOfUseComp.tsx
Normal file
98
frontend/app/legal/terms-of-use/TermsOfUseComp.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import SmallHero from '@/components/Helper/SmallHero';
|
||||
import React from 'react';
|
||||
import {motion} from 'framer-motion';
|
||||
|
||||
const TermsOfUseComp = () => {
|
||||
return (
|
||||
<div className="overflow-hidden bg-background text-foreground">
|
||||
{/* Hero Section */}
|
||||
<div className="mt-[10vh]">
|
||||
<SmallHero
|
||||
title="AGB"
|
||||
subtitle=""
|
||||
backgroundImage="/images/contact.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12">
|
||||
<motion.h2
|
||||
className="text-2xl md:text-3xl font-bold text-center"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.6}}
|
||||
>
|
||||
Schreib uns eine Nachricht
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-center mt-3 text-muted-foreground"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Wir melden uns schnellstmöglich bei dir!
|
||||
</motion.p>
|
||||
|
||||
<form className="mt-8 max-w-2xl mx-auto space-y-6">
|
||||
{/* Name & Email */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{["Dein Name", "Deine E-Mail"].map((label, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: index * 0.2}}
|
||||
>
|
||||
<label className="block font-semibold">{label}</label>
|
||||
<input
|
||||
type={index === 0 ? "text" : "email"}
|
||||
placeholder={index === 0 ? "Max Mustermann" : "max@example.com"}
|
||||
className="w-full p-3 rounded-lg border border-border bg-card text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.5}}
|
||||
>
|
||||
<label className="block font-semibold">Deine Nachricht</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder="Schreibe deine Nachricht..."
|
||||
className="w-full p-3 rounded-lg border border-border bg-card text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<motion.div
|
||||
className="text-center"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.7}}
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-3 bg-blue-600 text-white text-lg font-semibold rounded-lg shadow-md hover:bg-blue-700 transition-all"
|
||||
>
|
||||
📩 Nachricht senden
|
||||
</button>
|
||||
</motion.div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfUseComp;
|
||||
@@ -1,5 +1,12 @@
|
||||
import React from 'react';
|
||||
import TermsOfUseComp from "@/components/Legal/TermsOfUseComp";
|
||||
import TermsOfUseComp from "@/app/legal/terms-of-use/TermsOfUseComp";
|
||||
import type {Metadata} from "next";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Nutzungsbedingungen | Rhein Software",
|
||||
};
|
||||
}
|
||||
|
||||
const TermsOfUsePage = () => {
|
||||
return (
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import type {Metadata} from "next";
|
||||
import "../globals.css";
|
||||
|
||||
import Nav from "@/components/Navbar/Nav";
|
||||
import Footer from "@/components/Footer/Footer";
|
||||
import {ThemeProvider} from "@/components/provider/ThemeProvider";
|
||||
import React from "react";
|
||||
import {cookies} from "next/headers";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Leistungen | Rhein Software",
|
||||
description: "Rhein Software Development",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const cookieStore = await cookies();
|
||||
const theme = cookieStore.get("theme")?.value === "dark" ? "dark" : "light";
|
||||
const bgColor = themeColors[theme].primaryBg;
|
||||
|
||||
return (
|
||||
<html lang="de" data-theme={theme}>
|
||||
<head/>
|
||||
<body className="antialiased" style={{backgroundColor: bgColor}}>
|
||||
<ThemeProvider>
|
||||
<Nav/>
|
||||
{children}
|
||||
<Footer/>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import Services from "@/components/Services/Services";
|
||||
|
||||
const ContactPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<Services/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactPage;
|
||||
21
frontend/components.json
Normal file
21
frontend/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import Section from "@/components/Section";
|
||||
import AboutHero from "@/components/About/Section/AboutHero";
|
||||
import AboutTimeline from "@/components/About/Section/AboutTimeline";
|
||||
import TeamSection from "@/components/About/Section/TeamSection";
|
||||
import AboutIntro from "@/components/About/Section/AboutIntro";
|
||||
import AboutProcess from "@/components/About/Section/AboutProcess";
|
||||
|
||||
const AboutContent = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.7, ease: "easeOut"}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<AboutHero/>
|
||||
</Section>
|
||||
|
||||
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||
<AboutIntro/>
|
||||
</Section>
|
||||
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<AboutProcess/>
|
||||
</Section>
|
||||
|
||||
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||
<AboutTimeline/>
|
||||
</Section>
|
||||
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<TeamSection/>
|
||||
</Section>
|
||||
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutContent;
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import SmallHero from "@/components/Helper/SmallHero";
|
||||
|
||||
const AboutHero = () => {
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
<SmallHero
|
||||
title="Über uns"
|
||||
subtitle="Digitaler Partner für individuelle Softwarelösungen."
|
||||
backgroundImage="/images/contact.png"
|
||||
blurBackground
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutHero;
|
||||
@@ -1,68 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {motion} from 'framer-motion';
|
||||
import {useThemeColors} from '@/utils/useThemeColors';
|
||||
|
||||
const About = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative w-full py-24 transition-colors duration-700 ease-in-out"
|
||||
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||
>
|
||||
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto">
|
||||
<div className="flex flex-col">
|
||||
{/* Text */}
|
||||
<div className="p-0 max-w-4xl">
|
||||
<motion.p
|
||||
className="text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Wir sind Rhein-Software – Ihr Partner für digitale Produkte und individuelle
|
||||
Softwarelösungen.
|
||||
Wir entwickeln skalierbare, wartbare Anwendungen mit klarem Fokus: Technik, die begeistert –
|
||||
von der Architektur bis zum Go-Live.
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
className="mt-6 text-base md:text-lg leading-relaxed transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
Ob Start-up oder etabliertes Unternehmen: Wir begleiten Sie mit einem flexiblen Netzwerk,
|
||||
klarer Kommunikation und hohem Qualitätsanspruch – agil, lösungsorientiert und nah am
|
||||
Projekt.
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* CTA Button */}
|
||||
<motion.div
|
||||
className="mt-10 flex justify-end"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.5}}
|
||||
>
|
||||
{/*<Link href="/about">*/}
|
||||
{/* <button*/}
|
||||
{/* className="flex items-center gap-2 bg-blue-700 hover:bg-blue-900 text-white font-semibold px-5 py-2 rounded-full shadow-lg transition-all"*/}
|
||||
{/* >*/}
|
||||
{/* Mehr über uns <FiArrowRight size={18}/>*/}
|
||||
{/* </button>*/}
|
||||
{/*</Link>*/}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -1,158 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, {useState} from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
const processSteps = [
|
||||
{
|
||||
title: "Beratung",
|
||||
description: (
|
||||
<>
|
||||
In der <strong>Beratungsphase</strong> analysieren wir gemeinsam Ihre Anforderungen und
|
||||
Geschäftsziele. Dabei identifizieren wir Herausforderungen und definieren die Zielsetzung
|
||||
für Ihr Projekt.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Planung",
|
||||
description: (
|
||||
<>
|
||||
Wir erarbeiten ein <strong>technisches Konzept</strong> mit klarer Struktur, Meilensteinen und
|
||||
Ressourcenplanung. Eine solide Architektur bildet die Grundlage für ein skalierbares und wartbares
|
||||
System.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Entwicklung",
|
||||
description: (
|
||||
<>
|
||||
In iterativen Zyklen setzen wir das Projekt um. Regelmäßige <strong>Feedbackschleifen</strong>
|
||||
sorgen dafür, dass das Ergebnis Ihren Erwartungen entspricht und flexibel angepasst werden kann.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Test",
|
||||
description: (
|
||||
<>
|
||||
Durch umfangreiche <strong>Tests und Optimierungen</strong> stellen wir sicher, dass Ihre
|
||||
Anwendung robust, performant und benutzerfreundlich ist – noch vor dem Go-Live.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Go-Live",
|
||||
description: (
|
||||
<>
|
||||
Wir begleiten Sie beim <strong>produktiven Einsatz</strong> Ihrer Anwendung und unterstützen Sie
|
||||
auch nach dem Go-Live mit Support und Weiterentwicklungsmöglichkeiten.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const AboutProcess: React.FC = () => {
|
||||
const colors = useThemeColors();
|
||||
const [activeIndex, setActiveIndex] = useState<number>(0);
|
||||
|
||||
return (
|
||||
<section className="w-full px-6 sm:px-12 py-20 max-w-6xl mx-auto">
|
||||
<h2
|
||||
className="text-2xl sm:text-3xl font-bold text-left"
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
Unser Prozess
|
||||
</h2>
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-12 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
{/* Mobile View: Tab buttons */}
|
||||
<div className="block md:hidden mb-6">
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
{processSteps.map((step, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setActiveIndex(idx)}
|
||||
className={`w-full px-4 py-2 text-sm border rounded-full transition-colors ${
|
||||
activeIndex === idx
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
{step.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop View: 2-column layout */}
|
||||
<div className="hidden md:grid grid-cols-3 gap-8">
|
||||
{/* Left: Step List */}
|
||||
<div className="flex flex-col space-y-4">
|
||||
{processSteps.map((step, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setActiveIndex(idx)}
|
||||
className={`text-left px-4 py-3 border rounded-lg transition-colors ${
|
||||
activeIndex === idx
|
||||
? 'border-blue-600 bg-blue-50 dark:bg-gray-800'
|
||||
: 'border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
<span className="font-semibold">{idx + 1}. {step.title}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right: Step Content */}
|
||||
<div className="md:col-span-2 p-6 border border-gray-300 dark:border-gray-700 rounded-lg"
|
||||
style={{backgroundColor: colors.primaryBg}}>
|
||||
<motion.div
|
||||
key={activeIndex}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-4" style={{color: colors.primaryText}}>
|
||||
{processSteps[activeIndex].title}
|
||||
</h3>
|
||||
<div className="text-base space-y-1" style={{color: colors.secondaryText}}>
|
||||
{processSteps[activeIndex].description}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile View: Content Below Tabs */}
|
||||
<div className="block md:hidden">
|
||||
<div className="p-6 border border-gray-300 dark:border-gray-700 rounded-lg"
|
||||
style={{backgroundColor: colors.primaryBg}}>
|
||||
<motion.div
|
||||
key={activeIndex + '-mobile'}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-4" style={{color: colors.primaryText}}>
|
||||
{activeIndex + 1}. {processSteps[activeIndex].title}
|
||||
</h3>
|
||||
<div className="text-base space-y-1" style={{color: colors.secondaryText}}>
|
||||
{processSteps[activeIndex].description}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutProcess;
|
||||
@@ -1,94 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
const timeline = [
|
||||
{
|
||||
date: "Oktober 2024",
|
||||
title: "Projektgründung",
|
||||
description: "Entwicklung der Idee und erste Umsetzungsschritte – inspiriert durch Technik und Nachhaltigkeit.",
|
||||
current: false,
|
||||
},
|
||||
{
|
||||
date: "Mai 2025",
|
||||
title: "Go-Live",
|
||||
description: "Offizieller Start mit Kundenprojekten und einem umfassenden Full-Service-Angebot.",
|
||||
current: true,
|
||||
},
|
||||
];
|
||||
|
||||
const AboutTimeline3 = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<div className="relative w-full px-6 sm:px-12 py-8 max-w-5xl mx-auto">
|
||||
<h2
|
||||
className="text-2xl sm:text-3xl font-bold mt-10"
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
Von der Idee bis heute
|
||||
</h2>
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-12 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<div className="relative border-l-2 border-gray-300 dark:border-gray-700 ml-6">
|
||||
{timeline.map((item, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: idx * 0.2}}
|
||||
className="relative mb-10 pl-12"
|
||||
>
|
||||
{/* Timeline dot */}
|
||||
<div className="absolute left-[-22px] top-0 z-10">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full border-2 flex items-center justify-center bg-white dark:bg-gray-900 ${
|
||||
item.current
|
||||
? "border-blue-600"
|
||||
: "border-gray-400 dark:border-gray-600"
|
||||
}`}
|
||||
>
|
||||
{item.current && (
|
||||
<motion.span
|
||||
className="absolute w-10 h-10 rounded-full bg-blue-600 opacity-40"
|
||||
animate={{scale: [1, 1.6, 1], opacity: [0.4, 0, 0.4]}}
|
||||
transition={{repeat: Infinity, duration: 1.6}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline card */}
|
||||
<motion.div
|
||||
whileHover={{scale: 1.02, translateX: 4}}
|
||||
transition={{type: "spring", stiffness: 260, damping: 20}}
|
||||
className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-5 border border-gray-200 dark:border-gray-700 cursor-default"
|
||||
>
|
||||
<div className="text-sm text-blue-600 font-semibold mb-1">{item.date}</div>
|
||||
<div
|
||||
className="text-lg font-bold mb-2"
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="text-sm" style={{color: colors.secondaryText}}>
|
||||
{item.description}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutTimeline3;
|
||||
66
frontend/components/CTA/DualCTA.tsx
Normal file
66
frontend/components/CTA/DualCTA.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import {motion} from 'framer-motion';
|
||||
import {ArrowRight} from 'lucide-react';
|
||||
|
||||
export default function DualCTA() {
|
||||
return (
|
||||
<section
|
||||
className="relative py-24 px-6 md:px-16 lg:px-36 bg-gradient-to-br from-background via-muted to-background">
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 30}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.8, ease: 'easeOut'}}
|
||||
viewport={{once: true, amount: 0.3}}
|
||||
className="grid md:grid-cols-2 gap-8 max-w-7xl mx-auto"
|
||||
>
|
||||
<CTABox
|
||||
title="Fündig geworden?"
|
||||
text="Zögern Sie nicht, uns zu kontaktieren – wir helfen Ihnen gerne weiter."
|
||||
href="/contact"
|
||||
linkText="Zum Kontakt"
|
||||
/>
|
||||
<CTABox
|
||||
title="Mehr über uns erfahren?"
|
||||
text="Wir stehen für nahe und zuverlässige Betreuung. Erfahren Sie mehr über unsere Werte."
|
||||
href="/about"
|
||||
linkText="Über uns"
|
||||
/>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CTABox({
|
||||
title,
|
||||
text,
|
||||
href,
|
||||
linkText,
|
||||
}: Readonly<{
|
||||
title: string;
|
||||
text: string;
|
||||
href: string;
|
||||
linkText: string;
|
||||
}>) {
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{scale: 1.015}}
|
||||
className="flex flex-col justify-between h-full border border-border/50 backdrop-blur-sm bg-background/70 text-foreground px-8 py-10 rounded-2xl shadow-lg transition-all duration-300"
|
||||
>
|
||||
<div className="space-y-4 mb-6">
|
||||
<h3 className="text-2xl md:text-3xl font-bold tracking-tight">{title}</h3>
|
||||
<p className="text-base md:text-lg leading-relaxed text-muted-foreground">{text}</p>
|
||||
</div>
|
||||
<div className="w-full flex justify-end">
|
||||
<Link
|
||||
href={href}
|
||||
className="group inline-flex items-center font-semibold text-base md:text-lg hover:text-primary/80 transition-colors text-blue-600"
|
||||
>
|
||||
<span className="group-hover:underline">{linkText}</span>
|
||||
<ArrowRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1"/>
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import ContactHero from "@/components/Contact/Section/ContactHero";
|
||||
import ContactFormSection from "@/components/Contact/Section/ContactFormSection";
|
||||
import Section from "@/components/Section";
|
||||
|
||||
const Contact = () => {
|
||||
const colors = useThemeColors();
|
||||
import {motion} from 'framer-motion'
|
||||
import ContactFormSection from '@/components/Contact/Section/ContactFormSection'
|
||||
import SmallHero from '@/components/Helper/SmallHero'
|
||||
|
||||
export default function Contact() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.7, ease: "easeOut"}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<ContactHero/>
|
||||
</Section>
|
||||
<>
|
||||
<SmallHero
|
||||
title="Kontakt"
|
||||
subtitle="Du hast Fragen oder möchtest ein Projekt besprechen? Schreib uns!"
|
||||
backgroundImage="/images/contact.png"
|
||||
blurBackground
|
||||
/>
|
||||
|
||||
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||
<ContactFormSection/>
|
||||
</Section>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contact;
|
||||
<section className="bg-background text-foreground">
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.6}}
|
||||
>
|
||||
<ContactFormSection/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,69 +1,56 @@
|
||||
'use client';
|
||||
'use client'
|
||||
|
||||
import React, {useState} from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import React, {useState} from 'react'
|
||||
import {motion} from 'framer-motion'
|
||||
// import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
|
||||
const ContactFormSection = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
company: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
message: "",
|
||||
});
|
||||
name: '',
|
||||
email: '',
|
||||
company: '',
|
||||
phone: '',
|
||||
website: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [captchaToken, setCaptchaToken] = useState('')
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
// const hCaptchaSiteKey: string = process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY ?? "null";
|
||||
//
|
||||
// console.log(hCaptchaSiteKey);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setForm({...form, [e.target.name]: e.target.value});
|
||||
};
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setForm({...form, [e.target.name]: e.target.value})
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
// if (!captchaToken) {
|
||||
// setError("Bitte löse das CAPTCHA, um fortzufahren.");
|
||||
// setLoading(false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({...form, captcha: captchaToken}),
|
||||
});
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setSubmitted(true);
|
||||
setForm({name: "", email: "", company: "", phone: "", website: "", message: ""});
|
||||
setSubmitted(true)
|
||||
setForm({name: '', email: '', company: '', phone: '', website: '', message: ''})
|
||||
} else {
|
||||
const resJson = await res.json();
|
||||
setError(resJson?.error || "Ein Fehler ist aufgetreten. Bitte versuche es später erneut.");
|
||||
const resJson = await res.json()
|
||||
setError(resJson?.error || 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.')
|
||||
}
|
||||
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 sm:px-12 py-20 text-left transition-theme">
|
||||
<div className="w-full px-6 sm:px-12 py-20 text-left">
|
||||
<motion.h2
|
||||
className="text-2xl sm:text-3xl font-bold mb-2"
|
||||
style={{color: colors.primaryText}}
|
||||
className="text-2xl sm:text-3xl font-bold mb-2 text-foreground"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
@@ -73,8 +60,7 @@ const ContactFormSection = () => {
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-sm mb-8 max-w-xl"
|
||||
style={{color: colors.secondaryText}}
|
||||
className="text-sm mb-8 max-w-xl text-muted-foreground"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
@@ -84,46 +70,47 @@ const ContactFormSection = () => {
|
||||
</motion.p>
|
||||
|
||||
{submitted ? (
|
||||
<div className="text-green-600 font-semibold text-lg">✅ Deine Nachricht wurde erfolgreich
|
||||
gesendet!</div>
|
||||
<div className="text-green-600 font-semibold text-lg">
|
||||
✅ Deine Nachricht wurde erfolgreich gesendet!
|
||||
</div>
|
||||
) : (
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{
|
||||
label: "Dein Name *",
|
||||
name: "name",
|
||||
type: "text",
|
||||
label: 'Dein Name *',
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
placeholder: "Max Mustermann"
|
||||
placeholder: 'Max Mustermann'
|
||||
},
|
||||
{
|
||||
label: "Deine E-Mail *",
|
||||
name: "email",
|
||||
type: "email",
|
||||
label: 'Deine E-Mail *',
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
placeholder: "max@example.com"
|
||||
placeholder: 'max@example.com'
|
||||
},
|
||||
{
|
||||
label: "Firmenname (optional)",
|
||||
name: "company",
|
||||
type: "text",
|
||||
label: 'Firmenname (optional)',
|
||||
name: 'company',
|
||||
type: 'text',
|
||||
required: false,
|
||||
placeholder: "Mustermann GmbH"
|
||||
placeholder: 'Mustermann GmbH'
|
||||
},
|
||||
{
|
||||
label: "Telefonnummer (optional)",
|
||||
name: "phone",
|
||||
type: "tel",
|
||||
label: 'Telefonnummer (optional)',
|
||||
name: 'phone',
|
||||
type: 'tel',
|
||||
required: false,
|
||||
placeholder: "+49 123 456789"
|
||||
placeholder: '+49 123 456789'
|
||||
},
|
||||
{
|
||||
label: "Webseite (optional)",
|
||||
name: "website",
|
||||
type: "url",
|
||||
label: 'Webseite (optional)',
|
||||
name: 'website',
|
||||
type: 'url',
|
||||
required: false,
|
||||
placeholder: "https://..."
|
||||
placeholder: 'https://...'
|
||||
},
|
||||
].map((field, index) => (
|
||||
<motion.div
|
||||
@@ -133,7 +120,7 @@ const ContactFormSection = () => {
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: index * 0.1}}
|
||||
>
|
||||
<label className="block font-semibold mb-1" style={{color: colors.primaryText}}>
|
||||
<label className="block font-semibold mb-1 text-foreground">
|
||||
{field.label}
|
||||
</label>
|
||||
<input
|
||||
@@ -143,12 +130,7 @@ const ContactFormSection = () => {
|
||||
onChange={handleChange}
|
||||
required={field.required}
|
||||
placeholder={field.placeholder}
|
||||
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
style={{
|
||||
backgroundColor: colors.inputFieldBg,
|
||||
border: `1px solid ${colors.inputBorder}`,
|
||||
color: colors.primaryText,
|
||||
}}
|
||||
className="w-full p-3 rounded-md border bg-background text-foreground border-muted focus:outline-none focus:ring-2 focus:ring-primary transition"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -160,9 +142,7 @@ const ContactFormSection = () => {
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.6}}
|
||||
>
|
||||
<label className="block font-semibold mb-1" style={{color: colors.primaryText}}>
|
||||
Deine Nachricht *
|
||||
</label>
|
||||
<label className="block font-semibold mb-1 text-foreground">Deine Nachricht *</label>
|
||||
<textarea
|
||||
name="message"
|
||||
rows={4}
|
||||
@@ -170,30 +150,27 @@ const ContactFormSection = () => {
|
||||
value={form.message}
|
||||
onChange={handleChange}
|
||||
placeholder="Worum geht es?"
|
||||
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
style={{
|
||||
backgroundColor: colors.inputFieldBg,
|
||||
border: `1px solid ${colors.inputBorder}`,
|
||||
color: colors.primaryText,
|
||||
}}
|
||||
className="w-full p-3 rounded-md border bg-background text-foreground border-muted focus:outline-none focus:ring-2 focus:ring-primary transition"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/*<motion.div*/}
|
||||
{/* className="pt-2"*/}
|
||||
{/* initial={{opacity: 0, y: 10}}*/}
|
||||
{/* whileInView={{opacity: 1, y: 0}}*/}
|
||||
{/* viewport={{once: true}}*/}
|
||||
{/* transition={{duration: 0.5, delay: 0.7}}*/}
|
||||
{/*>*/}
|
||||
{/* <HCaptcha sitekey={hCaptchaSiteKey} onVerify={setCaptchaToken}/>*/}
|
||||
{/*</motion.div>*/}
|
||||
{/*
|
||||
<motion.div
|
||||
className="pt-2"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: 0.7 }}
|
||||
>
|
||||
<HCaptcha sitekey={hCaptchaSiteKey} onVerify={setCaptchaToken} />
|
||||
</motion.div>
|
||||
*/}
|
||||
|
||||
{/*{error && (*/}
|
||||
{/* <div className="text-red-600 font-medium pt-2">*/}
|
||||
{/* ❌ {error}*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
{error && (
|
||||
<div className="text-red-600 font-medium pt-2">
|
||||
❌ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="pt-4 flex justify-end"
|
||||
@@ -205,15 +182,15 @@ const ContactFormSection = () => {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-blue-600 text-white text-sm sm:text-base font-semibold rounded-lg shadow-md hover:bg-blue-700 transition-all disabled:opacity-50"
|
||||
className="px-6 py-3 bg-primary text-white text-sm sm:text-base font-semibold rounded-md shadow hover:bg-primary/90 transition-all disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Sende..." : "📩 Nachricht senden"}
|
||||
{loading ? 'Sende...' : '📩 Nachricht senden'}
|
||||
</button>
|
||||
</motion.div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactFormSection;
|
||||
export default ContactFormSection
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import SmallHero from "@/components/Helper/SmallHero";
|
||||
|
||||
const ContactHero = () => {
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
<SmallHero
|
||||
title="Kontakt"
|
||||
subtitle="Du hast Fragen oder möchtest ein Projekt besprechen? Schreib uns!"
|
||||
backgroundImage="/images/contact.png"
|
||||
blurBackground
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactHero;
|
||||
128
frontend/components/Cookie/CookieConsentBanner.tsx
Normal file
128
frontend/components/Cookie/CookieConsentBanner.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
// CookieConsentBanner.tsx
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function CookieConsentBanner() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stats, setStats] = useState(false);
|
||||
const [personalization, setPersonalization] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const consent = localStorage.getItem('cookie_consent');
|
||||
if (!consent) setVisible(true);
|
||||
|
||||
const show = () => {
|
||||
setVisible(true);
|
||||
};
|
||||
window.addEventListener('show-cookie-banner', show);
|
||||
return () => window.removeEventListener('show-cookie-banner', show);
|
||||
}, []);
|
||||
|
||||
const handleAccept = () => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
localStorage.setItem('cookie_consent', 'true');
|
||||
localStorage.setItem('cookie_stats', stats.toString());
|
||||
localStorage.setItem('cookie_personalization', personalization.toString());
|
||||
setVisible(false);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
setLoading(true);
|
||||
setStats(true);
|
||||
setPersonalization(true);
|
||||
setTimeout(() => {
|
||||
localStorage.setItem('cookie_consent', 'true');
|
||||
localStorage.setItem('cookie_stats', 'true');
|
||||
localStorage.setItem('cookie_personalization', 'true');
|
||||
setVisible(false);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
localStorage.setItem('cookie_consent', 'false');
|
||||
localStorage.setItem('cookie_stats', 'false');
|
||||
localStorage.setItem('cookie_personalization', 'false');
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return visible ? (
|
||||
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center px-4">
|
||||
<div className="bg-white max-w-2xl w-full rounded-xl shadow-lg overflow-hidden text-sm text-gray-800">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold">Wir nutzen Cookies und andere Technologien.</h2>
|
||||
</div>
|
||||
<div className="px-6 py-4 space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
<p>
|
||||
Diese Website nutzt Cookies und vergleichbare Funktionen zur Verarbeitung von
|
||||
Endgeräteinformationen und personenbezogenen Daten. Die Verarbeitung dient der Einbindung von
|
||||
Inhalten, externen Diensten und Elementen Dritter, der statistischen Analyse/Messung, der
|
||||
personalisierten Werbung sowie der Einbindung sozialer Medien.
|
||||
</p>
|
||||
<p>
|
||||
Je nach Funktion werden dabei Daten an Dritte weitergegeben und in Länder außerhalb der EU
|
||||
übertragen, in denen kein angemessenes Datenschutzniveau besteht – z. B. die USA. Ihre
|
||||
Einwilligung ist freiwillig und kann jederzeit über das Symbol unten links widerrufen werden.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<label className="block">
|
||||
<input type="checkbox" defaultChecked disabled className="mr-2"/>
|
||||
Notwendige Cookies (immer aktiv)
|
||||
</label>
|
||||
<label className="block">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={stats}
|
||||
onChange={(e) => setStats(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
Statistiken (z. B. Besuchertracking)
|
||||
</label>
|
||||
<label className="block">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={personalization}
|
||||
onChange={(e) => setPersonalization(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
Personalisierung (z. B. eingebettete Medien, Google Maps)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex flex-col sm:flex-row sm:justify-end gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={handleDecline}
|
||||
className="px-4 py-2 border rounded text-gray-600 hover:bg-gray-100"
|
||||
>
|
||||
Ablehnen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAccept}
|
||||
className="px-4 py-2 border rounded text-gray-700 hover:bg-gray-100 disabled:opacity-60"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Speichere ...' : 'Akzeptieren'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAcceptAll}
|
||||
className="px-4 py-2 bg-red-700 text-white rounded hover:bg-red-800 disabled:opacity-60"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Speichere ...' : 'Alles akzeptieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-3 border-t border-gray-100 text-xs text-gray-500 flex justify-start gap-4">
|
||||
<a href="/legal/imprint" className="hover:underline">Impressum</a>
|
||||
<a href="/legal/privacy" className="hover:underline">Datenschutzerklärung</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
@@ -3,95 +3,109 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import {motion} from 'framer-motion';
|
||||
import {Mail, Gavel, ShieldCheck, Cookie} from 'lucide-react';
|
||||
|
||||
const Footer = () => {
|
||||
const openCookieSettings = () => {
|
||||
window.dispatchEvent(new Event('show-cookie-banner'));
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.footer
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.6, ease: 'easeOut'}}
|
||||
className="py-10 transition-theme text-white"
|
||||
style={{
|
||||
backgroundColor: '#16171f', // modern dark blue-purple tone
|
||||
}}
|
||||
className="py-12 text-white"
|
||||
style={{backgroundColor: '#16171f'}}
|
||||
>
|
||||
<div className="w-[90%] mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-8">
|
||||
{/* Logo and description */}
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-white">
|
||||
<span className="text-3xl md:text-4xl text-pink-700">R</span>hein Software
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.6, delay: 0.2}}
|
||||
className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||
{/* Logo */}
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
<h1 className="text-2xl font-bold">
|
||||
<span className="text-pink-700 text-4xl">R</span>hein Software
|
||||
</h1>
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-400">
|
||||
Individuelle Web- und Appentwicklung mit Qualität und Weitblick.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Informationen */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Informationen</h3>
|
||||
<ul className="mt-4 space-y-4 text-sm font-semibold text-gray-400">
|
||||
<li>
|
||||
<Link href="/contact">
|
||||
<p className="nav_link transition-all duration-300 ease-in-out hover:text-white">
|
||||
Kontakt
|
||||
</p>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.4}}
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-4">Informationen</h3>
|
||||
<ul className="space-y-3 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4"/>
|
||||
<Link href="/contact" className="hover:underline">
|
||||
Kontakt
|
||||
</Link>
|
||||
</li>
|
||||
{/*<li>*/}
|
||||
{/* <Link href="/contact">*/}
|
||||
{/* <p className="nav_link transition-all duration-300 ease-in-out hover:text-white">*/}
|
||||
{/* Zahlung und Versand*/}
|
||||
{/* </p>*/}
|
||||
{/* </Link>*/}
|
||||
{/*</li>*/}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Rechtliches */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Rechtliches</h3>
|
||||
<ul className="mt-4 space-y-4 text-sm font-semibold text-gray-400">
|
||||
{/*<li>*/}
|
||||
{/* <Link href="/legal/terms-of-use">*/}
|
||||
{/* <p className="nav_link transition-all duration-300 ease-in-out hover:text-white">*/}
|
||||
{/* AGB*/}
|
||||
{/* </p>*/}
|
||||
{/* </Link>*/}
|
||||
{/*</li>*/}
|
||||
{/*<li>*/}
|
||||
{/* <Link href="/legal/revocation">*/}
|
||||
{/* <p className="nav_link transition-all duration-300 ease-in-out hover:text-white">*/}
|
||||
{/* Widerruf*/}
|
||||
{/* </p>*/}
|
||||
{/* </Link>*/}
|
||||
{/*</li>*/}
|
||||
<li>
|
||||
<Link href="/legal/privacy">
|
||||
<p className="nav_link transition-all duration-300 ease-in-out hover:text-white">
|
||||
Datenschutz
|
||||
</p>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.5}}
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-4">Rechtliches</h3>
|
||||
<ul className="space-y-3 text-sm text-gray-300">
|
||||
<li className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4"/>
|
||||
<Link href="/legal/privacy" className="hover:underline">
|
||||
Datenschutz
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/legal/imprint">
|
||||
<p className="nav_link transition-all duration-300 ease-in-out hover:text-white">
|
||||
Impressum
|
||||
</p>
|
||||
<li className="flex items-center gap-2">
|
||||
<Gavel className="w-4 h-4"/>
|
||||
<Link href="/legal/imprint" className="hover:underline">
|
||||
Impressum
|
||||
</Link>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Cookie className="w-4 h-4"/>
|
||||
<button
|
||||
onClick={openCookieSettings}
|
||||
className="hover:underline text-left"
|
||||
>
|
||||
Cookie-Einstellungen
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section */}
|
||||
<div
|
||||
className="mt-8 border-t border-gray-600 pt-8 flex flex-col md:flex-row justify-between items-center text-sm text-gray-400">
|
||||
<p className="text-center md:text-left">
|
||||
© 2025 Rhein Software Development. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
className="mt-12 border-t border-gray-700 pt-6 text-center text-sm text-gray-500"
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.6}}
|
||||
>
|
||||
© 2025 Rhein Software Development. Alle Rechte vorbehalten.
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export default Footer;
|
||||
@@ -1,58 +1,59 @@
|
||||
'use client';
|
||||
'use client'
|
||||
|
||||
import React, {useContext} from "react";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
import {motion} from "framer-motion";
|
||||
import {motion} from 'framer-motion'
|
||||
import clsx from 'clsx'
|
||||
|
||||
type SmallHeroProps = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
backgroundImage?: string;
|
||||
blurBackground?: boolean;
|
||||
};
|
||||
title: string
|
||||
subtitle?: string
|
||||
backgroundImage?: string
|
||||
blurBackground?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SmallHero = ({title, subtitle, backgroundImage, blurBackground}: SmallHeroProps) => {
|
||||
const {theme} = useContext(ThemeContext);
|
||||
const colors = themeColors[theme];
|
||||
const SmallHero = ({
|
||||
title,
|
||||
subtitle,
|
||||
backgroundImage,
|
||||
blurBackground,
|
||||
className = 'py-36'
|
||||
}: SmallHeroProps) => {
|
||||
const hasImage = !!backgroundImage
|
||||
|
||||
const primaryTextColor = backgroundImage ? "#ffffff" : colors.primaryText;
|
||||
const secondaryTextColor = backgroundImage ? "rgba(255, 255, 255, 0.8)" : "#6B7280"; // Tailwind gray-500
|
||||
const baseTextColor = hasImage ? 'text-white' : 'text-foreground'
|
||||
const subtitleTextColor = hasImage ? 'text-white/80' : 'text-muted-foreground'
|
||||
|
||||
return (
|
||||
<div className="relative w-full py-36 overflow-hidden">
|
||||
{backgroundImage && blurBackground && (
|
||||
<div className={clsx('relative w-full overflow-hidden', className)}>
|
||||
{hasImage && blurBackground && (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center blur-sm scale-[1.05] z-0 will-change-transform"
|
||||
style={{backgroundImage: `url(${backgroundImage})`}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Text content */}
|
||||
<div className="relative z-10 px-6 sm:px-12 max-w-5xl mx-auto">
|
||||
<div className="relative z-10 px-6 sm:px-12 max-w-6xl mx-auto">
|
||||
<motion.h1
|
||||
className="text-3xl sm:text-4xl font-bold text-left"
|
||||
className={clsx('text-3xl sm:text-4xl font-bold text-left', baseTextColor)}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.6}}
|
||||
style={{color: primaryTextColor}}
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
{subtitle && (
|
||||
<motion.p
|
||||
className="mt-3 text-lg text-left"
|
||||
className={clsx('mt-3 text-lg text-left', subtitleTextColor)}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.6, delay: 0.2}}
|
||||
style={{color: secondaryTextColor}}
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default SmallHero;
|
||||
export default SmallHero
|
||||
@@ -1,34 +0,0 @@
|
||||
export const themeColors: Record<
|
||||
"light" | "dark",
|
||||
{
|
||||
primaryBg: string;
|
||||
secondaryBg: string;
|
||||
navBg: string;
|
||||
footerBg: string;
|
||||
primaryText: string;
|
||||
secondaryText: string;
|
||||
inputFieldBg: string;
|
||||
inputBorder: string;
|
||||
}
|
||||
> = {
|
||||
light: {
|
||||
primaryBg: "#F3F4F6",
|
||||
secondaryBg: "#eff1f3",
|
||||
navBg: "#F9FAFB",
|
||||
footerBg: "#E5E7EB",
|
||||
primaryText: "#1E293B",
|
||||
secondaryText: "#475569",
|
||||
inputFieldBg: "#ffffff",
|
||||
inputBorder: "#cbd5e1",
|
||||
},
|
||||
dark: {
|
||||
primaryBg: "#1A1A23",
|
||||
secondaryBg: "#22222C",
|
||||
navBg: "#2A2A35",
|
||||
footerBg: "#1F1F29",
|
||||
primaryText: "#F0F0F3",
|
||||
secondaryText: "#C0C2CC",
|
||||
inputFieldBg: "#2D2D38",
|
||||
inputBorder: "#4B4B5A",
|
||||
},
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import ContactCTA from "@/components/Home/Sections/ContactCTA";
|
||||
import HomeServices from "@/components/Home/Sections/HomeServices";
|
||||
import TechStack from "@/components/Home/Sections/TechStack";
|
||||
import Section from "@/components/Section";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import Hero from "@/components/Home/Sections/Hero";
|
||||
|
||||
const Home = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.7, ease: "easeOut"}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Section style={{backgroundColor: colors.primaryBg}}>
|
||||
<Hero/>
|
||||
</Section>
|
||||
|
||||
{/*<Section style={{backgroundColor: colors.secondaryBg}} shadow>*/}
|
||||
{/* <About/>*/}
|
||||
{/*</Section>*/}
|
||||
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<HomeServices/>
|
||||
</Section>
|
||||
|
||||
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||
<TechStack/>
|
||||
</Section>
|
||||
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<ContactCTA/>
|
||||
</Section>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import {motion} from 'framer-motion';
|
||||
import {FiArrowRight} from 'react-icons/fi';
|
||||
import {useThemeColors} from '@/utils/useThemeColors';
|
||||
|
||||
type ContactCTAProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
buttonLabel?: string;
|
||||
};
|
||||
|
||||
const ContactCTA = ({
|
||||
title = "Interesse geweckt?",
|
||||
description = "Lass uns über dein Projekt sprechen. Wir freuen uns darauf, deine Ideen in die Realität umzusetzen.",
|
||||
buttonLabel = "Jetzt Kontakt aufnehmen",
|
||||
}: ContactCTAProps) => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative w-full py-24 overflow-hidden transition-colors duration-700 ease-in-out"
|
||||
style={{backgroundColor: colors.primaryBg, color: colors.primaryText}}
|
||||
>
|
||||
<div className="w-full max-w-4xl px-6 md:px-10 mx-auto text-center">
|
||||
<motion.h2 className="text-3xl md:text-4xl font-bold">
|
||||
{title}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="mt-4 text-sm md:text-base max-w-xl mx-auto"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="mt-8 flex justify-center"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
<Link href="/contact">
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-6 py-3 text-sm md:text-base font-semibold rounded-full bg-blue-700 hover:bg-blue-900 text-white shadow-md transition-all duration-300"
|
||||
>
|
||||
{buttonLabel} <FiArrowRight size={18}/>
|
||||
</button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactCTA;
|
||||
@@ -1,100 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import {Typewriter} from 'react-simple-typewriter';
|
||||
import {motion} from 'framer-motion';
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<div
|
||||
className="relative w-full pt-[4vh] md:pt-[12vh] h-screen flex flex-col overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: 'var(--primary-bg)',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/home_hero.jpg"
|
||||
alt="Rhein river aerial view"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="blur-md scale-105"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40"/>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 flex justify-center flex-col w-[90%] sm:w-[80%] h-full mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 items-center gap-12">
|
||||
{/* Text Content */}
|
||||
<div>
|
||||
<motion.h1
|
||||
className="text-3xl sm:text-4xl md:text-5xl mt-6 mb-6 font-bold text-white"
|
||||
initial={{opacity: 0, y: 30}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.6, ease: 'easeOut'}}
|
||||
>
|
||||
Rhein-Software Development
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className="text-lg md:text-xl text-gray-400"
|
||||
initial={{opacity: 0, y: 30}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.6, delay: 0.2, ease: 'easeOut'}}
|
||||
>
|
||||
Digitale Lösungen für dein Unternehmen.
|
||||
</motion.p>
|
||||
|
||||
<motion.p
|
||||
className="mt-4 text-lg md:text-xl font-semibold text-white"
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{delay: 0.9, duration: 0.6}}
|
||||
>
|
||||
<Typewriter
|
||||
words={['Beratung', 'Entwicklung', 'Wartung', 'Fehlerbehebung']}
|
||||
loop={true}
|
||||
cursor
|
||||
cursorStyle="_"
|
||||
typeSpeed={60}
|
||||
deleteSpeed={40}
|
||||
delaySpeed={1500}
|
||||
/>
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
{/* Floating Image */}
|
||||
<motion.div
|
||||
className="hidden lg:block"
|
||||
initial={{opacity: 0, y: 30}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.8, ease: 'easeOut', delay: 0.3}}
|
||||
>
|
||||
<motion.div
|
||||
animate={{y: [0, -10, 0]}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
className="animate-float"
|
||||
>
|
||||
<Image
|
||||
src="/images/hero.png"
|
||||
alt="hero graphic"
|
||||
width={700}
|
||||
height={700}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
@@ -1,105 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {FiServer, FiTool, FiMonitor, FiZap, FiArrowRight} from 'react-icons/fi';
|
||||
import {motion} from 'framer-motion';
|
||||
import {useThemeColors} from '@/utils/useThemeColors';
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: 'Beratung',
|
||||
icon: <FiMonitor size={24}/>,
|
||||
description:
|
||||
'Strategische und technische Beratung rund um digitale Produkte und Prozesse. Wir analysieren bestehende Systeme, identifizieren Potenziale und helfen dir, die passende Architektur für dein Projekt zu finden.',
|
||||
},
|
||||
{
|
||||
title: 'Entwicklung',
|
||||
icon: <FiZap size={24}/>,
|
||||
description:
|
||||
'Individuelle Softwareentwicklung – skalierbar, wartbar, zukunftssicher. Ob Web-App, API oder internes Tool: Wir setzen moderne Technologien ein, um genau die Lösung zu bauen, die du brauchst.',
|
||||
},
|
||||
{
|
||||
title: 'Managed Services',
|
||||
icon: <FiServer size={24}/>,
|
||||
description:
|
||||
'Wir betreuen Infrastruktur, Server und Systeme – verlässlich und performant. Unser Team kümmert sich um Hosting, Monitoring, Backups und sorgt für einen reibungslosen Betrieb deiner Plattform.',
|
||||
},
|
||||
{
|
||||
title: 'Fehlerbehebung',
|
||||
icon: <FiTool size={24}/>,
|
||||
description:
|
||||
'Schnelle Hilfe bei Bugs, Performance-Problemen oder Sicherheitslücken. Wir analysieren Probleme, beheben sie gezielt und sorgen dafür, dass dein System wieder stabil läuft – langfristig und zuverlässig.',
|
||||
},
|
||||
];
|
||||
|
||||
const HomeServices = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<section
|
||||
className="w-full py-24 transition-colors duration-700 ease-in-out"
|
||||
style={{backgroundColor: colors.primaryBg}}
|
||||
>
|
||||
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto" style={{color: colors.primaryText}}>
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-1 text-left transition-colors duration-700 ease-in-out"
|
||||
>
|
||||
Leistungen
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-10 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{services.map((service, index) => (
|
||||
<motion.div
|
||||
key={service.title}
|
||||
className="p-6 rounded-xl border shadow-md transition-colors duration-700 ease-in-out"
|
||||
style={{
|
||||
backgroundColor: colors.secondaryBg,
|
||||
borderColor: colors.secondaryBg,
|
||||
color: colors.primaryText,
|
||||
}}
|
||||
whileHover={{
|
||||
scale: 1.03,
|
||||
boxShadow: '0px 10px 20px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
initial={{opacity: 0, y: 30}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: index * 0.1}}
|
||||
>
|
||||
<div className="mb-3 text-blue-600">{service.icon}</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{service.title}</h3>
|
||||
<p className="text-sm leading-relaxed transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}>
|
||||
{service.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="mt-10 flex justify-end"
|
||||
initial={{opacity: 0}}
|
||||
whileInView={{opacity: 1}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.3}}
|
||||
>
|
||||
<a
|
||||
href="/services"
|
||||
className="text-sm font-semibold text-blue-600 hover:underline flex items-center gap-1"
|
||||
>
|
||||
Weitere Leistungen <FiArrowRight size={16}/>
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeServices;
|
||||
@@ -1,139 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import {motion} from 'framer-motion';
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
const techStack = {
|
||||
row1: [
|
||||
{
|
||||
category: 'Programmiersprachen & Frameworks – Backend',
|
||||
items: [
|
||||
{id: 'java', label: 'Java'},
|
||||
{id: 'dart', label: 'Dart'},
|
||||
{id: 'kotlin', label: 'Kotlin'},
|
||||
{id: 'spring', label: 'Spring'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Programmiersprachen & Frameworks – Frontend',
|
||||
items: [
|
||||
{id: 'html', label: 'HTML'},
|
||||
{id: 'css', label: 'CSS'},
|
||||
{id: 'bootstrap', label: 'Bootstrap'},
|
||||
{id: 'nextjs', label: 'Next.js'},
|
||||
{id: 'typescript', label: 'TypeScript'},
|
||||
{id: 'flutter', label: 'Flutter'},
|
||||
],
|
||||
},
|
||||
],
|
||||
row2: [
|
||||
{
|
||||
category: 'Betriebssysteme',
|
||||
items: [
|
||||
{id: 'macos', label: 'macOS'},
|
||||
{id: 'debian', label: 'Debian'},
|
||||
{id: 'ubuntu', label: 'Ubuntu'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Version Control & Collaboration',
|
||||
items: [
|
||||
{id: 'gitlab', label: 'GitLab'},
|
||||
{id: 'outline', label: 'Outline'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'DevOps & Infrastruktur',
|
||||
items: [
|
||||
{id: 'gitlab-ci', label: 'GitLab CI'},
|
||||
{id: 'docker', label: 'Docker'},
|
||||
{id: 'proxmox', label: 'Proxmox'},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const TechStack = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<section
|
||||
className="w-full py-20 transition-colors duration-700 ease-in-out"
|
||||
style={{backgroundColor: colors.secondaryBg}}
|
||||
>
|
||||
<div className="w-full max-w-6xl px-6 md:px-10 mx-auto" style={{color: colors.primaryText}}>
|
||||
<motion.h2
|
||||
className="text-3xl md:text-4xl font-bold mb-1 text-left transition-colors duration-700 ease-in-out"
|
||||
>
|
||||
Technologien
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mt-2 mb-6 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
|
||||
<motion.p
|
||||
className="text-sm md:text-base mb-10 transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}
|
||||
>
|
||||
Mit diesen Technologien realisieren wir moderne, leistungsstarke Softwarelösungen.
|
||||
</motion.p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{techStack.row1.map((group, index) => (
|
||||
<TechCard key={group.category} group={group} delay={index * 0.2} colors={colors}/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{techStack.row2.map((group, index) => (
|
||||
<TechCard key={group.category} group={group} delay={index * 0.2 + 0.4} colors={colors}/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const TechCard = ({
|
||||
group,
|
||||
delay,
|
||||
colors,
|
||||
}: {
|
||||
group: { category: string; items: { id: string; label: string }[] };
|
||||
delay: number;
|
||||
colors: ReturnType<typeof useThemeColors>;
|
||||
}) => (
|
||||
<motion.div
|
||||
className="p-4 rounded-lg border shadow-md transition-colors duration-700 ease-in-out"
|
||||
style={{
|
||||
backgroundColor: colors.primaryBg,
|
||||
borderColor: colors.primaryBg,
|
||||
color: colors.primaryText,
|
||||
}}
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
whileHover={{scale: 1.03, boxShadow: '0 10px 20px rgba(0,0,0,0.1)'}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay}}
|
||||
>
|
||||
<h3 className="text-base font-semibold mb-4">{group.category}</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{group.items.map(({id, label}) => (
|
||||
<div key={id} className="flex flex-col items-center text-center">
|
||||
<Image src={`/images/svg/${id}.svg`} alt={label} width={32} height={32} className="object-contain"/>
|
||||
<span className="text-[10px] mt-1 transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
export default TechStack;
|
||||
@@ -1,131 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, {useContext} from "react";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
import {motion} from "framer-motion";
|
||||
|
||||
const fadeInUp = {
|
||||
hidden: {opacity: 0, y: 30},
|
||||
visible: (i: number) => ({
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
delay: i * 0.2,
|
||||
ease: "easeOut",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const ImprintComp = () => {
|
||||
const {theme} = useContext(ThemeContext);
|
||||
const colors = themeColors[theme];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overflow-hidden transition-colors duration-500"
|
||||
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
|
||||
>
|
||||
{/* Imprint Content */}
|
||||
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12 space-y-10 text-base leading-relaxed">
|
||||
{[
|
||||
{
|
||||
title: "Impressum",
|
||||
content: (
|
||||
<>
|
||||
Thatsaphorn Atchariyaphap<br/>
|
||||
Rhein-Software (Einzelunternehmer)<br/>
|
||||
Mühlenstrasse 13<br/>
|
||||
79664 Wehr
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Kontakt",
|
||||
content: (
|
||||
<>
|
||||
Telefon: +49 (0) 151 24003632<br/>
|
||||
E-Mail:{" "}
|
||||
<a
|
||||
href="mailto:contact@rhein-software.dev"
|
||||
className="underline text-blue-500"
|
||||
>
|
||||
contact@rhein-software.dev
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "EU-Streitschlichtung",
|
||||
content: (
|
||||
<>
|
||||
Die Europäische Kommission stellt eine Plattform zur
|
||||
Online-Streitbeilegung (OS) bereit:{" "}
|
||||
<a
|
||||
href="https://ec.europa.eu/consumers/odr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-blue-500"
|
||||
>
|
||||
https://ec.europa.eu/consumers/odr/
|
||||
</a>
|
||||
.<br/>
|
||||
Unsere E-Mail-Adresse finden Sie oben im Impressum.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Verbraucherstreitbeilegung / Universalschlichtungsstelle",
|
||||
content: (
|
||||
<>
|
||||
Wir sind nicht bereit oder verpflichtet, an
|
||||
Streitbeilegungsverfahren vor einer
|
||||
Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</>
|
||||
),
|
||||
},
|
||||
].map((section, i) => (
|
||||
<motion.div
|
||||
key={section.title}
|
||||
custom={i}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<h2 className="text-2xl font-bold">{section.title}</h2>
|
||||
<motion.div
|
||||
className="w-6 h-[2px] mt-2 mb-6 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
<p>{section.content}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<motion.p
|
||||
className="text-sm text-gray-500"
|
||||
custom={4}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={fadeInUp}
|
||||
>
|
||||
Quelle:{" "}
|
||||
<a
|
||||
href="https://www.e-recht24.de"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
www.e-recht24.de
|
||||
</a>
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImprintComp;
|
||||
@@ -1,98 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import SmallHero from "@/components/Helper/SmallHero";
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
import AOS from "aos";
|
||||
|
||||
const RevocationComp = () => {
|
||||
const {theme} = useContext(ThemeContext);
|
||||
const colors = themeColors[theme];
|
||||
|
||||
useEffect(() => {
|
||||
AOS.init({
|
||||
duration: 1000,
|
||||
easing: "ease",
|
||||
once: true,
|
||||
anchorPlacement: "top-bottom",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden transition-colors duration-500"
|
||||
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="mt-[10vh]">
|
||||
<SmallHero
|
||||
title="Widerruf"
|
||||
subtitle=""
|
||||
backgroundImage="/images/contact.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-center"
|
||||
data-aos="fade-up"
|
||||
data-aos-delay="400"
|
||||
>
|
||||
Schreib uns eine Nachricht
|
||||
</h2>
|
||||
<p data-aos="fade-up" data-aos-delay="600"
|
||||
className="text-center mt-3 text-[var(--secondary-text)]">
|
||||
Wir melden uns schnellstmöglich bei dir!
|
||||
</p>
|
||||
|
||||
<form className="mt-8 max-w-2xl mx-auto space-y-6">
|
||||
{/* Name & Email */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{["Dein Name", "Deine E-Mail"].map((label, index) => (
|
||||
<div key={index} data-aos="fade-up" data-aos-delay={index * 100}>
|
||||
<label className="block font-semibold">{label}</label>
|
||||
<input
|
||||
type={index === 0 ? "text" : "email"}
|
||||
placeholder={index === 0 ? "Max Mustermann" : "max@example.com"}
|
||||
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
style={{
|
||||
backgroundColor: colors.inputFieldBg,
|
||||
border: `1px solid ${colors.inputBorder}`,
|
||||
color: colors.primaryText
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div data-aos="fade-up" data-aos-delay="300">
|
||||
<label className="block font-semibold">Deine Nachricht</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder="Schreibe deine Nachricht..."
|
||||
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
style={{
|
||||
backgroundColor: colors.inputFieldBg,
|
||||
border: `1px solid ${colors.inputBorder}`,
|
||||
color: colors.primaryText
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="text-center" data-aos="fade-up" data-aos-delay="400">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-3 bg-blue-600 text-white text-lg font-semibold rounded-lg shadow-md hover:bg-blue-700 transition-all"
|
||||
>
|
||||
📩 Nachricht senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);};
|
||||
|
||||
export default RevocationComp;
|
||||
@@ -1,99 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import SmallHero from "@/components/Helper/SmallHero";
|
||||
import React, {useContext, useEffect} from "react";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
import AOS from "aos";
|
||||
|
||||
const TermsOfUseComp = () => {
|
||||
const {theme} = useContext(ThemeContext);
|
||||
const colors = themeColors[theme];
|
||||
|
||||
useEffect(() => {
|
||||
AOS.init({
|
||||
duration: 1000,
|
||||
easing: "ease",
|
||||
once: true,
|
||||
anchorPlacement: "top-bottom",
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden transition-colors duration-500"
|
||||
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="mt-[10vh]">
|
||||
<SmallHero
|
||||
title="AGB"
|
||||
subtitle=""
|
||||
backgroundImage="/images/contact.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="mt-16 w-[90%] sm:w-[80%] mx-auto py-12">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-center"
|
||||
data-aos="fade-up"
|
||||
data-aos-delay="400"
|
||||
>
|
||||
Schreib uns eine Nachricht
|
||||
</h2>
|
||||
<p data-aos="fade-up" data-aos-delay="600"
|
||||
className="text-center mt-3 text-[var(--secondary-text)]">
|
||||
Wir melden uns schnellstmöglich bei dir!
|
||||
</p>
|
||||
|
||||
<form className="mt-8 max-w-2xl mx-auto space-y-6">
|
||||
{/* Name & Email */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{["Dein Name", "Deine E-Mail"].map((label, index) => (
|
||||
<div key={index} data-aos="fade-up" data-aos-delay={index * 100}>
|
||||
<label className="block font-semibold">{label}</label>
|
||||
<input
|
||||
type={index === 0 ? "text" : "email"}
|
||||
placeholder={index === 0 ? "Max Mustermann" : "max@example.com"}
|
||||
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
style={{
|
||||
backgroundColor: colors.inputFieldBg,
|
||||
border: `1px solid ${colors.inputBorder}`,
|
||||
color: colors.primaryText
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div data-aos="fade-up" data-aos-delay="300">
|
||||
<label className="block font-semibold">Deine Nachricht</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder="Schreibe deine Nachricht..."
|
||||
className="w-full p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
|
||||
style={{
|
||||
backgroundColor: colors.inputFieldBg,
|
||||
border: `1px solid ${colors.inputBorder}`,
|
||||
color: colors.primaryText
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="text-center" data-aos="fade-up" data-aos-delay="400">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-3 bg-blue-600 text-white text-lg font-semibold rounded-lg shadow-md hover:bg-blue-700 transition-all"
|
||||
>
|
||||
📩 Nachricht senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfUseComp;
|
||||
@@ -1,110 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {usePathname} from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {HiBars3BottomRight} from "react-icons/hi2";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import {navLinks} from "@/constant/Constant";
|
||||
|
||||
type Props = {
|
||||
openNav: () => void;
|
||||
};
|
||||
|
||||
const Nav = ({openNav}: Props) => {
|
||||
const [navBg, setNavBg] = useState(false);
|
||||
const [navHeight, setNavHeight] = useState("h-[10vh]");
|
||||
const [contentSize, setContentSize] = useState("text-base md:text-lg");
|
||||
const [buttonSize, setButtonSize] = useState("md:px-6 md:py-2 px-4 py-1 text-sm");
|
||||
|
||||
const {theme, toggleTheme} = useContext(ThemeContext);
|
||||
const colors = useThemeColors();
|
||||
const pathname = usePathname();
|
||||
|
||||
const navColorClass = theme === "dark" || !navBg ? "text-white" : "text-black";
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
if (window.scrollY >= 90) {
|
||||
setNavBg(true);
|
||||
setNavHeight("h-[8vh]");
|
||||
setContentSize("text-sm md:text-base");
|
||||
setButtonSize("md:px-5 md:py-1.5 px-3 py-1 text-xs");
|
||||
} else {
|
||||
setNavBg(false);
|
||||
setNavHeight("h-[10vh]");
|
||||
setContentSize("text-base md:text-lg");
|
||||
setButtonSize("md:px-6 md:py-2 px-4 py-1 text-sm");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handler);
|
||||
return () => window.removeEventListener("scroll", handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed w-full transition-all duration-300 ease-in-out ${navHeight} z-[1000] ${
|
||||
navBg ? "shadow-md" : ""
|
||||
}`}
|
||||
style={{backgroundColor: navBg ? colors.navBg : "transparent"}}
|
||||
>
|
||||
<div className="flex items-center h-full justify-between w-[90%] xl:w-[80%] mx-auto">
|
||||
<Link href="/">
|
||||
<h1 className={`${contentSize} font-bold cursor-pointer ${navColorClass}`}>
|
||||
<span className="text-lg md:text-xl text-pink-700">R</span>hein Software
|
||||
</h1>
|
||||
</Link>
|
||||
|
||||
<div className="hidden lg:flex items-center space-x-6">
|
||||
{navLinks.map((link) => (
|
||||
<Link href={link.url} key={link.id}>
|
||||
<p className={`relative group ${contentSize} uppercase ${getNavLinkClasses(pathname === link.url, navBg, theme, navColorClass)}`}>
|
||||
{link.label}
|
||||
{pathname !== link.url && (
|
||||
<span
|
||||
className="absolute bottom-0 left-0 w-full h-[2px] bg-current transform transition-transform duration-300 origin-right scale-x-0 group-hover:scale-x-100"/>
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link href="/contact">
|
||||
<button
|
||||
className={`${buttonSize} text-white font-semibold bg-blue-700 hover:bg-blue-900 rounded-full`}>
|
||||
Kontakt
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className={`w-7 h-7 flex items-center justify-center rounded-full ${navColorClass}`}
|
||||
style={{backgroundColor: colors.secondaryBg}}
|
||||
>
|
||||
{theme === "dark" ? "🌙" : "☀️"}
|
||||
</button>
|
||||
<HiBars3BottomRight
|
||||
onClick={openNav}
|
||||
className={`w-6 h-6 cursor-pointer lg:hidden ${navColorClass}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getNavLinkClasses = (
|
||||
isActive: boolean,
|
||||
navBg: boolean,
|
||||
theme: string,
|
||||
navColorClass: string
|
||||
): string => {
|
||||
if (isActive) return !navBg ? "text-white font-bold" : `${navColorClass} font-bold`;
|
||||
if (!navBg) return "text-white font-medium";
|
||||
return theme === "dark" ? "text-gray-300 font-medium" : "text-gray-700 font-medium";
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {navLinks} from "@/constant/Constant";
|
||||
import Link from "next/link";
|
||||
import React, {useContext} from "react";
|
||||
import {CgClose} from "react-icons/cg";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
type Props = {
|
||||
showNav: boolean;
|
||||
closeNav: () => void;
|
||||
};
|
||||
|
||||
const MobileNav = ({closeNav, showNav}: Props) => {
|
||||
const navOpen = showNav ? "translate-y-0 opacity-100" : "-translate-y-20 opacity-0 pointer-events-none";
|
||||
const {theme, toggleTheme} = useContext(ThemeContext);
|
||||
const colors = useThemeColors();
|
||||
|
||||
const textClass = theme === "dark" ? "text-white" : "text-black";
|
||||
|
||||
return (
|
||||
<div className="lg:hidden">
|
||||
<div
|
||||
className={`fixed inset-0 z-[10000] transition-opacity duration-500 ${
|
||||
showNav ? "opacity-60 bg-black" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
onClick={closeNav}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-full z-[10006] transform ${navOpen} transition-all duration-500 ease-in-out shadow-md rounded-b-2xl`}
|
||||
style={{backgroundColor: colors.navBg}}
|
||||
>
|
||||
<div className={`flex flex-col items-center justify-center py-8 space-y-4 px-4 relative ${textClass}`}>
|
||||
<CgClose
|
||||
onClick={closeNav}
|
||||
className={`absolute top-4 right-6 sm:right-8 sm:w-7 sm:h-7 w-6 h-6 cursor-pointer p-1 ${textClass}`}
|
||||
/>
|
||||
|
||||
{navLinks.map((link) => (
|
||||
<Link href={link.url} key={link.id}>
|
||||
<p className="nav__link uppercase text-[14px] sm:text-[16px] border-b pb-1 border-gray-400 transition-all duration-300 ease-in-out hover:scale-105">
|
||||
{link.label}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className={`mt-4 w-8 h-8 flex items-center justify-center rounded-full border border-gray-400 transition-all duration-300 ${textClass}`}
|
||||
style={{backgroundColor: colors.secondaryBg}}
|
||||
>
|
||||
{theme === "dark" ? "🌙" : "☀️"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileNav;
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, {useState} from "react";
|
||||
import DesktopNav from "./DesktopNav";
|
||||
import MobileNav from "./MobileNav";
|
||||
|
||||
const Nav = () => {
|
||||
const [showNav, setShowNav] = useState(false);
|
||||
const handleNavShow = () => {
|
||||
setShowNav(true);
|
||||
};
|
||||
const handleNavHide = () => {
|
||||
setShowNav(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DesktopNav openNav={handleNavShow}/>
|
||||
<MobileNav showNav={showNav} closeNav={handleNavHide}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
102
frontend/components/Navbar/Navbar.tsx
Normal file
102
frontend/components/Navbar/Navbar.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import {usePathname, useRouter} from 'next/navigation';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Sheet, SheetContent, SheetTrigger} from '@/components/ui/sheet';
|
||||
import {Menu} from 'lucide-react';
|
||||
import {ThemeToggle} from '@/components/theme-toggle';
|
||||
|
||||
const navLinks = [
|
||||
{id: 'services', label: 'Leistungen'},
|
||||
{id: 'about', label: 'Über uns'},
|
||||
{id: 'process', label: 'Ablauf'},
|
||||
{id: 'whyus', label: 'Warum wir'},
|
||||
{id: 'referral', label: 'Empfehlung'},
|
||||
{id: 'faq', label: 'FAQ'},
|
||||
];
|
||||
|
||||
const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const handleNavClick = (id: string) => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
if (pathname === '/') {
|
||||
const el = document.getElementById(id)
|
||||
if (el) {
|
||||
el.scrollIntoView({behavior: 'smooth', block: 'start'})
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('scrollToId', id)
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 sm:px-6 lg:px-8 flex justify-center mt-4 z-50 fixed">
|
||||
<header
|
||||
className="bg-background/50 backdrop-blur-md border shadow-lg rounded-xl w-full max-w-screen-xl py-3 px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => handleNavClick('start')}
|
||||
className="text-xl font-bold cursor-pointer"
|
||||
>
|
||||
<span className="text-pink-600">R</span>hein Software
|
||||
</button>
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden lg:flex items-center gap-6">
|
||||
{navLinks.map((link) => (
|
||||
<button
|
||||
key={link.id}
|
||||
onClick={() => handleNavClick(link.id)}
|
||||
className="cursor-pointer text-sm font-medium text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/contact">Kontakt</Link>
|
||||
</Button>
|
||||
|
||||
<ThemeToggle/>
|
||||
</nav>
|
||||
|
||||
{/* Mobile nav */}
|
||||
<div className="lg:hidden flex items-center gap-3">
|
||||
<ThemeToggle/>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Menu className="h-5 w-5"/>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="top" className="pt-10">
|
||||
<div className="flex flex-col space-y-4 text-center">
|
||||
{navLinks.map((link) => (
|
||||
<button
|
||||
key={link.id}
|
||||
onClick={() => handleNavClick(link.id)}
|
||||
className="cursor-pointer text-base font-semibold text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</button>
|
||||
))}
|
||||
<Button asChild className="mt-4 w-full">
|
||||
<Link href="/contact">Kontakt</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
136
frontend/components/PulsatingButton.tsx
Normal file
136
frontend/components/PulsatingButton.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import styled, {keyframes} from 'styled-components';
|
||||
|
||||
type PulsatingButtonProps = {
|
||||
label: string;
|
||||
href: string;
|
||||
color?: string;
|
||||
textColor?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
pulse?: boolean;
|
||||
};
|
||||
|
||||
const MAX_LAYERS = 2;
|
||||
|
||||
const pulse = keyframes`
|
||||
0%, 10% {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
80% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.15, 1.4);
|
||||
}
|
||||
81%, 100% {
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
}
|
||||
`;
|
||||
|
||||
const scale = keyframes`
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
35%, 80% {
|
||||
transform: scale(1.1, 1.35);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Layer = styled.div<{
|
||||
color: string;
|
||||
$width: number;
|
||||
$height: number;
|
||||
$layer: number;
|
||||
}>`
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
border: 1px solid ${({color, $layer}) =>
|
||||
$layer === 0 ? `${color}AA` : `${color}66`}; // L0: ~67%, L1: ~40%
|
||||
border-radius: 9999px;
|
||||
animation: ${({$layer}) => ($layer ? pulse : scale)} 1.5s infinite;
|
||||
width: ${({$width, $layer}) => $width + $layer * 8}px;
|
||||
height: ${({$height, $layer}) => $height + $layer * 8}px;
|
||||
z-index: ${({$layer}) => MAX_LAYERS - $layer};
|
||||
`;
|
||||
|
||||
// Use $-prefix to avoid prop leaking
|
||||
const StyledButton = styled.a.withConfig({
|
||||
shouldForwardProp: (prop) =>
|
||||
!['$bgColor', '$textColor', '$width', '$height'].includes(prop),
|
||||
})<{
|
||||
$bgColor: string;
|
||||
$textColor: string;
|
||||
$width: number;
|
||||
$height: number;
|
||||
}>`
|
||||
position: relative;
|
||||
z-index: ${MAX_LAYERS + 1};
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({$bgColor}) => $bgColor};
|
||||
color: ${({$textColor}) => $textColor};
|
||||
border-radius: 9999px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
width: ${({$width}) => $width}px;
|
||||
height: ${({$height}) => $height}px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`;
|
||||
|
||||
const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
||||
label,
|
||||
href,
|
||||
color = '#3B82F6', // Tailwind blue-500
|
||||
textColor = '#ffffff',
|
||||
width = 160,
|
||||
height = 48,
|
||||
pulse = true,
|
||||
}) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
{pulse &&
|
||||
Array.from({length: MAX_LAYERS}).map((_, index) => (
|
||||
<Layer
|
||||
key={index}
|
||||
color={color}
|
||||
$width={width}
|
||||
$height={height}
|
||||
$layer={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Link href={href} passHref legacyBehavior>
|
||||
<StyledButton
|
||||
$bgColor={color}
|
||||
$textColor={textColor}
|
||||
$width={width}
|
||||
$height={height}
|
||||
>
|
||||
{label}
|
||||
</StyledButton>
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PulsatingButton;
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {motion, AnimatePresence} from 'framer-motion';
|
||||
import {FiZap, FiMonitor, FiServer, FiTool} from 'react-icons/fi';
|
||||
import Development from "@/components/Services/Section/overview/Development";
|
||||
import Consulting from "@/components/Services/Section/overview/Consulting";
|
||||
import ManagedServices from "@/components/Services/Section/overview/ManagedServices";
|
||||
import BugFixing from "@/components/Services/Section/overview/BugFixing";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
const tabs = [
|
||||
{key: 'entwicklung', label: 'Entwicklung', icon: <FiZap size={20}/>},
|
||||
{key: 'beratung', label: 'Beratung', icon: <FiMonitor size={20}/>},
|
||||
{key: 'services', label: 'Managed Services', icon: <FiServer size={20}/>},
|
||||
{key: 'support', label: 'Fehlerbehebung', icon: <FiTool size={20}/>},
|
||||
];
|
||||
|
||||
const tabContent: Record<string, React.ReactNode> = {
|
||||
entwicklung: <Development/>,
|
||||
beratung: <Consulting/>,
|
||||
services: <ManagedServices/>,
|
||||
support: <BugFixing/>,
|
||||
};
|
||||
|
||||
const OverviewTabs = () => {
|
||||
const [activeTab, setActiveTab] = useState("entwicklung");
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-6 py-16 transition-theme text-left">
|
||||
<h2 className="text-3xl font-bold mb-2" style={{color: colors.primaryText}}>
|
||||
Was wir tun
|
||||
</h2>
|
||||
|
||||
<motion.div
|
||||
className="w-12 h-[2px] mb-6 bg-amber-500"
|
||||
initial={{opacity: 0, x: -20}}
|
||||
whileInView={{opacity: 1, x: 0}}
|
||||
viewport={{once: true}}
|
||||
transition={{duration: 0.4, delay: 0.1}}
|
||||
/>
|
||||
<p className="text-sm mb-10" style={{color: colors.secondaryText}}>
|
||||
In diesem Abschnitt geben wir dir einen Überblick über unsere zentralen Leistungen – von der technischen
|
||||
Entwicklung über Beratung bis hin zu Betrieb und Support.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mb-10">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className="flex-1 min-w-[150px] flex items-center justify-start gap-2 px-4 py-2 rounded-full border text-sm font-medium transition-all"
|
||||
style={{
|
||||
color: activeTab === tab.key ? '#ffffff' : colors.primaryText,
|
||||
backgroundColor: activeTab === tab.key ? '#1D4ED8' : 'transparent',
|
||||
borderColor: activeTab === tab.key ? '#1D4ED8' : colors.inputBorder,
|
||||
}}
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: -10}}
|
||||
transition={{duration: 0.4}}
|
||||
>
|
||||
{tabContent[activeTab]}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewTabs;
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import SmallHero from "@/components/Helper/SmallHero";
|
||||
|
||||
const ServiceHero = () => {
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
<SmallHero
|
||||
title="Unsere Leistungen"
|
||||
subtitle="Wir bieten maßgeschneiderte Lösungen – von der Beratung bis zum Betrieb."
|
||||
backgroundImage="/images/contact.png"
|
||||
blurBackground
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceHero;
|
||||
@@ -1,59 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
const BugFixing = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[95%] mx-auto grid grid-cols-1 gap-6 mt-8 mb-16 text-left"
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
<div>
|
||||
<motion.h2
|
||||
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5}}
|
||||
>
|
||||
🐞 Fehlerbehebung & Optimierung
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-sm font-medium leading-7"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Wir analysieren und beheben Fehler in bestehenden Systemen, optimieren die Performance und sorgen
|
||||
dafür, dass deine Software stabil und zuverlässig läuft.
|
||||
</motion.p>
|
||||
|
||||
<motion.h3
|
||||
className="mt-8 text-lg font-semibold"
|
||||
initial={{opacity: 0}}
|
||||
whileInView={{opacity: 1}}
|
||||
transition={{duration: 0.4, delay: 0.2}}
|
||||
>
|
||||
🔎 Fokusbereiche
|
||||
</motion.h3>
|
||||
|
||||
<ul
|
||||
className="list-disc list-inside text-sm mt-4 space-y-1"
|
||||
style={{color: colors.secondaryText}}
|
||||
>
|
||||
<li>Debugging & Troubleshooting</li>
|
||||
<li>Performance-Analyse</li>
|
||||
<li>Refactoring von Legacy-Code</li>
|
||||
<li>Stabilitäts- und Sicherheitsupdates</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BugFixing;
|
||||
@@ -1,60 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
const Consulting = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[95%] mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 mb-16 text-left"
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
<div>
|
||||
<motion.h2
|
||||
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5}}
|
||||
>
|
||||
🧠 Technische Beratung
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-sm font-medium leading-7"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
whileInView={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Wir unterstützen dich dabei, technische Entscheidungen fundiert zu treffen – von der Auswahl
|
||||
geeigneter Technologien bis hin zur Planung skalierbarer Architekturen. Gemeinsam finden wir den
|
||||
effizientesten Weg von der Idee bis zur Umsetzung – praxisnah, zielgerichtet und verständlich.
|
||||
</motion.p>
|
||||
|
||||
<motion.h3
|
||||
className="mt-8 text-lg font-semibold"
|
||||
initial={{opacity: 0}}
|
||||
whileInView={{opacity: 1}}
|
||||
transition={{duration: 0.4, delay: 0.2}}
|
||||
>
|
||||
🔍 Themenbereiche
|
||||
</motion.h3>
|
||||
|
||||
<ul
|
||||
className="list-disc list-inside text-sm mt-4 space-y-1"
|
||||
style={{color: colors.secondaryText}}
|
||||
>
|
||||
<li>Software-Architektur & Microservices</li>
|
||||
<li>Technologie- und Framework-Auswahl</li>
|
||||
<li>Prototyping & Machbarkeitsanalysen</li>
|
||||
<li>Projektplanung und agile Methodik</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Consulting;
|
||||
@@ -1,210 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import Image from "next/image";
|
||||
|
||||
const techStack = {
|
||||
row1: [
|
||||
{
|
||||
category: 'Programmiersprachen & Frameworks – Backend',
|
||||
items: [
|
||||
{id: 'java', label: 'Java'},
|
||||
{id: 'dart', label: 'Dart'},
|
||||
{id: 'kotlin', label: 'Kotlin'},
|
||||
{id: 'spring', label: 'Spring'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Programmiersprachen & Frameworks – Frontend',
|
||||
items: [
|
||||
{id: 'html', label: 'HTML'},
|
||||
{id: 'css', label: 'CSS'},
|
||||
{id: 'bootstrap', label: 'Bootstrap'},
|
||||
{id: 'nextjs', label: 'Next.js'},
|
||||
{id: 'typescript', label: 'TypeScript'},
|
||||
{id: 'flutter', label: 'Flutter'},
|
||||
],
|
||||
},
|
||||
],
|
||||
row2: [
|
||||
{
|
||||
category: 'Betriebssysteme',
|
||||
items: [
|
||||
{id: 'macos', label: 'macOS'},
|
||||
{id: 'debian', label: 'Debian'},
|
||||
{id: 'ubuntu', label: 'Ubuntu'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Version Control & Collaboration',
|
||||
items: [
|
||||
{id: 'gitlab', label: 'GitLab'},
|
||||
{id: 'outline', label: 'Outline'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'DevOps & Infrastruktur',
|
||||
items: [
|
||||
{id: 'gitlab-ci', label: 'GitLab CI'},
|
||||
{id: 'docker', label: 'Docker'},
|
||||
{id: 'proxmox', label: 'Proxmox'},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const TechCard = ({group}: {
|
||||
group: { category: string; items: { id: string; label: string }[] };
|
||||
}) => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.4}}
|
||||
className="p-4 rounded-lg border shadow-md transition-colors duration-700 ease-in-out"
|
||||
style={{
|
||||
backgroundColor: colors.primaryBg,
|
||||
borderColor: colors.primaryBg,
|
||||
color: colors.primaryText,
|
||||
}}
|
||||
>
|
||||
<h3 className="text-base font-semibold mb-4">{group.category}</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{group.items.map(({id, label}) => (
|
||||
<div key={id} className="flex flex-col items-center text-center">
|
||||
<Image
|
||||
src={`/images/svg/${id}.svg`}
|
||||
alt={label}
|
||||
width={32}
|
||||
height={32}
|
||||
className="object-contain"
|
||||
/>
|
||||
<span
|
||||
className="text-[10px] mt-1 transition-colors duration-700 ease-in-out"
|
||||
style={{color: colors.secondaryText}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const Development = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[95%] mx-auto grid grid-cols-1 gap-6 mt-8 mb-16 text-left"
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
<div>
|
||||
<motion.h2
|
||||
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5}}
|
||||
>
|
||||
💻 Full-Stack Entwicklung
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-sm font-medium leading-7"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Wir entwickeln individuelle Softwarelösungen – von der nativen Mobile-App über moderne Webseiten bis
|
||||
hin zu internen Tools.
|
||||
Unser Fokus liegt auf skalierbaren Architekturen, performanten Frontends und wartbaren Backends.
|
||||
<br/><br/>
|
||||
Egal ob API-Entwicklung, Admin-Dashboard oder komplexe Plattform – wir setzen moderne Technologien
|
||||
gezielt ein, um robuste, zukunftssichere Anwendungen zu realisieren.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="mt-6 text-sm font-medium space-y-3 pl-2"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">🚀</span>
|
||||
<p>Native Mobile-Apps mit Flutter</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">🌐</span>
|
||||
<p>Webseiten & Web-Portale mit Next.js</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">🧩</span>
|
||||
<p>Skalierbare Backends mit Spring Boot</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">📊</span>
|
||||
<p>Individuelle Dashboards & Admin-Panels</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">🔌</span>
|
||||
<p>API-Entwicklung (REST & GraphQL)</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">⚙️</span>
|
||||
<p>Automatisierte interne Tools</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">📦</span>
|
||||
<p>CI/CD & Container mit GitLab CI & Docker</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.h3
|
||||
className="mt-10 text-lg font-semibold"
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.4, delay: 0.2}}
|
||||
>
|
||||
🔧 Unser Tech Stack im Überblick
|
||||
</motion.h3>
|
||||
|
||||
<motion.p
|
||||
className="text-sm font-medium mb-4"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Mit diesem Stack entwickeln wir robuste, moderne Softwarelösungen – abgestimmt auf deine
|
||||
Anforderungen.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.5, delay: 0.1}}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{techStack.row1.map((group) => (
|
||||
<TechCard key={group.category} group={group}/>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{techStack.row2.map((group) => (
|
||||
<TechCard key={group.category} group={group}/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Development;
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
|
||||
const ManagedServices = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[95%] mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 mb-16 text-left"
|
||||
style={{color: colors.primaryText}}
|
||||
>
|
||||
<div>
|
||||
<motion.h2
|
||||
className="text-xl sm:text-2xl md:text-3xl font-bold mb-4"
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5}}
|
||||
>
|
||||
🛠️ Managed Services
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-sm font-medium leading-7"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.2}}
|
||||
>
|
||||
Wir übernehmen den Betrieb und die Wartung deiner Anwendungen – zuverlässig, sicher und skalierbar.
|
||||
So kannst du dich voll auf dein Geschäft konzentrieren.
|
||||
</motion.p>
|
||||
|
||||
<motion.h3
|
||||
className="mt-8 text-lg font-semibold"
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.4, delay: 0.2}}
|
||||
>
|
||||
🧰 Leistungen
|
||||
</motion.h3>
|
||||
|
||||
<motion.ul
|
||||
className="list-disc list-inside text-sm mt-4 space-y-1"
|
||||
style={{color: colors.secondaryText}}
|
||||
initial={{opacity: 0, y: 10}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.5, delay: 0.3}}
|
||||
>
|
||||
<li>Monitoring & Logging</li>
|
||||
<li>Security Updates & Wartung</li>
|
||||
<li>Cloud Deployment & Hosting</li>
|
||||
<li>24/7 Systemüberwachung</li>
|
||||
</motion.ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagedServices;
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from "react";
|
||||
import {motion} from "framer-motion";
|
||||
import {useThemeColors} from "@/utils/useThemeColors";
|
||||
import ServiceHero from "@/components/Services/Section/ServiceHero";
|
||||
import OverviewTabs from "@/components/Services/Section/OverviewTabs";
|
||||
import ContactCTA from "@/components/Home/Sections/ContactCTA";
|
||||
import Section from "@/components/Section";
|
||||
|
||||
const Home = () => {
|
||||
const colors = useThemeColors();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 20}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.7, ease: "easeOut"}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<ServiceHero/>
|
||||
</Section>
|
||||
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
|
||||
<OverviewTabs/>
|
||||
</Section>
|
||||
<Section style={{backgroundColor: colors.primaryBg}} shadow>
|
||||
<ContactCTA
|
||||
title="Nichts Passendes gefunden?"
|
||||
description="Nimm einfach Kontakt mit uns auf – gemeinsam finden wir die passende Lösung für dein Vorhaben."
|
||||
buttonLabel="Kontakt aufnehmen"
|
||||
/>
|
||||
</Section>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,50 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {createContext, useEffect, useState} from "react";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
type ThemeType = "light" | "dark";
|
||||
|
||||
export const ThemeContext = createContext<{
|
||||
theme: ThemeType;
|
||||
toggleTheme: () => void;
|
||||
}>({
|
||||
theme: "light",
|
||||
toggleTheme: () => {
|
||||
},
|
||||
});
|
||||
|
||||
export const ThemeProvider = ({children}: { children: React.ReactNode }) => {
|
||||
const [theme, setTheme] = useState<ThemeType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = Cookies.get("theme") as ThemeType | undefined;
|
||||
if (saved === "dark" || saved === "light") {
|
||||
setTheme(saved);
|
||||
} else {
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const defaultTheme: ThemeType = prefersDark ? "dark" : "light";
|
||||
setTheme(defaultTheme);
|
||||
Cookies.set("theme", defaultTheme, {expires: 365});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!theme) return;
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const next = theme === "dark" ? "light" : "dark";
|
||||
setTheme(next);
|
||||
Cookies.set("theme", next, {expires: 365});
|
||||
};
|
||||
|
||||
if (!theme) return null;
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{theme, toggleTheme}}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
11
frontend/components/theme-provider.tsx
Normal file
11
frontend/components/theme-provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {ThemeProvider as NextThemesProvider} from "next-themes"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: Readonly<React.ComponentProps<typeof NextThemesProvider>>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
26
frontend/components/theme-toggle.tsx
Normal file
26
frontend/components/theme-toggle.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:scale-0 dark:-rotate-90" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:scale-100 dark:rotate-0" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
57
frontend/components/ui/accordion.tsx
Normal file
57
frontend/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
57
frontend/components/ui/button.tsx
Normal file
57
frontend/components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
76
frontend/components/ui/card.tsx
Normal file
76
frontend/components/ui/card.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
201
frontend/components/ui/dropdown-menu.tsx
Normal file
201
frontend/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
128
frontend/components/ui/navigation-menu.tsx
Normal file
128
frontend/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
))
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
||||
)
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
))
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
))
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
140
frontend/components/ui/sheet.tsx
Normal file
140
frontend/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
49
frontend/constant/TechStack.ts
Normal file
49
frontend/constant/TechStack.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export const techStack = {
|
||||
row1: [
|
||||
{
|
||||
category: 'Programmiersprachen & Frameworks – Backend',
|
||||
items: [
|
||||
{id: 'java', label: 'Java'},
|
||||
{id: 'dart', label: 'Dart'},
|
||||
{id: 'kotlin', label: 'Kotlin'},
|
||||
{id: 'spring', label: 'Spring'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Programmiersprachen & Frameworks – Frontend',
|
||||
items: [
|
||||
{id: 'html', label: 'HTML'},
|
||||
{id: 'css', label: 'CSS'},
|
||||
{id: 'bootstrap', label: 'Bootstrap'},
|
||||
{id: 'nextjs', label: 'Next.js'},
|
||||
{id: 'typescript', label: 'TypeScript'},
|
||||
{id: 'flutter', label: 'Flutter'},
|
||||
],
|
||||
},
|
||||
],
|
||||
row2: [
|
||||
{
|
||||
category: 'Betriebssysteme',
|
||||
items: [
|
||||
{id: 'macos', label: 'macOS'},
|
||||
{id: 'debian', label: 'Debian'},
|
||||
{id: 'ubuntu', label: 'Ubuntu'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Version Control & Collaboration',
|
||||
items: [
|
||||
{id: 'gitlab', label: 'GitLab'},
|
||||
{id: 'outline', label: 'Outline'},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'DevOps & Infrastruktur',
|
||||
items: [
|
||||
{id: 'gitlab-ci', label: 'GitLab CI'},
|
||||
{id: 'docker', label: 'Docker'},
|
||||
{id: 'proxmox', label: 'Proxmox'},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
6
frontend/lib/utils.ts
Normal file
6
frontend/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
1370
frontend/package-lock.json
generated
1370
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,16 +9,30 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"aos": "^2.3.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.6.5",
|
||||
"js-cookie": "^3.0.5",
|
||||
"next": "15.1.7",
|
||||
"lucide-react": "^0.523.0",
|
||||
"next": "15.2.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nodemailer": "^6.10.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-simple-typewriter": "^5.0.1"
|
||||
"react-scroll": "^1.9.3",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
"react-vertical-timeline-component": "^3.5.3",
|
||||
"styled-components": "^6.1.19",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@@ -30,6 +44,8 @@
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-scroll": "^1.8.10",
|
||||
"@types/react-vertical-timeline-component": "^3.3.6",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
|
||||
@@ -1,12 +1,85 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./utils/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {},
|
||||
plugins: [],
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0'
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
}
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
},
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {useContext} from "react";
|
||||
import {ThemeContext} from "@/components/provider/ThemeProvider";
|
||||
import {themeColors} from "@/components/Helper/ThemeColors";
|
||||
|
||||
export const useThemeColors = () => {
|
||||
const {theme} = useContext(ThemeContext);
|
||||
return themeColors[theme];
|
||||
};
|
||||
Reference in New Issue
Block a user