feat: BLACK HEART DANCE HOUSE landing page

Landing page with Hero, About, Team, Classes, and Contact sections.
Light/dark mode, scroll reveal animations, Yandex Maps, responsive design.
Next.js 16 + Tailwind v4 + TypeScript.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 17:32:45 +03:00
parent 0588f3fd95
commit f263765597
35 changed files with 5542 additions and 96 deletions

View File

@@ -0,0 +1,41 @@
import Link from "next/link";
interface ButtonProps {
href?: string;
variant?: "primary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
className?: string;
onClick?: () => void;
}
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
export function Button({
href,
variant = "primary",
size = "md",
children,
className = "",
onClick,
}: ButtonProps) {
const classes = `btn-${variant} ${sizes[size]} ${className}`;
if (href) {
return (
<Link href={href} className={classes}>
{children}
</Link>
);
}
return (
<button onClick={onClick} className={classes}>
{children}
</button>
);
}

View File

@@ -0,0 +1,45 @@
"use client";
import { useEffect, useRef, useState } from "react";
interface RevealProps {
children: React.ReactNode;
className?: string;
}
export function Reveal({ children, className = "" }: RevealProps) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.unobserve(el);
}
},
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<div
ref={ref}
className={className}
style={{
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(30px)",
transition: "opacity 0.7s ease-out, transform 0.7s ease-out",
}}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,15 @@
interface SectionHeadingProps {
children: React.ReactNode;
className?: string;
}
export function SectionHeading({ children, className = "" }: SectionHeadingProps) {
return (
<h2
className={`font-display text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl ${className}`}
>
{children}
<span className="mt-2 block h-1 w-16 rounded bg-neutral-900 dark:bg-white" />
</h2>
);
}

View File

@@ -0,0 +1,31 @@
import { Instagram } from "lucide-react";
interface SocialLinksProps {
instagram?: string;
instagramHandle?: string;
className?: string;
iconSize?: number;
}
export function SocialLinks({
instagram,
instagramHandle,
className = "",
iconSize = 24,
}: SocialLinksProps) {
return (
<div className={`flex items-center gap-4 ${className}`}>
{instagram && (
<a
href={instagram}
target="_blank"
rel="noopener noreferrer"
className="social-icon flex items-center gap-2"
>
<Instagram size={iconSize} />
{instagramHandle && <span className="text-sm font-medium">{instagramHandle}</span>}
</a>
)}
</div>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
import { Moon, Sun } from "lucide-react";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const [dark, setDark] = useState(false);
useEffect(() => {
const stored = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const isDark = stored === "dark" || (!stored && prefersDark);
setDark(isDark);
document.documentElement.classList.toggle("dark", isDark);
}, []);
function toggle() {
const next = !dark;
setDark(next);
document.documentElement.classList.toggle("dark", next);
localStorage.setItem("theme", next ? "dark" : "light");
}
return (
<button
onClick={toggle}
aria-label="Переключить тему"
className="social-icon rounded-full p-2"
>
{dark ? <Sun size={20} /> : <Moon size={20} />}
</button>
);
}