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
225 lines
5.7 KiB
TypeScript
225 lines
5.7 KiB
TypeScript
/**
|
|
* Plants.net API Integration Service
|
|
* Provides connectivity to the plants.net API for plant identification,
|
|
* data enrichment, and community features
|
|
*/
|
|
|
|
export interface PlantsNetSearchResult {
|
|
id: string;
|
|
commonName: string;
|
|
scientificName: string;
|
|
genus?: string;
|
|
family?: string;
|
|
imageUrl?: string;
|
|
description?: string;
|
|
careInstructions?: string;
|
|
}
|
|
|
|
export interface PlantsNetCommunity {
|
|
nearbyGrowers: {
|
|
userId: string;
|
|
username: string;
|
|
location: {
|
|
city?: string;
|
|
country?: string;
|
|
distance?: number;
|
|
};
|
|
plantsOwned: string[];
|
|
}[];
|
|
}
|
|
|
|
export class PlantsNetService {
|
|
private apiKey: string;
|
|
private baseUrl: string;
|
|
|
|
constructor(apiKey?: string) {
|
|
this.apiKey = apiKey || process.env.PLANTS_NET_API_KEY || '';
|
|
this.baseUrl = 'https://api.plants.net/v1';
|
|
}
|
|
|
|
/**
|
|
* Search for plant by common or scientific name
|
|
*/
|
|
async searchPlant(query: string): Promise<PlantsNetSearchResult[]> {
|
|
try {
|
|
const response = await fetch(
|
|
`${this.baseUrl}/search?q=${encodeURIComponent(query)}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
console.error('Plants.net API error:', response.statusText);
|
|
return [];
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.results || [];
|
|
} catch (error) {
|
|
console.error('Error searching plants.net:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get detailed plant information by ID
|
|
*/
|
|
async getPlantDetails(plantId: string): Promise<PlantsNetSearchResult | null> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/plants/${plantId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error('Plants.net API error:', response.statusText);
|
|
return null;
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Error fetching plant details:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find nearby growers who have similar plants
|
|
*/
|
|
async findNearbyGrowers(
|
|
plantSpecies: string,
|
|
latitude: number,
|
|
longitude: number,
|
|
radiusKm: number = 50
|
|
): Promise<PlantsNetCommunity> {
|
|
try {
|
|
const response = await fetch(
|
|
`${this.baseUrl}/community/nearby?species=${encodeURIComponent(plantSpecies)}&lat=${latitude}&lon=${longitude}&radius=${radiusKm}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
console.error('Plants.net API error:', response.statusText);
|
|
return { nearbyGrowers: [] };
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Error finding nearby growers:', error);
|
|
return { nearbyGrowers: [] };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Identify plant from image (if API supports it)
|
|
*/
|
|
async identifyPlantFromImage(imageUrl: string): Promise<PlantsNetSearchResult[]> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/identify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ imageUrl }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error('Plants.net API error:', response.statusText);
|
|
return [];
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.suggestions || [];
|
|
} catch (error) {
|
|
console.error('Error identifying plant:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get care instructions for a plant
|
|
*/
|
|
async getCareInstructions(scientificName: string): Promise<string | null> {
|
|
try {
|
|
const response = await fetch(
|
|
`${this.baseUrl}/care/${encodeURIComponent(scientificName)}`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
console.error('Plants.net API error:', response.statusText);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.careInstructions || null;
|
|
} catch (error) {
|
|
console.error('Error fetching care instructions:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report a plant to the plants.net network
|
|
* This allows integration with their global plant tracking
|
|
*/
|
|
async reportPlantToNetwork(plantData: {
|
|
commonName: string;
|
|
scientificName?: string;
|
|
location: { latitude: number; longitude: number };
|
|
ownerId: string;
|
|
propagationType?: string;
|
|
}): Promise<{ success: boolean; plantsNetId?: string }> {
|
|
try {
|
|
const response = await fetch(`${this.baseUrl}/reports`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(plantData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error('Plants.net API error:', response.statusText);
|
|
return { success: false };
|
|
}
|
|
|
|
const data = await response.json();
|
|
return {
|
|
success: true,
|
|
plantsNetId: data.id,
|
|
};
|
|
} catch (error) {
|
|
console.error('Error reporting plant to network:', error);
|
|
return { success: false };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let plantsNetService: PlantsNetService | null = null;
|
|
|
|
export function getPlantsNetService(): PlantsNetService {
|
|
if (!plantsNetService) {
|
|
plantsNetService = new PlantsNetService();
|
|
}
|
|
return plantsNetService;
|
|
}
|