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:
41
src/components/ui/Button.tsx
Normal file
41
src/components/ui/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
src/components/ui/Reveal.tsx
Normal file
45
src/components/ui/Reveal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/components/ui/SectionHeading.tsx
Normal file
15
src/components/ui/SectionHeading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
src/components/ui/SocialLinks.tsx
Normal file
31
src/components/ui/SocialLinks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
src/components/ui/ThemeToggle.tsx
Normal file
33
src/components/ui/ThemeToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user