Merge branch 'tax-lawfirm' into 'dev'

Tax Lawfirm Demo 1

See merge request rheinsw/demo-websites!1
This commit was merged in pull request #1.
This commit is contained in:
2025-07-01 01:31:17 +00:00
37 changed files with 8077 additions and 5 deletions

View File

@@ -29,8 +29,6 @@ services:
ld1: ld1:
image: registry.boomlab.party/rheinsw/demo-websites/ld1 image: registry.boomlab.party/rheinsw/demo-websites/ld1
container_name: ld1 container_name: ld1
ports:
- "25601:3000"
restart: on-failure restart: on-failure
networks: networks:
- demos-net - demos-net
@@ -40,8 +38,15 @@ services:
ld2: ld2:
image: registry.boomlab.party/rheinsw/demo-websites/ld2 image: registry.boomlab.party/rheinsw/demo-websites/ld2
container_name: ld2 container_name: ld2
ports: restart: on-failure
- "25602:3000" networks:
- demos-net
environment:
- NODE_ENV=production
tld1:
image: registry.boomlab.party/rheinsw/demo-websites/tld1
container_name: tld1
restart: on-failure restart: on-failure
networks: networks:
- demos-net - demos-net

View File

@@ -33,3 +33,21 @@ docker_demo_2:
needs: needs:
- job: build_demo_2 - job: build_demo_2
artifacts: true artifacts: true
build_demo_3:
stage: build
extends: .build-next-template
variables:
IMAGE_NAME: tld1
WORKDIR_PATH: ./lawfirm-demos/tax-lawfirm-demos/tax-lawfirm-demo-1
docker_demo_3:
stage: dockerize
extends: .docker-build-template
variables:
IMAGE_NAME: tld1
WORKDIR_PATH: ./lawfirm-demos/tax-lawfirm-demos/tax-lawfirm-demo-1
DOCKERFILE_PATH: Dockerfile
needs:
- job: build_demo_3
artifacts: true

View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -0,0 +1,31 @@
import {Button} from '@/components/ui/button'
import Link from 'next/link'
import {HeroSection} from "@/app/(root)/sections/HeroSection";
import {ServicesSection} from "@/app/(root)/sections/ServicesSection";
import {AboutMeSection} from "@/app/(root)/sections/AboutMeSection";
export default function SteuerkanzleiLandingPage() {
return (
<main className="min-h-screen bg-background text-foreground font-sans">
{/* Hero Section */}
<HeroSection/>
{/* Leistungen */}
<ServicesSection/>
{/* Spezialisierungen */}
{/*<SpecialServicesSection/>*/}
<AboutMeSection/>
{/* Kontakt CTA */}
<section id="kontakt" className="py-20 px-6 text-center">
<h2 className="text-3xl font-bold mb-6">Kontakt aufnehmen</h2>
<p className="mb-8 text-lg">Lassen Sie uns über Ihre steuerlichen Fragen sprechen.</p>
<Link href="/kontakt">
<Button size="lg">Jetzt Termin vereinbaren</Button>
</Link>
</section>
</main>
)
}

View File

@@ -0,0 +1,73 @@
'use client'
import {
VerticalTimeline,
VerticalTimelineElement,
} from 'react-vertical-timeline-component'
import 'react-vertical-timeline-component/style.min.css'
import {GraduationCap, Briefcase, Building2} from 'lucide-react'
export function AboutMeSection() {
const timeline = [
{
date: '2010',
title: 'Abschluss Bachelor of Laws (LL.B.)',
desc: 'Studium der Wirtschaftsjuristik an der Hochschule XYZ.',
icon: <GraduationCap className="w-5 h-5"/>,
},
{
date: '2012',
title: 'Master Steuerrecht (M.A.)',
desc: 'Vertiefung im Bereich nationales und internationales Steuerrecht.',
icon: <GraduationCap className="w-5 h-5"/>,
},
{
date: '2014',
title: 'Steuerberaterprüfung bestanden',
desc: 'Zulassung als Steuerberaterin durch die Steuerberaterkammer ABC.',
icon: <Building2 className="w-5 h-5"/>,
},
{
date: '20152020',
title: 'Tätigkeit in mittelständischer Kanzlei',
desc: 'Beratung von Unternehmen, Freiberuflern und Privatpersonen.',
icon: <Briefcase className="w-5 h-5"/>,
},
{
date: 'seit 2021',
title: 'Eigene Kanzlei in Musterstadt',
desc: 'Gründung der Steuerkanzlei Mustermann mit Fokus auf digitale Beratung.',
icon: <Briefcase className="w-5 h-5"/>,
},
]
return (
<section id="about" className="py-20 px-6 bg-white">
<div className="max-w-4xl mx-auto">
<h2 className="text-3xl font-bold mb-8 text-left">Über mich</h2>
<p className="text-lg mb-12 text-left">
Als erfahrene Steuerberaterin begleite ich seit über zehn Jahren Mandanten in sämtlichen
steuerlichen
Fragen transparent, digital und auf Augenhöhe. Meine Schwerpunkte liegen in der vorausschauenden
Steuergestaltung sowie der Begleitung von Gründern und mittelständischen Unternehmen.
</p>
<VerticalTimeline lineColor="#e5e7eb">
{timeline.map(({date, title, desc, icon}) => (
<VerticalTimelineElement
key={date}
date={date}
icon={icon}
iconStyle={{background: '#0f172a', color: '#fff'}}
contentStyle={{background: '#f9fafb', color: '#0f172a'}}
contentArrowStyle={{borderRight: '7px solid #f9fafb'}}
>
<h3 className="text-xl font-semibold">{title}</h3>
<p className="text-sm text-muted-foreground">{desc}</p>
</VerticalTimelineElement>
))}
</VerticalTimeline>
</div>
</section>
)
}

View File

@@ -0,0 +1,72 @@
'use client'
import Image from 'next/image'
import {Button} from '@/components/ui/button'
import {motion as m, Variants} from 'framer-motion'
import Link from 'next/link'
const containerVariants: Variants = {
hidden: {opacity: 0},
visible: {
opacity: 1,
transition: {
delay: 0.2,
staggerChildren: 0.4,
ease: 'easeOut',
},
},
}
const itemVariants: Variants = {
hidden: {opacity: 0, y: 40},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.8,
ease: 'easeOut',
},
},
}
export function HeroSection() {
return (
<section className="relative h-[80vh] flex items-center text-white">
<Image
src="/images/hero.jpeg"
alt="Steuerkanzlei Hero"
fill
className="object-cover brightness-[0.4]"
priority
/>
<m.div
className="z-10 text-left px-6 md:px-24 max-w-2xl"
variants={containerVariants}
initial="hidden"
animate="visible"
>
<m.h1
className="text-4xl md:text-6xl font-bold leading-tight"
variants={itemVariants}
>
Ihr Partner für<br/>Steuerberatung & Finanzen
</m.h1>
<m.p
className="mt-6 text-lg md:text-xl"
variants={itemVariants}
>
Persönlich. Kompetent. Transparent.
</m.p>
<m.div className="mt-8" variants={itemVariants}>
<Link href="/kontakt">
<Button size="lg" variant="secondary">
Jetzt Kontakt aufnehmen
</Button>
</Link>
</m.div>
</m.div>
</section>
)
}

View File

@@ -0,0 +1,98 @@
'use client'
import {Briefcase, BookOpen, Building2, User, Coins, ReceiptText} from 'lucide-react'
import {motion as m, Variants} from 'framer-motion'
const containerVariants: Variants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.2,
delayChildren: 0.1,
},
},
}
const cardVariants: Variants = {
hidden: {opacity: 0, y: 30},
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: 'easeOut',
},
},
}
export function ServicesSection() {
const services = [
{
title: 'Einkommensteuer',
subtitle: 'Privatpersonen',
desc: 'Beratung und Erstellung Ihrer jährlichen Einkommensteuererklärung digital und unkompliziert.',
icon: <User className="w-6 h-6 text-primary"/>,
},
{
title: 'Jahresabschlüsse',
subtitle: 'Unternehmen',
desc: 'Erstellung von Handels- und Steuerbilanzen für Einzelunternehmen, GmbH & Co. KG oder Kapitalgesellschaften.',
icon: <BookOpen className="w-6 h-6 text-primary"/>,
},
{
title: 'Lohnbuchhaltung',
subtitle: 'Mitarbeiterabrechnung',
desc: 'Abwicklung Ihrer Lohn- und Gehaltsabrechnungen pünktlich, zuverlässig und digital.',
icon: <Coins className="w-6 h-6 text-primary"/>,
},
{
title: 'Existenzgründung',
subtitle: 'Startups & Gründer',
desc: 'Von der Idee zum Business steuerliche Beratung, Businessplan und Gründungszuschuss-Anträge.',
icon: <Briefcase className="w-6 h-6 text-primary"/>,
},
{
title: 'Unternehmensnachfolge',
subtitle: 'Übergabe & Erbe',
desc: 'Beratung zur steuerlich optimalen Übergabe an Familie oder Käufer. Langfristige Planung inklusive.',
icon: <Building2 className="w-6 h-6 text-primary"/>,
},
{
title: 'Finanzbuchhaltung',
subtitle: 'GoBD & DATEV-konform',
desc: 'Digitale Finanzbuchhaltung inkl. Belegtransfer, OPOS und monatlichen Auswertungen.',
icon: <ReceiptText className="w-6 h-6 text-primary"/>,
},
]
return (
<section id="leistungen" className="py-20 px-6 bg-muted/50">
<div className="max-w-6xl mx-auto text-left">
<h2 className="text-3xl font-bold mb-12 text-center">Unsere Leistungen</h2>
<m.div
className="grid md:grid-cols-2 gap-10"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{once: true, amount: 0.2}}
>
{services.map(({title, subtitle, desc, icon}) => (
<m.div
key={title}
variants={cardVariants}
className="p-6 bg-white rounded-xl shadow-sm hover:shadow-md border flex gap-4"
>
<div className="mt-1">{icon}</div>
<div>
<h3 className="text-xl font-semibold">{title}</h3>
<p className="text-sm text-muted-foreground mb-1">{subtitle}</p>
<p className="text-sm">{desc}</p>
</div>
</m.div>
))}
</m.div>
</div>
</section>
)
}

View File

@@ -0,0 +1,69 @@
'use client'
import {
Globe,
Briefcase,
Building2,
Landmark,
Lightbulb,
PiggyBank,
Users
} from 'lucide-react'
export function SpecialServicesSection() {
const items = [
{
title: 'Grenzgängerberatung',
desc: 'Spezialisierte Beratung für grenzüberschreitende Einkommen und Arbeitsverhältnisse.',
icon: <Globe className="w-8 h-8 text-primary"/>,
},
{
title: 'Beratung von Existenzgründern',
desc: 'Von der Geschäftsidee über den Businessplan bis zur steuerlichen Erfassung.',
icon: <Briefcase className="w-8 h-8 text-primary"/>,
},
{
title: 'Immobilienkaufberatung',
desc: 'Begleitung beim Immobilienkauf steuerliche Optimierung inklusive.',
icon: <Building2 className="w-8 h-8 text-primary"/>,
},
{
title: 'Begleitung bei Bankgesprächen',
desc: 'Vorbereitung und Teilnahme an Finanzierungsgesprächen mit Ihrer Bank.',
icon: <Landmark className="w-8 h-8 text-primary"/>,
},
{
title: 'Steuergestaltung und -planung',
desc: 'Vorausschauende Steueroptimierung für Unternehmen und Privatpersonen.',
icon: <Lightbulb className="w-8 h-8 text-primary"/>,
},
{
title: 'Betriebsprüfung begleiten',
desc: 'Strategische Vorbereitung und persönliche Begleitung von Außenprüfungen.',
icon: <Users className="w-8 h-8 text-primary"/>,
},
{
title: 'Vererben & Schenken',
desc: 'Steuerliche Gestaltung und Begleitung von Vermögensübertragungen.',
icon: <PiggyBank className="w-8 h-8 text-primary"/>,
},
]
return (
<section id="special-services" className="py-20 px-6 bg-muted/50 text-center">
<h2 className="text-3xl font-bold mb-12">Spezialleistungen</h2>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto text-left">
{items.map(({title, desc, icon}) => (
<div
key={title}
className="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition"
>
<div className="mb-4 flex justify-center">{icon}</div>
<h3 className="text-xl font-semibold mb-2 text-center">{title}</h3>
<p className="text-sm text-muted-foreground text-center">{desc}</p>
</div>
))}
</div>
</section>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,64 @@
'use client'
import {useState} from 'react'
import {Input} from '@/components/ui/input'
import {Textarea} from '@/components/ui/textarea'
import {Button} from '@/components/ui/button'
import {motion as m} from 'framer-motion'
import {CheckCircle} from 'lucide-react'
export default function KontaktPage() {
const [success, setSuccess] = useState(false)
const [loading, setLoading] = useState(false)
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
// Simulate delay
setTimeout(() => {
setLoading(false)
setSuccess(true)
}, 1000)
}
return (
<main className="min-h-screen py-20 px-6 max-w-2xl mx-auto">
<m.div
initial={{opacity: 0, y: 30}}
animate={{opacity: 1, y: 0}}
transition={{duration: 0.6}}
>
<h1 className="text-3xl font-bold mb-4 text-center">Kontakt</h1>
<p className="text-center mb-10 text-muted-foreground">
Sie haben Fragen? Schreiben Sie uns direkt über das Formular.
</p>
{success ? (
<div className="text-center">
<CheckCircle className="w-12 h-12 text-green-600 mx-auto mb-4"/>
<p className="text-lg">Vielen Dank! Ihre Nachricht wurde erfolgreich gesendet.</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-1">Name</label>
<Input id="name" name="name" required/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">E-Mail</label>
<Input id="email" name="email" type="email" required/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium mb-1">Nachricht</label>
<Textarea id="message" name="message" rows={5} required/>
</div>
<Button type="submit" disabled={loading}>
{loading ? 'Senden...' : 'Nachricht senden'}
</Button>
</form>
)}
</m.div>
</main>
)
}

View File

@@ -0,0 +1,48 @@
import type {Metadata} from "next";
import {Geist, Geist_Mono} from "next/font/google";
import "./globals.css";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import React from "react";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Steuerberater Mustermann",
description: "Ihre Steuerkanzlei für kompetente Beratung in allen Steuerfragen. Wir unterstützen Privatpersonen und Unternehmen mit maßgeschneiderten Steuerlösungen.",
keywords: "Steuerkanzlei, Steuerberatung, Steuerberater, Steuererklärung, Buchhaltung, Jahresabschluss",
viewport: "width=device-width, initial-scale=1",
robots: "index, follow",
openGraph: {
title: "Steuerberater Mustermann",
description: "Ihre Steuerkanzlei für kompetente Beratung in allen Steuerfragen.",
locale: "de_DE",
type: "website"
}
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="de">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Navbar/>
{children}
<Footer/>
</body>
</html>
);
}

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,11 @@
export default function Footer() {
return (
<footer className="bg-gray-900 text-white py-10 text-center text-sm">
<div>&copy; {new Date().getFullYear()} Steuerkanzlei Mustermann. Alle Rechte vorbehalten.</div>
<div className="mt-2">
<a href="#" className="underline mx-2">Impressum</a>|
<a href="#" className="underline mx-2">Datenschutz</a>
</div>
</footer>
)
}

View File

@@ -0,0 +1,71 @@
'use client'
import {Button} from '@/components/ui/button'
import {Menu} from 'lucide-react'
import Link from 'next/link'
import {useScrollTarget} from '@/hooks/useScrollTarget'
type NavItem = {
label: string
scrollTo?: string
href?: string
}
export default function Navbar() {
const {handleSectionClick} = useScrollTarget()
const navItems: NavItem[] = [
{label: 'Leistungen', scrollTo: 'leistungen'},
// { label: 'Spezialisierung', scrollTo: 'specialities' },
{label: 'Über mich', scrollTo: 'about'},
{label: 'Kontakt', href: '/kontakt'},
]
const handleClick = (item: NavItem) => {
if (item.scrollTo) {
handleSectionClick(item.scrollTo)
} else if (item.href) {
window.location.href = item.href
}
}
const handleLogoClick = () => {
if (window.location.pathname === '/') {
window.scrollTo({top: 0, behavior: 'smooth'})
} else {
localStorage.setItem('scrollTarget', 'top')
window.location.href = '/'
}
}
return (
<nav className="flex items-center justify-between px-6 py-4 border-b sticky top-0 bg-white z-50 shadow-sm">
<button
onClick={handleLogoClick}
className="text-xl font-bold hover:underline"
>
Steuerkanzlei
</button>
<div className="hidden md:flex items-center gap-6 text-sm">
{navItems.map((item) => (
<button
key={item.label}
onClick={() => handleClick(item)}
className="hover:underline"
>
{item.label}
</button>
))}
<Link href="/kontakt">
<Button size="sm">Jetzt Termin sichern</Button>
</Link>
</div>
<div className="md:hidden">
<Menu/>
</div>
</nav>
)
}

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@@ -0,0 +1,36 @@
'use client'
import {useEffect} from 'react'
export function useScrollTarget() {
const scrollToSection = (id: string) => {
if (id === 'top') {
window.scrollTo({top: 0, behavior: 'smooth'})
return
}
const el = document.getElementById(id)
if (el) {
el.scrollIntoView({behavior: 'smooth', block: 'start'})
}
}
const handleSectionClick = (id: string) => {
if (window.location.pathname !== '/') {
localStorage.setItem('scrollTarget', id)
window.location.href = '/'
} else {
scrollToSection(id)
}
}
useEffect(() => {
const target = localStorage.getItem('scrollTarget')
if (target) {
localStorage.removeItem('scrollTarget')
scrollToSection(target)
}
}, [])
return {scrollToSection, handleSectionClick}
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
{
"name": "tax-lawfirm-demo-1",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.19.2",
"lucide-react": "^0.525.0",
"next": "15.3.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-vertical-timeline-component": "^3.5.3",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-vertical-timeline-component": "^3.3.6",
"eslint": "^9",
"eslint-config-next": "15.3.4",
"tailwindcss": "^4",
"tw-animate-css": "^1.3.4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -18,7 +18,19 @@ export const demoCategories = [
name: 'Kanzlei Demo 2', name: 'Kanzlei Demo 2',
url: isProd ? '/lawfirm/demo2/' : `${BASE_URL}:25602`, url: isProd ? '/lawfirm/demo2/' : `${BASE_URL}:25602`,
description: [ description: [
'tbd', 'Ein-Mann Rechtsanwalt',
],
},
],
},
{
label: 'Steuerkanzlei',
items: [
{
name: 'Steuerkanzlei Demo 1',
url: isProd ? '/lawfirm/demo3/' : `${BASE_URL}:25601`,
description: [
'Ein-Mann Steuerkanzlei/Steuerberater',
], ],
}, },
], ],

View File

@@ -95,6 +95,18 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location /lawfirm/demo3/ {
access_by_lua_file /usr/local/openresty/nginx/conf/auth.lua;
rewrite ^/lawfirm/demo2(/.*)$ $1 break;
proxy_pass http://tld1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Add more locations as needed for other demos # Add more locations as needed for other demos
} }
} }