68 Commits

Author SHA1 Message Date
8990548c6a Refactor contact API route to enhance captcha validation 2025-04-27 18:41:14 +02:00
ddad30bcf8 Update ESLint config and optimize error handling 2025-04-21 09:05:59 +02:00
c497657093 revert 2025-04-21 02:24:39 +02:00
e5b40ba9f7 rename back to "err" 2025-04-21 02:21:37 +02:00
31df2409de Add fallback for lint warnings in CI 2025-04-21 02:18:37 +02:00
055455db6e Update object and function formatting 2025-04-21 02:13:49 +02:00
01492fbe18 Add nodemailer to production dependencies 2025-04-21 02:09:22 +02:00
3ecdde1455 Integrate hCaptcha and email handling into contact form
- Added hCaptcha integration for client-side validation and backend verification.
- Implemented Nodemailer for contact form submissions via email.
2025-04-21 02:07:19 +02:00
21e83d6ee7 Add required fields and optional inputs in contact form 2025-04-21 01:33:18 +02:00
14de4c8d88 Merge branch 'legal-sites' into 'dev'
Legal sites

See merge request rheinsw/website!32
2025-04-20 23:04:33 +00:00
ecef42837b Merge branch 'legal-sites' into 'dev'
Legal sites

See merge request rheinsw/website!32
2025-04-20 23:04:33 +00:00
5c168b4ceb Merge branch 'homepage-refactoring-pt4' into 'dev'
Homepage refactoring pt4

See merge request rheinsw/website!31
2025-04-20 09:42:40 +00:00
f2221815c8 Merge branch 'homepage-refactoring-pt4' into 'dev'
Homepage refactoring pt4

See merge request rheinsw/website!31
2025-04-20 09:42:40 +00:00
GitLab CI
5e1bca87a3 Merge remote-tracking branch 'origin/production' into dev 2025-04-16 17:43:21 +00:00
5970898bc9 Merge branch 'homepage-refactoring' into 'dev'
Homepage Refactoring - Pt .3

See merge request rheinsw/website!28
2025-04-16 17:37:41 +00:00
5c60594a4f Merge branch 'homepage-refactoring' into 'dev'
Homepage Refactoring - Pt .3

See merge request rheinsw/website!28
2025-04-16 17:37:41 +00:00
66cdadea16 Merge remote-tracking branch 'origin/production' into dev 2025-04-16 19:22:29 +02:00
f7b17e239c Merge branch 'homepage-refactoring' into 'dev'
Refactor the whole page.

See merge request rheinsw/website!26
2025-04-11 19:08:43 +00:00
14ec81f4c2 Merge branch 'homepage-refactoring' into 'dev'
Refactor the whole page.

See merge request rheinsw/website!26
2025-04-11 19:08:43 +00:00
d19a90ce9a Merge branch 'homepage-refactoring' into 'dev'
Homepage Refactoring - Pt. 1

See merge request rheinsw/website!25
2025-04-08 21:03:41 +00:00
f0a401a9a4 Remove unused variable cleanDarkBackground 2025-04-08 23:00:19 +02:00
6535bf37ec Added "Services" page & refactor layout structure
- Introduced a new Services component with basic setup.
- Updated layout files to ensure consistent global styling references.
- Enhanced Navbar with pathname detection for active link styling.
- Fixed navigation link URL for "Leistungen" in constants.
2025-04-08 22:57:58 +02:00
493d5bcfa2 Update Navbar contact button 2025-04-08 22:47:51 +02:00
5226cd1bd4 Update Navbar contact button 2025-04-08 22:42:32 +02:00
2d89fcfbb8 Refactor and update navigation and constants
- Refactor code for consistency in formatting and styling.
- Update `navLinks` to revise URLs and labels for improved clarity.
- Adjust button text in `DesktopNav` from "Portal" to "Kontakt".
2025-04-08 22:39:16 +02:00
63062734d2 Simplify Hero section subtitle text 2025-04-08 21:38:22 +02:00
c44c713925 Add react-simple-typewriter and integrate in Hero component
- Added `react-simple-typewriter` dependency to project.
- Integrated a typewriter effect in the Hero component for dynamic text.
2025-04-06 19:18:04 +02:00
a5454882ed Add floating animation to hero image and update text styles
- Introduce keyframes for float animation in globals.css.
- Apply float animation to the hero image and update text to white.
2025-04-06 19:01:31 +02:00
c9eb4e3c42 Fixed an issue where the screen flickers when reloading the page.
- Add theme detection based on cookies across layouts
2025-04-06 18:26:48 +02:00
f581783abf Fix wrong import path 2025-04-06 18:16:50 +02:00
4193e45592 Enhance Navbar responsiveness and theming
- Refactor `MobileNav` with theme context support and improved animations.
- Adjust `DesktopNav` background behavior for better clarity.
- Update `Hero` image style with scaling for visual impact.
2025-04-06 00:20:59 +02:00
35e3632011 Add background image and improve Hero section layout 2025-04-06 00:12:49 +02:00
82a43f48fd Moved Navbar-related components out of 'Home' directory. 2025-04-05 23:57:08 +02:00
481aa0fe63 Revert some changes 2025-04-05 23:28:12 +02:00
261354e96d Update GitLab CI cache paths
- Add `.next/cache/` to cache paths for improved performance.
2025-04-05 22:58:22 +02:00
0835299999 Merge branch 'fix-pipeline' into 'dev'
(revert) Remove unused 'policy: pull' configuration

See merge request rheinsw/website!24
2025-04-04 21:51:39 +00:00
4fd4c0e53c (revert) Remove unused 'policy: pull' configuration 2025-04-04 23:49:56 +02:00
576d784874 Merge branch 'fix-pipeline' into 'dev'
Remove unused 'policy: pull' configuration

See merge request rheinsw/website!23
2025-04-04 21:47:43 +00:00
2ba3576be9 Remove unused 'policy: pull' configuration 2025-04-04 23:44:52 +02:00
a37ef7c199 Merge branch 'fix-pipeline' into 'dev'
Fix pipeline

See merge request rheinsw/website!22
2025-04-04 21:27:57 +00:00
5f492e2e63 Pin Node.js base image to specific digest 2025-04-04 23:25:30 +02:00
1cc3f4857f Update Node.js image to version 22 2025-04-04 23:20:02 +02:00
f5dc50a488 Use npm ci for dependency installation 2025-04-04 22:40:27 +02:00
abcdc926ba Revert 2025-04-04 22:30:05 +02:00
90e7fcced0 Update postcss.config.mjs file
Attempt to fix the following error:
#5 13.62 Error: It looks like you're trying to use tailwindcss directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install @tailwindcss/postcss and update your PostCSS configuration.
2025-04-04 22:05:41 +02:00
33192468a9 Update Next.js to ^15.2.4 2025-04-04 20:53:51 +02:00
3ea1a264e9 (revert) Update PostCSS plugins configuration to fix an issue
#5 12.48 Failed to compile.
#5 12.48
#5 12.48 ./app/(root)/globals.css.webpack[javascript/auto]!=!./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[2]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[3]!./app/(root)/globals.css
#5 12.48 Error: It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install `@tailwindcss/postcss` and update your PostCSS configuration.
#5 12.48     at Oe (/app/node_modules/tailwindcss/dist/lib.js:33:1925)
#5 12.48     at LazyResult.runOnRoot (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:329:16)
#5 12.48     at LazyResult.runAsync (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:258:26)
#5 12.48     at LazyResult.async (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:160:30)
#5 12.48     at LazyResult.then (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:404:17)
#5 12.48
#5 12.48 Import trace for requested module:
#5 12.48 ./app/(root)/globals.css.webpack[javascript/auto]!=!./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[2]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[3]!./app/(root)/globals.css
#5 12.48 ./app/(root)/globals.css
2025-04-04 20:50:21 +02:00
668605522b Update PostCSS plugins configuration to fix an issue
#5 12.48 Failed to compile.
#5 12.48
#5 12.48 ./app/(root)/globals.css.webpack[javascript/auto]!=!./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[2]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[3]!./app/(root)/globals.css
#5 12.48 Error: It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install `@tailwindcss/postcss` and update your PostCSS configuration.
#5 12.48     at Oe (/app/node_modules/tailwindcss/dist/lib.js:33:1925)
#5 12.48     at LazyResult.runOnRoot (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:329:16)
#5 12.48     at LazyResult.runAsync (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:258:26)
#5 12.48     at LazyResult.async (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:160:30)
#5 12.48     at LazyResult.then (/app/node_modules/next/node_modules/postcss/lib/lazy-result.js:404:17)
#5 12.48
#5 12.48 Import trace for requested module:
#5 12.48 ./app/(root)/globals.css.webpack[javascript/auto]!=!./node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[2]!./node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[13].oneOf[10].use[3]!./app/(root)/globals.css
#5 12.48 ./app/(root)/globals.css
2025-04-04 20:49:13 +02:00
ee7cf14598 Merge branch 'navbar-refactoring' into 'dev'
Update @tailwindcss dependencies to version 4.1.3

See merge request rheinsw/website!20
2025-04-04 18:42:59 +00:00
26837bf575 Update @tailwindcss dependencies to version 4.1.3 2025-04-04 20:40:39 +02:00
2eb936fddf Merge branch 'navbar-refactoring' into 'dev'
Enhance MobileNav with theme context and visual updates

See merge request rheinsw/website!19
2025-04-04 18:36:44 +00:00
3c25d1a351 Merge branch 'navbar-refactoring' into 'dev'
Enhance MobileNav with theme context and visual updates

See merge request rheinsw/website!19
2025-04-04 18:36:44 +00:00
6882e6b460 Remove unused variable cleanDarkBackground 2025-04-04 20:32:34 +02:00
81729693bd Merge branch 'old-project-migration' into 'dev'
Migrate from old project

See merge request rheinsw/website!18
2025-04-04 18:31:44 +00:00
1867d10b78 Update Navbar design and responsiveness
- Adjust font sizes and button visibility for better scaling.
- Add hover effect on nav links and improve styling consistency.
2025-04-04 20:29:57 +02:00
915d6d1bcb Enhance MobileNav with theme context and visual updates
- Add ThemeContext to enable theme-based styling and toggle functionality.
- Refactor animation, layout, and styles for improved UX and responsiveness.
2025-04-04 20:29:47 +02:00
9125dd91d7 Update nav labels to German 2025-04-04 20:27:40 +02:00
4566f2559f Migrate from old project 2025-04-02 18:25:10 +02:00
GitLab CI
ce90c48825 Merge remote-tracking branch 'origin/production' into dev 2025-03-29 23:09:58 +00:00
91460f6f7a Merge branch 'ci/update-policy' into 'dev'
Update GitLab CI cache policy to use pull mode

See merge request rheinsw/website!16
2025-03-29 22:58:11 +00:00
03c06d23ad Merge branch 'ci/update-policy' into 'dev'
Update GitLab CI cache policy to use pull mode

See merge request rheinsw/website!16
2025-03-29 22:58:10 +00:00
GitLab CI
b054401ab0 Merge remote-tracking branch 'origin/production' into dev 2025-03-29 22:32:48 +00:00
221b50c363 Merge branch 'pipeline-refactoring' into 'dev'
Update CI images with digests and remove redundant declarations

See merge request rheinsw/website!14
2025-03-29 22:19:30 +00:00
b1cc973853 Merge branch 'pipeline-refactoring' into 'dev'
Update CI images with digests and remove redundant declarations

See merge request rheinsw/website!14
2025-03-29 22:19:30 +00:00
GitLab CI
df69bd3aab Merge remote-tracking branch 'origin/production' into dev 2025-03-29 21:37:17 +00:00
2e0679732e Merge branch 'pipeline-refactoring' into 'dev'
Update CI images with digests and remove redundant declarations

See merge request rheinsw/website!11
2025-03-29 21:30:29 +00:00
35d5fa8884 Merge branch 'pipeline-refactoring' into 'dev'
Update CI images with digests and remove redundant declarations

See merge request rheinsw/website!11
2025-03-29 21:30:28 +00:00
GitLab CI
6a401bfd63 Merge remote-tracking branch 'origin/production' into dev 2025-03-29 18:57:18 +00:00
35 changed files with 1324 additions and 437 deletions

37
app/about/layout.tsx Normal file
View File

@@ -0,0 +1,37 @@
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>
);
}

12
app/about/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
import React from 'react';
import AboutContent from "@/components/About/AboutContent";
const AboutPage = () => {
return (
<div>
<AboutContent/>
</div>
);
};
export default AboutPage;

54
app/api/contact/route.ts Normal file
View File

@@ -0,0 +1,54 @@
import {NextRequest, NextResponse} from 'next/server';
const HCAPTCHA_SECRET = process.env.HCAPTCHA_SECRET ?? '';
const SHARED_API_KEY = process.env.SHARED_API_KEY ?? '';
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const origin = req.headers.get("origin") || "http://localhost:3000";
const captchaToken = body.captcha;
if (!captchaToken) {
return NextResponse.json({success: false, error: 'Captcha is required'}, {status: 400});
}
// Step 1: Verify hCaptcha token with their API
const verifyResponse = await fetch('https://api.hcaptcha.com/siteverify', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
secret: HCAPTCHA_SECRET,
response: captchaToken,
}),
});
const captchaResult = await verifyResponse.json();
if (!captchaResult.success) {
return NextResponse.json({success: false, error: 'Captcha verification failed'}, {status: 403});
}
// Step 2: Forward valid contact request to Spring Boot backend
const backendRes = await fetch('http://localhost:8080/api/contact', {
method: 'POST',
headers: {
"Origin": origin,
'Content-Type': 'application/json',
'X-Frontend-Key': SHARED_API_KEY,
},
body: JSON.stringify(body),
});
const backendText = await backendRes.text();
if (!backendRes.ok) {
return NextResponse.json({success: false, error: backendText}, {status: backendRes.status});
}
return NextResponse.json({success: true, message: backendText});
} catch (err: any) {
console.error('[ContactAPI] error:', err);
return NextResponse.json({success: false, error: err.message}, {status: 500});
}
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ImprintComp from "@/components/Legal/ImprintComp";
import ImprintComp from "@/components/Legal/Imprint/ImprintComp";
const ImprintPage = () => {
return (

View File

@@ -1,5 +1,5 @@
import React from 'react';
import PrivacyComp from "@/components/Legal/PrivacyComp";
import PrivacyComp from "@/components/Legal/Privacy/PrivacyComp";
const PrivacyPage = () => {
return (

View File

@@ -0,0 +1,47 @@
'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;

View File

@@ -0,0 +1,19 @@
'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;

View File

@@ -0,0 +1,68 @@
'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;

View File

@@ -0,0 +1,158 @@
'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>&nbsp;
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;

View File

@@ -0,0 +1,94 @@
'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;

View File

@@ -0,0 +1,97 @@
'use client';
import React from "react";
import Image from "next/image";
import {motion} from "framer-motion";
import {useThemeColors} from "@/utils/useThemeColors";
const team = [
{
name: "Thatsaphorn",
role: "Gründer & Entwickler",
picture: "",
},
{
name: "Anonym",
role: "Vertrieb",
picture: "",
},
];
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">
<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}}
transition={{duration: 0.5}}
>
Das Team
</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}}
/>
<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)}`}
>
{team.map((member, idx) => (
<motion.div
key={member.name}
initial={{opacity: 0, y: 20}}
whileInView={{opacity: 1, y: 0}}
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}}
>
<motion.div
whileHover={{scale: 1.05}}
transition={{type: "spring", stiffness: 300, damping: 20}}
className="w-28 h-28 relative mb-4"
>
<Image
src={member.picture || fallbackImage}
alt={member.name}
fill
sizes="112px"
className="rounded-full object-cover shadow"
/>
</motion.div>
<div className="h-px w-8 bg-gray-300 dark:bg-gray-600 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}}>
{member.role}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
};
export default TeamSection;

View File

@@ -1,99 +1,30 @@
"use client";
'use client';
import React, {useEffect, useContext} from "react";
import AOS from "aos";
import "aos/dist/aos.css";
import SmallHero from "@/components/Helper/SmallHero";
import {ThemeContext} from "@/components/provider/ThemeProvider";
import {themeColors} from "@/components/Helper/ThemeColors";
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 {theme} = useContext(ThemeContext);
const colors = themeColors[theme];
useEffect(() => {
AOS.init({
duration: 1000,
easing: "ease",
once: true,
anchorPlacement: "top-bottom",
});
}, []);
const colors = useThemeColors();
return (
<div className="overflow-hidden transition-colors duration-500"
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}>
{/* Hero Section */}
<div className="mt-[10vh]">
<SmallHero
title="Kontakt"
subtitle="Hast du Fragen? Wir sind für dich da!"
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"
<motion.div
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{duration: 0.7, ease: "easeOut"}}
className="overflow-hidden"
>
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>
<Section style={{backgroundColor: colors.primaryBg}} shadow>
<ContactHero/>
</Section>
<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>
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
<ContactFormSection/>
</Section>
</motion.div>
);
};

View File

@@ -0,0 +1,192 @@
'use client';
import React, { useState } from "react";
import { motion } from "framer-motion";
import { useThemeColors } from "@/utils/useThemeColors";
import HCaptcha from "@hcaptcha/react-hcaptcha";
const ContactFormSection = () => {
const colors = useThemeColors();
const [form, setForm] = useState({
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 isDev = process.env.NODE_ENV === "development";
const hCaptchaSiteKey = isDev
? "10000000-ffff-ffff-ffff-000000000001" // hCaptcha test sitekey
: "ES_ff59a664dc764f92870bf2c7b4eab7c5";
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("");
if (!captchaToken) {
setError("Bitte löse das CAPTCHA, um fortzufahren.");
setLoading(false);
return;
}
try {
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: "" });
} else {
const resJson = await res.json();
setError(resJson?.error || "Ein Fehler ist aufgetreten. Bitte versuche es später erneut.");
}
} catch (_err) {
setError("Serverfehler. Bitte versuche es später erneut.");
}
setLoading(false);
};
return (
<div className="w-full px-6 sm:px-12 py-20 text-left transition-theme">
<motion.h2
className="text-2xl sm:text-3xl font-bold mb-2"
style={{ color: colors.primaryText }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
Schreib uns eine Nachricht
</motion.h2>
<motion.p
className="text-sm mb-8 max-w-xl"
style={{ color: colors.secondaryText }}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
Wir freuen uns über dein Interesse und melden uns schnellstmöglich bei dir zurück.
</motion.p>
{submitted ? (
<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", required: true, placeholder: "Max Mustermann" },
{ label: "Deine E-Mail *", name: "email", type: "email", required: true, placeholder: "max@example.com" },
{ label: "Firmenname (optional)", name: "company", type: "text", required: false, placeholder: "Mustermann GmbH" },
{ label: "Telefonnummer (optional)", name: "phone", type: "tel", required: false, placeholder: "+49 123 456789" },
{ label: "Webseite (optional)", name: "website", type: "url", required: false, placeholder: "https://..." },
].map((field, index) => (
<motion.div
key={field.name}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<label className="block font-semibold mb-1" style={{ color: colors.primaryText }}>
{field.label}
</label>
<input
type={field.type}
name={field.name}
value={form[field.name as keyof typeof form]}
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,
}}
/>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.6 }}
>
<label className="block font-semibold mb-1" style={{ color: colors.primaryText }}>
Deine Nachricht *
</label>
<textarea
name="message"
rows={4}
required
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,
}}
/>
</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>
)}
<motion.div
className="pt-4 flex justify-end"
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.8 }}
>
<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"
>
{loading ? "Sende..." : "📩 Nachricht senden"}
</button>
</motion.div>
</form>
)}
</div>
);
};
export default ContactFormSection;

View File

@@ -0,0 +1,19 @@
'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;

View File

@@ -36,13 +36,13 @@ const Footer = () => {
</p>
</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>
{/*<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>
@@ -50,20 +50,20 @@ const Footer = () => {
<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/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">

View File

@@ -1,38 +1,56 @@
import React from "react";
'use client';
import React, {useContext} from "react";
import {ThemeContext} from "@/components/provider/ThemeProvider";
import {themeColors} from "@/components/Helper/ThemeColors";
import {motion} from "framer-motion";
type SmallHeroProps = {
title: string;
subtitle?: string;
backgroundImage?: string; // Optional background image
backgroundImage?: string;
blurBackground?: boolean;
};
const SmallHero = ({title, subtitle, backgroundImage}: SmallHeroProps) => {
const SmallHero = ({title, subtitle, backgroundImage, blurBackground}: SmallHeroProps) => {
const {theme} = useContext(ThemeContext);
const colors = themeColors[theme];
const primaryTextColor = backgroundImage ? "#ffffff" : colors.primaryText;
const secondaryTextColor = backgroundImage ? "rgba(255, 255, 255, 0.8)" : "#6B7280"; // Tailwind gray-500
return (
<div className="relative w-full py-36 overflow-hidden">
{backgroundImage && blurBackground && (
<div
className="w-full py-20 text-center flex flex-col items-center justify-center bg-cover bg-center"
style={{
backgroundColor: backgroundImage ? "transparent" : "var(--primary-bg)", // Fallback if no image
color: "var(--primary-text)",
backgroundImage: backgroundImage ? `url(${backgroundImage})` : "none",
backgroundSize: "cover",
backgroundPosition: "center",
backgroundBlendMode: "overlay",
transition: "background-color 0.4s ease-in-out, color 0.4s ease-in-out",
}}
>
<h1 className="text-3xl sm:text-4xl font-bold"
data-aos="fade-up"
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">
<motion.h1
className="text-3xl sm:text-4xl font-bold text-left"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{duration: 0.6}}
style={{color: primaryTextColor}}
>
{title}
</h1>
{subtitle &&
<p className="mt-2 text-lg text-[var(--secondary-text)]"
data-aos="fade-up"
data-aos-delay="200"
</motion.h1>
{subtitle && (
<motion.p
className="mt-3 text-lg text-left"
initial={{opacity: 0, y: 10}}
animate={{opacity: 1, y: 0}}
transition={{duration: 0.6, delay: 0.2}}
style={{color: secondaryTextColor}}
>
{subtitle}
</p>
}
</motion.p>
)}
</div>
</div>
);
};

View File

@@ -1,7 +1,6 @@
'use client';
import React from "react";
import About from "@/components/Home/Sections/About";
import ContactCTA from "@/components/Home/Sections/ContactCTA";
import HomeServices from "@/components/Home/Sections/HomeServices";
import TechStack from "@/components/Home/Sections/TechStack";
@@ -24,9 +23,9 @@ const Home = () => {
<Hero/>
</Section>
<Section style={{backgroundColor: colors.secondaryBg}} shadow>
<About/>
</Section>
{/*<Section style={{backgroundColor: colors.secondaryBg}} shadow>*/}
{/* <About/>*/}
{/*</Section>*/}
<Section style={{backgroundColor: colors.primaryBg}} shadow>
<HomeServices/>

View File

@@ -5,7 +5,17 @@ import {motion} from 'framer-motion';
import {FiArrowRight} from 'react-icons/fi';
import {useThemeColors} from '@/utils/useThemeColors';
const ContactCTA = () => {
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 (
@@ -14,26 +24,21 @@ const ContactCTA = () => {
style={{backgroundColor: colors.primaryBg, color: colors.primaryText}}
>
<div className="w-full max-w-4xl px-6 md:px-10 mx-auto text-center">
{/* Headline */}
<motion.h2
className="text-3xl md:text-4xl font-bold transition-colors duration-700 ease-in-out"
>
Interesse geweckt?
<motion.h2 className="text-3xl md:text-4xl font-bold">
{title}
</motion.h2>
{/* Description */}
<motion.p
className="mt-4 text-sm md:text-base max-w-xl mx-auto transition-colors duration-700 ease-in-out"
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}}
>
Lass uns über dein Projekt sprechen. Wir freuen uns darauf, deine Ideen in die Realität umzusetzen.
{description}
</motion.p>
{/* CTA Button */}
<motion.div
className="mt-8 flex justify-center"
initial={{opacity: 0, y: 20}}
@@ -45,7 +50,7 @@ const ContactCTA = () => {
<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"
>
Jetzt Kontakt aufnehmen <FiArrowRight size={18}/>
{buttonLabel} <FiArrowRight size={18}/>
</button>
</Link>
</motion.div>

View File

@@ -0,0 +1,131 @@
"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;

View File

@@ -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 ImprintComp = () => {
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="Impressum"
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 ImprintComp;

View File

@@ -0,0 +1,145 @@
"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 PrivacyComp = () => {
const {theme} = useContext(ThemeContext);
const colors = themeColors[theme];
const sections = [
{
title: "1. Datenschutz auf einen Blick",
content: (
<>
<p>
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen
Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit
denen Sie persönlich identifiziert werden können.
</p>
<p>
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen Kontaktdaten
finden Sie im Abschnitt Hinweis zur Verantwortlichen Stelle.
</p>
</>
),
},
{
title: "2. Allgemeine Hinweise und Pflichtinformationen",
content: (
<>
<p>
Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
Datenschutzvorschriften sowie dieser Datenschutzerklärung. Wir weisen darauf hin, dass die
Datenübertragung im Internet Sicherheitslücken aufweisen kann.
</p>
<p>
Verantwortlich für die Datenverarbeitung:
<br/>
Rhein-Software Development
<br/>
Mühlenstrasse 13
<br/>
79664 Wehr
<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>
</p>
</>
),
},
{
title: "3. Ihre Rechte",
content: (
<p>
Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung,
Datenübertragbarkeit sowie auf Widerspruch gegen die Verarbeitung Ihrer Daten. Zudem haben Sie
ein Beschwerderecht bei der zuständigen Aufsichtsbehörde.
</p>
),
},
{
title: "4. Analyse-Tools und Cookies",
content: (
<p>
Beim Besuch dieser Website kann Ihr Surfverhalten statistisch ausgewertet werden. Das geschieht
vor allem mit sogenannten Analyseprogrammen und Cookies. Detaillierte Informationen hierzu
entnehmen Sie bitte unserer vollständigen Datenschutzerklärung.
</p>
),
},
{
title: "5. Sicherheit",
content: (
<p>
Diese Seite nutzt eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen
Sie an der Adresszeile des Browsers (https://“) und dem Schloss-Symbol.
</p>
),
},
{
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>
),
},
];
return (
<div
className="overflow-hidden transition-colors duration-500"
style={{backgroundColor: colors.secondaryBg, color: colors.primaryText}}
>
{/* Privacy Content */}
<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>
))}
</div>
</div>
);
};
export default PrivacyComp;

View File

@@ -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 PrivacyComp = () => {
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="Datenschutz"
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 PrivacyComp;

View File

@@ -51,9 +51,11 @@ const Nav = ({openNav}: Props) => {
style={{backgroundColor: navBg ? colors.navBg : "transparent"}}
>
<div className="flex items-center h-full justify-between w-[90%] xl:w-[80%] mx-auto">
<h1 className={`${contentSize} font-bold ${navColorClass}`}>
<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) => (
@@ -70,14 +72,13 @@ const Nav = ({openNav}: Props) => {
</div>
<div className="flex items-center space-x-3">
{pathname !== "/contact" && (
<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}`}

View File

@@ -1,9 +1,8 @@
// components/Leistungen/Section/OverviewTabs.tsx
'use client';
import React, {useState} from 'react';
import {motion, AnimatePresence} from 'framer-motion';
import {FiMonitor, FiUsers, FiSettings, FiTool} from 'react-icons/fi';
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";
@@ -11,53 +10,47 @@ import BugFixing from "@/components/Services/Section/overview/BugFixing";
import {useThemeColors} from "@/utils/useThemeColors";
const tabs = [
{key: 'entwicklung', label: 'Entwicklung', icon: <FiMonitor size={20}/>},
{key: 'beratung', label: 'Beratung', icon: <FiUsers size={20}/>},
{key: 'services', label: 'Managed Services', icon: <FiSettings size={20}/>},
{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/>
),
entwicklung: <Development/>,
beratung: <Consulting/>,
services: <ManagedServices/>,
support: <BugFixing/>,
};
const OverviewTabs = () => {
const [activeTab, setActiveTab] = useState("entwicklung");
const colors = useThemeColors();
return (
<div className="max-w-4xl mx-auto px-6 py-16 text-center transition-theme">
<h2
className="text-3xl font-bold mb-4"
style={{color: colors.primaryText}}
>
<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-8 bg-amber-500 mx-auto"
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 justify-center gap-4 mb-8 flex-wrap">
<div className="flex flex-wrap gap-4 mb-10">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className="flex items-center gap-2 px-4 py-2 rounded-full border text-sm font-medium transition-all"
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',

View File

@@ -1,29 +1,18 @@
// components/Leistungen/Section/Hero.tsx
'use client';
import React from "react";
import {motion} from "framer-motion";
import SmallHero from "@/components/Helper/SmallHero";
const ServiceHero = () => {
return (
<section className="py-24 text-center max-w-4xl mx-auto">
<motion.h1
className="text-4xl md:text-5xl font-bold mb-6"
initial={{opacity: 0, y: 20}}
animate={{opacity: 1, y: 0}}
transition={{duration: 0.6}}
>
Unsere Leistungen
</motion.h1>
<motion.p
className="text-lg text-gray-500"
initial={{opacity: 0, y: 10}}
animate={{opacity: 1, y: 0}}
transition={{duration: 0.6, delay: 0.2}}
>
Wir bieten maßgeschneiderte Lösungen von der Beratung bis zum Betrieb.
</motion.p>
</section>
<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>
);
};

View File

@@ -1,7 +1,6 @@
'use client';
import React from "react";
import Image from "next/image";
import {motion} from "framer-motion";
import {useThemeColors} from "@/utils/useThemeColors";
@@ -10,7 +9,7 @@ const BugFixing = () => {
return (
<div
className="w-[95%] sm:w-[80%] mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 mb-16"
className="w-[95%] mx-auto grid grid-cols-1 gap-6 mt-8 mb-16 text-left"
style={{color: colors.primaryText}}
>
<div>
@@ -22,6 +21,7 @@ const BugFixing = () => {
>
🐞 Fehlerbehebung & Optimierung
</motion.h2>
<motion.p
className="text-sm font-medium leading-7"
style={{color: colors.secondaryText}}
@@ -41,28 +41,17 @@ const BugFixing = () => {
>
🔎 Fokusbereiche
</motion.h3>
<ul className="list-disc list-inside text-sm mt-4 space-y-1" style={{color: colors.secondaryText}}>
<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 className="justify-self-center">
<motion.div
initial={{opacity: 0, y: 20}}
whileInView={{opacity: 1, y: 0}}
transition={{duration: 0.5, delay: 0.3}}
>
<Image
src="/images/bug_fixing.jpg"
alt="Bug fixing illustration"
width={300}
height={300}
className="object-contain"
/>
</motion.div>
</div>
</div>
);
};

View File

@@ -9,7 +9,7 @@ const Consulting = () => {
return (
<div
className="w-[95%] sm:w-[80%] mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 mb-16 text-left"
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>

View File

@@ -101,7 +101,7 @@ const Development = () => {
return (
<div
className="w-[95%] sm:w-[80%] mx-auto grid grid-cols-1 gap-6 mt-8 mb-16 text-left"
className="w-[95%] mx-auto grid grid-cols-1 gap-6 mt-8 mb-16 text-left"
style={{color: colors.primaryText}}
>
<div>

View File

@@ -9,7 +9,7 @@ const ManagedServices = () => {
return (
<div
className="w-[95%] sm:w-[80%] mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8 mb-16 text-left"
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>

View File

@@ -25,7 +25,11 @@ const Home = () => {
<OverviewTabs/>
</Section>
<Section style={{backgroundColor: colors.primaryBg}} shadow>
<ContactCTA/>
<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>
);

View File

@@ -6,6 +6,11 @@ export const navLinks = [
},
{
id: 2,
url: '/about',
label: 'Über Uns',
},
{
id: 3,
url: '/services',
label: 'Leistungen',
}

View File

@@ -11,6 +11,17 @@ const compat = new FlatCompat({
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
...compat.config({
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
]
}
})
];
export default eslintConfig;

64
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"framer-motion": "^12.6.5",
"js-cookie": "^3.0.5",
"next": "15.1.7",
"nodemailer": "^6.10.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.4.0",
@@ -20,10 +21,12 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@hcaptcha/react-hcaptcha": "^1.12.0",
"@tailwindcss/postcss": "^4.0.17",
"@types/aos": "^3.0.7",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
@@ -47,6 +50,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.0.tgz",
@@ -218,6 +234,28 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hcaptcha/loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-2.0.0.tgz",
"integrity": "sha512-fFQH6ApU/zCCl6Y1bnbsxsp1Er/lKX+qlgljrpWDeFcenpEtoP68hExlKSXECospzKLeSWcr06cbTjlR/x3IJA==",
"dev": true,
"license": "MIT"
},
"node_modules/@hcaptcha/react-hcaptcha": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.12.0.tgz",
"integrity": "sha512-QiHnQQ52k8SJJSHkc3cq4TlYzag7oPd4f5ZqnjVSe4fJDSlZaOQFtu5F5AYisVslwaitdDELPVLRsRJxiiI0Aw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.17.9",
"@hcaptcha/loader": "^2.0.0"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1268,6 +1306,16 @@
"undici-types": "~6.19.2"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "19.0.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
@@ -5010,6 +5058,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -5643,6 +5700,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true,
"license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",

View File

@@ -14,6 +14,7 @@
"framer-motion": "^12.6.5",
"js-cookie": "^3.0.5",
"next": "15.1.7",
"nodemailer": "^6.10.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.4.0",
@@ -21,10 +22,12 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@hcaptcha/react-hcaptcha": "^1.12.0",
"@tailwindcss/postcss": "^4.0.17",
"@types/aos": "^3.0.7",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB