Component Development Guide
Modular Sample Card System
The sample detail page (src/app/(nest)/sample/[id]/page.js
) uses a modular card system that allows developers to add new card components without modifying the main page file. Both universal cards and type specific cards can be developed. This guide shows you exactly how to create and integrate new cards.
Quick Start
- Copy the template:
src/components/sample-cards/TemplateCard.js
- Rename and customize your card
- Register in
src/components/sample-cards/registry.js
- Done! Your card automatically appears
Detailed Guide
1. Card Component Structure
All sample cards follow this pattern:
javascript
export function MyCard({ sample, handleChange, setSample }) {
// Guard clause (required)
if (!sample || typeof handleChange !== 'function') {
return <Skeleton className="h-[200px] w-full" />;
}
return (
<Card>
<CardHeader>
<CardTitle>My Card Title</CardTitle>
</CardHeader>
<CardContent>
{/* Your card content */}
</CardContent>
</Card>
);
}
// Metadata (required)
MyCard.displayName = 'MyCard';
MyCard.supportedTypes = ['plant', 'animal'];
MyCard.position = 'main'; // or 'sidebar'
2. Creating a New Card
Step 1: Copy the template
bash
cp src/components/sample-cards/TemplateCard.js src/components/sample-cards/MyNewCard.js
Step 2: Update the component
javascript
// In MyNewCard.js
export function MyNewCard({ sample, handleChange }) {
if (!sample || typeof handleChange !== 'function') {
return <Skeleton className="h-[200px] w-full" />;
}
return (
<Card>
<CardHeader>
<CardTitle>My Feature</CardTitle>
</CardHeader>
<CardContent>
<Label htmlFor="myField">My Field</Label>
<Input
id="myField"
value={sample.myField || ""}
onChange={(e) => handleChange("myField", e.target.value)}
/>
</CardContent>
</Card>
);
}
MyNewCard.displayName = 'MyNewCard';
MyNewCard.supportedTypes = ['my-sample-type'];
MyNewCard.position = 'main';
Step 3: Register your card
javascript
// In src/components/sample-cards/registry.js
import { MyNewCard } from './MyNewCard';
export const SAMPLE_CARDS = {
// ...existing cards...
'my-sample-type': [MyNewCard],
};
That's it! Your card will automatically appear for samples of type my-sample-type
.
Card Features
Props Available to Cards
sample
- Current sample data (required)handleChange
- Function to update sample fields (required)setSample
- Update sample state locallysamplesData
- All samples (for relationships)usersData
- All users (for dropdowns)handleStatusIncrementSample
- Increment counters
Card Positioning
javascript
MyCard.position = 'main'; // Left column (main content)
MyCard.position = 'sidebar'; // Right column
MyCard.position = 'full'; // Full width
Conditional Rendering
javascript
MyCard.shouldRender = (sample) => {
return sample.lifestatus === 'alive' && sample.type === 'animal';
};
Card Dependencies
javascript
MyCard.dependencies = ['usersData', 'handleStatusIncrementSample'];
Examples
Simple Input Card
javascript
export function SimpleCard({ sample, handleChange }) {
if (!sample || typeof handleChange !== 'function') {
return <Skeleton className="h-[200px] w-full" />;
}
return (
<Card>
<CardHeader>
<CardTitle>Simple Field</CardTitle>
</CardHeader>
<CardContent>
<Label htmlFor="simpleField">Field Name</Label>
<Input
id="simpleField"
value={sample.simpleField || ""}
onChange={(e) => handleChange("simpleField", e.target.value)}
/>
</CardContent>
</Card>
);
}
SimpleCard.displayName = 'SimpleCard';
SimpleCard.supportedTypes = ['any-type'];
SimpleCard.position = 'main';
Action Card with Button
javascript
export function ActionCard({ sample, handleStatusIncrementSample, setSample }) {
if (!sample) return <Skeleton className="h-[200px] w-full" />;
const handleAction = () => {
if (handleStatusIncrementSample) {
handleStatusIncrementSample(sample._id, "actionCount");
}
if (setSample) {
setSample(prev => ({
...prev,
actionCount: (prev.actionCount || 0) + 1
}));
}
};
return (
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent>
<p>Action count: {sample.actionCount || 0}</p>
</CardContent>
<CardFooter>
<Button onClick={handleAction}>Perform Action</Button>
</CardFooter>
</Card>
);
}
ActionCard.displayName = 'ActionCard';
ActionCard.supportedTypes = ['animal'];
ActionCard.position = 'sidebar';
ActionCard.dependencies = ['handleStatusIncrementSample', 'setSample'];
File Structure
src/components/sample-cards/
├── registry.js # Card registration
├── cardBase.js # Utilities and interfaces
├── TemplateCard.js # Copy this for new cards
├── PlantCard.js # Example extracted card
├── SoilCard.js
├── AnimalCard.js
└── YourNewCard.js # Your custom cards
Best Practices
- Always include guard clauses for prop validation
- Use Skeleton for loading states
- Follow naming conventions (PascalCase + 'Card' suffix)
- Test with different sample types
- Handle missing data gracefully
- Use semantic HTML and proper labels
Migration from Existing Code
To convert existing inline cards to the modular system:
- Extract the card JSX into a separate component file
- Add metadata (displayName, supportedTypes, position)
- Add to registry
- Remove from main page file
The system handles the rest automatically!