feat: initialize CalBook project with comprehensive scheduling, admin, and deployment infrastructure
This commit is contained in:
11
components/layout/accent-color.tsx
Normal file
11
components/layout/accent-color.tsx
Normal 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;
|
||||
}
|
||||
73
components/layout/animated-page.tsx
Normal file
73
components/layout/animated-page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
components/layout/legal-content-card.tsx
Normal file
38
components/layout/legal-content-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
58
components/layout/public-footer.tsx
Normal file
58
components/layout/public-footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
8
components/layout/session-provider.tsx
Normal file
8
components/layout/session-provider.tsx
Normal 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>;
|
||||
}
|
||||
34
components/layout/theme-provider.tsx
Normal file
34
components/layout/theme-provider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user