Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c3f0ad9d3 | |||
| 4b4e618687 | |||
| b10eec4be3 | |||
| d53dc085aa | |||
| 044b703b18 | |||
| 5f4b6748da | |||
| a3586f5bac | |||
| f3dc7ea2b2 | |||
| 35549e9890 | |||
| 1673651e22 | |||
| 42553f1d8c | |||
| 862d589b88 | |||
| e7bb5e5123 | |||
| ff6a177cfa | |||
| 823896bccb | |||
| 24c68d611a | |||
| d2c30ed43a |
@@ -0,0 +1,14 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- production
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- run: docker compose up -d --build
|
||||||
+4
-4
@@ -1,6 +1,7 @@
|
|||||||
# --- Base image ---
|
# --- Base image ---
|
||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
# --- Install dependencies ---
|
# --- Install dependencies ---
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
@@ -24,12 +25,11 @@ RUN addgroup -g 1001 nodejs \
|
|||||||
&& adduser -u 1001 -G nodejs -s /bin/sh -D nextjs
|
&& adduser -u 1001 -G nodejs -s /bin/sh -D nextjs
|
||||||
|
|
||||||
# Copy only what is needed for runtime
|
# Copy only what is needed for runtime
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/.next ./.next
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/package.json ./package.json
|
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["npm", "start"]
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
kleinholz-site:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: kleinholz-site
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.kleinholz.rule=Host(`kleinholz.bauhd.dev`)"
|
||||||
|
- "traefik.http.routers.kleinholz.entrypoints=websecure"
|
||||||
|
- "traefik.http.services.kleinholz.loadbalancer.server.port=3000"
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
name: web
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: "standalone"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+267
-333
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -9,9 +9,9 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.1.6",
|
"next": "16.2.6",
|
||||||
"react": "19.2.4",
|
"react": "19.2.6",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
+63
-41
@@ -1,51 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{ year: "26.10.1966", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." },
|
||||||
title: "Mobilkran",
|
{ year: "1972", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." },
|
||||||
text: "Wir glauben an digitale Lösungen, die Menschen verbinden. Technologie soll nicht nur funktionieren, sondern begeistern.",
|
{ year: "1972", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." },
|
||||||
image: "/mobilkran.jpg",
|
{ year: "1990", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." },
|
||||||
},
|
{ year: "1994", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." },
|
||||||
{
|
{ year: "1996", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." },
|
||||||
title: "Transport",
|
{ year: "31.10.2019", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." },
|
||||||
text: "Ein bunter Mix aus Kreativen, Entwicklern und Strategen – vereint durch die Leidenschaft für gute Ideen und sauberen Code.",
|
{ year: "01.11.2019", text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ullamcorper in ultricies vivamus nulla posuere elit tortor diam curabitur nec mattis commodo malesuada. Adipiscing pellentesque dapibus mattis dictumst dapibus ullamcorper elementum placerat ultricies quis bibendum aenean non." }
|
||||||
image: "/transport.jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "ADAC Abschleppdienst",
|
|
||||||
text: "Transparenz, Empathie und Nachhaltigkeit sind keine Buzzwords für uns, sondern gelebte Prinzipien in jedem Projekt.",
|
|
||||||
image: "/abschleppdienst.jpg",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function About() {
|
export default function Page() {
|
||||||
|
const refs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add("opacity-100", "translate-y-0");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
refs.current.forEach(el => el && observer.observe(el));
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white py-20 px-6 lg:px-24">
|
<main className="min-h-screen bg-gray-100 py-10">
|
||||||
<h1 className="text-4xl font-bold text-center mb-20 text-gray-800">Über uns</h1>
|
<h1 className="text-4xl font-bold text-center mb-20 text-gray-800">
|
||||||
|
Über uns
|
||||||
|
</h1>
|
||||||
|
|
||||||
<div className="space-y-28">
|
<div className="relative w-full flex justify-center py-20">
|
||||||
{sections.map((section, index) => (
|
<div className="relative w-full max-w-3xl">
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`flex flex-col lg:flex-row ${
|
|
||||||
index % 2 === 1 ? "lg:flex-row-reverse" : ""
|
|
||||||
} items-center gap-12`}
|
|
||||||
>
|
|
||||||
{/* Bild */}
|
|
||||||
<div className="w-full lg:w-1/2">
|
|
||||||
<img
|
|
||||||
src={section.image}
|
|
||||||
alt={section.title}
|
|
||||||
className="w-full h-auto rounded-xl shadow-xl object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Text */}
|
{/* Mittlere Linie */}
|
||||||
<div className="w-full lg:w-1/2">
|
<div className="absolute inset-0 flex justify-center">
|
||||||
<h2 className="text-3xl font-semibold text-gray-700 mb-4">{section.title}</h2>
|
<div className="w-1 bg-gray-300"></div>
|
||||||
<p className="text-gray-600 text-lg leading-relaxed">{section.text}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
ref={(el: HTMLDivElement | null): void => {
|
||||||
|
refs.current[index] = el;
|
||||||
|
}}
|
||||||
|
className="relative mb-20 opacity-0 translate-y-10 transition-all duration-700"
|
||||||
|
>
|
||||||
|
{/* Jahr */}
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white text-[var(--color-primary)] px-6 py-2 text-3xl font-bold shadow z-10">
|
||||||
|
{section.year}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text */}
|
||||||
|
<div className="mt-20 text-center">
|
||||||
|
<p className="text-gray-700 text-lg">{section.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import ContactForm from "@/components/ContactForm";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen bg-gray-100 py-10">
|
|
||||||
<ContactForm/>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
+1
-1
@@ -17,7 +17,7 @@ export default function Home() {
|
|||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<div className="w-full h-[400px]">
|
<div className="w-full h-100">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2489.1370108126625!2d6.875406576840839!3d51.400537171789225!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47b8c14b986f8f25%3A0x8cc0d4e323228ee8!2sAdac%20Mobilit%C3%A4tspartner%20Kleinholz!5e0!3m2!1sde!2sde!4v1755638002671!5m2!1sde!2sde"
|
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2489.1370108126625!2d6.875406576840839!3d51.400537171789225!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x47b8c14b986f8f25%3A0x8cc0d4e323228ee8!2sAdac%20Mobilit%C3%A4tspartner%20Kleinholz!5e0!3m2!1sde!2sde!4v1755638002671!5m2!1sde!2sde"
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const sections = [
|
||||||
|
{
|
||||||
|
title: "Mobilkran",
|
||||||
|
text: "Ob Baustelleneinrichtung, Montagearbeiten oder Schwerlasttransport – mit unseren Mobilkranen bewegen wir sicher und präzise, was andere nicht heben können. Unser erfahrenes Kranfahrer-Team sorgt für reibungslose Abläufe, kurze Standzeiten und höchste Sicherheitsstandards auf jeder Baustelle.",
|
||||||
|
image: "/mobilkran.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Transport",
|
||||||
|
text: "Wir transportieren Ihre Güter, Maschinen und Baustoffe zuverlässig ans Ziel. Mit moderner Fahrzeugflotte und langjähriger Erfahrung im Schwer- und Spezialtransport sind wir Ihr Partner für logistische Herausforderungen jeder Größenordnung.",
|
||||||
|
image: "/transport.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "ADAC Abschleppdienst",
|
||||||
|
text: "Als zertifizierter ADAC-Partnerbetrieb stehen wir Ihnen bei Pannen und Bergungen kompetent und schnell zur Seite. Vertrauen Sie auf geprüfte Qualität und die Sicherheit, die eine ADAC-Partnerschaft mit sich bringt.",
|
||||||
|
image: "/abschleppdienst.jpg",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Services() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white py-20 px-6 lg:px-24">
|
||||||
|
<h1 className="text-4xl font-bold text-center mb-20 text-gray-800">Dienstleistungen</h1>
|
||||||
|
|
||||||
|
<div className="space-y-28">
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex flex-col lg:flex-row ${
|
||||||
|
index % 2 === 1 ? "lg:flex-row-reverse" : ""
|
||||||
|
} items-center gap-12`}
|
||||||
|
>
|
||||||
|
{/* Bild */}
|
||||||
|
<div className="w-full lg:w-1/2">
|
||||||
|
<img
|
||||||
|
src={section.image}
|
||||||
|
alt={section.title}
|
||||||
|
className="w-full h-auto rounded-xl shadow-xl object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text */}
|
||||||
|
<div className="w-full lg:w-1/2">
|
||||||
|
<h2 className="text-3xl font-semibold text-gray-700 mb-4">{section.title}</h2>
|
||||||
|
<p className="text-gray-600 text-lg leading-relaxed">{section.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
// components/ContactForm.tsx
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export default function ContactForm() {
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
message: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [submitted, setSubmitted] = useState(false);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// Hier kannst du die Daten an dein Backend oder einen Dienst senden
|
|
||||||
console.log('Formulardaten:', formData);
|
|
||||||
setSubmitted(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-xl mx-auto p-6 bg-white shadow-md rounded-md">
|
|
||||||
<h2 className="text-2xl font-semibold mb-4">Kontaktformular</h2>
|
|
||||||
{submitted ? (
|
|
||||||
<p className="text-green-600">Vielen Dank für deine Nachricht!</p>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
required
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
|
||||||
E-Mail
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
id="email"
|
|
||||||
required
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700">
|
|
||||||
Nachricht
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
name="message"
|
|
||||||
id="message"
|
|
||||||
rows={5}
|
|
||||||
required
|
|
||||||
value={formData.message}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-[var(--color-primary)] text-white px-4 py-2 rounded hover:bg-[var(--color-primary)] transition"
|
|
||||||
>
|
|
||||||
Absenden
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,13 +9,13 @@ const Navbar: React.FC = () => {
|
|||||||
<nav className="bg-gray-800 text-white">
|
<nav className="bg-gray-800 text-white">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between h-16 items-center">
|
<div className="flex justify-between h-16 items-center">
|
||||||
<div className="flex-shrink-0">
|
<div className="shrink-0">
|
||||||
<Link href="/" className="text-[var(--color-primary)] text-xl font-bold">MKV Kleinholz</Link>
|
<Link href="/" className="text-[var(--color-primary)] text-xl font-bold">MKV Kleinholz</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:flex space-x-4">
|
<div className="hidden md:flex space-x-4">
|
||||||
<Link href="/" className="hover:text-gray-300">Home</Link>
|
<Link href="/" className="hover:text-gray-300">Home</Link>
|
||||||
|
<Link href="/services" className="hover:text-gray-300">Dienstleistungen</Link>
|
||||||
<Link href="/about" className="hover:text-gray-300">Über uns</Link>
|
<Link href="/about" className="hover:text-gray-300">Über uns</Link>
|
||||||
<Link href="/contact" className="hover:text-gray-300">Kontakt</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<button
|
<button
|
||||||
@@ -31,8 +31,8 @@ const Navbar: React.FC = () => {
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="md:hidden px-4 pb-4 space-y-2">
|
<div className="md:hidden px-4 pb-4 space-y-2">
|
||||||
<Link href="/" className="block hover:text-gray-300">Home</Link>
|
<Link href="/" className="block hover:text-gray-300">Home</Link>
|
||||||
|
<Link href="/services" className="block hover:text-gray-300">Dienstleistungen</Link>
|
||||||
<Link href="/about" className="block hover:text-gray-300">Über uns</Link>
|
<Link href="/about" className="block hover:text-gray-300">Über uns</Link>
|
||||||
<Link href="/contact" className="block hover:text-gray-300">Kontakt</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Reference in New Issue
Block a user