diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9ba7f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+dist
+.DS_Store
+server/public
+vite.config.ts.*
+*.tar.gz
\ No newline at end of file
diff --git a/.replit b/.replit
new file mode 100644
index 0000000..51f344f
--- /dev/null
+++ b/.replit
@@ -0,0 +1,36 @@
+modules = ["nodejs-20", "web", "postgresql-16"]
+run = "npm run dev"
+hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
+
+[nix]
+channel = "stable-24_05"
+
+[deployment]
+deploymentTarget = "autoscale"
+build = ["npm", "run", "build"]
+run = ["npm", "run", "start"]
+
+[[ports]]
+localPort = 5000
+externalPort = 80
+
+[workflows]
+runButton = "Project"
+
+[[workflows.workflow]]
+name = "Project"
+mode = "parallel"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "workflow.run"
+args = "Start application"
+
+[[workflows.workflow]]
+name = "Start application"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "shell.exec"
+args = "npm run dev"
+waitForPort = 5000
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..afdd8b0
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Pilates with Fadia | Find Balance, Strength & Inner Peace
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..f9e7b84
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,50 @@
+import { Switch, Route } from "wouter";
+import { queryClient } from "./lib/queryClient";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { AuthProvider } from "@/hooks/use-auth";
+
+import HomePage from "@/pages/home-page";
+import AboutPage from "@/pages/about-page";
+import ClassesPage from "@/pages/classes-page";
+import CommunityPage from "@/pages/community-page";
+import ContactPage from "@/pages/contact-page";
+import AuthPage from "@/pages/auth-page";
+import NotFound from "@/pages/not-found";
+import { ProtectedRoute } from "./lib/protected-route";
+import Header from "@/components/navigation/header";
+import Footer from "@/components/navigation/footer";
+
+function Router() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/about/about-section.tsx b/client/src/components/about/about-section.tsx
new file mode 100644
index 0000000..9476875
--- /dev/null
+++ b/client/src/components/about/about-section.tsx
@@ -0,0 +1,98 @@
+import { Link } from "wouter";
+import { ArabicDecoration } from "@/components/ui/arabic-decoration";
+
+export function AboutSection() {
+ const certifications = [
+ {
+ title: "Certified Instructor",
+ description: "Comprehensive Pilates Certification",
+ icon: "fa-certificate",
+ color: "teal",
+ },
+ {
+ title: "Advanced Training",
+ description: "Specialized in Rehabilitation",
+ icon: "fa-graduation-cap",
+ color: "purple",
+ },
+ {
+ title: "Wellness Expert",
+ description: "Holistic Approach to Health",
+ icon: "fa-heartbeat",
+ color: "rose",
+ },
+ {
+ title: "10+ Years Experience",
+ description: "Guiding Hundreds of Students",
+ icon: "fa-users",
+ color: "teal",
+ },
+ ];
+
+ return (
+
+
+
+
+ About Fadia
+
+
+ Discover the journey, philosophy, and passion behind Pilates with Fadia.
+
+
+
+
+
+
+
+
+
+
+
My Pilates Journey
+
+ With over a decade of experience in the art of Pilates, I have dedicated my life to understanding the profound connection between movement, breath, and wellness. My journey began in 2010 when I discovered how Pilates transformed not only my physical strength but my entire approach to well-being.
+
+
+
My Teaching Philosophy
+
+ I believe that Pilates is more than exercise—it's a path to self-discovery and inner harmony. My teaching combines classical techniques with modern approaches, always honoring the core principles of concentration, control, centering, flow, precision, and breath.
+
+
+
+
+ "Movement is medicine for creating change in a person's physical, emotional, and mental states."
+
+
+
+
+
+
+
+
+
+
+ {certifications.map((cert, index) => (
+
+
+
+
+
{cert.title}
+
{cert.description}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/client/src/components/classes/booking-calendar.tsx b/client/src/components/classes/booking-calendar.tsx
new file mode 100644
index 0000000..ec2d126
--- /dev/null
+++ b/client/src/components/classes/booking-calendar.tsx
@@ -0,0 +1,201 @@
+import { useState } from "react";
+import { Calendar } from "@/components/ui/calendar";
+import { Class, insertBookingSchema } from "@shared/schema";
+import { apiRequest, queryClient } from "@/lib/queryClient";
+import { useMutation } from "@tanstack/react-query";
+import { useToast } from "@/hooks/use-toast";
+import { Button } from "@/components/ui/button";
+import { format, addDays, isAfter, isBefore, addMinutes, startOfDay } from "date-fns";
+import { Loader2 } from "lucide-react";
+
+interface BookingCalendarProps {
+ selectedClass: Class;
+}
+
+export function BookingCalendar({ selectedClass }: BookingCalendarProps) {
+ const { toast } = useToast();
+ const [selectedDate, setSelectedDate] = useState(addDays(new Date(), 1));
+ const [selectedTime, setSelectedTime] = useState(null);
+
+ // Generate time slots for the selected date
+ const timeSlots = generateTimeSlots(selectedDate);
+
+ const bookMutation = useMutation({
+ mutationFn: async (bookingData: { classId: number, date: Date }) => {
+ const res = await apiRequest("POST", "/api/bookings", bookingData);
+ return await res.json();
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["/api/bookings"] });
+ toast({
+ title: "Booking successful",
+ description: "Your class has been booked successfully!",
+ });
+ // Reset selected time
+ setSelectedTime(null);
+ },
+ onError: (error: Error) => {
+ toast({
+ title: "Booking failed",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleConfirmBooking = () => {
+ if (!selectedDate || !selectedTime) {
+ toast({
+ title: "Booking error",
+ description: "Please select both a date and time",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ // Combine date and time
+ const [hours, minutes] = selectedTime.split(":").map(Number);
+ const bookingDateTime = new Date(selectedDate);
+ bookingDateTime.setHours(hours, minutes, 0, 0);
+
+ bookMutation.mutate({
+ classId: selectedClass.id,
+ date: bookingDateTime
+ });
+ };
+
+ const formatPrice = (price: number) => {
+ return `$${(price / 100).toFixed(2)}`;
+ };
+
+ return (
+
+
Book Your Next Class
+
+
+
+
+ isBefore(date, startOfDay(new Date())) || isAfter(date, addDays(new Date(), 60))}
+ />
+
+
+ {selectedDate && (
+
+
+ Available Times for {format(selectedDate, "MMMM d, yyyy")}
+
+
+ {timeSlots.map((time, index) => (
+
setSelectedTime(time)}
+ >
+ {formatTimeDisplay(time)}
+
+ ))}
+
+
+ )}
+
+
+
+
Booking Summary
+
+
+
+ Class:
+ {selectedClass.name}
+
+
+ Date:
+
+ {selectedDate ? format(selectedDate, "MMMM d, yyyy") : "Select a date"}
+
+
+
+ Time:
+
+ {selectedTime ? formatTimeDisplay(selectedTime) : "Select a time"}
+
+
+
+ Duration:
+ {selectedClass.duration} minutes
+
+
+ Price:
+ {formatPrice(selectedClass.price)}
+
+
+
+
+
+ Total:
+ {formatPrice(selectedClass.price)}
+
+
+
+
+ {bookMutation.isPending ? (
+ <>
+ Processing
+ >
+ ) : (
+ "Confirm Booking"
+ )}
+
+
+
+ You'll receive a confirmation email with details and calendar invite.
+
+
+
+
+ );
+}
+
+// Helper functions
+function generateTimeSlots(date?: Date): string[] {
+ if (!date) return [];
+
+ const slots = [];
+ const startTime = 9; // 9 AM
+ const endTime = 20; // 8 PM
+
+ for (let hour = startTime; hour < endTime; hour++) {
+ slots.push(`${hour}:00`);
+ slots.push(`${hour}:30`);
+ }
+
+ // Add a random selection of unavailable slots
+ const availableSlots = [...slots];
+ const numberOfUnavailableSlots = Math.floor(Math.random() * 4) + 2; // 2-5 unavailable slots
+
+ for (let i = 0; i < numberOfUnavailableSlots; i++) {
+ const randomIndex = Math.floor(Math.random() * availableSlots.length);
+ availableSlots.splice(randomIndex, 1);
+ }
+
+ return availableSlots;
+}
+
+function formatTimeDisplay(time: string): string {
+ const [hours, minutes] = time.split(":").map(Number);
+ const period = hours < 12 ? "AM" : "PM";
+ const displayHours = hours % 12 || 12;
+ return `${displayHours}:${minutes.toString().padStart(2, "0")} ${period}`;
+}
diff --git a/client/src/components/classes/class-card.tsx b/client/src/components/classes/class-card.tsx
new file mode 100644
index 0000000..b8d8013
--- /dev/null
+++ b/client/src/components/classes/class-card.tsx
@@ -0,0 +1,101 @@
+import { Class } from "@shared/schema";
+import { useAuth } from "@/hooks/use-auth";
+import { useState } from "react";
+import { useToast } from "@/hooks/use-toast";
+import { useLocation } from "wouter";
+
+interface ClassCardProps {
+ classData: Class;
+ onBookClick: (classId: number) => void;
+}
+
+export function ClassCard({ classData, onBookClick }: ClassCardProps) {
+ const { user } = useAuth();
+ const { toast } = useToast();
+ const [_, setLocation] = useLocation();
+
+ const formatPrice = (price: number) => {
+ return `$${(price / 100).toFixed(2)}`;
+ };
+
+ const formatDuration = (minutes: number) => {
+ return `${minutes} minutes`;
+ };
+
+ const handleBookNow = () => {
+ if (!user) {
+ toast({
+ title: "Authentication required",
+ description: "Please login or sign up to book a class",
+ variant: "destructive"
+ });
+ setLocation("/auth");
+ return;
+ }
+
+ onBookClick(classData.id);
+ };
+
+ // Determine badge color based on class type
+ const badgeColor = () => {
+ switch (classData.classType) {
+ case "group": return "bg-teal-light text-teal";
+ case "small-group": return "bg-purple-light text-purple";
+ case "private": return "bg-rose-light text-rose";
+ default: return "bg-gray-100 text-gray-800";
+ }
+ };
+
+ // Determine button color based on class type
+ const buttonColor = () => {
+ switch (classData.classType) {
+ case "group": return "bg-teal text-white";
+ case "small-group": return "bg-purple text-white";
+ case "private": return "bg-rose text-white";
+ default: return "bg-gray-500 text-white";
+ }
+ };
+
+ // Format class type for display
+ const formatClassType = (type: string) => {
+ switch (type) {
+ case "group": return "Group";
+ case "small-group": return "Small Group";
+ case "private": return "1-on-1";
+ default: return type;
+ }
+ };
+
+ return (
+
+
+
+
+
{classData.name}
+
+ {formatClassType(classData.classType)}
+
+
+
+ {classData.description}
+
+
+
+ {formatDuration(classData.duration)}
+
+ {formatPrice(classData.price)} / class
+
+
+ Book Now
+
+
+
+ );
+}
diff --git a/client/src/components/classes/classes-section.tsx b/client/src/components/classes/classes-section.tsx
new file mode 100644
index 0000000..f6772e5
--- /dev/null
+++ b/client/src/components/classes/classes-section.tsx
@@ -0,0 +1,94 @@
+import { useEffect, useState } from "react";
+import { ClassCard } from "./class-card";
+import { BookingCalendar } from "./booking-calendar";
+import { useQuery } from "@tanstack/react-query";
+import { Class } from "@shared/schema";
+import { ArabicDecoration } from "@/components/ui/arabic-decoration";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function ClassesSection() {
+ const [selectedClassId, setSelectedClassId] = useState(null);
+
+ const { data: classes, isLoading, error } = useQuery({
+ queryKey: ["/api/classes"],
+ });
+
+ const selectedClass = selectedClassId
+ ? classes?.find(c => c.id === selectedClassId)
+ : null;
+
+ const handleBookClick = (classId: number) => {
+ setSelectedClassId(classId);
+
+ // Scroll to booking calendar
+ const bookingCalendar = document.getElementById("booking-calendar");
+ if (bookingCalendar) {
+ bookingCalendar.scrollIntoView({ behavior: "smooth" });
+ }
+ };
+
+ if (error) {
+ return (
+
+
+
Error loading classes. Please try again later.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Our Classes
+
+
+ Discover the perfect Pilates class tailored to your needs and wellness goals.
+
+
+
+ {isLoading ? (
+
+ {[1, 2, 3].map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ ) : (
+
+ {classes?.map(classItem => (
+
+ ))}
+
+ )}
+
+
+ {selectedClass && (
+
+ )}
+
+
+
+ );
+}
diff --git a/client/src/components/community/community-section.tsx b/client/src/components/community/community-section.tsx
new file mode 100644
index 0000000..62abdfd
--- /dev/null
+++ b/client/src/components/community/community-section.tsx
@@ -0,0 +1,121 @@
+import { useState, useEffect } from "react";
+import { Testimonial } from "./testimonial";
+import { useAuth } from "@/hooks/use-auth";
+import { Link } from "wouter";
+import { ArabicDecoration } from "@/components/ui/arabic-decoration";
+import { Card, CardContent } from "@/components/ui/card";
+
+export function CommunitySection() {
+ const { user } = useAuth();
+ const [iframeLoaded, setIframeLoaded] = useState(false);
+
+ const testimonials = [
+ {
+ quote: "Fadia's approach to Pilates has completely transformed my relationship with my body. The community she's built is supportive and encouraging.",
+ author: "Sarah H.",
+ memberSince: "2021",
+ initials: "SH",
+ color: "teal",
+ },
+ {
+ quote: "As someone recovering from back surgery, I was hesitant to try Pilates. Fadia's expertise made me feel safe and I've gained strength I never thought possible.",
+ author: "Michael K.",
+ memberSince: "2022",
+ initials: "MK",
+ color: "purple",
+ },
+ {
+ quote: "The community aspect of Pilates with Fadia sets it apart. I've not only improved my physical health but have made wonderful connections with fellow members.",
+ author: "Amina L.",
+ memberSince: "2020",
+ initials: "AL",
+ color: "rose",
+ },
+ ];
+
+ // This function would load the actual whiteboard in a real implementation
+ // For now, we're simulating the load timing
+ useEffect(() => {
+ if (user) {
+ const timer = setTimeout(() => {
+ setIframeLoaded(true);
+ }, 1000);
+
+ return () => clearTimeout(timer);
+ }
+ }, [user]);
+
+ return (
+
+ );
+}
diff --git a/client/src/components/community/testimonial.tsx b/client/src/components/community/testimonial.tsx
new file mode 100644
index 0000000..012b360
--- /dev/null
+++ b/client/src/components/community/testimonial.tsx
@@ -0,0 +1,47 @@
+interface TestimonialProps {
+ quote: string;
+ author: string;
+ memberSince: string;
+ initials: string;
+ color: "teal" | "purple" | "rose";
+}
+
+export function Testimonial({ quote, author, memberSince, initials, color }: TestimonialProps) {
+ const colorClasses = {
+ teal: {
+ bg: "bg-teal-light",
+ text: "text-teal",
+ opacity: "text-teal opacity-30",
+ },
+ purple: {
+ bg: "bg-purple-light",
+ text: "text-purple",
+ opacity: "text-purple opacity-30",
+ },
+ rose: {
+ bg: "bg-rose-light",
+ text: "text-rose",
+ opacity: "text-rose opacity-30",
+ },
+ };
+
+ return (
+
+
+
+
+
+ "{quote}"
+
+
+
+ {initials}
+
+
+
{author}
+
Member since {memberSince}
+
+
+
+ );
+}
diff --git a/client/src/components/contact/contact-section.tsx b/client/src/components/contact/contact-section.tsx
new file mode 100644
index 0000000..32e3988
--- /dev/null
+++ b/client/src/components/contact/contact-section.tsx
@@ -0,0 +1,217 @@
+import { useState } from "react";
+import { useMutation } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+import { insertContactMessageSchema } from "@shared/schema";
+import { useToast } from "@/hooks/use-toast";
+import { ArabicDecoration } from "@/components/ui/arabic-decoration";
+import { Loader2 } from "lucide-react";
+
+export function ContactSection() {
+ const { toast } = useToast();
+ const [formData, setFormData] = useState({
+ name: "",
+ email: "",
+ subject: "",
+ message: ""
+ });
+
+ const contactMutation = useMutation({
+ mutationFn: async (data: typeof formData) => {
+ const res = await apiRequest("POST", "/api/contact", data);
+ return await res.json();
+ },
+ onSuccess: () => {
+ toast({
+ title: "Message sent",
+ description: "Thank you for your message. We'll get back to you soon!",
+ });
+ // Reset form
+ setFormData({
+ name: "",
+ email: "",
+ subject: "",
+ message: ""
+ });
+ },
+ onError: (error: Error) => {
+ toast({
+ title: "Failed to send message",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ contactMutation.mutate(formData);
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/home/feature-card.tsx b/client/src/components/home/feature-card.tsx
new file mode 100644
index 0000000..7f603c3
--- /dev/null
+++ b/client/src/components/home/feature-card.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+
+interface FeatureCardProps {
+ title: string;
+ description: string;
+ icon: string;
+ color: "teal" | "purple" | "rose";
+}
+
+export function FeatureCard({ title, description, icon, color }: FeatureCardProps) {
+ const colorClasses = {
+ teal: {
+ bg: "bg-teal-light",
+ iconBg: "bg-teal",
+ text: "text-teal",
+ },
+ purple: {
+ bg: "bg-purple-light",
+ iconBg: "bg-purple",
+ text: "text-purple",
+ },
+ rose: {
+ bg: "bg-rose-light",
+ iconBg: "bg-rose",
+ text: "text-rose",
+ },
+ };
+
+ return (
+
+
+
+
+
{title}
+
{description}
+
+ );
+}
+
+export function FeaturesSection() {
+ const features = [
+ {
+ title: "Balance",
+ description: "Find harmony between body and mind through mindful movement.",
+ icon: "fa-align-center",
+ color: "teal" as const,
+ },
+ {
+ title: "Strength",
+ description: "Build core power and muscular endurance through controlled exercises.",
+ icon: "fa-dumbbell",
+ color: "purple" as const,
+ },
+ {
+ title: "Flexibility",
+ description: "Enhance your range of motion and release tension throughout your body.",
+ icon: "fa-wind",
+ color: "rose" as const,
+ },
+ ];
+
+ return (
+
+
+ {features.map((feature, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/components/home/hero-section.tsx b/client/src/components/home/hero-section.tsx
new file mode 100644
index 0000000..5624ffd
--- /dev/null
+++ b/client/src/components/home/hero-section.tsx
@@ -0,0 +1,39 @@
+import { Link } from "wouter";
+
+export function HeroSection() {
+ return (
+
+
+
+
+
+ Find Balance, Strength & Inner Peace
+
+
+
+ Transform your body and mind through the art of Pilates. Join our serene studio for personalized sessions that promote harmony, flexibility, and strength.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/navigation/footer.tsx b/client/src/components/navigation/footer.tsx
new file mode 100644
index 0000000..34c582a
--- /dev/null
+++ b/client/src/components/navigation/footer.tsx
@@ -0,0 +1,130 @@
+import { Logo } from "@/components/ui/logo";
+import { Link } from "wouter";
+
+export default function Footer() {
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+
+
+
+
+
+ Transforming bodies and minds through the art of Pilates with a touch of cultural elegance.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © {currentYear} Pilates with Fadia. All rights reserved.
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/navigation/header.tsx b/client/src/components/navigation/header.tsx
new file mode 100644
index 0000000..891d499
--- /dev/null
+++ b/client/src/components/navigation/header.tsx
@@ -0,0 +1,143 @@
+import { useState } from "react";
+import { Link, useLocation } from "wouter";
+import { Logo } from "@/components/ui/logo";
+import { Button } from "@/components/ui/button";
+import { useAuth } from "@/hooks/use-auth";
+import {
+ Sheet,
+ SheetContent,
+ SheetTrigger,
+ SheetClose
+} from "@/components/ui/sheet";
+import { Menu } from "lucide-react";
+
+export default function Header() {
+ const [location] = useLocation();
+ const { user, logoutMutation } = useAuth();
+
+ const isActive = (path: string) => {
+ return location === path
+ ? "text-teal"
+ : "text-gray-700 hover:text-teal";
+ };
+
+ const navLinks = [
+ { name: "Home", path: "/" },
+ { name: "About", path: "/about" },
+ { name: "Classes", path: "/classes" },
+ { name: "Community", path: "/community" },
+ { name: "Contact", path: "/contact" },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ {/* Desktop Navigation */}
+
+ {navLinks.map((link) => (
+
+
+ {link.name}
+
+
+ ))}
+
+
+
+ {user ? (
+ <>
+
+ Hi, {user.fullName || user.username}
+
+
logoutMutation.mutate()}
+ >
+ Logout
+
+ >
+ ) : (
+ <>
+
+
+ Login
+
+
+
+
+ Sign Up
+
+
+ >
+ )}
+
+
+ {/* Mobile Menu */}
+
+
+
+
+ Open main menu
+
+
+
+
+
+ {navLinks.map((link) => (
+
+
+
+ {link.name}
+
+
+
+ ))}
+
+ {user ? (
+ <>
+
+ Hi, {user.fullName || user.username}
+
+
+ logoutMutation.mutate()}
+ >
+ Logout
+
+
+ >
+ ) : (
+ <>
+
+
+
+ Login
+
+
+
+
+
+
+ Sign Up
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/newsletter/newsletter-section.tsx b/client/src/components/newsletter/newsletter-section.tsx
new file mode 100644
index 0000000..7380482
--- /dev/null
+++ b/client/src/components/newsletter/newsletter-section.tsx
@@ -0,0 +1,138 @@
+import { useState } from "react";
+import { useMutation } from "@tanstack/react-query";
+import { apiRequest } from "@/lib/queryClient";
+import { insertNewsletterSchema } from "@shared/schema";
+import { useToast } from "@/hooks/use-toast";
+import { Loader2 } from "lucide-react";
+
+export function NewsletterSection() {
+ const { toast } = useToast();
+ const [email, setEmail] = useState("");
+ const [agreedToTerms, setAgreedToTerms] = useState(false);
+
+ const newsletterMutation = useMutation({
+ mutationFn: async (newsletterData: { email: string, agreedToTerms: boolean }) => {
+ const res = await apiRequest("POST", "/api/newsletter", newsletterData);
+ return await res.json();
+ },
+ onSuccess: () => {
+ toast({
+ title: "Subscription successful",
+ description: "Thank you for subscribing to our newsletter!",
+ });
+ // Reset form
+ setEmail("");
+ setAgreedToTerms(false);
+ },
+ onError: (error: Error) => {
+ toast({
+ title: "Subscription failed",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!email) {
+ toast({
+ title: "Email required",
+ description: "Please enter your email address",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ if (!agreedToTerms) {
+ toast({
+ title: "Consent required",
+ description: "Please agree to receive emails from Pilates with Fadia",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ newsletterMutation.mutate({ email, agreedToTerms });
+ };
+
+ return (
+
+
+
+
+
Join Our Newsletter
+
+ Stay updated with wellness tips, special class offerings, and community events. We promise to respect your inbox.
+
+
+
+
+ setEmail(e.target.value)}
+ required
+ />
+
+ {newsletterMutation.isPending ? (
+ <>
+
+ Subscribing...
+ >
+ ) : (
+ "Subscribe"
+ )}
+
+
+
+
+ setAgreedToTerms(e.target.checked)}
+ required
+ />
+ I agree to receive emails from Pilates with Fadia
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..e6a723d
--- /dev/null
+++ b/client/src/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..8722561
--- /dev/null
+++ b/client/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/client/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/client/src/components/ui/arabic-decoration.tsx b/client/src/components/ui/arabic-decoration.tsx
new file mode 100644
index 0000000..b1b907f
--- /dev/null
+++ b/client/src/components/ui/arabic-decoration.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+interface ArabicDecorationProps {
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function ArabicDecoration({ children, className = "" }: ArabicDecorationProps) {
+ return (
+
+ );
+}
diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/client/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/client/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..2174f71
--- /dev/null
+++ b/client/src/components/ui/calendar.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/client/src/components/ui/carousel.tsx b/client/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..9c2b9bf
--- /dev/null
+++ b/client/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/client/src/components/ui/chart.tsx b/client/src/components/ui/chart.tsx
new file mode 100644
index 0000000..39fba6d
--- /dev/null
+++ b/client/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+