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 ---
|
||||
FROM node:22-alpine AS base
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# --- Install dependencies ---
|
||||
FROM base AS deps
|
||||
@@ -24,12 +25,11 @@ RUN addgroup -g 1001 nodejs \
|
||||
&& adduser -u 1001 -G nodejs -s /bin/sh -D nextjs
|
||||
|
||||
# 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/.next ./.next
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
USER nextjs
|
||||
|
||||
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";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone"
|
||||
};
|
||||
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"next": "16.2.6",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
+55
-33
@@ -1,51 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: "Mobilkran",
|
||||
text: "Wir glauben an digitale Lösungen, die Menschen verbinden. Technologie soll nicht nur funktionieren, sondern begeistern.",
|
||||
image: "/mobilkran.jpg",
|
||||
},
|
||||
{
|
||||
title: "Transport",
|
||||
text: "Ein bunter Mix aus Kreativen, Entwicklern und Strategen – vereint durch die Leidenschaft für gute Ideen und sauberen Code.",
|
||||
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",
|
||||
},
|
||||
{ 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." },
|
||||
{ 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." },
|
||||
{ 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." },
|
||||
{ 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." },
|
||||
{ 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." },
|
||||
{ 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." }
|
||||
];
|
||||
|
||||
export default function About() {
|
||||
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">Über uns</h1>
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<div className="relative w-full flex justify-center py-20">
|
||||
<div className="relative w-full max-w-3xl">
|
||||
|
||||
{/* Mittlere Linie */}
|
||||
<div className="absolute inset-0 flex justify-center">
|
||||
<div className="w-1 bg-gray-300"></div>
|
||||
</div>
|
||||
|
||||
<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`}
|
||||
ref={(el: HTMLDivElement | null): void => {
|
||||
refs.current[index] = el;
|
||||
}}
|
||||
className="relative mb-20 opacity-0 translate-y-10 transition-all duration-700"
|
||||
>
|
||||
{/* 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"
|
||||
/>
|
||||
{/* 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="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 className="mt-20 text-center">
|
||||
<p className="text-gray-700 text-lg">{section.text}</p>
|
||||
</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>
|
||||
</section>
|
||||
</main>
|
||||
<div className="w-full h-[400px]">
|
||||
<div className="w-full h-100">
|
||||
<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"
|
||||
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">
|
||||
<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-shrink-0">
|
||||
<div className="shrink-0">
|
||||
<Link href="/" className="text-[var(--color-primary)] text-xl font-bold">MKV Kleinholz</Link>
|
||||
</div>
|
||||
<div className="hidden md:flex space-x-4">
|
||||
<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="/contact" className="hover:text-gray-300">Kontakt</Link>
|
||||
</div>
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
@@ -31,8 +31,8 @@ const Navbar: React.FC = () => {
|
||||
{isOpen && (
|
||||
<div className="md:hidden px-4 pb-4 space-y-2">
|
||||
<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="/contact" className="block hover:text-gray-300">Kontakt</Link>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user