localgreenchain/pages/plants/clone.tsx
Claude 1e14a700c7
Implement LocalGreenChain: Plant Cloning Blockchain System
This commit implements a complete blockchain-based plant tracking system
that preserves lineage across clones, seeds, and all plant offspring while
connecting growers through geographic proximity.

Features implemented:
- Custom blockchain with proof-of-work consensus
- Plant registration and cloning with lineage tracking
- Geographic discovery to find nearby plants and growers
- Integration with plants.net API for plant identification
- Comprehensive web UI for plant management
- RESTful API endpoints for all operations
- Network statistics and visualization

Core Components:
- lib/blockchain/: PlantBlock, PlantChain, and blockchain manager
- lib/services/: plants.net API and geolocation services
- pages/api/plants/: REST API endpoints for all operations
- pages/: Frontend UI pages for registration, exploration, and lineage

Technical Details:
- TypeScript for type safety
- Next.js for server-side rendering
- Tailwind CSS for responsive design
- JSON file-based blockchain storage
- Haversine distance calculations for geolocation
- OpenStreetMap integration for geocoding

This system enables large-scale adoption by:
- Making plant lineage tracking accessible to everyone
- Connecting local communities through plant sharing
- Providing immutable proof of plant provenance
- Supporting unlimited generations of plant propagation
- Scaling from individual growers to global networks

Documentation includes comprehensive README with:
- Quick start guide
- API reference
- Architecture details
- Scaling recommendations
- Use cases for various audiences
- Roadmap for future enhancements
2025-11-16 05:11:55 +00:00

430 lines
15 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Head from 'next/head';
export default function ClonePlant() {
const router = useRouter();
const { parentId } = router.query;
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [parentPlant, setParentPlant] = useState<any>(null);
const [formData, setFormData] = useState({
propagationType: 'clone' as 'seed' | 'clone' | 'cutting' | 'division' | 'grafting',
plantedDate: new Date().toISOString().split('T')[0],
status: 'sprouted' as const,
latitude: '',
longitude: '',
city: '',
country: '',
ownerName: '',
ownerEmail: '',
notes: '',
});
useEffect(() => {
if (parentId) {
fetchParentPlant();
}
}, [parentId]);
const fetchParentPlant = async () => {
try {
const response = await fetch(`/api/plants/${parentId}`);
const data = await response.json();
if (data.success) {
setParentPlant(data.plant);
}
} catch (error) {
console.error('Error fetching parent plant:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const newPlant = {
plantedDate: formData.plantedDate,
status: formData.status,
location: {
latitude: parseFloat(formData.latitude),
longitude: parseFloat(formData.longitude),
city: formData.city || undefined,
country: formData.country || undefined,
},
owner: {
id: `user-${Date.now()}`,
name: formData.ownerName,
email: formData.ownerEmail,
},
notes: formData.notes || undefined,
};
const response = await fetch('/api/plants/clone', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
parentPlantId: parentId,
propagationType: formData.propagationType,
newPlant,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to clone plant');
}
setSuccess(true);
setTimeout(() => {
router.push(`/plants/${data.plant.id}`);
}, 2000);
} catch (err: any) {
setError(err.message || 'An error occurred');
} finally {
setLoading(false);
}
};
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const getCurrentLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setFormData({
...formData,
latitude: position.coords.latitude.toString(),
longitude: position.coords.longitude.toString(),
});
},
(error) => {
setError('Unable to get your location: ' + error.message);
}
);
} else {
setError('Geolocation is not supported by your browser');
}
};
if (!parentId) {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center">
<div className="bg-white rounded-lg shadow-xl p-8 max-w-md">
<h2 className="text-2xl font-bold text-red-600 mb-4">Error</h2>
<p className="text-gray-700 mb-4">
No parent plant specified. Please select a plant to clone.
</p>
<Link href="/plants/explore">
<a className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
Browse Plants
</a>
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<Head>
<title>Clone Plant - LocalGreenChain</title>
</Head>
{/* Header */}
<header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<Link href="/">
<a className="text-2xl font-bold text-green-800">
🌱 LocalGreenChain
</a>
</Link>
<nav className="flex gap-4">
<Link href="/plants/explore">
<a className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">
Explore Network
</a>
</Link>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
<div className="bg-white rounded-lg shadow-xl p-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Clone Plant
</h1>
<p className="text-gray-600 mb-8">
Register a new offspring from an existing plant.
</p>
{/* Parent Plant Info */}
{parentPlant && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-8">
<h2 className="text-lg font-semibold text-green-900 mb-2">
Parent Plant
</h2>
<p className="text-green-800">
<strong>{parentPlant.commonName}</strong>
{parentPlant.scientificName && (
<span className="italic"> ({parentPlant.scientificName})</span>
)}
</p>
<p className="text-sm text-green-700 mt-1">
Generation {parentPlant.generation} Owned by{' '}
{parentPlant.owner.name}
</p>
</div>
)}
{error && (
<div className="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
{error}
</div>
)}
{success && (
<div className="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
Plant cloned successfully! Redirecting to plant page...
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Propagation Type */}
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Propagation Method
</h2>
<select
name="propagationType"
required
value={formData.propagationType}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="clone">Clone (exact genetic copy)</option>
<option value="seed">Seed</option>
<option value="cutting">Cutting</option>
<option value="division">Division</option>
<option value="grafting">Grafting</option>
</select>
<p className="mt-2 text-sm text-gray-600">
{formData.propagationType === 'clone' &&
'An exact genetic copy of the parent plant'}
{formData.propagationType === 'seed' &&
'Grown from seed (may have genetic variation)'}
{formData.propagationType === 'cutting' &&
'Propagated from a stem, leaf, or root cutting'}
{formData.propagationType === 'division' &&
'Separated from the parent plant'}
{formData.propagationType === 'grafting' &&
'Grafted onto another rootstock'}
</p>
</div>
{/* Plant Status */}
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Current Status
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Planted Date *
</label>
<input
type="date"
name="plantedDate"
required
value={formData.plantedDate}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status *
</label>
<select
name="status"
required
value={formData.status}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option value="sprouted">Sprouted</option>
<option value="growing">Growing</option>
<option value="mature">Mature</option>
<option value="flowering">Flowering</option>
<option value="fruiting">Fruiting</option>
<option value="dormant">Dormant</option>
</select>
</div>
</div>
</div>
{/* Location */}
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Location
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Latitude *
</label>
<input
type="number"
step="any"
name="latitude"
required
value={formData.latitude}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Longitude *
</label>
<input
type="number"
step="any"
name="longitude"
required
value={formData.longitude}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div className="md:col-span-2">
<button
type="button"
onClick={getCurrentLocation}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
📍 Use My Current Location
</button>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City
</label>
<input
type="text"
name="city"
value={formData.city}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country
</label>
<input
type="text"
name="country"
value={formData.country}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
</div>
{/* Owner Information */}
<div>
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Your Information
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Your Name *
</label>
<input
type="text"
name="ownerName"
required
value={formData.ownerName}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Your Email *
</label>
<input
type="email"
name="ownerEmail"
required
value={formData.ownerEmail}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
</div>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Notes (Optional)
</label>
<textarea
name="notes"
value={formData.notes}
onChange={handleChange}
rows={4}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="Add any notes about this plant or how you obtained it..."
/>
</div>
{/* Submit Button */}
<div className="flex gap-4">
<button
type="submit"
disabled={loading}
className="flex-1 px-6 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Cloning...' : 'Register Clone'}
</button>
<Link href={`/plants/${parentId}`}>
<a className="px-6 py-3 bg-gray-200 text-gray-700 font-semibold rounded-lg hover:bg-gray-300 transition">
Cancel
</a>
</Link>
</div>
</form>
</div>
</main>
</div>
);
}