Chameleon

New Component

Create reusable components in Chameleon

New Component

Learn how to create reusable components in Chameleon using React, TypeScript, and Tailwind CSS.

Overview

Components in Chameleon are built with React, TypeScript, and Tailwind CSS. They follow a consistent structure and can be reused across different pages. Components are organized in the src/components/ directory with proper type definitions.

Component Structure

Directory Organization

src/components/
├── ui/                    # Shadcn UI components
│   ├── button.tsx
│   ├── input.tsx
│   └── card.tsx
├── blocks/                # Layout blocks
│   ├── header.tsx
│   ├── footer.tsx
│   └── hero.tsx
├── forms/                 # Form components
│   ├── contact-form.tsx
│   └── login-form.tsx
└── common/                # Common components
    ├── loading.tsx
    └── error-boundary.tsx

Component Template

// src/components/common/example-component.tsx
import React from "react";
import { cn } from "@/lib/utils";

interface ExampleComponentProps {
  title: string;
  description?: string;
  className?: string;
  children?: React.ReactNode;
}

export function ExampleComponent({
  title,
  description,
  className,
  children,
}: ExampleComponentProps) {
  return (
    <div className={cn("p-4 border rounded-lg", className)}>
      <h3 className="text-lg font-semibold mb-2">{title}</h3>
      {description && (
        <p className="text-gray-600 mb-4">{description}</p>
      )}
      {children}
    </div>
  );
}

Component Development

Basic Component

// src/components/common/loading-spinner.tsx
import { cn } from "@/lib/utils";

interface LoadingSpinnerProps {
  size?: "sm" | "md" | "lg";
  className?: string;
}

export function LoadingSpinner({ size = "md", className }: LoadingSpinnerProps) {
  const sizeClasses = {
    sm: "w-4 h-4",
    md: "w-6 h-6",
    lg: "w-8 h-8",
  };

  return (
    <div className={cn("animate-spin", sizeClasses[size], className)}>
      <svg className="w-full h-full" viewBox="0 0 24 24">
        <circle
          className="opacity-25"
          cx="12"
          cy="12"
          r="10"
          stroke="currentColor"
          strokeWidth="4"
          fill="none"
        />
        <path
          className="opacity-75"
          fill="currentColor"
          d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
        />
      </svg>
    </div>
  );
}

Form Component

// src/components/forms/contact-form.tsx
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useTranslations } from "next-intl";

interface ContactFormProps {
  onSubmit: (data: FormData) => Promise<void>;
  className?: string;
}

export function ContactForm({ onSubmit, className }: ContactFormProps) {
  const t = useTranslations("contact.form");
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    const formData = new FormData(e.currentTarget);
    await onSubmit(formData);
    
    setIsSubmitting(false);
  };

  return (
    <form onSubmit={handleSubmit} className={className}>
      <div className="space-y-4">
        <div>
          <label htmlFor="name" className="block text-sm font-medium mb-1">
            {t("name")}
          </label>
          <Input
            id="name"
            name="name"
            type="text"
            required
            placeholder={t("name_placeholder")}
          />
        </div>
        
        <div>
          <label htmlFor="email" className="block text-sm font-medium mb-1">
            {t("email")}
          </label>
          <Input
            id="email"
            name="email"
            type="email"
            required
            placeholder={t("email_placeholder")}
          />
        </div>
        
        <div>
          <label htmlFor="message" className="block text-sm font-medium mb-1">
            {t("message")}
          </label>
          <Textarea
            id="message"
            name="message"
            required
            placeholder={t("message_placeholder")}
            rows={4}
          />
        </div>
        
        <Button type="submit" disabled={isSubmitting}>
          {isSubmitting ? t("submitting") : t("submit")}
        </Button>
      </div>
    </form>
  );
}

Layout Component

// src/components/blocks/section.tsx
import { cn } from "@/lib/utils";

interface SectionProps {
  children: React.ReactNode;
  className?: string;
  background?: "white" | "gray" | "primary";
  padding?: "sm" | "md" | "lg";
}

export function Section({
  children,
  className,
  background = "white",
  padding = "md",
}: SectionProps) {
  const backgroundClasses = {
    white: "bg-white",
    gray: "bg-gray-50",
    primary: "bg-blue-50",
  };

  const paddingClasses = {
    sm: "py-8",
    md: "py-16",
    lg: "py-24",
  };

  return (
    <section
      className={cn(
        backgroundClasses[background],
        paddingClasses[padding],
        className
      )}
    >
      <div className="container mx-auto px-4">
        {children}
      </div>
    </section>
  );
}

Component Usage

Using Components in Pages

// src/app/[locale]/(default)/about/page.tsx
import { Section } from "@/components/blocks/section";
import { ContactForm } from "@/components/forms/contact-form";
import { LoadingSpinner } from "@/components/common/loading-spinner";

export default function AboutPage() {
  const handleContactSubmit = async (formData: FormData) => {
    // Handle form submission
    console.log("Form submitted:", formData);
  };

  return (
    <div>
      <Section background="primary">
        <h1 className="text-3xl font-bold">About Us</h1>
        <p className="text-lg mt-4">Learn more about our company.</p>
      </Section>
      
      <Section>
        <ContactForm onSubmit={handleContactSubmit} />
      </Section>
    </div>
  );
}

Using Shadcn UI Components

// src/components/forms/user-profile-form.tsx
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export function UserProfileForm() {
  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>User Profile</CardTitle>
      </CardHeader>
      <CardContent>
        <form className="space-y-4">
          <div>
            <Label htmlFor="name">Name</Label>
            <Input id="name" type="text" />
          </div>
          <div>
            <Label htmlFor="email">Email</Label>
            <Input id="email" type="email" />
          </div>
          <Button type="submit">Save Changes</Button>
        </form>
      </CardContent>
    </Card>
  );
}

File Locations

  • src/components/ui/ - Shadcn UI components
  • src/components/blocks/ - Layout and page blocks
  • src/components/forms/ - Form components
  • src/components/common/ - Reusable common components
  • src/lib/utils.ts - Utility functions (cn helper)

Common Tasks

Create a New Component

  1. Create component file in appropriate directory
  2. Define TypeScript interface for props
  3. Implement component logic
  4. Add proper styling with Tailwind CSS
  5. Export component

Add Component to Page

import { YourComponent } from "@/components/your-component";

export default function Page() {
  return (
    <div>
      <YourComponent prop1="value1" prop2="value2" />
    </div>
  );
}

Style with Tailwind CSS

export function StyledComponent({ className }: { className?: string }) {
  return (
    <div className={cn(
      "flex items-center justify-center",
      "bg-white border border-gray-200 rounded-lg",
      "p-4 shadow-sm hover:shadow-md transition-shadow",
      className
    )}>
      Content
    </div>
  );
}

Add Internationalization

import { useTranslations } from "next-intl";

export function InternationalizedComponent() {
  const t = useTranslations("component");
  
  return (
    <div>
      <h2>{t("title")}</h2>
      <p>{t("description")}</p>
    </div>
  );
}

Troubleshooting

Component not rendering

Problem: Component not showing on page

Solution:

  1. Check import path is correct
  2. Verify component is properly exported
  3. Check for TypeScript errors
  4. Ensure component is used correctly

Styling issues

Problem: Component not styled correctly

Solution:

  1. Check Tailwind CSS classes
  2. Verify cn utility is imported
  3. Check for CSS conflicts
  4. Test responsive design

TypeScript errors

Problem: Type errors in component

Solution:

  1. Check prop interface definition
  2. Verify all required props are provided
  3. Check for type mismatches
  4. Ensure proper imports

Next Steps