Compare commits
11 Commits
e63fecab5d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ef2a987843 | |||
| f71cb7d24a | |||
| ab9e07f29b | |||
| 01b70e279f | |||
| 79c6595eab | |||
| 26806a26eb | |||
| 7751d1ec64 | |||
| 26cdb90c3a | |||
| 9fcb9c358b | |||
| 0f7c49e063 | |||
| ef8854d39d |
195
app.py
195
app.py
@@ -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,17 +253,19 @@ 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(),
|
||||
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,
|
||||
@@ -267,8 +273,8 @@ def home():
|
||||
selected_toxicity=toxicity_id,
|
||||
selected_size=size_id,
|
||||
selected_care_difficulty=care_difficulty_id,
|
||||
selected_growth_rate=growth_rate_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
|
||||
|
||||
47
init_db.py
47
init_db.py
@@ -1,14 +1,50 @@
|
||||
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()
|
||||
# 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("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')
|
||||
@@ -16,6 +52,9 @@ def init_db():
|
||||
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()
|
||||
BIN
static/fonts/Humber Town.otf
Normal file
BIN
static/fonts/Humber Town.otf
Normal file
Binary file not shown.
BIN
static/images/logo.jpg
Normal file
BIN
static/images/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
static/images/logo.png
Normal file
BIN
static/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -169,3 +179,35 @@ nav {
|
||||
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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -6,8 +6,19 @@
|
||||
<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>
|
||||
<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;">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
121
templates/recognize_plant.html
Normal file
121
templates/recognize_plant.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user