Compare commits

...

11 Commits

Author SHA1 Message Date
ef2a987843 logo 2025-06-17 08:34:12 +02:00
f71cb7d24a csv upload 2025-06-08 17:47:49 +02:00
ab9e07f29b font 2025-06-08 17:43:22 +02:00
01b70e279f Update recognize_plant.html 2025-06-08 17:35:57 +02:00
79c6595eab Update recognize_plant.html 2025-06-08 17:33:20 +02:00
26806a26eb Update base.html 2025-06-08 17:30:46 +02:00
7751d1ec64 plant recognition 2025-06-08 17:29:41 +02:00
26cdb90c3a section improvements 2025-06-08 17:23:47 +02:00
9fcb9c358b Update init_db.py 2025-06-08 17:14:47 +02:00
0f7c49e063 Update post.html 2025-06-08 17:13:23 +02:00
ef8854d39d Update init_db.py 2025-06-08 17:13:20 +02:00
12 changed files with 480 additions and 58 deletions

215
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, flash, session
from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, Response
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from datetime import datetime
@@ -6,6 +6,9 @@ from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
import os
from config import Config
import random
import csv
import io
app = Flask(__name__)
app.config.from_object(Config)
@@ -203,7 +206,7 @@ def home():
query = query.filter(Plant.care_difficulty_id == care_difficulty_id)
if growth_rate_id:
query = query.filter(Plant.growth_rate_id == growth_rate_id)
if section_id and hasattr(Plant, 'section_id'):
if section_id:
query = query.filter(Plant.section_id == section_id)
plants = query.order_by(Plant.date_added.desc()).all()
@@ -221,6 +224,7 @@ def home():
product_names.append(products_dict[int(pid)])
climate_info = climates_dict.get(plant.climate_id, {'name': '', 'icon': None, 'description': ''})
environment_info = environments_dict.get(plant.environment_id, {'name': '', 'icon': None, 'description': ''})
display_plants.append({
'id': plant.id,
'name': plant.name,
@@ -249,26 +253,28 @@ def home():
'growth_rate': plant.growth_rate.name if plant.growth_rate else '',
'growth_rate_icon': plant.growth_rate.icon if plant.growth_rate and plant.growth_rate.icon else None,
'growth_rate_description': plant.growth_rate.description if plant.growth_rate else '',
'section': plant.section
})
return render_template('home.html',
plants=display_plants,
climates=Climate.query.all(),
environments=Environment.query.all(),
lights=Light.query.order_by(Light.name).all(),
toxicities=Toxicity.query.order_by(Toxicity.name).all(),
sizes=Size.query.order_by(Size.name).all(),
difficulties=CareDifficulty.query.order_by(CareDifficulty.name).all(),
growth_rates=GrowthRate.query.order_by(GrowthRate.name).all(),
search=search,
selected_climate=climate_id,
selected_environment=environment_id,
selected_light=light_id,
selected_toxicity=toxicity_id,
selected_size=size_id,
selected_care_difficulty=care_difficulty_id,
selected_growth_rate=growth_rate_id
)
plants=display_plants,
climates=Climate.query.all(),
environments=Environment.query.all(),
lights=Light.query.all(),
toxicities=Toxicity.query.all(),
sizes=Size.query.all(),
difficulties=CareDifficulty.query.all(),
growth_rates=GrowthRate.query.all(),
sections=Section.query.all(),
search=search,
selected_climate=climate_id,
selected_environment=environment_id,
selected_light=light_id,
selected_toxicity=toxicity_id,
selected_size=size_id,
selected_care_difficulty=care_difficulty_id,
selected_growth_rate=growth_rate_id,
selected_section=section_id)
@app.route('/login', methods=['GET', 'POST'])
def login():
@@ -624,6 +630,7 @@ def edit_plant(plant_id):
sizes = Size.query.order_by(Size.name).all()
difficulties = CareDifficulty.query.order_by(CareDifficulty.name).all()
growth_rates = GrowthRate.query.order_by(GrowthRate.name).all()
sections = Section.query.order_by(Section.name).all()
if request.method == 'POST':
plant.name = request.form['name']
@@ -634,6 +641,7 @@ def edit_plant(plant_id):
plant.size_id = request.form.get('size_id')
plant.care_difficulty_id = request.form.get('care_difficulty_id')
plant.growth_rate_id = request.form.get('growth_rate_id')
plant.section_id = request.form.get('section_id')
plant.products = ','.join(request.form.getlist('product_ids'))
plant.description = request.form['description']
plant.care_guide = request.form.get('care_guide')
@@ -661,7 +669,8 @@ def edit_plant(plant_id):
toxicities=toxicities,
sizes=sizes,
difficulties=difficulties,
growth_rates=growth_rates)
growth_rates=growth_rates,
sections=sections)
@app.route('/plant/delete/<int:plant_id>', methods=['POST'])
def delete_plant(plant_id):
@@ -1432,6 +1441,172 @@ def seed_db():
))
db.session.commit()
@app.route('/recognize-plant', methods=['GET', 'POST'])
def recognize_plant():
if request.method == 'GET':
return render_template('recognize_plant.html')
# For proof of concept, just return a random plant
plants = Plant.query.all()
if not plants:
flash('No plants found in the database.', 'warning')
return redirect(url_for('home'))
random_plant = random.choice(plants)
return jsonify({'redirect': url_for('plant', plant_id=random_plant.id)})
@app.route('/admin/plants/upload-csv', methods=['POST'])
def upload_plants_csv():
if not is_logged_in():
return redirect(url_for('login'))
if 'csv_file' not in request.files:
flash('No file uploaded', 'error')
return redirect(url_for('manage_plants'))
file = request.files['csv_file']
if file.filename == '':
flash('No file selected', 'error')
return redirect(url_for('manage_plants'))
if not file.filename.endswith('.csv'):
flash('Please upload a CSV file', 'error')
return redirect(url_for('manage_plants'))
try:
# Read the CSV file
stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None)
csv_reader = csv.DictReader(stream)
# Get all the lookup dictionaries
climates = {c.name.lower(): c.id for c in Climate.query.all()}
environments = {e.name.lower(): e.id for e in Environment.query.all()}
lights = {l.name.lower(): l.id for l in Light.query.all()}
toxicities = {t.name.lower(): t.id for t in Toxicity.query.all()}
sizes = {s.name.lower(): s.id for s in Size.query.all()}
care_difficulties = {d.name.lower(): d.id for d in CareDifficulty.query.all()}
growth_rates = {r.name.lower(): r.id for r in GrowthRate.query.all()}
products = {p.name.lower(): p.id for p in Product.query.all()}
success_count = 0
error_count = 0
duplicate_count = 0
for row in csv_reader:
try:
# Check if plant with this name already exists
existing_plant = Plant.query.filter_by(name=row['name']).first()
if existing_plant:
duplicate_count += 1
continue
# Create new plant
plant = Plant(
name=row['name'],
description=row.get('description', ''),
care_guide=row.get('care_guide', ''),
date_added=datetime.utcnow()
)
# Set relationships using the lookup dictionaries
if row.get('climate'):
plant.climate_id = climates.get(row['climate'].lower())
if row.get('environment'):
plant.environment_id = environments.get(row['environment'].lower())
if row.get('light'):
plant.light_id = lights.get(row['light'].lower())
if row.get('toxicity'):
plant.toxicity_id = toxicities.get(row['toxicity'].lower())
if row.get('size'):
plant.size_id = sizes.get(row['size'].lower())
if row.get('care_difficulty'):
plant.care_difficulty_id = care_difficulties.get(row['care_difficulty'].lower())
if row.get('growth_rate'):
plant.growth_rate_id = growth_rates.get(row['growth_rate'].lower())
# Handle products (comma-separated list)
if row.get('products'):
product_ids = []
for product_name in row['products'].split(','):
product_name = product_name.strip().lower()
if product_name in products:
product_ids.append(str(products[product_name]))
plant.products = ','.join(product_ids)
db.session.add(plant)
success_count += 1
except Exception as e:
error_count += 1
print(f"Error processing row: {row}, Error: {str(e)}")
continue
db.session.commit()
message = f'Successfully imported {success_count} plants.'
if duplicate_count > 0:
message += f' {duplicate_count} duplicate plants were skipped.'
if error_count > 0:
message += f' {error_count} errors occurred.'
flash(message, 'success')
except Exception as e:
flash(f'Error processing CSV file: {str(e)}', 'error')
db.session.rollback()
return redirect(url_for('manage_plants'))
@app.route('/admin/plants/download-template')
def download_plants_template():
if not is_logged_in():
return redirect(url_for('login'))
# Create a CSV template with headers and example data
output = io.StringIO()
writer = csv.writer(output)
# Write headers
headers = [
'name',
'description',
'care_guide',
'climate',
'environment',
'light',
'toxicity',
'size',
'care_difficulty',
'growth_rate',
'products'
]
writer.writerow(headers)
# Write example row with available options
example_row = [
'Monstera Deliciosa', # name
'Beautiful tropical plant with distinctive leaf holes', # description
'Water weekly, mist leaves occasionally', # care_guide
', '.join([c.name for c in Climate.query.all()]), # climate options
', '.join([e.name for e in Environment.query.all()]), # environment options
', '.join([l.name for l in Light.query.all()]), # light options
', '.join([t.name for t in Toxicity.query.all()]), # toxicity options
', '.join([s.name for s in Size.query.all()]), # size options
', '.join([d.name for d in CareDifficulty.query.all()]), # care_difficulty options
', '.join([r.name for r in GrowthRate.query.all()]), # growth_rate options
', '.join([p.name for p in Product.query.all()]) # product options
]
writer.writerow(example_row)
# Create the response
output.seek(0)
return Response(
output,
mimetype='text/csv',
headers={
'Content-Disposition': 'attachment; filename=plants_template.csv',
'Content-Type': 'text/csv',
}
)
if __name__ == "__main__":
with app.app_context():
db.create_all() # Create all tables

View File

@@ -1,21 +1,60 @@
from app import app, db, User
from flask_migrate import upgrade
from sqlalchemy import text
def init_db():
with app.app_context():
# Create all tables
db.create_all()
# Run any pending migrations
upgrade()
# Create default admin user if it doesn't exist
admin = User.query.filter_by(username='admin').first()
if not admin:
admin = User(username='admin')
admin.set_password('admin123') # You should change this password after first login
db.session.add(admin)
# First, try to create the section table if it doesn't exist
try:
db.session.execute(text("""
CREATE TABLE IF NOT EXISTS section (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
icon VARCHAR(200)
)
"""))
db.session.commit()
print("Default admin user created!")
print("Section table created or already exists")
except Exception as e:
print(f"Error creating section table: {e}")
db.session.rollback()
# Then try to add the section_id column if it doesn't exist
try:
# Check if column exists
result = db.session.execute(text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name='plant' AND column_name='section_id'
"""))
if not result.fetchone():
print("Adding section_id column to plant table")
db.session.execute(text("""
ALTER TABLE plant
ADD COLUMN section_id INTEGER
REFERENCES section(id)
"""))
db.session.commit()
print("Added section_id column successfully")
else:
print("section_id column already exists")
except Exception as e:
print(f"Error adding section_id column: {e}")
db.session.rollback()
# Create default admin user if it doesn't exist
try:
admin = User.query.filter_by(username='admin').first()
if not admin:
admin = User(username='admin')
admin.set_password('admin123') # You should change this password after first login
db.session.add(admin)
db.session.commit()
print("Default admin user created!")
except Exception as e:
print(f"Error creating admin user: {e}")
db.session.rollback()
if __name__ == '__main__':
init_db()

Binary file not shown.

BIN
static/images/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -1,4 +1,12 @@
/* Planty theme variables (converted from LESS) */
@font-face {
font-family: 'Humber Town';
src: url('/static/fonts/Humber Town.otf') format('opentype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
body {
font-family: 'Quicksand', 'Segoe UI', Arial, sans-serif;
min-height: 100vh;
@@ -12,9 +20,10 @@ nav {
}
.site-title {
font-family: 'Humber Town', 'Quicksand', sans-serif;
color: #3e5637;
font-weight: bold;
font-size: 1.25rem;
font-weight: normal;
font-size: 1.5rem;
letter-spacing: 0.02em;
}
@@ -55,9 +64,10 @@ nav {
}
.admin-header-title {
font-family: 'Humber Town', 'Quicksand', sans-serif;
color: #3e5637;
font-size: 2rem;
font-weight: bold;
font-size: 2.5rem;
font-weight: normal;
letter-spacing: 0.01em;
}
@@ -168,4 +178,36 @@ nav {
.masonry > * {
break-inside: avoid;
margin-bottom: 2rem;
}
/* Add decorative font to card titles */
.card h2, .card h3 {
font-family: 'Humber Town', 'Quicksand', sans-serif;
font-weight: normal;
}
/* Add decorative font to special elements */
.plant-name, .environment-name, .climate-name {
font-family: 'Humber Town', 'Quicksand', sans-serif;
font-weight: normal;
}
/* Plant title styling */
.plant-card .plant-title,
.plant-detail .plant-title,
.plant-card h2,
.plant-detail h1,
article h1.text-4xl {
font-family: 'Humber Town', 'Quicksand', sans-serif;
font-weight: normal;
font-size: 4rem;
color: #3e5637;
margin-bottom: 0rem;
line-height: 1.2;
}
.plant-detail .plant-title,
.plant-detail h1,
article h1.text-4xl {
font-size: 6rem;
}

View File

@@ -16,15 +16,16 @@
<div class="flex space-x-7">
<div>
<a href="{{ url_for('home') }}" class="flex items-center py-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-7 h-7 text-[#6b8f71] mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 2C7 2 2 7 2 12c0 5 5 10 10 10s10-5 10-10c0-5-5-10-10-10zm0 0c0 4 4 8 8 8" />
</svg>
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Site Logo" class="w-10 h-10 object-contain mr-2" />
<span class="font-bold text-[#3e5637] text-xl tracking-tight">Verpot Je Lot</span>
</a>
</div>
</div>
<div class="flex items-center space-x-3">
<!-- Admin links moved to footer -->
<div class="flex items-center">
<a href="{{ url_for('recognize_plant') }}" class="inline-flex items-center px-4 py-2 bg-[#4e6b50] text-white rounded-lg hover:bg-[#3e5637] transition-colors duration-200 shadow-sm">
<i class="fas fa-camera-retro mr-2"></i>
<span class="font-medium">Recognize Plant</span>
</a>
</div>
</div>
</div>

View File

@@ -21,6 +21,16 @@
{% endif %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="section_id" class="block text-sm font-medium text-gray-700">Section</label>
<select name="section_id" id="section_id" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select a section</option>
{% for section in sections %}
<option value="{{ section.id }}" {% if plant.section_id == section.id %}selected{% endif %}>{{ section.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="climate_id" class="block text-sm font-medium text-gray-700">Climate</label>
<select name="climate_id" id="climate_id" required

View File

@@ -52,6 +52,12 @@
<option value="{{ rate.id }}" {% if selected_growth_rate == rate.id|string %}selected{% endif %}>{{ rate.name }}</option>
{% endfor %}
</select>
<select id="section" name="section" class="rounded-lg border border-gray-300 px-3 py-1 text-sm focus:border-[#6b8f71] focus:ring-[#6b8f71]">
<option value="">All Sections</option>
{% for section in sections %}
<option value="{{ section.id }}" {% if selected_section == section.id|string %}selected{% endif %}>{{ section.name }}</option>
{% endfor %}
</select>
<a href="{{ url_for('home') }}" class="btn-secondary px-4 py-1 text-sm font-semibold ml-2 sm:ml-0">Clear</a>
</form>
</div>
@@ -67,7 +73,7 @@
}
const form = document.getElementById('plant-filter-form');
const searchInput = document.getElementById('search');
const selects = [document.getElementById('climate'), document.getElementById('environment'), document.getElementById('light'), document.getElementById('toxicity'), document.getElementById('size'), document.getElementById('care_difficulty'), document.getElementById('growth_rate')];
const selects = [document.getElementById('climate'), document.getElementById('environment'), document.getElementById('light'), document.getElementById('toxicity'), document.getElementById('size'), document.getElementById('care_difficulty'), document.getElementById('growth_rate'), document.getElementById('section')];
// Auto-submit on select change
selects.forEach(sel => sel.addEventListener('change', () => form.submit()));
@@ -103,7 +109,17 @@
{% endif %}
<div class="w-full flex flex-col gap-2">
<h2 class="text-2xl font-bold mb-1 text-[#4e6b50] group-hover:text-[#3e5637] transition">{{ plant.name }}</h2>
{% if plant.section %}
<div class="text-sm text-[#6b8f71] mb-2">
<i class="fas fa-layer-group mr-1"></i>Section: {{ plant.section.name }}
</div>
{% endif %}
<div class="flex flex-wrap gap-2 mb-1">
<!-- Debug output -->
<div class="hidden">
Debug - Climate: {{ plant.climate }}
Debug - Environment: {{ plant.environment }}
</div>
<span class="tag-tooltip inline-flex items-center px-2 py-0.5 rounded text-[#3e5637] text-xs font-semibold" style="background-color: #d0e7d2;" data-type="climate">
{% if plant.climate_icon %}
<img src="{{ url_for('static', filename='icons/' ~ plant.climate_icon) }}" alt="Climate icon" class="w-4 h-4 mr-1 inline-block align-middle" />

View File

@@ -6,7 +6,18 @@
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold">All Plants</h2>
<button id="show-add-plant" class="btn-main px-6 py-2 font-semibold">Add Plant</button>
<div class="flex gap-4">
<a href="{{ url_for('download_plants_template') }}" class="btn-secondary px-6 py-2 font-semibold flex items-center gap-2">
<i class="fas fa-file-download"></i> Download Template
</a>
<form id="csv-upload-form" method="POST" action="{{ url_for('upload_plants_csv') }}" enctype="multipart/form-data" class="inline">
<input type="file" name="csv_file" id="csv_file" accept=".csv" class="hidden" onchange="this.form.submit()">
<button type="button" onclick="document.getElementById('csv_file').click()" class="btn-secondary px-6 py-2 font-semibold flex items-center gap-2">
<i class="fas fa-file-import"></i> Import CSV
</button>
</form>
<button id="show-add-plant" class="btn-main px-6 py-2 font-semibold">Add Plant</button>
</div>
</div>
<div id="add-plant-form-card" class="hidden mb-8">
<div class="bg-gray-50 rounded-lg shadow p-6 max-w-2xl mx-auto" style="overflow:visible;">
@@ -126,8 +137,6 @@
<thead>
<tr class="border-b">
<th class="py-2 px-3">Name</th>
<th class="py-2 px-3">Climate</th>
<th class="py-2 px-3">Environment</th>
<th class="py-2 px-3">Added</th>
<th class="py-2 px-3">Actions</th>
</tr>
@@ -136,8 +145,6 @@
{% for plant in plants %}
<tr class="border-b hover:bg-[#f5f7f2]">
<td class="py-2 px-3 font-semibold">{{ plant.name }}</td>
<td class="py-2 px-3">{{ climates[plant.climate_id] if plant.climate_id in climates else '' }}</td>
<td class="py-2 px-3">{{ environments[plant.environment_id] if plant.environment_id in environments else '' }}</td>
<td class="py-2 px-3"><span class="local-date" data-date="{{ plant.date_added.isoformat() }}"></span></td>
<td class="py-2 px-3">
<a href="{{ url_for('edit_plant', plant_id=plant.id) }}" class="btn-edit">Edit</a>

View File

@@ -14,6 +14,11 @@
</div>
<div class="md:w-1/2 flex flex-col gap-4">
<h1 class="text-4xl font-bold text-[#4e6b50] mb-2">{{ plant.name }}</h1>
{% if plant.section %}
<div class="text-sm text-[#6b8f71] mb-2">
<i class="fas fa-layer-group mr-1"></i>Section: {{ plant.section.name }}
</div>
{% endif %}
<div class="flex flex-wrap gap-2 mb-2">
{% if plant.climate %}
<span class="tag-tooltip inline-flex items-center px-2 py-0.5 rounded text-[#3e5637] text-xs font-semibold cursor-pointer transition-colors duration-200" style="background-color: #d0e7d2;" data-type="climate">
@@ -68,21 +73,21 @@
<div class="prose max-w-none mb-4 text-[#4e6b50]">{{ plant.description|safe }}</div>
</div>
</div>
{% if products %}
<div class="mb-8 mt-4">
<span class="font-semibold text-[#4e6b50]">Products:</span>
<span class="flex flex-wrap gap-2 mt-1">
{% for product in products %}
<span class="inline-block bg-[#e6ebe0] text-[#3e5637] px-3 py-1 rounded text-xs font-semibold border border-[#d0e7d2]">{{ product.name }}</span>
{% endfor %}
</span>
</div>
{% endif %}
{% if plant.care_guide %}
{% if plant.care_guide or products %}
<div class="mt-12">
<h2 class="text-2xl font-bold text-[#6b8f71] mb-3">Care Guide</h2>
<div class="prose max-w-none bg-[#f8f9fa] border border-[#e6ebe0] rounded-xl p-6 text-[#3e5637] shadow" style="min-height:80px;">
{{ plant.care_guide|safe }}
{% if products %}
<div class="mt-6">
<h3 class="text-lg font-semibold text-[#4e6b50] mb-2">Recommended Products</h3>
<div class="flex flex-wrap gap-2">
{% for product in products %}
<span class="inline-block bg-[#e6ebe0] text-[#3e5637] px-3 py-1 rounded text-sm font-semibold border border-[#d0e7d2]">{{ product.name }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
@@ -92,6 +97,12 @@
class="inline-block bg-[#6b8f71] text-white hover:bg-[#4e6b50] font-semibold px-6 py-2 rounded-lg shadow transition-colors duration-200">
🌱 View Similar Plants
</a>
{% if plant.section %}
<a href="{{ url_for('home', section=plant.section_id) }}"
class="inline-block bg-[#6b8f71] text-white hover:bg-[#4e6b50] font-semibold px-6 py-2 rounded-lg shadow transition-colors duration-200">
📚 View Plants in this Section
</a>
{% endif %}
<a href="{{ url_for('home') }}" class="inline-block bg-[#e6ebe0] text-[#3e5637] hover:bg-[#b7c7a3] hover:text-[#4e6b50] font-semibold px-6 py-2 rounded-lg shadow transition-colors duration-200">
← Back to Home
</a>

View File

@@ -0,0 +1,121 @@
{% extends "base.html" %}
{% block title %}Recognize Plant{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto bg-white p-8 rounded-2xl shadow-xl mt-8">
<h1 class="text-3xl font-bold text-[#4e6b50] mb-6 text-center">Plant Recognition</h1>
<!-- File Upload -->
<div class="bg-[#f8f9fa] p-6 rounded-xl border border-[#e6ebe0]">
<h2 class="text-xl font-semibold text-[#4e6b50] mb-4">Upload Image</h2>
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">Take a Photo</h3>
<div class="relative">
<video id="camera" class="w-full h-64 bg-gray-100 rounded-lg mb-4" autoplay playsinline></video>
<button id="capture" class="btn-main w-full">
<i class="fas fa-camera mr-2"></i>Capture Photo
</button>
</div>
<div id="preview-container" class="hidden">
<img id="preview" class="w-full h-64 object-cover rounded-lg mb-4" alt="Captured photo">
</div>
</div>
<div>
<h3 class="text-lg font-semibold mb-4">Or Upload an Image</h3>
<form id="upload-form" class="space-y-4">
<div class="flex items-center justify-center w-full">
<label for="file-upload" class="flex flex-col items-center justify-center w-full h-32 border-2 border-[#4e6b50] border-dashed rounded-lg cursor-pointer bg-[#f5f7f2] hover:bg-[#e6ebe0]">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i class="fas fa-cloud-upload-alt text-3xl text-[#4e6b50] mb-2"></i>
<p class="mb-2 text-sm text-[#4e6b50]"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-[#4e6b50]">PNG, JPG or JPEG</p>
</div>
<input id="file-upload" type="file" class="hidden" accept="image/*" />
</label>
</div>
</form>
</div>
</div>
</div>
<div class="mt-8 text-center">
<a href="{{ url_for('home') }}" class="inline-block bg-[#e6ebe0] text-[#3e5637] hover:bg-[#b7c7a3] hover:text-[#4e6b50] font-semibold px-6 py-2 rounded-lg shadow transition-colors duration-200">
← Back to Home
</a>
</div>
</div>
<script>
let stream = null;
const camera = document.getElementById('camera');
const capture = document.getElementById('capture');
const preview = document.getElementById('preview');
const previewContainer = document.getElementById('preview-container');
const fileUpload = document.getElementById('file-upload');
const uploadForm = document.getElementById('upload-form');
// Start camera when page loads
async function startCamera() {
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
camera.srcObject = stream;
} catch (err) {
console.error('Error accessing camera:', err);
alert('Could not access camera. Please make sure you have granted camera permissions.');
}
}
// Capture photo
capture.addEventListener('click', () => {
const canvas = document.createElement('canvas');
canvas.width = camera.videoWidth;
canvas.height = camera.videoHeight;
canvas.getContext('2d').drawImage(camera, 0, 0);
// Convert to blob and submit
canvas.toBlob(blob => {
submitImage(blob);
}, 'image/jpeg');
});
// Handle file upload
fileUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
submitImage(file);
}
});
// Submit image to server
function submitImage(imageData) {
const formData = new FormData();
formData.append('image', imageData);
fetch('/recognize-plant', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.redirect) {
window.location.href = data.redirect;
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while processing the image.');
});
}
// Start camera when page loads
startCamera();
// Clean up camera when leaving page
window.addEventListener('beforeunload', () => {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
</script>
{% endblock %}