Incremental Update POC complete

This commit is contained in:
Joshua Hare 2025-04-15 20:03:13 +10:00
parent b97d1b175d
commit dcbb57b474
17 changed files with 1367 additions and 119 deletions

4
.env Normal file
View File

@ -0,0 +1,4 @@
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=Ov23liuhXJZtRpcdqFzf
GITHUB_CLIENT_SECRET=a3c5db76c59eae2dfcc84c2c59019e019210c47a

123
.gitignore vendored Normal file
View File

@ -0,0 +1,123 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually ignored by MicroPython Checkignore tool
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff (if used later, good to have)
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff
instance/ # Important: Ignore instance folder (contains DB, config secrets)
.webassets-cache
# Scrapy stuff (if used later)
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# PEP 582; used by PDM, Flit, and Rye
__pypackages__/
# Celery stuff (if used later)
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env # Important: Ignore environment file (contains secrets)
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# PyDev project settings
.pydevproject
# VSCode Pylance cache
.pylance_cache/
# Editor directories and files
.idea/
.vscode/
*.sublime-project
*.sublime-workspace
*.DS_Store
Thumbs.db

View File

@ -5,7 +5,6 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY app/ app/ COPY app/ app/
COPY static/ static/ COPY static/ static/
COPY instance/ instance/ COPY instance/ instance/
copy uploads app/uploads/
COPY config.py . COPY config.py .
COPY .env . COPY .env .
EXPOSE 5000 EXPOSE 5000

View File

@ -1,5 +1,5 @@
#app/routes.py #app/routes.py
from flask import Blueprint, render_template, redirect, url_for, flash, request from flask import Blueprint, render_template, redirect, url_for, flash, request, send_from_directory, abort # Added send_from_directory and abort
from flask_login import login_required, current_user from flask_login import login_required, current_user
from app import db from app import db
from app.models import SmartApp, ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport from app.models import SmartApp, ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport
@ -16,12 +16,25 @@ logger = logging.getLogger(__name__)
gallery_bp = Blueprint('gallery', __name__) gallery_bp = Blueprint('gallery', __name__)
# Absolute path to the upload folder inside the container
UPLOAD_FOLDER = '/app/uploads/' UPLOAD_FOLDER = '/app/uploads/'
ALLOWED_EXTENSIONS = {'jpg', 'png'} ALLOWED_EXTENSIONS = {'jpg', 'png'}
def allowed_file(filename): def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# New route to serve uploaded files
@gallery_bp.route('/uploads/<path:filename>')
def uploaded_file(filename):
# Use the absolute path to the directory inside the container
absolute_upload_folder = os.path.abspath(UPLOAD_FOLDER)
logger.debug(f"Attempting to serve file: {filename} from {absolute_upload_folder}")
try:
return send_from_directory(absolute_upload_folder, filename)
except FileNotFoundError:
logger.error(f"File not found: {os.path.join(absolute_upload_folder, filename)}")
abort(404)
@gallery_bp.route('/') @gallery_bp.route('/')
def index(): def index():
return redirect(url_for('gallery.gallery')) return redirect(url_for('gallery.gallery'))
@ -92,22 +105,22 @@ def app_detail(app_id):
if app.categories: if app.categories:
category_ids = [int(cid) for cid in app.categories.split(',') if cid] category_ids = [int(cid) for cid in app.categories.split(',') if cid]
app_categories = Category.query.filter(Category.id.in_(category_ids)).all() app_categories = Category.query.filter(Category.id.in_(category_ids)).all()
app_specialties = [] app_specialties = []
if app.specialties: if app.specialties:
speciality_ids = [int(sid) for sid in app.specialties.split(',') if sid] speciality_ids = [int(sid) for sid in app.specialties.split(',') if sid]
app_specialties = Speciality.query.filter(Speciality.id.in_(speciality_ids)).all() app_specialties = Speciality.query.filter(Speciality.id.in_(speciality_ids)).all()
app_os_supports = [] app_os_supports = []
if app.os_support: if app.os_support:
os_ids = [int(oid) for oid in app.os_support.split(',') if oid] os_ids = [int(oid) for oid in app.os_support.split(',') if oid]
app_os_supports = OSSupport.query.filter(OSSupport.id.in_(os_ids)).all() app_os_supports = OSSupport.query.filter(OSSupport.id.in_(os_ids)).all()
app_ehr_supports = [] app_ehr_supports = []
if app.ehr_support: if app.ehr_support:
ehr_ids = [int(eid) for eid in app.ehr_support.split(',') if eid] ehr_ids = [int(eid) for eid in app.ehr_support.split(',') if eid]
app_ehr_supports = EHRSupport.query.filter(EHRSupport.id.in_(ehr_ids)).all() app_ehr_supports = EHRSupport.query.filter(EHRSupport.id.in_(ehr_ids)).all()
return render_template( return render_template(
'app_detail.html', 'app_detail.html',
app=app, app=app,
@ -134,6 +147,7 @@ def register():
# Ensure upload folder exists # Ensure upload folder exists
try: try:
# Use the absolute path inside the container
os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(UPLOAD_FOLDER, exist_ok=True)
logger.debug(f"Ensured {UPLOAD_FOLDER} exists") logger.debug(f"Ensured {UPLOAD_FOLDER} exists")
except Exception as e: except Exception as e:
@ -189,6 +203,7 @@ def register():
file = form.logo_upload.data file = form.logo_upload.data
if allowed_file(file.filename): if allowed_file(file.filename):
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
# Use absolute path for saving
save_path = os.path.join(UPLOAD_FOLDER, filename) save_path = os.path.join(UPLOAD_FOLDER, filename)
logger.debug(f"Attempting to save logo to {save_path}") logger.debug(f"Attempting to save logo to {save_path}")
try: try:
@ -199,7 +214,8 @@ def register():
logger.error(f"Failed to save logo to {save_path}") logger.error(f"Failed to save logo to {save_path}")
flash('Failed to save logo.', 'danger') flash('Failed to save logo.', 'danger')
return render_template('register.html', form=form) return render_template('register.html', form=form)
logo_url = f"/app/uploads/{filename}" # Store URL path for web access
logo_url = f"/uploads/{filename}" # CHANGED
logger.debug(f"Set logo_url to {logo_url}") logger.debug(f"Set logo_url to {logo_url}")
except Exception as e: except Exception as e:
logger.error(f"Error saving logo to {save_path}: {e}") logger.error(f"Error saving logo to {save_path}: {e}")
@ -209,11 +225,13 @@ def register():
# Handle app images # Handle app images
app_images = [] app_images = []
if form.app_image_urls.data: if form.app_image_urls.data:
app_images.extend([url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()]) # Keep existing URLs if they are valid URLs
if form.app_image_uploads.data: app_images.extend([url.strip() for url in form.app_image_urls.data.splitlines() if url.strip().startswith(('http://', 'https://'))])
if form.app_image_uploads.data: # Check if a file was uploaded
file = form.app_image_uploads.data file = form.app_image_uploads.data
if allowed_file(file.filename): if file and allowed_file(file.filename): # Check if file object exists and is allowed
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
# Use absolute path for saving
save_path = os.path.join(UPLOAD_FOLDER, filename) save_path = os.path.join(UPLOAD_FOLDER, filename)
logger.debug(f"Attempting to save app image to {save_path}") logger.debug(f"Attempting to save app image to {save_path}")
try: try:
@ -224,7 +242,8 @@ def register():
logger.error(f"Failed to save app image to {save_path}") logger.error(f"Failed to save app image to {save_path}")
flash('Failed to save app image.', 'danger') flash('Failed to save app image.', 'danger')
return render_template('register.html', form=form) return render_template('register.html', form=form)
app_images.append(f"/app/uploads/{filename}") # Store URL path for web access
app_images.append(f"/uploads/{filename}") # CHANGED
except Exception as e: except Exception as e:
logger.error(f"Error saving app image to {save_path}: {e}") logger.error(f"Error saving app image to {save_path}: {e}")
flash('Error saving app image.', 'danger') flash('Error saving app image.', 'danger')
@ -235,7 +254,7 @@ def register():
description=form.description.data, description=form.description.data,
developer=form.developer.data, developer=form.developer.data,
contact_email=form.contact_email.data, contact_email=form.contact_email.data,
logo_url=logo_url or None, logo_url=logo_url or None, # Use the potentially updated logo_url
launch_url=form.launch_url.data, launch_url=form.launch_url.data,
client_id=form.client_id.data, client_id=form.client_id.data,
scopes=scopes, scopes=scopes,
@ -247,7 +266,7 @@ def register():
specialties=','.join(map(str, form.specialties.data)) if form.specialties.data else None, specialties=','.join(map(str, form.specialties.data)) if form.specialties.data else None,
licensing_pricing_id=form.licensing_pricing.data, licensing_pricing_id=form.licensing_pricing.data,
os_support=','.join(map(str, form.os_support.data)) if form.os_support.data else None, os_support=','.join(map(str, form.os_support.data)) if form.os_support.data else None,
app_images=','.join(app_images) if app_images else None, app_images=','.join(app_images) if app_images else None, # Use the potentially updated app_images list
ehr_support=','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None, ehr_support=','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None,
user_id=current_user.id user_id=current_user.id
) )
@ -271,8 +290,9 @@ def edit_app(app_id):
if app.user_id != current_user.id: if app.user_id != current_user.id:
flash('You can only edit your own apps.', 'danger') flash('You can only edit your own apps.', 'danger')
return redirect(url_for('gallery.app_detail', app_id=app_id)) return redirect(url_for('gallery.app_detail', app_id=app_id))
form = SmartAppForm(obj=app) form = SmartAppForm(obj=app)
# Pre-populate multi-select fields correctly on GET request
if not form.is_submitted(): if not form.is_submitted():
if app.categories: if app.categories:
form.categories.data = [int(cid) for cid in app.categories.split(',') if cid] form.categories.data = [int(cid) for cid in app.categories.split(',') if cid]
@ -282,8 +302,14 @@ def edit_app(app_id):
form.os_support.data = [int(oid) for oid in app.os_support.split(',') if oid] form.os_support.data = [int(oid) for oid in app.os_support.split(',') if oid]
if app.ehr_support: if app.ehr_support:
form.ehr_support.data = [int(eid) for eid in app.ehr_support.split(',') if eid] form.ehr_support.data = [int(eid) for eid in app.ehr_support.split(',') if eid]
# Pre-populate the image URL textarea
if app.app_images: if app.app_images:
form.app_image_urls.data = '\n'.join(app.app_images.split(',') if app.app_images else []) # Filter out internal paths if mixed with URLs, show only URLs or internal paths formatted for web
current_images = [img for img in app.app_images.split(',') if img.startswith(('http://', 'https://', '/uploads/'))]
form.app_image_urls.data = '\n'.join(current_images)
else:
form.app_image_urls.data = '' # Ensure it's empty if no images
if form.validate_on_submit(): if form.validate_on_submit():
scopes = form.scopes.data scopes = form.scopes.data
@ -298,6 +324,7 @@ def edit_app(app_id):
# Ensure upload folder exists # Ensure upload folder exists
try: try:
# Use the absolute path inside the container
os.makedirs(UPLOAD_FOLDER, exist_ok=True) os.makedirs(UPLOAD_FOLDER, exist_ok=True)
logger.debug(f"Ensured {UPLOAD_FOLDER} exists") logger.debug(f"Ensured {UPLOAD_FOLDER} exists")
except Exception as e: except Exception as e:
@ -305,6 +332,7 @@ def edit_app(app_id):
flash('Error creating upload directory.', 'danger') flash('Error creating upload directory.', 'danger')
return render_template('edit_app.html', form=form, app=app) return render_template('edit_app.html', form=form, app=app)
# Handle new filter options (same as register)
if form.application_type_new.data: if form.application_type_new.data:
app_type = ApplicationType(name=form.application_type_new.data) app_type = ApplicationType(name=form.application_type_new.data)
db.session.add(app_type) db.session.add(app_type)
@ -314,11 +342,14 @@ def edit_app(app_id):
category = Category(name=form.categories_new.data) category = Category(name=form.categories_new.data)
db.session.add(category) db.session.add(category)
db.session.commit() db.session.commit()
# Ensure data is list before append if it was None initially
if form.categories.data is None: form.categories.data = []
form.categories.data.append(category.id) form.categories.data.append(category.id)
if form.os_support_new.data: if form.os_support_new.data:
os_support = OSSupport(name=form.os_support_new.data) os_support = OSSupport(name=form.os_support_new.data)
db.session.add(os_support) db.session.add(os_support)
db.session.commit() db.session.commit()
if form.os_support.data is None: form.os_support.data = []
form.os_support.data.append(os_support.id) form.os_support.data.append(os_support.id)
if form.fhir_compatibility_new.data: if form.fhir_compatibility_new.data:
fhir = FHIRSupport(name=form.fhir_compatibility_new.data) fhir = FHIRSupport(name=form.fhir_compatibility_new.data)
@ -329,6 +360,7 @@ def edit_app(app_id):
speciality = Speciality(name=form.specialties_new.data) speciality = Speciality(name=form.specialties_new.data)
db.session.add(speciality) db.session.add(speciality)
db.session.commit() db.session.commit()
if form.specialties.data is None: form.specialties.data = []
form.specialties.data.append(speciality.id) form.specialties.data.append(speciality.id)
if form.licensing_pricing_new.data: if form.licensing_pricing_new.data:
pricing = PricingLicense(name=form.licensing_pricing_new.data) pricing = PricingLicense(name=form.licensing_pricing_new.data)
@ -344,60 +376,70 @@ def edit_app(app_id):
ehr = EHRSupport(name=form.ehr_support_new.data) ehr = EHRSupport(name=form.ehr_support_new.data)
db.session.add(ehr) db.session.add(ehr)
db.session.commit() db.session.commit()
if form.ehr_support.data is None: form.ehr_support.data = []
form.ehr_support.data.append(ehr.id) form.ehr_support.data.append(ehr.id)
logo_url = form.logo_url.data
if form.logo_upload.data: # Handle logo update
logo_url = form.logo_url.data # Get URL from form first
if form.logo_upload.data: # If new logo uploaded, it takes precedence
file = form.logo_upload.data file = form.logo_upload.data
if allowed_file(file.filename): if allowed_file(file.filename):
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
# Use absolute path for saving
save_path = os.path.join(UPLOAD_FOLDER, filename) save_path = os.path.join(UPLOAD_FOLDER, filename)
logger.debug(f"Attempting to save logo to {save_path}") logger.debug(f"Attempting to save updated logo to {save_path}")
try: try:
file.save(save_path) file.save(save_path)
if os.path.exists(save_path): if os.path.exists(save_path):
logger.debug(f"Successfully saved logo to {save_path}") logger.debug(f"Successfully saved updated logo to {save_path}")
else: else:
logger.error(f"Failed to save logo to {save_path}") logger.error(f"Failed to save updated logo to {save_path}")
flash('Failed to save logo.', 'danger') flash('Failed to save logo.', 'danger')
return render_template('edit_app.html', form=form, app=app) return render_template('edit_app.html', form=form, app=app)
logo_url = f"/app/uploads/{filename}" # Store URL path for web access
logo_url = f"/uploads/{filename}" # CHANGED
logger.debug(f"Set logo_url to {logo_url}") logger.debug(f"Set logo_url to {logo_url}")
except Exception as e: except Exception as e:
logger.error(f"Error saving logo to {save_path}: {e}") logger.error(f"Error saving updated logo to {save_path}: {e}")
flash('Error saving logo.', 'danger') flash('Error saving logo.', 'danger')
return render_template('edit_app.html', form=form, app=app) return render_template('edit_app.html', form=form, app=app)
elif not logo_url: elif not logo_url: # If no new upload AND URL field is empty, keep existing
logo_url = app.logo_url logo_url = app.logo_url # Keep the old one only if the field is empty
app_images = [] # Handle app images update
if form.app_image_urls.data: # Start with URLs provided in the text area
app_images.extend([url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()]) app_images = [url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()]
if form.app_image_uploads.data:
if form.app_image_uploads.data: # Check if a file was uploaded
file = form.app_image_uploads.data file = form.app_image_uploads.data
if allowed_file(file.filename): if file and allowed_file(file.filename): # Check if file object exists and is allowed
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
# Use absolute path for saving
save_path = os.path.join(UPLOAD_FOLDER, filename) save_path = os.path.join(UPLOAD_FOLDER, filename)
logger.debug(f"Attempting to save app image to {save_path}") logger.debug(f"Attempting to save updated app image to {save_path}")
try: try:
file.save(save_path) file.save(save_path)
if os.path.exists(save_path): if os.path.exists(save_path):
logger.debug(f"Successfully saved app image to {save_path}") logger.debug(f"Successfully saved updated app image to {save_path}")
else: else:
logger.error(f"Failed to save app image to {save_path}") logger.error(f"Failed to save updated app image to {save_path}")
flash('Failed to save app image.', 'danger') flash('Failed to save app image.', 'danger')
return render_template('edit_app.html', form=form, app=app) return render_template('edit_app.html', form=form, app=app)
app_images.append(f"/app/uploads/{filename}") # Add the new image's URL path
app_images.append(f"/uploads/{filename}") # CHANGED
except Exception as e: except Exception as e:
logger.error(f"Error saving app image to {save_path}: {e}") logger.error(f"Error saving updated app image to {save_path}: {e}")
flash('Error saving app image.', 'danger') flash('Error saving app image.', 'danger')
return render_template('edit_app.html', form=form, app=app) return render_template('edit_app.html', form=form, app=app)
# Update app object
app.name = form.name.data app.name = form.name.data
app.description = form.description.data app.description = form.description.data
app.developer = form.developer.data app.developer = form.developer.data
app.contact_email = form.contact_email.data app.contact_email = form.contact_email.data
app.logo_url = logo_url or None app.logo_url = logo_url # Use the final determined logo_url
app.launch_url = form.launch_url.data app.launch_url = form.launch_url.data
app.client_id = form.client_id.data app.client_id = form.client_id.data
app.scopes = scopes app.scopes = scopes
@ -409,7 +451,7 @@ def edit_app(app_id):
app.specialties = ','.join(map(str, form.specialties.data)) if form.specialties.data else None app.specialties = ','.join(map(str, form.specialties.data)) if form.specialties.data else None
app.licensing_pricing_id = form.licensing_pricing.data app.licensing_pricing_id = form.licensing_pricing.data
app.os_support = ','.join(map(str, form.os_support.data)) if form.os_support.data else None app.os_support = ','.join(map(str, form.os_support.data)) if form.os_support.data else None
app.app_images = ','.join(app_images) if app_images else None app.app_images = ','.join(app_images) if app_images else None # Use the final list of image URLs/paths
app.ehr_support = ','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None app.ehr_support = ','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None
try: try:
db.session.commit() db.session.commit()
@ -421,8 +463,11 @@ def edit_app(app_id):
return render_template('edit_app.html', form=form, app=app) return render_template('edit_app.html', form=form, app=app)
flash('App updated successfully!', 'success') flash('App updated successfully!', 'success')
return redirect(url_for('gallery.app_detail', app_id=app_id)) return redirect(url_for('gallery.app_detail', app_id=app_id))
# Render the edit form on GET or if validation fails
return render_template('edit_app.html', form=form, app=app) return render_template('edit_app.html', form=form, app=app)
@gallery_bp.route('/gallery/delete/<int:app_id>', methods=['POST']) @gallery_bp.route('/gallery/delete/<int:app_id>', methods=['POST'])
@login_required @login_required
def delete_app(app_id): def delete_app(app_id):
@ -441,6 +486,7 @@ def my_listings():
apps = SmartApp.query.filter_by(user_id=current_user.id).all() apps = SmartApp.query.filter_by(user_id=current_user.id).all()
return render_template('my_listings.html', apps=apps) return render_template('my_listings.html', apps=apps)
# Keep the test route as is, it uses placeholder URLs directly
@gallery_bp.route('/test/add') @gallery_bp.route('/test/add')
@login_required @login_required
def add_test_app(): def add_test_app():

View File

@ -0,0 +1,58 @@
<!---app/templates/app_detail.html--->
{% extends "base.html" %}
{% block title %}{{ app.name }}{% endblock %}
{% block content %}
<div class="container mt-4">
<h1>{{ app.name }}</h1>
<div class="card shadow-sm">
<div class="card-body">
{% if app.logo_url %}
<img src="{{ app.logo_url }}" alt="{{ app.name }} logo" style="max-height: 200px; object-fit: contain; margin-bottom: 1rem;" onerror="this.style.display='none';">
{% else %}
<img src="https://via.placeholder.com/200?text=No+Logo" alt="No Logo" style="max-height: 200px; object-fit: contain; margin-bottom: 1rem;">
{% endif %}
<p><strong>Description:</strong> {{ app.description }}</p>
<p><strong>Developer:</strong> {{ app.developer }}</p>
<p><strong>Contact Email:</strong> {{ app.contact_email }}</p>
<p><strong>Launch URL:</strong> <a href="{{ app.launch_url }}">{{ app.launch_url }}</a></p>
<p><strong>Client ID:</strong> {{ app.client_id }}</p>
<p><strong>Scopes:</strong> {{ app.scopes }}</p>
{% if app.website %}
<p><strong>Website:</strong> <a href="{{ app.website }}">{{ app.website }}</a></p>
{% endif %}
{% if app_categories %}
<p><strong>Categories:</strong> {{ app_categories | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app_specialties %}
<p><strong>Specialties:</strong> {{ app_specialties | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app_os_supports %}
<p><strong>OS Support:</strong> {{ app_os_supports | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app_ehr_supports %}
<p><strong>EHR Support:</strong> {{ app_ehr_supports | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app.app_images %}
<h5>Additional Images:</h5>
<div class="row">
{% for img_url in app.app_images.split(',') %}
<div class="col-md-4 mb-3">
<img src="{{ img_url }}" alt="App Image" style="max-height: 150px; object-fit: contain;" onerror="this.style.display='none';">
</div>
{% endfor %}
</div>
{% endif %}
{% if current_user.is_authenticated and current_user.id == app.user_id %}
<a href="{{ url_for('gallery.edit_app', app_id=app.id) }}" class="btn btn-primary">Edit App</a>
<form action="{{ url_for('gallery.delete_app', app_id=app.id) }}" method="POST" style="display:inline;">
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this app?');">Delete App</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,183 @@
<!---app/templates/base.html--->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SMARTFLARE - Smart App Gallery - {% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<style>
.navbar-light {
background-color: #ffffff !important;
border-bottom: 1px solid #e0e0e0;
}
.navbar-brand {
font-weight: bold;
color: #007bff !important;
}
.nav-link {
color: #333 !important;
transition: color 0.2s ease;
}
.nav-link:hover {
color: #007bff !important;
}
.nav-link.active {
color: #007bff !important;
font-weight: bold;
border-bottom: 2px solid #007bff;
}
.dropdown-menu {
animation: fadeIn 0.3s ease;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #e9ecef;
}
.dropdown-item.active {
background-color: #007bff;
color: white !important;
}
.app-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-radius: 10px;
overflow: hidden;
}
.app-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
}
.card-img-top {
background-color: #f8f9fa;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.dark-mode .navbar-light {
background-color: #343a40 !important;
border-bottom: 1px solid #495057;
}
.dark-mode .nav-link {
color: #f8f9fa !important;
}
.dark-mode .nav-link:hover, .dark-mode .nav-link.active {
color: #4dabf7 !important;
border-bottom: 2px solid #4dabf7;
}
.dark-mode .dropdown-menu {
background-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dark-mode .dropdown-item {
color: #f8f9fa;
}
.dark-mode .dropdown-item:hover {
background-color: #6c757d;
}
.dark-mode .dropdown-item.active {
background-color: #4dabf7;
}
.dark-mode .app-card {
background-color: #343a40;
color: #f8f9fa;
}
.dark-mode .card-img-top {
background-color: #495057;
}
.dark-mode .text-muted {
color: #adb5bd !important;
}
.dark-mode .navbar-brand {
color: #4dabf7 !important;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('gallery.gallery') }}">SMART App Gallery</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('gallery.gallery') }}">Gallery</a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('gallery.register') }}">Register App</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<span class="nav-link theme-toggle" onclick="toggleTheme()">Toggle Theme</span>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{% if current_user.email and current_user.email | md5 %}
<img src="https://www.gravatar.com/avatar/{{ current_user.email | md5 }}?s=30&d=identicon" class="user-avatar" alt="User Avatar">
{% else %}
<div class="initials-avatar">{{ current_user.username[:2] | upper }}</div>
{% endif %}
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="{{ url_for('gallery.my_listings') }}">My Listings</a></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<script>
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
if (currentTheme === 'dark') {
html.removeAttribute('data-theme');
localStorage.setItem('theme', 'light');
} else {
html.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
}
}
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,214 @@
<!--app/templates/edit_app.html-->
{% extends "base.html" %}
{% block content %}
<h1>Edit App: {{ app.name }}</h1>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
{% for error in form.name.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows=4) }}
{% for error in form.description.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.developer.label(class="form-label") }}
{{ form.developer(class="form-control") }}
{% for error in form.developer.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.contact_email.label(class="form-label") }}
{{ form.contact_email(class="form-control") }}
{% for error in form.contact_email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
{{ form.logo_url(class="form-control") }}
<small class="form-text text-muted">Enter a URL or upload a new logo below. Leave blank to keep current logo.</small>
{% for error in form.logo_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% if app.logo_url %}
<p class="mt-2"><strong>Current Logo:</strong></p>
<img src="{{ app.logo_url }}" alt="Current Logo" style="max-height: 100px;">
{% endif %}
</div>
<div class="mb-3">
{{ form.logo_upload.label(class="form-label") }}
{{ form.logo_upload(class="form-control") }}
{% for error in form.logo_upload.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.launch_url.label(class="form-label") }}
{{ form.launch_url(class="form-control") }}
{% for error in form.launch_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.client_id.label(class="form-label") }}
{{ form.client_id(class="form-control") }}
{% for error in form.client_id.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.scopes.label(class="form-label") }}
{{ form.scopes(class="form-control", rows=3) }}
{% for error in form.scopes.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% if app.scopes and app.scopes.strip() %}
<p class="mt-2"><strong>Current Scopes:</strong>
{% for scope in app.scopes.split(',') %}
{% if scope.strip() %}
<span class="badge bg-primary me-1">{{ scope.strip() }}</span>
{% endif %}
{% endfor %}
</p>
{% else %}
<p class="mt-2 text-muted">No scopes defined.</p>
{% endif %}
</div>
<div class="mb-3">
{{ form.website.label(class="form-label") }}
{{ form.website(class="form-control") }}
{% for error in form.website.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.designed_for.label(class="form-label") }}
{{ form.designed_for(class="form-control") }}
{% for error in form.designed_for.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.designed_for_new.label(class="form-label mt-2") }}
{{ form.designed_for_new(class="form-control") }}
{% for error in form.designed_for_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.application_type.label(class="form-label") }}
{{ form.application_type(class="form-control") }}
{% for error in form.application_type.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.application_type_new.label(class="form-label mt-2") }}
{{ form.application_type_new(class="form-control") }}
{% for error in form.application_type_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.fhir_compatibility.label(class="form-label") }}
{{ form.fhir_compatibility(class="form-control") }}
{% for error in form.fhir_compatibility.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.fhir_compatibility_new.label(class="form-label mt-2") }}
{{ form.fhir_compatibility_new(class="form-control") }}
{% for error in form.fhir_compatibility_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.categories.label(class="form-label") }}
{{ form.categories(class="form-control") }}
{% for error in form.categories.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.categories_new.label(class="form-label mt-2") }}
{{ form.categories_new(class="form-control") }}
{% for error in form.categories_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.specialties.label(class="form-label") }}
{{ form.specialties(class="form-control") }}
{% for error in form.specialties.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.specialties_new.label(class="form-label mt-2") }}
{{ form.specialties_new(class="form-control") }}
{% for error in form.specialties_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.licensing_pricing.label(class="form-label") }}
{{ form.licensing_pricing(class="form-control") }}
{% for error in form.licensing_pricing.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.licensing_pricing_new.label(class="form-label mt-2") }}
{{ form.licensing_pricing_new(class="form-control") }}
{% for error in form.licensing_pricing_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.os_support.label(class="form-label") }}
{{ form.os_support(class="form-control") }}
{% for error in form.os_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.os_support_new.label(class="form-label mt-2") }}
{{ form.os_support_new(class="form-control") }}
{% for error in form.os_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.app_image_urls.label(class="form-label") }}
{{ form.app_image_urls(class="form-control", rows=3) }}
{% for error in form.app_image_urls.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% if app.app_images %}
<p class="mt-2"><strong>Current Images:</strong></p>
{% for image in app.app_images.split(',') %}
<img src="{{ image }}" alt="App Image" style="max-height: 100px; margin: 5px;">
{% endfor %}
{% endif %}
</div>
<div class="mb-3">
{{ form.app_image_uploads.label(class="form-label") }}
{{ form.app_image_uploads(class="form-control") }}
{% for error in form.app_image_uploads.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.ehr_support.label(class="form-label") }}
{{ form.ehr_support(class="form-control") }}
{% for error in form.ehr_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.ehr_support_new.label(class="form-label mt-2") }}
{{ form.ehr_support_new(class="form-control") }}
{% for error in form.ehr_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-secondary">Cancel</a>
</form>
{% endblock %}

View File

@ -0,0 +1,247 @@
<!---app/templates/gallery.html--->
{% extends "base.html" %}
{% block title %}Gallery{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Sticky Top Filter Bar -->
<nav class="navbar navbar-expand-lg navbar-light sticky-top shadow-sm mb-4">
<div class="container-fluid">
<span class="navbar-brand d-flex align-items-center">
<img src="{{ url_for('static', filename='smartflare.png') }}" alt="SMARTFLARE" height="40" class="me-2" onerror="this.style.display='none';">
Filter Apps
</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#filterNavbar" aria-controls="filterNavbar" aria-expanded="false" aria-label="Toggle filters">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="filterNavbar">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {% if not filter_params %}active{% endif %}" href="{{ url_for('gallery.gallery') }}">All Apps</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('application_type') %}active{% endif %}" href="#" id="appTypeDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Application Type
</a>
<ul class="dropdown-menu" aria-labelledby="appTypeDropdown">
{% for app_type in application_types %}
<li>
<a class="dropdown-item {% if app_type.id in filter_params.get('application_type', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', application_type=app_type.id, **(filter_params | rejectattr('application_type'))) }}">{{ app_type.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('category') %}active{% endif %}" href="#" id="categoryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Categories
</a>
<ul class="dropdown-menu" aria-labelledby="categoryDropdown">
{% for category in categories %}
<li>
<a class="dropdown-item {% if category.id in filter_params.get('category', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', category=category.id, **(filter_params | rejectattr('category'))) }}">{{ category.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('os_support') %}active{% endif %}" href="#" id="osSupportDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
OS Support
</a>
<ul class="dropdown-menu" aria-labelledby="osSupportDropdown">
{% for os in os_supports %}
<li>
<a class="dropdown-item {% if os.id in filter_params.get('os_support', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', os_support=os.id, **(filter_params | rejectattr('os_support'))) }}">{{ os.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('fhir_support') %}active{% endif %}" href="#" id="fhirSupportDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
FHIR Support
</a>
<ul class="dropdown-menu" aria-labelledby="fhirSupportDropdown">
{% for fhir in fhir_supports %}
<li>
<a class="dropdown-item {% if fhir.id in filter_params.get('fhir_support', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', fhir_support=fhir.id, **(filter_params | rejectattr('fhir_support'))) }}">{{ fhir.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('speciality') %}active{% endif %}" href="#" id="specialityDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Speciality
</a>
<ul class="dropdown-menu" aria-labelledby="specialityDropdown">
{% for speciality in specialties %}
<li>
<a class="dropdown-item {% if speciality.id in filter_params.get('speciality', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', speciality=speciality.id, **(filter_params | rejectattr('speciality'))) }}">{{ speciality.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('pricing_license') %}active{% endif %}" href="#" id="pricingDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Pricing/License
</a>
<ul class="dropdown-menu" aria-labelledby="pricingDropdown">
{% for pricing in pricing_licenses %}
<li>
<a class="dropdown-item {% if pricing.id in filter_params.get('pricing_license', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', pricing_license=pricing.id, **(filter_params | rejectattr('pricing_license'))) }}">{{ pricing.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('designed_for') %}active{% endif %}" href="#" id="designedForDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Designed For
</a>
<ul class="dropdown-menu" aria-labelledby="designedForDropdown">
{% for designed in designed_fors %}
<li>
<a class="dropdown-item {% if designed.id in filter_params.get('designed_for', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', designed_for=designed.id, **(filter_params | rejectattr('designed_for'))) }}">{{ designed.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('ehr_support') %}active{% endif %}" href="#" id="ehrSupportDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
EHR Support
</a>
<ul class="dropdown-menu" aria-labelledby="ehrSupportDropdown">
{% for ehr in ehr_supports %}
<li>
<a class="dropdown-item {% if ehr.id in filter_params.get('ehr_support', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', ehr_support=ehr.id, **(filter_params | rejectattr('ehr_support'))) }}">{{ ehr.name }}</a>
</li>
{% endfor %}
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- App Cards -->
<div class="container mt-4">
<p>Explore SMART on FHIR apps. Filter above to find the perfect app.</p>
{% if apps %}
<div class="row">
{% for app in apps %}
<div class="col-md-4 mb-3">
<div class="card app-card shadow-sm h-100">
{% if app.logo_url %}
<img src="{{ app.logo_url }}" class="card-img-top" alt="{{ app.name }} logo" style="max-height: 150px; object-fit: contain; padding: 1rem;" onerror="this.style.display='none';">
{% else %}
<img src="https://via.placeholder.com/150?text=No+Logo" class="card-img-top" alt="No Logo" style="max-height: 150px; object-fit: contain; padding: 1rem;">
{% endif %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ app.name }}</h5>
<p class="card-text flex-grow-1">{{ app.description | truncate(100) }}</p>
<p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted">No apps match your filters. Try adjusting the filters above.</p>
{% endif %}
</div>
</div>
<!-- Custom CSS for Pretty UI -->
<style>
.navbar-light {
background-color: #ffffff !important;
border-bottom: 1px solid #e0e0e0;
}
.navbar-brand {
font-weight: bold;
color: #007bff !important;
}
.nav-link {
color: #333 !important;
transition: color 0.2s ease;
}
.nav-link:hover {
color: #007bff !important;
}
.nav-link.active {
color: #007bff !important;
font-weight: bold;
border-bottom: 2px solid #007bff;
}
.dropdown-menu {
animation: fadeIn 0.3s ease;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #e9ecef;
}
.dropdown-item.active {
background-color: #007bff;
color: white !important;
}
.app-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-radius: 10px;
overflow: hidden;
}
.app-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
}
.card-img-top {
background-color: #f8f9fa;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.dark-mode .navbar-light {
background-color: #343a40 !important;
border-bottom: 1px solid #495057;
}
.dark-mode .nav-link {
color: #f8f9fa !important;
}
.dark-mode .nav-link:hover, .dark-mode .nav-link.active {
color: #4dabf7 !important;
border-bottom: 2px solid #4dabf7;
}
.dark-mode .dropdown-menu {
background-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dark-mode .dropdown-item {
color: #f8f9fa;
}
.dark-mode .dropdown-item:hover {
background-color: #6c757d;
}
.dark-mode .dropdown-item.active {
background-color: #4dabf7;
}
.dark-mode .app-card {
background-color: #343a40;
color: #f8f9fa;
}
.dark-mode .card-img-top {
background-color: #495057;
}
.dark-mode .text-muted {
color: #adb5bd !important;
}
.dark-mode .navbar-brand {
color: #4dabf7 !important;
}
</style>
{% endblock %}

View File

@ -0,0 +1,32 @@
<!---app/templates/login.html--->
{% extends "base.html" %}
{% block content %}
<h1>Login</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
<div class="mt-3">
<p>Or login with:</p>
<a href="{{ url_for('auth.login_google') }}" class="btn btn-outline-primary">Google</a>
<a href="{{ url_for('auth.login_github') }}" class="btn btn-outline-dark">GitHub</a>
</div>
<p class="mt-3">Don't have an account? <a href="{{ url_for('auth.register') }}">Register</a></p>
{% endblock %}

View File

@ -0,0 +1,33 @@
<!--app/templates/my_listings.html-->
{% extends "base.html" %}
{% block content %}
<h1>My Listings</h1>
{% if apps %}
<div class="row">
{% for app in apps %}
<div class="col-md-4 mb-3">
<div class="card">
{% if app.logo_url %}
<img src="{{ app.logo_url }}" class="card-img-top" alt="{{ app.name }} logo" style="max-height: 150px; object-fit: contain;">
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ app.name }}</h5>
<p class="card-text">{{ app.description | truncate(100) }}</p>
<p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary">View Details</a>
<a href="{{ url_for('gallery.edit_app', app_id=app.id) }}" class="btn btn-warning btn-sm">Edit</a>
<form action="{{ url_for('gallery.delete_app', app_id=app.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete {{ app.name }}?')">Delete</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p>You haven't registered any apps yet.</p>
<a href="{{ url_for('gallery.register') }}" class="btn btn-primary">Register an App</a>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,193 @@
<!---app/templates/register.html--->
{% extends "base.html" %}
{% block content %}
<h1>Register New App</h1>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
{% for error in form.name.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows=4) }}
{% for error in form.description.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.developer.label(class="form-label") }}
{{ form.developer(class="form-control") }}
{% for error in form.developer.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.contact_email.label(class="form-label") }}
{{ form.contact_email(class="form-control") }}
{% for error in form.contact_email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
{{ form.logo_url(class="form-control") }}
<small class="form-text text-muted">Enter a URL or upload a logo below.</small>
{% for error in form.logo_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.logo_upload.label(class="form-label") }}
{{ form.logo_upload(class="form-control") }}
{% for error in form.logo_upload.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.launch_url.label(class="form-label") }}
{{ form.launch_url(class="form-control") }}
{% for error in form.launch_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.client_id.label(class="form-label") }}
{{ form.client_id(class="form-control") }}
{% for error in form.client_id.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.scopes.label(class="form-label") }}
{{ form.scopes(class="form-control", rows=3) }}
{% for error in form.scopes.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.website.label(class="form-label") }}
{{ form.website(class="form-control") }}
{% for error in form.website.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.designed_for.label(class="form-label") }}
{{ form.designed_for(class="form-control") }}
{% for error in form.designed_for.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.designed_for_new.label(class="form-label mt-2") }}
{{ form.designed_for_new(class="form-control") }}
{% for error in form.designed_for_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.application_type.label(class="form-label") }}
{{ form.application_type(class="form-control") }}
{% for error in form.application_type.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.application_type_new.label(class="form-label mt-2") }}
{{ form.application_type_new(class="form-control") }}
{% for error in form.application_type_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.fhir_compatibility.label(class="form-label") }}
{{ form.fhir_compatibility(class="form-control") }}
{% for error in form.fhir_compatibility.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.fhir_compatibility_new.label(class="form-label mt-2") }}
{{ form.fhir_compatibility_new(class="form-control") }}
{% for error in form.fhir_compatibility_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.categories.label(class="form-label") }}
{{ 을 form.categories(class="form-control") }}
{% for error in form.categories.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.categories_new.label(class="form-label mt-2") }}
{{ form.categories_new(class="form-control") }}
{% for error in form.categories_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.specialties.label(class="form-label") }}
{{ form.specialties(class="form-control") }}
{% for error in form.specialties.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.specialties_new.label(class="form-label mt-2") }}
{{ form.specialties_new(class="form-control") }}
{% for error in form.specialties_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.licensing_pricing.label(class="form-label") }}
{{ form.licensing_pricing(class="form-control") }}
{% for error in form.licensing_pricing.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.licensing_pricing_new.label(class="form-label mt-2") }}
{{ form.licensing_pricing_new(class="form-control") }}
{% for error in form.licensing_pricing_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.os_support.label(class="form-label") }}
{{ form.os_support(class="form-control") }}
{% for error in form.os_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.os_support_new.label(class="form-label mt-2") }}
{{ form.os_support_new(class="form-control") }}
{% for error in form.os_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.app_image_urls.label(class="form-label") }}
{{ form.app_image_urls(class="form-control", rows=3) }}
{% for error in form.app_image_urls.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.app_image_uploads.label(class="form-label") }}
{{ form.app_image_uploads(class="form-control") }}
{% for error in form.app_image_uploads.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.ehr_support.label(class="form-label") }}
{{ form.ehr_support(class="form-control") }}
{% for error in form.ehr_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.ehr_support_new.label(class="form-label mt-2") }}
{{ form.ehr_support_new(class="form-control") }}
{% for error in form.ehr_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-secondary">Cancel</a>
</form>
{% endblock %}

View File

@ -0,0 +1,40 @@
<!--app/templates/register_user.html-->
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
{% for error in form.username.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control") }}
{% for error in form.confirm_password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
<p class="mt-3">Already have an account? <a href="{{ url_for('auth.login') }}">Login</a></p>
{% endblock %}

View File

@ -1,13 +1,11 @@
<!---app/templates/base.html--->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en"> <head>
<head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SMARTFLARE - Smart App Gallery - {% block title %}{% endblock %}</title> <title>SMARTFLARE - Smart App Gallery - {% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<style> <style>
/* Default (Light Theme) Styles */
.navbar-light { .navbar-light {
background-color: #ffffff !important; background-color: #ffffff !important;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
@ -49,6 +47,9 @@
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
/* Add default background/color if needed for light mode explicitly */
background-color: #ffffff;
color: #212529;
} }
.app-card:hover { .app-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
@ -57,47 +58,125 @@
.card-img-top { .card-img-top {
background-color: #f8f9fa; background-color: #f8f9fa;
} }
body {
/* Default body styles for light mode */
background-color: #f8f9fa; /* Example light background */
color: #212529; /* Default text color */
}
.card {
/* Default card styles */
background-color: #ffffff;
}
.text-muted {
/* Ensure default */
color: #6c757d !important;
}
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); } from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.dark-mode .navbar-light {
/* Dark Theme Styles - Triggered by html[data-theme="dark"] */
html[data-theme="dark"] body {
background-color: #212529; /* Dark background for body */
color: #f8f9fa; /* Light text */
}
html[data-theme="dark"] .navbar-light {
background-color: #343a40 !important; background-color: #343a40 !important;
border-bottom: 1px solid #495057; border-bottom: 1px solid #495057;
} }
.dark-mode .nav-link { html[data-theme="dark"] .navbar-brand { /* Override brand color */
color: #4dabf7 !important;
}
html[data-theme="dark"] .nav-link {
color: #f8f9fa !important; color: #f8f9fa !important;
} }
.dark-mode .nav-link:hover, .dark-mode .nav-link.active { html[data-theme="dark"] .nav-link:hover,
html[data-theme="dark"] .nav-link.active {
color: #4dabf7 !important; color: #4dabf7 !important;
border-bottom: 2px solid #4dabf7; border-bottom-color: #4dabf7; /* Ensure border color matches */
} }
.dark-mode .dropdown-menu { html[data-theme="dark"] .dropdown-menu {
background-color: #495057; background-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} }
.dark-mode .dropdown-item { html[data-theme="dark"] .dropdown-item {
color: #f8f9fa; color: #f8f9fa;
} }
.dark-mode .dropdown-item:hover { html[data-theme="dark"] .dropdown-item:hover {
background-color: #6c757d; background-color: #6c757d;
} }
.dark-mode .dropdown-item.active { html[data-theme="dark"] .dropdown-item.active {
background-color: #4dabf7; background-color: #4dabf7;
color: white !important; /* Ensure text stays white */
} }
.dark-mode .app-card { html[data-theme="dark"] .app-card {
background-color: #343a40; background-color: #343a40;
color: #f8f9fa; color: #f8f9fa;
border: 1px solid #495057; /* Optional: add border for contrast */
} }
.dark-mode .card-img-top { html[data-theme="dark"] .card { /* General card styling */
background-color: #343a40;
border: 1px solid #495057;
}
html[data-theme="dark"] .card-img-top {
background-color: #495057; background-color: #495057;
} }
.dark-mode .text-muted { html[data-theme="dark"] .text-muted {
color: #adb5bd !important; color: #adb5bd !important;
} }
.dark-mode .navbar-brand { /* Add any other specific dark mode overrides */
color: #4dabf7 !important; html[data-theme="dark"] h1,
html[data-theme="dark"] h5 {
color: #f8f9fa; /* Ensure headings are light */
} }
html[data-theme="dark"] .form-label {
color: #f8f9fa; /* Form labels */
}
html[data-theme="dark"] .form-control {
background-color: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .form-control::placeholder {
color: #adb5bd;
}
html[data-theme="dark"] .form-select {
background-color: #495057;
color: #f8f9fa;
border-color: #6c757d;
}
html[data-theme="dark"] .alert-danger {
background-color: #dc3545; /* Keep alerts visible */
color: white;
border-color: #dc3545;
}
html[data-theme="dark"] .alert-success {
background-color: #198754;
color: white;
border-color: #198754;
}
/* Ensure avatar styles work in dark mode if needed */
.initials-avatar {
display: inline-block;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #007bff; /* Or another color */
color: white;
text-align: center;
line-height: 30px;
font-weight: bold;
font-size: 14px;
vertical-align: middle;
}
.user-avatar {
border-radius: 50%;
vertical-align: middle;
}
</style> </style>
</head> </head>
<body> <body>
@ -105,22 +184,21 @@
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('gallery.gallery') }}">SMART App Gallery</a> <a class="navbar-brand" href="{{ url_for('gallery.gallery') }}">SMART App Gallery</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span> </button>
</button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto"> <ul class="navbar-nav me-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('gallery.gallery') }}">Gallery</a> <a class="nav-link {% if request.endpoint == 'gallery.gallery' %}active{% endif %}" href="{{ url_for('gallery.gallery') }}">Gallery</a>
</li> </li>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('gallery.register') }}">Register App</a> <a class="nav-link {% if request.endpoint == 'gallery.register' %}active{% endif %}" href="{{ url_for('gallery.register') }}">Register App</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<span class="nav-link theme-toggle" onclick="toggleTheme()">Toggle Theme</span> <button type="button" class="nav-link theme-toggle" onclick="toggleTheme()" style="background:none; border:none; padding:inherit; cursor:pointer; display:inline-block; line-height:inherit;">Toggle Theme</button>
</li> </li>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
@ -128,17 +206,21 @@
{% if current_user.email and current_user.email | md5 %} {% if current_user.email and current_user.email | md5 %}
<img src="https://www.gravatar.com/avatar/{{ current_user.email | md5 }}?s=30&d=identicon" class="user-avatar" alt="User Avatar"> <img src="https://www.gravatar.com/avatar/{{ current_user.email | md5 }}?s=30&d=identicon" class="user-avatar" alt="User Avatar">
{% else %} {% else %}
<div class="initials-avatar">{{ current_user.username[:2] | upper }}</div> <span class="initials-avatar">{{ current_user.username[:2] | upper }}</span>
{% endif %} {% endif %}
</a> {{ current_user.username }} </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="{{ url_for('gallery.my_listings') }}">My Listings</a></li> <li><a class="dropdown-item {% if request.endpoint == 'gallery.my_listings' %}active{% endif %}" href="{{ url_for('gallery.my_listings') }}">My Listings</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li> <li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul> </ul>
</li> </li>
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a> <a class="nav-link {% if request.endpoint == 'auth.login' %}active{% endif %}" href="{{ url_for('auth.login') }}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'auth.register' %}active{% endif %}" href="{{ url_for('auth.register') }}">Register</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -172,11 +254,35 @@
} }
} }
// Apply the saved theme on initial load
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') { if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark'); document.documentElement.setAttribute('data-theme', 'dark');
} }
// Optional: Set to light explicitly if needed, though lack of attribute usually defaults to light
// else {
// document.documentElement.removeAttribute('data-theme');
// }
// Add active class to nav links based on current page
const currentPath = window.location.pathname;
document.querySelectorAll('#navbarNav .nav-link').forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
} else {
link.classList.remove('active'); // Ensure others aren't marked active
}
});
// Handle dropdown item active state similarly if needed
document.querySelectorAll('#userDropdown + .dropdown-menu .dropdown-item').forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}); });
</script> </script>
</body> </body>

View File

@ -154,41 +154,39 @@
<!-- Custom CSS for Pretty UI --> <!-- Custom CSS for Pretty UI -->
<style> <style>
.navbar-light { .navbar-light {
background-color: #ffffff !important; background-color: var(--secondary-bg);
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid var(--text-color);
} }
.navbar-brand { .navbar-light .nav-link {
font-weight: bold; color: var(--text-color);
color: #007bff !important;
}
.nav-link {
color: #333 !important;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.nav-link:hover { .navbar-light .nav-link:hover {
color: #007bff !important; color: var(--link-color);
} }
.nav-link.active { .navbar-light .nav-link.active {
color: #007bff !important; color: var(--link-color);
font-weight: bold; font-weight: bold;
border-bottom: 2px solid #007bff; border-bottom: 2px solid var(--link-color);
} }
.dropdown-menu { .dropdown-menu {
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
border: none; border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px var(--card-hover-shadow);
border-radius: 8px; border-radius: 8px;
background-color: var(--card-bg);
} }
.dropdown-item { .dropdown-item {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
color: var(--text-color);
} }
.dropdown-item:hover { .dropdown-item:hover {
background-color: #e9ecef; background-color: var(--secondary-bg);
} }
.dropdown-item.active { .dropdown-item.active {
background-color: #007bff; background-color: var(--link-color);
color: white !important; color: white;
} }
.app-card { .app-card {
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
@ -197,51 +195,23 @@
} }
.app-card:hover { .app-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important; box-shadow: 0 6px 12px var(--card-hover-shadow);
} }
.card-img-top { .card-img-top {
background-color: #f8f9fa; background-color: var(--secondary-bg);
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); } from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.dark-mode .navbar-light { [data-theme="dark"] .navbar-light {
background-color: #343a40 !important;
border-bottom: 1px solid #495057; border-bottom: 1px solid #495057;
} }
.dark-mode .nav-link { [data-theme="dark"] .navbar-light .nav-link {
color: #f8f9fa !important; color: var(--text-color);
} }
.dark-mode .nav-link:hover, .dark-mode .nav-link.active { [data-theme="dark"] .navbar-light .nav-link:hover, [data-theme="dark"] .navbar-light .nav-link.active {
color: #4dabf7 !important; color: var(--link-color);
border-bottom: 2px solid #4dabf7;
}
.dark-mode .dropdown-menu {
background-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dark-mode .dropdown-item {
color: #f8f9fa;
}
.dark-mode .dropdown-item:hover {
background-color: #6c757d;
}
.dark-mode .dropdown-item.active {
background-color: #4dabf7;
}
.dark-mode .app-card {
background-color: #343a40;
color: #f8f9fa;
}
.dark-mode .card-img-top {
background-color: #495057;
}
.dark-mode .text-muted {
color: #adb5bd !important;
}
.dark-mode .navbar-brand {
color: #4dabf7 !important;
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@ -115,7 +115,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
{{ form.categories.label(class="form-label") }} {{ form.categories.label(class="form-label") }}
{{ form.categories(class="form-control") }} {{ form.categories(class="form-control") }}
{% for error in form.categories.errors %} {% for error in form.categories.errors %}
<span class="text-danger">{{ error }}</span> <span class="text-danger">{{ error }}</span>
{% endfor %} {% endfor %}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB