feat: initialize CalBook project with comprehensive scheduling, admin, and deployment infrastructure

This commit is contained in:
2026-05-07 13:04:02 +02:00
parent 51acfe9488
commit ee48a93824
133 changed files with 26049 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
"use client";
import { useEffect } from "react";
export function AccentColorScript({ color }: { color: string }) {
useEffect(() => {
if (color) document.documentElement.style.setProperty("--accent", color);
}, [color]);
return null;
}

View File

@@ -0,0 +1,73 @@
"use client";
import { motion, type Variants } from "framer-motion";
import type { ReactNode } from "react";
const pageVariants: Variants = {
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0 }
};
const cardVariants: Variants = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0 }
};
export function AnimatedPage({ children }: { children: ReactNode }) {
return (
<motion.div
variants={pageVariants}
initial="hidden"
animate="visible"
transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
>
{children}
</motion.div>
);
}
export function AnimatedCard({
children,
className,
delay = 0
}: {
children: ReactNode;
className?: string;
delay?: number;
}) {
return (
<motion.div
className={className}
variants={cardVariants}
initial="hidden"
animate="visible"
transition={{
duration: 0.3,
delay,
ease: [0.22, 1, 0.36, 1]
}}
>
{children}
</motion.div>
);
}
export function AnimatedStagger({
children,
staggerDelay = 0.06
}: {
children: ReactNode;
staggerDelay?: number;
}) {
return (
<motion.div
initial="hidden"
animate="visible"
variants={{
visible: { transition: { staggerChildren: staggerDelay } }
}}
>
{children}
</motion.div>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
type LegalContentCardProps = {
title: string;
content: string;
backHref?: string;
backLabel?: string;
};
export function LegalContentCard({
title,
content,
backHref = "/buchen",
backLabel = "Zurück zur Buchung"
}: LegalContentCardProps) {
return (
<motion.article
className="rounded-2xl border border-slate-200 bg-white p-6 lg:p-8 space-y-5"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
>
<h1 className="text-2xl font-bold text-slate-900">{title}</h1>
<div className="whitespace-pre-wrap text-sm text-slate-700 leading-relaxed">
{content}
</div>
<Link
href={backHref}
className="inline-flex text-sm font-medium text-indigo-600 hover:text-indigo-700"
>
{backLabel}
</Link>
</motion.article>
);
}

View File

@@ -0,0 +1,58 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
type PublicFooterProps = {
companyName?: string;
privacyLabel?: string;
privacyHref?: string;
imprintLabel?: string;
imprintHref?: string;
copyrightTemplate?: string;
className?: string;
};
export function PublicFooter({
companyName = "CalBook",
privacyLabel = "Datenschutz",
privacyHref = "/datenschutz",
imprintLabel = "Impressum",
imprintHref = "/impressum",
copyrightTemplate = "© {{year}} {{companyName}}",
className
}: PublicFooterProps) {
const year = String(new Date().getFullYear());
const copyrightText = (copyrightTemplate || "© {{year}} {{companyName}}")
.replace(/\{\{\s*year\s*\}\}/gi, year)
.replace(/\{\{\s*companyName\s*\}\}/gi, companyName)
.trim();
return (
<footer className={cn("w-full border-t border-slate-200 bg-slate-50", className)}>
<div className="w-full px-4 py-4 lg:px-8">
<div className="flex items-center justify-between gap-4">
<nav className="flex h-5 items-center gap-4 text-xs leading-none text-slate-500">
{privacyLabel && privacyHref ? (
<Link
href={privacyHref}
className="inline-flex h-5 items-center whitespace-nowrap transition-colors hover:text-slate-700"
>
{privacyLabel}
</Link>
) : null}
{imprintLabel && imprintHref ? (
<Link
href={imprintHref}
className="inline-flex h-5 items-center whitespace-nowrap transition-colors hover:text-slate-700"
>
{imprintLabel}
</Link>
) : null}
</nav>
<p className="flex h-5 items-center whitespace-nowrap text-right text-xs leading-none text-slate-400">
{copyrightText || `© ${year} ${companyName}`}
</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,8 @@
"use client";
import { SessionProvider } from "next-auth/react";
import { type ReactNode } from "react";
export function AuthSessionProvider({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@@ -0,0 +1,34 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ReactNode } from "react";
export type ThemeModeSetting = "light" | "dark" | "auto";
function toNextTheme(mode: ThemeModeSetting): "light" | "dark" | "system" {
if (mode === "light") return "light";
if (mode === "dark") return "dark";
return "system";
}
export function ThemeProvider({
children,
mode = "auto"
}: {
children: ReactNode;
mode?: ThemeModeSetting;
}) {
const defaultTheme = toNextTheme(mode);
const enableSystem = mode === "auto";
return (
<NextThemesProvider
attribute="class"
defaultTheme={defaultTheme}
enableSystem={enableSystem}
forcedTheme={mode === "auto" ? undefined : mode}
>
{children}
</NextThemesProvider>
);
}