Merge pull request #1 from vespo92/claude/plant-cloning-blockchain-0183C7S2LExNj47z9yiV98NR

Blockchain system for plant cloning
This commit is contained in:
Vinnie Esposito 2025-11-16 18:32:55 -06:00 committed by GitHub
commit 24569757f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 8406 additions and 12 deletions

View file

@ -1,3 +1,25 @@
# LocalGreenChain Environment Variables
# Plants.net API (optional)
PLANTS_NET_API_KEY=your_api_key_here
# Tor Configuration
TOR_ENABLED=false
TOR_SOCKS_HOST=127.0.0.1
TOR_SOCKS_PORT=9050
TOR_CONTROL_PORT=9051
TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/localgreenchain
# Privacy Settings
DEFAULT_PRIVACY_MODE=standard
ALLOW_ANONYMOUS_REGISTRATION=true
LOCATION_OBFUSCATION_DEFAULT=fuzzy
# Application Settings
NODE_ENV=development
PORT=3001
# Legacy Drupal Settings (for backward compatibility)
NEXT_PUBLIC_DRUPAL_BASE_URL=http://localhost:8080 NEXT_PUBLIC_DRUPAL_BASE_URL=http://localhost:8080
NEXT_IMAGE_DOMAIN=localhost NEXT_IMAGE_DOMAIN=localhost
DRUPAL_CLIENT_ID=52ce1a10-bf5c-4c81-8edf-eea3af95da84 DRUPAL_CLIENT_ID=52ce1a10-bf5c-4c81-8edf-eea3af95da84

1
.gitignore vendored
View file

@ -25,6 +25,7 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
bun-debug.log*
# local env files # local env files
.env.local .env.local

40
Dockerfile Normal file
View file

@ -0,0 +1,40 @@
# Dockerfile for LocalGreenChain
# Uses Bun for fast builds and runtime
FROM oven/bun:1 as base
WORKDIR /app
# Install dependencies
COPY package.json bun.lockb* ./
RUN bun install --frozen-lockfile
# Copy application code
COPY . .
# Build Next.js application
RUN bun run build
# Production stage
FROM oven/bun:1-slim as production
WORKDIR /app
# Copy dependencies and build output
COPY --from=base /app/node_modules ./node_modules
COPY --from=base /app/.next ./.next
COPY --from=base /app/public ./public
COPY --from=base /app/package.json ./package.json
COPY --from=base /app/next.config.js ./next.config.js
# Create data directory
RUN mkdir -p /app/data
# Expose port
EXPOSE 3001
# Set environment to production
ENV NODE_ENV=production
# Run the application
CMD ["bun", "run", "start"]

437
ENVIRONMENTAL_TRACKING.md Normal file
View file

@ -0,0 +1,437 @@
# 🌍 Environmental Tracking Guide
**Track soil, climate, nutrients, and growing conditions to optimize plant health and learn from successful growers.**
## Overview
LocalGreenChain's environmental tracking system allows you to record detailed information about where and how your plants are grown. This data helps you:
- **Understand** what conditions make plants thrive
- **Compare** your setup with successful growers
- **Optimize** growing conditions based on data
- **Learn** from the collective experience
- **Replicate** successful growing environments
- **Troubleshoot** plant health issues
## What Can You Track?
### 🌱 Soil Composition
- **Soil type**: Clay, sand, silt, loam, peat, chalk, or custom
- **pH level**: 0-14 scale (most plants prefer 6.0-7.5)
- **Texture**: Heavy, medium, or light
- **Drainage**: Poor, moderate, good, or excellent
- **Organic matter**: Percentage (0-100%)
- **Composition**: Clay/sand/silt percentages
- **Amendments**: Compost, manure, perlite, vermiculite, etc.
**Why it matters**: Soil is the foundation of plant health. pH affects nutrient availability, drainage prevents root rot, and organic matter improves structure.
### 🧪 Nutrients & Fertilization
- **NPK values**: Nitrogen, Phosphorus, Potassium percentages
- **Secondary nutrients**: Calcium, magnesium, sulfur
- **Micronutrients**: Iron, manganese, zinc, copper, boron, molybdenum
- **EC/TDS**: Electrical conductivity and total dissolved solids
- **Fertilizer schedule**: Type, amount, frequency, NPK ratios
- **Application history**: Track what you've added and when
**Why it matters**: Proper nutrition is crucial for growth. Too little causes deficiencies, too much can burn plants. Track what works!
### ☀️ Lighting Conditions
- **Light type**: Natural, artificial, or mixed
- **Natural light**:
- Exposure: Full sun, partial sun, partial shade, full shade
- Hours per day: 0-24
- Direction: North, south, east, west
- Quality: Direct, filtered, dappled, indirect
- **Artificial light**:
- Type: LED, fluorescent, HPS, MH, etc.
- Spectrum: Full spectrum, color temperature (K)
- Wattage and distance from plant
- Hours per day
- **Advanced metrics**: PPFD (μmol/m²/s), DLI (mol/m²/day)
**Why it matters**: Light is energy. Insufficient light causes leggy growth, too much can burn leaves. Track optimal levels for your species.
### 🌡️ Climate Conditions
- **Temperature**: Day/night temps, min/max ranges (Celsius)
- **Humidity**: Average, min, max percentages
- **Air circulation**: None, minimal, moderate, strong
- **Ventilation**: Poor, adequate, good, excellent
- **CO2 levels**: PPM (ambient ~400, enhanced ~1200-1500)
- **Season**: Spring, summer, fall, winter
- **Hardiness zone**: USDA zones (e.g., "9b", "10a")
**Why it matters**: Plants have optimal temperature and humidity ranges. Extremes cause stress or death. Good airflow prevents disease.
### 📍 Growing Location
- **Location type**: Indoor, outdoor, greenhouse, polytunnel, shade house, window, balcony
- **Indoor details**: Room type (bedroom, basement, grow tent)
- **Outdoor details**:
- Exposure to elements: Protected, semi-protected, exposed
- Elevation: Meters above sea level
- Slope: Flat, gentle, moderate, steep
- Aspect: Slope direction (affects sun exposure)
**Why it matters**: Location affects everything - temperature, humidity, light, pests. Indoor vs outdoor requires different care.
### 🪴 Container Information
- **Type**: Pot, raised bed, ground, hydroponic, aeroponic, fabric pot, etc.
- **Material**: Plastic, terracotta, ceramic, fabric, wood, metal
- **Size**: Gallons, diameter, or dimensions
- **Volume**: Liters
- **Depth**: Centimeters
- **Drainage**: Yes/no, number of holes
**Why it matters**: Container size affects root development and watering frequency. Drainage is critical - no drainage = dead plant.
### 💧 Watering Schedule
- **Method**: Hand water, drip, soaker hose, sprinkler, self-watering, hydroponic
- **Frequency**: Daily, every 2-3 days, weekly, etc.
- **Amount**: Liters, "until runoff", inches
- **Water source**: Tap, well, rain, filtered, distilled, RO
- **Water quality**:
- pH: Affects nutrient availability
- TDS: Total dissolved solids (PPM)
- Chlorine: Yes, no, or filtered
**Why it matters**: Over/underwatering kills more plants than anything. Water quality affects long-term soil health.
### 🌿 Surrounding Environment
- **Companion plants**: Other species growing nearby
- **Nearby features**: Trees, structures, walls, fences
- **Ground cover**: Mulch, grass, bare soil, gravel
- **Wildlife**:
- Pollinators: Bees, butterflies, hummingbirds
- Beneficial insects: Ladybugs, lacewings
- Pests: Name, severity, treatment, date observed
- Diseases: Name, symptoms, severity, treatment
- **Microclimate factors**:
- Wind exposure: Sheltered, moderate, exposed, windy
- Frost pockets: Yes/no
- Heat traps: Yes/no
- **Ecosystem type**: Urban, suburban, rural, forest, desert, coastal, mountain, tropical
**Why it matters**: Companion planting improves growth. Pest/disease tracking helps prevention. Microclimates explain unexpected results.
### 📈 Growth Metrics
- **Measurements**: Height, width, leaf count, flower count, fruit count
- **Health score**: 0-100 rating
- **Vigor**: Poor, fair, good, excellent
- **Tracking**: Photos and notes over time
**Why it matters**: Measure growth to evaluate if conditions are optimal. Data drives improvement.
## API Endpoints
### Get Recommendations
```http
GET /api/environment/recommendations?plantId=xyz
```
Returns personalized recommendations based on your plant's environment:
- **Critical issues**: Must fix immediately (no drainage, extreme temps)
- **High priority**: Important for health (pH problems, insufficient light)
- **Medium priority**: Optimization opportunities (humidity, amendments)
- **Low priority**: Minor improvements (water quality tweaks)
- **Environmental health score**: 0-100 overall rating
**Example response**:
```json
{
"environmentalHealth": 75,
"recommendations": [
{
"category": "soil",
"priority": "high",
"issue": "Low soil pH (5.2)",
"recommendation": "Add lime to raise pH. Most plants prefer 6.0-7.0",
"impact": "Acidic soil can lock out nutrients and harm roots"
},
{
"category": "climate",
"priority": "medium",
"issue": "Low humidity (25%)",
"recommendation": "Mist plants, use humidity tray, or group plants together",
"impact": "Low humidity can cause leaf tip browning and stress"
}
]
}
```
### Find Similar Environments
```http
GET /api/environment/similar?plantId=xyz&minScore=70
```
Finds other plants growing in similar conditions:
- Compare your environment with successful growers
- Learn from plants that thrive in similar setups
- Get ideas for what works in your conditions
**Similarity factors**:
- Location type (indoor/outdoor/greenhouse)
- Soil type and pH
- Temperature and humidity ranges
- Lighting conditions
- Water source and quality
- Container type
**Scores**:
- **90-100**: Nearly identical conditions
- **75-89**: Very similar - expect similar results
- **60-74**: Moderately similar
- **40-59**: Some similarities
- **0-39**: Very different
### Compare Two Plants
```http
GET /api/environment/compare?plant1=xyz&plant2=abc
```
Direct comparison of two specific plants:
- See exactly what's similar and different
- Learn why one plant might be thriving while another struggles
- Identify which factors to adjust
### Growth Correlation Analysis
```http
GET /api/environment/analysis?species=tomato
```
Network-wide analysis to identify optimal conditions:
- What pH do successful plants average?
- What temperature range works best?
- Which lighting type has best results?
- Success rates by location type, soil type, etc.
**Filter by species** to get species-specific insights!
## How Recommendations Work
The system analyzes your environment and generates priority-based recommendations:
### Soil Recommendations
- **pH < 5.5**: Add lime (priority: high)
- **pH > 7.5**: Add sulfur (priority: high)
- **Organic matter < 3%**: Add compost (priority: medium)
- **Poor drainage**: Add perlite/sand or raise beds (priority: high)
### Climate Recommendations
- **Temp > 35°C**: Provide shade, increase watering (priority: high)
- **Temp < 10°C**: Frost protection needed (priority: high)
- **Humidity < 30%**: Mist, use humidity trays (priority: medium)
- **Humidity > 80%**: Improve airflow (priority: medium)
### Light Recommendations
- **< 4 hours/day**: Move to brighter spot or add grow lights (priority: high)
- **Natural light direction**: Optimize plant placement
### Nutrient Recommendations
- **Low NPK**: Apply balanced fertilizer (priority: medium)
- **High EC/TDS**: Reduce feeding or flush (priority: medium)
### Water Recommendations
- **pH > 7.5**: Consider pH adjustment or rainwater (priority: low)
- **Chlorinated**: Let sit 24h or filter (priority: low)
### Container Recommendations
- **No drainage**: Add holes IMMEDIATELY (priority: CRITICAL)
- **Too small**: Repot to larger container (priority: medium)
## Environmental Health Scoring
The system calculates a 0-100 health score based on:
**Perfect score (100)**: Everything optimal
- Soil pH 6.0-7.0
- Good drainage
- Organic matter > 3%
- Temps 15-30°C
- Humidity 40-70%
- Adequate light (6+ hours or proper artificial)
- Proper container with drainage
- Good water quality
**Deductions**:
- **-15 points**: No container drainage (CRITICAL)
- **-15 points**: Insufficient light (< 4 hours)
- **-10 points**: Extreme pH (< 5.5 or > 7.5)
- **-10 points**: Extreme temperature (< 10°C or > 35°C)
- **-10 points**: Poor soil drainage
- **-5 points**: Low organic matter
- **-5 points**: Poor humidity (< 30% or > 80%)
- **-5 points**: No airflow
- **-5 points**: Chlorinated water
- **-5 points**: High water pH
**Score interpretation**:
- **90-100**: Excellent conditions
- **75-89**: Good conditions, minor improvements possible
- **60-74**: Adequate, several opportunities for optimization
- **40-59**: Suboptimal, multiple issues to address
- **0-39**: Poor conditions, plant likely struggling
## Best Practices
### When to Track Environmental Data
**Always track**:
- When registering a new plant
- After making changes (repotting, moving, fertilizing)
- When troubleshooting problems
- Before/after cloning (compare parent and clone environments)
**Update regularly**:
- Monthly for stable setups
- Weekly during active growth
- Daily when troubleshooting or experimenting
### What to Measure
**Essentials** (track these at minimum):
- Soil type and pH
- Location type (indoor/outdoor)
- Light exposure (hours/day)
- Temperature range
- Watering frequency
**Recommended** (for better insights):
- Soil drainage and amendments
- Humidity
- Container details
- Nutrient applications
- Growth measurements
**Advanced** (for serious growers):
- Full NPK and micronutrients
- EC/TDS measurements
- PPFD/DLI for lighting
- CO2 levels
- Pest/disease tracking
- Companion plants
### Tips for Accurate Tracking
1. **Measure, don't guess**: Use pH meters, thermometers, soil probes
2. **Track over time**: Conditions change - update seasonally
3. **Note anomalies**: Heatwaves, cold snaps, unusual events
4. **Photo documentation**: Pictures reveal patterns you might miss
5. **Consistent timing**: Measure at the same time of day
6. **Calibrate instruments**: Test meters regularly
7. **Record failures too**: Learning what doesn't work is valuable
## Use Cases
### For Home Growers
"My tomato plant is struggling. Let me check recommendations..."
- System identifies low soil pH and insufficient light
- Suggests adding lime and moving to sunnier spot
- Compares with successful tomato growers nearby
- Shows that similar plants thrive with 8+ hours sun and pH 6.5
### For Propagation
"I want to clone my best basil plant..."
- Records current environment that's producing healthy growth
- When giving clone to friend, shares environmental data
- Friend replicates conditions for best success
- Tracks if clone thrives in new environment
### For Troubleshooting
"Why are my pepper leaves yellowing?"
- Checks pH: 7.8 (too high)
- System recommends sulfur to lower pH
- Compares with healthy pepper plants: pH 6.0-6.5
- Adds sulfur, yellowing stops within 2 weeks
### For Learning
"What conditions work best for lavender?"
- Searches network for successful lavender plants
- Finds they all have: pH 6.5-7.5, full sun, excellent drainage
- Notices growers with poor drainage have struggling plants
- Adjusts setup before planting
### For Research
"Does companion planting really work?"
- Analyzes all plants with tomato-basil companions
- Compares health scores with lone tomatoes
- Statistical analysis shows 15% better vigor
- Data supports traditional growing wisdom
## Privacy Considerations
Environmental data can be:
- **Shared openly**: Help others learn from your setup
- **Kept private**: Track for yourself without broadcasting
- **Anonymous**: Share conditions without revealing location/identity
Use privacy settings to control:
- Whether environment data is visible to others
- Whether it appears in similarity searches
- Whether it's included in analysis
- Location obfuscation (see TOR_SETUP.md)
## Advanced Features
### Automated Recommendations
As you add more plants:
- System learns what works in YOUR specific conditions
- Personalized recommendations based on your history
- "You've had success with these pH levels..."
### Seasonal Tracking
- Record how conditions change through the year
- Plan planting schedules based on historical data
- Anticipate challenges (summer heat, winter cold)
### Clone Comparison
- Automatically compare clone environments with parent
- See if environmental differences affect growth
- Optimize clone success rates
### Community Insights
- "Plants in your area typically use X soil type"
- "Growers in similar climates report best results with Y"
- "This species thrives in conditions like yours"
## Future Enhancements
Coming soon:
- **Photo-based tracking**: Upload photos, AI analyzes plant health
- **Sensor integration**: Automatic data from soil sensors, weather stations
- **Mobile app**: Track on-the-go with smartphone
- **Notifications**: "Time to water", "pH out of range"
- **Marketplace integration**: Buy recommended products
- **Expert consultations**: Connect with master growers
## Troubleshooting
**"My recommendations don't make sense"**
- Check that measurements are accurate
- Ensure units are correct (Celsius not Fahrenheit)
- Verify pH probe is calibrated
- Update environmental data if conditions changed
**"No similar plants found"**
- Lower the minimum similarity score
- Your setup might be unique!
- Add more environmental details for better matching
- Check that other plants have environmental data
**"Analysis shows low success rate"**
- System is helping you identify what to improve
- Follow recommendations to optimize
- Learn from successful growers
- Don't be discouraged - this is how you learn!
## Getting Help
- **Issues**: GitHub Issues for bugs
- **Questions**: Community forum for growing advice
- **Contributions**: PRs welcome for new features
- **Data privacy**: See PRIVACY.md
---
**Remember**: The goal is learning and improvement, not perfection. Every data point helps you become a better grower!
Happy tracking! 🌱📊

413
README.md
View file

@ -1,11 +1,412 @@
# example-marketing # 🌱 LocalGreenChain
An example marketing site built using Drupal + JSON:API. **A blockchain for plants that preserves lineage across clones and seeds, connecting growers worldwide.**
Pages are built from the Landing page node type and paragraphs sourced from `/drupal`. LocalGreenChain is a revolutionary plant tracking system that uses blockchain technology to create an immutable record of plant lineage. Track every clone, cutting, seed, and offspring throughout multiple generations while connecting with nearby growers who share your passion for plants.
See https://demo.next-drupal.org ## ✨ Features
## License ### 🌳 **Lineage Tracking**
- Track every plant from its origin through unlimited generations
- Record propagation methods (clone, seed, cutting, division, grafting)
- Build comprehensive family trees showing ancestors, descendants, and siblings
- Immutable blockchain ensures lineage data can never be lost or altered
Licensed under the [MIT license](https://github.com/chapter-three/next-drupal/blob/master/LICENSE). ### 📍 **Geographic Connections**
- Find plants and growers near your location
- Discover plant clusters and hotspots in your area
- Connect with people who have plants from the same lineage
- Build local plant-sharing communities
### 🔗 **Blockchain Security**
- Proof-of-work consensus ensures data integrity
- Each plant registration is a permanent block in the chain
- Cryptographic hashing prevents tampering
- Distributed architecture for reliability
### 🌍 **Global Network**
- Integration with plants.net API for plant identification
- Track how your plants spread across the world
- See global distribution statistics by species and location
- Join a worldwide community of plant enthusiasts
### 🎨 **User-Friendly Interface**
- Beautiful, intuitive web interface
- Easy plant registration with geolocation support
- Visual lineage explorer
- Network statistics dashboard
- Mobile-responsive design
### 🔒 **Privacy & Anonymity (Tor Integration)**
- Anonymous plant registration with zero personal information
- Location obfuscation (fuzzy, city, country, or hidden)
- Tor hidden service support for .onion access
- SOCKS proxy integration for outbound Tor connections
- Privacy-preserving geolocation (share area, not exact location)
- Pseudonymous identities and wallet addresses
- End-to-end encrypted communications
### 🌍 **Environmental Tracking**
- Comprehensive soil composition tracking (type, pH, drainage, amendments)
- Complete nutrient monitoring (NPK, micronutrients, EC/TDS)
- Climate conditions (temperature, humidity, airflow, CO2)
- Lighting analysis (natural/artificial, hours, spectrum, PPFD/DLI)
- Watering schedules and water quality monitoring
- Container and growing location details
- Pest, disease, and companion plant tracking
- Growth metrics and health scoring
- Environmental comparison between plants
- Personalized growing recommendations
- Success correlation analysis across network
**📘 Full Guide**: See [ENVIRONMENTAL_TRACKING.md](./ENVIRONMENTAL_TRACKING.md)
## 🚀 Quick Start
### Prerequisites
- **Bun 1.0+** (https://bun.sh) - A fast JavaScript runtime and package manager
- Basic knowledge of plant propagation (optional but helpful!)
### Why Bun?
LocalGreenChain uses Bun for faster installation, builds, and runtime performance:
- **3x faster** package installation compared to npm
- **Native TypeScript support** without configuration
- **Built-in bundler** for optimal production builds
- **Compatible** with npm packages
### Installing Bun
If you don't have Bun installed:
```bash
# macOS/Linux/WSL
curl -fsSL https://bun.sh/install | bash
# Or using npm
npm install -g bun
```
### Installation
1. **Clone the repository**
```bash
git clone https://github.com/yourusername/localgreenchain.git
cd localgreenchain
```
2. **Install dependencies**
```bash
bun install
```
3. **Set up environment variables** (optional)
```bash
cp .env.example .env
```
Edit `.env` and add your plants.net API key if you have one:
```
PLANTS_NET_API_KEY=your_api_key_here
```
4. **Run the development server**
```bash
bun run dev
```
5. **Open your browser**
Navigate to [http://localhost:3001](http://localhost:3001)
### 🧅 Tor Setup (Optional - For Privacy)
For anonymous plant sharing and privacy protection:
```bash
# Quick start with Docker Compose
docker-compose -f docker-compose.tor.yml up -d
# Or manual setup
# 1. Install Tor
sudo apt install tor
# 2. Configure Tor (copy example config)
sudo cp tor/torrc.example /etc/tor/torrc
# 3. Start Tor
sudo systemctl start tor
# 4. Enable Tor in LocalGreenChain
# Edit .env and set: TOR_ENABLED=true
# 5. Start LocalGreenChain
bun run start
# 6. Get your .onion address
sudo cat /var/lib/tor/localgreenchain/hostname
```
**📘 Full Guide**: See [TOR_SETUP.md](./TOR_SETUP.md) for complete instructions
## 📖 How It Works
### The Blockchain
LocalGreenChain uses a custom blockchain implementation where:
- **Each block** represents a plant registration or update
- **Proof-of-work** mining ensures data integrity
- **Cryptographic hashing** links blocks together
- **Genesis block** initializes the chain
### Plant Lineage System
```
Original Plant (Generation 0)
├── Clone 1 (Generation 1)
│ ├── Seed 1-1 (Generation 2)
│ └── Cutting 1-2 (Generation 2)
├── Clone 2 (Generation 1)
└── Seed 3 (Generation 1)
└── Clone 3-1 (Generation 2)
```
Every plant maintains:
- **Parent reference**: Which plant it came from
- **Children array**: All offspring created from it
- **Generation number**: How many steps from the original
- **Propagation type**: How it was created
### Geographic Discovery
The system calculates distances between plants using the Haversine formula to:
- Find plants within a specified radius
- Cluster nearby plants to show hotspots
- Suggest connections based on proximity and species
## 🎯 Use Cases
### For Home Gardeners
- Track your plant collection and propagation success
- Share clones with friends while maintaining the lineage record
- Find local gardeners with similar plants for trading
### For Plant Nurseries
- Provide customers with verified lineage information
- Track all plants sold and their offspring
- Build trust through transparent provenance
### For Conservation Projects
- Document rare plant propagation efforts
- Track genetic diversity across populations
- Coordinate distribution of endangered species
### For Community Gardens
- Manage shared plant collections
- Track plant donations and their spread
- Build local food security networks
### For Research
- Study plant propagation success rates
- Track genetic drift across generations
- Analyze geographic distribution patterns
## 🛠️ API Reference
### Register a Plant
```http
POST /api/plants/register
Content-Type: application/json
{
"id": "plant-unique-id",
"commonName": "Tomato",
"scientificName": "Solanum lycopersicum",
"location": {
"latitude": 40.7128,
"longitude": -74.0060,
"city": "New York",
"country": "USA"
},
"owner": {
"id": "user-id",
"name": "John Doe",
"email": "john@example.com"
}
}
```
### Clone a Plant
```http
POST /api/plants/clone
Content-Type: application/json
{
"parentPlantId": "parent-plant-id",
"propagationType": "clone",
"newPlant": {
"location": {...},
"owner": {...}
}
}
```
### Find Nearby Plants
```http
GET /api/plants/nearby?lat=40.7128&lon=-74.0060&radius=50
```
### Get Plant Lineage
```http
GET /api/plants/lineage/plant-id
```
### Search Plants
```http
GET /api/plants/search?q=tomato&type=species
```
### Get Network Statistics
```http
GET /api/plants/network
```
## 🏗️ Architecture
### Tech Stack
- **Runtime**: Bun (fast JavaScript runtime and package manager)
- **Frontend**: Next.js, React, TypeScript, Tailwind CSS
- **Blockchain**: Custom implementation with proof-of-work
- **Storage**: JSON file-based (production can use database)
- **APIs**: RESTful endpoints
- **Geolocation**: Haversine distance calculation, OpenStreetMap geocoding
### Project Structure
```
localgreenchain/
├── lib/
│ ├── blockchain/
│ │ ├── types.ts # Type definitions
│ │ ├── PlantBlock.ts # Block implementation
│ │ ├── PlantChain.ts # Blockchain logic
│ │ └── manager.ts # Blockchain singleton
│ └── services/
│ ├── plantsnet.ts # Plants.net API integration
│ └── geolocation.ts # Location services
├── pages/
│ ├── index.tsx # Home page
│ ├── plants/
│ │ ├── register.tsx # Register new plant
│ │ ├── clone.tsx # Clone existing plant
│ │ ├── explore.tsx # Browse network
│ │ └── [id].tsx # Plant details
│ └── api/
│ └── plants/ # API endpoints
└── data/
└── plantchain.json # Blockchain storage
```
## 🌐 Scaling for Mass Adoption
### Current Implementation
- **Single-server blockchain**: Good for 10,000s of plants
- **File-based storage**: Simple and portable
- **Client-side rendering**: Fast initial setup
### Scaling Recommendations
#### For 100,000+ Plants
1. **Switch to database**: PostgreSQL or MongoDB
2. **Add caching**: Redis for frequently accessed data
3. **Optimize mining**: Reduce difficulty or use alternative consensus
#### For 1,000,000+ Plants
1. **Distributed blockchain**: Multiple nodes with consensus
2. **Sharding**: Geographic or species-based partitioning
3. **CDN**: Serve static content globally
4. **API rate limiting**: Protect against abuse
#### For Global Scale
1. **Blockchain network**: Peer-to-peer node system
2. **Mobile apps**: Native iOS/Android applications
3. **Offline support**: Sync when connection available
4. **Federation**: Regional chains with cross-chain references
## 🤝 Contributing
We welcome contributions! Here's how you can help:
1. **Add features**: New propagation types, plant care tracking, etc.
2. **Improve UI**: Design enhancements, accessibility
3. **Optimize blockchain**: Better consensus algorithms
4. **Mobile app**: React Native implementation
5. **Documentation**: Tutorials, translations
### Development Process
```bash
# Create a feature branch
git checkout -b feature/amazing-feature
# Make your changes
# ...
# Run tests (when available)
bun test
# Commit with clear message
git commit -m "Add amazing feature"
# Push and create pull request
git push origin feature/amazing-feature
```
## 📜 License
MIT License - see [LICENSE](LICENSE) file for details
## 🙏 Acknowledgments
- Inspired by the global plant-sharing community
- Built with Next.js and React
- Blockchain concept adapted for plant tracking
- Geocoding powered by OpenStreetMap Nominatim
## 📞 Support
- **Issues**: [GitHub Issues](https://github.com/yourusername/localgreenchain/issues)
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/localgreenchain/discussions)
- **Email**: support@localgreenchain.org
## 🗺️ Roadmap
### Phase 1: Core Features ✅
- [x] Blockchain implementation
- [x] Plant registration and cloning
- [x] Lineage tracking
- [x] Geographic search
- [x] Web interface
### Phase 2: Enhanced Features (Q2 2025)
- [ ] User authentication
- [ ] Plant care reminders
- [ ] Photo uploads
- [ ] QR code plant tags
- [ ] Mobile app (React Native)
### Phase 3: Community Features (Q3 2025)
- [ ] Trading marketplace
- [ ] Events and meetups
- [ ] Expert advice forum
- [ ] Plant care wiki
- [ ] Achievements and badges
### Phase 4: Advanced Features (Q4 2025)
- [ ] DNA/genetic tracking integration
- [ ] AI plant identification
- [ ] Environmental impact tracking
- [ ] Carbon sequestration calculations
- [ ] Partnership with botanical gardens
---
**Built with 💚 for the planet**
Start your plant lineage journey today! 🌱

455
TOR_SETUP.md Normal file
View file

@ -0,0 +1,455 @@
# 🧅 Tor Integration Guide for LocalGreenChain
This guide explains how to set up LocalGreenChain with Tor for maximum privacy and anonymity when sharing plant lineages.
## Why Use Tor with LocalGreenChain?
### Privacy Benefits
- **Anonymous Plant Registration**: Register plants without revealing your identity
- **Location Privacy**: Share general area without exposing exact home address
- **IP Protection**: Hide your IP address from other users and the network
- **Censorship Resistance**: Access the network even in restrictive environments
- **Secure Sharing**: Share plant clones with trusted community members anonymously
### Use Cases
- **Privacy-Conscious Growers**: Don't want to advertise exact plant locations
- **Sensitive Species**: Medicinal plants, rare species, or regulated botanicals
- **Community Building**: Connect with local growers without revealing identity
- **Research**: Anonymous data collection for botanical research
- **Security**: Protect against unwanted visitors or theft
## Table of Contents
1. [Quick Start](#quick-start)
2. [Installation Methods](#installation-methods)
3. [Configuration](#configuration)
4. [Running as Hidden Service](#running-as-hidden-service)
5. [Using Tor Browser](#using-tor-browser)
6. [Privacy Best Practices](#privacy-best-practices)
7. [Troubleshooting](#troubleshooting)
---
## Quick Start
### Option 1: Docker Compose (Recommended)
The easiest way to run LocalGreenChain with Tor:
```bash
# Copy environment variables
cp .env.example .env
# Edit .env and enable Tor
nano .env
# Set: TOR_ENABLED=true
# Start with Docker Compose
docker-compose -f docker-compose.tor.yml up -d
# Check if Tor is running
docker logs localgreenchain-tor
# Get your onion address
docker exec localgreenchain-tor cat /var/lib/tor/hidden_service/hostname
```
Your LocalGreenChain instance is now accessible via:
- Local: http://localhost:3001
- Onion: http://[your-address].onion (share this!)
### Option 2: Manual Installation
1. **Install Tor**
2. **Configure Tor for LocalGreenChain**
3. **Start LocalGreenChain with Tor enabled**
---
## Installation Methods
### Linux (Debian/Ubuntu)
```bash
# Install Tor
sudo apt update
sudo apt install tor
# Configure Tor for LocalGreenChain
sudo cp tor/torrc.example /etc/tor/torrc
# Edit configuration
sudo nano /etc/tor/torrc
# Create hidden service directory
sudo mkdir -p /var/lib/tor/localgreenchain
sudo chown -R debian-tor:debian-tor /var/lib/tor/localgreenchain
sudo chmod 700 /var/lib/tor/localgreenchain
# Start Tor
sudo systemctl start tor
sudo systemctl enable tor
# Check status
sudo systemctl status tor
# Get your onion address (wait ~1 minute for generation)
sudo cat /var/lib/tor/localgreenchain/hostname
```
### macOS
```bash
# Install Tor via Homebrew
brew install tor
# Copy configuration
cp tor/torrc.example /usr/local/etc/tor/torrc
# Edit configuration
nano /usr/local/etc/tor/torrc
# Create hidden service directory
mkdir -p ~/Library/Application\ Support/tor/localgreenchain
chmod 700 ~/Library/Application\ Support/tor/localgreenchain
# Update torrc with your path
# HiddenServiceDir ~/Library/Application Support/tor/localgreenchain
# Start Tor
brew services start tor
# Get your onion address
cat ~/Library/Application\ Support/tor/localgreenchain/hostname
```
### Windows (WSL)
```bash
# Install WSL if not already installed
# Then follow Linux instructions above
# Or use Tor Expert Bundle
# Download from: https://www.torproject.org/download/tor/
```
---
## Configuration
### Environment Variables
Edit `.env` file:
```bash
# Enable Tor
TOR_ENABLED=true
# Tor SOCKS proxy (default)
TOR_SOCKS_HOST=127.0.0.1
TOR_SOCKS_PORT=9050
# Tor control port
TOR_CONTROL_PORT=9051
# Hidden service directory
TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/localgreenchain
# Privacy defaults
DEFAULT_PRIVACY_MODE=standard
ALLOW_ANONYMOUS_REGISTRATION=true
LOCATION_OBFUSCATION_DEFAULT=fuzzy
```
### Tor Configuration (torrc)
Minimal configuration in `/etc/tor/torrc`:
```
# SOCKS proxy
SocksPort 9050
# Hidden Service for LocalGreenChain
HiddenServiceDir /var/lib/tor/localgreenchain/
HiddenServicePort 80 127.0.0.1:3001
# Optional: Multiple ports
# HiddenServicePort 443 127.0.0.1:3001
# Logging
Log notice file /var/log/tor/notices.log
# Privacy settings
IsolateDestAddr 1
IsolateDestPort 1
```
---
## Running as Hidden Service
### Start LocalGreenChain
```bash
# Install dependencies
bun install
# Start in production mode
bun run build
bun run start
# Or development mode
bun run dev
```
### Verify Hidden Service
```bash
# Check if Tor created keys
ls -la /var/lib/tor/localgreenchain/
# Should see:
# - hostname (your .onion address)
# - hs_ed25519_public_key
# - hs_ed25519_secret_key
# Get your onion address
cat /var/lib/tor/localgreenchain/hostname
```
### Share Your Onion Address
Your `.onion` address looks like:
```
abc123def456ghi789.onion
```
Share this with trusted community members to allow anonymous access!
---
## Using Tor Browser
### As a User (Accessing LocalGreenChain via Tor)
1. **Download Tor Browser**
- Visit: https://www.torproject.org/download/
- Install for your operating system
2. **Connect to Tor Network**
- Launch Tor Browser
- Click "Connect" to establish Tor connection
3. **Access LocalGreenChain**
- Option A: Via onion address (recommended)
```
http://[your-onion-address].onion
```
- Option B: Via clearnet (still anonymous)
```
http://your-domain.com
```
4. **Register Plants Anonymously**
- Go to "Anonymous Registration" page
- Your connection will be detected as coming from Tor
- All privacy features automatically enabled
### Privacy Indicators
LocalGreenChain will show you:
- 🧅 "Tor Active" badge when connected via Tor
- Privacy recommendations based on connection type
- Tor circuit information (country, not your IP)
---
## Privacy Best Practices
### For Maximum Anonymity
1. **Always Use Tor Browser**
- Don't access via regular browser
- Tor Browser includes additional privacy protections
2. **Enable Anonymous Mode**
- Use `/plants/register-anonymous` page
- Generate random IDs and pseudonyms
- Don't reuse usernames from other sites
3. **Location Privacy**
- Use "Fuzzy" or "City" level location sharing
- Never share exact coordinates
- Consider using "Hidden" for sensitive plants
4. **Operational Security (OpSec)**
- Don't include identifiable info in plant notes
- Use different pseudonyms for different plant types
- Don't correlate with social media accounts
- Clear browser data after each session
5. **Network Security**
- Only share your .onion address with trusted people
- Use secure channels (encrypted messaging) to share addresses
- Rotate your hidden service periodically if needed
### Privacy Levels Explained
| Level | Location Accuracy | Best For |
|-------|------------------|----------|
| **Exact** | ~100m | Public gardens, commercial nurseries |
| **Fuzzy** | 1-5km radius | Home gardens, privacy-conscious sharing |
| **City** | ~10km grid | Regional plant trading |
| **Country** | ~100km grid | National distribution tracking |
| **Hidden** | No location | Maximum privacy, sensitive species |
---
## Troubleshooting
### Tor Won't Start
```bash
# Check Tor status
sudo systemctl status tor
# View logs
sudo tail -f /var/log/tor/notices.log
# Common issues:
# 1. Port 9050 already in use
sudo lsof -i :9050
# 2. Permission issues
sudo chown -R debian-tor:debian-tor /var/lib/tor
sudo chmod 700 /var/lib/tor/localgreenchain
```
### Hidden Service Not Accessible
```bash
# Verify Tor is running
pgrep tor
# Check if hostname file exists
cat /var/lib/tor/localgreenchain/hostname
# Verify LocalGreenChain is running
curl http://localhost:3001
# Check Tor logs for errors
sudo tail -f /var/log/tor/notices.log
```
### "Tor Status: Not Available"
1. Check if Tor daemon is running
2. Verify SOCKS port (9050) is open
3. Check firewall settings
4. Restart Tor service
```bash
sudo systemctl restart tor
```
### Slow Onion Connection
This is normal! Tor routes through multiple nodes:
- First connection: 30-60 seconds
- Subsequent loads: 5-15 seconds
- Plant operations: Near instant (local blockchain)
---
## Advanced Topics
### Running Multiple Hidden Services
Edit `/etc/tor/torrc`:
```
# LocalGreenChain (public)
HiddenServiceDir /var/lib/tor/localgreenchain-public/
HiddenServicePort 80 127.0.0.1:3001
# LocalGreenChain (private - invite only)
HiddenServiceDir /var/lib/tor/localgreenchain-private/
HiddenServicePort 80 127.0.0.1:3002
```
### Client Authentication (v3 Onions)
Restrict access to authorized users only:
```
# In torrc
HiddenServiceDir /var/lib/tor/localgreenchain/
HiddenServicePort 80 127.0.0.1:3001
HiddenServiceAuthorizeClient stealth alice,bob
```
### Monitoring Tor Traffic
```bash
# Real-time connection monitoring
sudo nyx
# Or arm (older tool)
sudo arm
```
### Backup Your Hidden Service Keys
**IMPORTANT**: Your `.onion` address is tied to your keys!
```bash
# Backup keys
sudo cp -r /var/lib/tor/localgreenchain ~/tor-backup/
# Restore keys (on new server)
sudo cp -r ~/tor-backup/* /var/lib/tor/localgreenchain/
sudo chown -R debian-tor:debian-tor /var/lib/tor/localgreenchain
sudo systemctl restart tor
```
---
## Security Considerations
### What Tor DOES Protect
✅ Your IP address from other users
✅ Your browsing from your ISP
✅ Your location from the network
✅ Your identity when using anonymous mode
### What Tor DOESN'T Protect
❌ Poor operational security (sharing identifying info)
❌ Malware on your computer
❌ Logging in with real accounts
❌ Data you voluntarily share
### Remember
- **Tor provides anonymity, not security**
- Use HTTPS even over Tor (LocalGreenChain supports this)
- Don't mix anonymous and identified activities
- Keep Tor Browser up to date
- Trust the process - Tor has protected millions of users
---
## Getting Help
- **LocalGreenChain Tor Issues**: https://github.com/yourusername/localgreenchain/issues
- **Tor Project**: https://support.torproject.org
- **Privacy Community**: https://www.reddit.com/r/TOR
- **Security Audit**: See SECURITY.md
## Legal Notice
Using Tor is legal in most countries. However:
- Check local laws regarding Tor usage
- Using Tor for illegal activities is still illegal
- LocalGreenChain is for botanical education and legal plant sharing
- Respect plant import/export regulations
- Some plants may be regulated or controlled substances
Stay safe, stay private, and happy growing! 🌱🧅

19
bunfig.toml Normal file
View file

@ -0,0 +1,19 @@
# Bun configuration for LocalGreenChain
[install]
# Install dependencies from the lockfile if it exists
frozenLockfile = false
# Configure which registries to use
registry = "https://registry.npmjs.org/"
# Cache directory
cache = "node_modules/.cache/bun"
[run]
# Automatically install dependencies when running scripts
autoInstall = true
[test]
# Test configuration (when tests are added)
preload = []

View file

@ -0,0 +1,334 @@
import { useState, useEffect } from 'react';
import { GrowingEnvironment } from '../lib/environment/types';
import { EnvironmentalRecommendation } from '../lib/environment/types';
interface EnvironmentalDisplayProps {
environment: GrowingEnvironment;
plantId: string;
showRecommendations?: boolean;
}
export default function EnvironmentalDisplay({
environment,
plantId,
showRecommendations = true,
}: EnvironmentalDisplayProps) {
const [recommendations, setRecommendations] = useState<EnvironmentalRecommendation[]>([]);
const [healthScore, setHealthScore] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (showRecommendations) {
fetchRecommendations();
}
}, [plantId]);
const fetchRecommendations = async () => {
try {
const response = await fetch(`/api/environment/recommendations?plantId=${plantId}`);
const data = await response.json();
if (data.success) {
setRecommendations(data.recommendations);
setHealthScore(data.environmentalHealth);
}
} catch (error) {
console.error('Error fetching recommendations:', error);
} finally {
setLoading(false);
}
};
return (
<div className="space-y-6">
{/* Environmental Health Score */}
{healthScore !== null && (
<div className="bg-gradient-to-r from-green-50 to-emerald-50 rounded-lg p-6 border border-green-200">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-1">
Environmental Health Score
</h3>
<p className="text-sm text-gray-600">
Overall assessment of growing conditions
</p>
</div>
<div className="text-center">
<div className={`text-5xl font-bold ${getScoreColor(healthScore)}`}>
{healthScore}
</div>
<div className="text-sm text-gray-600 mt-1">/ 100</div>
</div>
</div>
<div className="mt-4">
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all ${getScoreBgColor(healthScore)}`}
style={{ width: `${healthScore}%` }}
></div>
</div>
<p className="text-sm text-gray-700 mt-2">{getScoreInterpretation(healthScore)}</p>
</div>
</div>
)}
{/* Recommendations */}
{showRecommendations && recommendations.length > 0 && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">
📋 Recommendations ({recommendations.length})
</h3>
<div className="space-y-3">
{recommendations.map((rec, idx) => (
<RecommendationCard key={idx} recommendation={rec} />
))}
</div>
</div>
)}
{/* Soil Information */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
🌱 Soil Composition
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<DataPoint label="Type" value={formatValue(environment.soil.type)} />
<DataPoint label="pH" value={environment.soil.pH.toFixed(1)} />
<DataPoint label="Texture" value={formatValue(environment.soil.texture)} />
<DataPoint label="Drainage" value={formatValue(environment.soil.drainage)} />
<DataPoint label="Organic Matter" value={`${environment.soil.organicMatter}%`} />
</div>
</div>
{/* Climate Conditions */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
🌡 Climate Conditions
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<DataPoint label="Day Temp" value={`${environment.climate.temperatureDay}°C`} />
<DataPoint label="Night Temp" value={`${environment.climate.temperatureNight}°C`} />
<DataPoint label="Humidity" value={`${environment.climate.humidityAverage}%`} />
<DataPoint label="Airflow" value={formatValue(environment.climate.airflow)} />
<DataPoint label="Ventilation" value={formatValue(environment.climate.ventilation)} />
{environment.climate.zone && (
<DataPoint label="Hardiness Zone" value={environment.climate.zone} />
)}
</div>
</div>
{/* Lighting */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 flex items-center">
Lighting
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<DataPoint label="Type" value={formatValue(environment.lighting.type)} />
{environment.lighting.naturalLight && (
<>
<DataPoint
label="Exposure"
value={formatValue(environment.lighting.naturalLight.exposure)}
/>
<DataPoint
label="Hours/Day"
value={`${environment.lighting.naturalLight.hoursPerDay}h`}
/>
<DataPoint
label="Direction"
value={formatValue(environment.lighting.naturalLight.direction)}
/>
</>
)}
{environment.lighting.artificialLight && (
<>
<DataPoint
label="Light Type"
value={formatValue(environment.lighting.artificialLight.type)}
/>
<DataPoint
label="Hours/Day"
value={`${environment.lighting.artificialLight.hoursPerDay}h`}
/>
</>
)}
</div>
</div>
{/* Location & Container */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">📍 Location</h3>
<div className="space-y-3">
<DataPoint label="Type" value={formatValue(environment.location.type)} />
{environment.location.room && (
<DataPoint label="Room" value={environment.location.room} />
)}
{environment.location.elevation && (
<DataPoint label="Elevation" value={`${environment.location.elevation}m`} />
)}
</div>
</div>
{environment.container && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">🪴 Container</h3>
<div className="space-y-3">
<DataPoint label="Type" value={formatValue(environment.container.type)} />
{environment.container.material && (
<DataPoint label="Material" value={formatValue(environment.container.material)} />
)}
{environment.container.size && (
<DataPoint label="Size" value={environment.container.size} />
)}
<DataPoint
label="Drainage"
value={environment.container.drainage === 'yes' ? '✓ Yes' : '✗ No'}
highlight={environment.container.drainage === 'no' ? 'red' : undefined}
/>
</div>
</div>
)}
</div>
{/* Watering */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">💧 Watering</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<DataPoint label="Method" value={formatValue(environment.watering.method)} />
<DataPoint label="Source" value={formatValue(environment.watering.waterSource)} />
{environment.watering.frequency && (
<DataPoint label="Frequency" value={environment.watering.frequency} />
)}
{environment.watering.waterQuality?.pH && (
<DataPoint label="Water pH" value={environment.watering.waterQuality.pH.toFixed(1)} />
)}
</div>
</div>
{/* Nutrients */}
{environment.nutrients && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">🧪 Nutrients</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<DataPoint
label="NPK"
value={`${environment.nutrients.nitrogen}-${environment.nutrients.phosphorus}-${environment.nutrients.potassium}`}
/>
{environment.nutrients.ec && (
<DataPoint label="EC" value={`${environment.nutrients.ec} mS/cm`} />
)}
{environment.nutrients.tds && (
<DataPoint label="TDS" value={`${environment.nutrients.tds} ppm`} />
)}
</div>
</div>
)}
{/* Surroundings */}
{environment.surroundings && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold text-gray-900 mb-4">🌿 Surroundings</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{environment.surroundings.ecosystem && (
<DataPoint label="Ecosystem" value={formatValue(environment.surroundings.ecosystem)} />
)}
{environment.surroundings.windExposure && (
<DataPoint label="Wind" value={formatValue(environment.surroundings.windExposure)} />
)}
{environment.surroundings.companionPlants && environment.surroundings.companionPlants.length > 0 && (
<div className="col-span-2">
<p className="text-sm font-medium text-gray-700">Companion Plants:</p>
<p className="text-sm text-gray-900">{environment.surroundings.companionPlants.join(', ')}</p>
</div>
)}
</div>
</div>
)}
</div>
);
}
function RecommendationCard({ recommendation }: { recommendation: EnvironmentalRecommendation }) {
const priorityColors = {
critical: 'border-red-500 bg-red-50',
high: 'border-orange-500 bg-orange-50',
medium: 'border-yellow-500 bg-yellow-50',
low: 'border-blue-500 bg-blue-50',
};
const priorityIcons = {
critical: '🚨',
high: '⚠️',
medium: '💡',
low: '',
};
return (
<div className={`border-l-4 p-4 rounded-r-lg ${priorityColors[recommendation.priority]}`}>
<div className="flex items-start">
<span className="text-2xl mr-3">{priorityIcons[recommendation.priority]}</span>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<h4 className="font-semibold text-gray-900">{recommendation.issue}</h4>
<span className="text-xs uppercase font-semibold text-gray-600">
{recommendation.priority}
</span>
</div>
<p className="text-sm text-gray-800 mb-2">
<strong>Recommendation:</strong> {recommendation.recommendation}
</p>
<p className="text-xs text-gray-700">
<strong>Impact:</strong> {recommendation.impact}
</p>
</div>
</div>
</div>
);
}
function DataPoint({
label,
value,
highlight,
}: {
label: string;
value: string;
highlight?: 'red' | 'green';
}) {
const highlightClass = highlight === 'red' ? 'text-red-600 font-semibold' : highlight === 'green' ? 'text-green-600 font-semibold' : '';
return (
<div>
<p className="text-xs text-gray-600 mb-1">{label}</p>
<p className={`text-sm font-medium text-gray-900 ${highlightClass}`}>{value}</p>
</div>
);
}
function formatValue(value: string): string {
return value.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
}
function getScoreColor(score: number): string {
if (score >= 90) return 'text-green-600';
if (score >= 75) return 'text-lime-600';
if (score >= 60) return 'text-yellow-600';
if (score >= 40) return 'text-orange-600';
return 'text-red-600';
}
function getScoreBgColor(score: number): string {
if (score >= 90) return 'bg-green-600';
if (score >= 75) return 'bg-lime-600';
if (score >= 60) return 'bg-yellow-600';
if (score >= 40) return 'bg-orange-600';
return 'bg-red-600';
}
function getScoreInterpretation(score: number): string {
if (score >= 90) return '🌟 Excellent conditions - your plant should thrive!';
if (score >= 75) return '✓ Good conditions with minor room for improvement';
if (score >= 60) return '⚡ Adequate conditions, several optimization opportunities';
if (score >= 40) return '⚠️ Suboptimal conditions, address high-priority issues';
return '🚨 Poor conditions, immediate action needed';
}

View file

@ -0,0 +1,708 @@
import { useState } from 'react';
import {
GrowingEnvironment,
SoilComposition,
ClimateConditions,
LightingConditions,
NutrientProfile,
WateringSchedule,
ContainerInfo,
EnvironmentLocation,
SurroundingEnvironment,
} from '../lib/environment/types';
interface EnvironmentalFormProps {
value: Partial<GrowingEnvironment>;
onChange: (env: Partial<GrowingEnvironment>) => void;
compact?: boolean;
}
export default function EnvironmentalForm({
value,
onChange,
compact = false,
}: EnvironmentalFormProps) {
const [activeSection, setActiveSection] = useState<string>('soil');
const updateSection = <K extends keyof GrowingEnvironment>(
section: K,
updates: Partial<GrowingEnvironment[K]>
) => {
onChange({
...value,
[section]: {
...value[section],
...updates,
},
});
};
const sections = [
{ id: 'soil', name: '🌱 Soil', icon: '🌱' },
{ id: 'nutrients', name: '🧪 Nutrients', icon: '🧪' },
{ id: 'lighting', name: '☀️ Light', icon: '☀️' },
{ id: 'climate', name: '🌡️ Climate', icon: '🌡️' },
{ id: 'location', name: '📍 Location', icon: '📍' },
{ id: 'container', name: '🪴 Container', icon: '🪴' },
{ id: 'watering', name: '💧 Water', icon: '💧' },
{ id: 'surroundings', name: '🌿 Surroundings', icon: '🌿' },
];
return (
<div className="bg-white rounded-lg shadow-lg">
{/* Section Tabs */}
<div className="flex overflow-x-auto border-b border-gray-200 bg-gray-50">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap transition ${
activeSection === section.id
? 'border-b-2 border-green-600 text-green-600 bg-white'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
<span className="mr-2">{section.icon}</span>
{section.name}
</button>
))}
</div>
{/* Section Content */}
<div className="p-6">
{activeSection === 'soil' && (
<SoilSection
value={value.soil}
onChange={(soil) => updateSection('soil', soil)}
/>
)}
{activeSection === 'nutrients' && (
<NutrientsSection
value={value.nutrients}
onChange={(nutrients) => updateSection('nutrients', nutrients)}
/>
)}
{activeSection === 'lighting' && (
<LightingSection
value={value.lighting}
onChange={(lighting) => updateSection('lighting', lighting)}
/>
)}
{activeSection === 'climate' && (
<ClimateSection
value={value.climate}
onChange={(climate) => updateSection('climate', climate)}
/>
)}
{activeSection === 'location' && (
<LocationSection
value={value.location}
onChange={(location) => updateSection('location', location)}
/>
)}
{activeSection === 'container' && (
<ContainerSection
value={value.container}
onChange={(container) => updateSection('container', container)}
/>
)}
{activeSection === 'watering' && (
<WateringSection
value={value.watering}
onChange={(watering) => updateSection('watering', watering)}
/>
)}
{activeSection === 'surroundings' && (
<SurroundingsSection
value={value.surroundings}
onChange={(surroundings) => updateSection('surroundings', surroundings)}
/>
)}
</div>
</div>
);
}
// Soil Section Component
function SoilSection({
value,
onChange,
}: {
value?: Partial<SoilComposition>;
onChange: (soil: Partial<SoilComposition>) => void;
}) {
const soil = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Soil Composition</h3>
<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">
Soil Type *
</label>
<select
value={soil.type || 'loam'}
onChange={(e) => onChange({ ...soil, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="clay">Clay</option>
<option value="sand">Sand</option>
<option value="silt">Silt</option>
<option value="loam">Loam (balanced)</option>
<option value="peat">Peat</option>
<option value="chalk">Chalk</option>
<option value="custom">Custom Mix</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Soil pH * <span className="text-xs text-gray-500">(most plants: 6.0-7.0)</span>
</label>
<input
type="number"
step="0.1"
min="0"
max="14"
value={soil.pH || 6.5}
onChange={(e) => onChange({ ...soil, pH: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
<div className="mt-1 text-xs text-gray-600">
{soil.pH && soil.pH < 5.5 && '⚠️ Acidic - may need lime'}
{soil.pH && soil.pH >= 5.5 && soil.pH <= 7.5 && '✓ Good range'}
{soil.pH && soil.pH > 7.5 && '⚠️ Alkaline - may need sulfur'}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Texture</label>
<select
value={soil.texture || 'medium'}
onChange={(e) => onChange({ ...soil, texture: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="heavy">Heavy (clay-rich)</option>
<option value="medium">Medium (balanced)</option>
<option value="light">Light (sandy)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Drainage</label>
<select
value={soil.drainage || 'good'}
onChange={(e) => onChange({ ...soil, drainage: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="poor">Poor (stays wet)</option>
<option value="moderate">Moderate</option>
<option value="good">Good</option>
<option value="excellent">Excellent (fast draining)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Organic Matter % <span className="text-xs text-gray-500">(ideal: 5-10%)</span>
</label>
<input
type="number"
min="0"
max="100"
value={soil.organicMatter || 5}
onChange={(e) => onChange({ ...soil, organicMatter: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600 mb-2">
💡 <strong>Tip:</strong> Test soil pH with a meter or test kit for accuracy. Most vegetables prefer pH 6.0-7.0.
</p>
</div>
</div>
);
}
// Nutrients Section Component
function NutrientsSection({
value,
onChange,
}: {
value?: Partial<NutrientProfile>;
onChange: (nutrients: Partial<NutrientProfile>) => void;
}) {
const nutrients = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Nutrient Profile</h3>
<div>
<h4 className="font-medium text-gray-900 mb-3">Primary Nutrients (NPK)</h4>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nitrogen (N) %
</label>
<input
type="number"
step="0.1"
min="0"
value={nutrients.nitrogen || 0}
onChange={(e) => onChange({ ...nutrients, nitrogen: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Phosphorus (P) %
</label>
<input
type="number"
step="0.1"
min="0"
value={nutrients.phosphorus || 0}
onChange={(e) => onChange({ ...nutrients, phosphorus: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Potassium (K) %
</label>
<input
type="number"
step="0.1"
min="0"
value={nutrients.potassium || 0}
onChange={(e) => onChange({ ...nutrients, potassium: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<p className="mt-2 text-xs text-gray-600">
NPK ratio: {nutrients.nitrogen || 0}-{nutrients.phosphorus || 0}-{nutrients.potassium || 0}
</p>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Leave at 0 if unknown. Use soil test kit for accurate NPK values.
</p>
</div>
</div>
);
}
// Lighting Section Component
function LightingSection({
value,
onChange,
}: {
value?: Partial<LightingConditions>;
onChange: (lighting: Partial<LightingConditions>) => void;
}) {
const lighting = value || { type: 'natural' };
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Lighting Conditions</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Light Type</label>
<select
value={lighting.type || 'natural'}
onChange={(e) => onChange({ ...lighting, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="natural">Natural Sunlight</option>
<option value="artificial">Artificial Light Only</option>
<option value="mixed">Mixed (Natural + Artificial)</option>
</select>
</div>
{(lighting.type === 'natural' || lighting.type === 'mixed') && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-yellow-50 rounded-lg">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sun Exposure
</label>
<select
value={lighting.naturalLight?.exposure || 'full_sun'}
onChange={(e) =>
onChange({
...lighting,
naturalLight: {
...lighting.naturalLight,
exposure: e.target.value as any,
} as any,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="full_sun">Full Sun (6+ hours)</option>
<option value="partial_sun">Partial Sun (4-6 hours)</option>
<option value="partial_shade">Partial Shade (2-4 hours)</option>
<option value="full_shade">Full Shade (&lt;2 hours)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Hours of Sunlight/Day
</label>
<input
type="number"
min="0"
max="24"
value={lighting.naturalLight?.hoursPerDay || 8}
onChange={(e) =>
onChange({
...lighting,
naturalLight: {
...lighting.naturalLight,
hoursPerDay: parseInt(e.target.value),
} as any,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
)}
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Most vegetables need 6+ hours of direct sunlight. Herbs can do well with 4-6 hours.
</p>
</div>
</div>
);
}
// Climate Section Component
function ClimateSection({
value,
onChange,
}: {
value?: Partial<ClimateConditions>;
onChange: (climate: Partial<ClimateConditions>) => void;
}) {
const climate = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Climate Conditions</h3>
<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">
Day Temperature (°C) *
</label>
<input
type="number"
value={climate.temperatureDay || 22}
onChange={(e) => onChange({ ...climate, temperatureDay: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Night Temperature (°C) *
</label>
<input
type="number"
value={climate.temperatureNight || 18}
onChange={(e) => onChange({ ...climate, temperatureNight: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Average Humidity (%) *
</label>
<input
type="number"
min="0"
max="100"
value={climate.humidityAverage || 50}
onChange={(e) => onChange({ ...climate, humidityAverage: parseFloat(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Airflow</label>
<select
value={climate.airflow || 'moderate'}
onChange={(e) => onChange({ ...climate, airflow: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="none">None (still air)</option>
<option value="minimal">Minimal</option>
<option value="moderate">Moderate (good)</option>
<option value="strong">Strong/Windy</option>
</select>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Most plants thrive at 18-25°C. Good airflow prevents disease.
</p>
</div>
</div>
);
}
// Location Section Component
function LocationSection({
value,
onChange,
}: {
value?: Partial<EnvironmentLocation>;
onChange: (location: Partial<EnvironmentLocation>) => void;
}) {
const location = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Growing Location</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Location Type *</label>
<select
value={location.type || 'outdoor'}
onChange={(e) => onChange({ ...location, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="indoor">Indoor</option>
<option value="outdoor">Outdoor</option>
<option value="greenhouse">Greenhouse</option>
<option value="polytunnel">Polytunnel</option>
<option value="shade_house">Shade House</option>
<option value="window">Window</option>
<option value="balcony">Balcony</option>
</select>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Location type affects climate control and pest exposure.
</p>
</div>
</div>
);
}
// Container Section Component
function ContainerSection({
value,
onChange,
}: {
value?: Partial<ContainerInfo>;
onChange: (container: Partial<ContainerInfo>) => void;
}) {
const container = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Container Information</h3>
<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">Container Type</label>
<select
value={container.type || 'pot'}
onChange={(e) => onChange({ ...container, type: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="pot">Pot</option>
<option value="raised_bed">Raised Bed</option>
<option value="ground">In Ground</option>
<option value="fabric_pot">Fabric Pot</option>
<option value="hydroponic">Hydroponic</option>
<option value="hanging_basket">Hanging Basket</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Drainage * <span className="text-xs text-red-600">(CRITICAL!)</span>
</label>
<select
value={container.drainage || 'yes'}
onChange={(e) => onChange({ ...container, drainage: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="yes">Yes (has holes)</option>
<option value="no">No (sealed)</option>
</select>
{container.drainage === 'no' && (
<p className="mt-1 text-xs text-red-600 font-semibold">
WARNING: No drainage will likely kill your plant!
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Size (optional)
</label>
<input
type="text"
placeholder="e.g., 5 gallon, 30cm diameter"
value={container.size || ''}
onChange={(e) => onChange({ ...container, size: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Always ensure drainage! Sitting water = root rot = dead plant.
</p>
</div>
</div>
);
}
// Watering Section Component
function WateringSection({
value,
onChange,
}: {
value?: Partial<WateringSchedule>;
onChange: (watering: Partial<WateringSchedule>) => void;
}) {
const watering = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Watering Schedule</h3>
<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">Method</label>
<select
value={watering.method || 'hand_water'}
onChange={(e) => onChange({ ...watering, method: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="hand_water">Hand Watering</option>
<option value="drip">Drip Irrigation</option>
<option value="soaker_hose">Soaker Hose</option>
<option value="sprinkler">Sprinkler</option>
<option value="self_watering">Self-Watering</option>
<option value="rain">Rain Fed</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Water Source</label>
<select
value={watering.waterSource || 'tap'}
onChange={(e) => onChange({ ...watering, waterSource: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="tap">Tap Water</option>
<option value="well">Well Water</option>
<option value="rain">Rain Water</option>
<option value="filtered">Filtered</option>
<option value="distilled">Distilled</option>
<option value="RO">Reverse Osmosis (RO)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Frequency (optional)
</label>
<input
type="text"
placeholder="e.g., daily, every 2-3 days"
value={watering.frequency || ''}
onChange={(e) => onChange({ ...watering, frequency: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Water when top inch of soil is dry. Overwatering kills more plants than underwatering!
</p>
</div>
</div>
);
}
// Surroundings Section Component
function SurroundingsSection({
value,
onChange,
}: {
value?: Partial<SurroundingEnvironment>;
onChange: (surroundings: Partial<SurroundingEnvironment>) => void;
}) {
const surroundings = value || {};
return (
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900">Surrounding Environment</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Ecosystem Type</label>
<select
value={surroundings.ecosystem || 'urban'}
onChange={(e) => onChange({ ...surroundings, ecosystem: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="urban">Urban</option>
<option value="suburban">Suburban</option>
<option value="rural">Rural</option>
<option value="forest">Forest</option>
<option value="desert">Desert</option>
<option value="coastal">Coastal</option>
<option value="mountain">Mountain</option>
<option value="tropical">Tropical</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Wind Exposure</label>
<select
value={surroundings.windExposure || 'moderate'}
onChange={(e) => onChange({ ...surroundings, windExposure: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
>
<option value="sheltered">Sheltered (protected)</option>
<option value="moderate">Moderate</option>
<option value="exposed">Exposed</option>
<option value="windy">Very Windy</option>
</select>
</div>
<div className="pt-4 border-t border-gray-200">
<p className="text-sm text-gray-600">
💡 <strong>Tip:</strong> Track companion plants and pests to learn what works in your ecosystem.
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,227 @@
import { useState, useEffect } from 'react';
import { PrivacySettings as IPrivacySettings } from '../lib/privacy/anonymity';
interface PrivacySettingsProps {
value: IPrivacySettings;
onChange: (settings: IPrivacySettings) => void;
showTorStatus?: boolean;
}
export default function PrivacySettings({
value,
onChange,
showTorStatus = true,
}: PrivacySettingsProps) {
const [torStatus, setTorStatus] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (showTorStatus) {
checkTorStatus();
}
}, [showTorStatus]);
const checkTorStatus = async () => {
try {
const response = await fetch('/api/privacy/tor-status');
const data = await response.json();
if (data.success) {
setTorStatus(data);
}
} catch (error) {
console.error('Error checking Tor status:', error);
} finally {
setLoading(false);
}
};
const handleChange = (field: keyof IPrivacySettings, newValue: any) => {
onChange({
...value,
[field]: newValue,
});
};
return (
<div className="bg-white rounded-lg shadow-lg p-6 border-2 border-purple-200">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900 flex items-center">
🔒 Privacy & Anonymity Settings
</h2>
{!loading && torStatus?.tor.connectionThroughTor && (
<span className="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-semibold">
🧅 Tor Active
</span>
)}
</div>
{/* Tor Status Banner */}
{showTorStatus && !loading && (
<div
className={`mb-6 p-4 rounded-lg ${
torStatus?.tor.connectionThroughTor
? 'bg-green-50 border border-green-200'
: 'bg-yellow-50 border border-yellow-200'
}`}
>
<div className="flex items-start">
<span className="text-2xl mr-3">
{torStatus?.tor.connectionThroughTor ? '🧅' : '⚠️'}
</span>
<div className="flex-1">
<h3 className="font-semibold text-gray-900 mb-1">
{torStatus?.tor.connectionThroughTor
? 'Tor Connection Active'
: 'Not Using Tor'}
</h3>
<p className="text-sm text-gray-700 mb-2">
{torStatus?.tor.connectionThroughTor
? 'Your connection is anonymous and routed through the Tor network.'
: 'For maximum privacy, consider accessing via Tor Browser.'}
</p>
{torStatus?.tor.onionAddress && (
<p className="text-sm font-mono bg-gray-100 p-2 rounded mt-2">
Onion Address: {torStatus.tor.onionAddress}
</p>
)}
</div>
</div>
</div>
)}
{/* Anonymous Mode Toggle */}
<div className="mb-6">
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={value.anonymousMode}
onChange={(e) => handleChange('anonymousMode', e.target.checked)}
className="w-5 h-5 text-purple-600 rounded focus:ring-2 focus:ring-purple-500"
/>
<span className="ml-3 text-gray-900 font-medium">
Enable Anonymous Mode
</span>
</label>
<p className="ml-8 text-sm text-gray-600 mt-1">
Generate random identifiers and hide personal information
</p>
</div>
{/* Location Privacy */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Location Privacy Level
</label>
<select
value={value.locationPrivacy}
onChange={(e) =>
handleChange(
'locationPrivacy',
e.target.value as 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden'
)
}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="exact">📍 Exact Location (Public)</option>
<option value="fuzzy">🎯 Fuzzy (±1-5km radius)</option>
<option value="city">🏙 City Level (~10km grid)</option>
<option value="country">🌍 Country/Region (~100km grid)</option>
<option value="hidden">🔒 Hidden (No location)</option>
</select>
<div className="mt-2 text-sm text-gray-600">
{value.locationPrivacy === 'exact' && (
<span className="text-red-600 font-medium">
Warning: Exact location may reveal your home address
</span>
)}
{value.locationPrivacy === 'fuzzy' && (
<span> Good balance of privacy and discoverability</span>
)}
{value.locationPrivacy === 'city' && (
<span> Only city-level information shared</span>
)}
{value.locationPrivacy === 'country' && (
<span> Only country/region visible</span>
)}
{value.locationPrivacy === 'hidden' && (
<span className="text-purple-600 font-medium">
🔒 Maximum privacy: Location completely hidden
</span>
)}
</div>
</div>
{/* Identity Privacy */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Identity Privacy
</label>
<select
value={value.identityPrivacy}
onChange={(e) =>
handleChange(
'identityPrivacy',
e.target.value as 'real' | 'pseudonym' | 'anonymous'
)
}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="real">👤 Real Name</option>
<option value="pseudonym">🎭 Pseudonym</option>
<option value="anonymous">🔒 Anonymous</option>
</select>
</div>
{/* Share Plant Details */}
<div className="mb-4">
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={value.sharePlantDetails}
onChange={(e) =>
handleChange('sharePlantDetails', e.target.checked)
}
className="w-5 h-5 text-purple-600 rounded focus:ring-2 focus:ring-purple-500"
/>
<span className="ml-3 text-gray-900 font-medium">
Share Plant Details (species, genus, family)
</span>
</label>
<p className="ml-8 text-sm text-gray-600 mt-1">
Uncheck to use generic plant identifiers
</p>
</div>
{/* Privacy Summary */}
<div className="mt-6 p-4 bg-purple-50 rounded-lg border border-purple-200">
<h3 className="font-semibold text-purple-900 mb-2">Privacy Summary</h3>
<ul className="text-sm text-purple-800 space-y-1">
<li>
Location: {value.locationPrivacy === 'exact' ? 'Visible to all' : 'Protected'}
</li>
<li>
Identity: {value.identityPrivacy === 'real' ? 'Real name' : 'Protected'}
</li>
<li>
Plant Info: {value.sharePlantDetails ? 'Shared' : 'Generic'}
</li>
<li>
Mode: {value.anonymousMode ? 'Anonymous 🔒' : 'Standard'}
</li>
</ul>
</div>
{/* Recommendations */}
{!loading && torStatus && torStatus.recommendations && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
<h3 className="font-semibold text-blue-900 mb-2">💡 Recommendations</h3>
<ul className="text-sm text-blue-800 space-y-1">
{torStatus.recommendations.map((rec: string, idx: number) => (
<li key={idx}> {rec}</li>
))}
</ul>
</div>
)}
</div>
);
}

67
docker-compose.tor.yml Normal file
View file

@ -0,0 +1,67 @@
version: '3.8'
services:
# Tor daemon
tor:
image: goldy/tor-hidden-service:latest
container_name: localgreenchain-tor
environment:
# Hidden service configuration
SERVICE_NAME: localgreenchain
SERVICE_PORT: 80
SERVICE_HOST: app
SERVICE_HOST_PORT: 3001
volumes:
- tor-data:/var/lib/tor
- ./tor/torrc.example:/etc/tor/torrc:ro
ports:
- "9050:9050" # SOCKS proxy
- "9051:9051" # Control port
networks:
- localgreenchain-network
restart: unless-stopped
# LocalGreenChain application
app:
build: .
container_name: localgreenchain-app
environment:
- NODE_ENV=production
- TOR_ENABLED=true
- TOR_SOCKS_HOST=tor
- TOR_SOCKS_PORT=9050
- TOR_CONTROL_PORT=9051
- TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/hidden_service
volumes:
- ./data:/app/data
- tor-data:/var/lib/tor:ro
depends_on:
- tor
networks:
- localgreenchain-network
restart: unless-stopped
command: bun run start
# Optional: nginx reverse proxy for additional security
nginx:
image: nginx:alpine
container_name: localgreenchain-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- localgreenchain-network
restart: unless-stopped
volumes:
tor-data:
driver: local
networks:
localgreenchain-network:
driver: bridge

View file

@ -0,0 +1,106 @@
import crypto from 'crypto';
import { PlantData, BlockData } from './types';
/**
* PlantBlock - Represents a single block in the plant blockchain
* Each block contains data about a plant and its lineage
*/
export class PlantBlock {
public index: number;
public timestamp: string;
public plant: PlantData;
public previousHash: string;
public hash: string;
public nonce: number;
constructor(
index: number,
timestamp: string,
plant: PlantData,
previousHash: string = ''
) {
this.index = index;
this.timestamp = timestamp;
this.plant = plant;
this.previousHash = previousHash;
this.nonce = 0;
this.hash = this.calculateHash();
}
/**
* Calculate the hash of this block using SHA-256
*/
calculateHash(): string {
return crypto
.createHash('sha256')
.update(
this.index +
this.previousHash +
this.timestamp +
JSON.stringify(this.plant) +
this.nonce
)
.digest('hex');
}
/**
* Mine the block using proof-of-work algorithm
* Difficulty determines how many leading zeros the hash must have
*/
mineBlock(difficulty: number): void {
const target = Array(difficulty + 1).join('0');
while (this.hash.substring(0, difficulty) !== target) {
this.nonce++;
this.hash = this.calculateHash();
}
console.log(`Block mined: ${this.hash}`);
}
/**
* Convert block to JSON for storage/transmission
*/
toJSON(): BlockData {
return {
index: this.index,
timestamp: this.timestamp,
plant: this.plant,
previousHash: this.previousHash,
hash: this.hash,
nonce: this.nonce,
};
}
/**
* Create a PlantBlock from JSON data
*/
static fromJSON(data: BlockData): PlantBlock {
const block = new PlantBlock(
data.index,
data.timestamp,
data.plant,
data.previousHash
);
block.hash = data.hash;
block.nonce = data.nonce;
return block;
}
/**
* Verify if this block is valid
*/
isValid(previousBlock?: PlantBlock): boolean {
// Check if hash is correct
if (this.hash !== this.calculateHash()) {
return false;
}
// Check if previous hash matches
if (previousBlock && this.previousHash !== previousBlock.hash) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,396 @@
import { PlantBlock } from './PlantBlock';
import { PlantData, PlantLineage, NearbyPlant, PlantNetwork } from './types';
/**
* PlantChain - The main blockchain for tracking plant lineage and ownership
* This blockchain records every plant, its clones, seeds, and ownership transfers
*/
export class PlantChain {
public chain: PlantBlock[];
public difficulty: number;
private plantIndex: Map<string, PlantBlock>; // Quick lookup by plant ID
constructor(difficulty: number = 4) {
this.chain = [this.createGenesisBlock()];
this.difficulty = difficulty;
this.plantIndex = new Map();
}
/**
* Create the first block in the chain
*/
private createGenesisBlock(): PlantBlock {
const genesisPlant: PlantData = {
id: 'genesis-plant-0',
commonName: 'Genesis Plant',
scientificName: 'Blockchain primordialis',
propagationType: 'original',
generation: 0,
plantedDate: new Date().toISOString(),
status: 'mature',
location: {
latitude: 0,
longitude: 0,
address: 'The Beginning',
},
owner: {
id: 'system',
name: 'LocalGreenChain',
email: 'system@localgreenchain.org',
},
childPlants: [],
registeredAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const block = new PlantBlock(0, new Date().toISOString(), genesisPlant, '0');
this.plantIndex.set(genesisPlant.id, block);
return block;
}
/**
* Get the latest block in the chain
*/
getLatestBlock(): PlantBlock {
return this.chain[this.chain.length - 1];
}
/**
* Register a new original plant (not a clone or seed)
*/
registerPlant(plant: PlantData): PlantBlock {
// Ensure plant has required fields
if (!plant.id || !plant.commonName || !plant.owner) {
throw new Error('Plant must have id, commonName, and owner');
}
// Check if plant already exists
if (this.plantIndex.has(plant.id)) {
throw new Error(`Plant with ID ${plant.id} already exists`);
}
// Set defaults
plant.generation = 0;
plant.propagationType = plant.propagationType || 'original';
plant.childPlants = [];
plant.registeredAt = new Date().toISOString();
plant.updatedAt = new Date().toISOString();
const newBlock = new PlantBlock(
this.chain.length,
new Date().toISOString(),
plant,
this.getLatestBlock().hash
);
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
this.plantIndex.set(plant.id, newBlock);
return newBlock;
}
/**
* Register a clone or seed from an existing plant
*/
clonePlant(
parentPlantId: string,
newPlant: Partial<PlantData>,
propagationType: 'seed' | 'clone' | 'cutting' | 'division' | 'grafting'
): PlantBlock {
// Find parent plant
const parentBlock = this.plantIndex.get(parentPlantId);
if (!parentBlock) {
throw new Error(`Parent plant ${parentPlantId} not found`);
}
const parentPlant = parentBlock.plant;
// Create new plant with inherited properties
const clonedPlant: PlantData = {
id: newPlant.id || `${parentPlantId}-${propagationType}-${Date.now()}`,
commonName: newPlant.commonName || parentPlant.commonName,
scientificName: newPlant.scientificName || parentPlant.scientificName,
species: newPlant.species || parentPlant.species,
genus: newPlant.genus || parentPlant.genus,
family: newPlant.family || parentPlant.family,
// Lineage
parentPlantId: parentPlantId,
propagationType: propagationType,
generation: parentPlant.generation + 1,
// Required fields from newPlant
plantedDate: newPlant.plantedDate || new Date().toISOString(),
status: newPlant.status || 'sprouted',
location: newPlant.location!,
owner: newPlant.owner!,
// Initialize child tracking
childPlants: [],
// Optional fields
notes: newPlant.notes,
images: newPlant.images,
plantsNetId: newPlant.plantsNetId,
registeredAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// Validate required fields
if (!clonedPlant.location || !clonedPlant.owner) {
throw new Error('Cloned plant must have location and owner');
}
// Add to blockchain
const newBlock = new PlantBlock(
this.chain.length,
new Date().toISOString(),
clonedPlant,
this.getLatestBlock().hash
);
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
this.plantIndex.set(clonedPlant.id, newBlock);
// Update parent plant's child list
parentPlant.childPlants.push(clonedPlant.id);
parentPlant.updatedAt = new Date().toISOString();
return newBlock;
}
/**
* Update plant status (growing, flowering, etc.)
*/
updatePlantStatus(
plantId: string,
updates: Partial<PlantData>
): PlantBlock {
const existingBlock = this.plantIndex.get(plantId);
if (!existingBlock) {
throw new Error(`Plant ${plantId} not found`);
}
const updatedPlant: PlantData = {
...existingBlock.plant,
...updates,
id: existingBlock.plant.id, // Cannot change ID
parentPlantId: existingBlock.plant.parentPlantId, // Cannot change lineage
childPlants: existingBlock.plant.childPlants, // Cannot change children
generation: existingBlock.plant.generation, // Cannot change generation
registeredAt: existingBlock.plant.registeredAt, // Cannot change registration
updatedAt: new Date().toISOString(),
};
const newBlock = new PlantBlock(
this.chain.length,
new Date().toISOString(),
updatedPlant,
this.getLatestBlock().hash
);
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
this.plantIndex.set(plantId, newBlock); // Update index to latest block
return newBlock;
}
/**
* Get a plant by ID (returns latest version)
*/
getPlant(plantId: string): PlantData | null {
const block = this.plantIndex.get(plantId);
return block ? block.plant : null;
}
/**
* Get complete lineage for a plant
*/
getPlantLineage(plantId: string): PlantLineage | null {
const plant = this.getPlant(plantId);
if (!plant) return null;
// Get all ancestors
const ancestors: PlantData[] = [];
let currentPlant = plant;
while (currentPlant.parentPlantId) {
const parent = this.getPlant(currentPlant.parentPlantId);
if (!parent) break;
ancestors.push(parent);
currentPlant = parent;
}
// Get all descendants recursively
const descendants: PlantData[] = [];
const getDescendants = (p: PlantData) => {
for (const childId of p.childPlants) {
const child = this.getPlant(childId);
if (child) {
descendants.push(child);
getDescendants(child);
}
}
};
getDescendants(plant);
// Get siblings (other plants from same parent)
const siblings: PlantData[] = [];
if (plant.parentPlantId) {
const parent = this.getPlant(plant.parentPlantId);
if (parent) {
for (const siblingId of parent.childPlants) {
if (siblingId !== plantId) {
const sibling = this.getPlant(siblingId);
if (sibling) siblings.push(sibling);
}
}
}
}
return {
plantId,
ancestors,
descendants,
siblings,
generation: plant.generation,
};
}
/**
* Find plants near a location (within radius in km)
*/
findNearbyPlants(
latitude: number,
longitude: number,
radiusKm: number = 50
): NearbyPlant[] {
const nearbyPlants: NearbyPlant[] = [];
// Get all unique plants (latest version of each)
const allPlants = Array.from(this.plantIndex.values()).map(block => block.plant);
for (const plant of allPlants) {
const distance = this.calculateDistance(
latitude,
longitude,
plant.location.latitude,
plant.location.longitude
);
if (distance <= radiusKm) {
nearbyPlants.push({
plant,
distance,
owner: plant.owner,
});
}
}
// Sort by distance
return nearbyPlants.sort((a, b) => a.distance - b.distance);
}
/**
* Calculate distance between two coordinates using Haversine formula
*/
private calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371; // Earth's radius in km
const dLat = this.toRadians(lat2 - lat1);
const dLon = this.toRadians(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(lat1)) *
Math.cos(this.toRadians(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private toRadians(degrees: number): number {
return degrees * (Math.PI / 180);
}
/**
* Get network statistics
*/
getNetworkStats(): PlantNetwork {
const allPlants = Array.from(this.plantIndex.values()).map(block => block.plant);
const owners = new Set<string>();
const species: { [key: string]: number } = {};
const countries: { [key: string]: number } = {};
for (const plant of allPlants) {
owners.add(plant.owner.id);
if (plant.scientificName) {
species[plant.scientificName] = (species[plant.scientificName] || 0) + 1;
}
if (plant.location.country) {
countries[plant.location.country] = (countries[plant.location.country] || 0) + 1;
}
}
return {
totalPlants: allPlants.length,
totalOwners: owners.size,
species,
globalDistribution: countries,
};
}
/**
* Validate the entire blockchain
*/
isChainValid(): boolean {
for (let i = 1; i < this.chain.length; i++) {
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
if (!currentBlock.isValid(previousBlock)) {
return false;
}
}
return true;
}
/**
* Export chain to JSON
*/
toJSON(): any {
return {
difficulty: this.difficulty,
chain: this.chain.map(block => block.toJSON()),
};
}
/**
* Import chain from JSON
*/
static fromJSON(data: any): PlantChain {
const plantChain = new PlantChain(data.difficulty);
plantChain.chain = data.chain.map((blockData: any) =>
PlantBlock.fromJSON(blockData)
);
// Rebuild index
plantChain.plantIndex.clear();
for (const block of plantChain.chain) {
plantChain.plantIndex.set(block.plant.id, block);
}
return plantChain;
}
}

106
lib/blockchain/manager.ts Normal file
View file

@ -0,0 +1,106 @@
/**
* Blockchain Manager
* Singleton to manage the global plant blockchain instance
*/
import { PlantChain } from './PlantChain';
import fs from 'fs';
import path from 'path';
const BLOCKCHAIN_FILE = path.join(process.cwd(), 'data', 'plantchain.json');
class BlockchainManager {
private static instance: BlockchainManager;
private plantChain: PlantChain;
private autoSaveInterval: NodeJS.Timeout | null = null;
private constructor() {
this.plantChain = this.loadBlockchain();
this.startAutoSave();
}
public static getInstance(): BlockchainManager {
if (!BlockchainManager.instance) {
BlockchainManager.instance = new BlockchainManager();
}
return BlockchainManager.instance;
}
public getChain(): PlantChain {
return this.plantChain;
}
/**
* Load blockchain from file or create new one
*/
private loadBlockchain(): PlantChain {
try {
// Ensure data directory exists
const dataDir = path.join(process.cwd(), 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
if (fs.existsSync(BLOCKCHAIN_FILE)) {
const data = fs.readFileSync(BLOCKCHAIN_FILE, 'utf-8');
const chainData = JSON.parse(data);
console.log('✓ Loaded existing blockchain with', chainData.chain.length, 'blocks');
return PlantChain.fromJSON(chainData);
}
} catch (error) {
console.error('Error loading blockchain:', error);
}
console.log('✓ Created new blockchain');
return new PlantChain(4); // difficulty of 4
}
/**
* Save blockchain to file
*/
public saveBlockchain(): void {
try {
const dataDir = path.join(process.cwd(), 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const data = JSON.stringify(this.plantChain.toJSON(), null, 2);
fs.writeFileSync(BLOCKCHAIN_FILE, data, 'utf-8');
console.log('✓ Blockchain saved');
} catch (error) {
console.error('Error saving blockchain:', error);
}
}
/**
* Auto-save blockchain every 5 minutes
*/
private startAutoSave(): void {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
}
this.autoSaveInterval = setInterval(() => {
this.saveBlockchain();
}, 5 * 60 * 1000); // 5 minutes
}
/**
* Stop auto-save
*/
public stopAutoSave(): void {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
}
}
}
export function getBlockchain(): PlantChain {
return BlockchainManager.getInstance().getChain();
}
export function saveBlockchain(): void {
BlockchainManager.getInstance().saveBlockchain();
}

87
lib/blockchain/types.ts Normal file
View file

@ -0,0 +1,87 @@
// Plant Blockchain Types
import { GrowingEnvironment, GrowthMetrics } from '../environment/types';
export interface PlantLocation {
latitude: number;
longitude: number;
address?: string;
city?: string;
country?: string;
}
export interface PlantOwner {
id: string;
name: string;
email: string;
walletAddress?: string;
}
export interface PlantData {
id: string;
commonName: string;
scientificName?: string;
species?: string;
genus?: string;
family?: string;
// Lineage tracking
parentPlantId?: string; // Original plant this came from
propagationType?: 'seed' | 'clone' | 'cutting' | 'division' | 'grafting' | 'original';
generation: number; // How many generations from the original
// Plant lifecycle
plantedDate: string;
harvestedDate?: string;
status: 'sprouted' | 'growing' | 'mature' | 'flowering' | 'fruiting' | 'dormant' | 'deceased';
// Location and ownership
location: PlantLocation;
owner: PlantOwner;
// Plant network
childPlants: string[]; // IDs of clones and seeds from this plant
// Environmental data
environment?: GrowingEnvironment;
growthMetrics?: GrowthMetrics;
// Additional metadata
notes?: string;
images?: string[];
plantsNetId?: string; // ID from plants.net API
// Timestamps
registeredAt: string;
updatedAt: string;
}
export interface BlockData {
index: number;
timestamp: string;
plant: PlantData;
previousHash: string;
hash: string;
nonce: number;
}
export interface PlantLineage {
plantId: string;
ancestors: PlantData[];
descendants: PlantData[];
siblings: PlantData[]; // Other plants from the same parent
generation: number;
}
export interface NearbyPlant {
plant: PlantData;
distance: number; // in kilometers
owner: PlantOwner;
}
export interface PlantNetwork {
totalPlants: number;
totalOwners: number;
species: { [key: string]: number };
globalDistribution: { [country: string]: number };
}

432
lib/environment/analysis.ts Normal file
View file

@ -0,0 +1,432 @@
/**
* Environmental Analysis Service
* Compares growing conditions, generates recommendations, and analyzes correlations
*/
import {
GrowingEnvironment,
EnvironmentalComparison,
EnvironmentalRecommendation,
SoilComposition,
ClimateConditions,
LightingConditions,
NutrientProfile,
} from './types';
import { PlantData } from '../blockchain/types';
/**
* Compare two growing environments and return similarity score
*/
export function compareEnvironments(
env1: GrowingEnvironment,
env2: GrowingEnvironment
): EnvironmentalComparison {
const similarities: string[] = [];
const differences: string[] = [];
let score = 0;
let maxScore = 0;
// Compare location type (10 points)
maxScore += 10;
if (env1.location.type === env2.location.type) {
score += 10;
similarities.push(`Both ${env1.location.type} growing`);
} else {
differences.push(`Location: ${env1.location.type} vs ${env2.location.type}`);
}
// Compare soil type (15 points)
maxScore += 15;
if (env1.soil.type === env2.soil.type) {
score += 15;
similarities.push(`Same soil type: ${env1.soil.type}`);
} else {
differences.push(`Soil: ${env1.soil.type} vs ${env2.soil.type}`);
}
// Compare soil pH (10 points, within 0.5 range)
maxScore += 10;
const pHDiff = Math.abs(env1.soil.pH - env2.soil.pH);
if (pHDiff <= 0.5) {
score += 10;
similarities.push(`Similar soil pH: ${env1.soil.pH.toFixed(1)}${env2.soil.pH.toFixed(1)}`);
} else if (pHDiff <= 1.0) {
score += 5;
differences.push(`Soil pH: ${env1.soil.pH.toFixed(1)} vs ${env2.soil.pH.toFixed(1)}`);
} else {
differences.push(`Soil pH differs significantly: ${env1.soil.pH.toFixed(1)} vs ${env2.soil.pH.toFixed(1)}`);
}
// Compare temperature (15 points)
maxScore += 15;
const tempDiff = Math.abs(env1.climate.temperatureDay - env2.climate.temperatureDay);
if (tempDiff <= 3) {
score += 15;
similarities.push(`Similar temperatures: ${env1.climate.temperatureDay}°C ≈ ${env2.climate.temperatureDay}°C`);
} else if (tempDiff <= 7) {
score += 8;
differences.push(`Temperature: ${env1.climate.temperatureDay}°C vs ${env2.climate.temperatureDay}°C`);
} else {
differences.push(`Temperature differs significantly: ${env1.climate.temperatureDay}°C vs ${env2.climate.temperatureDay}°C`);
}
// Compare humidity (10 points)
maxScore += 10;
const humidityDiff = Math.abs(env1.climate.humidityAverage - env2.climate.humidityAverage);
if (humidityDiff <= 10) {
score += 10;
similarities.push(`Similar humidity: ${env1.climate.humidityAverage}% ≈ ${env2.climate.humidityAverage}%`);
} else if (humidityDiff <= 20) {
score += 5;
} else {
differences.push(`Humidity: ${env1.climate.humidityAverage}% vs ${env2.climate.humidityAverage}%`);
}
// Compare lighting type (15 points)
maxScore += 15;
if (env1.lighting.type === env2.lighting.type) {
score += 15;
similarities.push(`Same lighting: ${env1.lighting.type}`);
} else {
differences.push(`Lighting: ${env1.lighting.type} vs ${env2.lighting.type}`);
}
// Compare natural light exposure if both use natural light (10 points)
if (env1.lighting.naturalLight && env2.lighting.naturalLight) {
maxScore += 10;
if (env1.lighting.naturalLight.exposure === env2.lighting.naturalLight.exposure) {
score += 10;
similarities.push(`Same sun exposure: ${env1.lighting.naturalLight.exposure}`);
} else {
differences.push(`Sun exposure: ${env1.lighting.naturalLight.exposure} vs ${env2.lighting.naturalLight.exposure}`);
}
}
// Compare water source (5 points)
maxScore += 5;
if (env1.watering.waterSource === env2.watering.waterSource) {
score += 5;
similarities.push(`Same water source: ${env1.watering.waterSource}`);
} else {
differences.push(`Water: ${env1.watering.waterSource} vs ${env2.watering.waterSource}`);
}
// Compare container type (10 points)
if (env1.container && env2.container) {
maxScore += 10;
if (env1.container.type === env2.container.type) {
score += 10;
similarities.push(`Same container: ${env1.container.type}`);
} else {
differences.push(`Container: ${env1.container.type} vs ${env2.container.type}`);
}
}
// Calculate final score as percentage
const finalScore = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;
return {
plant1: '', // Will be filled by caller
plant2: '', // Will be filled by caller
similarities,
differences,
score: finalScore,
};
}
/**
* Generate environmental recommendations based on plant data
*/
export function generateRecommendations(
plant: PlantData
): EnvironmentalRecommendation[] {
const recommendations: EnvironmentalRecommendation[] = [];
if (!plant.environment) {
recommendations.push({
category: 'general',
priority: 'high',
issue: 'No environmental data',
recommendation: 'Add environmental information to track growing conditions',
impact: 'Environmental tracking helps optimize growing conditions and share knowledge',
});
return recommendations;
}
const env = plant.environment;
// Soil pH recommendations
if (env.soil.pH < 5.5) {
recommendations.push({
category: 'soil',
priority: 'high',
issue: `Low soil pH (${env.soil.pH})`,
recommendation: 'Add lime to raise pH. Most plants prefer 6.0-7.0',
impact: 'Acidic soil can lock out nutrients and harm roots',
});
} else if (env.soil.pH > 7.5) {
recommendations.push({
category: 'soil',
priority: 'high',
issue: `High soil pH (${env.soil.pH})`,
recommendation: 'Add sulfur to lower pH. Most plants prefer 6.0-7.0',
impact: 'Alkaline soil can prevent nutrient uptake',
});
}
// Organic matter recommendations
if (env.soil.organicMatter < 3) {
recommendations.push({
category: 'soil',
priority: 'medium',
issue: `Low organic matter (${env.soil.organicMatter}%)`,
recommendation: 'Add compost or aged manure to improve soil structure',
impact: 'Increases water retention and nutrient availability',
});
}
// Drainage recommendations
if (env.soil.drainage === 'poor') {
recommendations.push({
category: 'soil',
priority: 'high',
issue: 'Poor soil drainage',
recommendation: 'Add perlite, sand, or create raised beds',
impact: 'Prevents root rot and improves oxygen availability',
});
}
// Temperature recommendations
if (env.climate.temperatureDay > 35) {
recommendations.push({
category: 'climate',
priority: 'high',
issue: `High daytime temperature (${env.climate.temperatureDay}°C)`,
recommendation: 'Provide shade during hottest hours, increase watering',
impact: 'Extreme heat can cause stress and reduce growth',
});
} else if (env.climate.temperatureDay < 10) {
recommendations.push({
category: 'climate',
priority: 'high',
issue: `Low daytime temperature (${env.climate.temperatureDay}°C)`,
recommendation: 'Provide frost protection or move indoors',
impact: 'Cold temperatures can slow growth or damage plants',
});
}
// Humidity recommendations
if (env.climate.humidityAverage < 30) {
recommendations.push({
category: 'climate',
priority: 'medium',
issue: `Low humidity (${env.climate.humidityAverage}%)`,
recommendation: 'Mist plants, use humidity tray, or group plants together',
impact: 'Low humidity can cause leaf tip browning and stress',
});
} else if (env.climate.humidityAverage > 80) {
recommendations.push({
category: 'climate',
priority: 'medium',
issue: `High humidity (${env.climate.humidityAverage}%)`,
recommendation: 'Improve air circulation to prevent fungal issues',
impact: 'High humidity can promote mold and disease',
});
}
// Lighting recommendations
if (env.lighting.type === 'natural' && env.lighting.naturalLight) {
const hoursPerDay = env.lighting.naturalLight.hoursPerDay;
if (hoursPerDay < 4 && plant.status === 'growing') {
recommendations.push({
category: 'light',
priority: 'high',
issue: `Insufficient light (${hoursPerDay} hours/day)`,
recommendation: 'Move to brighter location or supplement with grow lights',
impact: 'Inadequate light causes leggy growth and poor health',
});
}
}
// Nutrient recommendations
if (env.nutrients) {
const npk = [env.nutrients.nitrogen, env.nutrients.phosphorus, env.nutrients.potassium];
if (npk.some(n => n < 1)) {
recommendations.push({
category: 'nutrients',
priority: 'medium',
issue: `Low NPK levels (${npk.join('-')})`,
recommendation: 'Apply balanced fertilizer according to plant needs',
impact: 'Nutrient deficiency reduces growth and yield',
});
}
}
// Water quality recommendations
if (env.watering.waterQuality) {
if (env.watering.waterQuality.pH && env.watering.waterQuality.pH > 7.5) {
recommendations.push({
category: 'water',
priority: 'low',
issue: `Alkaline water pH (${env.watering.waterQuality.pH})`,
recommendation: 'Consider pH adjustment or rainwater collection',
impact: 'High water pH can gradually raise soil pH',
});
}
if (env.watering.waterQuality.chlorine === 'yes') {
recommendations.push({
category: 'water',
priority: 'low',
issue: 'Chlorinated water',
recommendation: 'Let water sit 24h before use or use filter',
impact: 'Chlorine can harm beneficial soil microbes',
});
}
}
// Container recommendations
if (env.container && env.container.drainage === 'no') {
recommendations.push({
category: 'general',
priority: 'critical',
issue: 'Container has no drainage',
recommendation: 'Add drainage holes immediately to prevent root rot',
impact: 'Critical: Standing water will kill most plants',
});
}
return recommendations.sort((a, b) => {
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
}
/**
* Find plants with similar growing conditions
*/
export function findSimilarEnvironments(
targetPlant: PlantData,
allPlants: PlantData[],
minScore: number = 70
): Array<{ plant: PlantData; comparison: EnvironmentalComparison }> {
if (!targetPlant.environment) {
return [];
}
const similar: Array<{ plant: PlantData; comparison: EnvironmentalComparison }> = [];
for (const plant of allPlants) {
// Skip self and plants without environment data
if (plant.id === targetPlant.id || !plant.environment) {
continue;
}
const comparison = compareEnvironments(targetPlant.environment, plant.environment);
comparison.plant1 = targetPlant.id;
comparison.plant2 = plant.id;
if (comparison.score >= minScore) {
similar.push({ plant, comparison });
}
}
// Sort by similarity score (highest first)
return similar.sort((a, b) => b.comparison.score - a.comparison.score);
}
/**
* Analyze growth success based on environmental factors
*/
export function analyzeGrowthCorrelation(plants: PlantData[]): {
bestConditions: Partial<GrowingEnvironment>;
insights: string[];
} {
const successfulPlants = plants.filter(
p => p.environment && (p.status === 'mature' || p.status === 'flowering' || p.status === 'fruiting')
);
if (successfulPlants.length === 0) {
return {
bestConditions: {},
insights: ['Not enough data to determine optimal conditions'],
};
}
const insights: string[] = [];
// Analyze soil pH
const pHValues = successfulPlants
.filter(p => p.environment?.soil.pH)
.map(p => p.environment!.soil.pH);
if (pHValues.length > 0) {
const avgPH = pHValues.reduce((a, b) => a + b, 0) / pHValues.length;
insights.push(`Successful plants average soil pH: ${avgPH.toFixed(1)}`);
}
// Analyze temperature
const temps = successfulPlants
.filter(p => p.environment?.climate.temperatureDay)
.map(p => p.environment!.climate.temperatureDay);
if (temps.length > 0) {
const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length;
const minTemp = Math.min(...temps);
const maxTemp = Math.max(...temps);
insights.push(`Successful temperature range: ${minTemp}-${maxTemp}°C (avg: ${avgTemp.toFixed(1)}°C)`);
}
// Analyze lighting
const lightingTypes = successfulPlants
.filter(p => p.environment?.lighting.type)
.map(p => p.environment!.lighting.type);
const lightCount = lightingTypes.reduce((acc, type) => {
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const mostCommonLight = Object.entries(lightCount)
.sort((a, b) => b[1] - a[1])[0];
if (mostCommonLight) {
insights.push(`Most successful lighting: ${mostCommonLight[0]} (${mostCommonLight[1]} plants)`);
}
return {
bestConditions: {
// Would be filled with actual aggregate data
},
insights,
};
}
/**
* Calculate environmental health score (0-100)
*/
export function calculateEnvironmentalHealth(env: GrowingEnvironment): number {
let score = 100;
let deductions = 0;
// Soil health (up to -20 points)
if (env.soil.pH < 5.5 || env.soil.pH > 7.5) deductions += 10;
if (env.soil.drainage === 'poor') deductions += 10;
if (env.soil.organicMatter < 3) deductions += 5;
// Climate (up to -20 points)
if (env.climate.temperatureDay > 35 || env.climate.temperatureDay < 10) deductions += 10;
if (env.climate.humidityAverage < 30 || env.climate.humidityAverage > 80) deductions += 5;
if (env.climate.airflow === 'none') deductions += 5;
// Lighting (up to -15 points)
if (env.lighting.naturalLight && env.lighting.naturalLight.hoursPerDay < 4) deductions += 15;
// Container (up to -15 points)
if (env.container && env.container.drainage === 'no') deductions += 15;
// Water (up to -10 points)
if (env.watering.waterQuality?.chlorine === 'yes') deductions += 5;
if (env.watering.waterQuality?.pH && env.watering.waterQuality.pH > 7.5) deductions += 5;
return Math.max(0, score - deductions);
}

253
lib/environment/types.ts Normal file
View file

@ -0,0 +1,253 @@
/**
* Environmental Data Types for LocalGreenChain
* Tracks soil, climate, nutrients, and growing conditions
*/
// Soil Composition
export interface SoilComposition {
type: 'clay' | 'sand' | 'silt' | 'loam' | 'peat' | 'chalk' | 'custom';
customType?: string;
// Soil properties
pH: number; // 0-14, most plants 6.0-7.5
texture: 'heavy' | 'medium' | 'light';
drainage: 'poor' | 'moderate' | 'good' | 'excellent';
organicMatter: number; // percentage 0-100
// Composition percentages (should sum to ~100)
clayPercent?: number;
sandPercent?: number;
siltPercent?: number;
// Amendments/additives
amendments?: SoilAmendment[];
notes?: string;
}
export interface SoilAmendment {
type: 'compost' | 'manure' | 'perlite' | 'vermiculite' | 'peat_moss' | 'coco_coir' | 'biochar' | 'lime' | 'sulfur' | 'other';
name: string;
amount?: string; // e.g., "2 cups per gallon", "10%"
dateAdded: string;
}
// Nutrients & Fertilization
export interface NutrientProfile {
// NPK values (percentage)
nitrogen: number; // N
phosphorus: number; // P
potassium: number; // K
// Secondary nutrients
calcium?: number;
magnesium?: number;
sulfur?: number;
// Micronutrients (ppm or mg/L)
iron?: number;
manganese?: number;
zinc?: number;
copper?: number;
boron?: number;
molybdenum?: number;
// EC (Electrical Conductivity) - measure of nutrient concentration
ec?: number; // mS/cm
// TDS (Total Dissolved Solids)
tds?: number; // ppm
lastTested?: string;
}
export interface FertilizerApplication {
id: string;
date: string;
type: 'organic' | 'synthetic' | 'liquid' | 'granular' | 'foliar' | 'slow_release';
name: string;
npk?: string; // e.g., "10-10-10", "5-2-3"
amount: string;
frequency?: string; // e.g., "weekly", "bi-weekly", "monthly"
notes?: string;
}
// Sunlight & Climate
export interface LightingConditions {
type: 'natural' | 'artificial' | 'mixed';
// Natural light
naturalLight?: {
exposure: 'full_sun' | 'partial_sun' | 'partial_shade' | 'full_shade';
hoursPerDay: number; // 0-24
direction: 'north' | 'south' | 'east' | 'west' | 'multiple';
quality: 'direct' | 'filtered' | 'dappled' | 'indirect';
};
// Artificial light
artificialLight?: {
type: 'LED' | 'fluorescent' | 'HPS' | 'MH' | 'incandescent' | 'mixed';
spectrum?: string; // e.g., "full spectrum", "6500K", "2700K"
wattage?: number;
hoursPerDay: number;
distance?: number; // cm from plant
};
// Light measurements
ppfd?: number; // Photosynthetic Photon Flux Density (μmol/m²/s)
dli?: number; // Daily Light Integral (mol/m²/day)
}
export interface ClimateConditions {
// Temperature (Celsius)
temperatureDay: number;
temperatureNight: number;
temperatureMin?: number;
temperatureMax?: number;
// Humidity (percentage)
humidityAverage: number;
humidityMin?: number;
humidityMax?: number;
// Air circulation
airflow: 'none' | 'minimal' | 'moderate' | 'strong';
ventilation: 'poor' | 'adequate' | 'good' | 'excellent';
// CO2 levels (ppm)
co2?: number; // ambient ~400, enhanced ~1200-1500
// Seasonal variation
season?: 'spring' | 'summer' | 'fall' | 'winter';
zone?: string; // USDA hardiness zone, e.g., "9b", "10a"
}
// Growing Environment
export interface GrowingEnvironment {
location: EnvironmentLocation;
container?: ContainerInfo;
soil: SoilComposition;
nutrients: NutrientProfile;
fertilizers?: FertilizerApplication[];
lighting: LightingConditions;
climate: ClimateConditions;
watering: WateringSchedule;
surroundings?: SurroundingEnvironment;
// Tracking
monitoringFrequency?: 'daily' | 'weekly' | 'bi-weekly' | 'monthly';
lastUpdated: string;
notes?: string;
}
export interface EnvironmentLocation {
type: 'indoor' | 'outdoor' | 'greenhouse' | 'polytunnel' | 'shade_house' | 'window' | 'balcony';
description?: string;
// For indoor
room?: string; // e.g., "bedroom", "basement", "grow tent"
// For outdoor
exposureToElements?: 'protected' | 'semi_protected' | 'exposed';
elevation?: number; // meters above sea level
slope?: 'flat' | 'gentle' | 'moderate' | 'steep';
aspect?: 'north' | 'south' | 'east' | 'west'; // slope direction
}
export interface ContainerInfo {
type: 'pot' | 'raised_bed' | 'ground' | 'hydroponic' | 'aeroponic' | 'aquaponic' | 'fabric_pot' | 'hanging_basket';
material?: 'plastic' | 'terracotta' | 'ceramic' | 'fabric' | 'wood' | 'metal' | 'concrete';
size?: string; // e.g., "5 gallon", "30cm diameter", "4x8 feet"
volume?: number; // liters
depth?: number; // cm
drainage: 'yes' | 'no';
drainageHoles?: number;
}
export interface WateringSchedule {
method: 'hand_water' | 'drip' | 'soaker_hose' | 'sprinkler' | 'self_watering' | 'hydroponic' | 'rain';
frequency?: string; // e.g., "daily", "every 2-3 days", "weekly"
amount?: string; // e.g., "1 liter", "until runoff", "1 inch"
waterSource: 'tap' | 'well' | 'rain' | 'filtered' | 'distilled' | 'RO';
waterQuality?: {
pH?: number;
tds?: number; // ppm
chlorine?: 'yes' | 'no' | 'filtered';
};
}
export interface SurroundingEnvironment {
// Companion plants
companionPlants?: string[]; // other plant species nearby
// Nearby features
nearbyTrees?: boolean;
nearbyStructures?: string; // e.g., "building", "wall", "fence"
groundCover?: string; // e.g., "mulch", "grass", "bare soil", "gravel"
// Wildlife & pests
pollinators?: string[]; // e.g., "bees", "butterflies", "hummingbirds"
beneficialInsects?: string[]; // e.g., "ladybugs", "lacewings"
pests?: PestInfo[];
diseases?: DiseaseInfo[];
// Microclimate factors
windExposure?: 'sheltered' | 'moderate' | 'exposed' | 'windy';
frostPocket?: boolean;
heatTrap?: boolean;
// Ecosystem type
ecosystem?: 'urban' | 'suburban' | 'rural' | 'forest' | 'desert' | 'coastal' | 'mountain' | 'tropical';
}
export interface PestInfo {
name: string;
severity: 'minor' | 'moderate' | 'severe';
treatment?: string;
dateObserved: string;
}
export interface DiseaseInfo {
name: string;
symptoms?: string;
severity: 'minor' | 'moderate' | 'severe';
treatment?: string;
dateObserved: string;
}
// Environmental Comparison & Analysis
export interface EnvironmentalComparison {
plant1: string; // plant ID
plant2: string; // plant ID
similarities: string[];
differences: string[];
score: number; // 0-100, how similar the environments are
}
export interface GrowthMetrics {
plantId: string;
measurements: PlantMeasurement[];
healthScore: number; // 0-100
vigor: 'poor' | 'fair' | 'good' | 'excellent';
issues?: string[];
}
export interface PlantMeasurement {
date: string;
height?: number; // cm
width?: number; // cm
leafCount?: number;
flowerCount?: number;
fruitCount?: number;
notes?: string;
photos?: string[];
}
// Helper types for environmental recommendations
export interface EnvironmentalRecommendation {
category: 'soil' | 'nutrients' | 'light' | 'water' | 'climate' | 'general';
priority: 'low' | 'medium' | 'high' | 'critical';
issue: string;
recommendation: string;
impact: string;
}

221
lib/privacy/anonymity.ts Normal file
View file

@ -0,0 +1,221 @@
import crypto from 'crypto';
/**
* Privacy and Anonymity Utilities for LocalGreenChain
* Provides tools for anonymous plant tracking while maintaining blockchain integrity
*/
export interface PrivacySettings {
anonymousMode: boolean;
locationPrivacy: 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden';
identityPrivacy: 'real' | 'pseudonym' | 'anonymous';
sharePlantDetails: boolean;
}
export interface FuzzyLocation {
latitude: number;
longitude: number;
accuracy: number; // radius in km
displayName: string;
}
/**
* Generate anonymous user ID using cryptographic hash
*/
export function generateAnonymousId(): string {
const randomBytes = crypto.randomBytes(32);
return 'anon_' + crypto.createHash('sha256').update(randomBytes).digest('hex').substring(0, 16);
}
/**
* Generate pseudonymous wallet address
*/
export function generateWalletAddress(): string {
const randomBytes = crypto.randomBytes(20);
return '0x' + randomBytes.toString('hex');
}
/**
* Obfuscate location based on privacy level
* This prevents exact home address tracking while allowing geographic discovery
*/
export function obfuscateLocation(
latitude: number,
longitude: number,
privacyLevel: 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden'
): FuzzyLocation {
switch (privacyLevel) {
case 'exact':
return {
latitude,
longitude,
accuracy: 0.1, // ~100m
displayName: 'Exact location',
};
case 'fuzzy':
// Add random offset within 1-5km radius
const fuzzRadius = 1 + Math.random() * 4; // 1-5 km
const angle = Math.random() * 2 * Math.PI;
const latOffset = (fuzzRadius / 111) * Math.cos(angle); // 111 km per degree
const lonOffset = (fuzzRadius / (111 * Math.cos(latitude * Math.PI / 180))) * Math.sin(angle);
return {
latitude: latitude + latOffset,
longitude: longitude + lonOffset,
accuracy: fuzzRadius,
displayName: `Within ${Math.round(fuzzRadius)} km`,
};
case 'city':
// Round to ~10km grid (0.1 degree ≈ 11km)
return {
latitude: Math.round(latitude * 10) / 10,
longitude: Math.round(longitude * 10) / 10,
accuracy: 10,
displayName: 'City area',
};
case 'country':
// Round to ~100km grid (1 degree ≈ 111km)
return {
latitude: Math.round(latitude),
longitude: Math.round(longitude),
accuracy: 100,
displayName: 'Country/Region',
};
case 'hidden':
return {
latitude: 0,
longitude: 0,
accuracy: 999999,
displayName: 'Location hidden',
};
default:
return obfuscateLocation(latitude, longitude, 'fuzzy');
}
}
/**
* Generate anonymous plant name
*/
export function generateAnonymousPlantName(plantType: string, generation: number): string {
const hash = crypto.randomBytes(4).toString('hex');
return `${plantType}-Gen${generation}-${hash}`;
}
/**
* Encrypt sensitive data for storage
*/
export function encryptData(data: string, key: string): string {
const algorithm = 'aes-256-cbc';
const keyHash = crypto.createHash('sha256').update(key).digest();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, keyHash, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
/**
* Decrypt sensitive data
*/
export function decryptData(encryptedData: string, key: string): string {
const algorithm = 'aes-256-cbc';
const keyHash = crypto.createHash('sha256').update(key).digest();
const parts = encryptedData.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const decipher = crypto.createDecipheriv(algorithm, keyHash, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Generate Tor-friendly onion address from plant ID
* This creates a deterministic onion-style identifier
*/
export function generateOnionIdentifier(plantId: string): string {
const hash = crypto.createHash('sha256').update(plantId).digest('hex');
return hash.substring(0, 16) + '.onion';
}
/**
* Create anonymous contact method
*/
export function createAnonymousContact(realEmail: string, privateKey: string): string {
// Hash email with private key to create anonymous identifier
const hash = crypto.createHash('sha256')
.update(realEmail + privateKey)
.digest('hex');
return `anon-${hash.substring(0, 12)}@localgreenchain.onion`;
}
/**
* Validate Tor connection
*/
export async function isTorConnection(req: any): Promise<boolean> {
// Check if request is coming through Tor
const forwardedFor = req.headers['x-forwarded-for'];
const realIp = req.headers['x-real-ip'];
// Tor exit nodes typically set specific headers
// This is a simplified check
return (
req.headers['x-tor-connection'] === 'true' ||
req.socket?.remoteAddress?.includes('127.0.0.1') // Tor proxy
);
}
/**
* Generate privacy report for plant
*/
export interface PrivacyReport {
locationPrivacy: string;
identityPrivacy: string;
dataEncryption: boolean;
torEnabled: boolean;
riskLevel: 'low' | 'medium' | 'high';
recommendations: string[];
}
export function generatePrivacyReport(settings: PrivacySettings): PrivacyReport {
const recommendations: string[] = [];
let riskLevel: 'low' | 'medium' | 'high' = 'low';
if (settings.locationPrivacy === 'exact') {
recommendations.push('Consider using fuzzy location to protect your privacy');
riskLevel = 'high';
}
if (settings.identityPrivacy === 'real') {
recommendations.push('Using real identity may compromise anonymity');
if (riskLevel === 'low') riskLevel = 'medium';
}
if (settings.sharePlantDetails && !settings.anonymousMode) {
recommendations.push('Sharing detailed plant info without anonymous mode enabled');
}
if (settings.anonymousMode && settings.locationPrivacy !== 'hidden') {
riskLevel = 'low';
recommendations.push('Good privacy settings enabled');
}
return {
locationPrivacy: settings.locationPrivacy,
identityPrivacy: settings.identityPrivacy,
dataEncryption: settings.anonymousMode,
torEnabled: false, // Will be detected at runtime
riskLevel,
recommendations: recommendations.length > 0 ? recommendations : ['Privacy settings are optimal'],
};
}

302
lib/services/geolocation.ts Normal file
View file

@ -0,0 +1,302 @@
/**
* Geolocation Service
* Provides location-based features for connecting plant owners
*/
import { PlantData, PlantLocation } from '../blockchain/types';
export interface PlantCluster {
centerLat: number;
centerLon: number;
plantCount: number;
plants: PlantData[];
radius: number; // in km
dominantSpecies: string[];
}
export interface ConnectionSuggestion {
plant1: PlantData;
plant2: PlantData;
distance: number;
matchReason: string; // e.g., "same species", "same lineage", "nearby location"
compatibilityScore: number; // 0-100
}
export class GeolocationService {
/**
* Calculate distance between two coordinates using Haversine formula
*/
calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371; // Earth's radius in km
const dLat = this.toRadians(lat2 - lat1);
const dLon = this.toRadians(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.toRadians(lat1)) *
Math.cos(this.toRadians(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private toRadians(degrees: number): number {
return degrees * (Math.PI / 180);
}
/**
* Find plant clusters in a region
* Groups nearby plants together to show hotspots of activity
*/
findPlantClusters(
plants: PlantData[],
clusterRadius: number = 10 // km
): PlantCluster[] {
const clusters: PlantCluster[] = [];
const processed = new Set<string>();
for (const plant of plants) {
if (processed.has(plant.id)) continue;
// Find all plants within cluster radius
const clusterPlants: PlantData[] = [plant];
processed.add(plant.id);
for (const otherPlant of plants) {
if (processed.has(otherPlant.id)) continue;
const distance = this.calculateDistance(
plant.location.latitude,
plant.location.longitude,
otherPlant.location.latitude,
otherPlant.location.longitude
);
if (distance <= clusterRadius) {
clusterPlants.push(otherPlant);
processed.add(otherPlant.id);
}
}
// Calculate cluster center (average of all positions)
const centerLat =
clusterPlants.reduce((sum, p) => sum + p.location.latitude, 0) /
clusterPlants.length;
const centerLon =
clusterPlants.reduce((sum, p) => sum + p.location.longitude, 0) /
clusterPlants.length;
// Find dominant species
const speciesCount: { [key: string]: number } = {};
for (const p of clusterPlants) {
if (p.scientificName) {
speciesCount[p.scientificName] =
(speciesCount[p.scientificName] || 0) + 1;
}
}
const dominantSpecies = Object.entries(speciesCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
.map(([species]) => species);
clusters.push({
centerLat,
centerLon,
plantCount: clusterPlants.length,
plants: clusterPlants,
radius: clusterRadius,
dominantSpecies,
});
}
return clusters.sort((a, b) => b.plantCount - a.plantCount);
}
/**
* Suggest connections between plant owners
* Finds compatible plants for sharing/trading
*/
suggestConnections(
userPlant: PlantData,
allPlants: PlantData[],
maxDistance: number = 50 // km
): ConnectionSuggestion[] {
const suggestions: ConnectionSuggestion[] = [];
for (const otherPlant of allPlants) {
// Skip if same owner
if (otherPlant.owner.id === userPlant.owner.id) continue;
// Skip if same plant
if (otherPlant.id === userPlant.id) continue;
const distance = this.calculateDistance(
userPlant.location.latitude,
userPlant.location.longitude,
otherPlant.location.latitude,
otherPlant.location.longitude
);
// Skip if too far
if (distance > maxDistance) continue;
let matchReason = '';
let compatibilityScore = 0;
// Check for same species
if (
userPlant.scientificName &&
userPlant.scientificName === otherPlant.scientificName
) {
matchReason = 'Same species';
compatibilityScore += 40;
}
// Check for same lineage
if (
userPlant.parentPlantId === otherPlant.parentPlantId &&
userPlant.parentPlantId
) {
matchReason =
matchReason + (matchReason ? ', ' : '') + 'Same parent plant';
compatibilityScore += 30;
}
// Check for same genus
if (userPlant.genus && userPlant.genus === otherPlant.genus) {
if (!matchReason) matchReason = 'Same genus';
compatibilityScore += 20;
}
// Proximity bonus
const proximityScore = Math.max(0, 20 - distance / 2.5);
compatibilityScore += proximityScore;
// Only suggest if there's some compatibility
if (compatibilityScore > 20) {
if (!matchReason) matchReason = 'Nearby location';
suggestions.push({
plant1: userPlant,
plant2: otherPlant,
distance,
matchReason,
compatibilityScore: Math.min(100, compatibilityScore),
});
}
}
return suggestions.sort((a, b) => b.compatibilityScore - a.compatibilityScore);
}
/**
* Get address from coordinates using reverse geocoding
* Note: In production, you'd use a service like Google Maps or OpenStreetMap
*/
async reverseGeocode(
latitude: number,
longitude: number
): Promise<Partial<PlantLocation>> {
try {
// Using OpenStreetMap Nominatim (free, but rate-limited)
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`,
{
headers: {
'User-Agent': 'LocalGreenChain/1.0',
},
}
);
if (!response.ok) {
console.error('Reverse geocoding error:', response.statusText);
return { latitude, longitude };
}
const data = await response.json();
return {
latitude,
longitude,
address: data.display_name,
city: data.address?.city || data.address?.town || data.address?.village,
country: data.address?.country,
};
} catch (error) {
console.error('Error in reverse geocoding:', error);
return { latitude, longitude };
}
}
/**
* Get coordinates from address using forward geocoding
*/
async geocode(
address: string
): Promise<{ latitude: number; longitude: number } | null> {
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`,
{
headers: {
'User-Agent': 'LocalGreenChain/1.0',
},
}
);
if (!response.ok) {
console.error('Geocoding error:', response.statusText);
return null;
}
const data = await response.json();
if (data.length === 0) return null;
return {
latitude: parseFloat(data[0].lat),
longitude: parseFloat(data[0].lon),
};
} catch (error) {
console.error('Error in geocoding:', error);
return null;
}
}
/**
* Check if a location is within a given boundary
*/
isWithinBounds(
location: { latitude: number; longitude: number },
bounds: {
north: number;
south: number;
east: number;
west: number;
}
): boolean {
return (
location.latitude <= bounds.north &&
location.latitude >= bounds.south &&
location.longitude <= bounds.east &&
location.longitude >= bounds.west
);
}
}
// Singleton instance
let geolocationService: GeolocationService | null = null;
export function getGeolocationService(): GeolocationService {
if (!geolocationService) {
geolocationService = new GeolocationService();
}
return geolocationService;
}

225
lib/services/plantsnet.ts Normal file
View file

@ -0,0 +1,225 @@
/**
* 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;
}

172
lib/services/tor.ts Normal file
View file

@ -0,0 +1,172 @@
/**
* Tor Integration Service
* Provides Tor network connectivity for anonymous plant sharing
*/
import { SocksProxyAgent } from 'socks-proxy-agent';
export interface TorConfig {
enabled: boolean;
socksHost: string;
socksPort: number;
controlPort: number;
hiddenServiceDir?: string;
}
export class TorService {
private config: TorConfig;
private proxyAgent: any;
constructor(config?: Partial<TorConfig>) {
this.config = {
enabled: process.env.TOR_ENABLED === 'true',
socksHost: process.env.TOR_SOCKS_HOST || '127.0.0.1',
socksPort: parseInt(process.env.TOR_SOCKS_PORT || '9050'),
controlPort: parseInt(process.env.TOR_CONTROL_PORT || '9051'),
hiddenServiceDir: process.env.TOR_HIDDEN_SERVICE_DIR,
...config,
};
if (this.config.enabled) {
this.initializeProxy();
}
}
/**
* Initialize SOCKS proxy for Tor
*/
private initializeProxy(): void {
const proxyUrl = `socks5://${this.config.socksHost}:${this.config.socksPort}`;
this.proxyAgent = new SocksProxyAgent(proxyUrl);
}
/**
* Check if Tor is available and running
*/
async isAvailable(): Promise<boolean> {
if (!this.config.enabled) return false;
try {
// Try to fetch check.torproject.org through Tor
const response = await fetch('https://check.torproject.org/api/ip', {
// @ts-ignore
agent: this.proxyAgent,
signal: AbortSignal.timeout(5000),
});
const data = await response.json();
return data.IsTor === true;
} catch (error) {
console.error('Tor availability check failed:', error);
return false;
}
}
/**
* Fetch data through Tor network
*/
async fetchThroughTor(url: string, options: RequestInit = {}): Promise<Response> {
if (!this.config.enabled) {
throw new Error('Tor is not enabled');
}
return fetch(url, {
...options,
// @ts-ignore
agent: this.proxyAgent,
});
}
/**
* Get current Tor circuit info
*/
async getCircuitInfo(): Promise<{ country?: string; ip?: string }> {
try {
const response = await this.fetchThroughTor('https://check.torproject.org/api/ip');
return await response.json();
} catch (error) {
return {};
}
}
/**
* Request new Tor identity (new circuit)
*/
async requestNewIdentity(): Promise<boolean> {
// This would require Tor control port access
// For now, just return true if Tor is enabled
return this.config.enabled;
}
/**
* Get onion service hostname if available
*/
getOnionAddress(): string | null {
if (!this.config.hiddenServiceDir) return null;
try {
const fs = require('fs');
const path = require('path');
const hostnamePath = path.join(this.config.hiddenServiceDir, 'hostname');
if (fs.existsSync(hostnamePath)) {
return fs.readFileSync(hostnamePath, 'utf-8').trim();
}
} catch (error) {
console.error('Error reading onion address:', error);
}
return null;
}
/**
* Generate QR code for onion address
*/
async generateOnionQRCode(): Promise<string | null> {
const onionAddress = this.getOnionAddress();
if (!onionAddress) return null;
// In production, you'd use a QR code library
// For now, return the address
return `http://${onionAddress}`;
}
/**
* Check if request came through Tor
*/
isRequestFromTor(headers: any): boolean {
// Check various indicators that request came through Tor
return (
headers['x-tor-connection'] === 'true' ||
headers['x-forwarded-proto'] === 'tor' ||
false
);
}
}
// Singleton instance
let torService: TorService | null = null;
export function getTorService(): TorService {
if (!torService) {
torService = new TorService();
}
return torService;
}
/**
* Middleware to detect and mark Tor connections
*/
export function torDetectionMiddleware(req: any, res: any, next: any) {
const torService = getTorService();
// Mark request as coming from Tor if detected
req.isTorConnection = torService.isRequestFromTor(req.headers);
// Add Tor status to response headers
if (req.isTorConnection) {
res.setHeader('X-Tor-Enabled', 'true');
}
next();
}

View file

@ -1,17 +1,18 @@
{ {
"name": "example-marketing", "name": "localgreenchain",
"version": "1.5.0", "version": "1.0.0",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "next dev -p 3001", "dev": "next dev -p 3001",
"build": "next build", "build": "next build",
"preview": "next build && next start -p 3001", "start": "next start -p 3001",
"preview": "bun run build && bun run start",
"lint": "next lint", "lint": "next lint",
"cy:open": "cypress open", "cy:open": "cypress open",
"cy:run": "cypress run", "cy:run": "cypress run",
"test:e2e": "start-server-and-test 'yarn preview' http://localhost:3001 cy:open", "test:e2e": "start-server-and-test 'bun run preview' http://localhost:3001 cy:open",
"test:e2e:ci": "start-server-and-test 'yarn preview' http://localhost:3001 cy:run" "test:e2e:ci": "start-server-and-test 'bun run preview' http://localhost:3001 cy:run"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
@ -25,7 +26,8 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-hook-form": "^7.8.6" "react-hook-form": "^7.8.6",
"socks-proxy-agent": "^8.0.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.9", "@babel/core": "^7.12.9",

View file

@ -0,0 +1,95 @@
/**
* API Route: Analyze growth correlations across all plants
* GET /api/environment/analysis?species=tomato (optional species filter)
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
import { analyzeGrowthCorrelation } from '../../../lib/environment/analysis';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { species } = req.query;
const blockchain = getBlockchain();
// Get all plants
let allPlants = Array.from(
new Set(blockchain.chain.map(block => block.plant.id))
)
.map(id => blockchain.getPlant(id)!)
.filter(Boolean);
// Filter by species if specified
if (species && typeof species === 'string') {
allPlants = allPlants.filter(
p =>
p.scientificName?.toLowerCase().includes(species.toLowerCase()) ||
p.commonName?.toLowerCase().includes(species.toLowerCase())
);
}
// Only include plants with environmental data
const plantsWithEnv = allPlants.filter(p => p.environment);
if (plantsWithEnv.length === 0) {
return res.status(200).json({
success: true,
message: 'No plants with environmental data found',
plantsAnalyzed: 0,
});
}
const analysis = analyzeGrowthCorrelation(plantsWithEnv);
// Calculate some additional statistics
const successRate =
(plantsWithEnv.filter(
p => p.status === 'mature' || p.status === 'flowering' || p.status === 'fruiting'
).length /
plantsWithEnv.length) *
100;
const locationTypes = plantsWithEnv.reduce((acc, p) => {
const type = p.environment!.location.type;
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const soilTypes = plantsWithEnv.reduce((acc, p) => {
const type = p.environment!.soil.type;
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
res.status(200).json({
success: true,
plantsAnalyzed: plantsWithEnv.length,
successRate: Math.round(successRate),
insights: analysis.insights,
statistics: {
locationTypes,
soilTypes,
avgTemperature:
plantsWithEnv.reduce((sum, p) => sum + (p.environment!.climate.temperatureDay || 0), 0) /
plantsWithEnv.length,
avgHumidity:
plantsWithEnv.reduce((sum, p) => sum + (p.environment!.climate.humidityAverage || 0), 0) /
plantsWithEnv.length,
avgSoilPH:
plantsWithEnv.reduce((sum, p) => sum + (p.environment!.soil.pH || 0), 0) /
plantsWithEnv.length,
},
});
} catch (error: any) {
console.error('Error analyzing growth correlation:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,80 @@
/**
* API Route: Compare growing environments of two plants
* GET /api/environment/compare?plant1=xyz&plant2=abc
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
import { compareEnvironments } from '../../../lib/environment/analysis';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { plant1, plant2 } = req.query;
if (!plant1 || !plant2 || typeof plant1 !== 'string' || typeof plant2 !== 'string') {
return res.status(400).json({ error: 'Missing plant1 and plant2 parameters' });
}
const blockchain = getBlockchain();
const plantData1 = blockchain.getPlant(plant1);
const plantData2 = blockchain.getPlant(plant2);
if (!plantData1 || !plantData2) {
return res.status(404).json({ error: 'One or both plants not found' });
}
if (!plantData1.environment || !plantData2.environment) {
return res.status(400).json({
error: 'Both plants must have environmental data',
plant1HasData: !!plantData1.environment,
plant2HasData: !!plantData2.environment,
});
}
const comparison = compareEnvironments(plantData1.environment, plantData2.environment);
comparison.plant1 = plant1;
comparison.plant2 = plant2;
res.status(200).json({
success: true,
plants: {
plant1: {
id: plantData1.id,
commonName: plantData1.commonName,
scientificName: plantData1.scientificName,
owner: plantData1.owner.name,
},
plant2: {
id: plantData2.id,
commonName: plantData2.commonName,
scientificName: plantData2.scientificName,
owner: plantData2.owner.name,
},
},
comparison: {
similarityScore: comparison.score,
similarities: comparison.similarities,
differences: comparison.differences,
},
interpretation: getScoreInterpretation(comparison.score),
});
} catch (error: any) {
console.error('Error comparing environments:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}
function getScoreInterpretation(score: number): string {
if (score >= 90) return 'Nearly identical growing conditions';
if (score >= 75) return 'Very similar environments - likely to have similar results';
if (score >= 60) return 'Moderately similar conditions';
if (score >= 40) return 'Some similarities but notable differences';
return 'Very different growing environments';
}

View file

@ -0,0 +1,61 @@
/**
* API Route: Get environmental recommendations for a plant
* GET /api/environment/recommendations?plantId=xyz
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
import {
generateRecommendations,
calculateEnvironmentalHealth,
} from '../../../lib/environment/analysis';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { plantId } = req.query;
if (!plantId || typeof plantId !== 'string') {
return res.status(400).json({ error: 'Missing plantId parameter' });
}
const blockchain = getBlockchain();
const plant = blockchain.getPlant(plantId);
if (!plant) {
return res.status(404).json({ error: 'Plant not found' });
}
const recommendations = generateRecommendations(plant);
const healthScore = plant.environment
? calculateEnvironmentalHealth(plant.environment)
: null;
res.status(200).json({
success: true,
plant: {
id: plant.id,
commonName: plant.commonName,
scientificName: plant.scientificName,
},
environmentalHealth: healthScore,
recommendations,
summary: {
criticalIssues: recommendations.filter(r => r.priority === 'critical').length,
highPriority: recommendations.filter(r => r.priority === 'high').length,
mediumPriority: recommendations.filter(r => r.priority === 'medium').length,
lowPriority: recommendations.filter(r => r.priority === 'low').length,
},
});
} catch (error: any) {
console.error('Error generating recommendations:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,74 @@
/**
* API Route: Find plants with similar growing environments
* GET /api/environment/similar?plantId=xyz&minScore=70
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
import { findSimilarEnvironments } from '../../../lib/environment/analysis';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { plantId, minScore } = req.query;
if (!plantId || typeof plantId !== 'string') {
return res.status(400).json({ error: 'Missing plantId parameter' });
}
const blockchain = getBlockchain();
const targetPlant = blockchain.getPlant(plantId);
if (!targetPlant) {
return res.status(404).json({ error: 'Plant not found' });
}
if (!targetPlant.environment) {
return res.status(400).json({
error: 'Plant has no environmental data',
message: 'Add environmental information to find similar plants',
});
}
// Get all plants
const allPlants = Array.from(
new Set(blockchain.chain.map(block => block.plant.id))
)
.map(id => blockchain.getPlant(id)!)
.filter(Boolean);
const minScoreValue = minScore ? parseInt(minScore as string) : 70;
const similarPlants = findSimilarEnvironments(targetPlant, allPlants, minScoreValue);
res.status(200).json({
success: true,
targetPlant: {
id: targetPlant.id,
commonName: targetPlant.commonName,
scientificName: targetPlant.scientificName,
},
similarCount: similarPlants.length,
similar: similarPlants.map(({ plant, comparison }) => ({
plant: {
id: plant.id,
commonName: plant.commonName,
scientificName: plant.scientificName,
owner: plant.owner.name,
location: plant.location.city || plant.location.country,
},
similarityScore: comparison.score,
similarities: comparison.similarities,
differences: comparison.differences,
})),
});
} catch (error: any) {
console.error('Error finding similar environments:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

59
pages/api/plants/[id].ts Normal file
View file

@ -0,0 +1,59 @@
/**
* API Route: Get plant details by ID
* GET /api/plants/[id]
* PUT /api/plants/[id] - Update plant status
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { id } = req.query;
if (!id || typeof id !== 'string') {
return res.status(400).json({ error: 'Invalid plant ID' });
}
const blockchain = getBlockchain();
if (req.method === 'GET') {
try {
const plant = blockchain.getPlant(id);
if (!plant) {
return res.status(404).json({ error: 'Plant not found' });
}
res.status(200).json({
success: true,
plant,
});
} catch (error: any) {
console.error('Error fetching plant:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
} else if (req.method === 'PUT') {
try {
const updates = req.body;
const block = blockchain.updatePlantStatus(id, updates);
// Save blockchain
saveBlockchain();
res.status(200).json({
success: true,
plant: block.plant,
message: 'Plant updated successfully',
});
} catch (error: any) {
console.error('Error updating plant:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}

70
pages/api/plants/clone.ts Normal file
View file

@ -0,0 +1,70 @@
/**
* API Route: Clone a plant (create offspring)
* POST /api/plants/clone
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager';
import { PlantData } from '../../../lib/blockchain/types';
interface CloneRequest {
parentPlantId: string;
propagationType: 'seed' | 'clone' | 'cutting' | 'division' | 'grafting';
newPlant: Partial<PlantData>;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { parentPlantId, propagationType, newPlant }: CloneRequest = req.body;
// Validate required fields
if (!parentPlantId || !propagationType || !newPlant) {
return res.status(400).json({
error: 'Missing required fields: parentPlantId, propagationType, newPlant',
});
}
if (!newPlant.location || !newPlant.owner) {
return res.status(400).json({
error: 'New plant must have location and owner',
});
}
const blockchain = getBlockchain();
// Clone the plant
const block = blockchain.clonePlant(
parentPlantId,
newPlant,
propagationType
);
// Save blockchain
saveBlockchain();
// Get parent for context
const parent = blockchain.getPlant(parentPlantId);
res.status(201).json({
success: true,
plant: block.plant,
parent: parent,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
},
message: `Successfully created ${propagationType} from parent plant`,
});
} catch (error: any) {
console.error('Error cloning plant:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,61 @@
/**
* API Route: Get connection suggestions for a plant
* GET /api/plants/connections?plantId=xyz&maxDistance=50
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
import { getGeolocationService } from '../../../lib/services/geolocation';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { plantId, maxDistance } = req.query;
if (!plantId || typeof plantId !== 'string') {
return res.status(400).json({ error: 'Missing plantId parameter' });
}
const blockchain = getBlockchain();
const plant = blockchain.getPlant(plantId);
if (!plant) {
return res.status(404).json({ error: 'Plant not found' });
}
const maxDistanceKm = maxDistance ? parseFloat(maxDistance as string) : 50;
// Get all plants
const allPlants = Array.from(
new Set(blockchain.chain.map(block => block.plant.id))
).map(id => blockchain.getPlant(id)!).filter(Boolean);
// Get connection suggestions
const geoService = getGeolocationService();
const suggestions = geoService.suggestConnections(
plant,
allPlants,
maxDistanceKm
);
res.status(200).json({
success: true,
plant: {
id: plant.id,
commonName: plant.commonName,
owner: plant.owner.name,
},
suggestionCount: suggestions.length,
suggestions: suggestions.slice(0, 20), // Limit to top 20
});
} catch (error: any) {
console.error('Error getting connection suggestions:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,45 @@
/**
* API Route: Get plant lineage (ancestors and descendants)
* GET /api/plants/lineage/[id]
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../../lib/blockchain/manager';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { id } = req.query;
if (!id || typeof id !== 'string') {
return res.status(400).json({ error: 'Invalid plant ID' });
}
const blockchain = getBlockchain();
const lineage = blockchain.getPlantLineage(id);
if (!lineage) {
return res.status(404).json({ error: 'Plant not found' });
}
res.status(200).json({
success: true,
lineage,
stats: {
ancestorCount: lineage.ancestors.length,
descendantCount: lineage.descendants.length,
siblingCount: lineage.siblings.length,
generation: lineage.generation,
},
});
} catch (error: any) {
console.error('Error fetching plant lineage:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,68 @@
/**
* API Route: Find nearby plants
* GET /api/plants/nearby?lat=123&lon=456&radius=50
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
import { getGeolocationService } from '../../../lib/services/geolocation';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { lat, lon, radius, species } = req.query;
if (!lat || !lon) {
return res.status(400).json({
error: 'Missing required parameters: lat, lon',
});
}
const latitude = parseFloat(lat as string);
const longitude = parseFloat(lon as string);
const radiusKm = radius ? parseFloat(radius as string) : 50;
if (isNaN(latitude) || isNaN(longitude) || isNaN(radiusKm)) {
return res.status(400).json({
error: 'Invalid parameters: lat, lon, and radius must be numbers',
});
}
const blockchain = getBlockchain();
let nearbyPlants = blockchain.findNearbyPlants(latitude, longitude, radiusKm);
// Filter by species if provided
if (species && typeof species === 'string') {
nearbyPlants = nearbyPlants.filter(
np => np.plant.scientificName === species || np.plant.commonName === species
);
}
// Get plant clusters
const geoService = getGeolocationService();
const allNearbyPlantData = nearbyPlants.map(np => np.plant);
const clusters = geoService.findPlantClusters(allNearbyPlantData, 10);
res.status(200).json({
success: true,
count: nearbyPlants.length,
plants: nearbyPlants,
clusters,
searchParams: {
latitude,
longitude,
radiusKm,
species: species || null,
},
});
} catch (error: any) {
console.error('Error finding nearby plants:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,38 @@
/**
* API Route: Get network statistics
* GET /api/plants/network
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const blockchain = getBlockchain();
const networkStats = blockchain.getNetworkStats();
// Calculate additional metrics
const chainLength = blockchain.chain.length;
const isValid = blockchain.isChainValid();
res.status(200).json({
success: true,
network: networkStats,
blockchain: {
totalBlocks: chainLength,
isValid,
difficulty: blockchain.difficulty,
},
});
} catch (error: any) {
console.error('Error fetching network stats:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,156 @@
/**
* API Route: Register an anonymous plant
* POST /api/plants/register-anonymous
*
* This endpoint allows for privacy-preserving plant registration
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager';
import { PlantData } from '../../../lib/blockchain/types';
import {
generateAnonymousId,
generateWalletAddress,
obfuscateLocation,
generateAnonymousPlantName,
createAnonymousContact,
PrivacySettings,
} from '../../../lib/privacy/anonymity';
import { getTorService } from '../../../lib/services/tor';
interface AnonymousPlantRequest {
commonName: string;
scientificName?: string;
species?: string;
genus?: string;
family?: string;
location: {
latitude: number;
longitude: number;
};
privacySettings: PrivacySettings;
pseudonym?: string;
encryptionKey?: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const requestData: AnonymousPlantRequest = req.body;
// Validate required fields
if (!requestData.commonName || !requestData.location || !requestData.privacySettings) {
return res.status(400).json({
error: 'Missing required fields: commonName, location, privacySettings',
});
}
const { location, privacySettings } = requestData;
// Check if request came through Tor
const torService = getTorService();
const isTorConnection = torService.isRequestFromTor(req.headers);
// Generate anonymous identifiers
const anonymousUserId = generateAnonymousId();
const walletAddress = generateWalletAddress();
const plantId = `plant-${generateAnonymousId()}`;
// Obfuscate location based on privacy settings
const fuzzyLocation = obfuscateLocation(
location.latitude,
location.longitude,
privacySettings.locationPrivacy
);
// Determine display name based on privacy settings
let displayName: string;
if (privacySettings.identityPrivacy === 'anonymous') {
displayName = 'Anonymous Grower';
} else if (privacySettings.identityPrivacy === 'pseudonym' && requestData.pseudonym) {
displayName = requestData.pseudonym;
} else {
displayName = `Grower-${anonymousUserId.substring(0, 8)}`;
}
// Create anonymous contact
const anonymousEmail = createAnonymousContact(
anonymousUserId,
requestData.encryptionKey || 'default-key'
);
// Build plant data with privacy protections
const plantData: PlantData = {
id: plantId,
commonName: privacySettings.sharePlantDetails
? requestData.commonName
: generateAnonymousPlantName(requestData.commonName, 0),
scientificName: privacySettings.sharePlantDetails ? requestData.scientificName : undefined,
species: privacySettings.sharePlantDetails ? requestData.species : undefined,
genus: privacySettings.sharePlantDetails ? requestData.genus : undefined,
family: privacySettings.sharePlantDetails ? requestData.family : undefined,
propagationType: 'original',
generation: 0,
plantedDate: new Date().toISOString(),
status: 'growing',
location: {
latitude: fuzzyLocation.latitude,
longitude: fuzzyLocation.longitude,
address: fuzzyLocation.displayName,
city: privacySettings.locationPrivacy !== 'hidden' ? 'Privacy Protected' : undefined,
country: privacySettings.locationPrivacy === 'country' ? 'Anonymous Region' : undefined,
},
owner: {
id: anonymousUserId,
name: displayName,
email: anonymousEmail,
walletAddress: walletAddress,
},
childPlants: [],
registeredAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notes: privacySettings.anonymousMode
? 'Registered anonymously via privacy mode'
: undefined,
};
const blockchain = getBlockchain();
// Register the plant
const block = blockchain.registerPlant(plantData);
// Save blockchain
saveBlockchain();
// Prepare response with privacy info
res.status(201).json({
success: true,
plant: block.plant,
privacy: {
anonymousId: anonymousUserId,
walletAddress: walletAddress,
locationAccuracy: fuzzyLocation.accuracy,
torConnection: isTorConnection,
privacyLevel: privacySettings.locationPrivacy,
},
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
},
message: 'Plant registered anonymously',
warning: !isTorConnection
? 'For maximum privacy, consider accessing through Tor network'
: null,
});
} catch (error: any) {
console.error('Error registering anonymous plant:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,69 @@
/**
* API Route: Register a new plant
* POST /api/plants/register
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager';
import { PlantData } from '../../../lib/blockchain/types';
import { getPlantsNetService } from '../../../lib/services/plantsnet';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const plantData: PlantData = req.body;
// Validate required fields
if (!plantData.id || !plantData.commonName || !plantData.owner || !plantData.location) {
return res.status(400).json({
error: 'Missing required fields: id, commonName, owner, location',
});
}
const blockchain = getBlockchain();
// Register the plant
const block = blockchain.registerPlant(plantData);
// Optionally report to plants.net
if (process.env.PLANTS_NET_API_KEY) {
const plantsNet = getPlantsNetService();
const result = await plantsNet.reportPlantToNetwork({
commonName: plantData.commonName,
scientificName: plantData.scientificName,
location: plantData.location,
ownerId: plantData.owner.id,
propagationType: plantData.propagationType,
});
if (result.success && result.plantsNetId) {
// Update plant with plants.net ID
blockchain.updatePlantStatus(plantData.id, {
plantsNetId: result.plantsNetId,
});
}
}
// Save blockchain
saveBlockchain();
res.status(201).json({
success: true,
plant: block.plant,
block: {
index: block.index,
hash: block.hash,
timestamp: block.timestamp,
},
});
} catch (error: any) {
console.error('Error registering plant:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,87 @@
/**
* API Route: Search for plants
* GET /api/plants/search?q=tomato&type=species
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getBlockchain } from '../../../lib/blockchain/manager';
import { getPlantsNetService } from '../../../lib/services/plantsnet';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { q, type } = req.query;
if (!q || typeof q !== 'string') {
return res.status(400).json({ error: 'Missing search query parameter: q' });
}
const blockchain = getBlockchain();
const searchTerm = q.toLowerCase();
// Search in blockchain
const allPlants = Array.from(
new Set(blockchain.chain.map(block => block.plant.id))
).map(id => blockchain.getPlant(id)!);
let results = allPlants.filter(plant => {
if (!plant) return false;
const searchIn = [
plant.commonName?.toLowerCase(),
plant.scientificName?.toLowerCase(),
plant.genus?.toLowerCase(),
plant.family?.toLowerCase(),
plant.owner.name?.toLowerCase(),
].filter(Boolean);
return searchIn.some(field => field?.includes(searchTerm));
});
// Filter by type if specified
if (type) {
switch (type) {
case 'species':
results = results.filter(p =>
p.scientificName?.toLowerCase().includes(searchTerm)
);
break;
case 'owner':
results = results.filter(p =>
p.owner.name?.toLowerCase().includes(searchTerm)
);
break;
case 'location':
results = results.filter(
p =>
p.location.city?.toLowerCase().includes(searchTerm) ||
p.location.country?.toLowerCase().includes(searchTerm)
);
break;
}
}
// Also search plants.net if API key is available
let plantsNetResults = [];
if (process.env.PLANTS_NET_API_KEY) {
const plantsNet = getPlantsNetService();
plantsNetResults = await plantsNet.searchPlant(q);
}
res.status(200).json({
success: true,
count: results.length,
results: results,
plantsNetResults: plantsNetResults,
});
} catch (error: any) {
console.error('Error searching plants:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

View file

@ -0,0 +1,70 @@
/**
* API Route: Check Tor status
* GET /api/privacy/tor-status
*/
import type { NextApiRequest, NextApiResponse } from 'next';
import { getTorService } from '../../../lib/services/tor';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const torService = getTorService();
// Check if Tor is enabled in configuration
const isEnabled = process.env.TOR_ENABLED === 'true';
// Check if request came through Tor
const isTorConnection = torService.isRequestFromTor(req.headers);
let isAvailable = false;
let circuitInfo = null;
let onionAddress = null;
if (isEnabled) {
// Check if Tor daemon is available
try {
isAvailable = await torService.isAvailable();
if (isAvailable) {
circuitInfo = await torService.getCircuitInfo();
onionAddress = torService.getOnionAddress();
}
} catch (error) {
console.error('Error checking Tor availability:', error);
}
}
res.status(200).json({
success: true,
tor: {
enabled: isEnabled,
available: isAvailable,
connectionThroughTor: isTorConnection,
onionAddress: onionAddress,
circuit: circuitInfo,
},
privacy: {
recommendTor: !isTorConnection,
privacyLevel: isTorConnection ? 'high' : 'standard',
ip: isTorConnection ? 'Hidden via Tor' : 'Visible',
},
recommendations: isTorConnection
? ['Your connection is private via Tor', 'Anonymous plant registration available']
: [
'For maximum privacy, access via Tor Browser',
'Download Tor from https://www.torproject.org',
`Or connect to our onion service: ${onionAddress || 'Not available'}`,
],
});
} catch (error: any) {
console.error('Error checking Tor status:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
}

281
pages/index.tsx Normal file
View file

@ -0,0 +1,281 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import Head from 'next/head';
interface NetworkStats {
totalPlants: number;
totalOwners: number;
species: { [key: string]: number };
globalDistribution: { [key: string]: number };
}
export default function Home() {
const [stats, setStats] = useState<NetworkStats | null>(null);
const [blockchainInfo, setBlockchainInfo] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchNetworkStats();
}, []);
const fetchNetworkStats = async () => {
try {
const response = await fetch('/api/plants/network');
const data = await response.json();
if (data.success) {
setStats(data.network);
setBlockchainInfo(data.blockchain);
}
} catch (error) {
console.error('Error fetching network stats:', error);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<Head>
<title>LocalGreenChain - Plant Cloning Blockchain</title>
<meta
name="description"
content="Track plant lineage and connect with growers worldwide"
/>
</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">
<div>
<h1 className="text-3xl font-bold text-green-800">
🌱 LocalGreenChain
</h1>
<p className="mt-1 text-sm text-gray-600">
Plant Cloning Blockchain Network
</p>
</div>
<nav className="flex gap-4">
<Link href="/plants/register">
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
Register Plant
</a>
</Link>
<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>
{/* Hero Section */}
<main className="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold text-gray-900 mb-4">
Track Your Plant's Journey
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
A blockchain for plants that preserves lineage across clones and
seeds. Connect with growers, share plants, and build a global green
network.
</p>
</div>
{/* Network Stats */}
{loading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading network stats...</p>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-12">
<StatCard
title="Total Plants"
value={stats?.totalPlants || 0}
icon="🌿"
color="green"
/>
<StatCard
title="Growers"
value={stats?.totalOwners || 0}
icon="👥"
color="blue"
/>
<StatCard
title="Species"
value={Object.keys(stats?.species || {}).length}
icon="🌺"
color="purple"
/>
<StatCard
title="Blockchain Blocks"
value={blockchainInfo?.totalBlocks || 0}
icon="⛓️"
color="orange"
/>
</div>
{/* Features */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
<FeatureCard
title="Track Lineage"
description="Every clone, cutting, and seed is recorded on the blockchain, preserving your plant's family tree forever."
icon="🌳"
/>
<FeatureCard
title="Connect Locally"
description="Find growers near you with similar plants. Share clones, trade seeds, and build your local plant community."
icon="📍"
/>
<FeatureCard
title="Global Network"
description="Join a worldwide network of plant enthusiasts. Track how your plants spread across the globe."
icon="🌍"
/>
</div>
{/* Top Species */}
{stats && Object.keys(stats.species).length > 0 && (
<div className="bg-white rounded-lg shadow-lg p-6 mb-12">
<h3 className="text-2xl font-bold text-gray-900 mb-4">
Popular Species
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(stats.species)
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([species, count]) => (
<div
key={species}
className="flex justify-between items-center p-3 bg-gray-50 rounded-lg"
>
<span className="font-medium text-gray-800">
{species}
</span>
<span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-semibold">
{count}
</span>
</div>
))}
</div>
</div>
)}
{/* Blockchain Status */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-2xl font-bold text-gray-900 mb-4">
Blockchain Status
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<p className="text-gray-600 text-sm">Status</p>
<p className="text-lg font-semibold">
{blockchainInfo?.isValid ? (
<span className="text-green-600"> Valid</span>
) : (
<span className="text-red-600"> Invalid</span>
)}
</p>
</div>
<div>
<p className="text-gray-600 text-sm">Mining Difficulty</p>
<p className="text-lg font-semibold text-gray-900">
{blockchainInfo?.difficulty || 'N/A'}
</p>
</div>
<div>
<p className="text-gray-600 text-sm">Total Blocks</p>
<p className="text-lg font-semibold text-gray-900">
{blockchainInfo?.totalBlocks || 0}
</p>
</div>
</div>
</div>
</>
)}
{/* CTA Section */}
<div className="mt-12 bg-gradient-to-r from-green-600 to-emerald-600 rounded-lg shadow-xl p-8 text-center text-white">
<h3 className="text-3xl font-bold mb-4">Ready to Get Started?</h3>
<p className="text-lg mb-6 opacity-90">
Register your first plant and join the global green blockchain
network.
</p>
<Link href="/plants/register">
<a className="inline-block px-8 py-3 bg-white text-green-600 font-semibold rounded-lg hover:bg-gray-100 transition">
Register Your First Plant
</a>
</Link>
</div>
</main>
{/* Footer */}
<footer className="mt-20 bg-white border-t border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
<p className="text-center text-gray-600">
© 2025 LocalGreenChain. Powered by blockchain technology. 🌱
</p>
</div>
</footer>
</div>
);
}
// Helper Components
function StatCard({
title,
value,
icon,
color,
}: {
title: string;
value: number;
icon: string;
color: string;
}) {
const colorClasses = {
green: 'bg-green-100 text-green-800',
blue: 'bg-blue-100 text-blue-800',
purple: 'bg-purple-100 text-purple-800',
orange: 'bg-orange-100 text-orange-800',
};
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-gray-600 text-sm">{title}</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{value}</p>
</div>
<div
className={`text-4xl ${colorClasses[color as keyof typeof colorClasses]
} w-16 h-16 rounded-full flex items-center justify-center`}
>
{icon}
</div>
</div>
</div>
);
}
function FeatureCard({
title,
description,
icon,
}: {
title: string;
description: string;
icon: string;
}) {
return (
<div className="bg-white rounded-lg shadow-lg p-6 text-center">
<div className="text-5xl mb-4">{icon}</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">{title}</h3>
<p className="text-gray-600">{description}</p>
</div>
);
}

491
pages/plants/[id].tsx Normal file
View file

@ -0,0 +1,491 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Head from 'next/head';
import EnvironmentalDisplay from '../../components/EnvironmentalDisplay';
import { GrowingEnvironment } from '../../lib/environment/types';
interface Plant {
id: string;
commonName: string;
scientificName?: string;
species?: string;
genus?: string;
family?: string;
parentPlantId?: string;
propagationType?: string;
generation: number;
plantedDate: string;
status: string;
location: {
latitude: number;
longitude: number;
city?: string;
country?: string;
address?: string;
};
owner: {
id: string;
name: string;
email: string;
};
childPlants: string[];
environment?: GrowingEnvironment;
notes?: string;
registeredAt: string;
updatedAt: string;
}
interface Lineage {
plantId: string;
ancestors: Plant[];
descendants: Plant[];
siblings: Plant[];
generation: number;
}
export default function PlantDetail() {
const router = useRouter();
const { id } = router.query;
const [plant, setPlant] = useState<Plant | null>(null);
const [lineage, setLineage] = useState<Lineage | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [activeTab, setActiveTab] = useState<'details' | 'lineage' | 'environment'>('details');
useEffect(() => {
if (id) {
fetchPlantData();
}
}, [id]);
const fetchPlantData = async () => {
try {
const [plantResponse, lineageResponse] = await Promise.all([
fetch(`/api/plants/${id}`),
fetch(`/api/plants/lineage/${id}`),
]);
const plantData = await plantResponse.json();
const lineageData = await lineageResponse.json();
if (!plantResponse.ok) {
throw new Error(plantData.error || 'Failed to fetch plant');
}
setPlant(plantData.plant);
if (lineageResponse.ok) {
setLineage(lineageData.lineage);
}
} catch (err: any) {
setError(err.message || 'An error occurred');
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-green-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading plant data...</p>
</div>
</div>
);
}
if (error || !plant) {
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">{error || 'Plant not found'}</p>
<Link href="/">
<a className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
Go Home
</a>
</Link>
</div>
</div>
);
}
const statusColors: { [key: string]: string } = {
sprouted: 'bg-yellow-100 text-yellow-800',
growing: 'bg-green-100 text-green-800',
mature: 'bg-blue-100 text-blue-800',
flowering: 'bg-purple-100 text-purple-800',
fruiting: 'bg-orange-100 text-orange-800',
dormant: 'bg-gray-100 text-gray-800',
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<Head>
<title>
{plant.commonName} - 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
</a>
</Link>
<Link href={`/plants/clone?parentId=${plant.id}`}>
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
Clone This Plant
</a>
</Link>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
{/* Plant Header */}
<div className="bg-white rounded-lg shadow-xl p-8 mb-6">
<div className="flex justify-between items-start mb-4">
<div>
<h1 className="text-4xl font-bold text-gray-900 mb-2">
{plant.commonName}
</h1>
{plant.scientificName && (
<p className="text-xl italic text-gray-600">
{plant.scientificName}
</p>
)}
</div>
<span
className={`px-4 py-2 rounded-full text-sm font-semibold ${
statusColors[plant.status] || 'bg-gray-100 text-gray-800'
}`}
>
{plant.status}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
<div className="bg-green-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Generation</p>
<p className="text-2xl font-bold text-green-800">
{plant.generation}
</p>
</div>
<div className="bg-blue-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Descendants</p>
<p className="text-2xl font-bold text-blue-800">
{plant.childPlants.length}
</p>
</div>
<div className="bg-purple-50 rounded-lg p-4">
<p className="text-sm text-gray-600">Propagation Type</p>
<p className="text-2xl font-bold text-purple-800 capitalize">
{plant.propagationType || 'Original'}
</p>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex gap-4 mb-6">
<button
onClick={() => setActiveTab('details')}
className={`px-6 py-3 rounded-lg font-semibold transition ${
activeTab === 'details'
? 'bg-green-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100'
}`}
>
📋 Details
</button>
<button
onClick={() => setActiveTab('lineage')}
className={`px-6 py-3 rounded-lg font-semibold transition ${
activeTab === 'lineage'
? 'bg-green-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100'
}`}
>
🌳 Family Tree
</button>
<button
onClick={() => setActiveTab('environment')}
className={`px-6 py-3 rounded-lg font-semibold transition ${
activeTab === 'environment'
? 'bg-green-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100'
}`}
>
🌍 Environment
{!plant.environment && (
<span className="ml-2 px-2 py-0.5 bg-yellow-500 text-white text-xs rounded-full">
Add
</span>
)}
</button>
</div>
{/* Details Tab */}
{activeTab === 'details' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Plant Information */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Plant Information
</h2>
<dl className="space-y-3">
<InfoRow label="Common Name" value={plant.commonName} />
{plant.scientificName && (
<InfoRow label="Scientific Name" value={plant.scientificName} />
)}
{plant.genus && <InfoRow label="Genus" value={plant.genus} />}
{plant.family && <InfoRow label="Family" value={plant.family} />}
<InfoRow
label="Planted Date"
value={new Date(plant.plantedDate).toLocaleDateString()}
/>
<InfoRow label="Status" value={plant.status} />
<InfoRow
label="Registered"
value={new Date(plant.registeredAt).toLocaleDateString()}
/>
</dl>
{plant.notes && (
<div className="mt-6">
<h3 className="font-semibold text-gray-900 mb-2">Notes</h3>
<p className="text-gray-700 bg-gray-50 p-3 rounded-lg">
{plant.notes}
</p>
</div>
)}
</div>
{/* Location & Owner */}
<div className="space-y-6">
{/* Owner */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Owner
</h2>
<dl className="space-y-3">
<InfoRow label="Name" value={plant.owner.name} />
<InfoRow label="Email" value={plant.owner.email} />
</dl>
</div>
{/* Location */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Location
</h2>
<dl className="space-y-3">
{plant.location.city && (
<InfoRow label="City" value={plant.location.city} />
)}
{plant.location.country && (
<InfoRow label="Country" value={plant.location.country} />
)}
<InfoRow
label="Coordinates"
value={`${plant.location.latitude.toFixed(4)}, ${plant.location.longitude.toFixed(4)}`}
/>
</dl>
</div>
</div>
</div>
)}
{/* Lineage Tab */}
{activeTab === 'lineage' && lineage && (
<div className="space-y-6">
{/* Ancestors */}
{lineage.ancestors.length > 0 && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
🌲 Ancestors ({lineage.ancestors.length})
</h2>
<div className="space-y-3">
{lineage.ancestors.map((ancestor, idx) => (
<PlantLineageCard
key={ancestor.id}
plant={ancestor}
label={`Generation ${ancestor.generation}`}
/>
))}
</div>
</div>
)}
{/* Siblings */}
{lineage.siblings.length > 0 && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
👥 Siblings ({lineage.siblings.length})
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{lineage.siblings.map((sibling) => (
<PlantLineageCard key={sibling.id} plant={sibling} />
))}
</div>
</div>
)}
{/* Descendants */}
{lineage.descendants.length > 0 && (
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
🌱 Descendants ({lineage.descendants.length})
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{lineage.descendants.map((descendant) => (
<PlantLineageCard
key={descendant.id}
plant={descendant}
label={`Gen ${descendant.generation} (${descendant.propagationType})`}
/>
))}
</div>
</div>
)}
{lineage.ancestors.length === 0 &&
lineage.siblings.length === 0 &&
lineage.descendants.length === 0 && (
<div className="bg-white rounded-lg shadow-lg p-12 text-center">
<p className="text-gray-600 text-lg">
This plant has no recorded lineage yet.
<br />
<Link href={`/plants/clone?parentId=${plant.id}`}>
<a className="text-green-600 hover:underline font-semibold">
Create a clone to start building the family tree!
</a>
</Link>
</p>
</div>
)}
</div>
)}
{/* Environment Tab */}
{activeTab === 'environment' && (
<div>
{plant.environment ? (
<EnvironmentalDisplay
environment={plant.environment}
plantId={plant.id}
showRecommendations={true}
/>
) : (
<div className="bg-white rounded-lg shadow-lg p-12 text-center">
<div className="text-6xl mb-4">🌍</div>
<h3 className="text-2xl font-bold text-gray-900 mb-2">
No Environmental Data Yet
</h3>
<p className="text-gray-600 text-lg mb-6">
Track soil, climate, nutrients, and growing conditions to:
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-4xl mx-auto mb-8">
<div className="bg-green-50 p-4 rounded-lg">
<div className="text-3xl mb-2">💡</div>
<h4 className="font-semibold text-gray-900 mb-1">Get Recommendations</h4>
<p className="text-sm text-gray-600">
Receive personalized advice to optimize conditions
</p>
</div>
<div className="bg-blue-50 p-4 rounded-lg">
<div className="text-3xl mb-2">🔍</div>
<h4 className="font-semibold text-gray-900 mb-1">Compare & Learn</h4>
<p className="text-sm text-gray-600">
Find what works for similar plants
</p>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="text-3xl mb-2">📈</div>
<h4 className="font-semibold text-gray-900 mb-1">Track Success</h4>
<p className="text-sm text-gray-600">
Monitor growth and health over time
</p>
</div>
</div>
<p className="text-sm text-gray-500 mb-6">
📘 Learn more in the{' '}
<a
href="https://github.com/yourusername/localgreenchain/blob/main/ENVIRONMENTAL_TRACKING.md"
target="_blank"
rel="noopener noreferrer"
className="text-green-600 hover:underline"
>
Environmental Tracking Guide
</a>
</p>
<button
onClick={() => alert('Environmental data editing coming soon! Use API for now.')}
className="px-6 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition"
>
Add Environmental Data
</button>
</div>
)}
</div>
)}
</main>
</div>
);
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-sm font-medium text-gray-600">{label}</dt>
<dd className="text-base text-gray-900 capitalize">{value}</dd>
</div>
);
}
function PlantLineageCard({
plant,
label,
}: {
plant: Plant;
label?: string;
}) {
return (
<Link href={`/plants/${plant.id}`}>
<a className="block p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition border border-gray-200">
<div className="flex justify-between items-start">
<div>
<h4 className="font-semibold text-gray-900">{plant.commonName}</h4>
{plant.scientificName && (
<p className="text-sm italic text-gray-600">
{plant.scientificName}
</p>
)}
<p className="text-sm text-gray-600 mt-1">
👤 {plant.owner.name}
</p>
</div>
{label && (
<span className="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-semibold">
{label}
</span>
)}
</div>
</a>
</Link>
);
}

430
pages/plants/clone.tsx Normal file
View file

@ -0,0 +1,430 @@
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>
);
}

333
pages/plants/explore.tsx Normal file
View file

@ -0,0 +1,333 @@
import { useState } from 'react';
import Link from 'next/link';
import Head from 'next/head';
interface Plant {
id: string;
commonName: string;
scientificName?: string;
owner: { name: string };
location: { city?: string; country?: string };
status: string;
generation: number;
}
interface NearbyPlant {
plant: Plant;
distance: number;
}
export default function ExplorePlants() {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<Plant[]>([]);
const [nearbyPlants, setNearbyPlants] = useState<NearbyPlant[]>([]);
const [loading, setLoading] = useState(false);
const [latitude, setLatitude] = useState('');
const [longitude, setLongitude] = useState('');
const [radius, setRadius] = useState('50');
const [activeTab, setActiveTab] = useState<'search' | 'nearby'>('search');
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch(
`/api/plants/search?q=${encodeURIComponent(searchTerm)}`
);
const data = await response.json();
if (data.success) {
setSearchResults(data.results);
}
} catch (error) {
console.error('Error searching plants:', error);
} finally {
setLoading(false);
}
};
const handleFindNearby = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch(
`/api/plants/nearby?lat=${latitude}&lon=${longitude}&radius=${radius}`
);
const data = await response.json();
if (data.success) {
setNearbyPlants(data.plants);
}
} catch (error) {
console.error('Error finding nearby plants:', error);
} finally {
setLoading(false);
}
};
const getCurrentLocation = () => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
setLatitude(position.coords.latitude.toString());
setLongitude(position.coords.longitude.toString());
},
(error) => {
console.error('Error getting location:', error);
}
);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<Head>
<title>Explore Plants - 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/register">
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
Register Plant
</a>
</Link>
</nav>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-gray-900 mb-8">
Explore the Plant Network
</h1>
{/* Tabs */}
<div className="flex gap-4 mb-6">
<button
onClick={() => setActiveTab('search')}
className={`px-6 py-3 rounded-lg font-semibold transition ${
activeTab === 'search'
? 'bg-green-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100'
}`}
>
🔍 Search Plants
</button>
<button
onClick={() => setActiveTab('nearby')}
className={`px-6 py-3 rounded-lg font-semibold transition ${
activeTab === 'nearby'
? 'bg-green-600 text-white'
: 'bg-white text-gray-700 hover:bg-gray-100'
}`}
>
📍 Find Nearby
</button>
</div>
{/* Search Tab */}
{activeTab === 'search' && (
<div className="bg-white rounded-lg shadow-lg p-6">
<form onSubmit={handleSearch} className="mb-6">
<div className="flex gap-4">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search by plant name, species, or owner..."
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
<button
type="submit"
disabled={loading}
className="px-8 py-3 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition disabled:opacity-50"
>
{loading ? 'Searching...' : 'Search'}
</button>
</div>
</form>
{searchResults.length > 0 && (
<div>
<h2 className="text-xl font-bold text-gray-900 mb-4">
Found {searchResults.length} plants
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{searchResults.map((plant) => (
<PlantCard key={plant.id} plant={plant} />
))}
</div>
</div>
)}
{searchResults.length === 0 && searchTerm && !loading && (
<p className="text-center text-gray-600 py-8">
No plants found. Try a different search term.
</p>
)}
</div>
)}
{/* Nearby Tab */}
{activeTab === 'nearby' && (
<div className="bg-white rounded-lg shadow-lg p-6">
<form onSubmit={handleFindNearby} className="mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Latitude
</label>
<input
type="number"
step="any"
value={latitude}
onChange={(e) => setLatitude(e.target.value)}
required
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"
value={longitude}
onChange={(e) => setLongitude(e.target.value)}
required
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">
Radius (km)
</label>
<input
type="number"
value={radius}
onChange={(e) => setRadius(e.target.value)}
required
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 className="flex gap-4">
<button
type="button"
onClick={getCurrentLocation}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
>
📍 Use My Location
</button>
<button
type="submit"
disabled={loading}
className="px-8 py-2 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition disabled:opacity-50"
>
{loading ? 'Finding...' : 'Find Nearby Plants'}
</button>
</div>
</form>
{nearbyPlants.length > 0 && (
<div>
<h2 className="text-xl font-bold text-gray-900 mb-4">
Found {nearbyPlants.length} nearby plants
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{nearbyPlants.map(({ plant, distance }) => (
<PlantCard
key={plant.id}
plant={plant}
distance={distance}
/>
))}
</div>
</div>
)}
{nearbyPlants.length === 0 && latitude && longitude && !loading && (
<p className="text-center text-gray-600 py-8">
No plants found nearby. Try increasing the search radius.
</p>
)}
</div>
)}
</main>
</div>
);
}
function PlantCard({
plant,
distance,
}: {
plant: Plant;
distance?: number;
}) {
const statusColors: { [key: string]: string } = {
sprouted: 'bg-yellow-100 text-yellow-800',
growing: 'bg-green-100 text-green-800',
mature: 'bg-blue-100 text-blue-800',
flowering: 'bg-purple-100 text-purple-800',
fruiting: 'bg-orange-100 text-orange-800',
dormant: 'bg-gray-100 text-gray-800',
};
return (
<Link href={`/plants/${plant.id}`}>
<a className="block bg-gray-50 rounded-lg p-4 hover:shadow-lg transition border border-gray-200">
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-bold text-gray-900">
{plant.commonName}
</h3>
<span
className={`px-2 py-1 rounded-full text-xs font-semibold ${
statusColors[plant.status] || 'bg-gray-100 text-gray-800'
}`}
>
{plant.status}
</span>
</div>
{plant.scientificName && (
<p className="text-sm italic text-gray-600 mb-2">
{plant.scientificName}
</p>
)}
<div className="space-y-1 text-sm text-gray-600">
<p>👤 {plant.owner.name}</p>
{(plant.location.city || plant.location.country) && (
<p>
📍{' '}
{[plant.location.city, plant.location.country]
.filter(Boolean)
.join(', ')}
</p>
)}
<p>🌱 Generation {plant.generation}</p>
{distance !== undefined && (
<p className="font-semibold text-green-600">
📏 {distance.toFixed(1)} km away
</p>
)}
</div>
</a>
</Link>
);
}

View file

@ -0,0 +1,360 @@
import { useState } from 'react';
import Link from 'next/link';
import Head from 'next/head';
import { useRouter } from 'next/router';
import PrivacySettings from '../../components/PrivacySettings';
import { PrivacySettings as IPrivacySettings } from '../../lib/privacy/anonymity';
export default function RegisterAnonymousPlant() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const [walletAddress, setWalletAddress] = useState('');
const [formData, setFormData] = useState({
commonName: '',
scientificName: '',
species: '',
genus: '',
family: '',
latitude: '',
longitude: '',
pseudonym: '',
encryptionKey: '',
});
const [privacySettings, setPrivacySettings] = useState<IPrivacySettings>({
anonymousMode: true,
locationPrivacy: 'fuzzy',
identityPrivacy: 'anonymous',
sharePlantDetails: true,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const response = await fetch('/api/plants/register-anonymous', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
commonName: formData.commonName,
scientificName: formData.scientificName || undefined,
species: formData.species || undefined,
genus: formData.genus || undefined,
family: formData.family || undefined,
location: {
latitude: parseFloat(formData.latitude),
longitude: parseFloat(formData.longitude),
},
privacySettings,
pseudonym: formData.pseudonym || undefined,
encryptionKey: formData.encryptionKey || undefined,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to register plant');
}
setSuccess(true);
setWalletAddress(data.privacy.walletAddress);
setTimeout(() => {
router.push(`/plants/${data.plant.id}`);
}, 3000);
} catch (err: any) {
setError(err.message || 'An error occurred');
} finally {
setLoading(false);
}
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
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-purple-50 to-indigo-100">
<Head>
<title>Anonymous Plant Registration - LocalGreenChain</title>
</Head>
{/* Header */}
<header className="bg-white shadow-sm border-b-2 border-purple-200">
<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-purple-800">
🌱 LocalGreenChain
</a>
</Link>
<nav className="flex gap-4">
<Link href="/plants/register">
<a className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">
Standard Registration
</a>
</Link>
<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-6xl mx-auto px-4 py-12 sm:px-6 lg:px-8">
<div className="mb-8 text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-2">
🔒 Anonymous Plant Registration
</h1>
<p className="text-lg text-gray-600">
Register your plant with maximum privacy protection
</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-6 bg-green-100 border border-green-400 text-green-700 rounded-lg">
<h3 className="font-bold text-lg mb-2">
Plant registered anonymously!
</h3>
<p className="mb-2">Your anonymous wallet address:</p>
<p className="font-mono bg-white p-2 rounded text-sm break-all">
{walletAddress}
</p>
<p className="mt-2 text-sm">
Save this address to manage your plant. Redirecting...
</p>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Privacy Settings */}
<div className="lg:col-span-1">
<PrivacySettings
value={privacySettings}
onChange={setPrivacySettings}
showTorStatus={true}
/>
</div>
{/* Right Column - Plant Information */}
<div className="lg:col-span-2">
<div className="bg-white rounded-lg shadow-xl p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Plant Information
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Basic Plant Info */}
<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-purple-500 focus:border-transparent"
placeholder="e.g., Tomato, Basil"
/>
</div>
{privacySettings.sharePlantDetails && (
<>
<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-purple-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-purple-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-purple-500 focus:border-transparent"
/>
</div>
</>
)}
</div>
{/* Location */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Location (will be obfuscated based on privacy settings)
</h3>
<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-purple-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-purple-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>
<p className="mt-2 text-sm text-gray-600">
Your exact location will be obfuscated based on your
privacy settings
</p>
</div>
</div>
</div>
{/* Optional Pseudonym */}
{privacySettings.identityPrivacy === 'pseudonym' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Pseudonym (optional)
</label>
<input
type="text"
name="pseudonym"
value={formData.pseudonym}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Your chosen display name"
/>
</div>
)}
{/* Encryption Key */}
{privacySettings.anonymousMode && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Encryption Key (optional)
</label>
<input
type="password"
name="encryptionKey"
value={formData.encryptionKey}
onChange={handleChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
placeholder="Optional password for extra security"
/>
<p className="mt-1 text-sm text-gray-600">
Used to generate your anonymous contact address
</p>
</div>
)}
{/* Submit Button */}
<div className="flex gap-4">
<button
type="submit"
disabled={loading}
className="flex-1 px-6 py-3 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Registering...' : '🔒 Register Anonymously'}
</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>
</div>
</div>
</main>
</div>
);
}

412
pages/plants/register.tsx Normal file
View file

@ -0,0 +1,412 @@
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>
);
}

41
tor/torrc.example Normal file
View file

@ -0,0 +1,41 @@
# Tor Configuration for LocalGreenChain Hidden Service
# Copy this file to /etc/tor/torrc or ~/.tor/torrc and edit as needed
# SOCKS proxy port (for outgoing Tor connections)
SocksPort 9050
# Control port for managing Tor
ControlPort 9051
# Hidden Service Configuration
# This allows your LocalGreenChain instance to be accessible via .onion address
HiddenServiceDir /var/lib/tor/localgreenchain/
HiddenServicePort 80 127.0.0.1:3001
# Optional: Multiple hidden service ports
# HiddenServicePort 443 127.0.0.1:3001
# Logging (for debugging)
Log notice file /var/log/tor/notices.log
# Privacy and Security Settings
# Reject non-anonymous single hop circuits
RejectAllOutboundTraffic 0
# Circuit isolation for better privacy
IsolateDestAddr 1
IsolateDestPort 1
# Bridge configuration (if needed to bypass censorship)
# UseBridges 1
# Bridge obfs4 [bridge address]
# Bandwidth settings (optional)
# RelayBandwidthRate 100 KB
# RelayBandwidthBurst 200 KB
# Directory for storing Tor data
DataDirectory /var/lib/tor
# Run as daemon
RunAsDaemon 1