Initial commit: dotzhauer v4 heimat + Dockerfile/nginx for Coolify
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
.DS_Store
|
||||
*.log
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
.vite
|
||||
.DS_Store
|
||||
*.log
|
||||
.claude
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
23
index.html
Normal file
23
index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#ebe1cc" />
|
||||
<title>Dotzauer · ein Cottbusser Familienbetrieb seit 1927</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Drei Generationen Familie Dotzauer — Handwerk für Kälte, Klima und Wärme. Aus Cottbus, mit dem Herzen für die Lausitz und ganz Deutschland."
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&family=Sora:wght@100..800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
49
nginx.conf
Normal file
49
nginx.conf
Normal file
@@ -0,0 +1,49 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/javascript
|
||||
application/json
|
||||
application/xml
|
||||
application/xml+rss
|
||||
image/svg+xml
|
||||
font/woff
|
||||
font/woff2;
|
||||
|
||||
# Long cache for hashed Vite assets
|
||||
location /assets/ {
|
||||
access_log off;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Don't cache the entry HTML
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
}
|
||||
1680
package-lock.json
generated
Normal file
1680
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "dotzauer-kaelte-klima",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 8081"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
25
src/App.jsx
Normal file
25
src/App.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import Nav from "./components/Nav.jsx";
|
||||
import Hero from "./components/Hero.jsx";
|
||||
import Services from "./components/Services.jsx";
|
||||
import Profile from "./components/Profile.jsx";
|
||||
import Gallery from "./components/Gallery.jsx";
|
||||
import Contact from "./components/Contact.jsx";
|
||||
import Footer from "./components/Footer.jsx";
|
||||
import useScrollReveal from "./hooks/useScrollReveal.js";
|
||||
|
||||
export default function App() {
|
||||
useScrollReveal();
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<main>
|
||||
<Hero />
|
||||
<Services />
|
||||
<Profile />
|
||||
<Gallery />
|
||||
<Contact />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
95
src/components/Contact.jsx
Normal file
95
src/components/Contact.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Contact() {
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
return (
|
||||
<section id="kontakt" className="section">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<span className="num">— Sprechen wir</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="eyebrow">finden Sie uns</span>
|
||||
<h2>
|
||||
Schreiben Sie uns — <em>wir freuen uns</em>.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="contact">
|
||||
<aside className="contact-card">
|
||||
<div className="letterhead">
|
||||
<h3>Familie Dotzauer</h3>
|
||||
<span className="h">in der Werkstatt erreichbar</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Werkstatt</strong>
|
||||
<span className="value">Maiberger Str. 2 · 03044 Cottbus</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Telefon</strong>
|
||||
<a href="tel:+4935586034 5">0355 / 86 03 45</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Notdienst (24 / 7)</strong>
|
||||
<a href="tel:+4935586034 6">0355 / 86 03 46</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>E-Post</strong>
|
||||
<a href="mailto:info@dotzauer-kaelte-klima.de">
|
||||
info@dotzauer-kaelte-klima.de
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Öffnungszeit</strong>
|
||||
<span className="value">Mo – Fr · 7 – 17 Uhr</span>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<form
|
||||
className="contact-form"
|
||||
onSubmit={(e) => { e.preventDefault(); setSent(true); }}
|
||||
>
|
||||
<span className="form-label">
|
||||
Hallo!
|
||||
<span className="small">Erzählen Sie uns, worum es geht — wir antworten binnen eines Werktages.</span>
|
||||
</span>
|
||||
|
||||
<div className="form-row">
|
||||
<label>
|
||||
<span>Ihr Name</span>
|
||||
<input type="text" required placeholder="Vor- und Nachname" />
|
||||
</label>
|
||||
<label>
|
||||
<span>E-Post</span>
|
||||
<input type="email" required placeholder="ihre@adresse.de" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-row full">
|
||||
<label>
|
||||
<span>Worum geht es?</span>
|
||||
<textarea rows="4" required placeholder="Was beschäftigt Sie? Welche Anlage, welcher Standort, welcher Termin?" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{sent ? (
|
||||
<p className="form-success">
|
||||
Vielen Dank! Wir melden uns binnen eines Werktages bei Ihnen. — Familie Dotzauer
|
||||
</p>
|
||||
) : (
|
||||
<div className="actions">
|
||||
<span className="note">Wir antworten binnen 24h ✓</span>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Absenden <span className="arrow">→</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
41
src/components/Footer.jsx
Normal file
41
src/components/Footer.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="footer-inner">
|
||||
<div>
|
||||
<div className="footer-brand">
|
||||
Dotzauer <span className="amp">&</span> Söhne
|
||||
</div>
|
||||
<span className="footer-brand-note">
|
||||
ein Familienbetrieb in dritter Generation, aus Cottbus
|
||||
</span>
|
||||
</div>
|
||||
<div className="footer-col">
|
||||
<h4>Werkstatt</h4>
|
||||
<ul>
|
||||
<li>Maiberger Str. 2</li>
|
||||
<li>03044 Cottbus</li>
|
||||
<li>Mo – Fr · 7 – 17 Uhr</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="footer-col">
|
||||
<h4>Sprechen wir</h4>
|
||||
<ul>
|
||||
<li><a href="tel:+4935586034 5">0355 / 86 03 45</a></li>
|
||||
<li><a href="mailto:info@dotzauer-kaelte-klima.de">info@dotzauer-kaelte-klima.de</a></li>
|
||||
<li><a href="#kontakt">Anfrageformular</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="footer-bar">
|
||||
<span>© {new Date().getFullYear()} · Familie Dotzauer · Cottbus</span>
|
||||
<div style={{ display: "flex", gap: "1.6rem", flexWrap: "wrap" }}>
|
||||
<a href="#impressum">Impressum</a>
|
||||
<a href="#datenschutz">Datenschutz</a>
|
||||
<a href="#agb">AGB</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
44
src/components/Gallery.jsx
Normal file
44
src/components/Gallery.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
const ITEMS = [
|
||||
{ title: "VRF Bürogebäude", place: "Cottbus", year: "2024", hue: 30 },
|
||||
{ title: "Kühlraum Gastronomie", place: "Berlin", year: "2024", hue: 18 },
|
||||
{ title: "Wärmepumpe Eigenheim", place: "Lübben", year: "2023", hue: 90 },
|
||||
{ title: "VRF-System Hotel", place: "Hoyerswerda", year: "2025", hue: 24 },
|
||||
{ title: "Tiefkühlanlage", place: "Spremberg", year: "2025", hue: 200 },
|
||||
{ title: "Loft-Klimatisierung", place: "Berlin", year: "2024", hue: 38 },
|
||||
{ title: "Wärmepumpen-Kaskade", place: "Forst", year: "2025", hue: 75 },
|
||||
{ title: "Industrie-Kühlung", place: "Cottbus", year: "2025", hue: 12 },
|
||||
{ title: "Klimaanlage Praxis", place: "Berlin", year: "2024", hue: 32 },
|
||||
];
|
||||
|
||||
export default function Gallery() {
|
||||
return (
|
||||
<section id="galerie" className="section">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<span className="num">— Unsere Arbeit</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="eyebrow">aus dem Werkbuch</span>
|
||||
<h2>
|
||||
Neun <em>Bilder</em> aus unserer Werkstatt.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gallery">
|
||||
{ITEMS.map((it, i) => (
|
||||
<figure
|
||||
key={it.title + i}
|
||||
className="g-item"
|
||||
data-cap={`${it.title} · ${it.place}`}
|
||||
style={{ "--g-hue": it.hue }}
|
||||
>
|
||||
<div className="g-photo">
|
||||
<div className="g-meta">{it.year}</div>
|
||||
</div>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
54
src/components/Hero.jsx
Normal file
54
src/components/Hero.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import PenguinStamp from "./PenguinStamp.jsx";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="hero" id="top">
|
||||
<div className="hero-grid">
|
||||
<div className="hero-text">
|
||||
<span className="hero-eyebrow">
|
||||
<span className="dot" />
|
||||
Familie Dotzauer · Cottbus · seit 1927
|
||||
</span>
|
||||
|
||||
<h1 className="hero-title">
|
||||
<span className="small">Drei Generationen,</span>
|
||||
ein <span className="it">Familien-</span><br/>
|
||||
handwerk.
|
||||
</h1>
|
||||
|
||||
<p className="hero-lead">
|
||||
Wir bauen <em>Kälte</em>, <em>Klima</em> und <em>Wärme</em> seit
|
||||
fast hundert Jahren. Klein genug, um Sie zu kennen — groß genug,
|
||||
um es richtig zu machen.
|
||||
</p>
|
||||
|
||||
<div className="hero-actions">
|
||||
<a href="#kontakt" className="btn btn-primary">
|
||||
Sprechen wir <span className="arrow">→</span>
|
||||
</a>
|
||||
<a href="#profil" className="link-arrow">
|
||||
Lernen Sie uns kennen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="hero-sign">
|
||||
<span className="em">Familie Dotzauer</span>
|
||||
<span>— in dritter Generation, aus Cottbus</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hero-photo" data-cap="Werkstatt I · Cottbus, 1927">
|
||||
<div className="photo">
|
||||
<div className="photo-overlay">
|
||||
<PenguinStamp size={120} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hero-photo-small">
|
||||
<div className="photo-inner" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
16
src/components/Logo.jsx
Normal file
16
src/components/Logo.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export default function Logo({ size = 28 }) {
|
||||
return (
|
||||
<svg viewBox="0 0 32 32" width={size} height={size} aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="logo-gradient" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor="#9be7ff" />
|
||||
<stop offset="100%" stopColor="#4ea8de" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill="url(#logo-gradient)"
|
||||
d="M16 2 L20 12 L30 14 L22 20 L25 30 L16 24 L7 30 L10 20 L2 14 L12 12 Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
35
src/components/Nav.jsx
Normal file
35
src/components/Nav.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Nav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const close = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<header className="nav-wrap">
|
||||
<nav className={`nav${open ? " open" : ""}`}>
|
||||
<a className="brand" href="#top" onClick={close}>
|
||||
<span>Dotzauer</span>
|
||||
<span className="amp">&</span>
|
||||
<span>Söhne</span>
|
||||
<span className="sub">seit 1927</span>
|
||||
</a>
|
||||
|
||||
<ul className="nav-links">
|
||||
<li><a href="#leistungen" onClick={close}>Leistungen</a></li>
|
||||
<li><a href="#profil" onClick={close}>Unsere Familie</a></li>
|
||||
<li><a href="#galerie" onClick={close}>Arbeiten</a></li>
|
||||
<li><a className="nav-cta" href="#kontakt" onClick={close}>Sprechen wir →</a></li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
className="nav-toggle"
|
||||
aria-label="Menü"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<span /><span /><span />
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
40
src/components/PenguinStamp.jsx
Normal file
40
src/components/PenguinStamp.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/* Vintage etching penguin — engraved-look hatched lines, for the Heimat photo overlay. */
|
||||
export default function PenguinStamp({ size = 96 }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 120 160"
|
||||
width={size}
|
||||
height={size * 1.33}
|
||||
aria-hidden="true"
|
||||
style={{ display: "block" }}
|
||||
>
|
||||
<defs>
|
||||
<pattern id="hatch" patternUnits="userSpaceOnUse" width="3" height="3" patternTransform="rotate(45)">
|
||||
<line x1="0" y1="0" x2="0" y2="3" stroke="currentColor" strokeWidth="0.7" opacity="0.7" />
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<g fill="currentColor" stroke="currentColor" strokeWidth="1" strokeLinejoin="round" strokeLinecap="round">
|
||||
{/* Body outline */}
|
||||
<ellipse cx="60" cy="92" rx="38" ry="50" fill="url(#hatch)" stroke="currentColor" strokeWidth="1.2" />
|
||||
{/* Belly highlight */}
|
||||
<ellipse cx="60" cy="96" rx="22" ry="34" fill="none" stroke="currentColor" strokeWidth="0.9" />
|
||||
{/* Head */}
|
||||
<circle cx="60" cy="36" r="24" fill="url(#hatch)" stroke="currentColor" strokeWidth="1.2" />
|
||||
{/* Face mask */}
|
||||
<ellipse cx="60" cy="40" rx="14" ry="13" fill="none" stroke="currentColor" strokeWidth="0.9" />
|
||||
{/* Beak */}
|
||||
<path d="M 53 42 L 67 42 L 60 52 Z" fill="currentColor" stroke="currentColor" />
|
||||
{/* Eye */}
|
||||
<circle cx="60" cy="36" r="2" fill="currentColor" />
|
||||
{/* Left flipper */}
|
||||
<path d="M 26 76 Q 18 100 24 124 Q 30 128 34 122 Q 32 100 32 76 Z" fill="url(#hatch)" stroke="currentColor" strokeWidth="1" />
|
||||
{/* Right flipper */}
|
||||
<path d="M 94 76 Q 102 100 96 124 Q 90 128 86 122 Q 88 100 88 76 Z" fill="url(#hatch)" stroke="currentColor" strokeWidth="1" />
|
||||
{/* Feet */}
|
||||
<ellipse cx="46" cy="146" rx="9" ry="3.5" fill="currentColor" />
|
||||
<ellipse cx="74" cy="146" rx="9" ry="3.5" fill="currentColor" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
74
src/components/Profile.jsx
Normal file
74
src/components/Profile.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
export default function Profile() {
|
||||
return (
|
||||
<section id="profil" className="section">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<span className="num">— Unsere Familie</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="eyebrow">unsere Geschichte</span>
|
||||
<h2>
|
||||
Drei Generationen, <em>eine</em> Werkstatt.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile">
|
||||
<div className="profile-text">
|
||||
<p>
|
||||
<span className="em">1927</span> öffnete unser Großvater die
|
||||
erste Werkstatt für Kühlanlagen in einer schmalen Gasse hinter
|
||||
dem Cottbusser Bahnhof. Eine Werkbank, ein Lehrling, ein
|
||||
Telefon.
|
||||
</p>
|
||||
<p>
|
||||
<span className="em">1962</span> übernahm unser Vater. Was
|
||||
Kühlung war, wurde Klimatechnik — und ein zweiter Standort
|
||||
in Berlin-Schöneberg.
|
||||
</p>
|
||||
<p>
|
||||
<span className="em">Heute</span> sind wir drei Geschwister
|
||||
an der Spitze, mit acht Mitarbeitern, zwei Meistern und einem
|
||||
Lehrling — klein genug, um Sie zu kennen, groß genug, um es
|
||||
richtig zu machen.
|
||||
</p>
|
||||
|
||||
<div className="profile-signature">
|
||||
Familie Dotzauer
|
||||
<span className="small">— III. Generation, Cottbus</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<figure className="profile-quote">
|
||||
<q>
|
||||
Wir bauen Anlagen, die wir auch in zwanzig Jahren noch
|
||||
warten können — von uns, in Ihrer Werkstatt, mit echter Hand.
|
||||
</q>
|
||||
<div className="author">
|
||||
<span>Th. Dotzauer · Meister · Cottbus</span>
|
||||
<span className="star">★★★★★</span>
|
||||
</div>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div className="profile-stats">
|
||||
<div>
|
||||
<div className="pstat-n">98<span className="u">+</span></div>
|
||||
<div className="pstat-l">Jahre Familienbetrieb</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="pstat-n">III</div>
|
||||
<div className="pstat-l">Generationen Handwerk</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="pstat-n">500<span className="u">/a</span></div>
|
||||
<div className="pstat-l">zufriedene Kunden</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="pstat-n">4,9<span className="u">★</span></div>
|
||||
<div className="pstat-l">Bewertung</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
39
src/components/Services.jsx
Normal file
39
src/components/Services.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
const SERVICES = [
|
||||
{ icon: "K", title: "Klimaanlagen", desc: "Split-, Multisplit- und VRF-Systeme für Wohnräume, Büros und kleine Gewerbe. Leise, energieeffizient, sauber installiert.", tag: "Wohnen · Gewerbe" },
|
||||
{ icon: "F", title: "Kältetechnik", desc: "Begehbare Kühl- und Tiefkühlräume, gewerbliche Kühlmöbel und Prozesskühlung — von der Hinterhof-Bäckerei bis zur Industrie.", tag: "Gastro · Industrie" },
|
||||
{ icon: "W", title: "Wärmepumpen", desc: "Luft-Wasser, Luft-Luft, Hybridsysteme. Heizen, kühlen, Warmwasser bereiten — nachhaltig und förderfähig.", tag: "BAFA · KfW" },
|
||||
{ icon: "S", title: "Wartung & Service", desc: "Regelmäßige Inspektion nach F-Gase-Verordnung, Dichtheitsprüfungen, sauber dokumentierte Wartungsverträge.", tag: "F-Gase-Zertifiziert" },
|
||||
{ icon: "N", title: "Notdienst", desc: "Wenn die Kühlung kippt oder die Klimaanlage streikt — wir sind 24 Stunden, 7 Tage erreichbar.", tag: "24 / 7" },
|
||||
{ icon: "P", title: "Planung", desc: "Individuelle Anlagenplanung für Neubau und Bestand — gemeinsam mit Ihnen, energieeffizient durchdacht.", tag: "Neubau · Bestand" },
|
||||
];
|
||||
|
||||
export default function Services() {
|
||||
return (
|
||||
<section id="leistungen" className="section">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<span className="num">— Was wir machen</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="eyebrow">unser Handwerk</span>
|
||||
<h2>
|
||||
Sechs Disziplinen — <em>ein</em> Anspruch.
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="services">
|
||||
{SERVICES.map((s) => (
|
||||
<article key={s.title} className="service">
|
||||
<div className="service-icon">{s.icon}</div>
|
||||
<div>
|
||||
<h3 className="service-title">{s.title}</h3>
|
||||
<p className="service-desc">{s.desc}</p>
|
||||
<span className="service-tag">{s.tag}</span>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
43
src/hooks/useScrollReveal.js
Normal file
43
src/hooks/useScrollReveal.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const SELECTORS = [
|
||||
".section-head",
|
||||
".service",
|
||||
".profile-text",
|
||||
".profile-quote",
|
||||
".profile-stats",
|
||||
".g-item",
|
||||
".contact-card",
|
||||
".contact-form",
|
||||
".footer-inner",
|
||||
];
|
||||
|
||||
export default function useScrollReveal() {
|
||||
useEffect(() => {
|
||||
const els = document.querySelectorAll(SELECTORS.join(","));
|
||||
els.forEach((el, i) => {
|
||||
el.classList.add("reveal");
|
||||
el.style.transitionDelay = `${Math.min(i * 30, 240)}ms`;
|
||||
});
|
||||
|
||||
if (!("IntersectionObserver" in window)) {
|
||||
els.forEach((el) => el.classList.add("in"));
|
||||
return;
|
||||
}
|
||||
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((e) => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add("in");
|
||||
io.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.12 }
|
||||
);
|
||||
els.forEach((el) => io.observe(el));
|
||||
|
||||
return () => io.disconnect();
|
||||
}, []);
|
||||
}
|
||||
10
src/main.jsx
Normal file
10
src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
1008
src/styles.css
Normal file
1008
src/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
8
vite.config.js
Normal file
8
vite.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 8083, strictPort: true, host: true, open: false },
|
||||
preview: { port: 8083, strictPort: true },
|
||||
});
|
||||
Reference in New Issue
Block a user