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 componentssrc/components/blocks/- Layout and page blockssrc/components/forms/- Form componentssrc/components/common/- Reusable common componentssrc/lib/utils.ts- Utility functions (cn helper)
Common Tasks
Create a New Component
- Create component file in appropriate directory
- Define TypeScript interface for props
- Implement component logic
- Add proper styling with Tailwind CSS
- 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:
- Check import path is correct
- Verify component is properly exported
- Check for TypeScript errors
- Ensure component is used correctly
Styling issues
Problem: Component not styled correctly
Solution:
- Check Tailwind CSS classes
- Verify
cnutility is imported - Check for CSS conflicts
- Test responsive design
TypeScript errors
Problem: Type errors in component
Solution:
- Check prop interface definition
- Verify all required props are provided
- Check for type mismatches
- Ensure proper imports
Next Steps
- New Page - Create pages that use components
- API Calls - Add API integration to components
- Internationalization - Multi-language component support