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
412 lines
14 KiB
TypeScript
412 lines
14 KiB
TypeScript
import { useState } from 'react';
|
|
import Link from 'next/link';
|
|
import Head from 'next/head';
|
|
import { useRouter } from 'next/router';
|
|
|
|
export default function RegisterPlant() {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
const [formData, setFormData] = useState({
|
|
id: `plant-${Date.now()}`,
|
|
commonName: '',
|
|
scientificName: '',
|
|
species: '',
|
|
genus: '',
|
|
family: '',
|
|
propagationType: 'original' as const,
|
|
plantedDate: new Date().toISOString().split('T')[0],
|
|
status: 'sprouted' as const,
|
|
latitude: '',
|
|
longitude: '',
|
|
address: '',
|
|
city: '',
|
|
country: '',
|
|
ownerName: '',
|
|
ownerEmail: '',
|
|
notes: '',
|
|
});
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
// Prepare plant data
|
|
const plantData = {
|
|
id: formData.id,
|
|
commonName: formData.commonName,
|
|
scientificName: formData.scientificName || undefined,
|
|
species: formData.species || undefined,
|
|
genus: formData.genus || undefined,
|
|
family: formData.family || undefined,
|
|
propagationType: formData.propagationType,
|
|
generation: 0,
|
|
plantedDate: formData.plantedDate,
|
|
status: formData.status,
|
|
location: {
|
|
latitude: parseFloat(formData.latitude),
|
|
longitude: parseFloat(formData.longitude),
|
|
address: formData.address || undefined,
|
|
city: formData.city || undefined,
|
|
country: formData.country || undefined,
|
|
},
|
|
owner: {
|
|
id: `user-${Date.now()}`,
|
|
name: formData.ownerName,
|
|
email: formData.ownerEmail,
|
|
},
|
|
childPlants: [],
|
|
notes: formData.notes || undefined,
|
|
registeredAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const response = await fetch('/api/plants/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(plantData),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to register 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');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
|
|
<Head>
|
|
<title>Register 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">
|
|
Register a New Plant
|
|
</h1>
|
|
<p className="text-gray-600 mb-8">
|
|
Add your plant to the blockchain and start tracking its lineage.
|
|
</p>
|
|
|
|
{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 registered successfully! Redirecting to plant page...
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Plant Information */}
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
|
Plant 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">
|
|
Common Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="commonName"
|
|
required
|
|
value={formData.commonName}
|
|
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"
|
|
placeholder="e.g., Tomato, Basil, Oak Tree"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Scientific Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="scientificName"
|
|
value={formData.scientificName}
|
|
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"
|
|
placeholder="e.g., Solanum lycopersicum"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Genus
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="genus"
|
|
value={formData.genus}
|
|
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">
|
|
Family
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="family"
|
|
value={formData.family}
|
|
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">
|
|
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 additional information about your plant..."
|
|
/>
|
|
</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 ? 'Registering...' : 'Register Plant'}
|
|
</button>
|
|
<Link href="/">
|
|
<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>
|
|
);
|
|
}
|